Einführung in Java 8 Lambda-Ausdrücke: Code sind Daten

Wer den Begriff „Daten“ hört, denkt zunächst einmal an Zahlen, Bytes, Zeichenketten oder auch komplexe Objekte mit ihrem Zustand. Wir wollen in diesem Kapitel diese Sicht ein wenig erweitern und auf Programmcode lenken. Java-Code, versinnbildlicht als Serie von Bytecodes, besteht auch aus Daten. Und wenn wir uns einmal auf diese Sichtweise einlassen, dass Code gleich Daten ist, dann lässt sich Code auch wie Daten übergeben und so von einem Punkt zum anderen übertragen, speichern und später referenzieren. Mit dieser Möglichkeit, Code zu übertragen, lässt sich das Verhalten von Algorithmen leicht anpassen. Beginnen wir mit ein paar Beispielen, bei denen Programmcode übergeben wird, auf den dann später zugegriffen wird:

  • Ein Thread führt Programmcode im Hintergrund aus. Der Programmcode, den der Java-Thread ausführen soll, wird in ein Objekt vom Typ Runnable verpackt, genau genommen in eine run()-Methode gesetzt. Kommt der Thread zum Zuge, ruft er die run()-Methode auf.
  • Ein Timer ist eine util-Klasse, die zu bestimmen Zeitpunkten Programmcode ausführen kann. Der Objektmethode scheduleAtFixedRate(…) wird dabei ein Objekt vom Typ TimerTask übergeben, das den Programmcode enthält.
  • Zum Sortieren von Daten kann eine eigene Ordnung definiert werden, die dem Sortierer als Comparator übergeben werden kann. Der Comparator deklariert eine Vergleichsmethode, an die sich der Sortierer wendet, um zwei Objekte in die gewünschte Reihenfolge zu bringen.
  • Aktiviert der Benutzer auf der Oberfläche eine Schaltfläche, so führt das zu einer Aktion. Der Programmcode steckt – beim UI-Framework Swing – in einem Objekt vom Typ ActionListener und wird an der Schaltfläche JButton mit addActionListener(…) fest gemacht. Kommt es zu einer Schaltflächenaktivierung, arbeitet das UI-System den Programmcode in der Methode actionPerformed(…) des gespeicherten ActionListener

Um Programmcode von einer Stelle zur anderen zu bringen, wird in Java immer der gleiche Mechanismus eingesetzt: Eine Klasse implementiert eine (in der Regel nichtstatische) Methode, in der der auszuführende Programmcode steht. Ein Objekt dieser Klasse wird an eine andere Stelle übergeben, und der Interessent greift dann über die Methode auf den Programmcode zu. Dass ein Objekt noch mehr als diese eine Implementierung enthalten kann, etwa Variablen, Konstanten, Konstruktoren, ist dafür nicht relevant. Diesen Mechanismus schauen wir uns jetzt in verschiedenen Varianten genauer an.

Innere Klassen als Code-Transporter

Bleiben wir bei dem Beispiel mit den Vergleichen. Angenommen, wir sollen Strings so sortieren, dass Leerraum vorne und hinten bei den Vergleichen ignoriert wird, also “ Newton “ gleich „Newton“ ist. Bei Vorgaben dieser Art muss einem Sortieralgorithmus ein Stückchen Code übergeben werden, damit er die korrekte Reihenfolge herstellen kann. Praktisch sieht das so aus:

import java.util.*;
 public class CompareTrimmedStrings {
   public static void main( String[] args ) {
     class TrimmingComparator implements Comparator<String> {
       @Override public int compare( String s1, String s2 ) {
         return s1.trim().compareTo( s2.trim() );
       }
     }
     String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
     Arrays.sort( words, new TrimmingComparator() );
     System.out.println( Arrays.toString( words ) );
   }
 }

Die Ausgabe ist:

[        Adele    , M,  Q, 
 Skyfall]

Der TrimmingComparator enthält in der compare(…)-Methode den Programmcode für die Vergleichslogik. Ein Exemplar vom TrimmingComparator wird aufgebaut und Arrays.sort(…) übergeben. Das geht mit weniger Code!

Innere anonyme Klassen als Code-Transporter

Klassen enthalten Programmcode, und Exemplare der Klassen werden an Methoden wie sort(…) übergeben, damit der Programmcode dort hinkommt, wo er gebraucht wird. Doch elegant ist das nicht. Für die Beschreibung des Programmcodes ist extra eine eigene Klasse erforderlich. Das ist viel Schreibarbeit, und über eine innere anonyme Klasse lässt sich der Programmcode schon ein wenig verkürzen:

String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
 Arrays.sort( words, new Comparator<String>() {
   @Override public int compare( String s1, String s2 ) {
     return s1.trim().compareTo( s2.trim() );
   } } );
 System.out.println( Arrays.toString( words ) );

Allerdings ist das immer noch aufwändig: Wir müssen eine Methode überschreiben und dann ein Objekt aufbauen. Für Programmautoren ist das lästig, und die JVM hat es mit vielen überflüssigen Klassendeklarationen zu tun. Die Frage ist: Wenn der Compiler weiß, dass bei sort(…) ein Comparator nötig ist, und wenn ein Comparator sowieso nur eine Methode hat, muss dann Comparator und compare(…) überhaupt genannt werden?

Abkürzende Schreibweise durch Lambda-Ausdrücke

Mit Lambda-Ausdrücken lässt sich Programmcode leichter an eine Methode übergeben, denn es gibt eine kompakte Syntax für die Implementierung von Schnittstellen mit einer Operation. Für unser Beispiel sieht das so aus:

String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
 Arrays.sort( words,
             (String s1, String s2) -> { return s1.trim().compareTo(s2.trim()); } );
 System.out.println( Arrays.toString( words ) );

Der in fett gesetzte Ausdruck nennt sich Lambda-Ausdruck. Er ist eine kompakte Art und Weise, Schnittstellen mit genau einer Methode zu implementieren; die Schnittstelle Comparator hat genau eine Operation compare(…).

Optisch sind sich ein Lambda-Ausdruck und eine Methodendeklaration ähnlich; was wegfällt sind Modifizierer, der Rückgabetyp, der Methodenname und (mögliche) throws-Klauseln.

Methodendeklaration Lambda-Ausdruck
public int compare
( String s1, String s2 )

{ return s1.trim().compareTo( s2.trim() ); }

( String s1, String s2 )
->
{ return s1.trim().compareTo( s2.trim() ); }

Tabelle 1.1: Vergleich der Methodendeklaration einer Schnittstelle mit dem Lambda-Ausdruck

Wenn wir uns den Lambda-Ausdruck als Implementierung dieser Schnittstelle anschauen, dann lässt sich dort nichts von Comparator oder compare(…) ablesen – ein Lambda-Ausdruck repräsentiert mehr oder weniger nur den Java-Code und lässt das, was der Compiler aus dem Kontext herleiten kann, weg.

Alle Lambda-Ausdrücke lassen sich in einer Syntax formulieren, die die folgende allgemeine Form hat:

( LambdaParameter ) -> { Anweisungen }

Lambda-Parameter sind sozusagen die Eingabewerte für die Anweisungen. Die Parameterliste wird so deklariert, wie von Methoden oder Konstruktoren bekannt, allerdings gibt es keine Varargs. Es gibt syntaktische Abkürzungen, wie wir später sehen werden, doch vorerst bleiben wir bei dieser Schreibweise.

Geschichte: Der Java-Begriff „Lambda-Ausdruck“ geht auf das Lambda-Kalkül (in der englischen Literatur Lambda calculus genannt, auch geschrieben als λ-calculus) aus den 1930er Jahren zurück und ist eine formale Sprache zur Untersuchung von Funktionen.

Default-Methoden, Teil 2, Default-Methoden zur Entwicklung von Bausteinen nutzen

Bevor wir zu nächsten Punkt kommen, müssen wir noch einmal inne halten und uns fragen, was denn das Kernkonzept der objektorientierten Programmierung ist. Wohl ohne zu Zögern können wir Klassen und Kapselung nennen. Klassen und Klassenbeziehungen das Gerüst jedes Java-Programms. Schauen wir uns Vererbung noch einmal genauer an, so wissen wir, das Unterklassen Spezialisierungen sind, und das Liskovsche Substitutionsprinzip gilt: Falls ein Typ gefordert ist, können wir auch einen Untertyp übergeben. So sollte perfekte Vererbung aussehen: Eine Unterklasse sollte das Verhalten spezialisieren, aber nicht einfach von einer Klasse erben, weil sie nützliche Funktionalität hat. Aber warum eigentlich nicht? Ein Problem ist, das uns die Einfachvererbung nur eine einzige Oberklasse erlaubt. Wenn eine Klasse so etwas Nützliches wie Logging anbietet, und unsere Klasse davon erbt, kann sie nicht gleichzeitig von einer anderen Klasse erben, um zum Beispiel Zustände in Konfigurationsdaten festzuhalten. Das Problem bei der „Funktionalitätsvererbung“ ist also, dass wir uns nur einmal festlegen können. Wenn eine Klasse eine gewisse Funktionalität einfach braucht, woher soll sie denn dann kommen, wenn nicht aus der Oberklasse? Eigentlich gibt es hier nur eine naheliegende Variante: Die Klasse greift auf andere Objekte zurück per Delegation. Das ist interessant, aber auch nicht optimal, insbesondere gilt dann nicht die ist-eine-Art-von-Beziehung. Falls das nicht gewünscht ist, ist das in Ordnung, doch wenn über diesen Typ eine Abstraktion läuft, ist das ungünstig.

Ein Dilemma. Gut wäre eine Technik, die einen Programmbaustein in eine Klasse setzen kann. Im Grunde so etwas wie Mehrfachvererbung, aber doch anders, weil die Bausteine nicht als komplette Typen auftreten – der Baustein selbst ist nur ein Implantat und alleine uninteressant. Auch ein Objekt kann von diesem Baustein-Typ nicht erzeugt werden.

Am ehesten sind die Bausteine mit abstrakten Klassen vergleichbar, doch das wären Klassen und Nutzer könnten nur einmal von diesem Baustein erben. Mit Java 8 gibt es aber eine ganz neue Möglichkeit, und zwar mit den erweiterten Schnittstelle: Sie bilden die Bausteine, von denen Klassen Funktionalität bekommen können. Andere Programmiersprachen bieten so etwas Ähnliches und das Konzept wird dort Mixin oder Trait genannt.[1] Diese Bausteine sind nützlich, denn so lässt sich ein Algorithmus in eine extra Compilationseinheit setzen und leichter wiederverwenden. Ein Beispiel.

Nehmen wir zwei erweiterte Schnittelle an: PersistentPreference und Logged. Die erste erweiterte Schnittstelle soll mit store() Schlüssel/Werte-Paare in die zentrale Konfiguration schreiben und get() soll sie auslesen:

import java.util.prefs.Preferences;

interface PersistentPreference {

default void store( String key, String value ) {

  Preferences.userRoot().put( key, value );

}

default String get( String key ) {

  return Preferences.userRoot().get( key, "" );

}

}

Die zweite erweiterte Schnittstelle ist Logged und bietet drei kompakte Logger-Methoden:

import java.util.logging.*;

interface Logged {

default void error( String message ) {

  Logger.getLogger( getClass().getName() ).log( Level.SEVERE, message );

}

default void warn( String message ) {

  Logger.getLogger( getClass().getName() ).log( Level.WARNING, message );

}

default void info( String message ) {

  Logger.getLogger( getClass().getName() ).log( Level.INFO, message );

}

}

Eine Klasse kann diese Bausteine nun einbauen:

class Player implements PersistentPreference, Logged {

// …

}

Die Methoden sind nun Teil vom Player und können auch von Unterklassen überschrieben werden. Als Aufgabe für den Leser bleibt, die Implementierung von store() im Player zu verändern, dass der Schlüssel immer mit „player.“ beginnt. Die Frage, die Leser beantworten sollten ist, ob store() von Player auf das store() von der erweiterten Schnittstelle zugreifen kann.

Default-Methoden weiter gedacht

Für diese Bausteine, also die erweiterten Schnittstellen, gibt es viele Anwendungsfälle. Da die Java- Bibliothek schon an die 20 Jahre als ist, würden heute einige Typen anders aussehen. Dass sich Objekte mit equals() vergleichen lassen können, könnte heute zum Beispiel in einer erweiterten Schnittstelle stehen, etwa so: interface Equals { boolean equals( Object that ) default { return this == that; } }. So müsste java.lang.Object die Methode nicht für alle vorschreiben, wobei das sicherlich jetzt kein Nachteil ist. Natürlich gilt das gleiche auf für die hashCode()-Methode, die heutzutage aus einer erweiterten Schnittstelle Hashable stammen könnte.

Und java.lang.Number ist ein weiters Beispiel. Die abstrakte Basisklasse für Werte-repräsentierende Objekte deklariert die abstrakten Methoden doubleValue(), floatValue(), intValue(), longValue() und die konkreten Methoden byteValue() und shortValue(). Bisher erben AtomicInteger, AtomicLong, BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short von dieser Oberklasse. Auch diese Funktionalität ließe sich mit einer erweiterten Schnittstelle umsetzen.

Da Schnittstellen auch Generics haben können, werden Default-Methoden noch vielseitiger. Baustein können auch andere Bausteine erweitern, da eine Schnittstelle andere Schnittstellen extenden kann. Es ist dabei egal, ob die die Schnitten erweitert sind oder nicht.

Zustand in den Bausteinen?

Nicht jeder wünschenswerte Baustein ist mit erweiterten Schnittstellen möglich. Ein Grund ist, dass die Schnittstellen keinen Zustand einbringen können. Einen Baustein für einen Container können wir nicht so einfach implementieren, da ein Container Kinder verwaltet, und hierfür ist eine Objektvariable für den Zustand nötig. Schnittstellen haben nur statische Variablen und die sind für alle sichtbar und selbst wenn die Schnittstelle eine modifizierbare Datenstruktur referenzieren würde, würde jeder Nutzer des Container-Bausteins von den Veränderungen betroffen sein. Da es keinen Zustand gibt, existieren auch für Schnittstellen keine Konstruktoren und folglich auch nicht für solche Bausteine. Denn wo es keinen Zustand gibt, gib es nichts zu initialisieren. Wenn eine Default-Methode einen Zustand benötigt, müssen sie selbst diesen Zustand erfragen. Wie das geht zeigt folgendes Beispiel.

Repräsentiert eine Klasse eine Menge von Objekten, die sich sortieren lassen können, können wir einen Baustein Sortable mit einer Methode sort() realisieren. Allerdings muss die Implementierung irgendwie an die Daten kommen und hier kommt der Trick ins Spiel: Zwar ist sort() eine Default-Methode, doch die erweiterte Schnittstelle besitzt Methoden, die die Klasse implementieren muss, die dem Sortierer die Daten geben. Im Quellcode sieht das so aus:

Teil 1:

import java.util.*;

interface Sortable<T extends Comparable> {

  T[] getValues();

  void setValues( T[] values );

  default void sort() {

    T[] values = getValues();

    Arrays.sort( values );

    setValues( values );

  };

}

Damit sort() an die Daten kommt, erwartet Sortable von den implementieren Klassen eine Methode getValues(). Und damit die Daten nach dem Sortieren wieder zurückgeschrieben werden können, eine zweite Methode setValues(…). Der Clou ist, das die Klasse, die später Sortable realisieren wird, mit den beiden Methoden dem Sortierer Zugriff auf den Daten gewährt – allerdings auch jedem anderem Stück Code da die Methoden öffentlich sind. Da bleibt ein Geschmäckle.

Ein Nutzer vor Sortable soll RandomValues sein; die Klasse erzeugt intern Zufallszahlen.

Teil 2:

class RandomValues implements Sortable<Integer>

{

  private List<Integer> values = new ArrayList<>();

  public RandomValues() {

    Random r = new Random();

    for ( int i = r.nextInt( 20 ) + 1; i > 0; i– )

    values.add( r.nextInt(10000) );

  }

  @Override public Integer[] getValues() {

    return values.toArray( new Integer[values.size()] );

  }

  @Override public void setValues( Integer[] values ) {

    this.values.clear();

   Collections.addAll( this.values, values );

  }

}

Damit sind die Typen vorbereitet und ein Demo schließt das Beispiel ab:

Teil 3:

public class SortableDemo {

  public static void main( String[] args ) {

    RandomValues r = new RandomValues();

    System.out.println( Arrays.toString( r.getValues() ) );

    r.sort();

    System.out.println( Arrays.toString( r.getValues() ) );

  }

}

Aufgerufen kommt auf die Konsole zum Beispiel:

[2732, 4568, 4708, 4302, 4315, 5946, 2004]

[2004, 2732, 4302, 4315, 4568, 4708, 5946]

So interessant diese Möglichkeit auch ist, ein Problem wurde schon angesprochen: Jede Methode in einer Schnittstelle ist public, ob sie nun eine abstrakte oder Default-Methode ist. Es wäre schön, wenn die Datenzugriffsmethoden nicht öffentlich sein würden, aber das geht nicht.

Wo wir gerade bei der Sichtbarkeit sind. Gibt es im Default-Code Code-Duplizierung, so kann der gemeinsame Code bisher nicht in private Methoden ausgelagert werden, da es private Operationen in Schnittstellen nicht gibt. Allerdings läuft gerade ein Test, ob so etwas eingeführt werden soll.

Warnung!

Natürlich lässt sich mit Rumgetrickse ein Speicherort finden, der Exemplarzustände speichert. Es lässt sich zum Beispiel in der Schnittstelle ein Assoziativspeicher referenzieren, der eine this-Instanz mit einem Objekt assoziiert. Ein Container-Baustein, der mit add() Objekte in eine Liste setzt und sie mit iterable() herausgibt, könnte so aussehen:

interface ListContainer<T> {

Map<Object, List<Object>> $ = new HashMap<>();

default void add( T e ) {

  if ( ! $.containsKey( this ) )

   $.put( this, new ArrayList<Object>() );

$.get( this ).add( e );

}

default public Iterable<T> iterable() {

  if ( ! $.containsKey( this ) )

   return Collections.emptyList();

  return (Iterable<T>) $.get( this );

}

}

Nicht nur die öffentliche Konstante $ ist ein Problem, sondern auch, dass es ein großartiges doppeltes Speicherloch ist. Ein Exemplar der Klasse, die diese erweitert Schnittstelle nutzt, kann nicht so einfach entfernt werden, denn in der Sammlung ist noch eine Referenz auf das Objekt, die das Garbage Collection verhindert. Und selbst wenn dieses Objekt weg wäre, hätten wir noch all die referenzierten Kinder der Sammlung in der Map. Und das Problem ist nicht wirklich zu lösen, und hier müsste tief mit schwachen Referenzen in die Java-Voodoo-Kiste gegriffen werden. Alles in allem, keine gute Idee und Java-Chefentwickler Brian Goetz macht auch klar: „Please don’t encourage techniques like this. There are a zillion "clever" things you can do in Java, but shouldn’t. We knew it wouldn’t be long before someone suggested this, and we can’t stop you. But please, use your power for good, and not for evil. Teach people to do it right, not to abuse it.”[2] Daher: Es ist eine schöne Spielerei, aber Zustand sollte eine Aufgabe der abstrakten Basisklassen oder vom Delegate sein.

Zusammenfassung

Was wir in den letzten Beispielen gemacht haben war, ein Standardverhalten in Klassen einzubauen, ohne das dabei der Zugriff auf die einmalige Basisklasse nötig war und ohne das die Klasse an Hilfsklassen delegiert. In dieser Arbeitsweise können Unterklassen in jedem Fall die Methoden überschreiben und spezialisieren. Wie haben es also mit üblichen Klassen zu tun und mit erweiterten Schnittstellen, die nicht selbst eigenständige Entitäten bilden. In der Praxis wird es immer Fälle geben, in denen für eine Umsetzung eines Problems entweder eine abstrakte Klasse oder eine erweiterte Schnittstelle in Frage kommt. Wir sollten und dann noch einmal an die Unterschiede erinnern: Eine abstrakten Klasse kann Methoden aller Sichtbarkeiten haben und sie auch final setzen, sodass sie nicht mehr überschrieben werden können. Eine Schnittstelle dagegen ist mit puren virtuellen und öffentlichen Methoden darauf ausgelegt, dass eben die Implementierung überschrieben werden kann.


[1] Siehe etwa http://scg.unibe.ch/archive/papers/Scha02aTraitsPlusGlue2002.pdf.

[2] http://mail.openjdk.java.net/pipermail/lambda-dev/2012-July/005166.html

Klassenimplementierung geht vor Default-Methoden

So wie zwar eine Sonnenfinsternis selten ist, aber vorkommt, so kann auch die seltene Konstellation eintreten, dass eine Klasse von zwei Seiten eine Implementierung bekommt. Erbt zum Beispiel eine Klasse A eine Implementierung für eine Methode f() und implementiert die Klasse A gleichzeitig eine erweiterte Schnittstelle I, von der sie eine Default-Methode f() vorgeschlagen bekommt, ist vom Compiler bzw. der Laufzeitumgebung eine Entscheidung gefragt. Zunächst muss der Compiler entscheiden, ob so etwas überhaupt syntaktisch korrekt ist. Die Antwort lautet: ja.

Ein Beispiel soll denn Fall demonstrieren:

interface Buyable {

double price();

default boolean hasPrice() { return price() > 0; }

}

abstract class NotBuyable implements Buyable {

@Override public boolean hasPrice() { return false; }

}

public class Love extends NotBuyable implements Buyable {

@Override public double price() { return 10_01; }

public static void main(String[] args) {

System.out.println( new Love().hasPrice() ); // false

}

}

Wieder ist Buyable eine Schnittstelle mit einer Default-Methode hasPrice(). Neu ist eine abstrakte Klasse NotBuyable, die hasPrice() mit false beantwortet. Eine Klasse Love erweitert erstens NotBuyable und bekommt von dort hasPrice() und implementiert zweitens Buyable, was ebenfalls eine Implementierung über die Default-Methode hasPrice() mitbringt. Nachdem wir festgestellt haben, dass dieses Szenario syntaktisch korrekt ist, muss die Laufzeitumgebung eine Entscheidung fällen. Die sieht so aus, dass die Implementierung aus einer Klasse gewinnt. Ausgabe ist also false.

Bleibt abschließend die Frage, wie sich der Compiler verhält, wenn einer Klasse aus zwei erweiterten Schnittstellen eine Default-Methode angeboten wird. Kurz und knapp: Das führt zu einem Compilerfehler. Die Klasse RockAndRoll zeigt dieses Dilemma:

interface Buyable {

boolean hasPrice();

}

interface Love extends Buyable {

@Override default public boolean hasPrice() { return false; }

}

interface Guitar extends Buyable {

@Override default public boolean hasPrice() { return false; }

}

public class RockAndRoll implements Love, Guitar { } // Compilerfehler

Default-Methoden aus speziellen Oberschnittstellen ansprechen

Eine Unterklasse kann eine konkrete Methode der Oberklasse überschreiben, aber dennoch auf die Implementierung der überschriebenen Methode zugreifen. Allerdings muss der Aufruf über super erfolgen, da sich sonst ein Methodenaufruf rekursiv verfängt.

Default-Methoden können andere Default-Methoden aus Oberschnittstellen ebenfalls überschreiben und mit neuem Verhalten implementieren. Doch genauso wie normale Methoden können sie mit super auf Default-Verhalten aus dem übergeordneten Typ zurückgreifen.

Nehmen wir für ein Beispiel wieder zwei erweiterte Schnittstelle Prices und Buyable an:

interface Priced {

double price();

default boolean hasPrice() { return price() > 0; }

}

interface Buyable extends Priced {

@Override double price();

@Override default boolean hasPrice() { return Priced.super.hasPrice() || price() == 0; }

}

In der Schnittstelle Priced sagt der Default-Code von hasPrice() aus, dass alles einen Preis hat, was echt über 0 liegt. Buyable dagegen nutzt eine andere Definition und implementiert daher das Default-Verhalten neu. Denn Buyable definiert hasPrice() so, dass auch ein Preis von 0 letztendlich bedeutet, dass es einen Preis hat. In der Implementierung von hasPrice() greift Buyable auf den Default-Code von Priced zurück, um vom Obertyp eine Entscheidung über die Preiseigenschaft zu bekommen, die aber mit der Oder-Verknüpfung noch verallgemeinert wird.

Abstrakte überschriebe Schnittellenoperationen nehmen Default-Methoden weg

Hintergrund zur Einführung von Default-Methoden war die Notwendigkeit, die Java-API von Schnittstellen im Nachhinein ohne nennenswerte Compilerfehler verändern zu können. Ideal ist, wenn neue Default-Methoden hinzukommen und Standard-Verhalten definieren, und es dadurch zu keinem Compiler-Fehler für implementierende Klassen kommt, oder zu Fehlern bei Schnittstellen, die erweiterte Schnittstellen erweitern.

In unserem Beispiel haben wir eine Schnittstelle Buyable für käufliche Dinge vorgesehen. Dazu müssen die „Dinge“ aber erst einmal einen Preis haben. Führen wir eine weitere Schnittstelle Priced ein:

interface Priced {}

interface Buyable extends Priced {

double price();

}

Die Schnittstelle Prices hat keine Vorgaben, sie soll nur Typen markieren. Nehmen wir an, es gibt viele Klassen, die Priced und auch Buyable implementieren.

In der Entwicklung stellt sich nun heraus, dass Priced auch eine Methode double price() bekommen soll. Kann die Methoden einfach als abstrakte Methode ohne Probleme hinzugekommen werden? Nicht wirklich, denn wenn es viele implementierende Klassen gibt, wird es auch viele Compilerfehler geben. Aber zur problemlosen Erweiterung von Schnittstellen sind Default-Methoden da. Kann also eine Default-Methode price() in Priced aufgenommen werden, also die Schnittstelle nachträglich verändert werden, ohne dass es Compilerfehlen gibt? Ja, das geht.

interface Priced {

default double price() { return -1; }

}

interface Buyable extends Priced {

@Override double price();

}

Interessant sind zwei Details. Zunächst einmal ist in Buyable die Methode weiterhin abstrakt, was bedeutet, eine abstrakte Methode überschreib eine Default-Methode. Klassen, die Buyable implementieren, müssen also weiterhin eine price()-Methode implementieren, wenn sie nicht selbst abstrakt sein wollen. Die nachträgliche Veränderung der Oberschnittstelle hat also keine Auswirkungen auf die anderen Klassen. Im Nachhinein kann aber zur Dokumentation die Annotation @Override and die Unterschnittstelle gesetzt werden. Das ist schon spannend, dass die Implementierung einer Default-Methode weggenommen werden kann. Bei der Sichtbarkeit ist das zum Beispiel nicht möglich: ist eine Methode einmal öffentlich, kann eine Unterklasse die Sichtbarkeit nicht einschränken.

Default-Methoden, Teil 1, Update mit aktueller Java 8 Syntax

Ist eine Schnittstelle einmal verbreitet, so sollte es dennoch möglich sein, Operationen hinzuzufügen. Java 8 bringt dafür eine Sprachänderung mit, die es Entwicklern erlaubt, neue Operationen einzuführen, ohne dass Unterklassen verpflichtet werden, diese Methoden zu implementieren. Damit das möglich ist, muss die Schnittstelle eine Standard-Implementierung mitbringen. Auf diese Weise ist das Problem gelöst, denn wenn eine Implementierung vorhanden ist, haben die implementierenden Klassen nichts zu meckern, und wenn sie das Standardverhalten überschreiben möchten, können sie das gerne machen. Oracle nennt diese Methoden in Schnittstelle mit vordefinierter Implementierung Default-Methoden[1]. Schnittstellen mit Default-Methoden heißen erweiterte Schnittstellen.

Eine Default-Methode unterscheidet sich syntaktisch in zwei Aspekten von herkömmlichen implizit abstrakten Methoden-Deklarationen.

· Die Deklaration einer Default-Methode beginnt mit dem Schlüsselwort default.

· Statt dass ein Semikolon das Ende der Deklaration anzeigt, steht bei einer Default-Methode stattdessen in geschweiften Klammen ein Block mit Implementierung. Die Implementierung wollen wir Default-Code nennen.

Sonst verhalten sich erweiterte Schnittstellen wie normale Schnittstellen. Eine Klasse, die eine Schnittstelle implementiert, erbt alle Operationen, sei es die abstrakten Methoden oder die Default-Methoden. Falls die Klasse nicht abstrakt sein soll muss sie alle von der Schnittstelle geerbten abstrakten Methoden realisieren; sie kann die Default-Methoden überschreiben, muss das aber nicht, denn eine Vorimplementierung ist ja schon gegeben.

Realisieren wir dies in einem Beispiel. Für Spielobjekte soll ein Lebenszyklus möglich sein; der besteht aus start() und finish(). Der Lebenszyklus ist als Schnittstelle vorgegeben, die Spielobjektklasse implementieren können. Version 1 der Schnittstelle sieht also aus:

interface GameLifecycle {

void start();

void finish();

}

Klassen wie Player, Room, Door können die Schnittstellen erweitern, und wenn sie dies tun, müssen sie die beiden Methoden implementieren. Bei Spielobjekten, die diese Schnittstelle implementieren, kann unser Hauptprogramm, das Spiel, diese Methoden aufrufen und den Spielobjekten Rückmeldung geben, ob sie gerade in das Spiel gebracht wurden, oder sie aus dem Spiel entfernt wurden.

Je länger Software lebt, desto mehr bedauern Entwickler Designentscheidungen. Die Umstellung einer ganzen Architektur ist eine Mammutaufgabe, einfache Änderungen wie das Umbenennen sind über ein Refactoring schnell erledigt. Nehmen wir an, auch bei unserer Schnittstelle gibt es einen Änderungswunsch – nur die Initialisierung und das Ende zu melden reicht nicht. Geht das Spiel in einen Pausenmodus, soll ein Spielobjekt die Möglichkeit bekommen, im Hintergrund laufende Programme anzuhalten. Das soll durch eine zusätzliche pause()-Methode in der Schnittstelle realisiert werden. Hier spielen uns die Default-Methoden perfekt in die Hand, denn wir können die Schnittstelle erweitern, aber eine leere Standardimplementierung mitgeben. So müssen Unterklassen die pause()-Methode nicht implementieren, können dies aber; Version 2 der nun erweiterten Schnittstelle GameLifecycle:

interface GameLifecycle {

void start();

void finish();

default void pause() {}

}

Klassen, die GameLifecycle schon genutzt haben, bekommen von der Änderung nichts mit. Der Vorteil: Die Schnittstelle kann sich weiter entwickeln, aber alles bleibt binärkompatibel und nichts muss neu compiliert werden. Vorhandener Code kann auf die neue Methode zurückgreifen, die automatisch mit der Implementierung vorhanden ist. Weiterhin verhalten sich Default-Methoden wie andere Methoden von Schnittstellen auch: es bleibt bei der dynamischen Bindung, wenn implementierende Klassen die Methoden überschreiben. Wenn eine Unterklasse wie Flower zum Beispiel bei der Spielpause nicht mehr blühen möchte, so überschreibt sie die Methode und lässt den Timer pausieren. Eine Tür dagegen hat nichts zu stoppen und kann pause() mit dem Default-Code so übernehmen.

Hinweis

Statt des leeren Blocks könnte der Rumpf auch throw new UnsupportedOperationException("Not yet implemented"); beinhalten, um anzukündigen, dass es keine Implementierung gibt. So führt eine hinzugenommene Default-Methode zwar zu keinem Compilerfehler, aber zur Laufzeit führen nicht überschriebene Methoden zu einer Ausnahme. Erreicht ist das Gegenteil vom Default-Code, weil eben keine Logik standardmäßig ausgeführt wird;das Auslösen einer Ausnahme zum Melden eines Fehlers wollen wir nicht als Logik ansehen.

Kontext der Default-Methoden

Default-Methoden verhalten sich wie Methoden in abstrakten Klassen und können alle Methoden der Schnittstelle (inklusive der geerbten Methoden) aufrufen. Die Methoden werden später dynamisch zur Laufzeit gebunden.

Nehmen wir eine Schnittstelle Buyable für käufliche Objekte:

interface Buyable {
  double price();
}

Leider schreibt die Schnittstelle nicht vor, ob Dinge überhaupt käuflich sind. Eine Methode wie isBuyable() wäre in Buyable ganz gut aufgehoben. Was kann aber die Default-Implementierung sein? Wir können auf price() zurückgreifen und testen, ob die Rückgabe ein gültiger Preis ist. Das soll gegeben sein, wenn der Preis echt größer 0 ist.

interface Buyable {
  double price();

default boolean isBuyable() { return price() > 0; }
}

Implementierende Klassen erben die Methode isBuyable() und beim Aufruf geht der interne Aufruf von price() an genau die Klasse, die Buyable und die Methode implementiert.

Hinweis

Eine Schnittstelle kann die Methoden der absoluten Oberklasse java.lang.Object ebenfalls deklarieren, etwa um mit Javadoc eine Beschreibung hinzuzufügen. Allerdings ist es nicht möglich, mit Default-Code Methoden wie toString() oder hashCode() vorzubelegen.

Neben der Möglichkeit auf Methoden zuzugreifen, steht auch die this-Referenz zur Verfügung. Das ist sehr wichtig, denn so kann der Default-Code an Utility-Methoden weiterreichen und einen Verweis auf sich selbst übergeben. Hätten wir zum Beispiel schon eine isBuyable(Buyable)-Methode in einer Utiltiy-Klasse PriceUtils implementiert, so könnte der Default-Code aus einer einfachen Weiterleitung bestehen:

class PriceUtils {

public static boolean isBuyable( Buyable b ) { return b.price() > 0; }

}

interface Buyable {

  double price();

default boolean isBuyable() { return PriceUtils.isBuyable( this ); }
}

Dass die Methode PriceUtils.isBuyable(Buyable) für den Parameter den Typ Buyable vorsieht und sich der Default-Code mit this auf genau so ein Buyable-Objekt bezieht, ist natürlich kein Zufall, sondern bewusst gewählt. Der Typ der this-Referenz zur Laufzeit entspricht dem der Klasse, die die Schnittstelle implementiert hat und dessen Objektexemplar gebildet wurde.

Haben die Default-Methoden weitere Parameter, so lassen sie auch diese weiter an die statische Methode reichen:

class PriceUtils {

public static boolean isBuyable( Buyable b ) { return b.price() > 0; }

public static double defaultPrice( Buyable b, double defaultPrice ) {

if ( b != null && b.price() > 0 )

return b.price();

return defaultPrice;

}

}

interface Buyable {
  double price();

default boolean isBuyable() { return PriceUtils.isBuyable( this ); }

default double defaultPrice( double defaultPrice ) {

return PriceUtils.defaultPrice( this, defaultPrice ); }
}

Es ist vorzuziehen, die Implementierung auszulagern, um die Schnittstellen nicht so Code-lastig werden zu lassen. Nutzt das JDK Default-Code, so gibt es in der Regel immer eine statische Methode in einer Utility-Klasse.

Neue Möglichkeiten mit Default-Methoden *

Default-Methoden geben Bibliotheksdesignern ganz neue Möglichkeiten. Heute ist noch gar nicht richtig abzusehen, was Entwickler damit machen werden und welche Richtung die Java-API einschlagen wird. Auf jeden Fall wird sich die Frage stellen, ob Standard-Implementierung als Default-Code in Schnittstellen wandert, oder wie bisher, Standard-Implementierungen als abstrakte Klasse bereitgestellt wird, von dem wiederum andere Klassen ableiten. Als Beispiel sei auf die Datenstrukturen verwiesen: Eine Schnittstelle Collection schreibt Standardverhalten vor, AbstractCollection gibt eine Implementierung soweit möglich vor, und Unterklassen wie Listen setzen dann noch einmal auf diese Basisimplementierung auf. Erweiterte Schnittstellen können Hierarchien abbauen, denn auf eine abstrakte Basisimplementierung kann verzichtet werden. Auf der anderen Seite kann aber eine abstrakte Klasse Zustand über Objektvariablen einführen, was eine Schnittstelle nie könnte.

Default-Methoden können aber noch etwas ganz anderes: Sie können als Bauelemente für Klassen dienen. Eine Klasse kann mehrere Schnittstellen mit Default-Methoden implementieren und erbt im Grunde damit Basisfunktionalität von verschiedenen Stellen. In anderen Programmiersprachen ist das als Mixin bzw. Trait bekannt.


[1] Der Name hat sich während der Planung für dieses Feature mehrfach gewandelt. Ganz am Anfang war der Name „defender methods“ im Umlauf, dann lange Zeit virtuelle Erweiterungsmethoden (engl. virtual extension methods).

Java-Versionen gehen mit Unicode-Standard Hand in Hand

In den letzten Jahren hat sich der Unicode-Standard erweitert, und Java ist den Erweiterungen gefolgt.

Java-Version

Unicode-Version

1.0

1.1.5

1.1

2.0

1.1.7

2.1

1.2, 1.3

2.1

1.4

3.0

5

4.0

6

4.0

7

6.0

8

6.2

Java-Versionen und ihr unterstützter Unicode-Standard

Die Java-Versionen von 1.0 bis 1.4 nutzen einen Unicode-Standard, der für jedes Zeichen 16 Bit reserviert. So legt Java jedes Zeichen in 2 Byte ab und ermöglicht die Kodierung von mehr als 65.000 Zeichen aus dem Bereich U+0000 bis U+FFFF. Der Bereich heißt auch BMP (Basic Multilingual Plane). Java 5 unterstützt erstmalig den Unicode 4.0-Standard, der 32 Bit (also 4 Byte) für die Abbildung eines Zeichens nötig macht. Doch mit dem Wechsel auf Unicode 4 wurde nicht die interne Länge für ein Java-Zeichen angehoben, sondern es bleibt dabei, dass ein char 2 Byte groß ist. Das heißt aber auch, dass Zeichen, die größer als 65.536 sind, irgendwie anders kodiert werden müssen. Der Trick ist, ein ein »großes« Unicode-Zeichen aus zwei chars zusammenzusetzen. Dieses Pärchen aus zwei 16-Bit-Zeichen heißt Surrogate-Paar. Sie bilden in der UTF-16-Kodierung ein Unicode 4.0-Zeichen. Diese Surrogate vergrößern den Bereich der Basic Multilingual Plane.

Mit der Einführung von Unicode 4 unter Java 5 gab es an den Klassen für Zeichen- und Zeichenkettenverarbeitung einige Änderungen, sodass etwa eine Methode, die nach einem Zeichen sucht, nun nicht nur mit einem char parametrisiert ist, sondern auch mit int, und der Methode damit auch ein Surrogate-Paar übergeben werden kann. In diesem Buch spielt das aber keine Rolle, da Unicode-Zeichen aus dem höheren Bereichen, etwa für die phönizische Schrift, die im Unicode-Block U+10900 bis U+1091F liegt – also kurz hinter 65536, was durch 2 Byte abbildbar ist –, nur für eine ganz kleine Gruppe von Interessenten wichtig sind.

Statische sum(…)/max(…)/min(…) Methoden in numerischen Wrapper-Klassen

In den numerischen Wrapper-Klassen, also Byte, Short, Integer, Long, Float, Double und auch Character – obwohl Character nicht die Basisklasse Number erweitert – gibt es seit Java 8 je drei neue Methoden: sum(…)/max(…)/min(…), die genau das machen, was der Methodenname verspricht.

final class java.lang.Byte|Short|Integer|Long|Float|Double
extends Number
implements Comparable<Integer>

§ static Typ sum(Typ a, Typ b)
Bildet die Summe zweier Werte und liefert diese zurück. Es entspricht einem einfachen a + b. Die Angabe Typ steht dabei für den entsprechenden primitiven Typ byte short, int, long, float oder double, etwa in int sum(int a, int b).

§ static Typ min(Typ a, Typ b)
Liefert das Minimum der zwei Zahlen.

§ static Typ max(Typ a, Typ b)
Liefert das Maximum der zwei Zahlen.

final class java.lang.Character
implements Comparable<Character>, Serializable

§ static Typ sum(Typ a, Typ b)
Liefert (char)(a + b) zurück.

§ static Typ min(Typ a, Typ b)
Liefert das kleinere der beiden Zeichen bezüglich der Unicode-Position.

§ static Typ max(Typ a, Typ b)
Liefert das größere der beiden Zeichen.

Die Methoden sich für sich genommen nicht spannend. Für die Summe (Addition) tut es genauso gut der +-Operator – er steckt sowieso hinter den sum(…)-Methoden – und so wird keiner auf die Idee kommen i = Integer.sum(i, 1) statt i++ zu schreiben. Für das Maximum/Minimum bietet die Math-Klasse auch schon entsprechende Methoden min(a,b)/max(a,b). Der Grund für diese drei Methoden ist vielmehr, dass sie im Zusammenhang mit Lambda-Ausdrücken interessant sind – dazu später mehr.

Java 8: Division mit Rundung Richtung negativ unendlich

Die Ganzzahldivision in Java ist simpel gestrickt. Vereinfacht ausgedrückt: Konvertiere die Ganzzahlen in Fließkommazahlen, führe die Division durch und schneide alles hinter dem Komma ab. So ergeben zum Beispiel 3 / 2 = 1 und 9 / 2 = 4. Bei negativem Ergebnis, durch entweder negativen Dividenden oder Divisor, das gleiche Spiel: -9 / 2 = -4 und 9 / -2 = -4. Schauen wir uns einmal die Rundungen an.

Ist das Ergebnis einer Division positiv und mit Nachkommaanteil, so wird das Ergebnis durch das Abschneiden der Nachkommastellen ein wenig kleiner, also abgerundet. Wäre 3/2 bei Fließkommazahlen 1,5, ist es bei einer Ganzzahldivision abgerundet 1. Bei negativen Ergebnissen einer Division ist das genau anders herum. Denn durch das Abschneiden der Nachkommastellen wird die Zahl etwas größer. -3/2 ist genau genommen -1,5, aber bei der Ganzzahldivision -1. Doch -1 ist größer als -1,5. Java wendet ein Verfahren an, was gegen null rundet.

In Java 8 hat die Mathe-Klasse zwei neue Methoden bekommen, die bei negativem Ergebnis einer Division nicht gegen null runden, sondern gegen negativ unendlich, also auch in Richtung der kleineren Zahl, wie es bei den positiven Ergebnissen ist.

class java.lang.Math

– static int floorDiv(int x, int y)

– static long floorDiv(long x, long y)

Ganz praktisch heißt das: 4/3 = Math.floorDiv(4, 3) = 1, aber wo -4 / 3 = -1 ergibt, liefert Math.floorDiv(-4, 3) = -2.

Wie kann ein Java-Compiler in Java implementiert sein?

Der Java-Compiler von Oracle und der Java-Compiler der Entwicklungsumgebung Eclipse sind selbst in Java implementiert und generieren diesen Bytecode (es gibt aber auch Java-Compiler in C++, wie den Jikes-Compiler. Natürlich gibt es da ein Henne-Ein-Problem: wie sollte ein neuer in Java geschriebener Compiler durch Java übersetzt werden? Daher entsteht der erste Compiler immer in einer anderen Sprache, und die übersetzt eine kleine Teilmenge der Zielsprache, und dann wird ein neuer Compiler in der Minisprache entwickelt. Im nächsten Schritt wachsen und vergrößern sich Grammatik und Compiler. In der Sprache der Compilerbauer heißt der Prozess Bootstrapping. Bei Java war das ein Prozess über mehrere Stufen. Patrick Naughton schreibt im Buch The Java handbook dazu: “Arthur van Hoff rewrote the compiler in Oak itself, replacing the C version that James originally wrote.“

Tiefe Objektkopien mit JAXB und JAXBSource

Die unmarshal(…)-Methoden ist überladen mit Parametern, die typische Datenquellen repräsentieren, etwa Dateien oder Eingabeströme wie ein Reader. Allerdings sind noch andere Parametertypen interessant, und es lohnt sich, hier einmal in die API-Dokumentation zu schauen. Ein spannender Typ ist javax.xml.transform.Source, beziehungsweise die Implementierung der Schnittstelle durch JAXBSource. JAXBSource ist die Quelle, aus denen JAXB seine Informationen bezieht, um ein neues Java-Objekt zu rekonstruieren.

Das nächste Beispiel nimmt sich ein Objekt room als Basis und erzeugt eine tiefe Kopie davon:

Room room = …

JAXBContext context = JAXBContext.newInstance( Room.class );

Unmarshaller unmarshaller = context.createUnmarshaller();

JAXBSource source = new JAXBSource( context, room );

Room copiedRoom = Room.class.cast( unmarshaller.unmarshal( source ) );

System.out.println( copiedRoom.getPlayers() ); // [com.tutego.insel.xml.jaxb.Player@…]

Das Beispiel zeigt somit, wie sich mit Hilfe von JAXB Objektkopien erzeugen lassen.

Wie funktioniert eigentlich invokedynamic?

Bei invokedynamic sind viel weniger Typinformationen nötig wie bei den anderen vier existierenden Bytecodes für Methodenaufrufe. Generiert wird der Bytecode zum Beispiel von Skriptsprachen, wenn Typinformationen fehlen. Die Compiler der Skriptsprachen nutzen Bytecode-Bibliotheken wie ASM (http://asm.ow2.org/) und umgehen den Java-Compiler zur Erstellung der Klassendateien.

Der neue Bytecode ist die eine Seite. Aber wenn die Typinformationen fehlen, insbesondere der wichtige Empfänger (wie PrintStream bei println()), wie kommt die JVM zur wirklichen Implementierung? Etwa bei unserem Beispiel von isEmpty(s), in dem es ein invokedynamic-Aufruf von s.length() gibt. Der Aufruf muss ja irgendwo landen.

function isEmpty(s) { return s == null || s.length() == 0; }

Zwei Dinge sind hier zusätzlich nötig. Das erste ist, dass es neben dem Aufruf von invokedynamic noch eine zusätzliche Information im Bytecode gibt, nämlich von einer Bootstrap-Methode. Findet die JVM zum ersten Mal ein invokedynamic, dann weiß sie nicht, an wen der Aufruf geht und wendet sich an die Bootstrap-Methode. Zur passenden Auswahl der Zielmethode übergibt die JVM an die Bootstrap-Methode Informationen über Aufrufer und Name, sodass alle wichtigen Informationen zur Auswahl des Ziels vorhanden sind. Hier sind wir nun in der zweiten Hälfte, denn zusätzlich zum neuen Bytecode gibt es ein neues Paket java.lang.invoke. Die Bootstrap-Methode spezifiziert mit der API des neuen Pakets die Zielmethode und gibt diese an die JVM weiter. Wenn das einmal geschehen ist, ist der Aufruf gebunden und die JVM ruft beim dynamischen Aufruf direkt die vom Bootstrap gelieferte Methode auf. Der genaue Ablauf bei den invokedynamic-Aufrufen dokumentiert das Paket.

Der neue Bytecode muss von einer Java 7-JVM unterstützt werden und wird von dynamischen Skriptsprachen generiert, um die Aufrufe schnell von der JVM ausführen zu lassen. Normale Entwickler werden den neuen Bytecode kaum brauchen, und im Quellcode ist er eh nicht sichtbar.

Welche Suppress-Warnings gibt es?

Neben den von Generics kommenden Kennungen rawtype und unchecked gibt es weitere, die allerdings nicht sonderlich gut dokumentiert sind. Das liegt auch daran, dass Meldungen während der Programmübersetzung zur Compilerinfrastruktur gehören und nicht zur Laufzeitumgebung, und damit nicht zur traditionellen Java-API. Der Compiler kann im Prinzip beliebige Codeanalysen beliebiger Komplexität vornehmen und bei vermuteten Fehlern Alarm schlagen. Um wir dürfen auch nicht vergessen, dass es nur Warnungen sind: wer als Programmierer alles richtig macht, wird die Meldungen nicht zu Gesicht bekommen. Dennoch ist es relevant, sie zu kennen, denn der Compiler wird manches Mal etwas anmerken, was Entwickler bewusst nutzen wollen, und dann gilt es, die Meldungen abzuschalten.

Die Macher vom Eclipse-Compiler (JDT) dokumentieren die unterstützten Warnungen unter http://help.eclipse.org/juno/index.jsp?topic=%2Forg.eclipse.jdt.doc.user%2Ftasks%2Ftask-suppress_warnings.htm. Neben den aufgeführten Meldungen all, rawtype und unchecked sind folgende noch interessant:

@SuppressWarnings

Unterdrückt Meldungen für

deprecation

veraltete Elemente, wie new java.util.Date(2012-1970, 3, 3).

incomplete-switch

ausgelassene Aufzählungen in swich-case-Anweisungen.

resource

ein nicht geschlossenes AutoClosable, wie new java.util.Scanner(System.in).nextLine().

serial

eine serialisierbare Klasse, die keine Serialisierung-ID besitzt.

unused

nicht benutzte Elemente, etwa nicht aufgerufene private Methoden.

Einige Werte von @SuppressWarnings

BridJ

BridJ verfolgt den gleichen Ansatz wie JNA, kann also auch aus Java heraus nativen Code von C(++) und auch Objective-C ansprechen. Nur steht es als quelloffene Bibliothek nicht unter der LGPL, sondern unter der BSD/Apache-Lizenz. Zudem ist es aktueller, nutzt Generics (was es abhängig von Java 5 macht, JNA benötigt nur 1.4) und diverse Tricks, um noch performantere native Aufrufe zu realisieren. Auch kann es besser mit den Eigenschaften von C++ umgehen, wie virtuellen Methodenaufrufen, Vererbung und Templates „verstehen“. Regelmäßige Updates gibt es für die Systeme Windows (x86, x64), Linux (x86, x64, arm32), Mac OS X (x86, x64, PPC), Solaris 10 (x86, bald x64 und SPARC), Android 3.0 (ARMv5) und experimentell auf jailbroken iOS iPhones (ARM). Ob das ältere JNA oder BridJ besser ist, muss im Einzelfall evaluiert werden. JNAerator unterstützt ebenfalls BidJ zur Generierung der typisierten Schnittstellen und Übergabeobjekten.

JNA (Java Native Access)

Eine Bibliothek zum Ansprechen dynamischer Bibliotheken ohne JNA-Kontakt und damit ohne C(++)-Compiler ist JNA (Java Native Access). Beheimatet ist das Projekt unter https://github.com/twall/jna/  und es steht unter der Lizenzform LGPL. JNA benötigt nur das Archiv jna.jar im Klassenpfad und geht dann fast von selbst zur dynamischen Bibliotheken. Verschiedene Plattformen werden unterstützt, dazu zählen Windows, Linux und Mac OS X.

Zur dem Zugriff müssen allerdings Java-Schnittstellen als typisierte Versionen der nativen Funktionen deklariert werden, damit auch die Datentypen von der Java-Seite automatisch auf Datentypen der nativen Seite gemappt werden können. Die Dokumentation unter https://github.com/twall/jna/blob/master/www/GettingStarted.md  eigt ein Beispiel (hier etwas gekürzt):

import com.sun.jna.*;

public class HelloWorld {
  public interface CLibrary extends Library {
    CLibrary INSTANCE = (CLibrary) Native.loadLibrary( Platform.isWindows() ? "msvcrt" : "c",
                                                       CLibrary.class );
    void printf( String format, Object... args );
  }

  public static void main( String[] args ) {
    CLibrary.INSTANCE.printf( "Hello, World\n" );
    for ( int i = 0; i < args.length; i++ )
      CLibrary.INSTANCE.printf( "Argument %d: %s\n", i, args[ i ] );
  }
}

Die Übertragung der Funktionen von der C-Seite in Java-Schnittstellen – wie CLibrary in dem Beispiel – kann auch mit JNAerator (http://code.google.com/p/jnaerator/) automatisiert werden.

Snippet: List all MessageDigest provider

Pattern digestPattern = Pattern.compile( "^(Alg\\.Alias\\.)?MessageDigest\\.(?<alg>(\\w|-)+)" );
for ( Provider p : Security.getProviders() ) {
  for ( Object o : Collections.list( p.keys() ) ) {
    for ( Matcher m = digestPattern.matcher( o.toString() ); m.find(); )
      System.out.println( m.group("alg") );
  }
}

The result is:

SHA-1 SHA MD5 SHA-384 SHA-512 SHA1 SHA MD5 SHA-256 MD2

Also look at http://download.java.net/jdk8/docs/technotes/guides/security/StandardNames.html.

Unicode-Skripte

Neben den Unicode-Blöcken gibt es noch ein anderen Konzept, was vielleicht noch wichtiger als die Blöcke selbst sind: Unicode-Skripte. Darunter ist eine Gruppe von Zeichen für eine Sprache (in der Regel) zu verstehen.[1] Um den Unterschied zu Blöcken noch einmal zusammenzufassen:

· Ein Unicode-Block ist ein einfacher von-bis-Bereich, und Stellen können für die spätere Belegung leer sein.

· Ein Unicode-Skript enthält keine freien Positionen.

· Zeichen eines Unicode-Skripts können aus verschiedenen Unicode-Blöcken stammen.

· Zeichen aus einem Block können in verschiedenen Unicode-Skripten auftauchen.

Auch für Skripte bietet Java in der Klasse Character ein öffentliches statische innere Attribut, wobei das erst seit Java 7 existiert und Oracle hier ein moderneres enum gewählt hat. Character.UnicodeScript besteht aus einer Reihe von Konstanten, wobei die Namen nach ISO 15924[2] gewählt sind. Auch gibt es eine of(int) Methode um das Skript für ein Zeichen zu erfragen und ein UnicodeScript-Objekt kann über forName(String) mit einem Namen aufgebaut werden.

Auch wenn die Character und String-Klasse arm an weiteren Methoden zu den Unicode-Blöcken und Unicode-Skripten ist, gibt es doch Unterstützung von ganz anderer Stellen: Von den regulären Ausdrücken. Sie können testen, ob Zeichen in gewissen Skripten oder Blöcken sind und damit Zeichen in Teilstrings aufspüren, sie löschen und entfernen. (Wir springen nun thematisch etwas vor.) Die Syntax in den regulären Ausdrücken ist \p{script=Skriptname} bzw. \p{block=Blockname} (auch der Präfix Is oder In ist erlaubt ohne Nutzung vom script=/block=-Konstrukt). Achtung, Nicht alle Unicode-Skripts werden unterstützt!

Beispiel

Teste drei Zeichen, ob sie arabisch sind:

System.out.println( "ح".matches( "\\p{script=Arabic}" ) ); // true

System.out.println( "ح".matches( "\\p{IsArabic}" ) ); // true

System.out.println( "ש".matches( "\\p{IsArabic}" ) ); // false

System.out.println( "1".matches( "\\p{IsArabic}" ) ); // false


[1] Siehe auch http://www.unicode.org/reports/tr24/.

[2] http://unicode.org/iso15924/iso15924-codes.html

Unicode-Blöcke

Unicode-Zeichen gehören immer Blöcken an und bei denen ist teilweise immer etwa Platz, um nachrückende Zeichen noch aufnehmen zu können. Beim lateinischen Alphabet ist das nicht so wichtig, wohl aber bei mathematischen Sonderzeichen oder anderen Symbolen.

Die Klasse Character deklariert eine öffentliche statische finale Klasse UnicodeBlock mit einer Vielzahl von Unicode-Blöcken, die als öffentliche statische Variablen in UnicodeBlock deklariert sind und selbst vom Typ UnicodeBlock sind. Character.UnicodeBlock.BASIC_LATIN ergibt zum Beispiel so einen Block, allerdings ist der Typ nicht so ausdrucksstark, nur der Name kommt bei einem toString() dabei raus, aber nicht etwa in welchem Bereich die Zeichen liegen. Auch fehlt die Möglichkeit alle Zeichen aufzuzählen oder zu testen, ob ein Zeichen im Block liegt. Was jedoch der Typ UnicodeBlock bietet sind zwei statische Methoden of(int) und of(char), die als Fabrikfunktionen einen UnicodeBlock für ein gewisses Zeichen geben. Der ist-ein-Element-von-Test lässt sich also damit indirekt realisieren.

Beispiel. Gib die Namen der Unicode-Blöcke für einige Zeichen aus:

UnicodeBlock basicLatin = Character.UnicodeBlock.BASIC_LATIN;

System.out.println( basicLatin );

System.out.println( Character.UnicodeBlock.of( ‚ß‘ ) );

System.out.println( Character.UnicodeBlock.of( ‚\u263A‘ ) );

System.out.println( Character.UnicodeBlock.of( ‚\u20ac‘ ) );

System.out.println( Character.UnicodeBlock.of( 0x1D15E ) );

Das liefert BASIC_LATIN LATIN_1_SUPPLEMENT MISCELLANEOUS_SYMBOLS

CURRENCY_SYMBOLS MUSICAL_SYMBOLS.

Das Wissen um den Bereich ist immer hilfreich dann, wenn ein unbekannter Text zugeordnet werden soll, denn auf diese Weise lässt sich erahnen, ob der Text zum Beispiel auf lateinischen Buchstaben basiert, er arabisch, chinesisch oder japanisch (Kanji/Kana) ist.

Über eine Reihe von Werten laufen, Ist-Element-von-Test

Rechts vom Doppelpunkt lässt sich auf die Schnelle ein Feld aufbauen, über welches das erweiterte for dann laufen kann.

for ( int prime : new int[]{ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 } )
  System.out.println( prime );

Das ist praktisch, um über eine feste Menge von Werten zu laufen. Das funktioniert auch für Objekte, etwa Strings:

for ( String name : new String[]{ "Krissy", "Morris", "Dan" } )
  System.out.println( name );

Einige Programmierer verstecken die Objekterzeugung auch in einen Methodenaufruf:

for ( String name : Arrays.asList( "Krissy", "Morris", "Dan" ) )
  System.out.println( name );

Arrays.asList(…) erzeugt kein Array als Rückgabe, sondern baut aus der variablen Argumentliste eine Sammlung auf, die von einem speziellen Typ Iterable ist – das kann auch die erweiterte for-Schleife ablaufen.

Unabhängig vom erweiterten for hat die Nutzung von Arrays.asList(…)noch einen anderen Vorteil, etwa bei ist-Element-von-Anfragen, etwa so:

if ( Arrays.asList( 1, 2, 3, 4, 5, 6, 7, 8, 10 ).contains( number ) )

  …