Kollisionen und Hash-Funktionen

Die Wahl der richtigen Hash-Funktion ist wichtig für die Performance. Denn eine »dumme« Hash-Funktion, die beispielsweise alle Schlüssel nur auf einen konstanten Wert abbildet, erreicht keine Verteilung, sondern lediglich eine lange Liste von Schlüssel-Werte-Paaren; diese Anhäufung an einer Stelle nennt sich Clustering. Doch auch bei der besten Verteilung über N Buckets ist nach dem Einfügen des Elements N + 1 irgendwo eine Liste mit mindestens zwei Elementen aufgebaut, daher vergrößert die Bibliothek standardmäßig das Feld. Ist aber die Hash-Funktion aber so schlecht, dass alles auf das gleiche Bucket abgebildet wird, hilft dieses Re-Hashing auch nicht.

Je länger die Datenstruktur der miteinander kollidierenden Einträge wird, desto langsamer wird der Zugriff der auf Hashing basierenden Datenstruktur insgesamt. Java basiert beim Hashing einzig auf der hashCode()-Methode, und es liegt in unserer Hand sie gut zu implementieren. Die Klasse String hat eine relativ einfache (dafür schnelle) Implementierung von hashCode(), die in der Vergangenheit zu Denial of Service Attacken führte gerade weil es zu Kollisionen kam.[1]


[1] In Java 7 wurde dafür in String eine zusätzliche Hash-Methode realisiert, doch in Java 8 diese Implementierung wieder entfernt und die Implementierung für das Hashing insgesamt verändert, zumindest für die aktuellen Klassen, das ältere java.util.Hashtable blieb unverändert.

Schnittstelle Map.Entry und Updates in Java 8

Während keySet() nur die eindeutigen Schlüssel in einer Menge liefert und die assoziierten Elemente in einem zweiten Schritt geholt werden müssten, gibt entrySet() ein Set von Elementen typisiert mit Map.Entry zurück. Entry ist eine innere Schnittstelle von Map, die eine API zum Zugriff auf Schlüssel-Werte-Paare deklariert. Die wichtigen Operationen dieser Schnittstelle sind getKey(), getValue() und setValue(), wobei die letzte Methode optional ist, aber etwa von HashMap angeboten wird. Neben diesen Methoden überschreibt Entry auch hashCode() und equals(…).

Beispiel: Laufe die Elemente HashMap als Menge von Map.Entry-Objekten ab:

for ( Map.Entry<String, String> e : h.entrySet() )
System.out.println( e.getKey() + „=“ + e.getValue() );

Map.Entry ist eher ein Interna und die Objekte dienen nicht der langfristen Speicherung. Ein entrySet() ist eine Momentaufnahme und das Ergebnis sollte nicht referenziert werden, denn ändert sich der darunterliegende Assoziativspeicher, ändern sich auch die Entry-Objekt und das Set<Map.Entry> als ganzes ist vielleicht nicht mehr gültig. Entry-Objekt sind nur gültig im Moment der Iteration, was den Nutzen eingeschränkt. Daher ist die Rückgabe von entrySet() mit Set<Map.Entry<K,V>> auch relativ unspezifisch, um was für eine Art von Set es sich genau handelt; ob HashSet oder vielleicht NavigableSet spielt keine Rolle.

Auch wenn die Map.Entry-Objekte nicht für die Speicherung gedacht sind, können Sie in Java 8 in einem Strom von Daten verarbeitet und in einer zustandsbehafteten Operation sortiert werden. Der Bonus der Entry-Objekte im Strom ist einfach, dass es von Vorteil ist, Schüssel und Wert in einem Objekte gekapselt zu sehen. Aber was ist, wenn jetzt der Strom sortiert werden soll, etwa nach dem Schlüssel, oder dem Wert? Hier kommen neue Methoden von Java 8 ins Spiel, die den nötigen Comparator liefern.

Beispiel: Erfrage eine Menge von Entry-Objekten und sortiere sie nach dem assoziierten Wert:

map.entrySet()

.stream()

.sorted( Map.Entry.<String, String>comparingByValue() )

.forEach( System.out::println );

 

static interface Map.Entry<K,V>

§ static <K extends Comparable<? super K>,V> Comparator<Map.Entry<K,V>> comparingByKey()

§ static <K,V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue()

§ static <K,V> Comparator<Map.Entry<K,V>> comparingByKey(Comparator<? super K> cmp)

§ static <K,V> Comparator<Map.Entry<K,V>> comparingByValue(Comparator<? super V> cmp)

String-Repräsentation, Gleichheitstest, Hashwert und Klon eines Assoziativspeicher

toString() auf Assoziativspeichern liefert eine Zeichenkette, die den Inhalt der Sammlung aufzeigt. Die Stringrepräsentation liefert jeden enthaltenen Schlüssel, gefolgt von einem Gleichheitszeichen und dem zugehörigen Wert. Entwickler sollten nie diese Zeichenkennung parsen bzw. irgendwelche Annahmen über die Formatierung machen.

Beispiel: Ein Assoziativspeicher soll die Zahlen 1, 2, 3 jeweils mit ihrem Quadrat assoziieren. Zum Aufbau benutzen wir eine fortgeschrittene Technik aus Java 8.

Map<Integer, Integer> map = Arrays.asList( 1, 2, 3 )

.stream()

.collect( Collectors.<Integer, Integer, Integer>toMap( id -> id, id -> id*id) );

System.out.println( map/*.toString*/ ); // {1=1, 2=4, 3=9}

Aus Object überschreiben die Standardimplementierungen die Methoden equals(…) und hashCode().

Die Klassen HashMap (und Unterklasse LinkedHashMap), IdentityHashMap, TreeMap, ConcurrentSkipListMap und EnumMap deklarieren eine öffentliche clone()-Methode, die eine Kopie eines Assoziativspeichers erzeugt. Die Kopie bezieht sich allerdings nur auf den Assoziativspeicher selbst; die Schlüssel- und Wert-Objekte teilen sich Original und Klon. Diese Form der Kopie nennt sich auch flache Kopie (engl. shallow copy). Eine Veränderung an den enthaltenen Schlüssel-Werte-Objekten betrifft also immer beide Datenstrukturen, und eine unsachgemäße Modifikation kann zu Unregelmäßigkeiten im Original führen. Eine ConcurrentHashMap oder WeakHashMap unterstützt kein clone(), und eigentlich ist clone() überhaupt nicht nötig, denn die Konstrukturen der Datenstrukturen können immer eine andere Datenstruktur als Vorlage nehmen, etwa clone = new ConcurrentHashMap(existingMap).

Map-Operationen in Abhängigkeit von (nicht-)existierenden Werten in Java 8

Die Map-API hat seit Java 8 einige clevere Methoden, die mehrere Operationen zusammenfassen, wobei die Funktionsweise folgendem Bauplan entspricht: ist ein assoziierter Wert zu einem Schlüssel (nicht) vorhanden, dann tue dies, sonst das.

interface java.util.Map<K,V>

§ default V putIfAbsent(K key, V value)
Testet zuerst, ob es zu dem gegeben Schlüssel key einen assoziierten Wert existiert und wenn ja, gibt es keine Änderung an der Datenstruktur, nur der alte assoziierte Wert wird zurückgegeben. Existiert kein assoziierter Wert, speichert die Datenstruktur zum Schlüssel den value. Die Rückgabe von putIfAbsent(…) ist null, falls es vorher keinen alten assoziierten Wert gab, andernfalls die Referenz vom alten Objekt (was auch null ein kann, wenn die Map auch null-Werte erlaubt), was jetzt durch den neuen Wert überschreiben wurde. Falls null als Wert in der Map erlaubt ist – wie etwa in HashMap – so gilt eine Besonderheit: ist ein existierender Schlüssel mit null assoziiert, dann würde putIfAbsent(…) den Wert null mit etwas anderem überschreiben.

§ default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
Vergleichbar mit putIfAbsent(…), nur nutzt diese Methode eine Berechnungsmethode statt einen festen Wert. Ein wichtiger Punkt ist, dass wenn die Berechnungsfunktion null zurückgibt, nichts an dem Assoziativspeicher verändert wird und der alte Wert bleibt. Der Rückgabewert ist immer entweder der letzte assoziierter Wert oder der neue Eintrag, es sein denn, die mappingFunction lieferte null. Die Methode lässt sich damit perfekt in einer Methodenkaskadierung verwenden der Art map.computeIfAbsent(…).methodeVonV(…).

§ default V computeIfPresent(K key, BiFunction<? super K, ? super V,? extends V> remappingFunction)
Überschreibt den assoziierten Wert mit einem von der Funktion berechneten neuen Wert, wenn das Schlüssel/Werte-Paar existiert. Dann liefert die Methode denen neuen Wert zurück. Zwei Sonderfälle sind zu unterscheiden. Falls es zu dem Schlüssel key keinen Wert gibt macht computeIfPresent(…) nichts und die Rückgabe ist null. Gibt es einen assoziierten Wert, doch die auf den Wert angewendete Funktion liefert null, wird das Schlüssel/Werte-Paar gelöscht und die Rückgabe ist ebenfalls null.

Beispiel. Java bietet von Haus aus keine Datenstruktur, die wie ein Assoziativspeicher arbeitetet, aber einen Schlüssel mit einer Sammlung von Werten assoziieren kann. Doch so eine Klasse ist schnell geschrieben:

class Multimap<K, V> {

private final Map<K, Collection<V>> map = new HashMap<>();

public Collection<V> get( K key ) {

return map.getOrDefault( key, Collections.<V> emptyList() );

}

public void put( K key, V value ) {

map.computeIfAbsent( key, k -> new ArrayList<>() )

.add( value );

}

}

Ein kleines Beispiel:

Multimap<Integer, String> mm = new Multimap<>();

System.out.println( mm.get( 1 ) ); // []

mm.put( 1, „eins“ );

System.out.println( mm.get( 1 ) ); // [eins]

mm.put( 1, „one“ );

System.out.println( mm.get( 1 ) ); // [eins, one]

interface java.util.Map<K,V>

§ default V merge(K key, V value, BiFunction<? super V, ? super V,? extends V> remappingFunction)
Setzt einen neuen Eintrag in die Map oder verschmilzt einen existierenden Eintrag mit der angegebenen Funktion. Von der Semantik her ist das die komplexeste Methode. Der erste Fall einfach, denn wenn es zum Schlüssel kein Wert gibt; dann wird das Schlüssel-/Werte Paar in die Map gesetzt und merge(…) liefert als Rückgabe den value. Gibt es schon einen assoziierten Wert, wird die Funktion mit dem alten Wert und value auf aufgerufen (eine BiFunction hat zwei Parameter) und der alte assoziierte Wert mit diesem neuen Wert überschrieben und die Rückgabe von merge(…) liefert diesen neuen Wert. Jetzt gibt es noch zwei Sonderfälle, und die hängen damit zusammen wenn das Argument value gleich null oder die Funktion null liefert. In beiden Fällen wird das Schlüssel/Wert-Paar gelöscht und die Rückgabe von merge(…) ist null.

Beispiel: Zu einer ID soll ein Punkt assoziiert werden. Neue hinzugefügte Punkte zu dieser ID sollen die Koordinate des ursprünglichen Punktes verschieben.

Map<Integer, Point> map = new HashMap<>();

BiFunction<? super Point, ? super Point, ? extends Point> remappingFunc =

(oldVal, val) -> { val.translate( oldVal.x, oldVal.y ); return val; };

map.merge( 1, new Point( 12, 3 ), remappingFunc );

System.out.println( map.get( 1 ) ); // java.awt.Point[x=12,y=3]

map.merge( 1, new Point( -2, 2 ), remappingFunc );

System.out.println( map.get( 1 ) ); // java.awt.Point[x=10,y=5]

map.merge( 1, new Point( 0, 5 ), remappingFunct );

System.out.println( map.get( 1 ) ); // java.awt.Point[x=10,y=10]

Strom von Zufallszahlen generieren

Sind mehrere Zufallszahlen nötig, ist eine Schleife mit wiederholten Aufrufen von nextXXX() nicht nötig; stattdessen gibt es in Random zwei Sorten von Methoden, die ein Bündel von Zufallszahlen liefern. Als erstes:

§ void nextBytes(byte[] bytes)
Füllt das Feld bytes mit Zufallsbytes auf.

Neu ab Java 8 sind Methoden, die einen Stream von Zufallszahlen liefern:

§ IntStream ints(…)

§ LongStream longs(…)

§ DoubleStream doubles(…)

Beispiel: Liefere 10 zufällige Zahlen, die vermutlich Primzahlen sind:

LongStream stream = new Random().longs()

.filter( v -> BigInteger.valueOf( v ).isProbablePrime(5) );

stream.limit( 10 ).forEach( System.out::println );

Die Methoden ints(…), longs(…) und doubles(…) gibt es in drei Spielarten.

Parametrisierung

Erklärung

IntSteam ints()

Liefert unendlichen Strom von Zufallszahlen im kompletten Wertebereich der Primitiven

LongStream longs()

DoubleStream doubles()

ints(long streamSize)

Liefert einen Strom mit streamSize Zufallszahlen

longs(long streamSize)

double(long streamSize)

ints(int randomNumberOrigin, int randomNumberBound)

longs(long randomNumberOrigin, long randomNumberBound)

doubles(double randomNumberOrigin, double randomNumberBound)

Liefert einen unendlichen Strom vom Zufallszahlen mit Werten im Bereich randomNumberOrigin (inklusiv) bis randomNumberBound (exklusiv)

ints(int randomNumberOrigin, int randomNumberBound)

longs(long randomNumberOrigin, long randomNumberBound)

doubles(double randomNumberOrigin, double randomNumberBound)

ints(long streamSize, int randomNumberOrigin, int randomNumberBound)

Liefert einen Strom mit streamSize Zufallszahlen an der Zahl mit Werten im Bereich randomNumberOrigin (inklusiv) bis randomNumberBound (exklusiv)

longs(long streamSize, long randomNumberOrigin, long randomNumberBound)

doubles(long streamSize, double randomNumberOrigin, double randomNumberBound)

Stream-Methoden der Random-Klasse

Beispiel: Gib 5 Fließkomma-Zufallszahlen im Bereich von 10 bis 20 aus.

new Random().doubles(5, 10, 20).forEach( System.out::println );

Beispiel ListResourceBundle

Ein Resource-Bundle ohne Dateien, realisiert als ListResourceBundle, kann so aussehen:

com/tutego/insel/bundle/MonthResourceBundle_de_DE.java, MonthResourceBundle_de_DE

public class MonthResourceBundle_de_DE extends ListResourceBundle {

private static final String[] MONTHS = {

„Jan“, „Feb“, „Mrz“, „Apr“, „Mai“, „Jun“, „Jul“, „Aug“, „Sep“, „Okt“, „Nov“, „Dez“

};

private static final Object[][] contents = {

{ „jan“, MONTHS[0] },

{ „month“, MONTHS }

};

@Override

protected Object[][] getContents() {

return contents;

}

}

Die Nutzung der Klasse ist wie folgt:

com/tutego/insel/bundle/MonthResourceBundleDemo.java, main()

ResourceBundle bundle = ResourceBundle.getBundle( „com.tutego.insel.bundle.MonthResourceBundle“ );

System.out.println( bundle.getString( „jan“ ) ); // Jan

System.out.println( Arrays.toString( bundle.getStringArray( „month“ ) ) ); // [Jan, Feb, …

System.out.println( Collections.list( bundle.getKeys() ) ); // [month, jan]

In diesem Fall lädt ResourceBundle.getBundle(…) keine Property-Datei, sondern eine Klasse im Klassenpfad. Die API von ResourceBundle bietet auch eine Methode getStringArray(), die ein Feld zurückgibt, jedoch ist das bei Ressourcen-Dateien nicht nötig, nur bei Ressourcen, die „von Hand“ programmiert wurden, wie in unserem Beispiel.

Parallele Berechnung von Präfixen über Arrays-Klasse in Java 8

Stehen mehrere Prozessoren bzw. Kerne zur Verfügung können einige Berechnungen bei Feldern parallelisiert werden. Eine Algorithmus nennt sich parallele Präfix-Berechnung und basiert auf der der Idee, dass eine assoziative Funktion – nennen wir sie f – auf eine bestimmte Art und Weise auf Elemente eines Feldes – nennen wir es a – angewendet wird, nämlich so:

· a[0]

· f(a[0], a[1])

· f(a[0], f(a[1], a[2]))

· f(a[0], f(a[1], f(a[2], a[3])))

· …

· f(a[0], f(a[1], … f(a[n-2], a[n-1])…))

In der Aufzählung sieht das etwas verwirrend aus, daher soll ein praktisches Beispiel zum Verständnis anregen. Das Feld sei [1, 3, 0, 4] und die binäre Funktion die Addition.

Index

Funktion

Ergebnis

0

a[0]

1

1

a[0] + a[1]

1 + 3 = 4

2

a[0] + (a[1] + a[2])

1 + (3+0) = 4

3

a[0] + (a[1] + (a[2] + a[3]))

1 + (3+(0+4)) = 8

Präfix-Berechnung vom Feld [1, 3, 0, 4] mit Additions-Funktion

Auf den ersten Blick wirkt das wenig spannend, doch kann der Algorithmus parallelisiert werden und somit im Besten Fall in logarithmischer Zeit (mit n Prozessoren) gelöst werden. Voraussetzung dafür ist allerdings eine assoziative Funktion, wie Summe, Maximum, … Ohne genau ins Detail zu gehen könnten wir uns vorstellen, dass ein Prozessor/Kern 0+4 berechnet, ein anderer zeitgleich 1+3, und dann das Ergebnis zusammengezählt wird.

Beispiel. Das Beispiel unserer Präfix-Berechnung mit Hilfe einer Methode aus Arrays:

int[] array = {1, 3, 0, 4};

Arrays.parallelPrefix( array, (a, b) -> a + b );

System.out.println( Arrays.toString( array ) ); // [1, 4, 4, 8]

Die Implementierung nutzt eine fortgeschrittene Syntax aus Java 8, die Lambda-Ausdrücke. statt (a + b) -> a + b kann es sogar mit Integer::sum noch verkürzt werden.

Ein weiteres Beispiel: Finde das Maximum in einer Menge von Fließkommazahlen:

double[] array = {4.8, 12.4, -0.7, 3.8 };

Arrays.parallelPrefix( array, Double::max );

System.out.println( array[array.length -1 ] ); // 12.4

Das Beispiel nutzt schon die Methode, die Arrays für die parallele Präfix-Berechnung bietet:

class java.util.Arrays

§ static void parallelPrefix(int[] array, IntBinaryOperator op)

§ static void parallelPrefix(int[] array, int fromIndex, int toIndex, IntBinaryOperator op)

§ static void parallelPrefix(long[] array, LongBinaryOperator op)

§ static void parallelPrefix(long[] array, int fromIndex, int toIndex, LongBinaryOperator op)

§ static void parallelPrefix(double[] array, DoubleBinaryOperator op)

§ static void parallelPrefix(double[] array, int fromIndex, int toIndex, DoubleBinaryOperator op)

§ static <T> void parallelPrefix(T[] array, BinaryOperator<T> op)

§ static <T> void parallelPrefix(T[] array, int fromIndex, int toIndex, BinaryOperator<T> op)

Internationalisierung von Log-Methoden mit setResourceBundle(…) und logrb(…)

Nutzer von log(…) und logp(…) können die Meldungen internationalisieren. Dafür bietet die API zwei Möglichkeiten. Als erstes kann seit Java 8 global für den Logger mit setResourceBundle(ResourceBundle bundle) ein ResourceBundle zugewiesen werden. Immer dann, wenn eine Log-Nachricht geschrieben wird, wird der Logger zunächst die Nachricht als Schlüssel in der Ressourcen-Abbildung nutzen; gibt es zu dem Schlüssel keine Übersetzung, gilt die Nachricht als Log-Ausgabe.

Neben dieser globalen Zuweisung über setResourceBundle(…) gibt es zwei Extra-Methoden logrb(…), die ResourceBundle-Objekte direkt annehmen:

· void logrb(Level level, String sourceClass, String sourceMethod, ResourceBundle bundle, String msg, Object… params)

· void logrb(Level level, String sourceClass, String sourceMethod, ResourceBundle bundle, String msg, Throwable thrown)

Beispiel: Die Log-Meldung nimmt logrb(…) also aus einem ResourceBundle und das kann so aussehen:

logger.logrb( Level.SEVERE, „Application“, „main“, bundle, „resource.MissingInput“ );

Erfragt wird also vom ResourceBundle bundle die Kennung mit der ID resource.MissingInput.

Logger-Methoden logp(…)

Die normalen log(…)- und Hilfsmethoden loggen eine Nachricht nach einem gewissen Log-Level. Es gibt weiterhin mehrere überladene logp(…)-Methoden, die zusätzlich über einen String einen Klassennamen und Methodennamen annehmen, der dann mit geloggt wird. Die einfachste Variante ist logp(Level level, String sourceClass, String sourceMethod, String msg).

Während die normalen Logger-Methoden wie fine(…) oder severe(…) nicht auf logp(…) zurückgreifen, sondern auf log(Level level, …), gibt es zwei Methoden in Logger, die über logp(…) arbeiten, das sind entering(…) und exiting(…), die verwendet werden, um das Betreten bzw. Verlassen von Methoden zu dokumentieren.

Strings zusammenhängen mit StringJoiner

Um String zu einem großen Ergebnis zusammenzuhängen gibt es mehrere Möglichkeiten: Zum einen der Plus-Operator, zum anderen StringBuilder/StringBuffer. Doch es geht noch ein bisschen einfacher, Teilstrings zu einem Gesamtstring zusammenzubauen. String bietet zum einen die praktische Hilfsmethode join(…). Dahinter steht eine kleine Klasse StringJoiner, die auch direkt genutzt werden kann:

StringJoiner sj = new StringJoiner( „, “ );

sj.add( „1“ ).add( „2“ ).add( „3“ );

System.out.println( sj.toString() ); // 1, 2, 3

Der Join-String ist vom Typ einer CharSequence und ist der Delimiter. Er wird zwischen jedes Element gesetzt, was hinzugefügt wurde. Die im Beispiel eingesetzte Methode add(CharSequence) nimmt ein CharSequence an und liefern den aktuellen StringJoiner zurück, sodass sich die add(…)-Aufrufe kaskadieren lassen. Mit der Methode merge(StringJoiner) lässt sich der Inhalt eines andere StringJoiner integrieren.

Zusammenhängen mit Infix, Prefix und Suffix

Nicht nur das Trennzeichen selbst lässt sich angeben, sondern auch ein Startzeichen und Endzeichen. Wenn etwa die Ausgabe vorne mit einem “{“ beginnen und hinten mit einem “}” enden soll, heißt es:

StringJoiner sj = new StringJoiner( „, „, „{„, „}“ );

Nichts zum Zusammenhängen gegeben

Es kommt vor, dass dem StringJoiner nichts zum Zusammenfügen gegeben wird. Dann wird er dennoch Präfix und Suffix einsetzen.

StringJoiner sj = new StringJoiner( „, „, „{„, „}“ );

System.out.println( sj.toString() ); // {}

Ist das unerwünscht, gibt setEmptyValue(CharSequence) ein Substitut an, das genau dann zum Zuge kommt, wenn kein add(…) etwas zum StringJoiner zufügte:

StringJoiner sj = new StringJoiner( „, „, „{„, „}“ ).setEmptyValue(„<nix>“);

System.out.println( sj.toString() ); // <nix>

Zusammenfassend bietet die Klasse zwei Konstruktoren und fünf Methoden:

class java.util.StringJoiner

§ StringJoiner(CharSequence delimiter)

§ StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)

§ StringJoiner add(CharSequence newElement)

§ StringJoiner merge(StringJoiner other)

§ StringJoiner setEmptyValue(CharSequence emptyValue)

§ int length()

§ String toString()

Aufzählung JDBCType, Schnittstelle SQLType in Java 8

Neu in Java 8 ist eine Aufzählung JDBCType, die knapp 40 JDBC-Typen enthält und anfängt mit ARRAY und BIGINT. Die Aufzählungstypen implementieren eine Schnittstelle SQLType, die ebenfalls neu in Java 8, und Methoden getName(), getVendor() und getVendorTypeNumber() vorschreibt.

Mit Hilfe von SQLType ist es nun möglich mit nur einer Methoden alle möglichen SQL-Typen zu schreiben. So findet sich in der PreparedStatement-Methode in Java 8 setObject(int parameterIndex, Object x, SQLType targetSqlType) was dann die JDBC-spezifischen Typ-Methoden wie setArray(int parameterIndex, Array x), setBigDecimal(int parameterIndex, BigDecimal x), … ergänzt.

@Native Markierungen in Java-Code für JNI

Zwischen Java-Programmen und nativen Programmen gibt es oft eine Wechselwirkung, dass Java-Programm native Funktionen aufrufen und diese wiederum Java-Methoden aufrufen oder auf Variablen zugreifen. Für den Fall, dass native Methoden auf Java-Konstanten zugreifen gibt es in Java 8 eine Annotation java.lang.annotation.Native, die genau diese Konstanten markiert. Auf diese Weise kann ein Generator-Werkzeug diese Java-Konstanten in native Header-Dateien kopieren, sodass für den Konstantenzugriff kein JNI-Aufruf nötig ist, sondern die Konstante aus der Header-Datei genommen werden kann. @Native hat den Retention-Typ SOURCE, ist also nur für Tools sichtbar, die auf der Code-Ebene arbeiten, etwa javac.

Unterprozess-Status erfragen und das Ende einleiten

Mit Methoden von Process lässt sich der Status des externen Programms erfragen und verändern. Die Methode waitFor(…) lässt den eigenen Thread so lange warten, bis das externe Programm zu Ende ist, oder löst eine InterruptedException aus, wenn das gestartete Programm unterbrochen wurde. Der Rückgabewert von waitFor() ist der Rückgabecode des externen Programms, eine zweite Variante von waitFor(…) wartet eine gegebene Zeit. Wurde das Programm schon beendet, liefert auch exitValue() den Rückgabewert. Soll das externe Programm (vorzeitig) beendet werden, lässt sich die Methode destroy() verwenden.

abstract class java.lang.Process

§ boolean isAlive()
Lebt der von der JVM gestartete Unterprozess noch?

§ abstract int exitValue()
Wenn das externe Programm beendet wurde, liefert exitValue() die Rückgabe des gestarteten Programms. Ist die Rückgabe 0, deutet das auf ein normales Ende hin. Läuft das Programm noch, gibt es eine IllegalThreadStateException.

§ abstract void destroy()
Beendet das externe Programm.

§ Process destroyForcibly()
Im Moment wie destroy(), kann von Unterklassen anders implementiert werden. Seit Java 8.

§ abstract void waitFor() throws InterruptedException
Wartet auf das Ende des externen Programms (ist es schon beendet, muss nicht gewartet werden), sonst blockiert die Methode, und liefert dann abschließend den exitValue().

§ boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException
Wartet die angegebene Zeit auf das Ende des gestarteten Programms. Wurde das externe Programm schon beendet, kehrt die Methode sofort zurück und liefert true; den Exit-Code liefert exitValue() weil hier, anders als bei waitFor() der Rückgabecode ein boolean ist. Läuft das Programm noch, und ist es nicht nach timeout Zeiteinheiten beendet, kehrt die Methode mit false zurück. Die Rückgabe ist true, wenn das gestartete Programm in dem Zeitfenster beendet wurde. Hinweis: Die Methode bricht das externe Programm nicht ab, wenn nach überschreiten der Zeit noch läuft. Diese Logik muss ein Programmierer übernehmen, und if ( ! waitFor(…) ) mit destroy() kombinieren.

Überlauf in Java 8 erkennen

In Java 8 kommen neue Methoden hinzu, die eine Überlauferkennung ermöglichen. Die Methoden gibt es in Math und StrictMath:

class java.lang.Math

class java.lang.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 zum Beispiel, 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.

Beispiel: Von der kleinsten Ganzzahl mit subtractExact(…) eins abzuziehen führt zur Ausnahme:

subtractExact( Integer.MIN_VALUE, 1 ); // ArithmeticException

In Math, aber nicht in StrictMath, gibt es weiterhin:

class java.lang.Math

· static int incrementExact(int a)

· static long incrementExact(long a)

· static int decrementExact(int a)

· static long decrementExact(long a)

· static int negateExact(int a)

· static long negateExact(long a)

Kann wegen des Wertebereiches die Operationen nicht durchgeführt werden, folgt wieder eine ArithmeticException.

Vergleich mit C#: C# verhält sich genauso wie Java und reagiert standardmäßig nicht auf Überlauf. Es gibt jedoch spezielle checked-Blöcke, die eine OverflowException melden, wenn es bei arithmetischen Grundoperationen zu einem Überlauf kommt. Folgendes löst diese Ausnahme aus: checked { int val = int.MaxValue; val++; }. Solche checked-Blöcke gibt es in Java nicht, wer diese besondere Überlaufkontrolle braucht, muss die Methoden nutzen, und ein val++ dann auch umschreiben zu Math.addExact(val, 1) bzw. Math.incrementExact(val);

Math.nextUp(…) und nextDown(…)

Was kommt nach und vor 1:

System.out.printf( „%.16f%n“, Math.nextUp( 1 ) );
System.out.printf( „%.16f%n“, Math.nextDown( 1 ) );
System.out.printf( „%.16f%n“, Math.nextAfter( 1, Double.POSITIVE_INFINITY ) );
System.out.printf( „%.16f%n“, Math.nextAfter( 1, Double.NEGATIVE_INFINITY ) );

Die Ausgabe ist:

1,000000119209289

0,9999999403953552

1,0000001192092896

0,9999999403953552

nextUp(d) ist eine Abkürzung für nextAfter(d, Double.POSITIVE_INFINITY) und nextDown(d) eine Abkürzung für nextAfter(d, Double.NEGATIVE_INFINITY). Ist das zweite Argument von Math.nextAfter(…) größer als das erste, dann wird die nächstgrößere Zahl zurückgegeben, ist sie kleiner, dann die nächstkleinere Zahl. Bei Gleichheit kommt die gleiche Zahl zurück.

Division mit Rundung Richtung negativ unendlich, alternativer Restwert

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.

Methoden für Division gegen minus unendlich, floorDiv(…)

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

class java.lang.StrictMath

  • static int floorDiv(int x, int y)
  • static long floorDiv(long x, long y)

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

Methoden für Division gegen minus unendlich, floorMod(…)

Die Division taucht indirekt auch bei der Berechnung des Restwertes auf. Zur Erinnerung: der Zusammenhang zwischen Division a/b und Restwert a%b (a heißt Dividend, b Divisor) ist (int)(a/b) * b + (a%b) = a. In der Gleichung gibt es eine Division, doch da es mit a / b und floorDiv(a, b) zwei Arten von Divisionen gibt, muss es folglich auch zwei Arten von Restwertbildung geben, die sich dann unterscheiden, wenn die Vorzeichen unterschiedlich sind. Neben a % b gibt es daher die Bibliotheksmethode floorMod(a, b) und der Zusammenhang zwischen floorMod(…) und floorDiv(…) ist: floorDiv(a, b) * b + floorMod(a, b) == b. Nach einer Umformung der Gleichung folgt floorMod(a, b) = a – (floorDiv(a, b) * b). Das Ergebnis ist im Bereich -abs(b) und abs(b) und das Vorzeichen des Ergebnisses bestimmt der Divisor b (beim %-Operator ist es der Dividend a).

Die Javdoc zeigt ein Beispiel mit den Werten 4 und 3 und unterschiedlichen Vorzeichen aus:

floorMod(…)-Methode

%-Operator

floorMod(+4, +3) == +1

+4 % +3 == +1

floorMod(+4, -3) == -2

+4 % -3 == +1

floorMod(-4, +3) == +2

-4 % +3 == -1

floorMod(-4, -3) == -1

-4 % -3 == -1

Ergebnis unterschiedlicher Restwertbildung

Sind die Vorzeichen für a und b identisch, so ist auch das Ergebnis von floorMod(a, b) und a % b gleich. Die Beispiele in der Tabelle macht auch den Unterschied im Ergebnis-Vorzeichen deutlich, welches einmal vom Dividenden (%) und einem vom Divisor (floorMod(…)) kommt. Die komplett anderen Ereignisse (vom Vorzeichen einmal abgesehen) beim Paar (+4,-3) und (-4,+3) ergeben sich ganz einfach aus unterschiedlichen Ergebnissen der Division: floorMod(+4, -3) = +4 – (floorDiv(+4, -3) * -3) = +4 – (-2 * -3) = +4 – +6 = -2, während +4 % -3 = +4 – (+4/-3 * -3) = +4 – (-1 * -3) = +4 – 3 = +1.

class java.lang.Math

class java.lang.StrictMath

  • static int floorMod(int x, int y)
  • static long floorMod(long x, long y)

Google Guava Closeables

To shorten things and not to repeat ourselves (usually the closing always looks the same) Google Commons offers the utility class com.google.common.io.Closeables. In this class you can find two static helper methods close(Closeable) and closeQuietly(Closeable). Both take an argument of type Closeable—like a FileInputStream—and call the close() method on this object eventually. If the argument is null, nothing happens; in our hand-coded version we use an extra if (out != null) to prevent a NullPointerException from out.close() if out is null.

The following example uses Closeable.closeQuietly(Closeable) to close a stream. Any potential IOExceptions caused by close() is swallowed by closeQuietly().

package com.tutego.googlecommon.io;

import java.io.*;

import java.util.logging.*;

import com.google.common.io.*;

public class CloseablesDemo {

private static final Logger log = Logger.getLogger( CloseablesDemo.class.getName() );

public static void main( String[] args ) {

InputStream in = null;

try {

in = Resources.getResource( CloseablesDemo.class, „test.txt“ ).openStream();

BufferedReader br = new BufferedReader( new InputStreamReader( in, „utf-8“ ) );

System.out.println( br.readLine() );

} catch ( IOException e ) {

log.log( Level.SEVERE, „IOException thrown while reading line“, e );

} finally {

Closeables.closeQuietly( in );

}

}

}

Using closeQuietly() shortens a program but it does not notify about any exception caused by the inner close() method. A lot of programmers ignore this exception and shortens there closing block to

try { out.close(); } catch ( Exception e ) {}

This style is dangerous, usually not for readers but for all modifying writers. If a writing stream can’t be closed the IOException shows a severe problem that probably the output is not complete and data is missing. For that reason the utility class Closeable offers a second message, close() which, in contrast to closeQuietly() first is denoted by an IOException clause and secondly controls with a boolean parameter if in case of an IOException caused by close() this exception should be swallowed or rethrown.

To summarize these methods:

class Closeables

static void close(Closeable closeable, boolean swallowIOException) throws IOException

Closes the closeable if it is not null. The boolean parameter controls whether an exception will be rethrown (pass false for swallowIOException) or swallowed (pass true).

static void closeQuietly Closeable closeable)

If closeable is not null this method calls closeable.close(). If close() throws an IOException this exception is swallowed by closeQuietly().Because closeQuietly(closeable) swallows the exception it is internally written as close(closeable, true).