Inhalt |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Wir haben bisher Applets Klasse für Klasse auf dem Server abgelegt. Alle Klassen, die das Applet benötigt, werden dann vom Server geladen, wenn sie zum ersten Mal verwendet werden. Dies führt zu lästigen Verzögerungen beim Programmablauf. In Java gibt es daher die Möglichkeit, Applets in komprimmierten Archiven zu laden. Solche Archive werden mit einem Zip-Programm gepackt und enthalten alle Klassen, die zum Ablauf nötig sind. Diese Archive werden komplett vor dem Start des Programms übertragen.
Das JDK liefert einen Packer namens jar mit, der Archive mit der Endung jar erzeugt. Die Kommandozeilen-Syntax ist
jar cf archive.jar *.class ...
Allerdings kann man auch andere Komprimierer verwenden, die das zip-Format verwenden. Solche Komprimierer sind unter Umständen sehr viel schneller und komfortabler. Sie haben lediglcih den Nachteil, das im Unterschied zu jar keine Manifest-Datei erzeugt wird, die alle Informationen über die Klassen enthält. Ein Beispiel ist das freie Paket von InfoZip, das die Programme zip und unzip enthält. Unter Unix und unter Windows sieht dann die Komprimierung so aus
zip archive.jar *.class ...
Falls Ihr Applet eigene Pakete verwendet, so sollten diese Klassen in den entsprechenden Unterverzeichnissen ebenfalls mit eingepackt werden.
Das Applet-Tag in der HTML-Datei muss dann folgendermaßen erweitert werden:
<APPLET CODE="Appletname.class" WIDTH="348" HEIGHT="322" ALIGN="BOTTOM" ARCHIVE="archive.jar"> Ihr Browser ist nicht java-fähig! </APPLET>
Dies hat zusätzlich den Vorteil, dass nur eine einzige Datei auf den Server geladen werden muss.
Sie können solche Archive auch für lokale Applikationen verwenden. Dazu setzen Sie den Classpath auf das Archiv und starten das Programm. Am Beispiel von HelloWorld.java:
javac HelloWorld.java jar cf HelloWorld.jar HelloWorld.class set CLASSPATH=HelloWorld.jar del HelloWorld.class java HelloWord
Wie man sieht, bezieht die Applikation nun alle Klassen aus dem Archiv. Diese Methode eignet sich besonders, um Applikationen auf anderen Rechnern zu installieren. Es ist nichts anderes zu tun, als das jar-File auf den Rechner zu kopieren und vor dem Programmstart den CLASSPATH zu setzen.
Übrigens kann man auch mehrere Archive in den CLASSPATH aufnehmen. Dies erleichtert die Installation von Zusätzen, wie etwa den Ressourcen.
Ressourcen sind Dateien und Einstellungen, die von einem Programm zu seiner Konfiguration benötigt werden. Dateien können zum Beispiel Texte, Grafiken und Sound enthalten. Einstellungen sind Paare von Schlüsseln und Zeichenketten und werden auch in Dateien abgelegt. Alle diese Dateien können entweder neben den Klassendateien auf der lokalen Festplatte abgelegt, oder mit in das Archiv gepackt werden.
Man kann natürlich Dateien auf der lokalen Platte oder auf dem Server mit den Methoden laden, die im Kapitel über Kommunikation beschrieben wurden. Dabei müsste man immer unterscheiden, ob das Programm als Applet vom Server oder lokal als Applikation gestartet wurde und entsprechend die Dateien auffinden. Eine Möglichkeit ist, es zunächst mit einer Datei im aktuellen Verzeichnis zu versuchen, und bei einer Exception die Datei im Netz aufzufinden. Einfacher geht es jedoch, wenn man die Datei als Ressource anspricht. Java sucht dann selbständig im Klassenpfad nach der Datei und findet die Datei auch in gepackten Archiven.
Das wesentliche Merkmal einer Ressource ist, dass sie nicht vom Programm geändert werden kann (read only). Es existieren daher keine Schreibzugriffe auf Ressourcen-Dateien.
Als erstes demonstrieren wir das Öffnen einer Textdatei als Ressource. Das folgende Programm zeigt eine Textdatei in einem Dialogfenster an. Dies kann zum Beispiel zur Anzeige von Hilfetexten benutzt werden. Die nächste Version von Java wird ein komplettes Hilfesystem mit Links enthalten. Bis dahin ist dies (neben der Dokumentation in einer HTML-Datei) eine Möglichkeit, Hilfe anzubieten.
Der entscheidende Teil ist, einen Stream zum Lesen von der Datei zu bekommen. Dazu wird die Methode getResourceAsStream(filename) von Class benutzt. Class ist eine allgemeine Klasse, die Klasseninformationen enthält. In diesem Fall benutzen wir eine Instanz von Class, die sich auf die aktuelle Klasse Help bezieht. Diese Instanz bekommen wir mit getClass(), einer Methode, die jedes Objekt hat. Class benutzt eigentlich seinen ClassLoader, der ja die Klassen von der Platte oder aus dem Archiv laden kann. Dieser ClassLoader kann deswegen auch Ressourcen laden.
Man muss noch beachten, dass Ressourcen-Dateien relativ zum aktuellen Verzeichnis, das als Wurzelverzeichnis "/" angegeben wird, bezeichnet werden.
class Help extends Frame implements ActionListener { TextArea V; Button Close; // display the help from the file named subject public Help (String subject) { super("Help"); addWindowListener( // to close properly new WindowAdapter () { public void windowClosing (WindowEvent e) { dispose(); } } ); // TextArea to display the text: V=new TextArea(); V.setEditable(false); // Read the file from the resource: try { BufferedReader in=new BufferedReader(new InputStreamReader( getClass().getResourceAsStream("/"+subject))); // read line after line: while (true) { String s=in.readLine(); if (s==null) break; V.append(s+"\n"); } in.close(); } catch (Exception e) { V.setText("Could not find the help file!"); } // Layout: setLayout(new BorderLayout()); setSize(500,400); add("Center",V); Panel p=new Panel(); p.add(Close=new Button("Close")); Close.addActionListener(this); add("South",p); setVisible(true); } public void actionPerformed (ActionEvent e) // close dialog { if (e.getSource()==Close) { setVisible(false); dispose(); } } }
Dem Konstruktor von Help wird der Dateiname (z.B. "help.txt") als Parameter übergeben. Zu beachten ist noch, dass als Stream auf die Ressource null zurückgegeben wird, wenn die Ressource nicht existiert.
Auf die gleiche Art und Weise kann man auch Images und Sounds laden. Wir haben schon Bilder aus einer Datei geladen. Applets tun dies einfach mit der getImage-Methode. Falls die Datei als Ressource angesprochen werden soll, kann man anstatt des Dateinamens einfach eine Ressourcendatei an getImage übergeben. Als Beispiel erzeugen wir einen Frame, der ein eigenes Icon (unter Windows als kleines Bild links oben im Fenster) anzeigt. Dieses Icon ist eine kleine Datei mit folgendem Inhalt:
Es handelt sich um eine 32x32-Pixel große GIF-Datei. Der Code für das Warten auf die Beendigung des Ladens wurde schon im Kapitel über Animation besprochen.
Man beachte außerdem, dass das Programm auch funktionieren soll, wenn die Ressource nicht vorhanden ist.
class AppleFrame extends Frame { public AppleFrame () { super("Appleset"); setSize(300,300); seticon("iapple.gif"); setVisible(true); } // load image and set icon: public void seticon (String file) { try { Image i=Toolkit.getDefaultToolkit().getImage( getClass().getResource("/"+file)); MediaTracker mt=new MediaTracker(this); mt.addImage(i,0); mt.waitForAll(); setIconImage(i); } catch (Exception e) {} } }
Leider enthält der Code für das Setzen von Icons in jeder Java-Version vor 1.1.6 einen Fehler, der unter Windows die Anwendung verhinderte. Außerdem soll die Java 1.1 Version von Netscape einen Fehler enthalten, der das Laden von Images als Ressource verhindert. In diesem Fall kann man das Image aus einer Datei laden. Im Falle einer lokalen Datei verwendet man dazu
getToolkit().getImage(file);
oder (wenn man die Datei relativ zum Verzeichnis haben will, in dem sich die Hauptklasse befindet)
getToolkit().getImage(System.getProperty("user.dir")+ System.getProperty("file.separator")+file);
Dabei ist file der Name der Datei. Die beiden Systemeigenschaften vorher werden in Kürze erklärt. Sie besorgen sich das aktuelle Directory. Vom Netz (etwa aus einem Applet) lädt man das Image mit
getToolkit().getImage(new URL(getCodeBase(),file))
Allerdings kann man mit Netscape auch den folgenden Code verwenden.
try { InputStream in=getClass().getResourceAsStream("/"+Filename); byte b[]=new byte[in.available()]; in.read(b); I=Toolkit.getDefaultToolkit().createImage(b); } catch (Exception e) { System.out.println(e); }
Dieser Code öffnet einen Eingabestrom und liest die Bytes der Datei in ein Byte-Array ein. Von dort wird erst das Image erzeugt.
Bezüglich Sound-Dateien gilt, dass die aktuelle Java-Version nur eine undokumentierte Sound-Schnittstelle für Applikationen hat. Applets haben allerdings die Methode play, die eine Audio-Datei auf dem Server abspielen kann. Verwendet wird hier das au-Format von Sun. Erst zukünftige Versionen werden komfortablere Routinen zum Abspielen (oder auch Aufnehmen) von Sounds enthalten. Als Ressourcen lassen sich Sounds so nicht angeben.
Hier soll nun die undokumentierte Schnittstelle zum Abspielen von Sounds verwendet werden. Die Sounds werden in Ressourcen-Dateien gespeichert.
public void play (String filename) { try { AudioStream AS=new AudioStream( getClass().getResourceAsStream("/"+filename)); AudioData Data=AS.getData(); AudioDataStream A=new AudioDataStream(Data); A.reset(); AudioPlayer.player.start(A); } catch (Exception e) {} }
Zu beachten ist, dass das Paket sun.audio.* importiert werden muss.
Programmeinstellungen lassen sich am leichtesten als Zeichenketten in Dateien speichern. Solche Einstellungen betreffen etwa Flags, die den Programmablauf steuern. Es wird hier zunächst angenommen, dass die Einstellungen vom Benutzer geändert werden können. Diese Dateien werden daher nicht als Ressourcen behandelt.
Will man Einstellungen einlesen, die nicht änderbare Ressourcen sind, so kann man natürlich auf diese Dateien wie oben beschrieben zugreifen. Bei Menütexten oder auszugebenden Meldungen sollte man allerdings erwägen, die Internationalisierungstools zu verwenden, die weiter unten beschrieben werden.
Zunächst werden Einstellungen in Instanzen von Properties gespeichert. Dieser Datentyp ist ein Kind von Hashtable. Es existieren allerdings Funktionen zum Lesen und Schreiben auf Streams. Angenommen, es existiert die Datei test.properties mit den Einstellungen
Test.file=test.txt Test.read=true
Dann stehen links vom Gleichheitszeichen die Schlüssel und rechts die Werte der Ressourcen. Z.B. hat "Test.file" den Wert "test.txt". Das folgende Programm liest nun diese Datei ein, ergänzt eine neue Einstellung und speichert sie wieder ab.
import java.util.*; import java.io.*; public class Test { public static void main (String args[]) { try { Properties P=new Properties(); // Lies Properties ein: FileInputStream in=new FileInputStream("test.properties"); P.load(in); in.close(); // Drucke einige Properties: System.out.println(P.getProperty("Test.file")); System.out.println(P.getProperty("Test.files")); System.out.println(P.getProperty("Test.files","no value")); System.out.println(toBoolean(P.getProperty("Test.load","false"))); // Fuege eine Property hinzu P.put("Test.save","false"); // Speichere die Properties wieder ab: FileOutputStream out=new FileOutputStream("test.properties"); P.save(out,"Test Properties"); out.close(); } catch (Exception e) {} } public static boolean toBoolean (String s) // Intepretiere true oder false { return s.equals("true"); } }
Die Funktion toBoolean intepretiert nur den String true oder false. Bei getProperty kann man einen Default-Wert übergeben. Dieser Wert wird genommen, wenn die Property nicht definiert ist. Ansonsten würde in diesem Fall null übergeben. Die Ausgabe des Programms ist folgendermaßen:
test.txt null no value true
Die Datei test.properties wird dabei verändert. Übrigens wird dabei der Java-interne Zeilenumbruch verwendet. Außerdem enthält die Datei nun eine Header mit dem übergebenen String und dem Datum.
#Test Properties #Mon ... Test.load=true Test.file=test.txt Test.save=false
Die Datei lässt sich natürlich nun als Property-Datei wiederverwenden.
Einige Properties werden vom System vorgegeben und sind mit Hilfe der System-Klasse erreichbar. Das folgende Programm listet alle diese System-Properties auf. Es zeigt außerdem, wie man eine Enumeration aller Keys in einer HashTable bekommt.
import java.util.*; public class Test { public static void main (String args[]) { Properties P=System.getProperties(); Enumeration e=P.keys(); while (e.hasMoreElements()) { String key=(String)e.nextElement(); String value=(String)P.get(key); System.out.println(key+"="+value); } } }
Die Ausgabe dieses Programm auf meinem System ist folgende:
user.language=de java.home=c:\jdk1.1.6\bin\.. java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport.cgi awt.toolkit=sun.awt.windows.WToolkit file.encoding.pkg=sun.io java.version=1.1.6 file.separator=\ line.separator= user.region=DE file.encoding=8859_1 java.compiler=symcjit java.vendor=Sun Microsystems Inc. user.timezone=ECT user.name=Rene os.arch=x86 os.name=Windows NT java.vendor.url=http://www.sun.com/ user.dir=D:\java\rene\kurs java.class.path=... java.class.version=45.3 os.version=4.0 path.separator=; user.home=C:\
Einige dieser Properties sind sehr gut für Applikationen verwendbar. Z.B. lässt sich die verwendete Java-Version abfragen. Weiter oben haben wir schon file.separator und user.dir verwendet.
Java-Programme sollen leicht auf jedem Rechner der Welt einsetzbar sein. Bei der Ein- und Ausgabe von Texten haben wir schon die Verwendung von Datei-Encoding kennengelernt. Nun gibt es aber weit mehr nationale Einstellungen, wie Zahlenformate, Datum usw. Außerdem soll ein Programm Menütexte und Fehlermeldungen internationalisieren.
Zu diesem Zweck verwendet Java die Klasse Locale. Diese Klasse hat statische Methoden zum Setzen und Abfragen der eingestellten Sprache. Solch eine Spracheinstellung wird mit einem String aus zwei kleinen Buchstaben, sowie wahlweise einem Unterstrich und einer weiteren Beschreibung, angegeben. Ein Beispiel ist "de" oder "de_DE" für Deutsch oder "en_US" und "en_GB" für englische Sprachräume.
Der eingestellte Sprachraum ist nun dafür verantwortlich, wie etwa Zahlen formatiert werden. Die Klasse, die Zahlen formatiert, ist NumberFormat. Man kann nicht direkt eine Instanz dieser Klasse anlegen, sondern benötigt dazu deren statische Methode getInstance() (und Vewandte). Mit Hilfe einer Instanz (die auch mit einem anderen Sprachraum initialisiert werden kann), kann man Zahlen lokal formatieren. Als Beispiel vergleichen wir englische und deutsche Ausgaben.
import java.util.*; import java.text.*; public class Test { public static void main (String args[]) { // lokale Ausgabe (hier deutsch) NumberFormat number=NumberFormat.getInstance(); System.out.println(number.format(Math.PI)); number.setMaximumFractionDigits(16); System.out.println(number.format(Math.PI)); number.setGroupingUsed(true); // Gruppiert Dreiergruppen von Zahlen System.out.println(number.format(1000000000.01)); // englische Ausgabe (US Format): number=NumberFormat.getInstance(new Locale("en","US")); System.out.println(number.format(Math.PI)); number.setMaximumFractionDigits(16); System.out.println(number.format(Math.PI)); number.setGroupingUsed(true); System.out.println(number.format(1000000000.01)); } }
Die Ausgaben unterscheiden sich durch die Verwendung von Komma und Punkt. Allerdings kann auch ein lokaler Computer anders eingestellt worden sein.
3,142 3,141592653589793 1.000.000.000,01 3.142 3.141592653589793 1,000,000,000.01
Offenbar ist der Sprachraum auch wichtig für Zahleingaben. Dazu fügen wir folgende Zeilen hinzu
try { double x=number.parse(number.format(1000000000.01)).doubleValue(); System.out.println(x); } catch (ParseException e) {}
Die Ausgabe zeigt, dass ein Formatierer seine eigenen Ausgabe lesen kann (auch wenn sie z.B. Gruppierungen enthält). Zurückgegeben wird ein Objekt Number, dessen Methoden man zur Umformatierung in Zahlen verwenden kann.
Alternativ kann man
Double.valueOf( number.parse(number.format(1000000000.01)).toString()).doubleValue();
verwenden. Dies macht zunächst aus Number einen String, den es mit Double.valueOf in ein Double umwandelt. Double ist die Wrapper-Klasse für double. Solche Wrapperklassen haben wir schon verwendet, um Zahlen in Vektoren zu speichern. Anschließend wird der Wert aus diesem Double ausgelesen.
Es existieren auch Formatierer für Währungseinheiten, Prozentwerte und Daten. Wir demonstrieren alle drei in einem Programm.
import java.util.*; import java.text.*; public class Test { public static void main (String args[]) { NumberFormat price=NumberFormat.getCurrencyInstance(); NumberFormat percent=NumberFormat.getPercentInstance(); System.out.println(price.format(12.31)); percent.setMinimumFractionDigits(2); System.out.println(percent.format(0.1231)); DateFormat date=DateFormat.getDateInstance(DateFormat.SHORT); System.out.println(date.format(new Date())); date=DateFormat.getDateTimeInstance( DateFormat.LONG,DateFormat.LONG); System.out.println(date.format(new Date())); } }
Die Ausgabe ist
12,31 DM 12,31% 29.06.98 29. Juni 1998 11:46:10 GMT+02:00
Die Klasse Date behandelt ein Datum. Sie ist insbesondere von der aktuellen Zeitzone betroffen.
Ein weiteres Gebiet für Internationalisierung ist die Ausgabe von Programmen in internationaler Form. Als Beispiel zeigen wir hier ein japanisches Java-Programm.
Wie man sieht, sind alle Menütexte japanisch. Das gleiche sollte für Fehlermeldungen und Hilfetexte gelten.
Um ein solches Programm zu erzeugen, sollte man die verwendeten Strings aus einer Ressource auslesen. Dazu verwendet man Property-Dateien, deren Namen aus einem String mit angehängtem Sprachraum und der Dateierweiterung .properties besteht. Ein Beispiel ist TestResource_de.properties. Java wählt diese Ressourcen-Datei automatisch aus, wenn man sich im deutschen Sprachraum befindet (oder Local.setDefault entsprechend eingestellt hat) und Ressourcen-Bündel verwendet. Dies geschieht am zweckmäßigsten in einer statischen Variable der Hauptklasse.
Beispiel:
public class Zirkel extends Applet { static ResourceBundle B; // verwendetes Bundle static String name (String tag, String def) // Finde Resourcenstring { String s; try { s=B.getString(tag); } catch (Exception e) { s=def; } return s; } public static void initBundle () // Initialisiere B { try { B=ResourceBundle.getBundle("zirkel"); } catch (Exception e) { B=null; } } public void init () // Zirkel wird als Applet aufgerufen { String s=getParameter("Language"); if (s!=null) Locale.setDefault(new Locale(s,"")); initBundle(); ... } public static void main (String args[]) // Zirkel wird als Applikation aufgerufen { if (args.length>=2) { if (args[0].startsWith("-l")) { Locale.setDefault(new Locale(args[1],"")); } } initBundle(); ... } }
Nun kann jede Klasse mittels
Zirkel.name(key,defaultvalue)
den Wert der Ressource key auslesen. Das Programm kann übrigens als Applet oder als Applikation gestartet werden. Wir stellen uns vor, dass beide ein Fenster erzeugen, in dem das eigentliche Programm läuft. In Falle eines Applets lässt sich der Sprachraum mit einem Parameter einstellen. Im Falle des Aufrufs über main, lässt sich der Sprachraum über zwei Kommandozeilenparameter der Form -l lang setzen. Der String für den Sprachunterraum wird hier nicht verwendet.
Unter Serialization versteht man die Fähigkeit von Objekten, sich in Strings umzuwandeln und umgekehrt. Diese Fähigkeit ist wichtig, wenn man bei so genannten verteilten Anwendungen Objekte über das Netz weiterreichen will, beispielsweise dann, wenn man Methoden von Objekten aufruft, die sich auf einem anderen Rechner befinden und die Objekte als Parameter verwenden.
In dieses Kapitel passt Serialization deswegen hinein, weil sie ein einfach zu handhabendes Datenformat ergibt.
Ein Objekt muss serialisierbar sein, um in einen String umgewandelt zu werden. Ein Objekt kann einfach dadurch, dass es das Interface Serializable implementiert, anzeigen, dass es serialisierbar ist. Es liegt allerdings in der Verantwortung des Objektes sicherzustellen, dass auch alle seine Variablen in Strings umgewandelt werden können. Für elementare Datentypen ist dies sicher der Fall. Außerdem ist es der Fall für Variablen, die sich auf serialisierbare Objekte oder Arrays von solchen Objekten beziehen. Einige vordefinierte Datentypen sind als serialisierbar deklariert.
Als Beispiel erzeugen wir ein Objekt vom Typ Complex, speichern es auf eine Datei und laden das Objekt wieder von der Datei.
import java.util.*; import java.io.*; class Complex implements Serializable // Eine einfache Klasse fuer komplexe Zahlen. { double Re,Im; public Complex (double re, double im) { Re=re; Im=im; } public double re () { return Re; } public double im () { return Im; } public String toString () // zum Ausdrucken { return Re+"+i*"+Im; } } public class Test { public static void main (String args[]) { Complex z=new Complex(2,1.1); System.out.println(z); try { // Object ausgeben: ObjectOutputStream out= new ObjectOutputStream( new FileOutputStream("test.dat")); out.writeObject(z); out.close(); // Object einlesen: ObjectInputStream in= new ObjectInputStream( new FileInputStream("test.dat")); Object o=in.readObject(); z=(Complex)o; in.close(); } catch (Exception e) { System.err.println(e); } System.out.println(z); } }
Dies funktioniert auch, wenn man mehrere Objekte auf dieselbe Datei speichert. Man muss nur wissen, wie viele und von welchem Typ diese Objekte sind. Und es funktioniert für Arrays.
public class Test { public static void main (String args[]) { // Erzeuge Array: Complex z[]={new Complex(2,1.1),new Complex(1,1)}; try { // Schreibe Array auf Datei: ObjectOutputStream out= new ObjectOutputStream(new FileOutputStream("test.dat")); out.writeObject(z); out.close(); // Lies Array ein: ObjectInputStream in= new ObjectInputStream(new FileInputStream("test.dat")); z=(Complex [])in.readObject(); in.close(); } catch (Exception e) { System.err.println(e); } // Drucke Array aus: for (int i=0; i<z.length; i++) System.out.println(z[i]); } }
Falls das Objekt Referenzen auf serialisierbare Objekte enthält, werden diese automatisch mitgeschrieben. Dadurch lässt sich eine komplexere Datenstruktur, wie etwa eine Liste, abspeichern.
Die Datei kann allerdings nur gelesen werden, wenn die zugehörigen Klassen bekannt sind und dieselbe Version haben. Bisweilen ändert sich jedoch an der Datenstruktur nichts, obwohl die Klasse neu übersetzt wurde. Man kann daher jeder serialisierbaren Klasse eine eigene Versionsnummer geben. Dies geschieht z.B. bei Complex folgendermaßen:
class Complex implements Serializable { static final long serialVersionUID=100; ... }
Nun werden alle Dateien, die dieselbe Versionsnummer enthalten, als kompatibel angesehen.
Eine noch bessere Kontrolle erhält man durch eigene Schreibroutinen. Auf dieses Thema wollen wir hier nicht eingehen.