java.net.ConnectException? Was tun?

Falls der Computer keinen direkten Internetzugriff hat, kommt es zu einem Timeout. Der übliche Fehler ist eine „java.net.ConnectException: Operation timed out“. Hier gilt zu prüfen, woher der Fehler kommt. Eine Fehlerquelle sind Proxies, die zwischen dem eigenen Rechner und dem Internet hängen. Proxy-Einstellungen können in Java gesetzt werden, wie im im Kapitel über Proxies in der Insel beschrieben. Eine gute Idee ist, mithilfe des Kommandozeilenprogramms telnet[1] die Erreichbarkeit eines Servers zu überprüfen. Es kann auch sein, dass die Verbindung grundsätzlich besteht, sie jedoch langsam ist, und Java aus Ungeduld aufgibt. Die Lösung ist dann, den Timeout hochzusetzen, etwa bei dem URLConnection-Objekt über setConnectTimeout(millis).


[1] Muss in einem aktuellen Windows erst aktiviert werden.

Abbruch-Signale

Das Betriebssystem sendet beim Abbruch eines Programms Signale. Für Windows/Unix-Systeme sind das zum Beispiel INT (Abbruch z. B. mit Strg-C), TERM (Aufforderung zur Terminierung), BREAK (Break-Anforderung vom Termin), usw.

Java kann diese unterschiedlichen Feinheiten des Programmabbruchs nicht auswerten, jedenfalls nicht mit einer erlaubten API. Es gibt aber zwei Klassen sun.misc.Signal und sun.misc.SignalHandler, die verwendet werden können, wenn Entwickler sich der Konsequenzen bewusst sind, dass diese API in Zukunft verschwinden könnte. Die Nutzung sieht so aus:

class MySignalHandler implements SignalHandler {

public void handle( Signal signal ) { … }

}

String signalName = "TERM";

handler = Signal.handle( new Signal( signalName ), new MySignalHandler() );

Weitere Informationen auch zu den unterschiedlichen Signaltypen liefern http://stackoverflow.com/questions/5023520/sending-signals-to-a-running-jvm und die referenzierten Unterseiten.

Gruppen in regulären Ausdrücken

In regulären Ausdrücken lassen sich Teilausdrücke in runde Klammen setzen und diese bilden dann Gruppen; im Englischen heißen sie capturing groups. Gruppen haben zwei Vorteile:

· An die Gruppe gesetzte Operationen wirken für die Gruppe. Beispiel: Der Ausdruck (\d\d)+ steht für gerade Anzahl von Ziffern.

· Auf eine Gruppe lässt sich gezielt zurückgreifen.

Beispiel 1

Ein String enthält zwei Zahlen, die mit einem Doppelpunkt getrennt sind. Wie interessieren uns für die erste und zweite Zahl:

Matcher matcher = Pattern.compile( "(\\d+):(\\d+)").matcher( "1:123" );

if ( matcher.find() )

System.out.println( matcher.group(1) + "," + matcher.group(2) ); // 1,123

Der Trick ist also, bei group(int) einen Index anzugeben; der beginnt bei 1.

Beispiel 2

Ein String aus einer Datei enthält einen Block in geschweiften Klammen. Wir interessieren uns für den Block.

String input = "line1\n{line2\nline3}\nline4";

Matcher matcher = Pattern.compile( "\\{(.*)\\}", Pattern.MULTILINE | Pattern.DOTALL ).matcher( input );

if ( matcher.find() )

  System.out.println( matcher.group( 1 ) ); // line1, dann Umbruch, line2

Bei der Angabe ohne Index, also group(), ist die Rückgabe alles was find() gefunden hat. Die Fundstellen sind natürlich Teilstrings der gesamten Gruppe.

Inselupdate: Property-Dateien mit java.util.Properties lesen und schreiben

Dateien, die Schlüssel-Werte-Paare als String repräsentieren und die Schlüssel und Wert durch ein Gleichheitszeichen trennen, nennen sich Property-Dateien. Sie kommen zur Programmkonfiguration häufig vor, und Java bietet mit der Klasse Properties die Möglichkeit, die Property-Dateien einzulesen und zu schreiben.

store() und load()-Methoden vom Properties Objekt

Die Methode store(…) dient zum Speichern der Zustände und load(…) zum Initialisieren eines Properties-Objekts aus einem Datenstrom. Die Schlüssel und Werte trennt ein Gleichheitszeichen. Die Lade-/Speicher-Methoden sind:

class java.util.Properties
extends Hashtable<Object,Object>

§ void store(OutputStream out, String comments)

§ void store(Writer writer, String comments)
Schreibt die Properties-Liste in des Ausgabestroms. Am Kopf der Datei wird eine Kennung geschrieben, die im zweiten Argument steht. Die Kennung darf null sein.

§ void load(InputStream inStream)

§ void load(Reader reader) throws IOException
Liest eine Properties-Liste aus einem Eingabestrom.

Ist der Typ ein Binärstrom also OutputStream/InputStream, so behandeln die Methoden die Zeichen in der ISO 8859-1 Kodierung. Reader/Writer erlauben eine freie Kodierung. Eine ähnliche Methode list(…) ist nur für Testausgaben gedacht ist, sie sollte nicht mit store(…) verwechselt werden.

Das folgende Beispiel initialisiert ein Properties-Objekt mit den Systemeigenschaften und fügt dann einen Wert hinzu. Anschließend macht store(…) die Daten persistent, load(…) liest sie wieder, und list(…) gibt die Eigenschaften auf dem Bildschirm aus:

Path path = Paths.get( "properties.txt" );

try ( Writer writer = Files.newBufferedWriter( path, StandardCharsets.UTF_8 ) ) {

Properties prop1 = new Properties( System.getProperties() );

prop1.setProperty( "MeinNameIst", "Forrest Gump" );

prop1.store( writer, "Eine Insel mit zwei Bergen" );

try ( Reader reader = Files.newBufferedReader( path, StandardCharsets.UTF_8 ) ) {

Properties prop2 = new Properties();

prop2.load( reader );

prop2.list( System.out );

}

}

catch ( IOException e ) {

e.printStackTrace();

}

Besonderheiten des Formats

Beginnt eine Zeile mit einem „#“ oder „!“ gilt sie als Kommentar und wird überlesen. Da der Schlüssel selbst aus einem Gleichheitszeichen bestehen kann, steht in dem Fall ein „\“ voran, folglich liefert Properties p = new Properties(); p.setProperty("=", "="); p.store(System.out, null); neben dem Kommentar die Zeile „\==\=“. Beim Einlesen berücksichtigen die Lesemethoden auch Zeilenumbrüche: eine Zeile darf mit \ enden und dann führt die folgende Zeile die vorangehende fort. Die Property „cars“ ist also “Honda, Mazda, BMW”, wenn steht:

cars = \

Honda, Mazda, \

BMW

Mit der internen Compiler-API auf den AST einer Klasse zugreifen

Der Zugriff zum Java-Compiler ist über die Java-Compiler-API standardisiert, jedoch sind alle Interna, wie die tatsächliche Repräsentation des Programmcodes verborgen. Die Compiler-API abstrahiert alles über Schnittstellen, und so kommen Entwickler nur mit JavaCompiler, StandardJavaFileManager und CompilationTask in Kontakt – alles Schnittstellen aus dem Paket javax.tools. Um etwas tiefer einzusteigen, lässt sich zum einem Trick greifen: Klassen implementieren Schnittstellen und wenn ein Programm den Schnittstellentyp auf den konkreten Klassentyp anpasst, dann stehen in der Regel mehr Methoden zur Verfügung. So lässt sich der CompilationTask auf eine com.sun.tools.javac.api.JavacTaskImpl casten und dann steht eine parse()-Methode für Verfügung. Die parse()-Methode liefert als Rückgabe eine Aufzählung von CompilationUnitTree. Um diesen Baum nun abzulaufen, lässt sich das Besuchermuster einsetzen. CompilationUnitTree bietet eine accept(…)-Methode; der übergeben wir einen TreeScanner. Die accept(…)-Methode ruft dann beim Ablaufen jedes Knotens unseren Besucher auf.

package com.tutego.tools.javac;

import java.io.*;
import java.net.*;
import javax.tools.*;
import javax.tools.JavaCompiler.CompilationTask;
import com.sun.source.tree.*;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.api.JavacTaskImpl;

public class PrintAllMethodNames {

  final static TreeScanner<?, ?> methodPrintingTreeVisitor = new TreeScanner<Void, Void>() {
    @Override public Void visitCompilationUnit( CompilationUnitTree unit, Void arg ) {
      System.out.println( "Paket: " + unit.getPackageName() );
      return super.visitCompilationUnit( unit, arg );
    };
    @Override public Void visitClass( ClassTree classTree, Void arg ) {
      System.out.println( "Klasse: " + classTree.getSimpleName() );
      return super.visitClass( classTree, arg );
    }
    @Override public Void visitMethod( MethodTree methodTree, Void arg ) {
      System.out.println( "Methode: " + methodTree.getName() );
      return super.visitMethod( methodTree, arg );
    }
  };

  public static void main( String[] args ) throws IOException, URISyntaxException {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null );
    URI filename = PrintAllMethodNames.class.getResource( "PrintAllMethodNames.java" ).toURI();
    Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects( new File( filename ) );
    CompilationTask task = compiler.getTask( null, null, null, null, null, fileObjects );

    JavacTaskImpl javacTask = (JavacTaskImpl) task;

    for ( CompilationUnitTree tree : javacTask.parse() )
      tree.accept( methodPrintingTreeVisitor, null );
  }
}

Ein TreeScanner hat viele Methoden, wir interessieren uns nur für den Start einer Compilationseinheit für den Paketnamen, für alle Klassen und Methoden. Wir könnten uns aber auch über alle Annotationen oder do-while-Schleifen informieren lassen. Die Ausgabe ist:

Paket: com.tutego.tools.javac

Klasse: PrintAllMethodNames

Klasse:

Methode: visitCompilationUnit

Methode: visitClass

Methode: visitMethod

Methode: main

Die zweite Angabe für den Klassennamen ist leer, da die anonyme Klasse eben keinen Namen hat.

Inselupdate: Konstruktoren der Formatter Klasse

Die String.format(…)-Methode und prinf(…)-Methoden der Ein-/Ausgabeklassen übernehmen die Aufbereitung nicht selbst, sondern delegieren sie an die Klasse java.util.Formatter. Das ist auch der Grund, warum die Dokumentation für die Formatspezifizierer nicht etwa an String.format(…) hängt, sondern an Formatter.

Konstruktor vom Formatter

Die Klasse Formatter hat eine beeindruckende Anzahl von Konstruktoren:

· Formatter()

· Formatter(Appendable a)

· Formatter(Appendable a, Locale l)

· Formatter(File file)

· Formatter(File file, String csn)

· Formatter(File file, String csn, Locale l)

· Formatter(Locale l)

· Formatter(OutputStream os)

· Formatter(OutputStream os, String csn)

· Formatter(OutputStream os, String csn, Locale l)

· Formatter(PrintStream ps)

· Formatter(String fileName)

· Formatter(String fileName, String csn)

· Formatter(String fileName, String csn, Locale l)

Wird nicht der Standardkonstruktur eingesetzt, schreibt der Formatter in die angegebene Quelle. Daher ist die Klasse schön für das Schreiben von Texten in Dateien geeignet. Formatter implementiert Closeable, ist also auch AutoCloseable. Ein Beispiel zum Schreiben in Dateien:

try ( Formatter out = new Formatter( "ausgabe.txt", StandardCharsets.ISO_8859_1.name() ) ) {

for ( int i = 0; i < 10; i++ )

  out.format( "%02d%n", i );

}

catch ( FileNotFoundException | UnsupportedEncodingException e ) {

e.printStackTrace();

}

Inselupdate: Methode mit variabler Argumentanzahl (Vararg)

Bei vielen Methoden ist es klar, wie viele Argumente sie haben; eine Sinus-Methode bekommt nur ein Argument, equals(Object) ein Objekt, println(…) nichts oder genau ein Argument, usw. Es gibt jedoch Methoden, bei denen die Zahl mehr oder weniger frei ist. Ein paar Beispiele:

· Wenn der Aufruf System.out.printf(formatierungsstring, arg1, args2, arg3, …) etwas auf dem Bildschirm ausgibt, ist erst einmal nicht bekannt, wie viele Argumente die Methode besitzt, denn sie sind abhängig vom Formatierungsstring.

· Fügt Collections.addAll(sammlung, elem1, elem2, elem3, …) etwas einer Sammlung hin, ist frei, wie viele Elemente es sind.

· Wird ein Pfad für das Dateisystem zusammengebaut, ist vorher unbekannt, wie viele Segmente Paths.get( anfang, segment1, segment2, … ) besitzt.

· Startet new ProcessBuilder("kommando", "arg1", "arg1", …).start() ein Hintergrundprogramm, ist der Methode unbekannt, wie viele Argumente dem externen Programm übergeben werden.

Um die Anzahl der Parameter beliebig zu gestalten, sieht Java Methoden mit variabler Argumentanzahl, auch Varargs genannt, vor.

Eine Methode mit variabler Argumentanzahl nutzt die Ellipse (»…«) zur Verdeutlichung, dass eine beliebige Anzahl Argumente angegeben werden dürfen, dazu zählt auch die Angabe keines Elements. Der Typ fällt dabei aber nicht unter den Tisch; er wird ebenfalls angegeben.

System.out.printf() nimmt eine beliebige Anzahl von Argumenten an

Eine Methode mit Varargs haben wir schon einige Male verwendet: printf(…). Die Deklaration ist wie folgt:

class java.io.PrintStream extends FilterOutputStream implements Appendable, Closeable

PrintStream printf(String format, Object… args)
Nimmt eine beliebige Liste von Argumenten an und formatiert sie nach dem gegebenen Formatierungsstring format.

Gültige Aufrufe von printf(…) sind demnach:

Aufruf

Variable Argumentliste

System.out.printf("%n")

ist leer

System.out.printf("%s", "Eins")

besteht aus nur einem Element: "Eins"

System.out.printf("%s,%s,%s", "1", "2", "3")

besteht aus drei Elementen: "1", "2", "3"

Maximum eines Feldes finden

Die Klasse java.lang.Math sieht eine statische max(…)-Methode mit zwei Argumenten vor, doch grundsätzlich könnte die Methode auch beliebig viele Argumente entgegennehmen und von diesen Elementen das Maximum bilden. Die Methode könnte so aussehen:

static int max( int… array ) {
}

max(…) behandelt den Parameter array wie ein Feld. Da wir Argumente vom Typ int fordern, ist array vom Typ int[] und kann so zum Beispiel mit dem erweiterten for durchlaufen werden:

for ( int e : array )

Werden variable Argumentlisten in der Signatur definiert, so dürfen sie nur den letzten Parameter bilden; andernfalls könnte der Compiler bei den Parametern nicht unbedingt zuordnen, was nun ein Vararg und was schon der nächste gefüllte Parameter ist:

public class MaxVarArgs {

  static int max( int… array ) {
    if ( array == null || array.length == 0 )
      throw new IllegalArgumentException( "Array null oder leer" );
    int currentMax = Integer.MIN_VALUE;
    for ( int e : array )
      if ( e > currentMax )
        currentMax = e;
    return currentMax;
  }
  public static void main( String[] args ) {
    System.out.println( max(1, 2, 9, 3) );     // 9
  }
}

Tipp: Vararg-Design. Muss eine Mindestanzahl von Argumenten garantiert werden – bei max(…) sollten das mindestens zwei sein – ist es besser, eine Deklaration wie folgt zu nutzen: max(int first, int second, int… rest).

Compiler baut Array auf

Der Nutzer kann jetzt die Methode aufrufen, ohne ein Feld für die Argumente explizit zu definieren. Er bekommt auch gar nicht mit, dass der Compiler im Hintergrund ein Feld mit vier Elementen angelegt hat. So übergibt der Compiler:

System.out.println( max( new int[] { 1, 2, 9, 3 } ) );

An der Schreibweise lässt sich gut ablesen, dass wir ein Feld auch von Hand übergeben können:

int[] ints = { 1, 2, 9, 3 };
System.out.println( max( ints ) );

Hinweis. Da Varargs als Felder umgesetzt werden, sind überladene Varianten wie max(int… array) und max(int[] array), also einmal mit einem Vararg und einmal mit einem Feld, nicht möglich. Besser ist es hier, immer eine Variante mit Varargs zu nehmen, da diese mächtiger ist. Einige Autoren schreiben auch die Einstiegsmethode main(String[] args) mit variablen Argumenten, also main(String… args). Das ist gültig, denn im Bytecode steht ja ein Array.

Arrays.asList() Beispiel und Hinweis

Beispiel: Gib das größte Element eines Feldes aus.

Integer[] ints = { 3, 9, -1, 0 };

System.out.println( Collections.max( Arrays.asList( ints ) ) );

Zum Ermitteln des Maximums bietet die Utility-Klasse Arrays keine Methode, daher bietet sich die max(…)-Methode von Collections an. Auch etwa zum Ersetzen von Feldelementen bietet Arrays nichts, aber Collections. Sortieren und Füllen kann Arrays aber schon, hier muss asList() nicht einspringen.

Hinweis: Wegen der Generics ist der Parameter-Typ von asList() ein Objekt-Feld, aber niemals ein primitives Feld. In unserem Beispiel von eben würde so etwas wie

int[] ints = { 3, 9, -1, 0 };

Arrays.asList( ints );

zwar kompilieren, aber die Rückgabe von Arrays.asList(ints) ist vom Typ List<int[]>, was bedeutet, die gesamte Liste besteht aus genau einem Element und dieses Element ist das primitive Feld. Zum Glück führt Collections.max(Arrays.asList(ints)) zu einem Compilerfehler, denn von einer List<int[]>, also eine Liste von Feldern, kann max(Collection<? extends T>) kein Maximum ermitteln.

gzip Kommandozeilenprogramm mit wenigen Zeilen Quellcode

package com.tutego.insel.io.zip;

import java.io.*;
import java.nio.file.*;
import java.util.zip.GZIPOutputStream;

public class gzip {

  public static void main( String[] args ) {
    if ( args.length != 1 ) {
      System.err.println( "Benutzung: gzip <source>" );
      return;
    }

    try ( OutputStream gos = new GZIPOutputStream( Files.newOutputStream( Paths.get( args[ 0 ] + ".gz" ) ) ) ) {
      Files.copy( Paths.get( args[ 0 ] ), gos );
    }
    catch ( IOException e ) {
      System.err.println( "Fehler: Kann nicht packen " + args[ 0 ] );
    }
  }
}

java.io.File und NIO.2-Path: wo beide zusammenpassen und wo nicht

Die Klasse File ist schon immer da gewesen und stark mit dem lokalen Dateisystem verbunden. So findet sich der Typ File weiterhin bei vielen Operationen. Wenn Runtime.exec(String[] cmdarray, String[] envp, File dir) einen Hintergrundprozess startet, dann ist dir genau das Startverzeichnis. Eine Abstraktion auf virtuelle Dateisysteme ist unpassend und File passt als Typ sehr gut. Doch obwohl sich Pfade vom NIO.2-Typ Path schon an einigen Stellen in der Java-API finden lassen, sind doch immer noch viele APIs mit File ausgestattet. Dass ImageIO.read(File input) nur ein File-Objekt annimmt, aber kein Path-Objekt, um ein Bild zu laden ist schade, wo es doch auch eine read(InputStream) und read(URL)-Methode gibt. Die Bibliotheksdesigner haben bisher keine Notwendigkeit gesehen, das nachzubessern, vielleicht aus deswegen nicht, weil Entwickler die Möglichkeit haben, etwa mit Files.newInputStream(path) von einem Pfad einen Eingabestrom zu erfragen. Der Weg ist auch der Beste, denn vom Path ein File-Objekt zu erfragen und dann Methoden aufzurufen, die das File-Objekt annehmen, birgt eine Gefahr: von ein NIO.2-Dateisystem, etwa Zip, ein File-Objekt zu erfragen wird nicht funktionieren, weil es die Datei vom File-Objekt ja gar nicht im lokalen Verzeichnis gibt! Einige Klasse erwarten nur File-Objekte und nichts anderes, also auch kein Strom, und hier zeigt sich, dass diese Klassen nicht auf virtuellen Dateisystemen funktionieren können. Etwa der JFileChooser. Der operiert nur auf dem lokalen Dateisystem, was sich an JFileChooser.getSelectedFile() und JFileChooser.setCurrentDirectory(File dir) ablesen lässt.

Java und Sequenzpunkte in C(++)

Je mehr Freiheiten ein Compiler hat, desto ungenierter kann er optimieren. Besonders Schreibzugriffe interessieren Compiler, denn kann er diese einsparen kann, läuft das Programm später ein bisschen schneller. Damit das Resultat eines Compilers jedoch beherrschbar bleibt, definiert der C(++)-Standard Sequenzpunkte (eng. sequence point), an dem alle Schreibzugriffe klar zugewiesen wurden. (Dass der Compiler später Optimierungen macht ist eine andere Geschichte; die Sequenzpunkte gehören zum semantischen Modell, Optimierungen verändern das nicht). Das Semikolon als Abschluss von Anweisungen bildet zum Beispiel einen Sequenzpunkt. Im Ausdruck wie i = i + 1; j = i; muss der Schreizugriff auf i aufgelöst sein, bevor ein Lesezugriff für die Zuweisung zu j erfolgt. Problematisch ist, dass es gar nicht so viele Sequenzpunkte gibt, und es passieren kann, dass zwischen zwei Sequenzpunkten zwei mehrdeutige Schreibzugriffe auf die gleiche Variable stattfinden. Da das jedoch in C(++) undefiniert ist, kann sich der Compiler so verhalten wie er will – er muss sich ja nun an den Sequenzpunkten so verhalten wie gefordert. Problemfälle sind: (i=j) + i oder, weil ein Inkrement/Dekrement ein Lese-/Schreibzugriff ist, auch i = i++, was ja nichts anderes als i = (i = i + 1) ist. Bedauerlicherweise bildet die Zuweisung keinen Sequenzpunkt. Bei Zuweisungen der Art i = ++i + –i kann alles Mögliche später in i stehen, je nachdem, was der Compiler zu welchem Zeitpunkt ausführt. In Java sind diese Dinge von der Spezifikation klar geregelt, in C(++) ist nur geregelt, dass das Verhalten zwischen den Sequenzpunkten klar sein muss. Doch moderne Compiler erkennen konkurrierende Schreibzugriffe zwischen zwei Sequenzpunkten und mahnen sie (bei entsprechender Warnstufe) an.[1]

 



[1]       Beim GCC ist der Schalter -Wsequence-point (der auch bei -Wall mitgenommen wird), siehe dazu http://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html.

Änderungen an Schnittstellen: Code-Kompatibilität und Binär-Kompatibilität

Sind Schnittstellen einmal deklariert und in einer großen Anwendung verbreitet, so sind Änderungen nur schwer möglich, da sie schnell die Kompatibilität brechen. Wird der Name einer Parametervariablen umbenannt, ist das kein Problem, aber bekommt eine Schnittstelle eine neue Operation, führt das zu einem Übersetzungsfehler, wenn nicht automatisch alle implementierenden Klassen diese neue Methode implementieren. Framework-Entwickler müssen also sehr drauf achten, wie sie Schnittstellen modifizieren, doch sie haben es in der Hand, wie weit die Kompatibilität gebrochen wird.

Geschichtsstunde

Schnittstellen später zu ändern, wenn schon viele Klassen die Schnittstelle implementieren, ist eine schlechte Idee. Denn erneuert sich die Schnittstelle, etwa wenn nur eine Operation hinzukommt oder sich ein Parametertyp ändert, dann sind plötzlich alle implementierenden Klassen kaputt. Sun selbst hat dies bei der Schnittstelle java.sql.Connection riskiert. Beim Übergang von Java 5 auf Java 6 wurde die Schnittstelle erweitert, und keine Treiberimplementierungen konnten mehr compiliert werden.

Code-Kompatibilität und Binär-Kompatibilität

Es gibt Änderungen, die führen zwar zu Compilerfehlern, wie neu eingeführten Operationen, sind aber zur Laufzeit in Ordnung. Bekommt eine Schnittstelle eine neue Methode, so ist das für die JVM überhaupt kein Problem. Die Laufzeitumgebung arbeitet auf den Klassendateien selbst und sie interessiert es nicht, ob eine Klasse brav alle Methoden der Schnittstelle implementiert; sie löst nur Methodenverweise auf. Wenn eine Schnittstelle plötzlich „mehr“ vorschreibt, hat sie damit kein Problem.

Während also fast alle Änderungen an Schnittstellten zum Bruch der Codebasis führen, sind doch einige Änderungen für die JVM in Ordnung. Wir nennen das Binär-Kompatibilität. Zu den binär-kompatiblen Änderungen zählen:

  • Neue Methode hinzufügen
  • Schnittstelle erbt von einer zusätzlichen Schnittstelle
  • Hinzufügen/Löschen einer throws-Ausnahme
  • Letzten Parametertyp von T[] in T… ändern
  • Neue Konstanten, also statische Variablen hinzufügen
  • Die Anzahl der binär-inkompatiblen Änderungen sind jedoch gradierender. Verboten sind:

  • Ändern des Methodennamens
  • Ändern der Parametertypen und Umsortieren der Parameter
  • Formalen Parameter hinzunehmen oder entfernen
  • Strategien zum Ändern von Schnittstellen

    Falls die Schnittstelle nicht groß veröffentlicht wurde, so lassen sich einfacher Änderungen vornehmen. Ist der Name einer Operation zum Beispiel schlecht gewählt, wird ein Refactoring in der IDE den Namen in der Schnittstelle genauso ändern wie auch alle Bezeichner in den implementierenden Klassen. Problematischer ist es, wenn externe Nutzer sich auf die Schnittstelle verlassen. Eine Lösung ist, diese Klienten ebenfalls zur Änderung zu zwingen, oder auf „Schönheitsänderungen“, wie dem Ändern des Methodenamens, einfach zu zu verzichten.

    Kommen Operationen hinzu, hat sich eine Konvention etabliert, die im Java-Universum oft anzutreffen ist: Soll eine Schnittstelle um Operationen erweitert werden, so gibt es eine neue Schnittstelle, die die alte erweitert, und auf „2“ endet; java.awt.LayoutManager2 ist so ein Beispiel aus dem Bereich der grafischen Oberflächen, Attributes2, EntityResolver2, Locator2 für XML-Verarbeitung sind weitere. Ein Blick auf die API vom Eclipse-Framework (http://help.eclipse.org/juno/topic/org.eclipse.platform.doc.isv/reference/api/index.html?overview-summary.html) zeigt, dass bei mehr als 3500 Typen dieses Muster um die 70 Mal angewendet wurde.

    Seit Java 8 gibt es eine weitere Möglichkeit Operationen in Schnittstellen hinzuzufügen, sogenannte Virtuelle Erweiterungsmethoden. Sie erweitern die Schnittstelle, fügen aber gleich schon eine vorgefertigte Implementierung mit, sodass Unterklassen nicht zwingend eine Implementierung anbieten müssen.

    Ressourcen aus dem Klassenpfad und aus Jar‑Archiven laden

    Um Ressourcen wie Grafiken oder Konfigurationsdateien aus Jar-Archiven zu laden gibt es eine Methoden am Class-Objekt: getResourceAsStream().

     

    class java.lang.Class

    implements Serializable, GenericDeclaration, Type, AnnotatedElement

    – InputStream getResourceAsStream(String name)
    Gibt einen Eingabestrom auf die Datei mit dem Namen name zurück, oder null, falls es keine Ressource mit dem Namen im Klassepfad gibt.

     

    Da der Klassenlader die Ressource findet, entdeckt er alle Dateien, die im Pfad des Klassenladers eingetragen sind. Das gilt auch für Jar-Archive, weil dort vom Klassenlader alles verfügbar ist. Die Methode getResourceAsStream() liefert auch null, wenn die Sicherheitsrichtlinien das Lesen verbieten. Da die Methode keine Ausnahme auslöst, muss auf jeden Fall getestet werden, ob die Rückgabe ungleich null war.

    Das folgende Programm liest ein Byte ein und gibt es auf dem Bildschirm aus:

     

    package com.tutego.insel.io.stream;

    import java.io.*;

    import java.util.Objects;

    public class GetResourceAsStreamDemo {

    public static void main( String[] args ) {

      String filename = "onebyte.txt";

      InputStream is = Objects.requireNonNull(

       GetResourceAsStreamDemo.class.getResourceAsStream( filename ),

        "Datei gibt es nicht!" );

      try {

       System.out.println( is.read() ); // 49

      }

      catch ( IOException e ) {

       e.printStackTrace();

      }

    }

    }

    Die Datei onebyte.txt befindet sich im gleichen Pfad wie auch die Klasse, liegt also in com/tutego/insel/io/stream/onebyte.txt. Liegt sie zum Beispiel im Wurzelverzeichnis des Pakets, muss sie mit "/onebyte.txt" angegeben werden. Liegen die Ressourcen außerhalb des Klassenpfades, können sie nicht gelesen werden. Der große Vorteil ist aber, dass die Methode alle Ressourcen anzapfen kann, die über den Klassenlader zugänglich sind, und das ist insbesondere der Fall, wenn die Dateien aus Jar-Archiven kommen – hier gibt es keinen üblichen Pfad im Dateisystem, der hört in der Regel beim Jar-Archiv selbst auf.

    Zum Nutzen der getResourceAsStream()-Methoden ist ein Class-Objekt nötig, was wir in unserem Fall über Klassenname.class besorgen. Das ist nötig, weil die Methode main() statisch ist. Andernfalls kann innerhalb von Objektmethoden auch getClass() eingesetzt werden, eine Methode, die jede Klasse aus der Basisklasse java.lang.Object erbt.

    Lesen aus Dateien und Schreiben in Dateien

    Um Daten aus Dateien lesen, oder sie schreiben zu können, ist eine Strom-Klasse nötig, die es schafft, die Operationen von Reader, Writer, InputStream und OutputStream auf Dateien abzubilden. Um an solche Implementierungen zu kommen gibt es drei verschiedene Ansätze:

    · Die Utility-Klasse Files bietet vier newXXX()-Methoden, um Lese-/Schreib-Datenströme für Zeichen- und Byte-orientierte Dateien zu bekommen.

    · Ein Class-Objekt bietet getResourceAsStream() und liefert einen InputStream, um Bytes aus Dateien im Klassenpfad zu lesen. Zum Schreiben gibt es nichts Vergleichbares und falls keine Bytes sondern Unicode-Zeichen gelesen werden sollen, muss der InputStream in einen Reader konvertiert werden.

    · Die speziellen Klassen FileInputStream, FileReader, FileOutputStream, FileWriter sind Strom-Klassen, die read()/write()-Methoden auf Dateien abbilden.

    Jede der Varianten hat Vor-/und Nachteile.

    Primitiv- und Verweis-Typ und der Vergleich mit Smalltalk und .NET

    Die Datentypen in Java zerfallen in zwei Kategorien:

    • Primitive Typen: Die primitiven (einfachen) Typen sind die eingebauten Datentypen für Zahlen, Unicode-Zeichen und Wahrheitswerte.
    • Referenztypen: Mit diesem Datentyp lassen sich Objektverweise etwa auf Zeichenketten, Datenstrukturen oder Zwergpinscher verwalten.

    Warum sich damals Sun für diese Teilung entschieden hat, lässt sich mit einem einfachen Grund erklären: Java ist als Programmiersprache entworfen worden, die kleine schwache Geräte unterstützen sollte, und auf denen musste die Java-Software, die am Anfang noch interpretierte wurde, so schnell wie möglich laufen. Unterscheidet der Compiler zwischen primitiven Typen und Referenztypen, so kann der er relativ leicht Bytecode erzeugen, der ebenfalls zwischen den beiden Typen unterscheidet. Damit kann die Laufzeitumgebung auch den Programmcode viel schneller ausführen, und das mit einem relativen einfachen Compiler. Das war für die Anfangszeit ein wichtiges Kriterium.

    Sprachvergleich mit Smalltalk und .NET

    In Smalltalk ist alles ein Objekt, auch die eingebauten Sprachdatentypen. Für Zahlen gibt es einen Basistyp Number und Integer, Float, Fraction als Untertypen. Immer noch gibt es arithmetische Operatoren (+, -, *, /, //, \\ um sie alle aufzuzählen), aber das sind nur Methoden der Klasse Number.[1] Für Java-Entwickler sind Methodennamen wie + oder – ungewöhnlich, doch in Smalltalk ist es das nicht. Syntaktisch unterscheidet sich ein 1 + 2 in Java und Smalltalk nicht, nur in Smalltalk ist die Addition ein Nachrichtenaufruf an das Integer-Objekt 1 an die Methode + mit dem Argument 2, was wiederum ein Integer-Objekt ist – die Objekte baut der Compiler selbständig aus den Literalen auf. Eine Klasse Integer für Ganzzahlen besitzt weitere Methoden wie asCharacter, floor usw.[2] Es ist wichtig zu verstehen, dass dies nur das semantische Modell auf der Sprachseite ist; das hat nichts damit zu tun, wie später die Laufzeitumgebung diese speziellen Nachrichtenaufrufe optimiert. Moderne Smalltalk-Laufzeitumgebungen mit Just-In-Time-Compilation sind bei arithmetischen Operationen auf einen ähnlichen Level wie C oder Java. Durch die Einteilung von Java in primitive Datentypen und Referenztypen haben die Sprachschöpfer einen objektorientierten Bruch in Kauf genommen, um die interpretierte Laufzeit Anfang der 1990er zu optimieren – eine Optimierung, die aus heutiger Sicht unnötig war.

    In .NET ist es eher wie in Java. Der Compiler kennt eingebauten Datentypen und gibt ihnen eine Sonderbehandlung, es sind keine Methodenaufrufe. Auch im Bytecode (Common Intermediate Language, kurz CIL in .NET genannt) finden sich Anweisungen wie Addition, Subtraktion wieder. Doch es gibt noch einen Unterschied zu Java. Der Compilder bildet Datentypen der .NET-Sprachen auf .NET-Klassen ab, und diese Klassen haben Methoden. In C# ist der eingebaute Datentyp float mit dem Datentyp Single (aus dem .NET-Paket System) identisch und es ist egal, ob Entwickler float f oder Single f schreiben. Doch Single (respektive float) hat im Vergleich zu Smalltalk keine mathematischen Operationen, aber dennoch ein paar wenige Methoden wie ToString()[3]. In .NET verhalten sich folglich die eingebauten Datentypen wie Objekte, sie haben Methoden, haben aber die gleiche Wertsemantik zum Beispiel bei Methodenaufrufen wie in Java, und sehen auch im Bytecode ähnlich aus, was ihnen die gleiche gute Performance gibt.


    [1] Die Dokumentation für das GNU Smalltalk zeigt auf: http://www.gnu.org/software/smalltalk/manual-base/html_node/Number_002darithmetic.html#Number_002darithmetic

    [2] http://www.gnu.org/software/smalltalk/manual-base/html_node/Integer.html.

    [3] Siehe http://msdn.microsoft.com/en-us/library/system.int32_members(v=vs.71).aspx.

    Besondere Rückgaben oder Ausnahmen?

    Nicht immer ist eine Ausnahme nötig, doch wann es eine Rückgabe wie null oder -1 gibt, und wann eine Ausnahme ausgelöst werden soll, ist nicht immer einfach zu beantworten und hängt vom Kontext ab. Ein Beispiel: Eine Methode liest eine Datei ein und führt eine Suche durch. Wenn eine bestimmte Teilzeichenkette nicht vorhanden ist, soll die Methode dann eine Ausnahme werfen oder nicht? Hier kommt es drauf an.

    1. Wenn das Dokument in der ersten Zeile eine Kennung tragen muss und der Test prüft auf diese Kennung, dann liegt ein Protokollfehler vor, wenn diese Kennung nicht vorhanden ist.

    2. Im Dokument gibt es eine einfache Textsuche. Ein Suchwort kann enthalten sein, muss aber nicht.

    Im ersten Fall passt eine Ausnahme gut, da ein interner Fehler vorliegt. Muss die Kennung in der Datei sein, ist sie aber nicht, darf dieser Fehler nicht untergehen und eine Ausnahme zeigt das perfekt an. Ob geprüft oder ungeprüft steht auf einem anderen Blatt. Im zweiten Fall ist eine Ausnahme unangebracht, da es kein Fehler ist, wenn der Suchstring nicht im Dokument ist; das kann vorkommen. Das ist das gleiche wie bei indexOf() oder matches() von String – die Methoden würden ja auch keine Ausnahmen werfen, wenn es keine Übereinstimmung gibt.

    Überlaufe

    Bei einigen mathematischen Fragestellungen muss sich feststellen lassen, ob Operationen wie die Addition, Subtraktion oder Multiplikation den Zahlenbereich sprengen, also etwa den Ganzzahlenbereich eines Integers von 32 Bit verlassen. Passt das Ergebnis einer Berechnung nicht in den Wertebereich einer Zahl, so wird dieser Fehler standardmäßig nicht von Java angezeigt; weder der Compiler noch die Laufzeitumgebung melden dieses Problem. Es gibt auch keine Ausnahme, Java hat keine eingebaute Überlaufkontrolle.

    Beispiel

    Mathematisch gilt a × a / a = a, also zum Beispiel 100 000 × 100 000 / 100 000 = 100 000. In Java ist das anders, da wir bei 100 000 × 100 000 einen Überlauf im int haben.

    System.out.println( 100000 * 100000 / 100000 );     // 14100

    liefert daher 14100. Wenn wir den Datentyp auf long erhöhen, indem wir hinter ein 100 000 ein L setzen, sind wir bei dieser Multiplikation noch sicher, da ein long das Ergebnis aufnehmen kann.

    System.out.println( 100000L * 100000 / 100000 );    // 100000

    Hinweis

    Ein Test auf Überlauf könnte aussehen: boolean canMultiply(int a, int b) { return a * b / b == a; } reichen. Doch eine JVM kann das zu return a == a; optimieren und somit zu return true; machen, sodass der Test nicht funktioniert.

    Überlauf erkennen

    Für eine Operation wie die Addition oder Subtraktion lässt sich relativ leicht erkennen, ob das Ergebnis über das Ziel hinausschießt. Eine Möglichkeit ist, bei der Addition zweier ints diese erst auf long zu bringen und dann den long mit der Konstanten Integer.MAX_VALUE/Integer.MIN_VALUE zu vergleichen. Aber über die Interna brauchen wir uns keine großen Gedanken machen, denn ab Java 8 kommen neue Methoden hinzu, die eine Überlauferkennung ermöglichen. Die Methoden gibt es in Math und StrictMath:

    · static int addExact(int x, int y)

    · static long addExact(long x, long y)

    · static int subtractExact(int x, int y)

    · static long subtractExact(long x, long y)

    · static int multiplyExact(int x, int y)

    · static long multiplyExact(long x, long y)

    · static int toIntExact(long value)

    Alle Methoden werfen eine ArithmeticException, falls die Operation nicht durchführbar ist, die letzte, wenn (int)value != value ist. Leider deklariert Java keine Unterklassen wie UnderflowException oder OverflowException, und Java meldet nur alles vom Typ ArithmeticException mit der Fehlermeldung „xxx overflow“, auch wenn es eigentlich ein Unterlauf ist:

    System.out.println( subtractExact( Integer.MIN_VALUE, 1 ) ); // ArithmeticException

    Inselupdate: Vorzeichenlos arbeiten

    Bis auf char sind in Java alle integralen Datentypen vorzeichenbehaftet und kodiert im Zweierkomplement. Bei einem byte stehen 8 Bit für die Kodierung eines Wertes zur Verfügung, jedoch sind es eigentlich nur 7 Bit, denn über ein Bit erfolgt die Kodierung des Vorzeichens. Der Wertebereich ist von -128 bis +127. Über einen Umweg ist es möglich, den vollen Wertebereich auszuschöpfen und so zu tun, als ob Java vorzeichenlose Datentypen hätte.

    byte als vorzeichenlosen Datentyp nutzen

    Eine wichtige Eigenschaft der expliziten Typanpassung bei Ganzzahltypen ist es, dass die überschüssigen Bytes einfach abgeschnitten werden. Betrachten wir die Typanpassung an einem Beispiel:

    int l = 0xABCD6F;

    byte b = (byte) 0xABCD6F;

    System.out.println( Integer.toBinaryString( l ) ); // 101010111100110101101111

    System.out.println( Integer.toBinaryString( b ) ); // 1101111

    Liegt eine Zahl im Bereich von 0 bis 255, so kann ein byte diese durch seine 8 Bit grundsätzlich speichern. Java muss jedoch mit der expliziten Typanpassung gezwungen werden, das Vorzeichenbit zu ignorieren. Erst dann entspricht die Zahl 255 acht gesetzten Bits, denn sie mit byte b = 255; zu belegen, funktioniert nicht.

    Damit die Weiterverarbeitung gelingt, muss noch eine andere Eigenschaft berücksichtigt werden. Sehen wir uns dazu folgende Ausgabe an:

    byte b1 = (byte) 255;

    byte b2 = -1;

    System.out.println( b1 ); // -1

    System.out.println( b2 ); // -1

    Das Bitmuster ist in beiden Fällen gleich, alle Bits sind gesetzt. Dass die Konsolenausgabe aber negativ ist, hat mit einer anderen Java-Eigenschaft zu tun: Java konvertiert das Byte, welches vorzeichenbehaftet ist, in ein int (der Parametertyp bei toBinaryString() ist int) und bei dieser Konvertierung wandert das Vorzeichen weiter. Das folgende Beispiel zeigt das bei der Binärausgabe:

    byte b = (byte) 255;
    int  i = 255;
    System.out.printf( "%d %s%n", b, Integer.toBinaryString(b) );

    // –1  11111111111111111111111111111111
    System.out.printf( "%d %s%n", i, Integer.toBinaryString(i) );

    // 255                         11111111

    Die Belegung der unteren 8 Bit vom byte b und int i ist identisch. Aber während beim int die oberen 3 Byte wirklich null sind, füllt Java durch die automatische Anpassung des Vorzeichens bei der Konvertierung von byte nach int im Zweierkomplement auf die oberen drei Byte mit 255 auf. Soll ohne Vorzeichen weitergerechnet werden stört das. Diese Automatisch Anpassung nimmt Java immer vor, wenn mit byte/short gerechnet wird und nicht nur wie in unserem Beispiel, wenn eine Methoden den Datentyp int fordert.

    Um bei der Weiterverarbeitung einen Datenwert zwischen 0 und 255 zu bekommen, also das Byte eines int vorzeichenlos zu sehen, schneiden wir mit der Und-Verknüpfung die unteren 8 Bit heraus – alle anderen Bits bleiben also ausgenommen:

    byte b = (byte) 255;

    System.out.println( b ); // -1

    System.out.println( b & 0xff ); // 255

    Bibliotheksmethoden für vorzeichenlose Behandlung

    Immer ein & 0xff an einen Ausdruck zu setzen um die oberen Bytes auszublenden ist zwar nicht sonderlich aufwändig, aber schön ist das auch nicht. Hübscher sind Methoden wie toUnsignedInt(byte), die mit dem Namen deutlich dokumentieren, was hier eigentlich passiert. In Java 8 gibt es daher einige Neuerungen.

    Neue Methoden in Byte:

  • static int toUnsignedInt(byte x)
  • static long toUnsignedLong(byte x)
  • In Integer:

  • static long toUnsignedLong(int x)
  • static String toUnsignedString(int i, int radix)
  • static String toUnsignedString(int i)
  • static int parseUnsignedInt(String s, int radix)
  • static int compareUnsigned(int x, int y)
  • static int divideUnsigned(int dividend, int divisor)
  • static int remainderUnsigned(int dividend, int divisor)
  • In Long:

  • String toUnsignedString(long i, int radix)
  • static String toUnsignedString(long i)
  • static long parseUnsignedLong(String s, int radix)
  • static int compareUnsigned(long x, long y)
  • static long divideUnsigned(long dividend, long divisor)
  • static long remainderUnsigned(long dividend, long divisor)
  • In Short:

  • static int toUnsignedInt(short x)
  • static long toUnsignedLong(short x)
  • Neben den einfachen Methoden toUnsignedXXX()-Methoden in den Wrapper-Klassen gesellen sich Methoden hinzu, die auch die Konvertierung in einem String bzw. das Parsen eines Strings ermöglichen. Bei Integer und Long lassen sich ebenfalls neue Methoden ablesen, die Vergleiche, Division und Restwertbildung vorzeichenlos durchführen.

    Echte typsichere Container

    Verwenden Entwickler die Sammlungen untypisiert, also mit dem Raw-Typ, so lässt sich nicht verhindern, dass ein ungewünschter Typ die Sammlung betritt und beim Entnehmen zu einer Ausnahme führen kann. Der Grund liegt in der internen Umsetzung, dass nur der Compiler selbst die Typsicherheit sicherstellt, aber nicht die Laufzeitumgebung. Um die Typsicherheit zu erhöhen, bietet die Collections-Klasse ein paar Wrapper-Methoden, die eine Sammlung nehmen und Operationen nur eines gewissen Typs durchlassen – verstoßt ein Aufrufer dagegen, gibt es eine ClassCastException.

    class java.util.Collections

    § static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)

    § static <E> List<E> checkedList(List<E> list, Class<E> type)

    § static <K,V> Map<K,V> checkedMap(Map<K,V> m, Class<K> keyType, Class<V> valueType)

    § static <E> Queue<E> checkedQueue(Queue<E> queue, Class<E> type)

    § static <E> Set<E> checkedSet(Set<E> s, Class<E> type)

    § static <K,V> SortedMap<K,V> checkedSortedMap(SortedMap<K,V> m, Class<K> keyType, Class<V> valueType)

    § static <E> SortedSet<E> checkedSortedSet(SortedSet<E> s, Class<E> type)

    Beispiel: Lasse in eine Menge nur Strings, aber nichts anderes.

    Set<String> set = Collections.checkedSet( new HashSet<String>(),

    String.class );

    set.add( "xyz" ); // Compiler OK

    Set rawset = set;

    rawset.add( "abc" ); // Compiler OK

    rawset.add( new Object() ); // Compiler OK,  N Laufzeitfehler

    Die Ausnahme ist: “Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Object element into collection with element type class java.lang.String”

    Auch wenn kein Programmierer freiwillig falsche Typen in eine Sammlung platziert, ist es doch besser, eine absolute Sicherheit zu bekommen, die nicht auf dem Compiler beruht. Wenn etwa ein Programm einer Skriptsprache die Möglichkeit eröffnet Elemente in eine Datenstruktur zu setzen, so lässt sich für die Skriptsprache eine geprüfte Sammlung zur Ablage nach außen geben. Achtet das Skript nicht auf den richtigen Typ, so knallt es im Skript. Das ist gewünscht, denn andernfalls würde viel später erst der falsche Typ bei der Bearbeitung auffallen und dann knallt es an der ganz falschen Stelle.