Inselupdate: enum im switch mit Pattern-Matching in Java 21

Seit Java 21 ist die Möglichkeit vom switch stark erweitert worden. Das betrifft auch Aufzählungstypen. Es war immer ein wenig umständlich, wenn ein switch Konstanten von verschiedenen Aufzählungstypen vergleichen sollte. Das ist einfacher geworden und dazu ein kleines Beispiel. Nehmen wir zwei enum an, die keine Typbeziehung haben:

enum CoffeeSize { SMALL, LARGE }

enum TeeSize { SMALL, LARGE }

Die Größen sollen nun zusammen behandelt werden. Wir können schreiben:

static void orderSize( Enum size ) {

  switch ( size ) {

    case CoffeeSize.SMALL -> System.out.println( "Small" );

    case TeeSize.SMALL -> System.out.println( "Small" );

    case CoffeeSize.LARGE -> System.out.println( "Large" );

    case TeeSize.LARGE -> System.out.println( "Large" );

    default -> {

    }

  }

}

Da der Compiler von allen einen beliebigen Enum ausgeht, kann er keine vollständige Abdeckung erkennen. Wir können das mit einem Basistyp allerdings lösen,

Wenn ein switch-Ausdruck mit einem einzelnen Aufzählungstyp verwendet wird, kann der Compiler überprüfen, ob alle Aufzählungselemente abgedeckt sind und ob ein default-Zweig entfallen kann. Der Compiler kann auch eine vollständige Abdeckung überprüfen, wenn der switch-Block verschiedene Aufzählungstypen behandelt und dabei ein versiegeltes Interface als Basistyp verwendet wird. Betragen wir das Beispiel aus dem vorherigen Kapitel erneut und schreiben es um:

sealed interface DrinkSize permits CoffeeSize, TeeSize {}

enum CoffeeSize implements DrinkSize { SMALL, LARGE }

enum TeeSize implements DrinkSize { SMALL, LARGE }




static void orderSize( DrinkSize size ) {

  switch ( size ) {

    case CoffeeSize.SMALL -> System.out.println( "Small" );

    case TeeSize.SMALL -> System.out.println( "Small" );

    case CoffeeSize.LARGE -> System.out.println( "Large" );

    case TeeSize.LARGE -> System.out.println( "Large" );

  }

}

 

Inselupdate: Neue Math Methoden clamp(…)

Seit Java 21 gibt es in der Klasse Math neue Methoden, die einen Wert in einem Bereich halten:

  • static double clamp(double value, double min, double man)
  • static float clamp(float value, float min, float max)
  • static int clamp(long value, int min, int man)
  • static long clamp(long value, long min, long max)

Die Methoden basieren im Kern auf einem verschachtelten Math.min(max, Math.max(value, min)), lösen aber Ausnahmen aus, wenn der Endwert vor dem Startwert liegt.

Inselupdate: Records implementieren Schnittstellen

Obwohl Records keine Klassen erweitern können, können sie Schnittstellen implementieren. Dies ermöglicht uns, Abstraktionen zu erstellen, was nützlich ist, um gemeinsame Record-Komponenten zu teilen.

Betrachten wir ein Beispiel. Wir wollen den Basistyp Event nicht mehr als abstrakte Oberklasse deklarieren, sondern als Schnittstelle:

interface Event {

  String about();

  int duration();

}

Das erlaubt es uns, zwei Records zu deklarieren, die die Event-Schnittstelle implementieren:

record Nap( String about, int duration ) implements Event {}

record Workout( String about, int duration, int caloriesBurned ) implements Event {}

Der clevere Teil dabei ist, dass die Records bereits die Zugriffsmethoden String about() und int duration() besitzen, sodass keine zusätzliche Implementierung erforderlich ist.

Mit dieser Typbeziehung können wir Folgendes tun:

Event event = new Nap( "Snooze Olympics", 69 );

System.out.println( event.about() );

System.out.println( event.duration() );

In diesem Fall ist der Referenztyp Event und der Objekttyp Nap. Mit dieser Abstraktion lässt sich perfekt Pattern-Matching und Record-Pattern einsetzen:

switch ( event ) {

  case Nap nap ->

      System.out.printf( "%d minutes of ninja-level rest!%n", nap.duration );

  case Workout( var about, var duration, var calories ) ->

      System.out.printf("You just burned %d calories for a guilt-free gummy bear.%n", calories );

  default -> {}

}

Inselupdate: Record-Patterns in Java 21

Führen wir für die folgenden Beispiele ein Record für Punkte ein:

record Point( int x, int y ) {}

Nun möchten wir überprüfen, ob die X-Y-Koordinaten eines Punktes auf null stehen. Dafür erstellen wir eine Methode namens isZeroPoint(…), die alle Objekttypen akzeptiert und false zurückgibt, wenn es sich nicht um einen Punkt handelt:

static boolean isZeroPoint( Point object ) {

  if ( object instanceof Point ) {

    Point point = (Point) object;

    return point.x() == 0 && point.y() == 0;

  }

  return false;

}

Eine kompaktere Version des Tests, die auf eine Zwischenvariable verzichtet, kann so aussehen:

return object instanceof Point && ((Point) object).x() == 0 && ((Point) object).y() == 0;

Lesbarerer ist es nicht.

Der Einsatz einer Pattern-Variable verbessert die Lesbarkeit und Klarheit des Codes:

static boolean isZeroPoint( Object object ) {

  return object instanceof Point p && p.x() == 0 && p.y() == 0;

}

Der Test p.x() == 0 && p.y() == 0 lässt sich mit einem Trick noch weiter verkürzen:
static boolean isZeroPoint( Object object ) {

  return object instanceof Point p && (p.x() | p.y()) == 0;

}

Wenn zwei Zahlen mittels der bitweisen ODER-Operation verknüpft werden und eine davon nicht null ist, wird auch das Ergebnis nicht null sein. Dies ermöglicht eine elegante und kompakte Überprüfung der Nullbedingung für beide Koordinaten gleichzeitig.

Record-Pattern einsetzen

Auffällig an der bisherigen Lösung ist die Notwendigkeit einer Point-Variable p, um auf p.x() und p.y() zuzugreifen. Java 21 führt Record-Pattern[1] ein, die in anderen Programmiersprachen als Destrukturierung bezeichnet werden. Record-Pattern können sowohl bei instanceof als auch bei switch verwendet werden. Hier ist ein Beispiel für instanceof, wodurch isZeroPoint(…) etwas kürzer wird:

static boolean isZeroPoint( Point point ) {

  return point instanceof Point( int a, int b ) && (a | b) == 0;

}

Der Teil Point(int a, int b) nennt sich Record-Pattern. Nach einer Übereinstimmung werden neue lokale Variablen a und b eingeführt, die vom Punkt die Koordinaten enthalten. Das heißt, a wird mit point.x() und b mit point.y() belegt. Die Variablennamen müssen nicht mit den Record-Komponentennamen übereinstimmen; in diesem Fall heißen sie a und b, x und y wären aber möglich. Wichtig ist, alle Record-Komponenten aufzulisten; keine Record-Komponente darf ausgelassen werden.

Kommen wir von Punkten zu Linien. Betrachten wir ein neues Record:

record Line( Point start, Point end ) {}

Schreiben wir eine zweite Methode isZeroLine(…), die überprüft, ob die beiden Punkte der Linie Null sind oder nicht. Beginnen wir mit einer Pattern-Variablen:

static boolean isZeroLine( Object object ) {

  return    object instanceof Line line

         && (  line.start().x() | line.start().y()

             | line.end().x()   | line.end().y() ) == 0;

}

Da es sich bei Line um einen Record handelt, kann das Record-Pattern angewendet werden:

static boolean isZeroLine( Object object ) {

  return    object instanceof Line( Point start, Point end )

         && (start.x() | start.y() | end.x() | end.y()) == 0;

}

Die Datentypen können mit var abgekürzt werden:

static boolean isZeroLine( Object object ) {

  return    object instanceof Line( var start, var end )

         && (start.x() | start.y() | end.x() | end.y()) == 0;

}

Geschachtelte Record-Pattern

Record-Pattern können auch verschachtelt werden, um komplexere Strukturen abzubilden:

static boolean isZeroLine( Object object ) {

  return object instanceof Line(

    Point( int x1, int y1 ), Point( int x2, int y2 )

   ) && (x1 | y1 | x2 | y2) == 0;

}

Das Code-Volumen schrumpft nicht, daher ist es Geschmacksache, ob die Variante besser ist.

Record-Pattern bei switch

Das Pattern-Matching bei switch ist in Java 21 noch leistungsfähiger geworden. Erstellen wir eine zweite Methode isZero(Object), die sowohl Punkte als auch Linien überprüfen kann. Lösen wir die Aufgabe zuerst wie bekannt mit Pattern-Variablen:

static boolean isZero( Object o ) {

  return switch ( o ) {

    case Point p -> (p.x() | p.y()) == 0;

    case Line l -> (l.start().x() | l.start().y() | l.end().x() | l.end().y()) == 0;

    default -> false;

  };

}

Neben instanceof sind Record-Pattern auch bei switch-case möglich. Die Methode kann wie folgt umgeschrieben werden:

static boolean isZero( Object o ) {

  return switch ( o ) {

    case Point( int x, int y ) -> (x | y) == 0;

    case Line( Point s, Point e ) -> isZero( s ) && isZero( e );

    default -> false;

  };

}

Bei der Linie lässt sich das Record-Pattern wieder schachteln:

static boolean isZero( Object o ) {

  return switch ( o ) {

    …

    case Line( Point( int x1, int y1 ), Point( int x2, int y2 ) )

          -> (x1 | y1 | x2 | y2) == 0;

    …

  };

}

Allerdings ist auch hier der Code wieder länger.

Pattern-Matching mit Record-Pattern und when

Wir haben gesehen, dass Pattern-Matching mit Record-Pattern zur Destrukturierung möglich ist. Auch lässt sich ein when für eine weitere Abfrage einsetzen. Die Bedingung hinter when kann auf die Pattern-Variable oder Variable aus dem Record-Pattern zugreifen.

Ein Beispiel: Wir möchten Candy und Book als Records implementieren:

record Candy( int calories ) {}

record Book( String title, int numberOfPages ) {}

Ein Block kann wie folgt Ausgaben zu unterschiedlichen Objekttypen formulieren:

Object object = new Candy( 120 );




switch ( object ) {

  case Candy candy

  when candy.calories > 10_000 ->

      System.out.println( "Are you trying to sweeten the whole world?" );




  case Candy candy ->

      System.out.println("Is this candy trying to start a dance party in my mouth?");




  case Book( var title, var pages )

  when pages > 100 ->

      System.out.println(

          "Looks like someone was on a mission to make the dictionary jealous." );




  case Book( var title, var pages )

  when title.isEmpty() ->

      System.out.println("Diving into books that forgot to introduce themselves.");




  case Book -> System.out.println( "Opening minds, one page at a time" );




  default -> System.out.println("Who knew boredom could be so three-dimensional?");

};

Das Beispiel zeigt Pattern-Matching sowohl ohne als auch mit Record-Pattern. Auch hier müssen wir erneut die Dominanz beachten. Es wäre inkorrekt, case Book -> über die anderen Fallblöcke case Book( var title, var pages ) when … zu setzen.

[1] https://openjdk.org/jeps/440

Inselupdate: Pattern-Matching bei switch in Java 21

Wir haben bereits gesehen, dass der instanceof-Operator genutzt werden kann, um einen einzelnen Typ zu prüfen. Nun können wir diese Fallunterscheidung erweitern, um mehrere Typen zu testen. Nehmen wir an, dass die Zustände von Nap, Workout und Event im XML-Format gespeichert werden sollen. In diesem Kontext kann eine neue Methode die Abbildung auf XML übernehmen:

static String toXml( Object o ) {

  if ( o == null )

    return "<null />";

  if ( o instanceof Nap nap )

    return "<nap about=\"%s\" duration=%s />".formatted( nap.about, nap.duration );

  else if ( o instanceof Workout workout )

    return "<workout about=\"%s\" duration=%s caloriesBurned=%s/> "

        .formatted( workout.about, workout.duration,

                    workout.caloriesBurned );

  else if ( o instanceof Event event )

    return "<event about=\"%s\" duration=%s />".formatted( event.about, event.duration );

  else

    return "<object />";

}

Gerade für solche Abfragen wurde in Java 21 die switch-Anweisung bzw. der switch-Ausdruck erweitert, um die Möglichkeit zu bieten, Typen zu testen und somit eine kaskadierte Typprüfung durchzuführen. Diese Erweiterung wird als Pattern-Matching bei switch (engl. Pattern Matching for switch)[1] bezeichnet, und es handelt sich um das Pendant zum Pattern-Matching bei instanceof.

Die Methode toXml(…) kann folgendermaßen umgeschrieben werden:

static String toXml( Object o ) {

  switch ( o ) {

    case null -> {

      return "<null />";

    }

    case Nap nap -> {

      return "<nap about=\"%s\" duration=%s />".formatted( nap.about, nap.duration );

    }

    case Workout workout -> {

      return "<workout about=\"%s\" duration=%s caloriesBurned=%s/> "

          .formatted( workout.about, workout.duration,

                      workout.caloriesBurned );

    }

    case Event event -> {

      return "<event about=\"%s\" duration=%s />".formatted( event.about, event.duration );

    }

    default -> {

      return "<object />";

    }

  }

}

Die Überprüfung auf null ist mithilfe von case null möglich – eine weitere Erweiterung von Java 21.

Hinweis: Auch in Fällen, in denen eine switch-Anweisung verwendet wird, muss beim Pattern-Matching die Abdeckung vollständig sein. Daher ist in unserem Fall der default-Zweig erforderlich.

Die gewählte Lösung mit der switch-Anweisung ist zwar umsetzbar, doch da jeder switch-Block mit einem return endet, ist auch ein switch-Ausdruck möglich. Eine alternative Schreibeweise für toXml(…) lautet:

static String toXml( Object o ) {

  return switch ( o ) {

    case null -> "<null />";

    case Nap nap ->

        "<nap about=\"%s\" duration=%s />".formatted( nap.about, nap.duration );

    case Workout workout ->

        "<workout about=\"%s\" duration=%s caloriesBurned=%s/> "

            .formatted( workout.about, workout.duration,

                        workout.caloriesBurned );

    case Event event ->

        "<event about=\"%s\" duration=%s />".formatted( event.about, event.duration );

    default -> "<object />";

  };

}

Dominanz

Normalerweise spielt bei einem switch-case die Reihenfolge der case-Blöcke keine Rolle –abgesehen vom Durchfallen, was jedoch bei -> nicht mehr existiert. Beim Pattern-Matching spielt die Reihenfolge sehr wohl eine Rolle und folgendes wäre nicht korrekt:

return switch ( o ) {      

  case null -> "<null />";

  case Event event -> "…";       // ☠ case-Label dominiert

  case Nap nap -> "…";

  …

}

Das case-Label case Event event dominiert das case-Label case Nap nap, daher müssen wir die Reihenfolge berücksichtigen. Im Übrigen gibt es bei der Ausnahmebehandlung einen ähnlichen Fall, Details finden meine Leser im Abschnitt 9.3.5, „Schon gefangen?“

Pattern-Matching mit when Wächter

Bisher haben wir in den case-Blocken nur den Typ überprüft. Es ist jedoch möglich, zusätzliche Bedingungen anzufügen. Hierfür wird nach der Pattern-Variable das Schlüsselwort when verwendet, gefolgt von einer Bedingung, die auf die Pattern-Variable:

Event event = new Nap();

switch ( event ) {

  case Nap nap

  when nap.duration < 10 ->

      System.out.println( "Too brief a sleep, not worth it." );




  case Nap nap

  when nap.duration > 100 -> System.out.println( "That's too long, wake up." );

 

  case Nap nap -> System.out.println( "Recharge and renew with every sleep" );




  case Workout workout -> System.out.println( "Elevate your fitness game");




  default -> {}

}

Hinter dem Schlüsselwort when kann eine Bedingung angegeben werden, die als Wächter (engl. guard) bezeichnet wird. Die Auswertung des case-Blocks erfolgt erst, wenn der Typ übereinstimmt und die Bedingung erfüllt ist. Die Prüfung auf den gleichen Typ kann mehrfach in verschiedenen Blöcken erfolgen. Wir müssen auch hier wieder die Dominanz berücksichtigen. So wäre es falsch, mit case Nap nap -> zu beginnen und erst dahinter ein case Nap nap when … -> zu setzen.

[1] https://openjdk.org/jeps/433

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.