Von C nach Java, Teil 2: Files, I/O und eine Entwicklungsumgebung

Kann die Verarbeitung von Dateien oder auch das Paketmanagement unter Java ähnlich komfortabel funktionieren, wie unter C? Tatsächlich müssen Umsteiger das Rad nicht ständig neu erfinden, Files und I/O sind mit einer geeigneten Entwicklungsumgebung meist ebenso zugänglich.

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 13 Min.
Von
  • Andreas Nieden
Inhaltsverzeichnis

Kann die Verarbeitung von Dateien oder auch das Paketmanagement unter Java ähnlich komfortabel funktionieren, wie unter C? Tatsächlich müssen Umsteiger das Rad nicht ständig neu erfinden, Files und I/O sind mit einer geeigneten Entwicklungsumgebung meist ebenso zugänglich.

Teil eins der Artikelserie Von C nach Java hat die nötigen Schritte auf Kommandozeilenebene aufgezeigt, jetzt folgt die Beschreibung einer Umgebung, die die Entwicklung, das Debugging und die Pflege der im weiteren Verlauf vorgestellten Programme wesentlich vereinfacht. Es geht um Files und I/O, um die Einführung einer Entwicklungsumgebung und darum, eigene Programme und Packages in selbsterzeugte Java-Archive (JAR) zu verpacken und bereits vorhandene Archive zu nutzen, um nicht das viel beschworene Rad ständig neu erfinden zu müssen.

Files und I/O sind ein wesentlicher Bestandteil jeder Programmiersprache, und gerade Java bietet hier eine ganze Reihe an vorkonfektionierten Klassen, die das Handling von Dateisystemen und der darin gespeicherten Daten vereinfachen. Die meisten Klassen befinden sich im Package java.io. Der erste Teil dieser Reihe stellte bereits die File-Klasse vor. Anders hingegen als man vielleicht vermuten möchte, ist das File kein Objekt, von dem Daten gelesen oder in das Daten geschrieben werden. Im Package java.io ist die File-Klasse ein Objekt, das eine Datei lediglich beschreibt. Es enthält den Namen, die Größe und den Typ und kann jedes Objekt im Dateisystem beschreiben, nicht nur die klassische Datei, sondern eben auch ein Verzeichnis. Und es bietet bei Verzeichnissen zusätzlich die Möglichkeit, auf deren Inhalt, also auf die File-Objekte, die sich in dem Verzeichnis befinden, zuzugreifen (hierfür gibt es zwei Methoden).

Als erstes Beispiel sei ein kurzes Programm gezeigt, dass ausgehend von einem benutzerdefinierten Verzeichnis alle darunterliegenden Dateien und Verzeichnisse ausgibt und zwar einmal in klassischem C-Code (Unix) und dann als Java-Pendant.

Die Aufgabenstellung ist also relativ simpel. Jedoch ist bei C schon der erste Stolperstein in Sicht, denn standardmäßig gibt es keine Funktion, die ein Verzeichnis ausliest. Es muss daher eine Erweiterung her, die das bewerkstelligt. Wenn das Programm unter Unix und Windows laufen soll, ist die Verwendung des GNU C-Compilers die beste Herangehensweise. Unter Windows kommt idealerweise die MinGW-Umgebung zum Einsatz.

Hier gibt es – definiert in dirent.h – das Funktionsset opendir/readdir, mit dessen Hilfe ein Verzeichnis gelesen werden kann. Hier nun der erste Teil des Beispiel-Codes (cfind.c):

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <malloc.h>
#include <errno.h>

typedef struct dirent DIRENT, *PDIRENT;

typedef struct {
int numItems;
PDIRENT pDirEntList;
} DIR_S, *PDIR_S;

PDIR_S getDirectory(char *);

Im Code sind zunächst die obligatorischen Include-Statements angegeben, die dafür sorgen, dass die im weiteren Verlauf des Programms verwendeten Funktionen und Datenstrukturen definiert und sauber erkannt werden. Dann wird ein Datentyp definiert (DIR_S), der später eine komplette Verzeichnisstruktur aufnehmen soll. Die Funktion getDirectory(char *), die im weiteren Verlauf ein Verzeichnis einliest, wird deklariert, anschließend geht es mit dem eigentlichen Programm los.

int main(int argc, char *argv[]) {

if (argc > 1) {
int j;
for (j=1; j<argc; getDirectory(argv[j]));
} else getDirectory(".");
}

Das Hauptprogramm selbst enthält nur den Aufruf von getDirectory() und fällt daher relativ klein aus. Ist kein Argument angegeben, arbeitet das Programm einfach das aktuelle Verzeichnis (".") ab.

Nun zu der Funktion getDirectory(). Ihr übergibt man als Argument den Pfadnamen des einzulesenden Verzeichnisses:

PDIR_S getDirectory(char *sDirName) {
DIR *dir=opendir(sDirName);
PDIRENT pdir;
PDIR_S pDir_s=calloc(1,sizeof(DIR_S));
int j;
    if (!dir) return NULL;
while ((pdir=readdir(dir))!=NULL) {

pDir_s->pDirEntList=realloc(pDir_s->pDirEntList, (pDir_s->numItems+1)*sizeof(DIRENT));
memcpy(&pDir_s->pDirEntList[pDir_s->numItems++],pdir,sizeof(DIRENT));

    }
closedir(dir);
for (j=0;j<pDir_s->numItems;j++) {
struct stat buf;
char sPathName[200];

sprintf(sPathName,"%s/%s", sDirName, pDir_s->pDirEntList[j].d_name);
if (stat(sPathName, &buf)==-1) {
fprintf(stderr,"stat(%s) failed with %d (%s)", sPathName, errno, strerror(errno));
} else if (S_ISDIR(buf.st_mode) && strcmp(pDir_s->pDirEntList[j].d_name,".") && strcmp(pDir_s->pDirEntList[j].d_name,"..")) {

            printf("%s\n", sPathName);
getDirectory(sPathName);
}
}
return pDir_s;
}

Die Funktion gibt einen Zeiger auf die selbst definierte Struktur DIR_S zurück. Das ist noch gar nicht so interessant, denn die oben genannte Kodierung dient nur als Beispiel und lässt die weitere Verwendung dieses Zeigers dem Entwickler offen, der das Programm nach Belieben erweitern kann. Wichtig ist, dass die Funktion beim Erkennen eines Unterverzeichnisses sich selbst wieder aufruft, also rekursiv arbeitet. So lassen sich alle Unterverzeichnisse innerhalb des aufgerufenen Verzeichnisses auf der Konsole ausgeben.

Zum Vergleich jetzt ein Blick auf das entsprechende Pendant in Java, bei dem der Quelltext sowohl kürzer als auch einfacher zu lesen ist, als das im in C programmierten Beispiel der Fall ist:

import java.io.*;

public class CFind extends File {
public CFind(String s) throws Exception {
super(s);
if (!isDirectory()) throw new Exception(s+" is NOT a directory!");
for (File f:listFiles()) {
if (f.isDirectory()) {
System.out.println(f.getAbsolutePath());
new CFind(f.getAbsolutePath());
}
}
}

public static void main(String[] args) {
try {
if (args.length > 0) {
for (String s: args) {
new CFind(s);
}
} else new CFind(".");
} catch (Exception e) {
e.printStackTrace();
}
}
}

Das ist tatsächlich alles. Zu Beginn bindet das Programm hier die Klassen des Packages java.io ein (import). Dann folgt die Klassendefinition der Anwendungsklasse CFind. Mittels extends File wird die Klasse CFind zur Subklasse von java.io.File, also quasi selbst zum File-Objekt. Das bietet sich in diesem Fall an, da das einzige tragende Argument im Programm eben ein Verzeichnis ist.

Die Software startet aus der statischen main-Methode heraus, die man aus der Java VM aufruft. Als Argument kann man auch hier ein oder mehrere Verzeichnisnamen mitgeben. Ist kein Verzeichnisname vorhanden, so startet das Programm ebenfalls mit dem aktuellen Verzeichnis ("."). Da sich das gesamte Geschehen im Konstruktor der Anwendungsklasse abspielt, kann man das CFind()-Objekt einfach mit new CFind() erzeugen, wobei die zurückgegebene Referenz ignoriert wird.

Etwaige Ausnahmen fängt der try...catch-Block auf, und beendet in dem Fall die Programmausführung mit einer Ausgabe des sogenannten StackTrace.

Nun zum Konstruktor. Als erste Anweisung ist super(s) anzugeben, da CFind die java.io.File-Klasse erweitert und innerhalb der ersten Anweisung der Konstruktor eben dieser Superklasse aufgerufen werden muss. Danach hat CFind unter anderem alle Methoden der File-Klasse geerbt und sie stehen uneingeschränkt zur Verfügung. Dann folgt die Sicherheitsabfrage (if (!isDirectory()), ob das File-Objekt auch tatsächlich ein Verzeichnis beschreibt. Falls nicht, beendet eine entsprechende Exception das Geschehen an dieser Stelle.

Die folgende for-Schleife sucht alle Verzeichniseinträge dieses Verzeichnisses, gibt deren Namen aus und erzeugt für sie je ein neues CFind-Objekt. listFiles() ist eine Methode in java.io.File, die innerhalb eines Verzeichnisses alle Datei- und Verzeichniseinträge sucht und sie in einem File-Array zurückgibt. Daher kann man die for-Schleife auch in dieser Syntax und in dieser kurzen Form kodieren.

Wie das Beispiel zeigt, lassen sich komplexere Aufgabenstellungen mit Java kürzer und leichter verständlich umsetzen, als mit klassischem C.

Jetzt ist es Zeit, insbesondere für die Arbeit an größeren Programmen, das Augenmerk auf eine geeignete Entwicklungsumgebung zu richten – schließlich gestaltet sich die Handhabung mehrerer Quelltexte mit Editor, Compiler und so weiter schnell umständlich und unübersichtlich. Das Mittel der Wahl ist derzeit wohl die Eclipse-Entwicklungsumgebung, die als Open Source sowohl für Unix (X11) als auch für Microsoft Windows zur Verfügung steht.

Für die Zwecke dieser Artikelreihe eignen sich aus dem Downloadverzeichnis die Pakete Eclipse Classic und Eclipse IDE for Java Developers.

Nach dem Herunterladen der jeweiligen Archive genügt es, sie in ein beliebiges Verzeichnis zu entpacken und für das jeweilige Executable (unter Windows eclipse.exe) eine Verknüpfung anzulegen. Fertig. Alle weiteren Einstellungen nimmt man im Filesystem vor, es gibt unter Windows keinerlei Registry-Einträge, was auch das Entfernen der Software ungemein erleichtert (man löscht sie einfach nur aus dem Dateisystem).

Nach dem ersten Start legt man zunächst den sogenannten "Workspace" fest (Abbildung 1). Das ist ein Verzeichnis, das alle Projekte aufnimmt, auch alle später erstellte Packages und Klassen. Mit der Aktivierung des Kontrollkästchens "use this as the default ..." gilt die Auswahl als festgeschrieben und es folgt keine weitere Eingabeaufforderung.

Zunächst wird der Workspace für das Projekt definiert (Abb. 1)

Der "Welcome Screen" zeigt sich nur beim ersten Aufruf der Software und bietet die Möglichkeit, verschiedene Tutorials zu durchsuchen. Ein Klick auf das "Workbench"-Symbol auf der rechten Seite startet die eigentliche Workbench (Abbildung 2).

Die eigentliche Workbench für das Beispielprojekt (Abb. 2)

Jetzt soll das erste Java-Projekt erstellt werden. Das initiiert man durch Anwählen des Menüs File->New->Java Project, woraufhin der selbe Dialog erscheint, der schon für das erste Projekt mit dem Projektnamen "JFind" befüllt wurde. Für die ersten Schritte ist es vollkommen ausreichend, lediglich den Projektnamen einzutragen und dann auf Finish zu klicken. Das legt im zu Beginn festgelegten Workspace ein Projekt JFind mit den Default-Einstellungen an.

Auch wenn es nicht unbedingt notwendig ist, empfiehlt es sich stets ein Package zu erzeugen, in dem sich die eigentlichen Anwendungs- und gegebenenfalls weitere Klassen anlegen lassen. Das ist dann auch der nächste Schritt: Links im Workspace sieht man den Package-Explorer, darin das gerade erzeugte Projekt JFind. Im Kontextmenü zu JFind (rechte Maustaste) gibt es die Option, mit New->Package ein Paket zu erstellen (Abbildung 3). Im folgenden Dialog trägt man den Namen des Paketes ein (den Quellordner hat Eclipse aufgrund der Projektvorgaben selbst ausgefüllt).

Über das Kontextmenü kann ein neues Paket erstellt werden (Abb. 3)

Es empfiehlt sich, bei der Vergabe des Namens (zur besseren Strukturierung und Vereinfachung der späteren Co-Existenz weiterer Applikationen) eine URL-Form zu verwenden. Hier kann man nach Belieben eine eigene Konvention vorgeben. Eclipse legt gemäß dieser Konvention die entsprechenden Unterverzeichnisse an. Bei Verwendung von net.nieden.Find ergibt das im Workspace das Verzeichnis ../workspace/JFind/net/nieden/Find, unter dem wiederum die Verzeichnisse /src und /bin zu finden sind. Die Java-Sourcen legt man unter /src und die class-Files unter /bin ab.

Mit dem Klick auf Finish erzeugt man das Package innerhalb des aktuellen Projekts. Nun fehlt nur noch die Anwendungsklasse, die in dem Package angelegt werden muss. Das Vorgehen versteht sich von selbst: Per Rechtsklick wählt man im Kontextmenü zum Package New->Class und schon kann man die Anwendungsklasse CFind erzeugen (Abbildung 4).

Neue Anwendungsklassen werden ebenfalls über das Kontextmenü angelegt (Abb. 4)

Es genügt, hier lediglich den Klassennamen einzugeben und, da die Anwendungsklasse von außen aufgerufen werden soll, die Stub-Methode über das Kontrollkästch public static void main(String[] args) zu aktivieren. Nach dem Klick auf Finish erzeugt Eclipse den Quellcode der Anwendungsklasse und stellt sie im Editor dar.

Nun kann man den im Laufe dieses Artikels entwickelten Code zu CFind per Copy and Paste in den Editor von Eclipse übertragen. Wichtig ist, dass man die erste Zeile package net.nieden.Find wiederherstellt (Abbildung 5).

Nicht vergessen: Für das Beispiel muss die erste Codezeile wieder ergänzt werden! (Abb. 5)

Nach dem Speichern des Quelltextes hat Eclipse die Anwendungsklasse erzeugt und das Programm ist zur Ausführung bereit (Ctrl-F11). Sollen spezielle Parameter über die Kommandozeile mitgegeben werden, muss man das zuvor in Eclipse konfigurieren. Das geht via Project->Properties->Run/Debug Settings.

In der Auswahl für "Launch configuration for JFind" lassen sich mit Klick auf Edit verschiedene Parameter für die Ausführung angeben. Im folgenden Pagecontrol findet man via Arguments->Program arguments das Edit-Feld für die Kommandozeilenargumente (Abbildung 6).

Für die Programmausführung lassen sich auch zusätzliche Kommandozeilenargumente mit angeben (Abb. 6)

Nach längerem Nutzen von Eclipse möchte man auf den grossen Komfort kaum noch verzichten – zu bequem z.B. ist die automatische Vervollständigung beim Editieren des Quelltextes – das Programm schreibt sich fast von selbst. Ob der Entwickler jedoch immer ein Java-Projekt erzeugt, selbst wenn nur ein "kleines" Skript für die Kommandozeile erzeugt werden soll, bleibt dahingestellt. In diesen Fällen genügt meist der klassische Ansatz (Quelltext mit einem Editor bearbeiten, übersetzen und Ausführen des Codes mit javac, bzw. mit java).

Im nächsten Teil wird zu Beginn das Handling von Packages und JAR-Archiven besprochen, bevor es dann weitergeht mit dem Umgang von Daten und Datenstrukturen (z.B. mit ArrayListen, HashSets und Hashtabellen zum schnellen Auffinden von Informationen).

Andreas Nieden
blickt auf fast drei Jahrzehnte an Erfahrung mit Programmiersprachen wie Assembler, C und C++ zurück und ist in den letzten 15 Jahren als Berater im Netzwerk- und Systemmanagement-Umfeld bei Großunternehmen tätig.
(rl)