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.

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.

Standard Edition (Java SE): OracleJDK und die Kommerzialisierung

Oracle vermarktet auf der Basis vom OpenJDK ihr eigenes Projekt OracleJDK. Ab Java 11 sind das OracleJDK und OpenJDK vom Code her identisch. Das Oracle JDK ist die »offizielle« Version, die die Java-Download-Seite von Oracle anbietet.

Long Term Support (LTS)

Die halbjährlichen Java-Releases haben zur Folge, dass Versionen immer dann veraltet sind, wenn eine neue Version erscheint. In dem Moment, in dem Java 10 kam, war Java 9 veraltet, das gleiche ist bei Java 11, es machte sofort Java 10 zur alten Version. Das allein wäre kein Problem, wenn die älteren Versionen mit Sicherheitsupdates versorgt würden, aber Oracle investiert für die Allgemeinheit in die alten Versionen keine Zeit und Mühe mehr.

Für Unternehmen ist das ein Problem, denn es erzeugt Stress, mit den Änderungen mitziehen zu müssen. Aus diesem Grund bietet Oracle alle drei Jahre eine Java-Version mit Long Term Support (LTS) und versorgt sie mit Updates und Sicherheitspatches. Die nächsten LTS-Versionen nach Java 8 ist Java 11 (September 2018) und dann nach 3 Jahren Java 17 (September 2021). Das ist für weniger agile Unternehmen gut. Oracle will ihre Java SE 8-Implementierung noch bis mindestens Dezember 2020 pflegen, geplant ist bis März 2025[1] und Java 11 bis 2023, beides Releases mit LTS.

Kommerzialisierung vom OracleJDK

Auf den ersten Blick sieht das gut aus: es gibt regelmäßige Updates für agile Unternehmen und die konservativen Unternehmen setzen auf eine LTS-Version. Der Problem ist allerdings, dass alle OracleJDK-Versionen nicht kommerziell eingesetzt werden dürfen; der Hersteller erlaubt die Nutzung nur für „development, testing, prototyping or demonstrating purposes“. Für Java 8 endet die Schonfrist im Januar 2019.

Wer das OracleJDK kommerziell einsetzen möchte, und nicht nur in einer Entwicklungs- oder Testumgebung, muss eine Lizenz von Oracle erwerben. Es wird monatlich abgerechnet, die Vertragszeit ist mindestes in Jahr. Es stehen zwei Modelle zur Auswahl:

Java SE Subscription Java SE Desktop Subscription
Für Serveranwendungen Für Client-Anwendungen
Abrechnung pro Prozessor Abrechnung pro Benutzer
ab 25 USD/Monat, für 1-99 Benutzer ab 2,50 USD/Monat für 1-999 Benutzer/Clients

Tabelle: Zwei Lizenzmodelle für Oracle Java SE

Oracle wendet bei der Java SE Subscription das gleiche Geschäftsmodell wie bei der Oracle-Datenbank an. Wie genau Rechner in der Cloud mit einer unbestimmten Anzahl der Prozessoren abgerechnet werden soll ist noch unklar.[2] Interessenten sollten die „Oracle Java SE Subscription FAQ“ unter http://www.oracle.com/technetwork/java/javaseproducts/overview/javasesubscriptionfaq-4891443.html studieren und Oracle-Berater hinzuziehen. Wer Client- und Serveranwendungen nutzt, muss zweimal bezahlen, statt »write once, run anywhere«, »write once, pay everywhere«.

Die Kosten können sich schnell summieren, doch bekommen Unternehmen damit Support und insbesondere für Java 8 immer noch für einige Jahre Unterstützung. Nachteil ist, dass es das Subscription-Modell nur für die LTS-Versionen gibt, Unternehmen also gezwungen werden, größere Versionssprünge zu machen. Nach Java 11 kommt erst im September 2021 die Version Java 17 mit dem nächsten LTS.

Java Platform, Standard Edition in drei Paketen: JDK, JRE, Server JRE

In der Oracle Java SE-Familie gibt es verschiedene Ausprägungen: das JDK und das JRE. Da diejenigen, die Java-Programme nur laufen lassen möchten, nicht unbedingt alle Entwicklungstools benötigen, hat Oracle Pakete geschnürt:

  • Mit dem Java Development Kit (JDK) lassen sich Java SE-Applikationen entwickeln. Dem JDK sind Hilfsprogramme beigelegt, die für die Java-Entwicklung nötig sind. Dazu zählen der essenzielle Compiler, aber auch andere Hilfsprogramme, etwa zur Signierung von Java-Archiven oder zum Start einer Management-Konsole. In den Versionen Java 1.2, 1.3 und 1.4 heißt das JDK Java 2 Software Development Kit (J2SDK), kurz SDK, ab Java 5 heißt es wieder JDK.
  • Das Java SE Runtime Environment (JRE) enthält genau das, was zur Ausführung von Java-Programmen nötig ist. Die Distribution umfasst nur die JVM und Java-Bibliotheken, aber weder den Quellcode der Java-Bibliotheken noch Tools wie Management-Tools.
  • Das Server JRE ist für 64-Bit-Server-Umgebungen gedacht und enthält anders als das herkömmliche JRE nicht den Auto-Updater, das Plugin-Tool oder einen Installer. Wie das JDK enthält es hingegen eine Management-Konsole.

Die drei Produkte können von der Webseite http://www.oracle.com/technetwork/java/javase/downloads/ bezogen werden – die Lizenzbestimmungen sind einzuhalten.

Oracle JDK 10 Certified System Configurations

Eine ideale, perfekt getestete und unterstützte Umgebung gilt als Oracle Certified System Configuration. Das ist eine Kombination aus Betriebssystem mit installierten Service-Packs; das beschreibt das Unternehmen unter https://www.oracle.com/technetwork/java/javase/documentation/jdk10certconfig-4417031.html. Bei gemeldeten Bugs auf nicht zertifizierten Plattformen kann dann schnell ein »Sorry, das ist eine nicht unterstützte Plattform, das schauen wir uns nicht weiter an« folgen. Bei Linux ist zum Beispiel die Gentoo-Distribution nicht in der Liste, wäre also »Not certified on Oracle VM«. Das heißt nicht, dass Java dort nicht zu 100 % läuft, nur, dass es im Fehlerfall eben keinen Fix geben muss.

[1] http://www.oracle.com/technetwork/java/javase/javaclientroadmapupdate2018mar-4414431.pdf

[2] http://houseofbrick.com/the-oracle-parking-garage/ ist dann gar nicht mehr zum Lachen …

Standard Edition (Java SE): OpenJDK

Das freie und unter der GPL stehende OpenJDK (http://openjdk.java.net/) bildet die Referenzimplementierung für Java SE. Alle Entwicklungen finden dort statt. Der Fortschritt ist live zu beobachten, regelmäßig fixen und erweitern Hunderte von Entwicklern die Codebasis. Die Quellen für das OpenJDK lassen sich im Mercurial Repository unter http://hg.openjdk.java.net/jdk/jdk11 einsehen, ein Wechsel auf GitHub wird diskutiert.

OpenJDK-Builds von Oracle

Übersetzte Versionen sind über http://jdk.java.net/ verklinkt, es gibt von Oracle OpenJDK x64-Builds für

  • Windows
  • Linux
  • macOS
  • Alpine Linux

OpenJDK-Builds von AdoptOpenJDK

Auch unter https://adoptopenjdk.net/ lässt sich für unterschiedliche Betriebssysteme eine Version herunterladen. Angeboten werden x64-Builds für

  • Windows
  • Linux
  • macOS
  • Linux s390x
  • Linux ppc64le
  • Linux aarch64
  • Linux arm32
  • AIX ppc64

AdoptOpenJDK ist eine Serverfarm, die regelmäßig Builds vom OpenJDK baut, dazu weitere Software wie die JavaFX-Implementierung OpenJFX und alternative Laufzeitumgebungen wie Eclipse OpenJ9 einbindet.

Weitere OpenJDK-Builds

Das Unternehmen Azul bietet ebenfalls Builds an, auch lässt sich ein Support-Vertrag abschließen: http://www.azul.com/downloads/zulu/. Neben den Plattformen Windows, Linux und macOS gibt es von Azul ebenfalls Docker-Images.

Red Hat bietet neben Linux auch eine Windows-Version vom OpenJDK an: http://developers.redhat.com/products/openjdk/overview/.

Apple pflegte lange Zeit eine komplett eigene JVM, bis Apple den Code an Oracle für das OpenJDK übergab. Auch Google setzt bei Android neuerdings auf das OpenJDK.

OpenJDK unter Windows installieren

Während Oracle ein JDK und ein JRE anbietet und alles mit einem Installationsprogramm ausliefert, kommt das OpenJDK nur als  komprimiertes Archiv. Wir laden von http://jdk.java.net/10/ für Windows die TAR.GZ-Datei, hinter der eine Datei wie openjdk-10.0.2_windows-x64_bin.tar.gz steckt. Wir benötigen einen Entpacker wie 7-Zip für das unter Windows ehr ungewöhnliche Dateiformat.

Programme und Ordner im Java-Verzeichnis

Nach dem Auspacken entsteht ein Ordner jdk-10.0.2 mit dem OpenJDK. Es enthält ausführbare Programme wie Compiler und Interpreter sowie die Bibliotheken und Quellcodes. Wenn wir die Rechte haben, wollen wir den JDK-Ordner in der Windows-Programmorder setzen und für die weiteren Beispiele folgenden Ort annehmen: C:\Programme\Java\jdk-10.

Ordner/Datei Bedeutung
bin Hier befinden sich Entwicklungswerkzeuge, unter anderem der Interpreter java und beim JDK der Compiler javac.
conf Konfigurationsdateien, Anpassungen sind hier selten nötig.
include Dateien für die Anbindung von Java an C(++)-Programme
jmods Java-Module vom JDK, etwa das Basis-Modul
legal eine Reihe von COPYRIGHT-Textdateien
lib interne JDK-Tools
lib/src.zip Archiv mit dem Quellcode der öffentlichen Bibliotheken*
release Datei mit Schüssel-Wert-Paaren

Tabelle: Ordnerstruktur

Testen der Installation und Pfade setzen

Gehen wir in das bin-Verzeichnis C:\Program Files\Java\jdk-10\bin können wir aufrufen:

C:\Program Files\Java\jdk-10\bin>java -version

openjdk version „10.0.2“ 2018-07-17

OpenJDK Runtime Environment 18.3 (build 10.0.2+13)

OpenJDK 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode)

Wir erweitern als nächstes die PATH-Variable um das bin-Verzeichnis. Wenn wir jetzt eine Shell öffnen, können wir javac für den Java-Compiler aufrufen oder java für die Laufzeitumgebung. Auch kann eine Entwicklungsumgebung wie Eclipse gestartet werden.

OpenJDK deinstallieren

Nach dem Löschen des Ordners ist Java deinstalliert.

Download der Dokumentation

Die API-Dokumentationen der Standardbibliothek und die der Tools sind kein Teil des JDK; eine Trennung ist sinnvoll, denn sonst würde der Download nur unnötig größer, die Dokumentation kann schließlich auch online angeschaut werden. Die Hilfe kann online unter https://docs.oracle.com/javase/10/docs/api/ eingesehen oder als ZIP-Datei extra bezogen und lokal ausgepackt werden.

Die Java SE-Plattform, Versionen und Release-Zyklen

Die Java Platform, Standard Edition (Java SE) ist eine Systemumgebung zur Entwicklung und Ausführung von Java-Programmen. Java SE enthält alles, was zur Entwicklung von Java-Programmen nötig ist. Obwohl die Begrifflichkeit etwas unscharf ist, lässt sich die Java SE als Spezifikation verstehen und nicht als Implementierung. Damit Java-Programme übersetzt und ausgeführt werden können, müssen aber ein konkreter Compiler, Interpreter und die Java-Bibliotheken auf unserem Rechner installiert sein. Es gibt unterschiedliche Implementierungen, etwa das OpenJDK.

Versionen der Java SE

Am 23. Mai 1995 stellte damals noch Sun Java erstmals der breiten Öffentlichkeit vor. Seitdem ist viel passiert, und in jeder Version erweiterte sich die Java-Bibliothek. Dennoch gibt es von einer Version zur nächsten kaum Inkompatibilitäten, und fast zu 100 % kann das, was unter Java n übersetzt wurde, auch unter Java n + 1 übersetzt werden – nur selten gibt es Abstriche in der Bytecode-Kompatibilität.[1]

Version Datum Einige Neuerungen oder Besonderheiten
1.0 Januar 1995 Erste Version. Folgende 1.0.x-Versionen lösen diverse Sicherheitsprobleme.
1.1 Februar 1997 Neuerungen bei der Ereignisbehandlung, beim Umgang mit Unicode-Dateien (Reader/Writer statt nur Streams), außerdem Datenbankunterstützung via JDBC sowie innere Klassen und eine standardisierte Unterstützung für Nicht-Java-Code (nativen Code)
1.2 November 1998 Es heißt nun nicht mehr JDK, sondern Java 2 Software Development Kit (SDK). Swing ist die neue Bibliothek für grafische Oberflächen, und es gibt eine Collection-API für Datenstrukturen und Algorithmen.
1.3 Mai 2000 Namensdienste mit JNDI, verteilte Programmierung mit RMI/IIOP, Sound-Unterstützung
1.4 Februar 2002 Schnittstelle für XML-Parser, Logging, neues IO-System (NIO), reguläre Ausdrücke, Assertions
5 September 2004 Das Java-SDK heißt wieder JDK. Neu sind generische Typen, typsichere Aufzählungen, erweitertes for, Autoboxing, Annotationen.
6 Ende 2006 Webservices, Skript-Unterstützung, Compiler-API, Java-Objekte an XML-Dokumente binden, System Tray
7 Juli 2011 Kleine Sprachänderungen, NIO2, erste freie Version unter der GNU General Public License (GPL)
8 März 2014 Sprachänderungen Lambda-Ausdrücke, Stream-API
9 September 2017 Modularisierung von Anwendungen
10 März 2018 Lokale Variablendeklarationen mit var
11 September  2018 Entfernung des java.ee-Moduls

Tabelle 1.1: Neuerungen und Besonderheiten der verschiedenen Java-Versionen

Die Produktzyklen zeigen einige Sprünge, besonders Java 9 wurde zweimal verschoben.

Oracle und Sun waren sehr lange konservativ darin, das Bytecodeformat zu ändern, sodass eine ältere JVM im Prinzip Programme einer neuen Java-Version ausführen konnte. Aber gerade in den Versionen Java 7 und Java 8 gab es doch einige Neuerungen, die die Aufwärtskompatibilität brechen; eine JVM der Version 7 kann also keine Programme mehr ausführen, die ein Java 8-Compiler übersetzt hat. Da einige Teile aus den Java 11-Bibliotheken entfernt wurden, ist auch dadurch die Abwärtskompatibilität eingeschränkt.

Feature-Release vs. zeitorientiertes Release

20 Jahre lang bestimmten Features die Freigabe von neuen Java-Versionen; die Entwickler setzten bestimmte Neuerungen auf die Wunschliste, und wenn alle Features realisiert und getestet waren, erfolgte die allgemeine Verfügbarkeit (eng. general availability, kurz GA). Hauptproblem dieses Feature-basierten Vorgehensmodells waren die Verzögerungen, die mit Problemen bei der Implementierung einhergingen. Vieldiskutiert war das Java 9-Release, weil es unbedingt ein Modulsystem enthalten sollte.

Die Antwort auf diese Probleme, und der Wusch der Java-Community nach häufigeren Releases, beantwortet Oracle mit der „JEP 322: Time-Based Release Versioning“[2]. Vier Releases sind im Jahr geplant:

  • Im März und September erscheinen Haupt-Releases, wie Java 10, Java 11.
  • Updates erscheinen einen Monat nach einem Haupt-Release und dann im Abstand von drei Monaten. Im April und Juli erscheinen folglich Updates 10.0.1 und 10.0.2. Für Java 11 sind das Oktober 2018 und Januar 2019.

Anders gesagt: Im Halbjahresrhythmus gibt es Updates, die es Oracle erlauben, in der schnelllebigen IT-Zeit die die Sprache und Bibliotheken weiter zu entwickeln und neue Spracheigenschaften zu integrieren. Kommt es zu Verzögerungen, hält das nicht gleich das ganze Release auf. Java 10 war im März 2018 das erste Release nach diesem Zeitplan, Java 11 folgte im September 2018.

Codenamen, Namensänderungen und Vendor-Versionsnummer

Die ersten Java-Versionen waren Java 1.0, Java 1.1, usw. Mit Java 5 entfiel das Präfix »1.« in der Versionskennung des Produkts, sodass es einfach nur Java 5, Java 6, etc. hieß; in den Entwicklerversionen blieb die Schreibweise mit der »1.« aber weiterhin bis Java 9 gültig.[3] In Java 10 kommt durch das „Time-Based Release Versioning“[4] eine Vendor-Kennung hinzu, sodass alternativ zu Java 11 auch von 18.9 die Sprache ist; gut lässt sich daran die Jahreszahl und der Monat vom Release ablesen.

[1] Die Seite http://tutego.de/go/migratingtojava5 zeigt auf, wie Walmart der Umstieg auf Java 5 gelang – relativ problemlos: »[…] the overall feeling is that a migration to Java 1.5 in a production environment can be a mostly painless exercise.«

[2]     http://openjdk.java.net/jeps/322

[3] Siehe dazu http://docs.oracle.com/javase/1.5.0/docs/relnotes/version-5.0.html.

[4] http://openjdk.java.net/jeps/322

Die Entwicklung von Java und seine Zukunftsaussichten

Vor 20 Jahren war ein großer Pluspunkt die Einfachheit im Vergleich zum Vorgänger C++ und das Fehlen von »gefährlichen« syntaktischen Konstrukten. So schrieb einer der Sprachväter, James Gosling – der nach der Übernahme von Sun durch Oracle das Unternehmen verließ –, schon 1997:

»Java ist eine Arbeitssprache. Sie ist nicht das Produkt einer Doktorarbeit, sondern eine Sprache für einen Job. Java fühlt sich für viele Programmierer sehr vertraut an, denn ich tendiere stark dazu, Dinge zu bevorzugen, die schon oft verwendet wurden, statt Dingen, die eher wie eine gute Idee klangen.«[1]

Der Wunsch nach einer einfachen Sprache besteht bis heute, allerdings ist in den letzten 20 Jahren viel passiert, und Java deutlich komplexer geworden. Bedeutende Sprachänderungen gab es in Java 5 (also etwa zehn Jahre nach der Einführung von Java) und in Java 8. Das Modulsystem in Java 9 bringt ebenfalls neue Herausforderung.

Bei der Dreifaltigkeit der Java-Plattform – 1. Java als Programmiersprache, 2. den Standardbibliotheken und 3. der JVM als Laufzeitumgebung – lässt sich erkennen, dass es große Bewegung bei den unterschiedlichen Programmiersprachen auf der Java-Laufzeitumgebung gibt. Es zeichnet sich ab, dass Java-Entwickler weiterhin in Java entwickeln werden, aber eine zweite Programmiersprache auf der JVM zusätzlich nutzen. Das kann JavaScript, Groovy, Kotlin, Scala, Jython, JRuby oder eine andere JVM-Sprache sein. Dadurch, dass die alternativen Programmiersprachen auf der JVM aufsetzen, können sie alle Java-Bibliotheken nutzen und daher Java als Programmiersprache in einigen Bereichen ablösen. Dass die alternativen Sprachen auf die üblichen Standardbibliotheken zurückgreifen, funktioniert reibungslos, allerdings ist der umgekehrte Weg, dass etwa Scala-Bibliotheken aus Jython heraus genutzt werden, (noch) nicht standardisiert. Bei der .NET-Plattform klappt das besser, und hier ist es wirklich egal, ob C# oder VB.NET Klassen deklariert oder nutzt.

Als die Übernahme von Sun vor der Tür stand, zeigte Oracle sich sehr engagiert gegenüber den Sun-Technologien. Nach der Übername 2010 wandelt sich das Bild etwas, und Oracle hat eher für negative Schlagzeilen gesorgt, etwa als es die Unterstützung für OpenSolaris eingestellt hat, seitdem es MySQL zurückfährt und als Gefahr für die eigene Datenbank sieht und weil es OpenOffice erst spät an Apache übergeben hat (als sich LibreOffice schon verselbstständigt hatte). Auch was die Informationspolitik und Unterstützung von Usergroups angeht, verhält sich Oracle ganz anders als Sun. Durch die Klage gegen Google wegen Urheberrechtsverletzungen in Android machte sich Oracle auch keine Freunde. Android gilt als Beweis, dass Java auf dem Client tatsächlich erfolgreich ist. Anlässlich der Sicherheitslücken in Java machte das Unternehmen ebenfalls keine gute Figur, insgesamt dürfte auf einer Bewertung zu finden sein: »Oracle hat sich bemüht, den Anforderungen gerecht zu werden.«

Fast 10 Jahre nach der Übernahm gibt es neue radikale Änderungen. Erstmals entfernte Oracle Teile der Java SE-Bibliothek, wie CORBA-Unterstützung, JAXB, JAX-RS (Web-Services), Applets, Java Web Start und JavaFX, auch die JavaScript-Engine soll verschwinden. Damit ist die bisher heilige Abwärtskompatibilität aufgegeben – Java 11 kann nicht in jedem Fall ältere Java-Software ausführen. Ein weiter Schock ist die Kommerzialisierung. Oracle bringt mit dem OracleJDK alle halbe Jahre ein neues Release heraus, was jedoch ab Java 11 nur für „development, testing, prototyping or demonstrating purposes“ genutzt werden darf, also nicht mehr kommerziell; für eine kommerzielle Nutzung fallen Gebühren an.[2] Da das OracleJDK allerdings nur eine Java SE-Implementierung von vielen ist, gibt es gute Alternativen, wie das OpenJDK.

[1] Im Original: »Java is a blue collar language. It’s not PhD thesis material but a language for a job. Java feels very familiar to many different programmers because I had a very strong tendency to prefer things that had been used a lot over things that just sounded like a good idea.« (http://www.computer.org/portal/web/csdl/doi/10.1109/2.587548)

[2] http://www.oracle.com/technetwork/java/eol-135779.html

Es war einmal: RIA mit JavaFX

Rich Internet Applications (RIA) sind grafisch anspruchsvolle Webanwendungen, die in der Regel Daten aus dem Internet beziehen. Viele Jahre beherrschte Adobe Flash hier fast zu 100 % das Feld, und Microsoft spielte mit Silverlight zeitweilig mit. Auch Oracle wollte aus strategischen Gründen das Feld nicht den Mitbewerbern überlassen. Als sich abzeichnete, dass Applets zu unflexibel für komplexe GUI-Anwendungen sind, veröffentlichte Oracle nach längerer interner Projektphase Ende 2008 die JavaFX-Plattform. JavaFX ist ein ganz neuer GUI-Stack und komplett von Swing und AWT entkoppelt.

In der ersten JavaFX-Version gehörte die Programmiersprache JavaFX Script mit dazu, doch ab Version JavaFX 2 hat Oracle die Richtung geändert: JavaFX ist nun eine pure Java-Bibliothek, und JavaFX Script verschwand. Mit Skriptsprachen auf der Java-VM ist jedoch ein vergleichbares »Feeling« gewährleistet.

Eine Zeit lang sah es so aus, als ob JavaFX den GUI-Stack AWT/Swing beerben könnte. Swing ist eine GUI-Bibliothek, die schon seit Java 1.2 (1998) zum Standard gehört, doch seit 10 Jahren keinen nennenswerten Features mehr bekommen hat, wohl aber regelmäßig Bugs gefixt werden. Nach anfänglichem Hype um JavaFX hat Oracle viele Entwickler abgezogen und auch JavaFX auf das Abstellgleis gestellt. Für Oracle spielen Desktop-Technologien keine Rolle mehr, weshalb sich Oracle 2018 ganz von JavaFX verabschiedet hat: JavaFX wurde aus der Java SE entfernt und ist kein Teil mehr von Java 11. Oracle hat JavaFX in das OpenJFX (https://wiki.openjdk.java.net/display/OpenJFX/Main) überführt, wo es als Open-Source-Projekt weiterentwickelt wird – ein Mirror ist https://github.com/teamfx. Für JavaFX-Fans ist das eher ein Vorteil als Nachteil, denn eine Entkopplung ermöglicht eine flexiblere Weiterentwicklung; OpenJFX ist dann ein Modul wie jedes andere auch.

Es war einmal: Applets

Es ist nicht untertrieben, dem Web eine Schlüsselposition bei der Verbreitung von Java zuzuschreiben. Populär wurde Java Mitte der 1990er Jahre durch Applets – Java-Programme, die ein Browser ausführt. Eine HTML-Datei referenzierte das Applet, es bekam auf der Webseite einen Platz zugewiesen, der Browser holte sich eigenständig die Klassendateien und Ressourcen über das Netz und führte sie in der JVM aus.

Im Laufe ging die Bedeutung für Java-Applets immer weiter zurück, und das lag an der Sprache, die ebenfalls 1995 erschien: JavaScript. Java-Applets brachten erstmals Dynamik und bewegte Grafik in die bis dahin statischen Webseiten, doch als dann die Web-Standards CSS (1996) und SVG auftauchen (2001), setzen immer mehr Web-Entwickler eine Kombination von JavaScript mit diesen Standards. Denn Java-Applets haben, genauso wie Flash oder  Silverlight alle ein Problem: Sie benötigen ein Browser-Plugin. Das macht sie unattraktiv für Unternehmen, da im Internet kein Kunde ausgeschlossen werden soll. Früher, als die Webstandards noch nicht so weit entwickelt waren, brachten Flash und Silverlight Dynamik auf die Webseite, doch heute sind aufwändige Webanwendungen mit JavaScript und HTML 5/CSS3 realisierbar. Auch Microsoft stoppte bei Silverlight 5 die Entwicklung und bevorzugt nun Lösungen auf der Basis von JavaScript + HTML 5 + CSS3, insbesondere für mobile Endgeräte, da den Redmondern klar ist, dass es nie Silverlight auf dem iPhone oder Android geben wird.[1] Adobe selbst beginnt mit Konvertern von Flash nach HTML 5/CSS3/JavaScript und zeigt damit auch die Zukunft auf.

Es ist lange her, dass die frühen Browser Netscape und Internet Explorer eine JVM integrierten – irgendwann haben die Hersteller die Java-Laufzeitumgebung entfernt. Eine Zeit lang lieferte Oracle das Java Plug-in aus, um die eigene JVM in den Browser zu integrierten. Moderne Browser unterstützen das Java Plug-in jedoch nicht mehr. Wegen fehlender Unterstützung in den Browsern – und den gute Web-Standards – markierte Oracle in Java 9 Applets als veraltet und entfernte sie komplett in Java 10. Java Web Start war eine Alternative ähnlich den Applets, mit der sich Programme aus dem Internet laden lassen, allerdings ist auch Java Web Start nicht mehr Teil von Java 11.

[1] Mit https://xamarin.com/platform gibt es interessante Ansätze.

JSON-Serialisierung mit JSR 353/374 und JSR 367

Im Internet hat JSON das XML-Format zwecks Objektübertragung zwischen Server und Browser fast vollständig verdrängt. Das liegt daran, dass ein Browser JSON-Strings direkt in JavaScript-Objekte konvertieren kann, XML-Dokumente aber erst aufwändiger verarbeitet werden müssen.

Neben dem Einsatzgebiet im Internet bietet JSON auch ein kompaktes Format, um etwa lokale Konfigurationsdateien zu kodieren.

JSON im Kontext von JavaScript

Nehmen wir folgende Zeile JavaScript-Code, die ein Person-Objekt mit zwei Properties für Name und Alter definiert. Eine Property wird über ein Schlüssel-Wert-Paar beschrieben:

var person = { „name“ : „Michael Jackson“, „age“ : 50 };

Die Definition eines Objekts geschieht in der JSON (JavaScript Object Notation). Als Datentypen unterstützt JSON Zahlen, Wahrheitswerte, Strings, Arrays, null und Objekte – wie unser Beispiel zeigt. Die Deklarationen können geschachtelt sein, um Unterobjekte aufzubauen.

Zum Zugriff auf die JSON-Daten kommt der Punkt zum Einsatz, sodass der Name nach der Auswertung durch person.name zugänglich ist.

Eine Personenbeschreibung wie diese kann in einem String stehen, der von JavaScript zur Laufzeit ausgewertet wird.

var json = ‚person = { „name“ : „Michael Jackson“, „age“ : 50 };‘;

Der Zugriff auf person.name liefert wie vorher den Namen, denn nach der Auswertung mit eval(…) wird JavaScript ein neues Objekt mit person im Kontext anlegen.

JSON ist besonders praktisch, wenn es darum geht, Daten zwischen einem Server und einem Browser mit JavaScript-Interpreter auszutauschen. Denn wenn der String json nicht von Hand mit einem String initialisiert wurde, sondern ein Server die Zeichenkette person = { … }; liefert, haben wir das, was heutzutage in modernen Ajax-Webanwendungen passiert.

Die letzte Frage ist nun, wie elegant der Server Zeichenketten im Datenaustauschformat JSON erzeugt und so Objekte überträgt. Den String per Hand aufzubauen ist eine Lösung, aber es geht besser.

JSON-Verarbeitung mit der Java API for JSON Processing

Zum Verarbeiten von JSON-Dokumenten gibt es in der Java SE keine Standardklassen, sodass sich eine Reihe von Open-Source-Bibliotheken herausgeprägt haben; Jackson (http://tutego.de/go/jackson) gehört zu den populärsten Lösungen. 2013 wurde dann die JSR 353, »Java API for JSON Processing«, verabschiedet, die Teil der Jakarta EE 7 ist, 2014 dann das JSR 374 »Java API for JSON Processing 1.1«.

Wir können die JSON-API in unseren Java SE-Programmen nutzen, müssen dafür aber Java-Archive im Klassenpfad einbinden. Die Referenzimplementierung befindet sich unter https://javaee.github.io/jsonp/. Am einfachsten haben es Maven-Nutzer, sie binden in ihre POM die Abhängigkeiten auf den „Default provider for JSR 374:Java API for Processing JSON“ ein:

<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1</version>
</dependency>

Die Implementierung hat eine Abhängigkeit zur JSON-API javax.json:javax.json-api, die wir dann nicht unbedingt selbst in die POM mit aufnehmen müssen.

Aufbau von JSON-Objekten, Formatieren und Parsen

Der Typ JsonObject ist in der API zentral, denn er definiert ein hierarchisches Model mit den Schlüssel-Wert-Paaren eines JSON-Objekts. Um ein JsonObject aufzubauen, können wir über den JsonObjectBuilder gehen, oder wir lassen den Parser JsonReader aus einer String-Repräsentation ein JsonObject erzeugen. Zum formatierten Schreiben in einen Ausgabestrom können wir einen einfachen JsonWriter von der Json-Klasse holen – es geht aber noch einfacher über toString() – oder über die JsonWriterFactory arbeiten, falls wir eine hübsche eingerückte Ausgabe wünschen. Ein Beispiel:

Point p = new Point( 10, 20 );

JsonObjectBuilder objBuilder = Json.createObjectBuilder()
.add( „x“, p.x )
.add( „y“, p.y );
JsonObject jsonObj = objBuilder.build();

System.out.println( jsonObj );  // {„x“:10,“y“:20}

Json.createWriter( System.out ).write( jsonObj ); // {„x“:10,“y“:20}

Map<String, Boolean> config = new HashMap<>();
config.put( JsonGenerator.PRETTY_PRINTING, true );
JsonWriterFactory writerFactory = Json.createWriterFactory( config );

StringWriter out = new StringWriter();
writerFactory.createWriter( out ).write( jsonObj );
System.out.println( out );  // {\n    „x“: 10, …

JsonReader reader = Json.createReader( new StringReader( out.toString() ) );
System.out.println( reader.readObject().getInt( „x“ ) ); // 10

Soll der assoziierte Wert ein Array sein, so wird das Array mit Json.createArrayBuilder().add(..).add(..) aufgebaut und gefüllt.

JSON-Streaming API

So wie es für XML eine Pull-API gibt, existiert sie auch für JSON-Dokumente; das ist von Vorteil, wenn die Daten sehr umfangreich sind. Ein Beispiel zeigt das sehr gut:

URL url = new URL( „https://data.cityofnewyork.us/api/views/25th-nujf/rows.json?accessType=DOWNLOAD“ );

try ( JsonParser parser = Json.createParser( url.openStream() ) ) {
while ( parser.hasNext() ) {
switch ( parser.next() ) {
case KEY_NAME:
case VALUE_STRING:
System.out.println( parser.getString() );
break;
case VALUE_NUMBER:
System.out.println( parser.getBigDecimal() );
break;
case VALUE_TRUE:
System.out.println( true );
break;
case VALUE_FALSE:
System.out.println( false );
break;
case VALUE_NULL:
case START_ARRAY:
case END_ARRAY:
case START_OBJECT:
case END_OBJECT:
// Ignore
}
}
}

Zum Schreiben gibt es Vergleichbares, einen JsonGenerator; die API ist selbsterklärend:

JsonGenerator gen = Json.createGenerator( System.out );
gen.writeStartArray();
gen.writeStartObject();
gen.write( „x“, „12“ );
gen.write( „y“, „2“ );
gen.writeEnd();
gen.writeStartObject()
.write( „x“, „99“ )
.write( „x“, „123“ )
.writeEnd();
gen.writeEnd().close();
// [{„x“:“12″,“y“:“2″},{„x“:“99″,“x“:“123″}]

Der JsonGenerator ist AutoCloseable, sodass er gut in einem try-mit-Ressoucen-Block eingesetzt werden kann.

Objekt-JSON-Mapping

Das JSR 353 beschreibt kein automatisches Objekt-JSON-Mapping, wie JAXB das Objekt-XML-Mapping ermöglicht. Das wird im Standard JSR 367, »Java API for JSON Binding (JSON-B)« definiert. Die Website http://json-b.net/ liefert Hintergrundinformationen, die Referenzimplementierung ist Eclipse Yasson.

Um das mit einem Beispiel testen zu können, nehmen wie Yasson in unsere POM mit auf:

<dependency>

<groupId>org.eclipse</groupId>

<artifactId>yasson</artifactId>

<version>1.0.1</version>

</dependency>

Die Dependency selbst hat eine Maven Abhängigkeit auf javax.json.bind:javax.json.bind-api, also der eigentliche API.

Zur Abbildung bauen wir uns ein Beispielobjekt und bringen es in das JSON-Format. Im zweiten Schritt übertragen wir den JSON-String wieder auf das Objekt:

public class JsonbDemo {

public static class EvilLaboratory {

public String from;

public double volume;

public boolean didPayElectricityBill;

public List<String> items;

}

public static void main( String[] args ) {

EvilLaboratory lab1 = new EvilLaboratory();

lab1.from = „Frank“;

lab1.didPayElectricityBill = true;

lab1.volume = 12442.33;

lab1.items = List.of( „corpses“, „animals“ );

 

Jsonb jsonbuilder = JsonbBuilder.create();

String jsonString = jsonbuilder.toJson( lab1 );

System.out.println( jsonString );

 

EvilLaboratory lab2 = jsonbuilder.fromJson( jsonString, EvilLaboratory.class );

System.out.printf( „from=%s, volume=%s, didPayElectricityBill=%s, items=%s“,

lab2.from, lab2.volume, lab2.didPayElectricityBill, lab2.items );

}

}

Insel: Apache Commons CSV

Eine freie Java-Bibliothek zum Lesen und Schreiben von CSV-Dokumenten ist Apache Commons CSV (https://commons.apache.org/proper/commons-csv/). Sie verwaltet unterschiedliche CSV-Formate wie Microsoft Excel, RFC 4180.

Auf der Webseite gibt es einen Download-Link für das Java-Archiv, doch am einfachsten ist die Einbindung über die Maven-POM-Datei – wir fügen folgende Abhängigkeit hinzu:

<dependency>

<groupId>org.apache.commons</groupId>

<artifactId>commons-csv</artifactId>

<version>1.5</version>

</dependency>

Über die Abhängigkeit bekommen wir neue Typen im Paket org.apache.commons.csv. Mit dem CSVPrinter lassen sich CSV-Dokumente korrekt erzeugen, und er behandelt auch Fluchtsymbole korrekt. Erzeugen wir ein CSV-Dokument im Speicher:

StringBuilder appendable = new StringBuilder();

CSVFormat csvFormat = CSVFormat.EXCEL.withHeader( „DisplayName“, „ISO3Language“ );

try ( CSVPrinter csvPrinter = new CSVPrinter( appendable, csvFormat ) ) {

for ( Locale l : Arrays.copyOf( Locale.getAvailableLocales(), 5 ) )

csvPrinter.printRecord( l.getDisplayName(), l.getISO3Language() );

}

Starten wir das Programm erscheint auf der Konsole:

DisplayName,ISO3Language

„“,

Norwegisch Nynorsk,nno

Arabisch (Jordanien),ara

Bulgarisch,bul

Kabuverdianu,kea

Lesen wir das Dokument wieder ein und greifen dazu auf die Klasse CSVParser zurück. Es gibt zwei Ansätze:

  1. Da CSVParser ein Iterable<CSVRecord> ist, können wir direkt die Zeilen mit dem erweiterten for Das bietet sich für größere Datenmengen an.
  2. Ist die Anzahl der Daten überschaubar, liefert getRecords() eine gefüllte util.List<CSVRecord> mit alle Zeilen. Das kann praktisch sein, denn eine List hat eine stream()-Methode, sodass die Zeilen praktisch als Stream<CSVRecord> weiterverarbeitet werden können.

Zum Programm:

try ( StringReader reader = new StringReader( appendable.toString() );

CSVParser csvParser = new CSVParser( reader,

CSVFormat.EXCEL.withFirstRecordAsHeader() ) ) {

for ( CSVRecord csvRecord : csvParser )

System.out.printf( „Record=%d – %s, %s%n“,

csvRecord.getRecordNumber(),

csvRecord.get( 0 ), csvRecord.get( 1 ) );

}

Die Ausgabe ist:

Record=1 – ,

Record=2 – Norwegisch Nynorsk, nno

Record=3 – Arabisch (Jordanien), ara

Record=4 – Bulgarisch, bul

Record=5 – Kabuverdianu, kea

Automatisches Feststellen der Typen mit var

Java 10 hat eine Erweiterung gebracht, dass der Variablentyp bei gewissen Deklarationen entfallen kann und wir einfach stattdessen var nutzen können:

var name = „Barack Hussein Obama II“;

var age = 48;

var income = 400000;

var gender = ‚m‘;

var isPresident = false;

Wir sehen, dass im Gegensatz zu unserem vorherigen Beispiel nicht mehr die Variablentypen wie String oder int bei der Variablendeklaration explizit im Code stehen, sondern nur noch var. Das heißt allerdings nicht, dass der Compiler die Typen offen lässt! Der Compiler braucht zwingend die rechte Seite neben dem Gleichheitszeichen, um den Typ feststellen zu können, das nennt sich Local-Variable Type Inference. Daher gibt es in unserem Programm auch eine Unstimmigkeit, nämlich bei var income = 400000, die gut ein Probleme mit var aufzeigt: die Variable ist kein double mehr wie vorher, sondern 400000 ist ein Ganzzahl-Literal, weshalb der Java-Compiler der Variablen income den Typ int gibt.

Die Nutzung von var soll Entwicklern helfen, Code kürzer zu schreiben, insbesondere, wenn der Variablenname schon eindeutig auf den Typ hinweist. Finden wie eine Variable text vor, ist der Typ String naheliegend, genauso wie age ein int ist, oder ein Präfix wie is oder has auf eine boolean-Variable hinweist. Aber wenn var auf die Kosten der Verständlichkeit geht, darf die Abkürzung nicht eingesetzt werden. Auch der Java-Compiler gibt Schranken:

  • var ist nur dann möglich, wenn eine Initialisierung einen Typ vorgibt. Eine Deklaration der Art var age; ohne Initialisierung ist nicht möglich und führt zu einem Compilerfehler.
  • var kann nur bei lokalen Variablen eingesetzt werden, wo der Bereich überschaubar ist. Es gibt aber noch viele weitere Stellen, wo in Java Variablen deklariert werden – dort ist var nicht möglich.

Sprachvergleich: Java ist mit var relativ spät dran.[1] Andere statisch getypte Sprachen bieten die Möglichkeit schon länger, etwa C++ mit auto oder C# auch mit var. Auch JavaScript nutzt var, allerdings in einem völlig anderen Kontext: in JavaScript sind Variablen erst zur Laufzeit getypt, und alle Operationen werden erst zur Ausführungszeit geprüft, während Java die Typsicherheit mit var nicht aufgibt.

[1] http://openjdk.java.net/jeps/286

Inselupdate: Microsoft und Java

In der Anfangszeit verursachte Microsoft einigen Wirbel um Java. Mit Visual J++ (gesprochen »Jay Plus Plus«) bot Microsoft schon früh einen eigenen Java-Compiler (als Teil des Microsoft Development Kits) und mit der Microsoft Java Virtual Machine (MSJVM) eine eigene schnelle Laufzeitumgebung. Das Problem war nur, dass Dinge wie RMI und JNI absichtlich fehlten[1] – JNI wurde 1998 nachgereicht. Entgegen allen Standards führte der J++-Compiler neue Schlüsselwörter wie multicast und delegate ein. Außerdem fügte Microsoft einige neue Methoden und Eigenschaften hinzu, zum Beispiel J/Direct, um der plattformunabhängigen Programmiersprache den Windows-Stempel zu verpassen. Mit J/Direct konnten Programmierer aus Java heraus direkt auf Funktionen der Win32-API zugreifen und damit reine Windows-Programme in Java programmieren. Durch Integration von DirectX sollte die Internet-Programmiersprache Java multimediafähig gemacht werden. Das führte natürlich zu dem Problem, dass Applikationen, die mit J++ erstellt wurden, nicht zwangsläufig auf anderen Plattformen liefen. Sun klagte gegen Microsoft.

Da es Sun in der Vergangenheit finanziell nicht besonders gut ging, pumpte Microsoft im April 2004 satte 1,6 Milliarden US-Dollar in die Firma. Microsoft erkaufte sich damit das Ende der Kartellprobleme und Patentstreitigkeiten. Dass es bis zu dieser Einigung nicht einfach gewesen war, zeigen Aussagen von Microsoft-Projektleiter Ben Slivka über das JDK bzw. die Java Foundation Classes, man müsse sie »bei jeder sich bietenden Gelegenheit anpissen« (»pissing on at every opportunity«).[2]

Im Januar 2004 beendete Microsoft die Arbeit an J++, denn die Energie floss in das .NET-Framework und die .NET-Sprachen. Am Anfang gab es mit J# eine Java-Version, die Java-Programme auf der Microsoft .NET-Laufzeitumgebung CLR ausführt, doch Anfang 2007 wurde auch J# eingestellt. Das freie IKVM.NET (http://www.ikvm.net) ist eine JVM für .NET und verfügt über einen Übersetzer von Java-Bytecode nach .NET-Bytecode, was es möglich macht, Java-Programme unter .NET zu nutzen. Das ist praktisch, denn für Java gibt es eine riesige Anzahl von Programmen, die somit auch für .NET-Entwickler zugänglich sind. Bedauerlicherweise wird das Projekt nicht weiterentwickelt.

Microsoft hat sich lange Zeit aus der Java-Entwicklung nahezu vollständig zurückgezogen. Es waren eher überschaubare Projekte wie der Microsoft JDBC Driver for SQL Server. Das Verhältnis ist heute auch deutlich entspannter, und Microsoft geht wieder einen Schritt auf Java zu. Zu erkennen ist das am Beitritt in die Jakarta EE Working Group und an der Unterstützung von Java-Applikationen in der Windows Cloud Azure.[3] Vielleicht gratuliert Microsoft irgendwann einmal Oracle, wie es auch Linux zum 20. Geburtstag gratuliert hat.[4]

[1] http://www.microsoft.com/presspass/legal/charles.mspx

[2] Würden wir nicht gerade im westlichen Kulturkreis leben, wäre diese Geste auch nicht zwangsläufig unappetitlich. Im alten Mesopotamien steht »pissing on« für »anbeten«. Da jedoch die E‐Mail nicht aus dem Zweistromland kam, bleibt die wahre Bedeutung wohl unserer Fantasie überlassen.

[3] https://azure.microsoft.com/de-de/develop/java/

[4] http://www.youtube.com/watch?v=ZA2kqAIOoZM