Inselraus: parse*(…)- und print*()-Methoden in DatatypeConverter für Hex-Ausgaben

Das javax.xml.bind-Paket bietet eine Klasse DatatypeConverter, die eigentlich für die Abbildung von XML-Typen auf Java-Typen gedacht ist, doch auch so einige nützliche Methoden bereitstellt. Wir finden in der Klasse statische parse*(String)-Methoden und print*(…)-Methoden: Die ersten konvertieren einen String in diverse Datentypen – etwa short parseShort(String) –, und die zweiten formatieren einen bestimmten Datentyp in einen String – etwa String printShort(short). Für die meisten Methoden gibt es mit String.valueOf(…) und den parse*(…)/toString(…)-Methoden in den Wrapper-Klassen bessere Alternativen, und die Umwandlung von Datumswerten und Fließkommazahlen ist nicht lokalisiert, doch hervorzuheben sind folgende zwei Methoden:

final class javax.xml.bind.DatatypeConverter

  • staticbyte[]parseHexBinary(StringlexicalXSDHexBinary)
  • staticStringprintHexBinary(byte[]val)

Mit diesen statischen Methoden können leicht Byte-Arrays in die String-Repräsentationen hexadezimal konvertiert werden. Das ist nötig, wenn etwa Bytes in einer Text-Konfigurationsdatei abgelegt werden sollen. DatatypeConverter bietet auch Methoden für eine Base64-Kodierung, allerdings sind die geschachtelten Klassen in java.util.Base64 üblicher.

Beispiel:

Konvertiere ein Byte-Array in eine String-Repräsentation, einmal im klassischen Hex-Format, einmal in Base64-Kodierung:

byte[] bytes = { 1, 2, 3, (byte) 254, (byte) 255 };
String s1 = DatatypeConverter.printHexBinary( bytes );
String s2 = DatatypeConverter.printBase64Binary( bytes );
System.out.println( s1 ); // 010203FEFF
System.out.println( s2 ); // AQID/v8=
// Arrays.equals( bytes, DatatypeConverter.parseHexBinary( s1 ) ) == true
// Arrays.equals( bytes, DatatypeConverter.parseBase64Binary( s2 ) ) == true

Inselupdate: strictfp verschwindet in Java 17

Zwischen Java 1.2 und Java 16 unterstützte der Java-Compiler das Schlüsselwort strictfp. Die Unterscheidung sollte es der Laufzeitumgebung erlauben, nicht strikt nach der IEEE-754-Spezifikation zu arbeiten und damit schneller auf der lokalen Hardware Java-Programme auszuführen. Doch seit 20 Jahren unterstützen alle Prozessoren IEEE-754, und die Unterscheidung ist irrelevant geworden – alle von der JVM ausgeführten mathematischen Operationen sind wie im IEEE-754-Standard beschrieben. Mit dem Abschied von strictfp wird auch die Klasse StrictMath irrelevant.

Details: https://openjdk.java.net/jeps/306

Insel: Textblöcke (text blocks)

Strings mit einem Zeilenumbruch kommen in Programmen immer wieder vor, etwa bei Bildschirmausgaben, eingebettetem HTML, XML, JSON oder SQL. Ein Beispiel:

String joke =
"Lehrer: '76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.'\n" +
               "Schüler: 'Herr Lehrer, so viele sind wir doch gar nicht!'";

Im ersten String-Literal steht \n für den Zeilenumbruch. Das Pluszeichen setzt die beiden Strings zusammen. Der Java-Compiler stellt selbständig aus den beiden Konstanten ein größeres String Literal her.

In Java 15 wurden Textblöcke eingeführt (standardisiert im JEP 378: Text Blocks, https://openjdk.java.net/jeps/378). Damit lassen sich einfacher mehrzeilige Strings aufbauen. Drei doppelte Anführungszeichen leiten einen Textblock ein (opening delimiter genannt) und drei doppelte Anführungszeichen schließen einen Textblock wieder ab (closing delimiter genannt). Mithilfe von Textblöcken sieht unser Beispiel von oben so aus:

String joke = """
   Lehrer: '76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.'
   Schüler: 'Herr Lehrer, so viele sind wir doch gar nicht!'""";
System.out.println( joke );

Ein Textblock wird immer mit drei doppelten Anführungszeichen eingeleitet, und ein Zeilenumbruch muss folgen. Das bedeutet automatisch, dass die Textblöcke immer mindestens zwei Quellcodezeilen umfassen, und niemals nur in eine Zeile geschrieben werden können.

Nach der Einleitung eines Textblocks hängt der Compiler an jede Zeile, die nicht mit drei Anführungszeichen abgeschlossen wird, einen Zeilenumbruch LINE-FEED, kurs LF (Unicode \u000A). In unserem Beispiel haben wir nur einen Zeilenumbruch. Hätten wir die drei Anführungszeichen in die nächste Zeile geschrieben, hätten wir zwei Zeilenumbrüche im Ergebnis:

String joke = """
   Lehrer: '76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.'
   Schüler: 'Herr Lehrer, so viele sind wir doch gar nicht!'
   """;

Es macht also einen Unterschied, ob die drei Anführungszeichen zum Schließen des Textblocks in einer eigenen Zeile stehen (der entstehende String endet mit einem LF) oder am Ende eines Strings (der entstehende String endet nicht mit einem LF). Beim Schreiben würde das sicherlich etwas symmetrischer und hübscher aussehen, wenn der Beginn eines Textblocks und das Ende eines Textblocks jeweils in einer einzelnen Zeile stehen würden, doch das können wir nicht immer machen, denn das ergibt immer am Ende einen Zeilenumbruch, der vielleicht nicht gewünscht ist.

Textblöcke sind eine alternative Schreibweise für Strings in doppelten Anführungszeichen. Im Bytecode ist später nicht mehr zu erkennen, auf welche Art und Weise der String entstanden ist. Textblöcke können genauso wie reguläre Strings an allen Stellen eingesetzt werden, an denen Strings gefordert werden, etwa als Argument:

System.out.println( """
Lehrer: '76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.'
Schüler: 'Herr Lehrer, so viele sind wir doch gar nicht!'""" );

Ist im Quellcode innerhalb vom Textblock ein Zeilenumbruch sinnvoll, damit z. B. die Zeile nicht so lang wird, lässt sich mit einem Backslash der Zeilenumbruch im Ergebnisstring verhindern:

String joke2 = """
  Warum haben Giraffen so einen langen Hals? \
  Weil der Kopf so weit oben ist.""";
// Warum haben Giraffen so einen langen Hals? Weil der Kopf so weit oben ist.
System.out.println( joke2 );

Da Textblöcke Strings sind, lassen sie sich auch mit dem +-Operator konkatenieren:

String person1 = "Lehrer", person2 = "Schüler";
String joke = person1 +
               """
               : '76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.'
               """ + person2 + """
               : 'Herr Lehrer, so viele sind wir doch gar nicht!'""";
System.out.println( joke );

Es lässt sich ablesen, dass das Einsetzen von Variableninhalten nicht besonders elegant ist — das war allerdings nicht das Ziel der Textblöcke gewesen, so etwas wie String-Interpolation zu schaffen, was etwa JavaScript kann. Etwas zur Hilfe kommt die String-Methode formatted(…), die als Objektmethode hinzugekommen ist:

String person1 = "Lehrer", person2 = "Schüler";
String joke = """
   %s: '76 %% aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.'
   %s: 'Herr Lehrer, so viele sind wir doch gar nicht!'""".formatted( person1, person2 );
System.out.println( joke );

Die Einrückung der Zeilen in einem Textblock spielt eine elementare Rolle. Die Zeilen eines Textblocks müssen nicht ganz links am Anfang einer Zeile bei der Position 0 beginnen, sondern dürfen rechts eingerückt sein und den üblichen Konventionen in der Einrückung von Java Programmen folgen. Bei der Einrückung sollte beiläufiger Weißraum (engl. incidental white space) aus dem eigentlichen Textblock ausgenommen werden. Hier wendet der Java-Compiler einen kleinen Algorithmus an. Die Regel ist, dass die Zeile die am weitesten links steht (die Zeile mit dem drei abschließenden Anführungszeichen gehört dazu) den beiläufigen Weißraum bestimmt, der abgeschnitten wird. Es ist dabei egal, ob das Zeichen ein Tabulator oder Leerzeichen ist, auch wenn das auf dem Bildschirm anders aussieht! Bestehen bleibt rechts von dieser Stelle der wesentliche Weißraum (engl. essential white space).

Beispiel 1:

String joke2 = """
   Warum haben Giraffen so einen langen Hals?
     Weil der Kopf so weit oben ist.""";

Ausgabe 1:

Warum haben Giraffen so einen langen Hals?

  Weil der Kopf so weit oben ist.

Beispiel 2:

String joke2 = """
   Warum haben Giraffen so einen langen Hals?
Weil der Kopf so weit oben ist.""";

Ausgabe 2:

    Warum haben Giraffen so einen langen Hals?

Weil der Kopf so weit oben ist.

Beispiel 3:

String joke2 = """
    Warum haben Giraffen so einen langen Hals?
    Weil der Kopf so weit oben ist.
""";

Ausgabe 3:

     Warum haben Giraffen so einen langen Hals?

     Weil der Kopf so weit oben ist.

Kommen wir abschließend noch zu ein paar Regeln. Im Quellcode könnte ein Zeilenende durch unterschiedliche Symbole im Text vorkommen: CR (\u000D), CR-LF (\u000D\u000A) oder LF (\u000A). Der Compiler führt eine sogenannte Normalisierung durch, dass am Ende nur ein LF (\u000A) im String steht.

Textblöcke sind keine sogenannten Raw-Strings, die „roh“ sind, sondern können alle Escape-Sequenzen, unter anderem \n, \t, \‘, \“ und \\, enthalten. So lässt sich am Ende immer noch mit \r eine CR-LF-Sequenz erschaffen:

String joke = """
   Lehrer: '76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.'\r
   Schüler: 'Herr Lehrer, so viele sind wir doch gar nicht!'""";

Das Ergebnis wäre „Lehrer: ’76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung.’\u000D\u000ASchüler: ‚Herr Lehrer, so viele sind wir doch gar nicht!'“.

Kommen im String einfache oder doppelte Anführungszeichen vor, können diese ohne Ausmaskierung eingesetzt werden. Ein Sonderfall ist allerdings, wenn das doppelte Anführungszeichen direkt am Ende vor den drei schließenden doppelten Anführungszeichen steht, dann eine Maskierung notwendig:

String joke = """
   Lehrer: "76 % aller Schüler in dieser Klasse haben keine Ahnung von Prozentrechnung."
   Schüler: "Herr Lehrer, so viele sind wir doch gar nicht!\"""";

Sollen drei Anführungszeichen selbst in der Ausgabe vorkommen, muss eines der drei Anführungszeichen ausmaskiert werden, um nicht fälschlicherweise als Abschluss interpretiert zu werden. Für die Ausgabe

1 "
2 ""
3 """
4 """"
5 """""
6 """"""

müssen wir schreiben:

System.out.println( """
                   1 "
                   2 ""
                   3 ""\"
                   4 ""\""
                   5 ""\"""
                   6 ""\"""\"""" );

Solche Zeichenketten sind nicht mehr besonders gut lesbar …

Eine weitere Regel ist, dass Leerzeichen am Ende abgeschnitten werden. Aus dem Textblock

String s = """
1␣␣␣␣␣␣␣
2␣␣␣
""";

wird der String „1\u0002“ — der “Offene Kasten” ␣ stehen hier für Leerzeichen, die man sonst im Druck nicht sehen würde.

Sollen Leerzeichen am Ende zählen, so führt man die Escape-Sequenz \s ein. Dies wird übersetzt in ein reguläres Leerzeichen \u0020. Für das obere Beispiel bedeutet das:

String s = """
          1      \s
          2  \s
          """;

Leser können mit der folgenden Aufgabe ihr Verständnis prüfen: Was für ein String entsteht?

String joke2 = """
   Warum haben Giraffen so einen langen Hals? \
   Weil der Kopf so weit oben ist.
             """;

Es entsteht „Warum haben Giraffen so einen langen Hals? Weil der Kopf so weit oben ist.\n“.

Was ist eine compile-time constant expression?

Sind Variablen final, heißt das lediglich, dass es eine einmalige Zuweisung geben darf. Ob die Werte allerdings zur Laufzeit berechnet werden oder nicht, hat erst einmal nichts mit final zu tun. In folgendem Beispiel ist die Variable eine zur Compilezeit bekannte Konstante:

public class Finance {
  public static final int TAX = 19;
}

Greift eine andere Klasse auf die Variable TAX zu, ist das im Quellcode nicht als direkter Variablenzugriff Finance.TAX kodiert, sondern der Compiler hat das Literal 19 direkt an jeder Aufrufstelle eingesetzt. Dies ist eine Optimierung des Compilers, die er laut Java-Spezifikation vornehmen kann.

Wir sprechen in diesem Zusammenhang von einer compile-time constant expression wenn gilt:

  • ein Attribut ist final,
  • der Datentyp ist ein primitiver oder String,
  • das Attribut wird mit einer vom Compiler berechneten Konstanten initialisiert.

Das Einsetzen der konstanten Werte ist praktisch, bringt aber ein Probleme mit sich, wenn das finale Attribut sich ändert. Dann muss nämlich auch jede Klasse übersetzt werden, die Bezug auf die Konstante hatte. Werden die abhängigen Klassen nicht neu übersetzt, ist in ihnen immer noch der alte Wert eincompiliert.

Die Lösung ist, die bezugnehmenden Klassen neu zu übersetzen und sich am besten anzugewöhnen, bei einer Änderung einer Konstanten gleich alles neu zu compilieren. Ein anderer Weg transformiert die finale Variable in eine später initialisierte Form:

public class Finance {
  public static final int TAX = Integer.valueOf( 19 );
}

Die Initialisierung findet im statischen Initialisierer statt, und die Konstante mit dem Literal 19 ist zunächst einmal verschwunden. Der Compiler wird also beim Zugriff auf Finance.TAX keine Konstante 19 vorfinden und daher das Literal an den Aufrufstellen nicht einbauen können. In der Klassendatei wird der Bezug Finance.TAX vorhanden sein, und eine Änderung der Konstanten erzwingt keine neue Übersetzung der Klassen.


Switch Expressions

Java hat seit Version 1.0 eine switch-Anweisung zum Kontrollfluss. Im Wesentlichen basiert die Syntax auf der Programmiersprache C, die auf die 1970er Jahre zurückgeht. In Java 12 wurde eine neue Syntax probeweise eingeführt, in Java 13 verändert und in Java 14 endgültig integriert.

Insgesamt kann switch in vier Formen auftauchen:

Anweisung/Ausdruck Ab Java-Version Syntax Durchfall vollständige Abdeckung
Anweisung 1.0 : Ja Nein
Anweisung 14 -> Nein Nein
Ausdruck 14 : Ja Ja
Ausdruck 14 -> Nein Ja

Vier Typen von switch

Den ersten Typ haben wir schon ausgiebig betrachtet, schauen wir uns die weiteren Varianten an.

Vereinfachte Switch-Anweisung, kein Durchfall, keine vollständige Abdeckung

Bei der vereinfachten switch-Anweisung steht hinter dem Label bzw. default kein Doppelpunkt, sondern ein ->. Dieser hat nichts mit Lambda-Ausdrucken zu tun, auch wenn die Symbole gleich sind. Hinter dem Pfeil steht entweder ein Ausdruck, ein Block in geschweiften Klammern oder ein throw-Anweisung, die eine Ausnahme auslöst. Implizit beendet ein break jeden Zweig, es gibt also kein Durchfallen mehr.

Beispiel

String operator = "+";

switch ( operator ) {

  case "+" -> System.out.println( "Plus" );

  case "-" -> { String minus = "Minus"; System.out.println( minus ); }

}

Dadurch, dass bei mehreren Anweisungen immer Blöcke gesetzt werden müssen, tritt eine lokale Variable auch nicht aus dem Bereich aus.

Ein default kann gesetzt werden, muss aber nicht. Das switch muss nicht jede Möglichkeit abdecken, was bei Zahlen und Strings eh nicht funktioniert.

Beispiel

String operator = "+";

switch ( operator ) {

  case "+" -> System.out.println( "Plus" );

  case "-" -> System.out.println( "Minus" );

  default  -> System.out.println( "Unbekannter Operator" );

}

Bei vereinfachten switch-Anweisungen sind mehrere Labels möglich, die die gleiche Behandlung haben. Kommas trennen die Labels.

Beispiel

String operator = "+";

switch ( operator ) {

  case "+"      -> System.out.println( "Plus" );

  case "*", "×" -> System.out.println( "Mal" );

}

Switch-Ausdrücke, kein Durchfall, vollständige Abdeckung

Traditionell finden sich die Fallunterscheidungen mit switch als Anweisung und Anweisungen geben nichts zurück. In Java 14 ist es möglich, switch als Ausdruck mit Ergebnis zu nutzen.

Beispiel

String operator = "+";

String writtenOperator = (switch ( operator ) {

  case "+" -> "Plus";

  case "-" -> "Minus";

  default  -> "Unbekannter Operator";

} ).toUpperCase();

System.out.println( writtenOperator );

Ausdrücke müssen immer Ergebnisse liefern, und folglich muss switch immer einen Pfad auf einen Wert nehmen. Der übliche Fall ist default wie gezeigt, es gibt allerdings Sonderfälle, wie bei Aufzählungen, wo der Compiler prüfen kann, dass alle Möglichkeiten abgedeckt sind.

Beispiel

DayOfWeek today = LocalDate.now().getDayOfWeek();

System.out.println( switch ( today ) {

  case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Arbeit";

  case SATURDAY, SUNDAY -> "Party";

} );

Falls rechts neben dem Pfeil kein einfacher Ausdruck steht, sondern ein Block muss auch dieser Block ein Ergebnis zurückgeben. Dafür wird das neue Schlüsselwort yield eingesetzt, hinter dem ein Ausdruck kommt.

Beispiel

String operator = "+";

System.out.println( switch ( operator ) {

  case "+" -> "Plus";

  case "-" -> { String minus = "Minus"; yield minus; }

  default  -> throw new IllegalArgumentException( "Unknown operator" );

} );

Ein Block muss ein yield besitzen oder eine ungeprüfte Ausnahme auslösen.

Switch-Expression mit :-Syntax, mit Durchfall, vollständige Abdeckung

Auch die Doppelpunkt-Syntax lässt sich als Ausdruck einsetzten, mit ihr ist auch ein Durchfall wieder möglich; ein yield ist zwingend, oder eine ungeprüfte Ausnahme. Die Syntax birgt mit dem Durchfallen eine Fehlerquelle, sodass es vielleicht die schlechteste Variante ist.

Beispiel

String operator = "+";

System.out.println( switch ( operator ) {

case "+" : yield "Plus";

case "*" : System.out.println( "Sternchen" );

case "×" : yield "Mal";

default  : throw new IllegalArgumentException( "Unknown operator" );

} );

Dummy, Fake, Stub und Mock

Gute objektorientiert entworfene Systeme zeichnen sich dadurch aus, dass es eine hohe Interaktion mit anderen Objekten gibt. Idealerweise zerlegt eine Klasse ein Problem nur bis zu dem Punkt, an dem es sich einer anderen Klasse bedienen kann, die dieses einfachere Problem löst. Schwierig wird es, wenn eine eigene Klasse auf eine andere komplexe Klasse zurückgreift und das Objekt nur dann sinnvoll arbeitet, wenn das referenzierte Objekt da ist und irgendwie sinnvoll antwortet. Diese Abhängigkeit ist ungünstig, denn das Ziel eines guten Tests besteht ja darin, lokal zu sein, also die eigentliche Klasse zu testen und nicht alle referenzierten Klassen um sie herum gleich mit.

In der Praxis begegnen uns drei Hilfskonstrukte, die die Lokalität von Tests ermöglichen:

  • Fake-Objekte: Sie sind eine gültige Implementierung einer Schnittstelle. Wenn zum Beispiel ein Repository auf die Datenbank geht, kann ein Fake-Implementierung Datensätze in einer Datenstruktur speichern. Das Verhalten ist nachgebildet und vereinfacht, aber funktionsfähig. So liefert ein Fake-Repository statt Kunden aus der Datenbank immer die gleichen N vorgefertigten Kunden. Fake-Objekte sind auch praktisch, wenn zum Beispiel eine GUI-Anwendung programmiert wird, die statt echter Datenbankdaten erst einmal mit den Fake-Objekten entwickelt wird und so die Demodaten anzeigt. Wenn ein Team die GUI baut und ein anderes Team den Service, so können beide Gruppen unabhängig arbeiten, und das GUI-Team muss nicht erst auf die Implementierung warten.
  • Stub-Objekte: Stub-Objekte implementieren ein bestimmtes Protokoll, sodass sie für den Testfall immer die gleichen Antworten geben können. Wenn etwa ein E-Mail-Service eine Methode isTransmitted() anbietet, so kann der Stub immer true liefern. Stubs haben also kein Verhalten, sondern der Rumpf der Methoden ist quasi leer und minimal. Sie gibt es nur für die Testfälle.
  • Mock-Objekte: Mock-Objekte werden von einem Testfall »aufgeladen« und zeigen dann das gewünschte Verhalten – sie liefern also nicht wie Stubs immer das gleiche Ergebnis. In der Regel werden Mock-Objekte durch Bibliotheken wie mockito(http://mockito.org) oder EasyMock (http://easymock.org) automatisch zur Laufzeit erzeugt.

Diese drei Typen können wir unter dem Oberbegriff Dummy-Objekt zusammenfassen. Grundsätzlich gilt bei den vier Begriffen aber, dass sie von Autoren nicht einheitlich verwendet werden.[1]

Beispiel: Mockito-Beispiel

Nehmen wir an, alles aus org.mockito.Mockito.* ist statisch importiert und wir wollen eine java.util.List aufbauen. Dazu muss Mockito erst etwas aufbauen, was sich wie List verhält:

List<?> mockedList = mock( List.class );

Im nächsten Schritt muss das Verhalten der speziellen Liste bestimmt werden:

when( mockedList.get(0) ).thenReturn( „tutego“ );

Anschließend ist die Liste bereit zur Nutzung:

System.out.println( mockedList.get(0) ); // tutego

[1] Die Seite http://xunitpatterns.com/Mocks,%20Fakes,%20Stubs%20and%20Dummies.html stellt einige Autoren mit ihrer Begriffsnutzung vor.

this für kaskadierte Methoden und Builder

Die append(…)-Methoden bei StringBuilder liefern die this-Referenz, sodass sich Folgendes schreiben lässt:

StringBuilder sb = new StringBuilder();
sb.append( "Android oder iPhone" ).append( '?' );

Jedes append(…) liefert das StringBuilder-Objekt, auf dem es aufgerufen wird – wir können also Methoden kaskadiert anhängen oder es bleiben lassen.

Wir wollen diese Möglichkeit bei einem Zauberer (Klasse Wizard) programmieren, sodass die Methoden name(String) und age(int) Spielername und Alter zuweisen. Beide Methoden liefern ihr eigenes Wizard-Objekt über die this-Referenz zurück:

class Wizard {



  String name = "";

  int age;




  Wizard name( String name ) { this.name = name; return this; }

  String name() { return name; }




  Wizard age( int item ) { this.age = item; return this; }

  int age() { return age; }




  String format() {

    return name + " ist " + age;

  }

}

Erzeugen wir einen Wizard, und kaskadieren wir einige Methoden:

Wizard gundalf = new Wizard().name( "Gundalf" ).age( 60 );

System.out.println( gundalf.name() );         // Gundalf

System.out.println( gundalf.format() );       // Gundalf ist 60

Der Ausdruck new Wizard() liefert eine Referenz, die wir sofort für den Methodenaufruf nutzen. Da name(String) wiederum eine Objektreferenz vom Typ Wizard liefert, ist dahinter direkt .age(int) möglich. Die Verschachtelung von name(„Gunalf“).age(60) bewirkt, dass Name und Alter gesetzt werden und der jeweils nächste Methodenaufruf in der Kette über this eine Referenz auf dasselbe Objekt, aber mit verändertem internem Zustand bekommt.

Beispiele dieser Bauart sind in der Java-Bibliothek an einigen Stellen zu finden. Sie werden auch Builder genannt.

Hinweis: Die Methode Wizard name(String) ist mit ihrer Rückgabe praktisch, verstößt aber aus zwei Gründen gegen die JavaBeans-Konvention: Setter dürfen keine Rückgabe haben und müssen immer mit set beginnen. JavaBeans sind also nicht so dieser kompakten Builder-Schreibweise „kompatibel“.

Finale Variablen und der Modifizierer final

Variablen können mit dem Modifizierer final deklariert werden, sodass genau eine Zuweisung möglich ist. Dieses zusätzliche Schlüsselwort verbietet folglich eine weitere Zuweisung an diese Variable, sodass sie nicht mehr verändert werden kann. Ein üblicher Anwendungsfall sind Konstanten.

int width = 40, height = 12;

final int area = width * height;

final int perimeter;

final var random = Math.random() * 100;

perimeter = width * 2 + height * 2;

area = 200;         //  Compilerfehler
perimeter = 100;    //  Compilerfehler

Im Fall einer versuchten zweiten Zuweisung meldet der Compiler von Eclipse: »The final local variable … cannot be assigned. It must be blank and not using a compound assignment.«; IntelliJ meldet über den Java-Compiler »cannot assign a value to final variable …«.

Java erlaubt bei finalen Werten eine aufgeschobene Initialisierung. Das heißt, dass nicht zwingend zum Zeitpunkt der Variablendeklaration ein Wert zugewiesen werden muss. Das sehen wir im Beispiel an der Variablen perimeter.

Werden Variablen deklariert und initialisiert können final und var zusammen eingesetzt werden; einige Programmiersprachen bieten hier ein eigenes Schlüsselwort, wie val, Java jedoch nicht.

Ausblick

Auch Objektvariablen und Klassenvariablen können final sein. Allerdings müssen die Variablen dann entweder bei der Deklaration belegt werden, oder in einer aufgeschobenen Initialisierung im Konstruktor. Das Schlüsselwort final hat noch zusätzliche Bedeutungen im Zusammenhang mit Vererbung.

Der Restwert-Operator %

Eine Ganzzahldivision muss nicht unbedingt glatt aufgehen, wie im Fall von 9/2. In diesem Fall gibt es den Rest 1. Diesen Rest liefert der Restwert-Operator (engl. remainder operator), oft auch Modulo genannt.[1] Die Operanden können auch negativ sein.

Beispiel: System.out.println( 9 % 2 );            // 1

Die Division und der Restwert richten sich in Java nach einer einfachen Formel: (int)(a/b) × b + (a%b) = a.

Beispiel: Die Gleichung ist erfüllt, wenn wir etwa a = 10 und b = 3 wählen. Es gilt: (int)(10/3) = 3 und 10 % 3 ergibt 1. Dann ergeben 3 * 3 + 1 = 10.

Aus dieser Gleichung folgt, dass beim Restwert das Ergebnis nur dann negativ ist, wenn der Dividend negativ ist; das Ergebnis ist nur dann positiv, wenn der Dividend positiv ist. Es ist leicht einzusehen, dass das Ergebnis der Restwert-Operation immer echt kleiner ist als der Wert des Divisors. Wir haben den gleichen Fall wie bei der Ganzzahldivision, dass ein Divisor mit dem Wert 0 eine ArithmeticException auslöst und bei Fließkommazahlen zum Ergebnis NaN führt.

System.out.println( „+5% +3  = “ + (+5% +3) );   //  2
System.out.println( „+5 / +3 = “ + (+5 / +3) );  //  1

System.out.println( „+5% -3  = “ + (+5% -3) );   //  2
System.out.println( „+5 / -3 = “ + (+5 / -3) );  // -1

System.out.println( „-5% +3  = “ + (-5% +3) );   // -2
System.out.println( „-5 / +3 = “ + (-5 / +3) );  // -1

System.out.println( „-5% -3  = “ + (-5% -3) );   // -2
System.out.println( „-5 / -3 = “ + (-5 / -3) );  //  1

Gewöhnungsbedürftig ist die Tatsache, dass der erste Operand (Dividend) das Vorzeichen des Restes definiert und niemals der zweite (Divisor).

Hinweis: Um mit value % 2 == 1 zu testen, ob value eine ungerade Zahl ist, muss value positiv sein, denn -3 % 2 wertet Java zu –1 aus. Der Test auf ungerade Zahlen wird erst wieder korrekt mit value % 2 != 0.

Restwert für Fließkommazahlen

Der Restwert-Operator ist auch auf Fließkommazahlen anwendbar, und die Operanden können wiederum negativ sein.

Beispiel: Teste, ob eine double-Zahl doch eine Ganzzahl ist: (d % 1) == 0. Wem das zu verrückst ist, der nutzt alternativ d == Math.rint(d).

Restwert für Fließkommazahlen und Math.IEEEremainder( ) *

Über die oben genannte Formel können wir auch bei Fließkommazahlen das Ergebnis einer Restwert-Operation leicht berechnen. Dabei muss beachtet werden, dass sich der Operator nicht so wie unter IEEE 754 verhält. Denn diese Norm schreibt vor, dass die Restwert-Operation den Rest von einer rundenden Division berechnet und nicht von einer abschneidenden. So wäre das Verhalten nicht analog zum Restwert bei Ganzzahlen. Java definiert den Restwert jedoch bei Fließkommazahlen genauso wie den Restwert bei Ganzzahlen. Wünschen wir ein Restwert-Verhalten, wie IEEE 754 es vorschreibt, so können wir die statische Bibliotheksmethode Math.IEEEremainder(…)[2] verwenden.

Auch bei der Restwert-Operation bei Fließkommazahlen werden wir niemals eine Exception erwarten. Eventuelle Fehler werden, wie im IEEE-Standard beschrieben, mit NaN angegeben. Ein Überlauf oder Unterlauf kann zwar vorkommen, aber nicht geprüft werden.

[1] Mathematiker unterscheiden die beiden Begriffe Rest und Modulo, da ein Modulo nicht negativ ist, der Rest in Java aber schon. Das soll uns aber egal sein.

[2] Es gibt auch Methoden, die nicht mit Kleinbuchstaben beginnen, wobei das sehr selten ist und nur in Sonderfällen auftritt. ieeeRemainder() sah für die Autoren nicht nett aus.

Immutable-Objekte und Wither-Methoden

Objekte, dessen Zustände nicht verändert werden können, heißen immutable. Die Klassen deklarieren in so einem Fall keine öffentlichen Variablen und auch keine Methoden mit Seiteneffekten, die diese Zustände modifizieren könnten. Setter gibt es folglich nicht, nur vielleicht Getter.

Damit die Objekte ihre Werte bekommen, gibt es unterschiedliche Wege – parametrisierte Konstruktoren sind ein guter Weg. Die Belegungen lassen sich beim Konstruktoraufruf übergeben und so sehr gut direkt in finale Variablen schreiben. In der Java-Bibliothek gibt es eine Reihe solcher Klassen, die keinen parameterlosen Konstruktor besitzen, und nur einige parametrisierte, die Werte erwarten. Die im Konstruktor übergebenen Werte initialisieren das Objekt, und es behält diese Werte sein ganzes Leben lang. Zu den Klassen gehören zum Beispiel Integer, Double, Color, File oder Font.

Immutable-Objekte, die auch die equals(…)-Methode implementieren heißen Werteobjekt (engl. value object).

Wither-Methoden

Auch wenn sich Objekte mit Setten nicht ändern lassen, so soll es doch möglich sein, neue Objekte mit veränderten Zuständen zu erschaffen. Ein Blick auf die Klasse String zeigt zum Beispiel trim() und toUpperCase() – das Ergebnis sind neue Strings.

Um allgemein Zustandsvariablen zu verändern können Wither-Methoden verwendet werden; sie sind ähnlich wie Setter, nur verändern sie keinen Zustand am aktuellen Objekt, sondern führen zu einem neuen Objekt mit dem geänderten Zustand.

Getter Setter Wither
Typ getXXX() void setXXX(Typ xxx) ImmutableTyp withXXX(Typ xxx)

Namenskonvention der Getter, Setter, Wither für eine Property xxx

Dazu ein Beispiel. Ein Raum hat einen Namen und eine Größe. Die Exemplare sollen unveränderbar sein:

class Room {

  private final String name;

  private final int size;




  public Room( String name, int size ) {

    this.name = name;

    this.size = size;

  }




  public String getName() {

    return name;

  }




  public int getSize() {

    return size;

  }




  public Room withName( String name ) {

    return new Room( name, size );

  }




  public Room withSize( int size ) {

    return new Room( name, size );

  }

}

NetBeans 10 Java-Projekt anlegen

NetBeans (http://netbeans.org) bietet komfortable Möglichkeiten zur Java SE- und Java Enterprise-Entwicklung mit Editoren und Wizards für die Erstellung grafischer Oberflächen und Webanwendungen. Die Hauptentwicklung leistete damals Sun Microsystems, doch als Oracle Sun übernahm, hatten Entwicklungsumgebungen keine große Priorität im Datenbankhaus. Es gab zwar Weiterentwicklungen, doch schleppte sich Oracle eher von Version zu Version. Oracle hat letztendlich die Codebasis an die Apache Foundation übergeben und sich damit von der IDE getrennt. Das neue Zuhause ist https://netbeans.apache.org/.

NetBeans installieren

Die aktuelle Version Apache NetBeans 10.0 unterstützt Java 11. Nach dem Download vom https://netbeans.apache.org/download/ bekommen wir eine ZIP-Datei, die wir auspacken. Im bin-Verzeichnis gibt es netbeans64.exe für Windows und eine Shell-Datei für Unix-System. Vor dem Start müssen wir jedoch eine Variable anpassen, die auf das JDK zeigt, andernfalls gibt es den Fehler „cannot find java 1.8 or higher“.

Im etc-Verzeichnis von NetBeans liegt die Datei netbeans.conf, die wir editieren müssen, denn nach der Installation steht auskommentiert:

etc/netbeans.conf, Aussschnitt

#netbeans_jdkhome=“/path/to/jdk“

Das ändern wir und setzen den Pfad (je nach Ort natürlich anders):

netbeans_jdkhome=“C:\Programme\Java\jdk-11″

NetBeans lässt sich starten.

Das erste Projekt

Es öffnet File > New Project…. einen neuen Dialog. Hier lassen sich komplette Java-Projekte mit Beispielcode anlegen. Wählen wir links Java und rechts Java Application und gehen auf Next >.

Es folgt ein neuer Dialog, in dem wir Java SE aktivieren müssen. Machen wir das! Nach ein paar Sekunden kommt ein neuer Dialog, der vorausgefüllt ist. Wir können den Projektnamen, den Speicherort und den Klassennamen und das Paket ändern.  Finish generiert das Projekt.

Danach öffnet sich der Editor. Setzen wir den Cursor in die main-Methode. Schreiben wir sout mit einem Tabulator gefolgt, expandiert dies zu System.out.println(„“);.

Das Programm kann gestartet werden a) über die Symbolleiste und dem grünen Play-Button, b) im Menü unter Run, dann Run Project, oder c) über die Taste F6.

Die Startzeiten für Projekte sind etwas schlechter als bei Eclipse, denn NetBeans lässt bei jedem Start ein Ant-Skript laufen, das sich Zeit nimmt.

Diamant vs. var

Diamant und var haben vergleichbare Aufgaben, unterscheiden sich aber durch die Quelle der Informationen. Beim Diamanten ist es zum Beispiel bei einer Zuweisung die linke Seite, die dem Compiler die Information gibt, was auf der rechten Seite der Zuweisung für ein Typ gemeint ist. Bei var wiederum ist das anderes herum: die rechte Seite hat den Kontext und daher kann links der Variablentyp entfallen:

List<String> list1 = new ArrayList<>();  // List<String>

var list2 = new ArrayList<String>();     // ArrayList<String>

var list3 = new ArrayList<>();           // ArrayList<Object>

Im letzten Fall gibt es keinen Compilerfehler, nur ist eben nichts bekannt über das Typargument, und daher gilt Object.

Um Code abzukürzen haben wir damit zwei Möglichkeiten: var oder Diamond.

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.

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