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)

Rekursiv nach Dateien/Ordnern suchen mit Files.find(…)

Neu in Java 8 ist die Methode find(…) in Files um Dateien nach gewissen Kriterien zu finden.

final class java.nio.file.Files

  • static Stream<Path> find(Path start, int maxDepth, BiPredicate<Path,BasicFileAttributes> matcher, FileVisitOption… options) throws IOException
    Sucht einen Verzeichnisbaum rekursiv ab und wendet auf jede Path den Filter (Prädikat) an. Falls der Filter zusagt, kommt der Path in den Ergebnis-Stream.

Beispiel und Hinweis: Finde alle Ordner unter dem Standard-Windows Bilder-Verzeichnis und gib sie aus:

Files.find( Paths.get( System.getProperty( „user.home“ ) )

.resolve( „Pictures“ ),

Integer.MAX_VALUE,

(p,attr) -> Files.isReadable( p ) && attr.isDirectory()

).forEach( System.out::println );

Intern greift find(…) auf den gleichen Mechanismus wie walk(…) zurück, doch ist eine Eigenimplementierung mit Hilfe von walk(…) mitunter besser, da wir beim visitFileFailed(…) Fehler ignorieren können – bei find(…) führen Fehler direkt zum Abbruch. Bei Windows führt eine rekursive Suche schnell zu einem java.nio.file.AccessDeniedException durch einen Ordner, bei dem Java nicht dran darf und dann ist mit find(…) sofort Schluss.

Rekursives Ablaufen des Verzeichnisbaums mit Stream oder FileVisitor

Die Utility-Klasse Files bietet vier statische Methoden (zwei mehr in Java 8), die, bei einem Startordner beginnend, die Verzeichnisse rekursiv ablaufen.

final class java.nio.file.Files

  • static Stream<Path> walk(Path start, FileVisitOption… options) throws IOException
  • static Stream<Path> walk(Path start, int maxDepth, FileVisitOption… options) throws IOException
  • static Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
  • static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor)

Bei allen Varianten bestimmt der erste Parameter den Startordner. Während walk(…)einen java.util.stream.Stream liefert (die Methoden sind neu in Java 8), erwarten die anderen beiden walkFileTree(…)-Methoden ein Objekt mit Callback-Methoden, die walkFileTree(…) beim Ablaufen des Verzeichnisbaums aufruft

Verzeichnislistings (DirectoryStream/Stream) holen

In der Klasse Files finden sich vier Methoden (eine mehr unter Java 8), um zu einem gegebenen Verzeichnis alle Dateien und Unterverzeichnisse aufzulisten:

final class java.nio.file.Files

  • static Stream<Path> list(Path dir) throws IOException (neu in Java 8)
  • static DirectoryStream<Path> newDirectoryStream(Path dir) throws IOException
  • static DirectoryStream<Path> newDirectoryStream(Path dir,
    DirectoryStream.Filter<? super Path> filter) throws IOException
  • static DirectoryStream<Path> newDirectoryStream(Path dir, String glob) throws IOException

Die Rückgabe DirectoryStream<T> ist ein Closeable (und somit AutoCloseable) sowie Iterable<T>, und so unterscheidet sich die Möglichkeit zur Anfrage der Dateien im Ordner grundsätzlich von der Methode list(…) in der Klasse File, die immer alle Dateien in einem Feld auf einmal zurückliefert. Bei einem DirectoryStream wird Element für Element über den Iterator geholt; trotz des Namensanhangs „Stream“ ist der DirectoryStream kein Strom im Sinne von java.util.stream. Ein Stream<String> hingegen liefert die kompakte Methode list(Path), sie nutzt intern einen DirectoryStream.

try ( DirectoryStream<Path> files =
Files.newDirectoryStream( Paths.get( „c:/“ ) ) ) {
  for ( Path path : files )
    System.out.println( path.getFileName() );
}

Aus der Tatsache, dass die Dateien und Unterverzeichnisse nicht in einem Rutsch geholt werden, leitet sich die Konsequenz ab, dass der DirectoryStream/Stream<String> geschlossen werden muss, da nicht klar ist, ob der Benutzer wirklich alle Dateien abholt oder nach den ersten 10 Einträgen aufhört. Die Schnittstelle DirectoryStream erweitert die Schnittstelle Closeable (und die ist AutoCloseable, weshalb unser Beispiel ein try-mit-Ressourcen nutzt) und Stream implementiert AutoCloseable, daher ist es guter Stil, den DirectoryStream/Stream am Ende zu schließen, um blockierte Ressourcen freizugeben. try-mit-Ressourcen gibt immer etwaige Ressourcen frei, auch wenn es beim Ablaufen des Verzeichnisses zu einer Ausnahme kam.

Dateiinhalte lesen/schreiben mit Utility-Klasse Files

Da die Klasse Path nur Pfade, aber keine Dateiinformationen wie die Länge oder Änderungszeit repräsentiert und Path auch keine Möglichkeit bietet, Dateien anzulegen und zu löschen, übernimmt die Klasse Files diese Aufgaben.

Einfaches Einlesen und Schreiben von Dateien

Mit den Methoden readAllBytes(…), readAllLines(…), lines(…) bzw. write(…) kann Files einfach ein Dateiinhalt einlesen oder Strings bzw. ein Byte-Feld schreiben.

URI uri = ListAllLines.class.getResource( „/lyrics.txt“ ).toURI();
Path path = Paths.get( uri );
System.out.printf( „Datei ‚%s‘ mit Länge %d Byte(s) hat folgende Zeilen:%n“, path.getFileName(), Files.size( path ) );
int lineCnt = 1;
for ( String line : Files.readAllLines( path /*, StandardCharsets.UTF_8 vor Java 8 */) )
  System.out.println( lineCnt++ + „: “ + line );

final class java.nio.file.Files

  • static long size(Path path) throws IOException
    Liefert die Größe der Datei.
  • static byte[] readAllBytes(Path path) throws IOException
    Liest die Datei komplett in ein Byte-Feld ein.
  • static List<String> readAllLines(Path path) throws IOException (Java 8)
    static List<String> readAllLines(Path path, Charset cs) throws IOException
  • Liest Zeile für Zeile die Datei ein und liefert eine Liste dieser Zeilen. Optional ist die Angabe einer Kodierung, standardmäßig ist es StandardCharsets.UTF_8.
  • static Path write(Path path, byte[] bytes, OpenOption… options) throws IOException
    Schreibt eine Byte-Feld in eine Datei.
  • static Path write(Path path, Iterable<? extends CharSequence> lines,
    OpenOption… options) throws IOException (Java 8)
  • static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs,
    OpenOption… options) throws IOException
    Schreibt alle Zeilen aus dem Iterable in eine Datei. Optional ist die Kodierung, die StandardCharsets.UTF_8 ist, wenn nicht anders angegeben.
  • static Stream<String> lines(Path path)
  • Stream<String> lines(Path path, Charset cs)
    Liefert einen Stream von Zeilen einer Datei. Optional ist die Angabe der Kodierung, die sonst standardmäßig StandardCharsets.UTF_8 ist. Beide Methoden sind neu in Java 8.

Die Aufzählung OpenOption ist ein Vararg, und daher sind Argumente nicht zwingend nötig. StandardOpenOption ist eine Aufzählung vom Typ OpenOption mit Konstanten wie APPEND, CREATE, …

Hinweis: Auch wenn es naheliegt, die Files-Methode zum Einlesen mit einem Path-Objekt zu füttern, das ein HTTP-URI repräsentiert, funktioniert dies nicht. So liefert schon die erste Zeile des Programms eine Ausnahme des Typs »java.nio.file.FileSystemNotFoundException: Provider „http“ not installed«.

Path path = Paths.get( new URI „http://tutego.de/javabuch/aufgaben/bond.txt“ ) );
List<String> content = Files.readAllBytes( path );
System.out.println( content );

Vielleicht kommt in der Zukunft ein Standard-Provider von Oracle, doch es ist davon auszugehen, dass quelloffene Lösungen diese Lücke schließen werden. Schwer zu programmieren sind Dateisystem-Provider nämlich nicht.

Datenströme kopieren

Sollen die Daten nicht direkt aus einer Datei in eine byte-Feld/String-Liste gehen bzw. aus einer byte-Feld/String-Sammlung in eine Datei, sondern von einer Datei in einen Datenstrom, so bieten sich zwei copy(…)-Methoden an:

final class java.nio.file.Files

  • static long copy(InputStream in, Path target, CopyOption… options)
    Entleert den Eingabestrom und kopiert die Daten in die Datei.
  • static long copy(Path source, OutputStream out)
    Kopiert alle Daten aus der Datei in den Ausgabestrom.

Zeichenorientierte Datenströme über Files beziehen

Neben den statischen Files-Methoden newOutputStream(…) und newInputStream(…) gibt es zwei Methoden, die zeichenorientierte Ströme liefern, also Writer/Reader.

final abstract java.nio.file.Files

  • static BufferedReader newBufferedReader(Path path, Charset cs)
    throws IOException
  • static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption… options)
    throws IOException
    Liefert einen Unicode-zeichenlesenden Ein-/Ausgabestrom. Das Charset-Objekts bestimmt, in welcher Zeichenkodierung sich die Texte befinden, damit sie korrekt in Unicode konvertiert werden.
  • static BufferedReader newBufferedReader(Path path)
    throws IOException
    Entspricht newBufferedReader(path, StandardCharsets.UTF_8). Erst in Java 8.
  • static BufferedWriter newBufferedWriter(Path path, OpenOption… options)
    throws IOException
    Entspricht Files.newBufferedWriter(path, StandardCharsets.UTF_8, options). Erst in Java 8,

BufferedReader und BufferedWriter sind Unterklassen von Reader/Writer die zum Zwecke der Optimierung Dateien im internen Puffer zwischenspeichern.

MAC-Adressen auslesen

Die MAC-Adresse (von Media-Access-Control) ist eine (im Idealfall) eindeutige Adresse eines Netzwerkgeräts. MAC-Adressen sind für Ethernet-Verbindungen essenziell, da auf der physikalischen Übertragungsebene Signale zu einer gewünschten Netzwerkkarte aufgebaut werden. Wegen der Eindeutigkeit eignen sie sich gut als Schlüssel, und es ist interessant, auch in Java diese Adresse auszulesen. Das geht mit der Klasse NetworkInterface recht unkompliziert. Alle lokale Netzwerkschnittstellen liefert NetworkInterface.getNetworkInterfaces(), ist die IP-Adresse bekannt, können wir NetworkInterface.getByInetAddress(InetAddress) nutzen.

for ( NetworkInterface ni : Collections.list( NetworkInterface.getNetworkInterfaces() ) ) {
  byte[] adr = ni.getHardwareAddress();
  if ( adr == null || adr.length != 6 )
    continue;
  String mac = String.format( "%02X:%02X:%02X:%02X:%02X:%02X",
                                    adr[0], adr[1], adr[2], adr[3], adr[4], adr[5] );
  System.out.println( mac );
}

Auf der Windows Kommandozeile liefert ipconfig /all alle MAC-Adressen, die dort „physikalische Adresse” heißen.