Java 11 ist draußen

Download unter

  • https://www.oracle.com/technetwork/java/javase/downloads/jdk11-downloads-5066655.html
  • http://jdk.java.net/11

Unter http://www.tutego.de/java/java-se-11-oracle-jdk-11-openjdk_11_java_18.9.html fasse ich die Neuerungen kompakt zusammen.

Hier im Blog gibt es die Kategorie http://www.tutego.de/blog/javainsel/category/java-11/ für alle Neuerungen in Java 11. Das geht in der Regel ins Buch.

Heise hat eine Java 11-Launch-Session aufgenommen: https://www.youtube.com/watch?v=CUuCVHWeO-Y

Klassenlader

Ein Klassenlader ist dafür verantwortlich, die Binärrepräsentation einer Klasse aus einem Hintergrundspeicher oder Hauptspeicher zu laden. Aus der Datenquelle (im Allgemeinen die .class-Datei) liefert der Klassenlader ein Byte-Array mit den Informationen, die im zweiten Schritt dazu verwendet werden, die Klasse ins Laufzeitsystem einzubringen; das ist Linking. Es gibt vordefinierte Klassenlader und die Möglichkeit, eigene Klassenlader zu schreiben, um etwa verschlüsselte vom Netzwerk zu beziehen oder komprimierte .class-Dateien aus Datenbanken zu laden.

Klassenladen auf Abruf

Nehmen wir zu Beginn ein einfaches Programm mit drei Klassen:

package com.tutego.insel.tool;

 

public class HambachForest {

public static void main( String[] args ) {

boolean rweWantsToCutTrees = true;

Forrest hambachForest = new Forrest();

if ( rweWantsToCutTrees ) {

Protest<Forrest> p1 = new Protest<>();

p1.believeIn = hambachForest;

}

}

}

 

class Forrest { }

 

class Protest<T> {

T believeIn;

java.time.LocalDate since;

}

Wenn die Laufzeitumgebung das Programm HambachForest startet, muss sie eine Reihe von Klassen laden. Das tut sie dynamisch zur Laufzeit. Sofort wird klar, dass es zumindest HambachForest sein muss. Und da die JVM die statische main(String[])-Methode aufruft und Optionen übergibt, muss auch String geladen sein. Unsichtbar stecken noch andere referenzierte Klassen dahinter, die nicht direkt sichtbar sind. So wird zum Beispiel Object geladen, da implizit in der Klassendeklaration von HambachForest steht: class HambachForest extends Object. Intern ziehen die Typen viele weitere Typen nach sich. String implementiert Serializable, CharSequence und Comparable, also müssen diese drei Schnittstellen auch geladen werden. Und so geht das weiter, je nachdem, welche Programmpfade abgelaufen werden. Wichtig ist aber, zu verstehen, dass diese Klassendateien so spät wie möglich geladen werden.

Klassenlader bei der Arbeit zusehen

Im Beispiel lädt die Laufzeitumgebung selbstständig die Klassen (implizites Klassenladen). Klassen lassen sich auch mit Class.forName(String) über ihren Namen laden (explizites Klassenladen).

Um zu sehen, welche Klassen überhaupt geladen werden, lässt sich der virtuellen Maschine beim Start der Laufzeitumgebung ein Schalter mitgeben: -verbose:class. Dann gibt die Maschine beim Lauf alle Typen aus, die sie lädt. Nehmen wir das Beispiel von eben, so ist die Ausgabe mit dem aktivierten Schalter unter Java 11 fast 500 Zeilen lang; ein Ausschnitt:

$ java -verbose:class com.tutego.insel.tool.HambachForest

[0.010s][info][class,load] opened: C:\Program Files\Java\jdk-11\lib\modules

[0.032s][info][class,load] java.lang.Object source: jrt:/java.base

[0.032s][info][class,load] java.io.Serializable source: jrt:/java.base

[0.033s][info][class,load] java.lang.Comparable source: jrt:/java.base

[0.036s][info][class,load] java.lang.CharSequence source: jrt:/java.base

[0.037s][info][class,load] java.lang.String source: jrt:/java.base

[0.684s][info][class,load] sun.security.util.Debug source: jrt:/java.base

[0.685s][info][class,load] com.tutego.insel.tool.HambachForest source: file:/C:/Inselprogramme/target/classes/

[0.687s][info][class,load] java.lang.PublicMethods$MethodList source: jrt:/java.base

[0.687s][info][class,load] java.lang.PublicMethods$Key source: jrt:/java.base

[0.689s][info][class,load] java.lang.Void source: jrt:/java.base

[0.690s][info][class,load] com.tutego.insel.tool.Forrest source: file:/C:/Inselprogramme/target/classes/

[0.691s][info][class,load] jdk.internal.misc.TerminatingThreadLocal$1 source: jrt:/java.base

[0.692s][info][class,load] java.lang.Shutdown source: jrt:/java.base

[0.692s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base

Ändern wir die Variable rweWantsToCutTrees auf true, so wird unsere Klasse Protest geladen, und in der Ausgabe kommt nur eine Zeile hinzu! Das wundert auf den ersten Blick, denn die Klasse referenziert LocalDate. Doch ein LocalDate wird nicht benötigt, also auch nicht geladen. Der Klassenlader bezieht nur Klassen, wenn die für den Programmablauf benötigt werden, nicht aber durch die reine Deklaration als Attribut. Wenn wir LocalDate mit zum Beispiel LocalDate.now() initialisieren kommen stattliche 200 Klassendateien hinzu.

Eclipse 4.9 (2018-09) ist fertig, Spring Tool Suite 3.9.6 setzt sofort drauf auf

Heise schreibt recht gut darüber: https://www.heise.de/developer/meldung/Entwicklungsumgebung-Eclipse-Auf-Photon-folgt-2018-09-4168224.html. Leider ist noch ein Java 11-Plugin nötig, so muss ich das im kommenden Java 11-Buch auch extra mit einführen.

Zur STS-Ankündigung: https://spring.io/blog/2018/09/20/spring-tool-suite-3-9-6-released, https://docs.spring.io/sts/nan/v396/NewAndNoteworthy.html

 

Weißraum entfernen

In einer Benutzereingabe oder Konfigurationsdatei steht nicht selten vor oder hinter dem wichtigen Teil eines Textes Weißraum wie Leerzeichen oder Tabulatoren. Vor der Bearbeitung sollten sie entfernt werden. Die String-Klasse bietet dazu trim() und seit Java 11 strip(), stripLeading() und stripTrailing() an. Der Unterschied:

Methode Entfernt …
trim() … am Anfang und am Ende des Strings alle Codepoints kleiner oder gleich dem Leerzeichen ‚U+0020‘
strip() … alle Zeichen am Anfang und am Ende des Strings, die nach der Definition von Character.isWhitespace(int) Leerzeichen sind
stripLeading() … wie strip(), allerdings nur am Anfang des Strings
stripTrailing() … wie strip(), allerdings nur am Ende des Strings

Unterschiede von trim() und stripXXX()

Alle vier Methoden entfernen keinen Weißraum inmitten des Strings.

Beispiel: Entferne Leer- und ähnliche Füllzeichen am Anfang und Ende eines Strings:

String s = “ \tSprich zu der Hand.\n  \t „;
System.out.println( „‚“ + s.trim() + „‚“ ); // ‚Sprich zu der Hand.‘

 

Beispiel: Teste, ob ein String mit Abzug allen Weißraums leer ist:

boolean isBlank = „“.equals( s.trim() );

Alternativ:

boolean isBlank = s.trim().isEmpty();

Strings aus Wiederholungen generieren

In Java 11 ist eine Objektmethode repeat(int count) eingezogen, die einen gegeben String vervielfacht.

Beispiel: Wiederhole den String s dreimal:

String s = „tu“;

System.out.println( s.repeat( 3 ) );    // tututu

Bevor es die Methode in Java 11 gab, sah eine alternative Lösung etwa so aus:

int    n = 3;
String t = new String( new char[ n ] ).replace( „\0“, s );
System.out.println( t );                // tututu

String-Länge und Test auf Leer-String

String-Objekte verwalten intern die Zeichenreihe, die sie repräsentieren, und bieten eine Vielzahl von Methoden, um die Eigenschaften des Objekts preiszugeben. Eine Methode haben wir schon benutzt: length(). Für String-Objekte ist sie so implementiert, dass sie die Anzahl der Zeichen im String (die Länge des Strings) zurückgibt. Um herauszufinden, ob der String keine Zeichen hat, lässt sich neben length() == 0 auch die Methode isEmpty() nutzen. In Java 11 ist die Methode isBlank() hinzugekommen, die testet, ob der String leer ist, oder nur aus Weißraum besteht; Weißraum ist jedes Zeichen, bei dem Character.isWhitespace(int) wahr anzeigt.

Anweisung Ergebnis
„“.length() 0
„“.isEmpty() true
“ „.length() 1
“ „.isEmpty() false
“ „.isBlank() true
String s = null; s.length(); NullPointerException

Tabelle 1.1: Ergebnisse der Methoden length(), isEmpty() und isBlank()

Vom Zeichen zum String

Um ein Unicode-Zeichen ein einen String zu konvertieren können wir die statische überladene String-Methode valueOf(char) nutzen. Eine vergleichbare Methode gibt es auch in Character, und zwar die statische Methode toString(char). Beide Methoden haben die Einschränkung, dass das Unicode-Zeichen nur 2 Byte lang sein kann. String deklariert dafür auch valueOfCodePoint(int). So eine Methode fehlte bisher in Character; erst in Java 11 ist toString(int) eingezogen; intern delegiert sie an valueOfCodePoint(int).

Statische compare(…)-Methode in CharSequence

Seit Java 11 gibt es in CharSequence eine neue Methode compare(…), die zwei CharSequence-Objekte lexikografisch vergleicht.

interface java.lang.CharSequence

  • static int compare(CharSequence cs1, CharSequence cs2)
    Vergleicht die beiden Zeichenketten lexikografisch.

Die statische Methode hat den Vorteil, dass nun alle Kombination von CharBuffer, Segment, String, StringBuffer, StringBuilder mit nur dieser einen Methode geprüft werden können. Und wenn der Vergleich  0 ergibt, so wissen wir auch, dass die Zeichenfolgen die gleichen Zeichen enthalten.

Vergleichen von StringBuilder-Exemplaren und String mit StringBuilder

Zum Vergleichen von Strings bietet sich die bekannte equals(…)-Methode an. Diese ist aber bei StringBuilder nicht wie erwartet implementiert. Dazu gesellen sich andere Methoden, die zum Beispiel unabhängig von der Groß-/Kleinschreibung vergleichen.

equals(…) bei der String-Klasse

Die Klasse String implementiert die equals(Object)-Methode, sodass ein String mit einem anderen String verglichen werden kann. Allerdings vergleich equals(Object) von String nur String/String-Paare. Die Methode beginnt erst dann den Vergleich, wenn das Argument auch vom Typ String ist. Das bedeutet, dass der Compiler alle Übergaben auch vom Typ StringBuilder bei equals(Object) zulässt, doch zur Laufzeit ist das Ergebnis immer false, da eben ein StringBuilder nicht vom Typ String ist. Ob die Zeichenfolgen dabei gleich sind, spielt keine Rolle.

contentEquals(…) beim String

Eine allgemeine Methode zum Vergleichen eines Strings mit entweder einem anderen String oder mit StringBuilder ist contentEquals(CharSequence). Die Methode liefert die Rückgabe true, wenn der String und die CharSequence (String, StringBuilder und StringBuffer sind Klassen vom Typ CharSequence) den gleichen Zeicheninhalt haben. Die interne Länge des Puffers spielt keine Rolle. Ist das Argument null, wird eine NullPointerException ausgelöst.

Beispiel: Vergleiche einen String mit einem StringBuilder:

String        s  = „Elektrisch-Zahnbürster“;
StringBuilder sb = new StringBuilder( „Elektrisch-Zahnbürster“ );
System.out.println( s.equals(sb) );                   // false
System.out.println( s.equals(sb.toString()) );        // true
System.out.println( s.contentEquals(sb) );            // true

Kein eigenes equals(…) bei StringBuilder

Wollen wir zwei StringBuilder-Objekte miteinander vergleichen, so geht das nicht mit der equals(…)-Methode. Es gibt zwar die übliche von Object geerbte Methode, doch das heißt, nur Objektreferenzen werden verglichen. Anders gesagt: StringBuilder überschreibt die equals(…)-Methode nicht. Wenn also zwei verschiedene StringBuilder-Objekte mit gleichem Inhalt mit equals(…) verglichen werden, kommt trotzdem immer false heraus.

Beispiel: Um den inhaltlichen Vergleich von zwei StringBuilder-Objekten zu realisieren, können wir sie erst mit toString() in Strings umwandeln und dann mit String-Methoden vergleichen:

StringBuilder sb1 = new StringBuilder( „The Ocean Cleanup“ );

StringBuilder sb2 = new StringBuilder( „The Ocean Cleanup“ );

System.out.println( sb1.equals( sb2 ) );                        // false
System.out.println( sb1.toString().equals( sb2.toString() ) );  // true
System.out.println( sb1.toString().contentEquals( sb2 ) );      // true

StringBuilder ist Comparable

Seit Java 11 bietet StringBuilder eine Methode int compareTo​(StringBuilder another) sodass lexikografische Vergleiche möglich sind. (StringBuilder implementiert die Schnittstelle Comparable<StringBuilder>.) Somit realisieren String und StringBuilder beide eine Ordnung, siehe „Lexikografische Vergleiche mit Größer-kleiner-Relation“.

Eine Begleiterscheinung ist die Tatsache, dass bei gleichen Zeichenfolgen die Rückgabe von compareTo(…) gleich 0 ist. Das ist deutlich besser als erst den StringBuilder in einen String zu konvertieren.

Beispiel:

StringBuilder sb1 = new StringBuilder( „The Ocean Cleanup“ );

StringBuilder sb2 = new StringBuilder( „The Ocean Cleanup“ );

System.out.println( sb1.compareTo( sb2 ) == 0 );                // true

Mit ByteArrayOutputStream in ein Byte-Feld schreiben

Ein ByteArrayOutputStream ist ein OutputStream, der die geschriebenen Daten intern in einem byte-Array speichert. Die Größe des Arrays vergrößert sich dynamisch zu den geschriebenen Daten.

class java.io.ByteArrayOutputStream
extends OutputStream

  • ByteArrayOutputStream()
    Erzeugt ein neues OutputStream-Objekt, das die Daten in einem internen Byte-Array abbildet.
  • ByteArrayOutputStream(intsize)
    Erzeugt ein ByteArrayOutputStream mit einer gewünschten anfänglichen Pufferkapazität.

Als OutputStream erbt der ByteArrayOutputStream alle Methoden, die jedoch allesamt eine IOException auslösen. Bei einem Strom der in den Speicher schreibt kann das nicht passieren. Daher wurde in Java 11 eine neue Methode writeBytes(byte[]) eingeführt, die keine IOException auslöst.

Mit die wichtigste Methode ist toByteArray(), die ein byte[] mit dem geschriebenen Inhalt liefert. reset() löscht den internen Puffer. Eine praktische Methode ist writeTo(OutputStream out). Hinter ihr steckt ein out.write(buf, 0, count), das für uns in das nicht sichtbare interne Feld buf schreibt. Es gibt drei toString(…)-Methoden, die das Byte-Array in einen String konvertieren: toString(String charsetName) und toString​(Charset charset) – seit Java 10 – bekommen als Argument die Zeichenkodierung übergeben und ByteArrayOutputStream überschreibt toString() von der Oberklasse Object was die Standard Plattform-Zeichenkodierung nimmt.

Files: Einfaches Einlesen und Schreiben von Dateien

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

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

final class java.nio.file.Files

  • staticbyte[]readAllBytes(Pathpath)throwsIOException
    Liest die Datei komplett in ein Byte-Feld ein.
  • staticList<String>readAllLines(Pathpath)throwsIOException
  • staticList<String>readAllLines(Pathpath,Charsetcs)throwsIOException
    Liest die Datei Zeile für Zeile ein und liefert eine Liste dieser Zeilen. Optional ist die Angabe einer Kodierung, standardmäßig ist es UTF_8.
  • static String readString(Path path) throws IOException
  • static String readString(Path path, Charset cs) throws IOException
    Liest eine Datei komplett aus und liefert den Inhalt als String. Ohne Kodierung gilt standardmäßig UTF-8. Beide Methoden neu in Java 11.
  • staticPathwrite(Pathpath,byte[]bytes,..options)throwsIOException
    Schreibt ein Byte-Array in eine Datei.
  • staticPathwrite(Pathpath,Iterable<?extendsCharSequence>lines,..
    options) throws IOException
  • staticPathwrite(Pathpath,Iterable<?extendsCharSequence>lines,Charsetcs,
    .. options) throws IOException
    Schreibt alle Zeilen aus dem Iterable in eine Datei. Optional ist die Kodierung, die StandardCharsets.UTF_8 ist, so nicht anders angegeben.
  • static Path writeString(Path path, CharSequence csq, OpenOption… options) throws IOException
  • static Path writeString(Path path, CharSequence csq, Charset cs, OpenOption… options) throws IOException
    Schreibt eine Zeichenfolge in die genannte Datei. Der übergebene path wird zurückgegeben. Ohne Kodierung gilt standardmäßig UTF-8. Beide Methoden neu in Java 11.

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 usw.

Beispiel: Lies eine UTF-8-kodierte Datei ein:

String s = Files.readString( path );

Bevor die praktische Methode in Java 11 einzog, sah eine Alternative so aus:

String s = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 );

Hinweis: Auch wenn es naheliegt, die Files-Methode zum Einlesen mit einem Path-Objekt zu füttern, das einen 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«.

URI uri = new URI( „http://tutego.de/javabuch/aufgaben/bond.txt“ );
Path path = Paths.get( uri );     //
List<String> content = Files.readAllLines( 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.

Prädikate aus java.util.regex.Pattern

Die Pattern-Methoden asPredicate() und asMatchPredicate() (ab Java 11) liefern ein Predicate<String>, sodass ein regulärer Ausdruck als Kriterium, zum Beispiel zum Filtern oder Löschen von Einträgen in Datenstrukturen, genutzt werden kann.

Es unterscheiden sich die Methoden wie folgt:

Methode Implementierung
Predicate<String> asPredicate() return s -> matcher(s).find()
Predicate<String> asMatchPredicate() return s -> matcher(s).matches();

asPredicate() matcht bei einem Teilstring, asMatchPredicate() den gesamten String

Beispiel: Lösche aus einer Liste alle Strings, die leer sind oder Zahlen enthalten:

List<String> list = new ArrayList<>( Arrays.asList( "", "69cool", "1898", "Sisi" ) );

list.removeIf( Pattern.compile( "\\d+" ).asPredicate().or( String::isEmpty ) );

System.out.println( list );            // [Sisi]

Ändern wir asPredicate() in asMatchPredicate() ist die Ausgabe [69cool, Sisi].

Einführung in reguläre Ausdrücke mit der Java-API

Ein regulärer Ausdruck (engl. regular expression, kurz Regex) ist die Beschreibung eines Musters (engl. pattern). Reguläre Ausdrücke werden bei der Zeichenkettenverarbeitung beim Suchen und Ersetzen eingesetzt. Für folgende Szenarien bietet die Java-Bibliothek entsprechende Unterstützung an:

  • Frage nach einer kompletten Übereinstimmung: Passt eine Zeichenfolge komplett auf ein Muster? Wir nennen das vollständigen Match. Die Rückgabe einer solchen Anfrage ist einfach wahr oder falsch.
  • Finde Teil-Strings: Das Pattern beschreibt einen Teil-String, und gesucht sind alle Vorkommen dieses Musters in einem Such-String.
  • Ersetze Teilfolgen: Das Pattern beschreibt Zeichenfolgen, die durch andere Zeichenfolgen ersetzt werden.
  • Zerlegen einer Zeichenfolge: Das Muster steht für Trennzeichen, sodass nach dem Zerlegen eine Sammlung von Zeichenfolgen entsteht.

Tipp: Regex-Ausdrücke lassen sich über Tools visualisieren. Online ist das zum Beispiel mit https://www.debuggex.com/ oder http://regexper.com/ möglich, http://xenon.stanford.edu/~xusch/regexp/ »liest« reguläre Ausdrücke vor.

Regex-API

Ein Pattern-Matcher ist die »Maschine«, die reguläre Ausdrücke verarbeitet. Zugriff auf diese Mustermaschine bietet die Klasse Matcher. Dazu kommt die Klasse Pattern, die die regulären Ausdrücke in einem internen vorcompilierten Format repräsentiert. Beide Klassen befinden sich im Paket java.util.regex. Um die Sache etwas zu vereinfachen, gibt es bei String zwei kleine Hilfsmethoden, die im Hintergrund auf die Klassen verweisen, um eine einfachere API anbieten zu können; diese nennen sich auch Fassaden-Methoden. Wir werden am Anfang erst mit den String-Methoden arbeiten und uns später die Klasse Pattern genauer anschauen. Die die Objektmethode matches(…) der Klasse String testet, ob ein regulärer Ausdruck eine Zeichenfolge komplett beschreibt.

Konstruktion von regulären Ausdrücken

In diesem Abschnitt wollen wir schauen, was ein Pattern-Matcher alles erkennen kann und wie die Syntax dafür aussieht.

Literale Zeichen

Der einfachste reguläre Ausdruck besteht aus einzelnen Zeichen, den Literalen.

Ausdruck Ergebnis
„tutego“.matches( „tutego“ ) true
„tutego“.matches( „Tutego“ ) false
„tutego“.matches( „-tutego-“ ) false

Tabelle: Einfache reguläre Ausdrücke und ihr Ergebnis

Für diesen Fall benötigen wir keine regulären Ausdrücke, ein equals(…) würde reichen.

Hinweis: Bei Java ist es immer so, dass der reguläre Ausdruck den gesamten String komplett treffen muss, und nicht nur einen Teilstring. In nahezu jeder anderen Sprache und Bibliothek ist das nicht so, hier zählt ein Teilstring als „match“. In einer JavaScript-Konsole:

„tutego“.match( /tutego/ )

[„tutego“, index: 0, input: „tutego“, groups: undefined]

„Tutego“.match( /tutego/ )

null

„-tutego-„.match( /tutego/ )

[„tutego“, index: 1, input: „-tutego-„, groups: undefined]

Spezialzeichen (Metazeichen)

In regulären Ausdrücken sind einige Zeichen reserviert, weshalb sie nicht als einfaches Literal gewertet werden. Zu diesen Zeichen mit besonders Bedeutung zählen: \ (Backslash), ^ (Caret), $ (Dollarzeichen), . (Punkt), | vertikaler Strich, ? (Fragezeichen), * (Sternchen), + (Pluszeichen), (, ) (runde Klammer auf und zu), [, ] (eckige Klammer auf und zu), { (geschweifte Klammer auf).

Um diese Zeichen als Literal verwenden zu können ist eine Ausmaskierung mit \ nötig.

Ausdruck Ergebnis
„1+1“.matches( „1+1“ ) false
„1+1“.matches( „1\\+1“ ) true
„11111“.matches( „1+1“ ) true
„+1“.matches( „+1“ ) PatternSyntaxException: Dangling meta character ‚+‘ near index 0

Metazeichen müssen ausmaskiert werden

Der Ausdruck „11111“.matches(„1+1“) macht deutlich, dass + ein besonders Symbol ist, was für Wiederholungen steht. Wird es, wie im letzten Fall, falsch angewendet, folgt eine Ausnahme.

Zeichenklassen

Mit einer Zeichenklasse ist es möglich ein von mehren Zeichen aus einer Menge zu matchen. So steht [aeiou] für eines der Zeichen a, e, i, o oder u. Die Reihenfolge der Zeichen spielt keine Rolle. Mit einem Minuszeichen lassen sich Bereiche definieren. So steht [0-9a-fA-F] für die Zeichen 0, 1, 2, …, 9 oder Groß-/Kleinbuchstaben a, b, c, d, e, f, A, B, C, D, E, F. Auch hier spielt die Reihenfolge keine Rolle, es hätte auch [a-fA-F0-9] heißen können. Mehrere Bereiche mit rechteckigen Klammern lassen sich hintereinander stellen.

Die Metazeichen wie * oder + können ohne Ausmaskierung in Zeichenklassen verwendet werden, nur eben das Minus nicht, es sei denn, es steht am Anfang oder am Ende.

 

Ausdruck Ergebnis
„tutego“.matches(„[tT]utego“ ) true
„Tutego“.matches(„[tT]utego“ ) true
„Nr. 1“.matches( „Nr\\. [0-9]“ ) true
„1*2“.matches( „[0-9][+*/][0-9]“ ) true
„1*2“.matches( „[0-9][+*/][0-9]“ ) true
„1-2“.matches( „[0-9][+*/][0-9]“ ) PatternSyntaxException: Illegal character range near index 8
„1-2“.matches( „[0-9][+\\-*/][0-9]“ ) true

Beispiel für Zeichenklassen

Hinweis: Es ist wichtig daran zu denken, dass es immer nur einzelnen Zeichen sind und keine Zahlenbereiche. Wenn wir [1-99] vor uns haben, dann ist das mitnichten ein regulärer Ausdruck für Zahlen von 1 bis 99, sondern nur die Ziffern 1, 2, 3, …, 9 und dann noch einmal die 9 extra, was redundant ist, und gekürzt werden kann auf [1-9].

Negative Zeichenklassen

Steht direkt hinter der öffnenden eckigen Klammer ein ^, definiert das negative Zeichenklassen. Der Match ist dann auf allen Zeichen, die in der negativen Zeichenklasse nicht vorkommen.

 

Ausdruck Ergebnis
„1“.matches( „[^-+*/]“ ) true
„ß“.matches( „[^-+*/]“ ) true
„/“.matches( „[^-+*/]“ ) false
„“.matches( „[^-+*/]“ false

Beispiel für negative Zeichenklassen

Das letzte Beispiel macht deutlich, dass eine negative Zeichenklasse auch für ein Zeichen, nämlich für ein Zeichen, das eben nicht +, -, *, / ist. Kein Zeichen kann das nicht sein.

Jedes Zeichen (.)

Der . (Punkt) ist ein mächtiges Metazeichen und steht für (fast) jedes erdenkliche Zeichen.

Hinweis: Der . (Punkt) matcht standardmäßig keinen Zeilenumbruch. Das hat historische Gründe, denn die ersten Werkzeuge arbeiteten zeilenbasiert, und da war es nicht gewünscht, wenn der Ausdruck mit auf die nächste Zeile ging. In Java können wir einstellen, ob der Punkt auch einen Zeilenumbruch erkennen soll.

Ausdruck Ergebnis
„Filk“.matches( „F..k“ ) true
„123“.matches( „\\d\\d\\d“ ) true
„a b“.matches( „a\\sb“ ) true
„a\nb“.matches( „a\\sb“ ) true
„\n“.matches( „.“ ) false

Beispiel mit dem Punkt

Vordefinierte Zeichenklassen

Gewisse Zeichenklassen kommen immer wieder vor, etwa für Ziffern. Daher gibt es vordefinierte Zeichenklassen, die uns Schreibarbeit ersparen und den regulären Ausdruck übersichtlicher machen. Die wichtigsten sind:

Zeichenklasse Enthält
\d Ziffer: [0-9]
\D Keine Ziffer: [^0-9] bzw. [^\d]
\s Weißraum: [ \t\n\x0B\f\r]
\S Keinen Weißraum: [^\s]
\w Wortzeichen: [a-zA-Z_0-9]
\W Keine Wortzeichen: [^\w]
\p{Blank} Leerzeichen oder Tab: [ \t]
\p{Lower}, \p{Upper} Einen Klein-/Großbuchstaben: [a-z] bzw. [A-Z]
\p{Alpha} Einen Buchstaben: [\p{Lower}\p{Upper}]
\p{Alnum} Ein alphanumerisches Zeichen: [\p{Alpha}\p{Digit}]
\p{Punct} Ein Interpunktionszeichen: !“#$%&'()*+,-./:;<=>?@[\]^_`{|}~
\p{Graph} Ein sichtbares Zeichen: [\p{Alnum}\p{Punct}]
\p{Print} Ein druckbares Zeichen: [\p{Graph}]

Tabelle: Vordefinierte Zeichenklassen

Bei den Wortzeichen handelt es sich standardmäßig um die ASCII-Zeichen und nicht um deutsche Zeichen mit unseren Umlauten oder allgemeine Unicode-Zeichen. Eine umfassende Übersicht liefert die API-Dokumentation der Klasse java.util.regex.Pattern unter https://docs.oracle.com/javase/10/docs/api/java/util/regex/Pattern.html, die auch weitere vordefinierte Zeichenklassen auflistet.

 

Ausdruck Ergebnis
„123“.matches( „\\d\\d\\d“ ) true
„a b“.matches( „a\\sb“ ) true
„a\nb“.matches( „a\\sb“ ) true

Beispiel mit vordefinierten Zeichenklassen

Vorhanden oder nicht?

Steht hinter einem Zeichen ein Fragezeichen, so ist es optional. Das Fragezeichen nennen wir auch Quantifizierer. Auch hinter einer Zeichenklasse, vordefinierten Zeichenklasse ist ein Fragezeichen erlaubt. Mehrere Zeichen können durch runde Klammen zusammengefasst werden.

 

Ausdruck Ergebnis
„Lyric“.matches( „Lyrics?“ ) true
„Lyrics“.matches( „Lyrics?“ ) true
„1“.matches( „\\d?“ ) true
„“.matches( „\\d?“ ) true
„Christian“.matches( „Chris(tian)?“ ) true
„Chris“.matches( „Chris(tian)?“ ) true

Beispiel mit optionalen Zeichenfolgen

Beliebige Wiederholungen

Neben dem Fragzeichen an gibt es weitere Quantifizierer. Für eine Zeichenkette X gilt:

 

Quantifizierer Anzahl an Wiederholungen
X? X kommt einmal oder keinmal vor.
X* X kommt keinmal oder beliebig oft vor.
X+ X kommt einmal oder beliebig oft vor.

Tabelle: Quantifizierer im Umgang mit einer Zeichenkette X

Sehen wir uns ein paar Ausdrücke an:

Ausdruck Ergebnis
„Gooooo“.matches( „Go+“ ) true
„Go“.matches( „Go+“ ) true
„G“.matches( „Go+“ ) false
„Go“.matches( „Go*“ ) true
„G“.matches( „Go*“ ) true
„lalala“.matches( „(la)+“ ) true
„yo 4711“.matches( „yo \\d+“ ) true

Tabelle: Beispiele für reguläre Ausdrücke mit Wiederholungen

Eine Sonderform ist X(?!Y) – das drückt aus, dass der reguläre Ausdruck Y dem regulären Ausdruck X nicht folgen darf (die API-Dokumentation spricht von »zero-width negative lookahead«).

Ein weiterer Quantifizierer kann die Anzahl einschränken und die Anzahl eines Vorkommens genauer beschreiben:

  • X{n}. X muss genau n-mal vorkommen.
  • X{n,}. X kommt mindestens n-mal vor.
  • X{n,m}. X kommt mindestens n-, aber maximal m-mal vor.

Beispiel

Eine E-Mail-Adresse endet mit einem Domain-Namen, der zwei oder drei Zeichen lang ist. Ein einfacher regulärer Ausdruck sieht aus aus: „[\\w|-]+@\\w[\\w|-]*\\.[a-z]{2,3}“.

Die Klassen Pattern und Matcher, Pattern.matches(…) bzw. String#matches(…)

Der Aufruf der Objektmethode matches(String) auf einem String-Objekt bzw. das statische Pattern.matches(String, CharSequence) ist nur eine Abkürzung für die Übersetzung eines Patterns und Anwendung von matches() auf einem Matcher-Objekt.

String#matches(…) Pattern.matches(…)
public boolean
matches(String regex) {
return Pattern.matches(regex, this);
}
public static boolean
matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}

Tabelle: Implementierungen der beiden matches(…)-Methoden

Während also die Objektmethode matches(String) von String zu Pattern.matches(String, CharSequence) delegiert, steht hinter der statischen Fassadenmethode in Pattern die wirkliche Nutzung der beiden zentralen Klassen Pattern für das Muster und Matcher für die Mustermaschine. Wenn wir also schreiben „Filk“.matches(„F..k“) ist das äquivalent zu Pattern.matches(„F..k“, „Filk“) und das ist äquivalent zu

Pattern p = Pattern.compile( „F..k“ );
Matcher m = p.matcher( „Filk“ );
boolean b = m.matches();

HTTP Client und WebSocket API in Java 11

Der HTTP-Standard ist schon relativ alt, und die Ursprünge wurden Anfang der 1990er Jahre gelegt. Offiziell wurde die Version 1.0 im Jahr 1996 verabschiedet, also etwa zu gleichen Zeit, als Java 1.0 erschien. 2015 wurde HTTP/2 verabschiedet, und viele Verbesserungen kamen in den Standard. Die Java-Klassen rund um HTTP wurden allerdings nicht aktualisiert, sodass quelloffene Bibliotheken wie Apache HttpComponents (http://hc.apache.org/) oder die Asynchronous Http and WebSocket Client library for Java (http://github.com/AsyncHttpClient/async-http-client) die Lücken füllten.

1.1.1          Modul java.net.http

Statt die existierenden HTTP-Klassen wie HttpURLConnection um die Neuerungen von HTTP/2 zu erweitern, hat Oracle das Modul java.net.http eingeführt. Erstmalig wurde es in Java 9 im »Brutkasten« (engl. incubator) mit ausgeliefert und in Java 11 festgeschrieben. Neben synchronen Aufrufen können auch asynchrone Aufrufe getätigt werden, die ein modernes CompletableFuture lieferrn.

Das Modul besteht aus einem Paket java.net.http und den wesentlichen Typen:

  • HttpClient
  • HttpRequest, HttpResponse
  • WebSocket

Schauen wir uns ein Beispiel an:

try {

  URI uri = new URI( "https://tutego.de/" );

  HttpRequest request = HttpRequest.newBuilder().uri( uri ).GET().build();

  HttpResponse<String> response =

    HttpClient.newHttpClient().send( request, BodyHandlers.ofString() );

  System.out.println( response.body() );

}

catch ( URISyntaxException | IOException | InterruptedException e ) {

  e.printStackTrace();

}

Die Methoden von HttpRequest sind allesamt abstrakt.

abstract class java.net.http.HttpRequest

  • Optional<HttpRequest.BodyPublisher> bodyPublisher()
  • boolean expectContinue()
  • HttpHeaders headers()
  • String method()
  • Builder newBuilder()
  • Builder newBuilder(URI uri)
  • Optional<Duration> timeout()
  • URI uri()
  • Version version()

Führt der HttpClient die Anfrage aus, bekommen wir ein HttpResponse; die Schnittstelle deklariert folgende Methoden:

public interface java.net.http.HttpResponse<T>

  • T body()
  • HttpHeaders headers()
  • Optional<HttpResponse<T>> previousResponse()
  • HttpRequest request()
  • Optional<SSLSession> sslSession()
  • int statusCode()
  • URI uri()
  • Version version()

Was die Methode body() für einen Typ liefert, bestimmt die Parametrisierung von HttpResponse<T>, und die ergibt sich aus dem übergebenen BodyHandler<T> der HttpClient-Methode send(…):

public abstract class java.net.http.HttpClient

  • static HttpClient newHttpClient()
  • HttpResponse<T> send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)

Der BodyHandler ist eine funktionale Schnittstelle mit einer Methode

  • BodySubscriber<T> apply​(HttpResponse.ResponseInfo responseInfo)

Es gibt einige vordefinierte Implementierungen in der Klasse HttpResponse.BodyHandlers:

public static class java.net.http.HttpResponse.BodyHandlers

  • static HBodyHandler<byte[]> ofByteArray()
  • staticBodyHandler<Path> ofFile​(Path file)
  • static HttpRespoBodyHandler<Stream<String>> ofLines()
  • static HBodyHandler<String> ofString()

Weitere Methoden nennt die Javadoc.