Core Java & Languages - JAX-Konferenz Blog https://jax.de/blog/core-java-jvm-languages/ Java, Architecture & Software Innovation Thu, 24 Apr 2025 09:11:37 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Spring Boot vs. Quarkus: Ein direkter Vergleich aus der Praxis für Entwickler https://jax.de/blog/spring-boot-vs-quarkus-praxisvergleich/ Thu, 13 Mar 2025 14:46:58 +0000 https://jax.de/?p=107257 Bei neuen Projekten oder Frameworkmigrationen konzentriert sich die Diskussion über moderne Java-Frameworks oft auf zwei Namen: Spring Boot und Quarkus. Beide haben ihre Vorzüge und versprechen, das Leben von Entwicklern einfacher zu machen. Aber welches Framework ist nun besser geeignet? Ein Praxistest.

The post Spring Boot vs. Quarkus: Ein direkter Vergleich aus der Praxis für Entwickler appeared first on JAX.

]]>
Ich möchte mit meiner persönlichen Reise in der Softwareentwicklung beginnen, um meine Perspektive für den Vergleich zu setzen. Am Anfang meiner beruflichen Laufbahn war ich in einem großen Projekt, das alles andere als modern war. Die Anwendung war ein typischer „Big Ball of Mud“: eine Mischung aus Servlet, JSP, JSF1 und JSF2. Irgendwann haben wir uns entschieden, eine neue einheitliche Architektur mit dem damals modernen Java EE 5 bzw. 6 zu bauen. Das war meine erste Erfahrung mit Java EE. In dieser Zeit habe ich viel über die grundlegenden Konzepte gelernt, wie man Java EE sinnvoll einsetzt.

Nach diesem Projekt arbeitete ich in einem Unternehmen, das Java-EE-Systeme beim Kunden einsetzte. Das System lief meistens on Premise beim Kunden. Wir hatten eine einzige Codebasis, die auf über hundert Kundensystemen lief – mit jeweils unterschiedlichen Konfigurationen, insbesondere mit verschiedenen Kombinationen aus Application-Server und Datenbank. Das war mein erster Kontakt mit der Idee von Kompatibilität in Java EE. Ich habe erlebt, wie robust diese Technologie sein kann, selbst wenn sie auf verschiedenen Application-Servern wie WebSphere, JBoss oder NetWeaver eingesetzt wird.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Ab 2018 wechselte ich zu Spring Boot. Und was soll ich sagen: Es war ein Gamechanger. Viele Aufgaben, die früher mühsam waren, wurden durch Spring Boot praktisch und komfortabel automatisiert. Ein besonderes Highlight waren die Datenbank-Repositories mit der automatischen Generierung der JPQL-Querys aus dem Methodennamen. Das fand ich genial.

Als ich dann im Jahr 2023 ein Quarkus-Projekt angeboten bekam, wollte ich es eben wegen Quarkus haben. Meine Erwartung war, dass sich die Vorzüge von Spring Boot und Jakarta EE (ehemals Java EE) vereinen: moderne, komfortable Entwicklung zusammen mit offiziellen Standards, die man nur einmal lernen muss und die einfacher zu migrieren sind. Ich wollte verstehen, wie sich dieses neue Framework im Vergleich zu Spring Boot schlägt. Nach den ersten Monaten mit Quarkus war die Idee zu einem Vortrag geboren, der die Grundlage für diesen Artikel bildet.

Spring Boot und Quarkus

Spring Boot ist ein Open-Source-Framework, das von VMware entwickelt wurde. Es wurde 2013 veröffentlicht und basiert auf dem Spring-Framework, das seit 2003 existiert. Spring Boot ermöglicht es Entwicklern, eigenständige, produktionsreife Anwendungen zu erstellen, die ohne großen Aufwand direkt einsatzbereit sind. Auf GitHub wird es mit 75,1 k Stars, 40,7 k Forks und 3,4 k Beobachtern (Stand: November 2024) verzeichnet, und es gibt über 528 000 Ergebnisse für Spring-Boot-Projekte.

Quarkus wiederum ist ein Open-Source-Framework von Red Hat, das 2019 veröffentlicht wurde. Es ist speziell für Java-Anwendungen optimiert, die in Kubernetes-Umgebungen laufen. Quarkus basiert auf Eclipse MicroProfile und nutzt Technologien wie OpenJDK HotSpot und GraalVM. Auf GitHub hat Quarkus 13,8 k Stars, 2,7 k Forks und 259 Beobachter. Es gibt 24,6 k Quarkus-bezogene Repositories (Stand: November 2024).

Mein Vergleich: Entwicklererfahrungen im Fokus

Im Folgenden will ich nicht die technischen und operativen Vorteile von Quarkus vergleichen. Vergleiche der Startzeiten und des Speicherverbrauchs können an anderer Stelle nachgelesen werden. Hier fokussiere ich mich bewusst auf die Developer Experience.

Der Vergleich zwischen Spring Boot und Quarkus basiert auf meiner praktischen Arbeit mit beiden Frameworks. Als roten Faden habe ich eine Spring-Boot-Anwendung möglichst ähnlich mit Quarkus nachgebildet. Für die Vergleichbarkeit habe ich folgende Rahmenbedingungen gesetzt:

  • Datenbankschema und Frontend unverändert: Die JPA-Entities und das REST-basierte Frontend sollten möglichst eins zu eins austauschbar sein.
  • Im eigenen Ökosystem bleiben: Es gibt Möglichkeiten, Teile eines Frameworks in anderen zu nutzen, z. B. JAX-RS in Spring Boot oder Spring Data in Quarkus. Für den Vergleich der Developer Experience habe ich das bewusst ausgeschlossen.
  • Keine Out-of-the-box-Endpoints: Automatisch generierte Endpoints, wie sie etwa Spring Data REST bietet, wurden nicht berücksichtigt, da hier oft die Möglichkeit fehlt, individuelle fachliche Logik einzubauen.

Für den Vergleich habe ich folgende Aspekte untersucht:

  • Dokumentation
  • Start
  • REST-Interface
  • Dependency Injection
  • ORM
  • Authentication
  • Developer-Tools
  • Docker Builds
  • Testing
  • Native

Bei der Spring-Boot-Anwendung handelt es sich um eine einfache Blogsoftware, die ein REST-Interface hat, zudem Spring Boot 3.3, Spring Security mit Basic Auth, Daten in einer PostgreSQL-Datenbank speichert und über ein einfaches Frontend mit Vanilla JS, Fetch API und Bootstrap verfügt. Diese Software verwende ich üblicherweise als Lehrbeispiel, da man hier alle Aspekte einer modernen Webanwendung findet und sie dennoch einfach genug ist. Die originale Spring-Boot-Anwendung ist unter [1] zu finden, die migrierte Quarkus-Anwendung unter [2].

Dokumentation

Die Dokumentation von Spring Boot [3] ist umfangreich und gut strukturiert. Es gibt offizielle Guides, die von der Installation bis zu fortgeschrittenen Themen reichen.

Quarkus setzt auf einen pragmatischen Ansatz [4]. Die Guides sind oft kürzer, dafür aber praxisorientiert. Besonders hervorzuheben ist die spezifische Dokumentation zur nativen Image-Erstellung und Kubernetes-Integration.

Hier will ich noch anmerken, dass Red Hat das Buch „Quarkus for Spring Developers“ von Eric Deandrea [5] kostenfrei zur Verfügung stellt, was eine gute Quelle für Umlernende ist. Zudem stimme ich Adam Bien zu, wenn er auf die Kritik an der Quarkus-Doku eingeht, dass die Quarkus-Guides zu komplex seien. Da Quarkus auf dem MicroProfile des Jakarta-EE-Standards basiert [6], kann man diese ebenfalls gut nutzen und muss sie nicht extra bei Quarkus aufführen; dazu ist das Video „How To Learn Quarkus“ von Adam Bien empfehlenswert [7].

Mein Fazit zur Dokumentation: Beide Frameworks bieten eine solide Dokumentation. Hier gibt es keinen klaren Gewinner.

Start

Sowohl Spring Boot mit dem Spring Initializr [8] als auch Quarkus [9] bieten Websites an, um ein Projekt-Set-up einfach zu generieren (Abb. 1 und 2). Sie existieren jeweils auch als CLI-Werkzeug und sind auch in den gängigsten IDEs integriert. Mein Fazit zum Start: Beide Frameworks bieten hier die gleiche Funktionalität an.

Abb. 1: Projekt-Set-up mit Spring Initializr …

Abb. 2: … und mit Quarkus

REST-Interface

Spring Boot macht die Arbeit mit REST-Interfaces besonders einfach. Mit Annotationen wie @RestController und @GetMapping fügt es sich perfekt in das Spring-MVC-Framework ein. Besonders praktisch: Funktionen wie die Paginierung sind schon eingebaut und können ohne großen Aufwand genutzt werden. Hier das Beispiel eines GET Endpoint mit Paginierung in Spring Boot:

@GetMapping(path = "/entries", produces = MediaType.APPLICATION_JSON_VALUE)

public Page<Entry> getAllEntries(@ParameterObject Pageable pageable) {

  return entryService.getAllEntries(pageable);

}

Quarkus hingegen setzt auf den JAX-RS-Standard und verwendet Annotationen wie @Path und @GET. Es integriert RESTEasy sowie andere JAX-RS-Implementierungen und bietet damit eine saubere und standardisierte Lösung. Allerdings fehlt eine direkte Unterstützung der Paginierung innerhalb des ORM, was zusätzlichen Entwicklungsaufwand erfordern kann. Die Nachimplementierung dieser Funktionalität empfand ich als besonders umständlich. Das Beispiel eines GET Endpoints mit manueller Paginierung in Quarkus zeigt Listing 1.

Listing 1

@GET

@Transactional

public Response getAllEntries(

  @QueryParam("sort") List<String> sortQuery,

  @QueryParam("page") @DefaultValue("0") int pageIndex,

  @QueryParam("size") @DefaultValue("10") int pageSize) {

  // Idea from https://quarkus.io/guides/rest-data-panache#hr-generating-resources

  // But I did not find the getSortFromQuery, so I implemented it my self

  Page page = Page.of(pageIndex, pageSize);

  Sort sort = getSortFromQuery(sortQuery);

  List<Entry> entires = entryService.getAllEntries(sort, page).list();

  Long allEntriesCount = entryService.getAllEntriesCount();

  PageOutput<Entry> pageOutput = PageOutput.of(entires, pageIndex, pageSize, allEntriesCount);

 

  return Response.ok(pageOutput).build();

}

Mein Fazit zum REST-Interface: Spring Boot punktet hier durch die sofort einsatzbereite Paginierung und die saubere Möglichkeit zur Änderung des Standard-HTTP-Codes mittels der @ResponseStatus-Annotation gegenüber Quarkus.

Dependency Injection

Spring Boot nutzt die für das Spring-Framework typischen Annotationen wie @Autowired, @Component und @Service für die Dependency Injection. Eine Schwäche ist das sogenannte Self-Inject-Problem: Wenn sich eine Bean selbst injiziert, können Probleme auftreten, da man das proxy-Objekt nicht aufruft und so z. B. keine neue Transaktion auslöst, obwohl es so annotiert ist. Die Definition eines Service in Spring Boot mit Konstruktorinjektion zeigt Listing 2.

Listing 2

@Service

public class EntryService {

   private final EntryRepository entryRepository;

  private final TagRepository tagRepository;

   public EntryService(EntryRepository entryRepository, TagRepository tagRepository) {

    this.entryRepository = entryRepository;

    this.tagRepository = tagRepository;

  }

 [...]

}

Quarkus setzt auf den CDI-Standard. Annotationen wie @Inject, @ApplicationScoped und @Singleton sorgen für eine saubere Integration. Auch hier tritt das Self-Inject-Problem auf, was zeigt, dass es kein spezifisches Problem von Spring Boot ist. Die Definition eines Service in Quarkus mit Konstruktorinjektion zeigt Listing 3.

Listing 3

@ApplicationScoped

public class EntryService {

 

  private final EntryRepository entryRepository;

  private final TagRepository tagRepository;

 

  public EntryService(EntryRepository entryRepository, TagRepository tagRepository) {

    this.entryRepository = entryRepository;

    this.tagRepository = tagRepository;

  }

[...]

}

Mein Fazit zur Dependency Injection: Beide Frameworks sind in diesem Bereich gleichwertig.

ORM (Object-relational Mapping)

Spring Boot verwendet Spring Data JPA. Die Möglichkeit, Methoden einfach durch interface-Definitionen zu implementieren, ist ein großer Vorteil. Zudem reicht ein JDBC-Treiber, um eine Datenbank anzubinden. Ein Spring Data Repository mit interface-Definition sieht so aus:

public interface EntryRepository extends JpaRepository<Entry, Long> {

  List<Entry> findByTags_NameOrderByCreatedDesc(String tagName);

  List<Entry> findByAuthor(BlogUser author, Pageable pageable);

}

Quarkus bietet Panache als Ergänzung zu JPA. Diese Lösung ist zwar elegant, erreicht aber nicht den Komfort von Spring Data JPA. Jakarta Data könnte hier irgendwann einmal die gleiche Funktionalität liefern, aber die Bibliothek ist noch nicht so weit, den vollen Umfang abzubilden, den Spring Data bietet. Besonders auffällig ist, dass Quarkus bei der Standardgenerierung von Datenbankschemas Unterschiede aufweist, was bei Migrationen zu Problemen führen kann. Hier musste ich in der application.properties den Wert quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy setzen, um das gleiche Verhalten zu erhalten. Außerdem scheinen die Möglichkeiten, Datenbanken einzubinden, eingeschränkt, da hier ein einfacher JDBC-Treiber nicht ausreicht, sondern eine JDBC Driver Extension dafür existieren muss, siehe [10]. Für die gängigsten Datenbanken findet man diese zwar, aber nicht für alle. Ein Beispiel für Quarkus mit JPQL und PanacheRepository zeigt Listing 4.

Listing 4

@ApplicationScoped

public class EntryRepository implements PanacheRepository<Entry> {

  public List<Entry> findByTags_NameOrderByCreatedDesc(String tagName) {

    return list("SELECT e FROM Entry e JOIN e.tags t WHERE t.name = ?1 ORDER BY e.created DESC", tagName);

  }

  public List<Entry> findByAuthor(BlogUser author, Sort sort, Page page) {

    return find("author", sort, author).page(page).list();

  }

}

Mein Fazit zum ORM: Spring Boot ist hier für mich deutlich überlegen.

Developer-Tools

Spring Boot bietet DevTools für Hot Reload und Actuator für Monitoring. Das erleichtert die Entwicklung und Überwachung von Anwendungen.

Der Dev Mode von Quarkus ist ein echtes Highlight. Änderungen im Code werden (meistens) sofort übernommen, ohne dass die Anwendung neu gestartet werden muss. Die Dev UI bietet eine übersichtliche Darstellung von Beans, Endpunkten und Konfigurationen.

Mein Fazit zu Developer-Tools: Quarkus bietet eine modernere Entwicklererfahrung.

Docker Builds

Spring Boot bietet praktische Möglichkeiten, Docker Images zu erstellen. Mit dem Spring-Boot-Maven-Plug-in können containerisierte Anwendungen direkt gebaut werden. Spring Boot verwendet Buildpacks, um Images ohne die Erstellung eines Dockerfiles zu generieren. Die Imagegröße liegt hier bei etwa 385 MB.

Quarkus hingegen liefert ein Dockerfile direkt mit, was Entwicklern die Konfiguration erleichtert. Darüber hinaus gibt es das Plug-in quarkus.container-image, das den Build-Prozess ebenfalls vereinfacht. Allerdings ist die standardmäßige Imagegröße mit 502 MB etwas größer als bei Spring Boot.

Mein Fazit zu Docker Builds: Spring Boot hat in diesem Bereich einen Vorteil, da es kleinere Images erzeugt und man sich durch Buildpacks die Wartung der Dockerfiles spart.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Native Builds

Spring Boot unterstützt die Erstellung nativer Images durch das Plug-in org.graalvm.buildtools:native-maven-plugin. Um ein natives Image zu erstellen, wird der Befehl mvn -Pnative spring-boot:build-image verwendet. Das resultierende Image hat eine Größe von etwa 225 MB.

Quarkus bietet ebenfalls eine einfache Möglichkeit, native Images zu erstellen. Mit dem Befehl ./mvnw package -Dnative wird das native Image erzeugt. Zusätzlich kann mit docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/simple-blog-quarkus-fetch ein Docker Image erstellt werden. Es ist mit etwa 133 MB deutlich schlanker. Quarkus liefert außerdem umfassende Dokumentationen zur Kompatibilität und zu unterstützten Bibliotheken, die auf der GraalVM-Website verfügbar sind.

Mein Fazit zu Native Builds: Quarkus wurde mit Blick auf Native Builds entwickelt. Spring Boot hat in diesem Bereich seit Version 3 deutlich aufgeholt, aber Quarkus bleibt führend. Da Native Builds deutlich länger kompilieren, sollten sie in der täglichen Entwicklung lokal keine Rolle spielen. Man kann sie in Pipelines auslagern. Allerdings muss man bei Native Builds aufpassen: Nicht jede Java-Bibliotheken ist auch kompatibel mit nativen Images [11].

Testing

Spring Boot bietet mit @SpringBootTest eine Annotation, die den gesamten Spring-Kontext für Tests lädt. Damit lassen sich umfassende End-to-End-Tests durchführen, allerdings kann das Laden des gesamten Kontexts zu längeren Testzeiten führen.

Quarkus hat hier einige interessante Ansätze. Mit @QuarkusTest wird der gesamte Quarkus-Kontext geladen, ähnlich wie bei Spring Boot. Zusätzlich gibt es @QuarkusIntegrationTest, das eine Quarkus-Instanz in einer separaten JVM startet, was für Integrationstests nützlich ist. Ein besonderes Highlight von Quarkus ist die automatische Unterstützung von Testcontainern. Ohne zusätzliche Konfiguration werden beispielsweise Datenbankcontainer automatisch gestartet, was den Testaufbau erheblich vereinfacht.

Mein Fazit zum Testing: Quarkus bietet in diesem Bereich durch die automatische Testcontainer-Integration und flexible Testmöglichkeiten mehr Komfort als Spring Boot.

Fazit: Es kommt darauf an

Mein abschließendes Fazit lautet, dass die Frameworks sehr ähnlich sind, sodass sich ohne fachlichen Use Case eine Migration in die eine oder andere Richtung nicht lohnt.

Welche soll man nun wählen? Die beste Technologie ist die, die euer Team beherrscht. Der Erfolg eines Projekts hängt auch hier nicht hauptsächlich von der Wahl des Frameworks ab, sondern von den Menschen, die damit arbeiten. Wenn das Team in Spring Boot fit ist, dann hat es ohne fachliche Anforderungen keinen Sinn, Quarkus einzusetzen. Es sei denn, das Team ist neugierig und möchte Quarkus lernen. Wenn das Team die Jakarta-EE-Standards im Schlaf beherrscht und mit Spring nicht zurechtkommt, gibt es keinen Grund Spring Boot einsetzen. Also: Hört auf eure Teams.

Links & Literatur

[1] https://github.com/gruemme/simple-blog-sb-fetch

[2] https://github.com/gruemme/simple-blog-quarkus-fetch

[3] https://spring.io/projects/spring-boot

[4] https://quarkus.io/guides/

[5] Deandrea, Eric: „Quarkus for Spring Developers“: https://developers.redhat.com/e-books/quarkus-spring-developers

[6] https://microprofile.io

[7] https://www.youtube.com/watch?v=H7O7mIJCLFY

[8] https://start.spring.io

[9] https://code.quarkus.io

[10] https://quarkus.io/guides/hibernate-orm#setting-up-and-configuring-hibernate-orm

[11] https://www.graalvm.org/latest/reference-manual/native-image/metadata/Compatibility

The post Spring Boot vs. Quarkus: Ein direkter Vergleich aus der Praxis für Entwickler appeared first on JAX.

]]>
JavaFX-Animationen auf dem Prüfstand: Node oder Canvas? https://jax.de/blog/javafx-rendering-effizienz-node-canvas-vergleich/ Mon, 07 Oct 2024 08:22:14 +0000 https://jax.de/?p=90204 JavaFX bietet vielseitige Möglichkeiten für Benutzeroberflächen, doch die Wahl zwischen Nodes und Canvas ist entscheidend für die Performance. Dieser Artikel vergleicht beide Methoden und zeigt, wie sich Animationen auf Geräten wie dem Raspberry Pi auswirken. Mit einer Demoanwendung wird demonstriert, wie Canvas die Effizienz steigern kann, um die richtige Komponente für leistungsstarke JavaFX-Anwendungen zu wählen.

The post JavaFX-Animationen auf dem Prüfstand: Node oder Canvas? appeared first on JAX.

]]>
Kürzlich habe ich mit der Installation eines JDK experimentiert, das JavaFX enthält. Das vereinfacht die Ausführung von JavaFX-Anwendungen, da man die JavaFX-Laufzeitumgebung nicht separat herunterladen muss, beispielsweise von der Gluon-Website [1]. Ich habe diese Experimente auf einem Raspberry Pi durchgeführt und eine kleine Testanwendung verwendet, die eine Menge sich bewegender Punkte auf den Bildschirm bringt. Dabei habe ich bemerkt, dass die Leistung bei vielen dieser Kreisobjekte langsamer wird.

Ich habe schon mehrmals gelesen, dass ein Canvas für diese Art von Anwendungsfall viel effizienter sein kann, und das hat mich dazu veranlasst, eine Testanwendung mit „Bouncing Balls“ (Abb. 1) zu erstellen, die es einfach macht, Nodes und Canvas zu vergleichen.

Abb. 1: Meine Testanwendung

Node versus Canvas

In JavaFX sind sowohl Nodes als auch Canvas Teil des Scene Graphs, aber sie haben unterschiedliche Use Cases. Die Wahl zwischen den beiden hängt oft von den spezifischen Anforderungen Ihrer Anwendung ab. Sie verwenden Nodes für statische Inhalte wie Eingabeformulare, Datentabellen, Dashboards mit Diagrammen … Das ist in der Regel bequemer und effizienter. Das Canvas bietet Ihnen mehr Flexibilität, wenn Sie dynamische oder benutzerdefinierte Inhalte erstellen müssen.

JavaFX Node

javafx.scene.Node ist die Basisklasse und alle visuellen JavaFX-Komponenten erweitern sie. Das geht mehrere „Schichten“ tief. Zum Beispiel Button > ButtonBase > Labeled > Control > Region > Parent > Node.

Zusammengefasst:

  • Ein Node in JavaFX repräsentiert ein Element des Scene Graph.
  • Dazu gehören UI-Steuerelemente wie Buttons, Labels, Text Fields, Shapes, Images, Media, Embedded Web Browser usw.
  • Jeder Node kann im 3D-Raum positioniert und transformiert werden, er kann Events handlen und es können Effekte auf ihn angewendet werden.
  • Node ist eine Basisklasse für alle visuellen Elemente.
  • Die Verwendung von Nodes wird als „Retained Mode Rendering“ bezeichnet.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Das sind einige typische Komponenten, die von Node abgeleitet sind:

Label label = new Label("Hello World!");
Button button = new Button("Click Me!");

JavaFX Canvas

javafx.scene.canvas erweitert ebenfalls Node, fügt aber spezielle Funktionen hinzu. Sie können Ihren eigenen Inhalt auf dem Canvas zeichnen, indem Sie eine Reihe von Grafikbefehlen verwenden, die von einem GraphicsContext bereitgestellt werden.

Zusammengefasst:

  • Sie zeichnen auf einem Canvas mit einem GraphicsContext.
  • Das direkte Zeichnen auf einem Canvas wird als „Immediate Mode Rendering“ bezeichnet.
  • Das gibt Ihnen mehr Flexibilität, ist aber weniger effizient, wenn sich der Inhalt nicht oft ändert.

In diesem Beispiel wird ein Rechteck gezeichnet:

Canvas canvas = new Canvas(400, 300);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.BLUE);
gc.fillRect(50, 50, 100, 70);

Demoanwendung

Die Demoanwendung kann im GitHub Gist unter [2] gefunden werden. Sie enthält Code, um eine Menge sich bewegender Kreise zu erzeugen – sowohl als Nodes als auch gezeichnet auf einem Canvas. Der Wert am Anfang des Codes definiert, welcher Ansatz verwendet wird:

private static int TYPE_OF_TEST = 1; // 1 = Nodes, 2 = Canvas

Nodes verwenden

Wenn Sie Nodes verwenden, wird dem Bildschirm ein Bereich hinzugefügt, in dem Bälle eingefügt werden. Bei jedem Ball handelt es sich um einen Circle Node mit einer Bewegungsmethode (Listing 1).

class BallNode extends Circle {
  private final Color randomColor = Color.color(Math.random(), 
    Math.random(), Math.random());
  private final int size = r.nextInt(1, 10);
  private double dx = r.nextInt(1, 5);
  private double dy = r.nextInt(1, 5);

  public BallNode() {
    this.setRadius(size / 2);
    this.setFill(randomColor);
    relocate(r.nextInt(380), r.nextInt(620));
  }

  public void move() {
     if (hitRightOrLeftEdge()) {
      dx *= -1; // Ball hit right or left 
    }
    if (hitTopOrBottom()) {
      dy *= -1; // Ball hit top or bottom
    }
    setLayoutX(getLayoutX() + dx);
    setLayoutY(getLayoutY() + dy);
  }

  ...
}

Canvas verwenden

Wenn Sie das Canvas verwenden, ist jeder Ball ein Datenobjekt, und alle Bälle werden bei jedem Tick auf das Canvas gezeichnet (Listing 2).

class BallDrawing {
  private final Color fill = Color.color(Math.random(), 
    Math.random(), Math.random());
  private final int size = r.nextInt(1, 10);
  private double x = r.nextInt(APP_WIDTH);
  private double y = r.nextInt(APP_HEIGHT - TOP_OFFSET);
  private double dx = r.nextInt(1, 5);
  private double dy = r.nextInt(1, 5);

  public void move() {
    if (hitRightOrLeftEdge()) {
      dx *= -1; // Ball hit right or left
    }
    if (hitTopOrBottom()) {
      dy *= -1; // Ball hit top or bottom
    }
    x += dx;
    y += dy;
  }

  ...
}

Verschieben der Objekte

Die Anwendung verwendet eine Timeline, um alle fünf Millisekunden weitere Objekte hinzuzufügen und sie zu verschieben (Listing 3).

Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5), t -&amp;gt; onTick()));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();

private void onTick() {
  if (TYPE_OF_TEST == 1) {
    // Add ball nodes to the pane
    for (var i = 0; i &amp;lt; ADD_BALLS_PER_TICK; i++) {
      paneBalls.getChildren().add(new BallNode());
    }

    // Move all the balls in the pane
    for (Node ballNode : paneBalls.getChildren()) {
      ((BallNode) ballNode).move();
    }
  } else if (TYPE_OF_TEST == 2) {
    // Add balls to the list of balls to be drawn
    for (var i = 0; i &amp;lt; ADD_BALLS_PER_TICK; i++) {
      ballDrawings.add(new BallDrawing());
    }
    
    // Clear the canvas (remove all the previously balls that were drawn)
    context.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight());

    // Move all the balls in the list, and draw them on the Canvas
    for (BallDrawing ballDrawing : ballDrawings) {
      ballDrawing.move();
      context.setFill(ballDrawing.getFill());
      context.fillOval(ballDrawing.getX(), ballDrawing.getY(),
        ballDrawing.getSize(),  ballDrawing.getSize());
    }
  }
}

Ausführen der Anwendung

Zum Ausführen der Anwendung habe ich folgenden Ansatz gewählt:

  • den Code in einer Datei FxNodesVersusCanvas.java speichern
  • eine Java-Laufzeitumgebung mit JavaFX installieren, z. B. von Azul Zulu [3] oder mit SDKMAN [4]: sdk install java 22.0.1.fx-zulu
  • JBang installieren, entweder von [5] oder mit SDKMAN: sdk install jbang
  • die Anwendung starten mit: jbang FxNodesVersusCanvas.java

Leistung im Vergleich

Natürlich hängt die Leistung vom System ab, auf dem Sie die Anwendung ausführen. Wie Sie im Video unter [6] und in Abbildung 2 sehen können, habe ich es sowohl auf einem Apple Mac Studio als auch auf einem Raspberry Pi 5 ausgeführt. Das Ergebnis ist konsistent, da man ungefähr zehnmal mehr Objekte zum Canvas verglichen mit der Anzahl der Nodes hinzufügen kann, bevor die Framerate einbricht. Das ist kein „wissenschaftliches Ergebnis“, aber es vermittelt einen guten Eindruck davon, was mit Canvas erreicht werden kann.

  • Raspberry Pi wird bei 3k Nodes deutlich langsamer als bei 30k Nodes auf Canvas
  • Mac wird bei 15k Nodes langsamer als bei 150k auf Canvas

Das Bild zeigt zwei nebeneinander angeordnete JavaFX-Demos mit der Überschrift „JavaFX Demo, Nodes versus Canvas“. Beide Fenster zeigen eine große Menge bunter Punkte, die zufällig auf weißem Hintergrund verteilt sind. In der oberen Zeile wird Java-Version 17.0.1 und JavaFX-Version 22.0.1 verwendet. Das linke Fenster zeigt eine Anzahl von 7.210 Punkten und eine Bildrate von 5 FPS (Frames per Second), während das rechte Fenster 7.232 Punkte mit einer Bildrate von 16 FPS anzeigt. Es handelt sich offenbar um einen Vergleich der Leistung von JavaFX-Nodes versus Canvas.

Abb. 2: Das laufende Experiment

Fazit

Eine große Anzahl visueller Komponenten in einer typischen JavaFX-Benutzeroberfläche würde eine schlecht gestaltete Anwendung darstellen. Stellen Sie sich ein langes Registrierungsformular mit Hunderten von Eingabefeldern und Beschriftungen vor … Das würde Ihre Benutzer in den Wahnsinn treiben. Aber in anderen Fällen, in denen Sie eine komplexe Animation oder eine fortgeschrittene Benutzerschnittstellenkomponente erzeugen wollen, ist die Möglichkeit, auf dem Canvas zu zeichnen, ein idealer Ansatz.


Links & Literatur

[1] https://gluonhq.com/products/javafx/

[2] https://gist.github.com/FDelporte/c74cdf59ecd9ef1b14df86e08faa0c56

[3] https://www.azul.com/downloads/?package=jdk-fx#zulu

[4] https://sdkman.io

[5] https://www.jbang.dev

[6] https://www.youtube.com/watch?v=nJGRW5xP_AE

[7] https://leanpub.com/gettingstartedwithjavaontheraspberrypi/

[8] https://www.elektor.com/getting-started-with-java-on-the-raspberry-pi

The post JavaFX-Animationen auf dem Prüfstand: Node oder Canvas? appeared first on JAX.

]]>
Erweiterte Streams in Java 23: Was JEP 473 Entwicklern bietet https://jax.de/blog/java-23-stream-gatherers-jep-473-neuerungen/ Mon, 02 Sep 2024 12:30:52 +0000 https://jax.de/?p=90169 Java 23 bringt eine Vielzahl von Verbesserungen in der Syntax und bei den APIs. Dieser Artikel fokussiert sich auf „JEP 473 – Stream Gatherers (Second Preview)“ als interessante Neuerung bei Streams. JEP 473 folgt auf JEP 461 aus Java 22, in dem Stream Gatherers bereits als Erweiterung der Stream-Verarbeitung vorgestellt wurden, die die Definition eigener Intermediate Operations erlauben. Schauen wir uns an, was uns Neues erwartet.

The post Erweiterte Streams in Java 23: Was JEP 473 Entwicklern bietet appeared first on JAX.

]]>
Die in Java 8 LTS eingeführten Streams waren von Anfang an recht mächtig. In den folgenden Java-Versionen wurden verschiedene Erweiterungen im Bereich der Terminal Operations hinzugefügt. Erinnern wir uns: Terminal Operations dienen dazu, die Berechnungen eines Streams abzuschließen und den Stream beispielsweise in eine Collection oder einen Ergebniswert zu überführen.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Mit JEP 473 wird eine Erweiterung des Stream API zur Unterstützung benutzerdefinierter Intermediate Operations umgesetzt. Darunter versteht man Verarbeitungsschritte wie das Filtern und Transformieren, die sich zu komplexeren Aktionen verbinden lassen. Bisher gab es zwar diverse vordefinierte Intermediate Operations, eine Erweiterungsmöglichkeit war allerdings nicht vorgesehen. Eine solche ist jedoch wünschenswert, um Aufgaben realisieren zu können, die zuvor nicht ohne Weiteres oder nur mit Tricks und eher umständlich umzusetzen waren.

Einführung

Nehmen wir an, wir wollten alle Duplikate aus einem Stream herausfiltern und für ein Kriterium angeben. Um es einfach nachvollziehbar zu halten, betrachten wir einen Stream von Strings und als Kriterium deren Länge.

Hypothetisch wäre das wie folgt mit einer Intermediate Operation in Form einer fiktiven Methode distinctBy() bezüglich der Länge umsetzbar, indem man String::length als Kriterium definiert:

var result = Stream.of("Tim", "Tom", "Jim", "Mike").
  distinctBy(String::length).   // hypothetisch
  toList();

// result ==> [Tim, Mike]

Bitte beachten Sie, dass ich mich bei einigen Beispielen von jenen aus dem Original-JEP [1] inspirieren lassen und diese angepasst oder erweitert habe.

Abhilfe mit den bisherigen Möglichkeiten

Schauen wir uns einmal an, wie sich Duplikate bezüglich der Stringlänge mit den bisherigen Möglichkeiten des Stream API vermeiden lassen (Listing 1) – den dazu ebenfalls benötigten Record DistinctByLength stelle ich weiter unter vor.

jshell> var result = Stream.of("Tim", "Tom", "Jim", "Mike").
  ...>                      map(DistinctByLength::new). // #1
  ...>                      distinct().                 // #2
  ...>                      map(DistinctByLength::str). // #3
  ...>                      toList();
result ==> [Tim, Mike]

Schauen wir uns einmal an, wie sich Duplikate bezüglich der Stringlänge mit den bisherigen Möglichkeiten des Stream API vermeiden lassen (Listing 1) – den dazu ebenfalls benötigten Record DistinctByLength stelle ich weiter unter vor.

record DistinctByLength(String str)
{
  @Override
  public boolean equals(Object obj)
  {
    return obj instanceof DistinctByLength(String other) &&
      str.length() == other.length();
  }

  @Override
  public int hashCode()
  {
    return str == null ? 0 : Integer.hashCode(str.length());
  }
}

Dieser Record ist lediglich ein Wrapper um einen String und besitzt dazu ein Attribut str sowie die korrespondierende Zugriffsmethode. Damit wir den Record für unseren Zweck verwenden können, müssen wir die Methoden equals() und hashCode() auf die Stringlänge ausgerichtet überschreiben. In der Implementierung von equals() verwenden wir das Pattern Matching bei instanceof in Kombination mit Record Patterns, wodurch sich der Sourcecode sehr kompakt halten lässt.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Beispiel Gruppierung

Ein weiteres Beispiel für den Bedarf an selbst definierten Intermediate Operations ist die Gruppierung der Daten eines Streams in Abschnitte fixer Größe. Zur Demonstration sollen jeweils vier Zahlen zu einer Einheit zusammengefasst, also gruppiert werden. Für unser Beispiel sollen nur die ersten drei Gruppen ins Ergebnis aufgenommen werden. Auch hier wird wieder der an den JEP angelehnte Sourcecode mit einer fiktiven Methode windowFixed() gezeigt (Listing 3).

record DistinctByLength(String str)
{
  @Override
  public boolean equals(Object obj)
  {
    return obj instanceof DistinctByLength(String other) &&
      str.length() == other.length();
  }

  @Override
  public int hashCode()
  {
    return str == null ? 0 : Integer.hashCode(str.length());
  }
}

Neu: Interface Gatherer und die Methode gather()

Im Lauf der Jahre ist aus der Java-Community einiges an Vorschlägen und Wünschen für Intermediate Operations als Ergänzung für das Stream API eingebracht worden. Oftmals sind diese in ganz spezifischen Kontexten sinnvoll. Hätte man sie alle ins JDK integriert, hätte dies das API allerdings ziemlich aufgebläht und den Einstieg in das (ohnehin schon umfangreiche) Stream API (weiter) erschwert. Um aber dennoch die Flexibilität benutzerdefinierter Intermediate Operations zu ermöglichen, wird ein ähnlicher Ansatz wie bei den Terminal Operations und dem Extension Point in Form der Methode collect(Collector) und des Interface java.util.stream.Collector verfolgt. Durch diese Kombination lassen sich Terminal Operations bei Bedarf individuell ergänzen.

Um flexibel neue Intermediate Operations bereitstellen zu können, offeriert das Stream API nun eine Methode gather(Gatherer) in Kombination mit dem Interface

java.util.stream.Gatherer. Wollten wir die zuvor besprochene distinctBy()-Funktionalität selbst realisieren, so könnten wir dazu einen eigenen Gatherer implementieren – das würde jedoch den Rahmen dieser Einführung sprengen.

Ausgewählte Gatherer

Praktischerweise sind zur Umsetzung einiger Vorschläge und Wünsche aus der Java-Community nach spezifischen Intermediate Operations bereits ein paar Gatherer in das JDK aufgenommen worden. Sie sind in der Utility-Klasse java.util.stream.Gatherers definiert. Zum Nachvollziehen der Beispiele ist folgender Import nötig:

jshell> import java.util.stream.*

windowFixed

Um einen Stream in kleinere Bestandteile fixer Größe ohne Überlappung zu unterteilen, dient windowFixed() aus dem JDK. Greifen wir das zweite Beispiel aus der Einführung auf und schauen uns an, wie einfach es sich jetzt mit JDK-Basisfunktionalität realisieren lässt.

 

Nachfolgend wird per iterate() ein unendlicher Stream von Zahlen erzeugt und durch Aufruf von windowFixed(4) jeweils in Teilbereiche der Größe vier untergliedert. Mit limit(3) wird die Anzahl an Teilbereichen auf drei begrenzt und diese werden durch Aufruf von toList() in Form einer Liste als Ergebnis bereitgestellt (Listing 4).

jshell> var result = Stream.iterate(0, i -> i + 1).
  ...>                      gather(Gatherers.windowFixed(4)).
  ...>                      limit(3).
  ...>                      toList()
resultNew ==> [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]

Beim Unterteilen in Bereiche fester Größe gibt es einen Spezialfall zu beachten: Enthält ein Datenbestand nicht genügend Elemente, um die gewünschte Teilbereichsgröße zu füllen, enthält der letzte Teilbereich weniger Elemente. Als Beispiel dient ein Stream mit einem durch Aufruf von of() erzeugten fixen Datenbestand der Werte 0 bis einschließlich 6. Dieser wird mit windowFixed(3) in Teilbereiche der Größe drei unterteilt, wodurch der letzte Teilbereich nur ein Element enthält, nämlich die Zahl 6 (Listing 5).

jshell> var result = Stream.of(0, 1, 2, 3, 4, 5, 6).
  ...>                      gather(Gatherers.windowFixed(3)).
  ...>                      toList()
result ==> [[0, 1, 2], [3, 4, 5], [6]]

windowSliding

Neben dem Unterteilen in jeweils unabhängige Teilbereiche kann auch eine Untergliederung mit Überlagerungen von Interesse sein. Um einen Stream in kleinere Bestandteile fixer Größe mit Überlappung zu unterteilen, dient die Methode windowSliding() aus dem JDK.

Wieder wird per iterate() ein unendlicher Stream von Zahlen erzeugt und durch Aufruf von windowSliding(4) jeweils in Teilbereiche der Größe vier untergliedert, allerdings mit einer Überlappung bzw. Verschiebung um ein Element. Mit limit(3) wird die Anzahl an Teilbereichen auf drei begrenzt. Wie zuvor werden diese durch Aufruf von toList() in Form einer Liste als Ergebnis bereitgestellt (Listing 6).

jshell> var result = Stream.iterate(0, i -> i + 1).
  ...>                      gather(Gatherers.windowSliding(4)).
  ...>                      limit(3).
  ...>                      toList()
result ==> [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]]

Betrachten wir die Auswirkungen auf das zuvor als Spezialfall aufgeführte Beispiel eines Streams wieder mit den Werten 0 bis inklusive 6. Statt mit windowFixed() wird hier windowSliding() genutzt. Dadurch wird der Datenbestand in sich überlappende Teilbereiche untergliedert. Dementsprechend tritt hier die Situation eines unvollständigen letzten Teilbereichs nicht auf, sondern es werden fünf Teilbereiche mit je drei Elementen erzeugt (Listing 7).

jshell> var result = Stream.of(0, 1, 2, 3, 4, 5, 6).
  ...>                      gather(Gatherers.windowSliding(3)).
  ...>                      toList()
result ==> [[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]]

Bei dieser Art von Operation kann der zuvor behandelte Spezialfall eines Datenbestands mit einer nicht ausreichenden Menge an Elementen normalerweise nicht auftreten. Das ist lediglich dann möglich, wenn die Länge der Eingabe kleiner als die Window-Größe ist – in dem Fall besteht das Ergebnis aus der gesamten Eingabe, wie es nachfolgend für einen Datenbestand von drei Werten und eine Window-Größe von fünf zu sehen ist. Das Ergebnis ist eine Liste, die wiederum eine Liste mit drei Elementen enthält (Listing 8).

jshell> var resultNew = Stream.of(1, 2, 3).
  ...>                         gather(Gatherers.windowSliding(5)).
  ...>                         toList()
resultNew ==> [[1, 2, 3]]

fold

Dazu, nämlich die Werte eines Streams miteinander zu verknüpfen, dient die Methode fold(). Sie arbeitet ähnlich wie die Terminal Operation reduce(), die ein Ergebnis aus einer Folge von Elementen erzeugt, indem wiederholt eine Operation zur Kombination, beispielsweise + oder * für Zahlen, auf die Elemente angewendet wird. Dazu gibt man einen Startwert und eine Berechnungsvorschrift an. Diese Letztere fest, wie das bisherige Ergebnis mit dem aktuellen Element verknüpft wird.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Nutzen wir dieses Wissen als Ausgangsbasis für ein Beispiel mit fold(). Damit lässt sich die Summe der Werte mit 0 als Startwert und einer Addition als Berechnungsvorschrift wie in Listing 9 berechnen.

jshell> var crossSum = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                        gather(Gatherers.fold(() -> 0L,
  ...>                          (result, number) -> result + number)).
  ...>                        findFirst()
crossSum ==> Optional[28]

Bedenken Sie, dass gather() einen Stream als Ergebnis zurückgibt. Hier ist das ein einelementiger Stream. Um daraus einen Wert auszulesen, dient der Aufruf von findFirst(), der ein Optional<T> liefert, weil theoretisch der Stream auch leer sein könnte.

Als Berechnungsvorschrift können wir alternativ etwa eine Multiplikation mit einem Startwert von 1 nutzen und so für die spezielle Wertefolge von 1 bis 7 die Fakultät berechnen (Listing 10). Ganz allgemein handelt es sich um eine Multiplikation der gegebenen Zahlen (Listing 11).

jshell> var result = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                      gather(Gatherers.fold(() -> 1L,
  ...>                        (result, number) -> result * number)).
  ...>                      findFirst()
result ==> Optional[5040]
jshell> var result = Stream.of(10, 20, 30, 40, 50).
  ...>                      gather(Gatherers.fold(() -> 1L,
  ...>                        (result, number) -> result * number)).
  ...>                      findFirst()
result ==> Optional[12000000]

Aktionen für abweichende Typen

Was passiert, wenn wir zur Kombination der Werte auch solche Aktionen ausführen wollen, die nicht für die Typen der Werte, hier int, definiert sind? Als Beispiel wird ein Zahlenwert in einen String umgewandelt und dieser gemäß dem Zahlenwert durch Aufruf der Methode repeat() der Klasse String wiederholt (Listing 12).

jshell> var repeatedNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                               gather(Gatherers.fold(() -> "", 
  ...>                                 (result, number) -> result + 
  ...>                                 ("" +   number).repeat(number))).  
  ...>                               toList()
repeatedNumbers ==> [1223334444555556666667777777]

Varianten mir reduce()

Nur der Vollständigkeit halber seien hier die vorherigen Berechnungen als Varianten mit reduce() gezeigt. Weil reduce() eine Terminal Operation ist, lässt sie keine weitere Verarbeitung im Stream mehr zu – zudem funktionieren die Aktionen nur auf den Typen der Werte, womit sich das zuletzt gezeigte Beispiel nicht umsetzen lässt (Listing 13). Genau wie bei fold() werden bei reduce() ein Startwert und eine Berechnungsvorschrift angegeben. Daraus entsteht dann ein Ergebniswert.

jshell> var sum = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                   reduce(0, (result, number) -> result + number)
sum ==> 28

jshell> var result = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                      reduce(1, (result, number) -> result * number)
result ==> 5040

jshell> var result = Stream.of(10, 20, 30, 40, 50).
  ...>                      reduce(1, (result, number) -> result * number)
result ==> 12000000

scan

Sollen alle Elemente eines Streams zu neuen Kombinationen zusammengeführt werden, sodass jeweils immer ein Element dazukommt, kommt scan() zum Einsatz. Die Methode arbeitet ähnlich wie fold(), das die Werte zu einem Ergebnis kombiniert. Bei scan() wird dagegen für jedes weitere Element ein neues Ergebnis produziert.

 

Zunächst nutzen wir dies für die Ermittlung von Summen (Listing 14). Danach kombinieren wir Texte statt Ziffern nur durch Abwandlung des Startwerts, für den wir hier einen Leerstring nutzen, wodurch das + zu einer Stringkonkatenation wird (Listing 15).

jshell> var crossSums = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                         gather(Gatherers.scan(() -> 0, 
  ...>                           (result, number) -> result + number)).
  ...>                         toList()
crossSums ==> [1, 3, 6, 10, 15, 21, 28]
jshell> var crossSums = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                         gather(Gatherers.scan(() -> 0, 
  ...>                           (result, number) -> result + number)).
  ...>                         toList()
crossSums ==> [1, 3, 6, 10, 15, 21, 28]

Man könnte auch eine n-malige Wiederholung realisieren – dabei wird schön der Unterschied zu fold() deutlich (Listing 16).

jshell> var repeatedNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7).
  ...>                               gather(Gatherers.scan(() -> "",
  ...>                                 (result, number) -> result +   
  ...>                                 ("" + number).repeat(number))). 
  ...>                               toList()
repeatedNumbers ==> [1, 122, 122333, 1223334444, 122333444455555, 122 ... 3334444555556666667777777]

Fazit

In diesem Artikel haben wir uns mit den Stream Gatherers als Preview-Feature beschäftigt. Zunächst habe ich erläutert, warum diese Neuerung für uns Entwickler nützlich und hilfreich ist. Danach wurden diverse bereits im JDK vordefinierte Stream Gatherers anhand von kleinen Anwendungsbeispielen vorgestellt. Insbesondere wurden auch Randfälle und Besonderheiten beleuchtet. Dadurch sollten Sie einen guten ersten Überblick gewonnen haben und fit genug sein, um eigene Experimente zu starten.

Neben den Stream Gatherers enthält Java 23 viele weitere Neuerungen, die die Programmiersprache voranbringen und attraktiver machen. Diese Modernisierung sollte dazu beitragen, dass Java weiterhin konkurrenzfähig bleibt und sich in modernen Anwendungsbereichen behauptet und insbesondere auch zu anderen derzeit populären Sprachen wie Python oder Kotlin aufschließt. Auch für komplette Newbies wird die Einstiegshürde gesenkt: Dank JEP 477 (Implicitly Declared Classes and Instance Main Methods (Third Preview)) lassen sich kleinere Java-Programme viel schneller und mit deutlich weniger Zeilen sowie für Anfänger schwierigen Begrifflichkeiten erstellen.

In diesem Sinne: Happy Coding mit dem brandaktuellen Java 23!

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

The post Erweiterte Streams in Java 23: Was JEP 473 Entwicklern bietet appeared first on JAX.

]]>
Java Core 2024: Ein umfassender Überblick https://jax.de/blog/java-core-2024-ueberblick/ Thu, 04 Jul 2024 12:56:46 +0000 https://jax.de/?p=89923 Wer populäre Technologien einsetzt, bekommt im Problemfall schneller Hilfe und hat auch bei der Entwicklerrekrutierung weniger Probleme. Die aktuelle Ausgabe des von dem SaaS-Anbieter New Relic veröffentlichten Status-quo-Reports zeigt, wie es um das Java-Ökosystem steht.

The post Java Core 2024: Ein umfassender Überblick appeared first on JAX.

]]>
Der New-Relic-Report [1] speist sich überwiegend aus Daten, die das Unternehmen aus den hauseigenen Observability-Systemen erhält. Wir stellen die Ergebnisse vor und gehen kurz auf die Besonderheiten verschiedener GC-Algorithmen ein. Bei der Bewertung der Daten ist zu beachten, dass die Informationen auf im Einsatz befindlichen Systemen beruhen – Prototypen und nur auf der Workstation eines Entwicklers lebende Programme werden nur selten mit New Relic verbunden.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Schnellere Releases

Oracles Entscheidung, die Auslieferung neuer Java-Releases zu beschleunigen, wirkt sich auf das gesamte Ökosystem aus. Java 21 war aufgrund der Inklusion diverser Preview-Technologien ein besonders interessantes Release, das rasch angenommen wurde. Genauer: Nach der Auslieferung wurden binnen sechs Monaten 1,4 Prozent der überwachten Applikationen umgestellt – die Vorgängervariante Java 17 erreichte in der entsprechenden Zeitspanne nur eine Umstellung von 0,37 Prozent.

Relevant ist auch, dass Nicht-LTS-Versionen in den Ergebnissen von New Relic eine untergeordnete Rolle spielen: Weniger als 2 Prozent der untersuchten Programme nutzen sie produktiv.

Mindestens ebenso relevant ist die Frage, welches JDK beziehungsweise welche Runtime zur Ausführung verwendet wird – Oracle hat in der Vergangenheit durch eigenwillige lizenzpolitische Entscheidungen einiges an Goodwill verspielt, was sich in den Zahlen widerspiegelt (Abb. 1).

Abb. 1: Das Wachstum anderer Anbieter erfolgt fast ausschließlich auf Oracles Kosten (Bildquelle: [1])

Der Höhenflug von Amazon war dabei von kurzer Dauer, mittlerweile liegt der Großteil des Wachstums im Bereich des von der Eclipse Foundation verwalteten Adoptium. Azul Systems ist mehr oder weniger konstant, auch Red Hat und BellSoft erfreuen sich einer loyalen Nutzerschaft. SAP und Ubuntu konnten ihre (minimalen) Mindshare-Anteile indes nicht wirklich halten.

Ressourcen und ihr Management

Neue Designparadigmen wie Serverless und Microservices wurden von der Java-Community schnellstmöglich umgesetzt bzw. angenommen. Das geht mit interessanten Veränderungen hinsichtlich des Ressourcenbedarfs der individuellen Komponenten eines Systems einher.

Neue Designparadigmen wie Serverless und Microservices wurden von der Java-Community schnellstmöglich umgesetzt bzw. angenommen. Das geht mit interessanten Veränderungen hinsichtlich des Ressourcenbedarfs der individuellen Komponenten eines Systems einher.

Kennzahl 2023 [%]
1 bis 4 57,7
5 bis 8 25
9 bis 16 8,2
17 bis 32 4,1
33 bis 64 3,0
Mehr als 64 2,0

Tabelle 1: Verfügbare logische Prozessoren nach Java-Anwendungen im Jahr 2023

Im Bereich der JVM-Speicherzuweisungen setzt sich dieser Trend dagegen nicht fort. Sehr kleine VMs sind nach wie vor sehr beliebt, während sehr große Speicherbereiche eine (eher geringe) Schrumpfung zeigen. Der Gutteil der Systeme kommt indes mit weniger als 2 GB aus, was die im Markt vorherrschende Meinung von Java als Speicherfresser relativiert.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Insbesondere im Embedded-Bereich wird Java wegen der durch den Garbage Collector (GC) systemimmanenten „Denkpausen“ kritisiert. Die diversen JVM-Anbieter begegnen diesem Problem seit einiger Zeit durch neuartige GC-Algorithmen, die insbesondere auf mehrkernigen Systemen die Minderung der Probleme ermöglichen (Abb. 2). Zu berücksichtigen ist dabei, dass die verschiedenen VMs unterschiedliche Standardeinstellungen mitbringen – die Umstellung von Java 11 auf G1 und der damit einhergehende Zuwachs an darauf basierenden Systemen belegt, dass viele Installationen nach dem Prinzip „defaults are fine“ agieren.

Abb. 2: Garbage Collectors, die von Java-LTS-Versionen genutzt werden (Bildquelle: [1])

Der Rückgang im Bereich des Klassikers Serial ist im Zusammenhang mit der oben besprochenen Änderung an der Konstruktion der Systeme interessant – er hält das Gesamtsystem an, ist aber auf ressourcenbeschränkten Systemen am effizientesten.

Im Fall von G1 gilt aufgrund des Aufräumens kleiner Speicherbereiche, dass das System insbesondere auf nebenläufigen Maschinen für weniger Ärger sorgt. Der Garbage Collector kann seine Stärken vor allem dann ausspielen, wenn die JVM mehr als 4 GB Speicher zugewiesen hat. Die feinere Unterteilung macht die Aufräumprozesse effizienter.

Im Fall von G1 gilt aufgrund des Aufräumens kleiner Speicherbereiche, dass das System insbesondere auf nebenläufigen Maschinen für weniger Ärger sorgt. Der Garbage Collector kann seine Stärken vor allem dann ausspielen, wenn die JVM mehr als 4 GB Speicher zugewiesen hat. Die feinere Unterteilung macht die Aufräumprozesse effizienter.

Frameworks am Puls der Zeit

Die Verfügbarkeit von Modularisierungssystemen wie Maven oder die in Gradle integrierte Artefaktverwaltung animieren Entwickler dazu, Komponenten aus dem Ökosystem zur Erfüllung der anstehenden Aufgaben heranzuziehen.

 

Die erste in diesem Zusammenhang wichtige Frage betrifft die Art der Datenspeicherung: Schon aus dem Enterprise-Fokus folgt, dass Java-Applikationen häufig mit Datenbankservern aller Sorten interagieren. Die Herkunft von Java aus dem Sun- bzw. Oracle-Umfeld spiegelt sich in einer klaren Marktdominanz der Oracle Database wider, die in fast 20 Prozent der von New Relic überwachten Java-Applikationen zum Einsatz kommt (Tabelle 2).

Datenbank Nutzerschaft [%]
Oracle Database 17,3
PostgreSQL 14,4
MySQL 12,5
MongoDB 7,4
DynamoDB 4,9
SQL Server 4,4
Cassandra 2,7
Elasticsearch 2,5
MariaDB 1,4
Redshift 0,3

Tabelle 2: Die beliebtesten Datenbankserver [1]

Dabei dominieren klassische, auf SQL basierende Datenbanken: Werden die Anteile der drei Bestplatzierten addiert, erhält man einen Gesamtwert von 44,2 Prozent. An vierter Stelle folgt MongoDB; Java-Datenbanken wie die Graphdatenbank Neu4J sind überhaupt nicht auf den Rangplätzen anzutreffen.

Ein weiteres Thema betrifft den Verbreitungsgrad der Kryptographienutzung: Aus den von New Relic erhobenen Zahlen lässt sich ableiten, dass 41 Prozent der überwachten Applikationen auf die ein oder andere Weise auf eine Kryptographiebibliothek zurückgreifen. Das muss aber nicht unbedingt durch eine explizite Willensäußerung des Entwicklerteams bedingt sein, es ist genauso gut vorstellbar, dass die Bibliothek als Dependency einer anderen Bibliothek Eingang in die Build-Artefaktliste findet.

 

Jedenfalls zeigt die Verteilung der verwendeten Bibliotheken nur wenig Überraschendes (Abb. 3). Der erste Platz geht an den Klassiker Bouncy Castle, während die in diversen Spring-Frameworks inkludierte Spring Security den zweiten Platz einnimmt.

Abb. 3: Meistgenutzte Verschlüsselungsbibliotheken für Java-Anwendungen (Bildquelle: [1])

In seinem Report weist New Relic darauf hin, dass man ein baldiges deutliches Wachstum von Amazon Corretto erwartet. Ursache dafür ist demnach erstens die Vereinheitlichung der Software-Supply-Chain und zweitens die im Allgemeinen sehr gute Performance der diversen von Amazon implementierten Algorithmen.

Eine weitere im Report gestellte Frage betrifft die Art, wie Java-Applikationen Logging-Informationen sammeln. SLF4J wird dabei von 83 Prozent der Entwickler benutzt und damit ein Framework, das wie in Abbildung 4 schematisch dargestellt als Abstraktionsschicht zwischen der Applikation und dem jeweiligen Logging-Framework fungiert und zu einer Steigerung der Flexibilität beiträgt.

Abb. 4: SLF4J abstrahiert zwischen Applikationscode und dem jeweiligen Logging-Framework

Neben diesem genutzten Shortcut gilt, dass sich Log4j nach wie vor als absoluter Platzhirsch im Bereich der Logging-Frameworks etabliert hat: In 76,4 Prozent der von New Relic überwachten Applikationen findet sich eine Abhängigkeit auf diesen Universallogger. An zweiter Stelle steht JBoss (Abb. 5).

Abb. 5: Die beliebtesten Logging-Frameworks (Bildquelle: [1])

Stack Overflow als Pulsmesser

Dass die Anzahl der im Entwicklerfragedienst Stack Overflow zu bestimmten Technologien sichtbaren Interaktionen eine gute Benchmark für die Popularität der jeweiligen Technologie darstellt, soll in den folgenden Schritten als gegeben angenommen werden. Im Hause NewRelic bietet man mit GenAI seit einiger Zeit etwas Vergleichbares an, das auf Java-Entwickler fokussiert ist. Abbildung 6 zeigt, wie sich die Anfragen an diese KI über die verschiedenen Kategorien verteilen. Unter Learning versteht man dabei im Hause New Relic dabei nicht Machine Learning. Vielmehr handelt es sich um Fragen, die man auch als „How to“-Questions bezeichnen würde.

Abb. 6: Entwicklerfragen an die New-Relic-KI nach Themen (Bildquelle: [1])

Fazit

Die von New Relic erhobenen Informationen geben Entwicklern und Nutzern einen Überblick über den Zustand der Java-Entwicklung als Ganzes. Die rasche Annahme neuer Technologien zeigt, dass Sorgen um das Ableben der Java-Entwicklung gelinde gesagt vollkommen übertrieben sind.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 


Links & Literatur

[1] https://newrelic.com/sites/default/files/2024-05/new-relic-state-of-the-java-ecosystem-report-2024-05-21.pdf

[2] https://openjdk.org/jeps/376

The post Java Core 2024: Ein umfassender Überblick appeared first on JAX.

]]>
Java 22 … and beyond | JAX 2024 Keynote https://jax.de/blog/java-release-cycle-beschleunigt-neue-features-java-22/ Tue, 07 May 2024 11:20:44 +0000 https://jax.de/?p=89728 Brian Goetz, Java Language Architect bei Oracle, hielt am 24. April auf der JAX eine fesselnde Keynote. Er hob die jüngsten Fortschritte von Java hervor und ging dabei über die Funktionen von Java 22 hinaus. Was hält die Zukunft für Java-Entwickler:innen bereit?

The post Java 22 … and beyond | JAX 2024 Keynote appeared first on JAX.

]]>
Das neue Release-Modell für Java-Versionen ist laut Goetz ein klarer Vorteil für Entwickler:innen. Es ermöglicht einen schnelleren Zugang zu den neuesten Features und Sicherheitsupdates und sorgt so für moderne und sichere Anwendungen.

Die Verbesserungen gehen aber weit über den Release-Zyklus hinaus. Bahnbrechende Innovationen wie Project Loom (leichtgewichtige virtuelle Threads) und ZGC (Low-Pause Garbage Collection) haben die Java-Entwicklung nachhaltig verändert.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Das Projekt Panama verbessert die Entwickler:innen-Erfahrung durch eine sicherere und leistungsfähigere Methode zur Interaktion mit nativen Codebibliotheken. Das Projekt Amber führt datenorientierte Programmierfunktionen wie Records und Pattern Matching ein und macht Java ausdrucksstärker.

Die Zukunft ist vielversprechend, aber Goetz räumt auch ein, dass es Herausforderungen gibt. Library-Upgrades können durch Abhängigkeiten von älteren Funktionen behindert werden. Um diese Lücke zu schließen, schlägt er einen schnelleren Release-Zyklus für das Library-Ökosystem vor, um mit der Innovation von Java Schritt zu halten.

Diese Keynote ist vollgepackt mit Erkenntnissen für Java-Entwickler:innen. Sehen Sie sich die Aufzeichnung an, um mehr über diese Fortschritte zu erfahren und zu sehen, was Java als Nächstes erwartet!

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

The post Java 22 … and beyond | JAX 2024 Keynote appeared first on JAX.

]]>
Java Module und JPMS: Endlich bereit für den Einsatz? https://jax.de/blog/java-module-jpms-einfuehrung-vorteile-herausforderungen-best-practices/ Mon, 08 Apr 2024 07:41:42 +0000 https://jax.de/?p=89614 Wer im Web nach Java-Modulen oder dem Java Platform Module System (JPMS) sucht, stößt vor allem auf Kritik. Ist das JPMS also eher Flop als top? Auch wenn Module nur sehr langsam im Java Ecosystem angenommen werden, gibt es Projekte, die sie mit großem Erfolg einsetzen. Wir schauen die Vor- und Nachteile an, erfahren, mit welchen Best Practices man ein großes Projekt mit JPMS zum Erfolg führen kann, betrachten, welche Probleme es gibt, wann der Einsatz von JPMS sinnvoll ist, und zeigen, was Entwickler von (Open-Source-)Bibliotheken auch dann beachten sollten, wenn sie kein Interesse an Java-Modulen haben.

The post Java Module und JPMS: Endlich bereit für den Einsatz? appeared first on JAX.

]]>
Jeder Java-Entwickler kennt das: Die Sichtbarkeiten public, protected, default und private sind nicht immer hilfreich. Wer hat sich nicht schon gewünscht, dass etwas nur für das eigene Package sowie für Sub-Packages sichtbar wäre? Noch schlimmer ist das Problem der Namespace Pollution: In einem großen Projekt sammeln sich unzählige Bibliotheken im Classpath, und die Auto Completion für generische Begriffe wie List, Entity, Metadata oder StringUtil liefert alle möglichen und unmöglichen Treffer und im Worst Case den richtigen erst ganz am Ende der Liste (Abb. 1).

Abb. 1: Namespace Pollution

Lösungen für diese Probleme sowie eine Alternative für den mit Java 17 abgekündigten Security Manager [1] bieten Java-Module. Der Einsatz von Modulen ist eine grundlegende Entscheidung, da das Java-Modul-System nur ganz oder gar nicht verwendet werden kann. Wenn man es nutzt, kommen neben den Vorteilen auch grundsätzliche Veränderungen im Verhalten dazu. Im Open-Source-Projekt m-m-m [2] nutze ich das JPMS extensiv und mit großer Begeisterung. Ein weiteres großes Java-Open-Source-Projekt, das JPMS konsequent nutzt, ist Helidon [3]. Insgesamt wurde JPMS aber bisher vom Java Ecosystem nur sehr zögerlich angenommen.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Dieser Artikel versteht sich als eine logische Fortsetzung von [4] und will Mut machen, sich mit dem JPMS auseinanderzusetzen. Er geht auf Details und Best Practices ein, die über die typischen Hello-World-Tutorials zu Java-Modulen im Web hinausgehen.

Java-Module

Begriffe wie Modul oder auch Komponente werden in der IT nicht präzise definiert und daher in verschiedenen Kontexten unterschiedlich verwendet, was zu großer Verwirrung führen kann. Relativ gut kann man sich darauf einigen, dass es sich dabei um geschlossene Funktionseinheiten mit definiertem API handelt und dadurch eine bessere Kapselung erreicht wird. In Java ist ein Modul eine Einheit, wie man es von einer JAR-Datei oder einem Maven-Modul kennt, die über einen spezifischen Moduldeskriptor verfügt (oder für die als Automatic Module ein Moduldeskriptor dynamisch erzeugt wird).

Der Moduldeskriptor

Dieser Deskriptor wird im Source Code in der Datei module-info.java im Default-Package definiert. Es handelt sich hierbei um ein völlig anderes Konstrukt als Class, Interface, Enum, Annotation oder Record. Beim Build wird aus dem Modul eine JAR-Datei, die im Wurzelverzeichnis eine module-info.class-Datei enthält. Der Modul-Deskriptor definiert eine ganze Reihe wichtiger Eigenschaften des Moduls:

  • Name des Moduls
  • Abhängigkeiten von anderen Modulen
  • Liste der exportieren Packages
  • Angebotene sowie verwendete Services
  • Reflection-Zugriffe

Modulname und Packages

Der Name des Moduls dient als globaler, weltweit eindeutiger Identifier, um das Modul zu referenzieren. Er folgt den gleichen Syntaxregeln wie ein Package, ist davon aber prinzipiell völlig unabhängig. Nutzer von Maven oder Gradle können es sich als Kombination von Group-ID und Artifact-ID vorstellen. Es hat sich als Best Practice herausgestellt, Package- und Modulnamen sowie Artifact-ID in Einklang zu bringen. Das bedeutet, dass sich der Modulname als Namensraum bzw. Präfix in den Packages, die im Modul enthalten sind, wiederfindet. Diese Konvention hat auch den Vorteil, dass Entwickler, die ein Modul verwenden möchten, einfach zwischen Modulnamen und Packages hin und her mappen können, ohne in einer externen Dokumentation nachschauen zu müssen. Eine Klasse aus einem bekannten Package zu verwenden, heißt zwar noch nicht, dass der Modulname mit dem Package-Namen übereinstimmt, aber wenn der Modulname ein Präfix davon ist, kann er in modernen IDEs mit Autovervollständigung gezielt gefunden werden.

Zudem sind alle Java-Entwickler bereits mit den Best Practices für Packages samt den Namensräumen als „umgedrehten Domainnamen“ vertraut. Zur Veranschaulichung stellen wir uns als konkretes Beispiel vor, dass wir für awesome-restaurant.com arbeiten und gerade das Backend für Onlinereservierungen und -bestellungen bauen. Der Java-Code verwendet für Bestellungen bereits Packages wie com.awesome_restaurant.online.order.domain oder com.awesome_restaurant.online.order.repository. Da ein Minuszeichen in Package-Namen nicht zulässig ist, wurde es beim Mapping der Domain auf Java-Packages durch einen Unterstrich ersetzt (man kann es natürlich auch weglassen oder durch einen Punkt ersetzen).

Möchte ich nun das Backend als Java-Modul bereitstellen, könnte meine module-info.java-Datei so aussehen:

module com.awesome_restaurant.online.order {
  requires jakarta.persistence;
  // ...
  exports com.awesome_restaurant.online.order.domain;
  // ...
}

Abhängigkeiten von anderen Modulen

Abhängigkeiten werden in der module-info.java-Datei durch das Schlüsselwort requires eingeleitet. Am Ende folgt der Name des angeforderten Moduls. Im obigen Beispiel ist das jakarta.persistence, der neue Modulname der JPA aus Jakarta EE.

Optional kann requires noch einer der beiden folgenden Modifier folgen:

  • transitiv: Damit wird die Abhängigkeit transitiv, d. h. Module, die mein Modul anfordern, erhalten diese Abhängigkeit automatisch ebenfalls. Andernfalls ist das nicht so und der Code der Abhängigkeit ist für sie zunächst unsichtbar. Anders als bei Maven oder Gradle betrifft diese Eigenschaft nur die Sichtbarkeit und ändert nichts daran, dass die Abhängigkeit zur Laufzeit gebraucht wird. Somit wird beim JPMS sehr genau zwischen transitiven und nichttransitiven Abhängigkeiten unterschieden: Ich deklariere Abhängigkeiten von einem anderen Modul nur dann als transitiv, wenn es für die Verwendung meines Moduls (via API) auch für den Aufrufer sichtbar sein muss oder etwas völlig Querschnittliches ist, wie z. B. das SLF4J API zum Logging. Tauchen z. B. Typen aus diesem Modul in meinem eigenen API auf, dann deklariere ich die Abhängigkeit als transitiv. Andernfalls erhalte ich von meiner IDE eine Warnung. Abhängigkeiten, die ich nur intern für meine Implementierungen benötige, deklariere ich nicht transitiv und vermeide damit das Namespace-Pollution-Problem.
  • static: Damit wird eine Abhängigkeit optional, d. h., das angeforderte Modul ist zur Laufzeit nicht erforderlich und das JPMS wirft keinen Fehler, wenn es fehlt. Natürlich muss ich meinen Code so schreiben, dass er damit umgehen kann. Am Schlüsselwort static sieht man mal wieder, dass der Java-Compiler auf reservierte Schlüsselwörter fokussiert ist. Hier wurde also offensichtlich ein bereits in Java reserviertes Schlüsselwort missbraucht, was für diesen Kontext nicht gerade selbsterklärend ist. Es wäre wesentlich intuitiver gewesen, hier einfach optional statt static zu schreiben.

Damit kommen wir zur ersten starken Einschränkung bei der Verwendung des JPMS: In meinem Modul sehe ich fremden Code nur dann, wenn er aus einem Modul kommt, das als Abhängigkeit per requires importiert wird (inklusive transitiver Abhängigkeiten). Das betrifft sogar Klassen aus dem JDK – bis auf solche, die aus dem Modul java.base stammen. Das ist super, weil es das eingangs erwähnte Problem der Namespace Pollution löst.

 

Gleichzeitig bedeutet es aber auch, dass ich nur Code importieren und auf ihn zugreifen kann, wenn er selbst wiederum als Java-Modul bereitsteht. Und damit beginnt schon ein großes Dilemma: Die coole Open-Source-Bibliothek, die ich gerne nutzen möchte, ist kein Modul? Dann kann ich sie in meinem Modul erst mal nicht verwenden.

An dieser Stelle möchten wir die verschiedenen Arten von Java-Modulen differenzieren:

  • Systemmodule werden vom JRE bzw. JDK bereitgestellt. Mit dem Projekt Jigsaw (auf Deutsch Stichsäge) hat Oracle die Mammutaufgabe gemeistert, das JDK als historisch gewachsenen Codehaufen sauber in Java-Module mit definierten Abhängigkeiten zu zerschneiden. Das beschleunigt den Start der JVM. Module wie java.desktop (Swing) oder java.rmi mit all ihren Klassen müssen nicht geladen werden, wenn man sie nicht braucht.
  • Custom- oder Application-Module sind Module des Java Ecosystem, die explizit über einen Moduldeskriptor (module-info.class) verfügen, aber nicht zum JDK gehören. Auf ihnen liegt der Fokus dieses Artikels.
  • Automatic Module sind ein Behelfsmittel für das angesprochene Problem, eine Bibliothek aus einem Modul zu nutzen, die selbst keinen Moduldeskriptor hat. Entwickler von (Open-Source-)Bibliotheken für Java, die keine Lust haben, Moduldeskriptoren für ihre JARs zu pflegen, sollten unbedingt in ihrer Manifest-Datei einen Modulnamen über die Property Automatic-Module-Name festlegen. Damit kann das JAR über diesen Modulnamen importiert werden und alle Packages sind automatisch exportiert. Ist auch das nicht der Fall, bleibt als letzter Notanker nur die Möglichkeit, die Bibliothek nach bestimmten Regeln über den Kernbestandteil des Dateinamens der JAR-Datei anzusprechen. Das mag zum Ausprobieren einen Versuch wert sein – ich rate jedoch davon ab, es als finale Lösung für ein Release zu verwenden. Stattdessen sollte man die Autoren der Bibliothek per Feature-Request-Ticket überzeugen, einen Automatic-Module-Name festzulegen oder man verzichtet einfach auf den Einsatz der Bibliothek und sucht eine Alternative.
  • Unnamed Module: In Java gibt es genau ein Unnamed Module, in dem automatisch alles landet, was kein Modul ist. Andere Module können aber nicht auf Code daraus zugreifen.

Neue Spielregeln für Packages und Reflection

Die Spielregeln beim JPMS sind vom Prinzip her das Gegenteil von dem, was man als Java-Entwickler mit den normalen Classpaths gewohnt ist: Im normalen Java ist grundsätzlich alles sichtbar und erlaubt, außer, wir schränken es mit Schlüsselwörtern, wie z. B. private ein. Packages selbst sind immer public. Beim JPMS ist mein eigener Code zunächst nur innerhalb meines Moduls sichtbar. Will ich das ändern, muss ich Packages explizit exportieren. Hierzu verwenden wir in der module-info.java-Datei das Schlüsselwort exports, gefolgt von dem Package, das wir veröffentlichen möchten. Das geht nur für Packages, die im Modul auch existieren und nicht leer sind. Ich muss also jedes einzelne Package, das ich exportieren möchte, explizit auflisten.

Eine Syntax, die alle Packages automatisch exportiert, ist nicht vorgesehen. Das ist auch sinnvoll, denn so wird verhindert, dass ein nachträglich hinzugefügtes Package aus Versehen exportiert und damit öffentlich wird. Es geht hierbei um das Geheimnisprinzip, das der erfahrene Java-Entwickler aus der Softwarearchitektur kennt. Nur beim Automatic Module werden automatisch alle Packages exportiert und damit das gesamte Verhalten von JARs im Classpath für Module simuliert.

Die Restriktionen gehen aber noch weiter: Ich kann keinen Code in ein Package platzieren, in das bereits ein anderes Modul Code platziert – auch wenn ich als Autor dieses anderen Moduls alles unter meiner Kontrolle habe und sogar dann nicht, wenn das Package nicht exportiert ist. Dieses sogenannte Split-Package-Konstrukt wird in konventionellem Java manchmal eingesetzt, um Sichtbarkeiten bewusst zu hintergehen: Ich lege meine eigene Klasse ins Package org.hibernate, um an eine Methode von Hibernate zu kommen, die protected oder default als Sichtbarkeit hat. Und wenn dieser Hack nicht reicht, hole ich den Holzhammer raus und nutze Reflection und mache mit der Methode setAccessible(true) alles zugänglich. Um die Katze vollständig aus dem Sack zu lassen: Reflection geht in JPMS per Default auch nicht mehr, außer, sie wird explizit erlaubt.

Als ich mich zum ersten Mal mit dem JPMS auseinandergesetzt habe, war spätestens das der Moment, wo ich geschluckt und das Thema erstmal beiseite gewischt habe. Das ist vermutlich auch der Grund, warum viele Artikel im Web so negativ über das JPMS berichten und vom Einsatz abraten. Das ist aber sehr schade, denn meines Erachtens gründet diese Reaktion ausschließlich in der Macht der Gewohnheit.

IT ist ständig im Wandel und Patterns, die vor zehn oder zwanzig Jahren unser Denken bestimmt haben, sind heute überholt und wurden durch neue ersetzt. Aus Sicht von IT-Security und Zero Trust ist das Design des JPMS genial für einen Neustart. Sind nicht gerade Reflection und Serialisierung die Wurzel aller Übel der gravierenden CVEs in der Java-Welt? Und ist nicht die Verwendung von (Deep) Reflection zur Laufzeit spätestens seit dem Erscheinen von GraalVM, Quarkus und Co. ein Konstrukt des vergangenen Jahrtausends? Mit JEP 411 wurde der Security Manager beerdigt und man sollte sich die Frage stellen, ob man nicht auf einem sinkenden Schiff segelt, wenn man sich nicht mit dem JPMS auseinandersetzt.

Wer sich in diesem Sinne auf das JPMS einlassen kann, der wird sich nach einer gewissen Eingewöhnungszeit fragen, warum Java nicht von Anfang an so konzipiert wurde. Mit dem konkreten Ausprobieren kommt auch die Erfahrung, um die Vor- und Nachteile abwägen zu können und zu wissen, in welchem Vorhaben der Einsatz sinnvoll ist und wo eher nicht. Der Vollständigkeit halber sei noch erwähnt, dass das Schlüsselwort open vor module im Moduldeskriptor das ganze Modul per Reflection freigibt oder das Schlüsselwort opens dies nur für ein explizites Package erlaubt (analog zu exports). Im Allgemeinen rate ich von diesem Konstrukt jedoch ab.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

In meiner Zeit vor dem JPMS verwendete ich in meinen Packages Segmente wie api oder impl, um das auszudrücken, was ich ohne JPMS nicht ausdrücken konnte. Mit externen Tools wie SonarQube konnte ich dann ungewollte Zugriffe aus einem api-Package auf ein impl-Package aufdecken. Inzwischen kann ich auf künstliche Segmente wie api verzichten und über meinen Moduldeskriptor festlegen, welches Package zum API gehört und daher exportiert wird. Die Implementierungen können immer noch in impl-Sub-Packages liegen, wenn man es explizit machen möchte. Da diese für andere Module unsichtbar sind, können die Packages und sämtliche enthaltenen Klassen nach Belieben umbenannt werden, ohne Gefahr zu laufen, dass ein Nutzer des Moduls beim nächsten Release Compilerfehler erhält. Das Beste ist, dass alle diese internen Klassen und ihre Methoden nach Belieben public sein dürfen und der Spagat zwischen flexibler interner Nutzung und Verhinderung ungewollter externer Nutzung beendet ist.

Was aber, wenn mein Projekt aus vielen Modulen besteht und ich bestimmte Zugriffe für eigene Module erlauben und für fremde Module verbieten will? Kein Problem! Im Moduldeskriptor kann ich dazu beim exports-Statement nach dem Package und dem Schlüsselwort to einen oder mehrere Modulnamen angeben, auf die der Zugriff eingeschränkt wird. In unserem Beispiel könnte das so aussehen:

exports com.awesome_restaurant.online.order.repository to
com.awesome_restaurant.online.order.app,
com.awesome_restaurant.online.reservation.app;

Wichtig zum Verständnis ist, dass nach exports ein Package kommt und nach to Modulnamen. Wildcards, um gleich einen ganzen Namensraum zu erlauben (z. B. com.awesome_restaurant.*), werden dabei leider nicht unterstützt.

Services 2.0

In Java gibt es seit der Version 1.6 den ServiceLoader, um Services dynamisch anzufordern, die sich über textuelle Dateien unter META-INF/services konfigurieren lassen. Das Konstrukt hat keine übermäßige Verbreitung gefunden und ist vielen Java-Entwicklern unbekannt. In Frameworks wie Spring oder Quarkus hat man mit Dependency Injection bereits mächtigere Werkzeuge für dieses Problem.

Mit dem JPMS erfährt der ServiceLoader aber ein Facelifting und damit eine Wiedergeburt: Im Moduldeskriptor können angebotene und genutzte Services Refactoring-stabil angegeben werden. Damit können Module Implementierungen einer Schnittstelle (oder abstrakten Klasse) anfordern, und beliebige andere Module können dazu Implementierungen bereitstellen. Dieses Konstrukt ermöglicht es, mit purem Java, unabhängig von irgendwelchen Frameworks (und Reflection Magic), dynamische Services umzusetzen. Egal ob mit ServiceLoader oder Frameworks wie Spring haben sich für mich zwei Muster herauskristallisiert:

  • Singleton: Ich möchte zu einem Interface genau eine Instanz haben. Woher die Implementierung kommt und wie diese zusammengebaut wird, ist für die Nutzung abstrahiert und wird vom Framework oder eben von Java geregelt. Ein Beispiel wäre ein Interface ConfigurationService oder der berüchtigte EntityManager.
  • Plug-ins: Ich möchte etwas flexibel erweiterbar gestalten und biete selbst ein Interface an, zu dem es auch zur Laufzeit viele unterschiedliche Implementierungen parallel geben darf, die wir Plug-ins nennen. Sie erweitern den Funktionsumfang und dazu können unterschiedliche Parteien in Harmonie beitragen. Als Beispiel stellen wir uns vor, dass wir eine Suchmaschine programmieren, und bieten als Plug-in-Interface TextExtractor an, das zu einem bestimmten Dateiformat den reinen Text extrahieren kann. Ein Modul stellt eine Implementierung PdfTextExtractor bereit, ein anderes XpsTextExtractor usw. Keins dieser Module exportiert irgendetwas, und die nutzende Anwendung importiert diese Module, ohne direkten Zugriff auf deren Klassen zu erhalten.

 

In beiden Fällen füge ich im Modul, das auf den Service zugreifen möchte, ein uses-Statement in den Modul-Deskriptor ein und gebe dabei das Interface des Service an:

module org.search.engine {
  uses org.search.engine.extractor.TextExtractor;
  exports org.search.engine;
  exports org.search.engine.extractor;
  // ...
}

Um im Code an Implementierungen des Service TextExtractor zu gelangen, erzeuge ich einen passenden ServiceLoader und iteriere über die verfügbaren Implementierungen:

ServiceLoader<TextExtractor> serviceLoader = ServiceLoader.load(TextExtractor.class);
for (TextExtractor extractor : serviceLoader) {
  register(extractor);
}

Dabei ist es wichtig, dass der Aufruf von ServiceLoader.load im Code des Moduls stattfindet, in dem das uses-Statement definiert ist. Wer eine generische Hilfsklasse in ein anderes Modul auslagert, die den ServiceLoader anhand eines übergebenen Class-Objekts lädt, wird das schmerzlich bemerken. Die Hilfsklasse habe ich in meinem Projekt trotzdem gebaut, muss aber den ServiceLoader außerhalb im verantwortlichen Modul erzeugen und ihn dann an die Hilfsklasse übergeben. Nutzen entsteht aber erst dann, wenn für den Service auch mindestens eine Implementierung bereitgestellt wird.

In einem anderen Modul implementieren wir dazu besagten PdfTextExtractor und stellen diese Implementierung über ein provides-Statement im Moduldeskriptor zur Verfügung:

module com.company.search.extractor.pdf {
  provides org.search.engine.extractor.TextExtractor
  with com.company.search.extractor.pdf.PdfTextExtractor;
}

Eine Anwendung, die das Ganze im Zusammenspiel verwenden möchte, importiert die Suchmaschine samt der gewünschten Plug-in-Module:

module com.awesome_restaurant.online.app {
  requires org.search.engine;
  requires com.company.search.extractor.pdf;
  requires com.enterprise.search.extractor.xps;
  // ...
}

Brauche ich den XPS-Support nicht mehr, entferne ich einfach das entsprechende requires-Statement. Will ich ein anderes Format unterstützen, lade ich das passende Plug-in über eine weitere requires-Anweisung (wobei das requires-Statement optional ist, solange sich das Modul mit dem Plug-in im Modulpfad befindet).

Hier noch einige Tipps aus dem praktischen Umgang damit:

  • Findet der ServiceLoader zur Laufzeit für den angeforderten Service gar keine oder „zu viele“ Implementierung(en), so muss ich mich selbst um die Fehlerbehandlung kümmern.
  • Für einen Singleton Service möchte ich eine Exception, wenn keine Implementierung gefunden wurde. Gegebenenfalls möchte ich aber auch eine Default- bzw. Fallback-Implementierung mitliefern, die dann verwendet wird.
  • Finde ich für einen Singleton Service mehrere Implementierungen, will ich typischerweise auch eine Exception. Doch der ServiceLoader und das Java-Modul bieten nicht viel Flexibilität (wie z. B. DI-Frameworks aus Spring oder Quarkus): Eine Möglichkeit, einen Service durch ein Modul wieder zu entfernen oder zu ersetzen, gibt es nicht. Mit extrem feingranularen Modulschnitten kann man das alles irgendwie lösen, aber das überfordert schnell die Nutzer meiner Module. Daher habe ich mir angewöhnt, Implementierungen aus meinem eigenen Package Namespace bei Singleton Services zu ignorieren, falls es auch eine Implementierung aus einem externen Package gibt. Damit erlaube ich externen Nutzern meiner Bibliotheken, meine Implementierung durch ihre eigene zu ersetzen, und erhöhe die Flexibilität.

Internationalisierung

Java bietet mit ResourceBundle und MessageFormat die Grundlagen für die Internationalisierung einer Anwendung, also für die Unterstützung mehrerer Sprachen z. B. bei Texten für den Endnutzer. Das Laden eines solchen ResourceBundle aus properties-Dateien (z. B. UiMessages_de.properties) lädt jedoch eine Ressource per ClassLoader, was wie ein Reflection-Zugriff gewertet wird. Daher gibt es Probleme, wenn man diese properties-Dateien in einem Modul verpackt und dann generischen Code aus einem anderen Modul diese als ResourceBundle laden will. Solche Aspekte sind in Java nicht klar dokumentiert und es hat mich initial etwas Zeit gekostet, das vollständig zu verstehen. Um nicht das komplette Modul mit dem Schlüsselwort open für Reflection freizugeben, ist die pragmatischste Lösung, Bundles zur Internationalisierung in separaten JAR-Dateien ohne Moduldeskriptor auszuliefern. Sind sie im Class-/Modulpfad, so landen sie im Unnamed Module und können ohne Probleme geladen werden.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

IDE-Unterstützung

Als ich die ersten Versuche mit Java-Modulen gemacht habe, war die Unterstützung in gängigen Entwicklungsumgebungen katastrophal bis nicht vorhanden. Inzwischen hat sich das deutlich stabilisiert und auch Code Completion und Refactoring werden unterstützt. Ich kann sowohl in IntelliJ als auch in Eclipse komplexe Projekte mit vielen Java-Modulen zuverlässig entwickeln. In Eclipse gibt es noch diverse Bugs bei module-info.java-Dateien mit dem Code-Formatter und insbesondere mit Import-Statements. Ich verzichte daher einfach auf Imports in diesen Dateien und verwende voll qualifizierte Typen. Auch bei IntelliJ gibt es Kinderkrankheiten, wie z. B. IllegalAccessError, weil irgendein Teil von JUnit im Unnamed Module gelandet ist.

Tests

Im einfachen Fall schreibt man seine JUnit-Tests ganz normal wie in Projekten ohne Java-Module. Will man aber im Test ein zusätzliches Testmodul haben, einen Service zum Testen hinzufügen oder Ähnliches, gibt es Probleme. Eigentlich würde man gern unter src/test/java eine zusätzliche module-info.java hinterlegen können, die dann für den Test verwendet wird. Genau das wird auch in der Maven-Dokumentation als Lösung propagiert – wird aber von IDEs gar nicht unterstützt [5]. Als Workaround erstelle ich dann zusätzliche Module, die nur zum Testen dienen, und schließe diese vom Deployment aus, sodass sie nicht im Release veröffentlicht werden.

Entscheidung für oder gegen JPMS

Im nächsten Spring-Boot- oder Quarkus-Projekt würde ich persönlich JPMS trotzdem noch nicht nutzen. Diese Frameworks basieren maßgeblich auf Reflection, und mit Java-Modulen schaffe ich mir hier viele Probleme, ohne großen Nutzen zu stiften. Inzwischen ist es immerhin technisch möglich, was schon mal als großer Fortschritt zu werten ist. Wenn ich ein Backend mit JPMS ausprobieren will, kann ich das mit Helidon tun, das besser für JPMS geeignet ist, jedoch im Vergleich zu Spring Boot und Quarkus eher ein Schattendasein fristet. Micronaut kann mit dem JPMS aktuell gar nicht verwendet werden [6].

Schreibe ich hingegen eine Java-App ohne große Frameworks, sollte ich das JPMS ausprobieren. Entwickle ich eine Bibliothek, die möglichst viele Nutzer erreichen soll, ist die Unterstützung des JPMS Pflicht. Andernfalls schließe ich diverse potenzielle Nutzer von vornherein aus.

Fazit und Ausblick

Im Jahr 2018 konnte man das JPMS zwar schon vollständig nutzen, kämpfte aber gegen Windmühlen. In der heutigen Zeit hat sich das JPMS so weit etabliert, dass die Unterstützung sämtlicher Tools ausgereift und die der wichtigen Java-Bibliotheken gegeben ist. Es ist also an der Zeit, sich intensiver mit dem Thema auseinanderzusetzen und eigene Experimente und Erfahrungen zu machen. Für Entwickler von Bibliotheken ist es heutzutage Pflicht, Moduldeskriptoren (mindestens als Automatic-Module-Name) mitzuliefern.

Wenn sich Java weiter weg von Deep Reflection zur Laufzeit hin zu AoT und CWA entwickelt und auch die Frameworks sich entsprechend weiterentwickeln, könnte die Verwendung des JPMS in der Zukunft zumindest für Green-Field-Projekte zum Standard werden.


Links & Literatur

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

[2] https://m-m-m.github.io

[3] https://github.com/helidon-io/helidon

[4] Langer, Angelika; Kreft, Klaus: „Eine Anwendung in vielen Teilen. Project Jigsaw aka Java Module System“; in: Java Magazin 10.2017, https://entwickler.de/reader/reading/java-magazin/10.2017/1976daae3f43cab4336cd7ad

[5] https://github.com/eclipse-jdt/eclipse.jdt.core/issues/1465

[6] https://github.com/micronaut-projects/micronaut-core/issues/6395

The post Java Module und JPMS: Endlich bereit für den Einsatz? appeared first on JAX.

]]>
Showdown der funktionalen Programmierung auf der JVM https://jax.de/blog/kotlin-vs-scala-funktionale-programmierung-jvm/ Mon, 19 Feb 2024 10:08:20 +0000 https://jax.de/?p=89461 Kotlin ist eine Weiterentwicklung von Java mit zusätzlichen Features und Vereinfachungen. Die Ideen dafür kamen überwiegend aus der funktionalen Programmierung. Kein Wunder, bietet diese doch gegenüber OOP viele Vorteile, wie architektonische Entkopplung durch unveränderliche Daten und bessere Domänenmodelle durch mächtigere Abstraktionen. Aber reichen Kotlins Innovationen aus, um es zu einer waschechten funktionalen Programmiersprache zu machen?

The post Showdown der funktionalen Programmierung auf der JVM appeared first on JAX.

]]>
Der Begriff „funktionale Programmiersprache“ gründete sich ursprünglich darauf, dass die Sprache Funktionen „erstklassig“, also als normale Objekte behandelt und erlaubt, sie zur Laufzeit zu erzeugen. Bei statisch typisierten Sprachen (und dazu gehört auch Kotlin) bedeutet der Begriff außerdem, dass es das Typsystem erlaubt, Funktionen zu beschreiben, und die erstklassige Manipulation von Funktionen zumindest nicht behindert.

Ein wichtiger pragmatischer Aspekt funktionaler Programmierung (FP) ist die Verwendung unveränderlicher (immutable) Daten. Diese kann eine Programmiersprache mehr oder weniger gut unterstützen. Dazu gehört auch die Möglichkeit, Funktionen ohne die Verwendung von Zuweisungen zu schreiben. Schließlich zählt die Unterstützung von höherstehenden Abstraktionen wie Monaden zur Standardausrüstung funktionaler Sprachen.

Unveränderliche Daten

Das mit den unveränderlichen Daten klingt eigentlich ganz einfach: Wir verzichten auf Zuweisungen, fertig. Damit das praktikabel wird, gehört allerdings doch mehr dazu. Wenn eine Klasse unveränderliche Objekte beschreibt, ist es meist sinnvoll, einen Konstruktor zu definieren, der für jedes Attribut der Klasse ein Argument akzeptiert, außerdem equals– und hashCode()-Methoden, die ausschließlich über die Gleichheit beziehungsweise die Hashfunktion der Attribute definiert sind, Ähnliches gilt für die toString()-Methode. In Java hat die IDE, die Konstruktor und Methoden automatisch generieren kann, diesen Job traditionell erledigt. Danach liegt aber eine Menge Code herum, der nur das Offensichtliche tut und Übersicht sowie Wartung behindert.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Glücklicherweise hat Kotlin sogenannte Data Classes, in denen dieser Code automatisch vom Compiler generiert wird. Das ist so eine Klasse:

data class Rectangle(val length: Double, val width: Double)

Das data vor class sorgt für die automatische Generierung von Konstruktor und Methoden, val sorgt dafür, dass die entsprechenden Attribute nicht verändert werden können. (Weil das so praktisch ist, hat Java längst mit Records nachgezogen.)

Bei funktionalen Sprachen sind Pendants zur Java Collections Library dabei, die allerdings ebenfalls ohne Veränderungen auskommen. Bei Kotlin sind scheinbar unveränderliche Collections dabei. So hängen wir zum Beispiel eine Liste an eine andere Liste:

val list1 = listOf(1,2,3)
val list2 = listOf(4,5,6)
val list3 = list1 + list2

Hier ist list3 eine neue Liste mit den gleichen Elementen wie list1 (die unverändert bleibt) und den Elementen von list2 zusätzlich dahinter. Hinter den Kulissen tut aber die gute alte Java ArrayList ihren Dienst: Das bedeutet, dass die Operation + list2 ein neues Array für list3 anlegt und alle Elemente sowohl von list1 als auch von list2 nach list3 kopiert – und das ist teuer. Aus diesem Grund verwenden die Collection Libraries funktionaler Sprachen sogenannte funktionale Datenstrukturen, die nicht bei jedem Update alles kopieren müssen. Insbesondere ist eine Liste in der funktionalen Programmierung eine konkrete Datenstruktur, die bei allen (anderen) funktionalen Sprachen fest eingebaut ist. Wir können sie aber in Kotlin nachbauen:

sealed interface List<out A>
object Empty : List<Nothing>
data class Cons<A>(val first: A, val rest: List<A>) : List<A>

Eine Liste ist also entweder Empty (die leere Liste) oder eine sogenannte Cons-Liste, bestehend aus erstem Element und rest-Liste (es handelt sich also um eine unveränderliche, einfach verkettete Liste). FP-Pendants zu den ersten beiden Listen von oben können wir so konstruieren:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))
val list2 = Cons(4, Cons(5, Cons(6, Empty)))

Das ist etwas umständlich, verglichen mit listOf, dieses Manko könnten wir aber mit einer einfachen varargs-Funktion beheben. Die plus-Methode auf den eingebauten Kotlin-Listen können wir für die FP-Listen ebenfalls implementieren:

fun <A> List<A>.append(other: List<A>): List<A> =
  when (this) {
    is Empty -> other
    is Cons ->
       Cons(this.first, this.rest.append(other))
  }

Das Beispiel von oben funktioniert mit FP-Listen so:

val list3 = list1.append(list2)

Die append-Methode wirft ein paar Fragen auf: Warum ist sie eine „Extension Method“? Könnte das bei der Rekursion Probleme machen? Dazu später mehr, hier geht es ja erst einmal um unveränderliche Daten: Und tatsächlich verändert append gar nichts, kopiert die Elemente aus list1, hängt aber list2 an, ohne sie zu kopieren. Hinterher teilen sich also list3 und list2 dieselben Cons-Objekte. Weil Listen unveränderlich sind, ist das unproblematisch: Das Programm kann so tun, als wären list2 und list3 völlig unabhängig voneinander. Die append-Methode verbraucht nur so viel Laufzeit und Speicher, wie list1 lang ist – die Länge von list2 ist völlig irrelevant.

Listen sind für viele Situationen geeignet, aber natürlich nicht für alle – wenn list1 lang ist, braucht append auch mehr Zeit und viel Speicherplatz. Entsprechend haben die Collection Libraries funktionaler Sprachen in der Regel auch noch ausgefeiltere Datenstrukturen auf der Basis von Bäumen beziehungsweise Trees im Gepäck, bei denen die meisten Operationen quasi konstant laufen.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Solche funktionalen Datenstrukturen gibt es auch für Java und Kotlin in Form der Vavr-Library [1], die neben FP-Listen auch Vektoren mitbringt. Für Vavr gibt es auch das Add-on vavr-kotlin [2], das die Vavr Collections in Kotlin noch etwas idiomatischer zugänglich macht. Zwischenfazit: Funktionale Datenstrukturen sind bei Kotlin nicht standardmäßig dabei, können aber in Form von Vavr nachgeladen werden. Tolle Sache.

Erstklassige Funktionen

Kommen wir zur Unterstützung für Funktionen als Objekte: Da macht Kotlin gegenüber Java einen riesigen Sprung durch zwei Neuerungen:

  • Das Kotlin-Typsystem kennt Funktionstypen, die man sich in Java umständlich als Single-Method-Interface selbst definieren muss.
  • Kotlin macht effektiv keinen Unterschied zwischen Value Types und Reference Types.

Was das bringt, zeigt die Implementierung des FP-Klassikers map, den eine Funktion (die als Objekt übergeben wird) auf alle Elemente einer Liste anwendet und dann eine Liste der Ergebnisse zurückliefert:

fun <A, B> List<A>.map(f: (A) -> B): List<B> =
  when (this) {
    is Empty -> Empty
    is Cons ->
     Cons(f(this.first),
       this.rest.map(f))
  }

Super-Sache:

val list4 = list3.map { x -> x + 1 }

Das geht natürlich auch in Java, das Pendant sieht fast identisch aus:

var list4 = list3.map(x -> x + 1);

Allerdings funktioniert das in Java nur so richtig gut, wenn der Lambdaausdruck unmittelbar im Methodenaufruf steht. In dem Moment, in dem wir ihn herausziehen, sieht die Sache anders aus. In Kotlin ist das kein Problem:

val inc = { x: Int -> x + 1 }
val list4 = list3.map(inc)

Wir müssen bei inc den Typ von x angeben, weil noch andere Typen die Operation + 1 (Double, Long) unterstützen. Den Typ der Funktion inferiert Kotlin nun als (Int) -> Int. In Java müssen wir uns allerdings beim Typ von inc für ein Interface entscheiden – am Naheliegendsten ist IntUnaryOperator:

IntUnaryOperator inc = x -> x + 1;

Allerdings können wir den nicht an map übergeben – es kommt zu einer Fehlermeldung: „‘map(Function<Integer,B>)’ cannot be applied to ‘(IntUnaryOperator)’“. Das liegt daran, dass map ein Argument des Typs Function<A, B> akzeptiert – wir mussten uns ja für ein spezifisches Interface entscheiden. Leider passen Function und IntUnaryOperator nicht zueinander, weil Function auf Referenztypen besteht, während IntUnaryOperator auf den Value Type int festgelegt ist. Die Konsequenz ist, dass zum Beispiel das Stream-Interface in Java neben map auch mapToInt, mapToDouble und mapToLong anbieten muss, die jeweils ein IntStream, DoubleStream und LongStream produzieren. Das ist umständlich und macht fortgeschrittene Anwendungen von Funktionen höherer Ordnung in Java unpraktikabel – in Kotlin kein Problem, ein weiterer Pluspunkt.

Schleifen und Endrekursion

Zu erstklassiger Unterstützung von Funktionen gehört auch, dass wir in der Lage sein sollten, Funktionen ohne Verwendung von Zuweisungen zu schreiben. Dafür sind append und map gute Beispiele – rein funktional programmiert. Sie haben aber beide denselben Haken: Für lange Listen funktionieren sie nicht, weil sie einen Stack-Overflow verursachen. Das liegt an einigen Eigenheiten der JVM: Sie muss sich merken, wie es nach dem rekursiven Aufruf weitergeht (mit Cons) und verwendet dafür einen speziellen Speicherbereich, den Stack. Dieser Stack ist in seiner Größe beschränkt und viel kleiner als der Hauptspeicher, der eigentlich zur Verfügung stünde. Außerdem ist die Größe beim Start der JVM festgelegt und später nicht mehr zu ändern. Funktionale Sprachen mit eigener Runtime haben dieses Problem meist nicht, weil sie flexiblere Datenstrukturen anstatt eines Stacks mit fester Größe verwenden.

fun <A> List<A>.append(other: List<A>): List<A> {
  var list = this.reverse()
  var acc = other
  while (list is Cons) {
    acc = Cons(list.first, acc)
    list = list.rest
  }
  return acc
}

In Java wäre der einzige Weg, das Problem zu vermeiden, eine Schleife – und Schleifen benötigen Zuweisungen, um von einem Durchlauf zum nächsten zu kommunizieren. In Kotlin sieht das so aus wie in Listing 1. Die neue append-Funktion benutzt zwei Schleifenvariablen: die Eingabeliste und das Zwischenergebnis, den Akkumulator. Die Eingabeliste wird am Anfang umgedreht, weil Listen von hinten nach vorn konstruiert werden.

Das neue append funktioniert, ist aber aus funktionaler Sicht schlecht – insbesondere, weil while-Schleifen fehleranfällig sind. Werden zum Beispiel die beiden Zuweisungen an acc und list vertauscht, funktioniert die Methode nicht mehr. Diese Methode können wir auch ohne Zuweisungen mit einer Hilfsfunktion schreiben, die den Akkumulator als Parameter akzeptiert (Listing 2).

fun <A> List<A>.append(other: List<A>): List<A> =
  appendHelper(this.reverse(), other)

fun <A> appendHelper(list: List<A>, acc: List<A>): List<A> =
  when (list) {
    is Empty -> acc
    is Cons ->
      appendHelper(list.rest, Cons(list.first, acc))
  }

Diese Version kommt ohne Zuweisungen aus und benutzt einen rekursiven Aufruf statt der while-Schleife. (Die Hilfsfunktion könnten wir auch lokal innerhalb von append definieren – ein weiterer Punkt für FP-Support in Kotlin.) Anders als der rekursive Aufruf der ursprünglichen append-Version ist dieser hier ein sogenannter Tail Call – also ein Aufruf, der am Ende der Funktion steht.

 

Solche Tail Calls brauchen eigentlich keinen Platz auf dem Stack – entsprechend bestünde bei appendHelper auch keine Gefahr, dass der Stack überläuft. „Eigentlich“ deshalb, weil die JVM leider auch für Tail Calls völlig unnötig Stackplatz verbraucht. (Das ist ein seit mindestens 1996 bekannter Bug, und FP-Fans fiebern mit jedem neuen JVM-Release darauf, dass endlich ein Fix dabei ist.) Wir können aber den Kotlin-Compiler überreden, den rekursiven Tail Call intern in eine Schleife zu kompilieren, indem wir vor die Funktionsdefinition das Wort tailrec schreiben. Das geht leider nur bei statischen Selbstaufrufen (insbesondere also nicht bei Aufrufen von Methoden, die spät gebunden werden), aber immerhin, mehr ist auf der JVM auch nicht drin. Zwischenfazit: Funktionen sind in Kotlin tatsächlich so erstklassig, wie es auf der JVM geht. Die Richtung stimmt also auch hier.

Typen

Kommen wir nochmal zu den Typen: Wir haben ja schon gesehen, dass Kotlin mit Funktionstypen eine wesentliche Anforderung funktionaler Programmierung erfüllt. Leider hat das Typsystem aber auch einige Tücken. Um die zu verstehen, werfen wir einen Blick darauf, wie die Definition von Listen in der funktionalen Programmiersprache Haskell aussehen würde (wenn Listen da nicht schon eingebaut wären):

data List a = Empty | Cons a (List a)

Die Struktur ist ähnlich wie die Kotlin-Definition, aber zwei Unterschiede fallen auf: In Kotlin steht beim Typparameter A noch ein out davor. Außerdem steht beim Empty-Objekt Nothing als Typparameter von List.

Das kommt daher, dass Kotlin versucht, sowohl OO als auch FP zu unterstützen: OO braucht Klassenhierarchien, was auf der Typebene zu Subtyping führt. Deshalb ist es dort notwendig, dass bei Empty ein Typparameter für List angegeben wird – und da der Typparameter A für den Typ der Elemente der Liste steht, die leere Liste aber keine Elemente hat, müssen wir einen Typ angeben, der zu allen anderen passt. In Kotlin ist das eben Nothing, ein „magischer“ Typ ohne Elemente, der als Subtyp aller anderen Typen fungiert (das Gegenteil von Any sozusagen.) Außerdem ist out notwendig, damit eine Liste aus Elementen bestehen kann, die unterschiedliche Typen haben – solange sie einen Supertyp gemeinsam haben. Falls das kompliziert klingt: ist es. Wir können uns der Sache annähern, indem wir out entfernen. Sofort kommt eine Fehlermeldung, zum Beispiel bei:

val list1 = Cons(1, Cons(2, Cons(3, Empty)))

Abgekürzt lautet diese so: „Type mismatch: inferred type is Int but Nothing was expected“. Das liegt daran, dass Empty den Typ List<Empty> hat, wir aber für die Gesamtliste den Typ List<Int> brauchen. Das out sorgt jetzt dafür, dass List<Empty> ein Subtyp von List<Int> ist und damit alles wieder passt – ein Fall von sogenannter Kovarianz. (Wenn wir in geschrieben hätten, wäre Kontravarianz angesagt und es würde andere Fehlermeldungen geben.)

Das Wort out steht dafür, dass die Methoden von List andere Listen nur als „Output“ produzieren dürfen. Das bedeutet insbesondere, dass wir append nicht als „normale“ Methode innerhalb von List implementieren können, weil sie die zweite Liste als Eingabe akzeptiert. Diese Einschränkung gilt nicht für Extension Methods, weshalb wir append als eine solche definiert haben.

Die Situation ist aber immerhin besser als in Java, das erfordert, die Varianzannotationen an die Methoden zu schreiben. Die Java-Stream-Version von append, genannt concat (ebenfalls als statische Methode realisiert), hat zum Beispiel diese Signatur:

static <T> Stream<T> concat(Stream<? extends T> a,
                            Stream<? extends T> b)

Die Kombination von OO und FP führt also zu einigen Komplikationen, auch wenn Kotlin die Situation besser meistert als Java.

Higher-kinded Types

Kotlin hat sich sichtlich von Scala inspirieren lassen, was die FP-Unterstützung betrifft, aber auch auf einige Aspekte von Scala verzichtet – in der Hoffnung, die aus Scala bekannte Komplexität einzudämmen. Dem sind leider auch nützliche Features des Scala-Typsystems zum Opfer gefallen.

Eine wesentliche Einschränkung gegenüber Scala betrifft sogenannte Higher-kinded Types, die es in Scala gibt, in Kotlin leider nicht. Zur Erläuterung dieser Typen schauen wir uns eine Kotlin-Version des Optional-Typs aus Java an. Er ist nützlich, wenn eine Funktion manchmal ein Ergebnis liefert, manchmal aber auch nicht. Entsprechend gibt es zwei Fälle Some und None, ähnlich wie bei Listen:

sealed interface Option<out A>
object None : Option<Nothing>
data class Some<A>(val value: A) : Option<A>

Wir können für Option eine praktische map-Methode definieren:

fun <A, B> Option<A>.map(f: (A) -> B): Option<B> =
  when (this) {
    is None -> None
    is Some -> Some(f(this.value))
  }

Diese Verwandtschaft mit der map-Methode von List können wir sehen, wenn wir beide Signaturen untereinanderschreiben:

fun <A, B> List  <A>.map(f: (A) -> B): List  <B>
fun <A, B> Option<A>.map(f: (A) -> B): Option<B>

Typen wie List und Option, die eine map-Operation unterstützen, heißen in der funktionalen Programmierung Funktor und spielen dort eine wichtige Rolle. Deshalb liegt es nahe, ein Interface Functor zu definieren, in dem die map-Methode steht. Das könnte so aussehen:

interface Functor<F> {
  fun <A, B> map(f: (A) -> B): F<B>
}

Mit dieser Definition wäre Functor ein Higher-kinded Type: Ein Typkonstruktor (also ein Typ mit Generics), der einen anderen Typkonstruktor als Parameter akzeptiert. Das ist dann aber leider doch ein Schritt zu viel für Kotlin, der Compiler quittiert: „Type arguments are not allowed for type parameters“. Es gibt zwar einen Trick, um Higher-kinded Types zu simulieren, nachzulesen zum Beispiel in dem wunderbaren Buch „Functional Programming in Kotlin“ [3]. Er ist aber so umständlich, dass er in der Praxis kaum anwendbar ist.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Beim Typsystem ist in Kotlin also noch Luft nach oben. Dass es geht, zeigt Scala, das Higher-kinded Types unterstützt und für das es entsprechend Libraries mit höherstehenden Konzepten wie Functor gibt, die in Kotlin einfach nicht praktikabel sind. Fairerweise muss man aber zugegeben, dass Higher-kinded Types auch von manchen „richtigen“ funktionalen Sprachen wie F# nicht unterstützt werden.

Monaden

Wo funktionale Programmierung ist, sind Monaden oft nicht weit: Sie sind dort fürs Grobe zuständig und erlauben die Verknüpfung von Funktionen in sequenziellen Abläufen mit Effekten. Effekte sind Dinge, die neben der reinen Berechnung stattfinden – typischerweise Interaktionen mit der Außenwelt. Effekte können zum Beispiel benutzt werden, um in hexagonaler Architektur zwischen Ports und Adaptern zu vermitteln. Für eine komplette Einführung in Monaden fehlt uns hier der Platz, darum skizzieren wir nur, wie sie in Kotlin funktionieren.

Die Idee ist ganz einfach: Ein sequenzieller Prozess wird durch ein Objekt repräsentiert. Für die Objekte erstellen wir einen speziellen Typ (das ist die Monade), in dem genau die Operationen enthalten sind, die in dem Prozess zulässig sind. Zum Beispiel enthält die kleine Demo-Library kotlin-free-monads [4] ein Interface TtyM, um Prozesse abzubilden, die eine textuelle Ausgabe generiereren:

sealed interface TtyM<out A> {
  fun <B> bind(next: (A) -> TtyM<B>): TtyM<B>
}

Die bind-Operation (manchmal auch flatMap genannt) macht zusammen mit einer sogenannten pure-Operation TtyM zu einer Monade. Der Typparameter A ist für das Ergebnis des Prozesses zuständig. Wenn wir die TtyM-Monade benutzen, verwenden wir also nicht mehr println, sondern konstruieren stattdessen ein Write-Objekt, dessen Klasse TtyM implementiert.

Diese Vorgehensweise hat den Vorteil, dass wir das TtyM-Objekt in unterschiedlichen Kontexten ausführen können. Im normalen Betrieb macht das folgende Funktion, die für jedes Write einfach println ausführt:

fun <A> run(tty: TtyM<A>): A

Für automatisierte Tests wollen wir aber wissen, welcher Text ausgegeben wurde, und dafür gibt es eine Funktion, die die Zeilen des Outputs in eine Liste schreibt:

fun <A> output(tty: TtyM<A>, output: MutableList<String>): A

Das ist praktisch, wenn wir schon ein TtyM-Objekt haben. Aber dessen Konstruktion mit Konstruktor- und Methodenaufrufen ist normalerweise ziemlich umständlich, mit vielen geschachtelten Lambdaausdrücken und Klammern. Aus diesem Grund haben die meisten funktionalen Sprachen eine spezielle, deutlich kompaktere Syntax für monadische Programme. Kotlin bringt eine solche Syntax zwar nicht direkt mit, wir können sie aber mit Hilfe von suspend-Funktionen selbst bauen, sodass ein TtyM-Programm zum Beispiel so gebaut werden kann:

TtyDsl.effect {
  write("Mike")
  write("Sperber")
  pure(42)
}

Das Lambdaargument von effect ist eine suspend-Funktion. Das suspend sorgt dafür, dass der Compiler die Funktion in eine Coroutine transformiert. Die wiederum erlaubt der effect-Funktion, eine monadische Form der Funktion zu extrahieren. Wer sich für die Details interessiert, sei an das Kotlin-free-monads-Projekt [4] verwiesen.

Auch hier also ein Pluspunkt für Kotlin, zumindest gegenüber Java. Die Verwendung von suspend-Funktionen für monadische Syntax ist aber gegenüber funktionalen Sprachen deutlich aufwendiger und auch mit einigen Einschränkungen verbunden, sodass immer noch Luft nach oben bleibt.

Fazit

Mit zwei Fragen sind wir in diesen Artikel gestartet:

  1. Ist Kotlin funktional genug, dass es sich in lohnt, darin funktional zu programmieren?
  2. Ist Kotlin die richtige Sprache, wenn man auf der JVM funktional programmieren möchte?

Die Antwort auf die erste Frage ist ein entschiedenes Ja: Kotlin unterstützt die einfache Definition unveränderlicher Daten, kennt Typen für Funktionen, eliminiert die lästige Unterscheidung zwischen Referenztypen und Value Types, kompiliert rekursive Tail Calls korrekt, vereinfacht gegenüber Java den Umgang mit Generics und erlaubt die Definition monadischer Syntax. Das ist mehr als genug, um funktionale Programmierung gewinnbringend zu nutzen, mächtige Abstraktionen zu definieren und architektonisch von der Entkopplung durch Unveränderlichkeit zu profitieren. Wer mehr darüber wissen möchte, dem sei das oben erwähnte Buch [3] ans Herz gelegt. Eine grundsätzliche Einführung in funktionale Programmierung gibt es in meinem eigenen Buch „Schreibe Dein Programm!“ [5].

Die zweite Frage ist schwieriger zu beantworten: Auf der JVM gibt es zwei (weitere) funktionale Sprachen, nämlich Clojure und Scala. Beide sind großartig – während aber Clojure in vielerlei Hinsicht fundamental anders tickt als Kotlin und Java (kein Typsystem, Lisp-Syntax), ist Scala die Verlängerung der Linie von Java zu Kotlin.

 

Scala unterstützt funktionale Programmierung besser als Kotlin: Es gibt Higher-kinded Types, die Monaden funktionieren in Scala deutlich einfacher und außerdem erleichtern sogenannte Implicits es, automatisch die richtigen Implementierungen für Interfaces zu finden. Des Weiteren existiert um Scala herum ein umfangreiches Ökosystem funktionaler Libraries und Frameworks.

Die gegenüber Scala verringerte Komplexität von Kotlin fällt in der Praxis kaum ins Gewicht. Viele der typischen Schwierigkeiten in der täglichen Arbeit gibt es in Kotlin auch. Die IDE-Unterstützung ist für beide ähnlich gut. Wer also funktionale Programmierung mag und die Wahl hat, wird wahrscheinlich mit Scala glücklicher.


Links & Literatur

[1] Vavr, funktionale Library für Java: https://github.com/vavr-io/vavr

[2] Kotlin-Wrapper für Vavr: https://github.com/vavr-io/vavr-kotlin

[3] Vermeulen, Marco; Bjarnason, Rúnar; Chiusano, Paul: „Functional Programming in Kotlin“; Manning, 2021

[4] Monaden-Library für Kotlin: https://github.com/active-group/kotlin-free-monad

[5] Sperber, Michael; Klaeren, Herbert: „Schreibe Dein Programm!“; Tübingen University Press, 2023

The post Showdown der funktionalen Programmierung auf der JVM appeared first on JAX.

]]>
Java 21 und die Helden von heute https://jax.de/blog/java21-und-die-helden-von-heute/ Wed, 15 Nov 2023 09:43:45 +0000 https://jax.de/?p=89073 In Java 21 sind viele neue Features enthalten. Doch neben den hippen, großartigen Features für Parallelität und funktionaler Programmierung gibt es auch einige eher unscheinbare, die einen großen Einfluss haben werden: Wir nennen sie Hidden Heroes.

The post Java 21 und die Helden von heute appeared first on JAX.

]]>
In diesem Artikel werden „Generational ZGC“ (JEP 439), „Key Encapsulation Mechanism API“ (JEP 452) und „Code Snippets in Java Doc“ (JEP 413) vorgestellt. Beim ersten JEP handelt es sich um eine kleine Anpassung an den neuem Z Garbage Collector, der einen riesigen Performance Boost auf jeder Größe von Heap verspricht. Beim zweiten geht es um ein neues API mit dem Schlüssel für eine besonders sichere symmetrische Verschlüsselung, die mit Public/Private-Key-Methoden übertragen werden kann, und im letzten um die Einbindung von Codebeispielen in der Java-API-Dokumentation.

JEP 439: Generational ZGC

Eine Instanz auf dem Heap kann entfernt werden, wenn sie nicht mehr benötigt wird. In hardwarenahen Sprachen sind die Entwicklerinnen und Entwickler dafür zuständig, den korrekten, sicheren Zeitpunkt zu bestimmen. Der sichere Zeitpunkt ist der, wenn keine andere Instanz mehr eine Referenz auf die Instanz gespeichert hat. Durch Call-by-Reference-Aufrufe von Methoden können sich diese Referenzen sehr weit im System verbreiten und die Analyse wird komplex. Die manuelle Bestimmung des Zeitpunkts der sicheren Entfernung ist sehr fehleranfällig und deswegen setzt die JVM, wie viele moderne Ökosysteme, auf das Konzept des Garbage Collector, kurz GC. Ein Garbage Collector überwacht dabei automatisiert den Speicher und entfernt Instanzen nach dem sicheren Zeitpunkt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Hidden Heroes

Hidden Heroes leitet der Autor von dem Begriff „Hidden Champions“ ab, der von Hermann Simon geprägt wurde. Hidden Champions bezeichnen kleine Firmen, die Weltmarktführer sind und wenig Wahrnehmung in der Öffentlichkeit haben. Analog dazu sind Hidden Heroes Java-Features, die große Auswirkungen auf das JDK-Environment haben, aber gefühlt zu wenig Aufmerksamkeit in der Community bekommen.

In der Java Virtual Machine gibt es seit längerem mehrere verschieden implementierte Garbage Collectors mit diversen Schwerpunkten. Sie alle haben die GC-Pause gemein, in der die Anwendung pausiert werden muss:

  • Serial GC ist der einfachste GC. Er benutzt nur einen Thread und pausiert die Applikation, während er läuft. Er ist für Einzelkernsysteme und nicht für Hardware mit mehreren Kernen geeignet, da kein Nutzen aus mehreren Rechenkernen gezogen werden kann.
  • Parallel GC[1] verhält sich wie Serial GC, verwendet aber mehrere Kerne und ist damit eine bessere Alternative für Anwendungen auf Mehrkernhardware. Wie auch bei Serial GC ist die Länge der GC-Pause primär abhängig von der Größe des Heap-Speichers.
  • G1 GC heißt eigentlich Garbage First GC [2] und wendet Partitionen auf dem Heap an. Die Partitionen werden entsprechend dem freien Speicher aufsteigend priorisiert und analysiert. G1 GC analysiert und entfernt Instanzen, bis die konfigurierte fixe Länge der GC-Pause verstrichen ist. Es werden oft nicht alle Partitionen komplett bearbeitet, denn das Ziel von G1 ist eine möglichst feste Pausenzeit. Zudem eignet er sich gut für Maschinen mit vielen Prozessoren und großen Heaps.

Relativ neu ist der Z Garbage Collector [3], der in Java 15 eingeführt wurde, kurz ZGC genannt. Der ZGC führt die arbeitsintensive Analyse des Heap parallel zur Ausführung der Anwendung in eigenen Threads durch. Dadurch muss die Anwendung nur sehr kurz unterbrochen werden, um die Threads zu synchronisieren. Die GC-Pause ist dabei 1 ms lang und unabhängig von der analysierten Heapgröße. Realisiert wird die Parallelität mit Hilfe von Colored Pointer und Load Barriers. Colored Pointer sind eine Dekoration eines Pointers, einer Speicheradresse mit Metainformationen über den Status der Instanz. Load Barriers werden für Zugriffe auf Referenzen eingebaut. Sie werten Colored Pointer aus und führen potenziell notwendige Weiterleitungen durch, bevor die Anwendung auf die Instanz zugreift, um Adressänderungen zu verschleiern. Der hochoptimierte Algorithmus von ZGC wird im Artikel „Deep-dive of ZGC’s Architecture“ [4] auf Dev.java ausführlich beschrieben und ist in der Lage, sehr kleine und sehr große Heaps bis 16 TB effizient zu bearbeiten. Aktiviert werden kann ZGC durch den Kommandozeilenparameter -XX:+UseZGC. Bei der ersten Verwendung von ZGC wird empfohlen, auch GC-Logging (-Xlog:gc) zu aktivieren, um das Finetuning der Konfiguration zu ermöglichen. Neben der Anzahl der von ZGC verwendeten Threads (-XX:ConcGCThreads) und einigen Linux-spezifischen Parametern kann auch das Zurückgeben von Speicher an das Betriebssystem aktiviert werden (-XX:+ZUncommit).

Mit JEP 439: Generational ZGC [5] wird ZGC um die Partitionierung des Heap in Generationen, also je einen Bereich für „jung“ und „alt“, erweitert. Der Bereich für die jungen Instanzen unterteilt sich noch in „Eden“ und „Survivor“. Neu erzeugte Instanzen werden in der Regel in Eden erzeugt und, wenn sie den ersten Durchlauf des GC „überleben“, nach Survivor kopiert. Nachdem sie eine feste Anzahl GC-Läufe in Survivor „überlebt“ haben, werden Instanzen in den Bereich für alte Instanzen kopiert. Die Partitionierung ermöglicht die Anwendung der Weak Generational Hypothesis [6], die im Kern aussagt: „Junge Instanzen haben die Tendenz, jung zu sterben“. Als Konsequenz kann unter Instanzen im Bereich „jung“ und besonders unter denen im Bereich Eden vermutet werden, dass sie nicht mehr referenziert werden und zu entfernen sind. Im Bereich „alt“ sind wahrscheinlich kaum zu entfernende Instanzen enthalten und diese werden nicht so häufig analysiert. Durch die getrennte Behandlung verringert sich der durchschnittliche Aufwand der Analyse und der GC-Prozess wird effizienter. Aktiviert werden kann diese Partitionierung mit dem Parameter -XX:+ZGenerational. Zukünftig soll ZGC nur noch den Ansatz mit Generationen verwenden, dann ist der Parameter nicht mehr notwendig.

Mit seiner effizienten Behandlung von riesigen Heaps, der geringen GC-Pause und dem Fokus auf das Entfernen von jungen Instanzen ist Genrational ZGC optimiert für datenintensive Anwendungen, die eine kurze Antwortzeit erfordern. Damit kann Generational ZGC eine gute Wahl für moderne datengetriebene Systeme im Enterprise-Umfeld sein. Durch die hohe Komplexität und viel notwendiger Theorie in diesem Themenfeld fehlt es Generational ZGC ein wenig an Aufmerksamkeit, deswegen ist Generational ZGC ein Hidden Hero des Java-Ökosystems.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

JEP 452: Key Encapsulation Mechanism API

In der elektronischen Kommunikation wird an vielen Stellen auf Verschlüsselung gesetzt. Viele der heute gängigen Verfahren gelten als nicht sicher für ein Zeitalter mit Quantencomputern. An einem solchen Quantencomputer könnte zum Beispiel Mallroy forschen – und vor kurzen hatte er einen großen Durchbruch. Alice und Bob wollen deswegen eine Überraschungsparty für ihren Freund Mallroy organisieren. Damit Mallroy unwissend bleibt, wollen Alice und Bob nur über sichere, verschlüsselte Kanäle miteinander kommunizieren. Sie wollen zahlreiche Nachrichten austauschen und Mallroy könnte schon den Quantencomputer zur Verfügung haben, deswegen kommt nur ein effizientes und hochsicheres Verfahren in Frage.

Bei ihrer Recherche erfahren sie, dass es symmetrische und asymmetrische Verfahren gibt. Bei einem asymmetrischen Verschlüsselungsverfahren werden zwei verschiedene Schlüssel zum Ver- und Entschlüsseln verwendet. Der Schlüssel zum Verschlüsseln kann gefahrenlos übermittelt werden, leider sind diese Algorithmen in der Regel nicht hochperformant und auch nicht besonders sicher. Bei symmetrischen Verfahren wird derselbe Schlüssel zum Ver- und Entschlüsseln verwendet, sie punkten bei Effizienz und Sicherheit ganz klar. Leider benötigen beide Seiten das Wissen über den verwendeten Schlüssel und dieser kann, da geheim, nicht trivial versendet werden.

Im 2001 von Cramer und Shoup veröffentlichten Artikel „Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack“ [7] wird in § 7.1 der Mechanismus „Key Encapsulation“ beschrieben, mit dem es möglich ist, Schlüssel für symmetrische Verschlüsselungsverfahren unter Zuhilfenahme asymmetrischer Verschlüsselung sicher zu übertragen. Dieses unter der Abkürzung KEM bekannte Verfahren wird unter anderem vom BSI und der NIST in ihren Post-Quanten-Kryptografie-Konzepten als Basisbaustein betrachtet [8], [9]. In Abbildung 1 ist der KEM-Ablauf zwischen Alice und Bob dargestellt. Zuerst generiert Alice ein Schlüsselpaar aus öffentlichem und privatem Schlüssel. Der öffentliche Schlüssel wird an Bob übertragen. An dieser Stelle ist eine Transportverschlüsselung zwischen Alice und Bob wichtig, aber nicht Gegenstand des Verfahrens. Bob erzeugt einen zufälligen Schlüssel für das später verwendete symmetrische Verschlüsselungsverfahren und verschlüsselt diesen mit dem öffentlichen Schlüssel von Alice. Dieses als „Encupsulated“ bekannte Datenpaket wird an Alice zurückübertragen, und nur sie kann es mit ihrem privaten Schlüssel entschlüsseln. Von nun an können Alice und Bob ihre Nachrichten mit einer symmetrischen Verschlüsselung austauschen, das Schlüsselpaar wird nicht mehr gebraucht.

Abb. 1: Bob und Alice tauschen den Schlüssel für eine symmetrische Verschlüsselung (gelb) mit KEM aus

Da die Implementierung sicherheitsrelevanter Features und insbesondere von Verschlüsselungen ein hochkomplexes Feld ist, gleichzeitig aber die Verschlüsselung eine grundlegende Anforderung moderner Systeme ist, wird mit JEP 452 [10] ein API zur Durchführung eines KEM-Prozesses in Java 21 eingeführt. Das API folgt dem von der ISO 18033-2 definierten und oben beschriebenen Prozess. Dabei bilden drei Bausteine das Fundament für eine sichere Verschlüsselung. Der javax.crypto.KEM.Encapsulator verwendet den öffentlichen Schlüssel und den erzeugten symmetrischen Schlüssel, um ein Encapsulated zu erzeugen. Das Encapsulated wird durch eine Instance von javax.crypto.KEM.Encapsulated repräsentiert und kann direkt als Bytearray von Bob übertragen werden. Auf Alices Seite wird die Klasse javax.crypto.KEM.Decapsulator verwendet, um mit dem passenden privaten Schlüssel den symmetrischen Schlüssel zu erlangen.

Im nun folgenden Beispiel bilden die Methoden sendToBob/sendToAlice und retrieveFromBob/ retrieveFromAlice die jeweilig sendenden beziehungsweise empfangenden Enden der unsicheren Kommunikationskanäle zwischen Alice und Bob ab. Listing 1 zeigt, wie das Set-up des KEM-Prozesses bei Alice mit dem KeyPairGenerator implementiert wird. Die Methode KeyPairGenerator#getInstance(String) erzeugt einen Schlüsselpaarerzeuger für das RSA-Verfahren. Über ein Service Provider Interface ist es möglich, hier weitere Algorithmen zu hinterlegen. Mit dem Aufruf von KeyPairGenerator#generateKeyPair() wird ein Paar aus öffentlichem und privatem Schlüssel erzeugt. Dieses Paar wird bei Alice hinterlegt und der öffentliche Schlüssel an Bob versandt.

var keyGen = KeyPairGenerator.getInstance("RSA");
var pair = keyGen.generateKeyPair();

sendToBob(pair.getPublic());

In Listing 2 wird die erste Phase der Key Exchanges auf der Seite von Bob gezeigt. Nachdem der öffentliche Schlüssel von Alice empfangen wurde, wird mir KEM#getInstance(String) eine für RSA konfigurierte Implementierung gewählt. Neue Implementierungen können auch hier durch ein SPI bereitgestellt werden. Auf der konkreten KEM-RSA-Instanz wird mit der Methode KEM#newEncapsulator(java.security.PublicKey) eine Encapsulator-Instanz für RSA-KEM erzeugt. Der zufällig erzeugte Schlüssel kann und sollte über Encapsulated#key() abgerufen und bei Bob hinterlegt werden (hier in der Variable secret). Mit diesem Schlüssel wird die spätere symmetrische Verschlüsselung durchgeführt. Auf das zu versendende Encapsulate kann über die Methode Encapsulated#encapsulate() zugegriffen werden und es wird von Bob an Alice versendet.

var publicKey = retrieveFromAlice();

var encapsulator = KEM.getInstance("RSA-KEM").newEncapsulator(publicKey);
var secret = encapsulate.key();
var encapsulate = encapsulator.encapsulate();

sendToAlice(encapsulate.encapsulation());

In Listing 3 wird die zweite Phase der Key Encapsulation bei Alice gezeigt. Nachdem das Encapsulate von Bob empfangen und der private Schlüssel aus dem Schlüsselpaar des Set-ups entnommen wurde, kann mit dem Entpacken des symmetrischen Schlüssels begonnen werden. Zuerst wird wieder mit der Methode KEM#getInstance(String) die konkrete Implementierung von KEM für RSA-KEM geladen. Mit dem Aufruf KEM# newDecapsulator(PrivateKey) wird unter Zuhilfenahme des privaten Schlüssels der symmetrische Schlüssel entpackt.

byte[] encapsulate = retrieveFromBob();
var privateKey = pair.getPrivate();

var decapsulator = KEM.getInstance("RSA-KEM").newDecapsulator(privateKey);
var secret = decapsulator.decapsulate(encapsulate);

Von diesem Moment an ist das verwendete öffentliche und private Schlüsselpaar nicht mehr notwendig und kann gelöscht werden. Die Kommunikation kann nun sicher und effizient mit einem symmetrischen Verschlüsselungsverfahren durchgeführt werden. Mit diesem sehr neuen Feature in Java 21 rüstet die OpenJDK-Community das Ökosystem Java für das Post-Quantum-Zeitalter auf. Die Verwendung von Java in einer Welt voller Quantencomputer ist nun möglich und aus diesem Grund sollte dieses Feature mehr Aufmerksamkeit bekommen. Es ist auf jeden Fall ein Hidden Hero von Java 21.

JEP 413: Code Snippets in Java API Documentation

An Schnittstellen stellen Entwickler und Entwicklerinnen ganz eigene Anforderungen. Oft sind Benutzbarkeit und verständliche Dokumentation die wichtigsten Aspekte. Aus diesem Grund ist Javadoc als Dokumentationswerkzeug für Source Code bereits seit Java 1 Teil des JDK. Mit Javadoc können Kommentare mit beschreibenden Tags angereichert werden und in eine navigierbare und durchsuchbare Schnittstellendokumentation überführt werden. Neben Beschreibungen der Parameter und des Verhaltens ist es sinnvoll, die beabsichtigte Verwendung zu dokumentieren, um die Hürden der Einarbeitung zu verringern. Es haben sich einige unterschiedliche Ansätze etabliert:

  1. Separate Tutorials wie die Referenzdokumentation von Spring [11] oder Extensions Guides von Quarkus [12]. Hier liegt der Fokus eher auf der Verwendung des Frameworks und weniger auf dem API.
  2. Bei Code Snippets als HTML mit <pre>{@code } </pre> liegt das API im Fokus. Das Snippet wird leider nicht ansehnlich formatiert und muss bei jeder Änderung an der Schnittstelle bedacht werden. Ein Beispiel ist java.util.stream.Stream [13], [14].
  3. Unit Tests als Dokumentation zu betrachten, ist ein guter Ansatz, vorausgesetzt die Tests sind gut und die verwendenden Entwickler und Entwicklerinnen haben Zugriff auf den Code. Bedauerlicherweise fehlt bei diesem Ansatz oft die Verknüpfung zwischen Code und Tests.

Mit JEP 413: Code Snippets in der Java-API-Dokumentation [15] hält Java seit Version 18 eine Möglichkeit bereit, Source-Code-Auszüge mit Syntax-Highlighting und Testbarkeit zu vereinen, um Entwicklerinnen und Entwicklern das Einbinden von guten Beispielen in der API-Dokumentation zu ermöglichen. Um ein Snippet in einem Javadoc-Kommentar einzubinden, wird der neue {@snippet: …  }-Tag verwendet.

/**
 * Berechnung der MwSt für einen Privatkunden beim Einkauf in Höhe von 1055
 * {@snippet :
 *   var kunde = new Privatkunde("Merlin", "");
 *   var wert = 1055d;
 *   // ...
 *   var mwst = MwStRechner.PlainOOP.calculateMwSt(kunde, wert);
 * }
 */

In Listing 4 wird in einem Inline Snippet die erwartete Interaktion mit dem API des MwStRechner aus dem Data-Oriented-Programming-Beispielprojekt des Autors [16] dargestellt. In der generierten Dokumentation ist der Bereich ab der neuen Zeile hinter dem Doppelpunkt bis zur letzten Zeile vor der schließenden Klammer als formatierter Source Code mit Syntax-Highlighting enthalten. Bei Inline Snippets gibt es zwei Randbedingungen:

  • Ein mehrzeiliger Kommentar mit /* */ ist nicht erlaubt.
  • Für jede geöffnete Klammer muss auch eine schließende enthalten sein.

Ohne diese Randbedingungen ist es dem Generator nicht möglich, die Passage korrekt zu konvertieren. Zusätzlich muss die syntaktische Korrektheit manuell geprüft und Schnittstellenänderungen beachtet werden. Für ein externes Snippet gelten diese Rahmenbedingungen allesamt nicht. Bei einem externen Snippet wird der Inhalt nicht im selben Kommentar angegeben, sondern aus einer vorhandenen Java-Datei entnommen.

/**
 * Berechnung der MwSt für einen Privatkunden beim Einkauf in Höhe von 1055
 * {@snippet file="SwitchExpressionsSnippets.java" region="example"}
 */

// Datei: snippet-files/Snippets.java
class Snippets {
  public void snippet01() {
    void snippet01() {
    // @start region="example"
    // @replace region="replace" regex='(= new.*;)|(= [0-9]*d;)' replacement="= ..."
    // @highlight region="highlight" regex="\bMwStRechner\b"
    var kunde = new Privatkunde("Merlin", "[email protected]"); // @replace regex="var" replacement="Kunde"
    var wert = 1055d; // @replace regex="var" replacement="double"
    /* .. */
    var mwst = MwStRechner.PlainOOP.calculateMwSt(kunde, wert); // @link substring="PlainOOP.calculateMwSt" target="MwStRechner.PlainOOP#calculateMwSt"
    // @end @end @end
  }
}

In Listing 5 wird ein externes Snippet verwendet, das auf eine Datei Snippets.java (auch Listing 5) im Ordner snippet-files verweist. Dieser Ordner liegt direkt neben der Datei, in der das Snippet eingebunden wird und kann mit Hilfe der Konfiguration -snippet-path überschrieben werden. Den Pfad auf den Ordner mit dem Test zu konfigurieren, erscheint dem Autor als guter Standard. Dadurch ist es möglich, die geschriebenen Tests als Beispiele wiederzuverwenden. In Listing 2 ist zudem eine Region definiert. Dadurch werden nur bestimmte Bereiche der referenzierten Java-Datei verwendet und die Datei kann so Beispiele für verschiedene Anwendungsfälle enthalten.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Neben Bereichen sind in dem Snippet in Listing 5 noch weitere Anpassungen durchgeführt worden. Mit den @replace Tags werden per regulärem Ausdruck zuerst alle Initialisierungen durch ersetzt, da sie nicht direkt zum Beispiel beitragen. Das Schlüsselwort var wird in den entsprechenden Zeilen durch den Datentyp ersetzt. Hierbei wird keine Region angeben, also die Ersetzung nur auf diese Zeile angewandt. Mit dem Tag @highlight wird jedes Vorkommen von MwStRechner hervorgehoben und mit @link ein Link zur Methode MwStRechner.PlainOOP#calculateMwSt erstellt. In Abbildung 2 wird das Ergebnis der Javadoc-Generierung des Listings 5 gezeigt.

Abb. 2: Aus Listing 5 generierte Dokumentation mit Ersetzungen, Links und Highlights

Durch die Möglichkeit zur Verknüpfung zwischen Source Code und API-Dokumentation kann gewährleistet werden, dass auch Listings in Javadoc aktuell und von hoher Qualität sind. Die Dokumentation von APIs gewinnt dadurch an Qualität, Aktualität und einem hohen Grad an Verständlichkeit. Werden alle Tags in einem Snippet verwendet, wird das Lesen des zugrundeliegenden Codes erschwert. Das betrifft vor allem die Entwicklerinnen und Entwickler von Frameworks und Tools. Von ihrem Mehraufwand werden aber alle profitieren, deswegen sind Code Snippets in Java API Documentation auf jeden Fall ein Hidden Hero im Java-21-Ökosystem.

Zusammenfassung

In diesem Artikel wurden drei der vielen Hidden Heroes im JDK-Ökosystem abseits von Virtual Threads und Pattern Matching gezeigt. Es gibt noch einige mehr, wie beispielsweise Class Data Sharing und den Simple Web Server, aber das ist Material für einen weiteren Beitrag auf Konferenzen, User Groups oder für das Selbststudium.


Links & Literatur

[1] https://docs.oracle.com/en/java/javase/20/gctuning/parallel-collector1.html

[2] https://docs.oracle.com/en/java/javase/20/gctuning/garbage-first-garbage-collector-tuning.html

[3] https://docs.oracle.com/en/java/javase/20/gctuning/z-garbage-collector.html

[4] https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/

[5] https://openjdk.org/jeps/439

[6] https://docs.oracle.com/en/java/javase/17/gctuning/garbage-collector-implementation.html

[7] Design and Analysis of Practical Public-Key Encryption Schemes Secure against Adaptive Chosen Ciphertext Attack, Crammer und Shoup 2001, https://eprint.iacr.org/2001/108.pdf

[8] https://www.bsi.bund.de/DE/Themen/Unternehmen-und-Organisationen/Informationen-und-Empfehlungen/Quantentechnologien-und-Post-Quanten-Kryptografie/Post-Quanten-Kryptografie/post-quanten-kryptografie_node.html

[9] https://csrc.nist.gov/News/2022/pqc-candidates-to-be-standardized-and-round-4

[10] https://openjdk.org/jeps/452

[11] https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

[12] https://quarkus.io/guides/resteasy-reactive

[13] https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html

[14] https://github.com/openjdk/jdk/blob/4de3a6be9e60b9676f2199cd18eadb54a9d6e3fe/src/java.base/share/classes/java/util/stream/Stream.java#L52-L57

[15] https://openjdk.org/jeps/413

[16] https://github.com/MBoegers/DataOrientedJava

The post Java 21 und die Helden von heute appeared first on JAX.

]]>
Neues in Java 21: JEP 430 und 431 https://jax.de/blog/neues-in-java-21-jep-430-und-431/ Wed, 04 Oct 2023 11:55:16 +0000 https://jax.de/?p=89017 Java 21 bringt viele Verbesserungen in Syntax und APIs. In diesem Beitrag stehen die beiden Neuerungen JEP 430: „String Templates (Preview)“ sowie JEP 431: „Sequenced Collections“ im Fokus. Durch diese Funktionalitäten wird Java komfortabler und angenehmer in der Handhabung und besser für die Zukunft gerüstet. String Templates adressieren das Aufbereiten textueller Informationen, die aus fixen und variablen Bestandteilen bestehen, und Sequenced Collections vereinfachen den Zugriff und die Verwaltung von Daten am Anfang und Ende einer Collection.

The post Neues in Java 21: JEP 430 und 431 appeared first on JAX.

]]>
Regelmäßig müssen Strings aus fixen und variablen Textbausteinen zusammengesetzt werden. Um das zu tun, gibt es verschiedene Varianten, angefangen bei der einfachen Konkatenation mit + bis hin zur formatierten Aufbereitung. Die mit Java 21 neu eingeführten String Templates (JEP 430) ergänzen die bisherigen Varianten um eine elegante Möglichkeit, Ausdrücke angeben zu können, die zur Laufzeit ausgewertet und passend in den String integriert werden. Allerdings sind die String Templates noch ein Preview-Feature und müssen beim Kompilieren und Ausführen explizit aktiviert werden.

Bisherige Vorgehensweise

Schauen wir kurz zurück. Um formatierte Ausgaben in Java zu erzeugen, verwenden viele Entwickler die folgenden Varianten, um aus Variablen und Textbausteinen zu kombinieren:

String result = "Calculation: " + x + " plus " + y + " equals " + (x + y);
System.out.println(result);

und

String resultSB = new StringBuilder().append("Calculation: ").append(x).append(" plus ").append(y).append(" equals ").append(x + y).toString();
System.out.println(resultSB);

Die Stringkonkatenation mit + ist leicht verständlich und oft auch durchaus lesbar. Die Lesbarkeit nimmt jedoch bei einer zunehmenden Anzahl von Verknüpfungen ab. Noch dramatischer ist der Effekt beim Einsatz eines StringBuffers oder StringBuilders und dessen Methode append(): Je mehr Elemente zu verknüpfen sind, desto unleserlicher und schwieriger nachvollziehbar wird das Konstrukt.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Weitere, in der Praxis seltener anzutreffende Möglichkeiten sind die Methoden format() und formatted() aus der Klasse String. Beide nutzen die gleichen Platzhalter, variieren aber ein wenig in der Handhabung, wobei meine Präferenz bei formatted() liegt.

var resultF1 = String.format("Calculation: %d plus %d equals %d", x, y, x + y);
System.out.println(resultF1);

var resultF2 = "Calculation: %d plus %d equals %d".formatted(x, y, x + y);
System.out.println(resultF2);

Darüber hinaus existiert noch die Klasse java.text.MessageFormat. Von ihr werden zunächst per Konstruktoraufruf mit fixem Text sowie integrierten Platzhaltern Instanzen erzeugt und danach mit format() befüllt – sowohl die Platzhalter als auch die Angabe der Werte unterscheiden sich von den beiden vorherigen Varianten:

var msgFormat = new MessageFormat("Calculation: {0} plus {1} equals {2}");
System.out.println(msgFormat.format(new Object[] { x, y, x + y }));

Möglichkeiten anderer Programmiersprachen

Viele Programmiersprachen unterstützten alternativ zur Stringkonkatenation die sogenannte Stringinterpolation oder auch formatierte Strings. Dabei kommt ein String zum Einsatz, in den an verschiedenen Positionen speziell gekennzeichnete Platzhalter integriert sind, die dann zur Laufzeit durch die entsprechenden Werte ersetzt werden. Das gilt beispielsweise für die f-Strings in Python. Etwas Ähnliches bieten Kotlin, Swift und C# durch unterschiedliche Notationen mit Platzhaltern:

  • Python: “Calculation: {x} + {y} = {x + y}”
  • Kotlin: “Calculation: $x + $y = ${x + y}”
  • Swift: “Calculation: \(x) + \(y) = \(x + y)”
  • C#: $”Calculation: {x} + {y}= {x + y}”

In allen Fällen werden die Platzhalter im String durch die entsprechenden Werte der Variablen, insbesondere auch von Berechnungen, ersetzt.

Die Alternative: String Templates (Preview)

Mit JEP 430 werden String Templates eingeführt, die wir uns nun anschauen wollen. Bilden wir das einführende Beispiel damit nach – aber bitte bedenken Sie, dass es sich um ein Preview-Feature handelt, das extra aktiviert werden muss, etwa wie folgt für die JShell:

$ jshell --enable-preview
|  Willkommen bei JShell - Version 21
|  Geben Sie für eine Einführung Folgendes ein: /help intro

Lernen wir zunächst die grundlegende Syntax kennen. Dabei muss dem String Template das Kürzel STR vorangestellt werden. Es aktiviert einen sogenannten Template Processor, der dann die Platzhalter im Format \{expression} ersetzt, im einfachsten Fall einfache Variablennamen durch deren Wert (Listing 1).

jshell> int x = 47
x ==> 47

jshell> int y = 11
y ==> 11

jshell> System.out.println(STR."Calculation: \{x} + \{y} = \{x + y}")
Calculation: 47 + 11 = 58

Bei der Angabe der Ausdrücke ist man nicht auf einfache mathematische Operationen beschränkt, sondern es lassen sich beliebige Java-Aufrufe einfügen, also auch Methoden (Listing 2).

jshell> int x = 7
x ==> 7

jshell> int y = 2
y ==> 2

jshell> STR."Berechnung: \{x} mal \{y} = \{Math.multiplyExact(x, y)}"
$3 ==> "Berechnung: 7 mal 2 = 14"

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Praxistipp

Bei der Einführung gab es diverse Diskussionen über das Format der Ausdrücke. Weil viele Third-Party-Bibliotheken etwa $, # oder {} dafür nutzen, hat man sich für ein Format entschieden, das nur innerhalb von String Templates gültig ist. Interessanterweise ist das STR per Definition public static final und automatisch in jeder Java-Datei bereits vorhanden.

Praktischerweise kann man String Templates auch in Kombination mit Textblöcken nutzen (Listing 3).

jshell> int statusCode = 201
statusCode ==> 201

jshell> var msg = "CREATED"
msg ==> "CREATED"

jshell> String json = STR."""
  ...>     {
  ...>       "statusCode": \{statusCode},
  ...>       "msg": "\{msg}"
  ...>     }""";
  ...> 

json ==> "{\n  \"statusCode\": 201,\n  \"msg\": \"CREATED\"\n}"

Tatsächlich gefällt mir in dem Zusammenhang die Methode formatted() ziemlich gut, und ich würde sie String Templates sogar vorziehen:

jshell> String json = STR."""
  ...> {
  ...>     "statusCode": %d,
  ...>     "msg": "%s"
  ...> }""".formatted(statusCode, msg);
json ==> "{\n    \"statusCode\": 201,\n    \"msg\": \"CREATED\"\n}"

String Templates lassen sich auch zur Aufbereitung einer simplen HTML-Seite nutzen (Listing 4). Nach dem Ersetzen entsteht das HTML in Listing 5.

String title = "My First Web Page";
String text = "My Hobbies:";
var hobbies = List.of("Cycling", "Hiking", "Shopping");
String html = STR."""
<html>
  <head><title>\{title}</title></head>
  <body>
    <p>\{text}</p>
    <ul>
      <li>\{hobbies.get(0)}</li>
      <li>\{hobbies.get(1)}</li>
      <li>\{hobbies.get(2)}</li>
    </ul>
  </body>
</html>""";
<html>
  <head><title>My First Web Page</title></head>
  <body>
    <p>My Hobbies:</p>
    <ul>
      <li>Cycling</li>
      <li>Hiking</li>
      <li>Shopping</li>
    </ul>
  </body>
</html>

Speichert man das Ganze als Datei und öffnet diese in einem Browser, so erhält man in etwa die Darstellung in Abbildung 1.

Abb. 1: Simple HTML-Seite mit String Templates

Besonderheiten

Interessanterweise ist es möglich, auch komplexere Aktionen in einem Platzhalter auszuführen. Als eher einfaches Beispiel haben wir bereits einen Methodenaufruf gesehen. Es sind aber auch Aktionen wie die in Listing 6 möglich, also ein Postinkrement, der ?-Operator oder Zugriffe auf das Date and Time API – in diesem Muster lassen sich dann einfache Anführungszeichen ohne Escaping nutzen.

int index = 0;
var modified = STR."\{index++}, \{index++}, \{index++}, \{index++}";
System.out.println(modified);

String filePath = "tmp.dat";
File file = new File(filePath);
  String old = "The file " + filePath + " " + file.exists() ? "does" : "does not" + " exist");
 String msg = STR. "."The file \{filePath} \{file.exists() ? 
                  "does" : "does not"} exist";

String currentTime = STR."Current time: \{DateTimeFormatter.ofPattern("HH:mm").format(LocalTime.now())}";
System.out.println(currentTime);

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Neben STR existieren weitere vordefinierte Prozessoren, etwa FMT, um eine Ausgabe wie mit String.format() zu erzielen. Betrachten wir dazu die Methode in Listing 7.

private static void alternativeStringProcessors() {
  int x = 47;
  int y = 11;
  String calculation1 = FMT."%6d\{x} + %6d\{y} = %6d\{x + y}";
  System.out.println("fmt calculation 1: " +calculation1);

  float base = 3.0f;
  float addon = 0.1415f;

  String calculation2 = FMT."%2.4f\{base} + %2.4f\{addon}" +
                        FMT."= %2.4f\{base + addon}";
  System.out.println("fmt calculation 2 " + calculation2);

    String calculation3 = FMT."Math.PI * 1.000 = %4.6f\{Math.PI * 1000}";
    System.out.println("fmt calculation 3 " + calculation3);
}

Als Ausgabe erhält man:

fmt calculation 1:     47  +     11 =     58
fmt calculation 2: 3.0000  + 0.1415 = 3.1415
fmt calculation 3: Math.PI * 1.000 = 3141.592654

Während STR standardmäßig verfügbar ist, muss man FMT auf geeignete Weise importieren:

import static java.util.FormatProcessor.FMT;

Eigene Stringprozessoren

Die Arbeitsweise von STR und FMT ist eingängig. Möchte man selbst steuernd eingreifen, ist es sogar möglich, eigene Template Processors zu erstellen, indem man das Interface StringTemplate.Processor implementiert. Dabei existieren diverse Methoden, die man für eine angepasste Variante nutzen kann. Folgendes Beispiel zeigt sowohl STR als auch die eigene Ausprägung in Form der Klasse MyProcessor im Einsatz:

String name = "Michael";
int age = 52;
System.out.println(STR."Hello \{name}. You are \{age} years old.");

var myProc = new MyProcessor();
System.out.println(myProc."Hello \{name}. You are \{age} years old.");

Betrachten wir die Ausgaben, die auch verschiedene Resultate der internen Methoden zeigen, um das Verständnis dafür zu erhöhen, wie die Abläufe und Verarbeitungsschritte innerhalb eines Template Processor erfolgen. Ziel des eigenen Prozessors ist es, die Werte jeweils speziell mit >> und << zu markieren:

Hello Michael. You are 52 years old.
-- process() --
=> fragments: [Hello , . You are ,  years old.]
=> values: [Michael, 52]
=> interpolate: Hello Michael. You are 52 years old.
Hello >>Michael<<. You are >>52<< years old.

Tatsächlich ist gar nicht viel Arbeit in der Methode process() zu erledigen, insbesondere würde man die Ausgaben weglassen, hier dienen sie lediglich dem Verständnis und der leichteren Nachvollziehbarkeit (Listing 8).

static class MyProcessor implements StringTemplate.Processor<String,
                                    IllegalArgumentException> {
    @Override
    public String process(StringTemplate stringTemplate) 
                  throws IllegalArgumentException {
        System.out.println("\n-- process() --");
        System.out.println("=> fragments: " + stringTemplate.fragments());
        System.out.println("=> values: " + stringTemplate.values());
        System.out.println("=> interpolate: " + stringTemplate.interpolate());
        System.out.println();

        var adjustedValues = stringTemplate.values().stream().
                                            map(str -> ">>" + str + "<<").
                                            toList();

        return StringTemplate.interpolate(stringTemplate.fragments(),
                                          adjustedValues);
    }
}

JEP 431: Sequenced Collections

Das Java Collections API ist eins der ältesten und am besten konzipierten APIs im JDK und bietet die drei Haupttypen List, Set und Map. Was allerdings fehlt, ist so etwas wie eine geordnete Reihenfolge von Elementen in Form eines Typs. Einige Collections haben eine sogenannte Encounter Order/Iteration Order (Begegnungsreihenfolge), d. h., es ist definiert, in welcher Reihenfolge die Elemente durchlaufen werden, etwa

  • List: indexbasiert, von vorne nach hinten
  • TreeSet: indirekt durch Comparable oder übergebenen Comparator definiert
  • LinkedHashSet: gemäß der Einfügereihenfolge

Dagegen definiert etwa HashSet keine derartige Encounter Order – und auch HashMap tut das nicht.

 

Auf Basis der Encounter Order ist auch das erste bzw. letzte Element definiert. Zudem lässt sich so eine Append-Funktionalität für vorn und hinten realisieren. Darüber hinaus ergibt sich auch die umgekehrte Reihenfolge. All das wird von Sequenced Collections adressiert.

Einführende Beispiele

Bis Java 21 war es mühsam, auf das letzte Element einer Collection zuzugreifen. Schlimmer noch – es existieren diverse, jeweils vom Typ der Collection abhängige unterschiedliche Wege:

var lastListElement = list.get(list.size() - 1);
var lastSortedElement = sortedSet.last();
var lastDequeElement = deque.getLast();

Fast ebenso unhandlich waren mitunter Zugriffe auf das erste Element, insbesondere für Sets:

var firstElement = list.get(0);
var firstUnordered = hashSet.iterator().next();
var firstOrdered = treeSet.iterator().next();
var firstLhs = linkedHashSet.iterator().next();

Sich all diese Besonderheiten zu merken, ist mühsam und fehleranfällig. Selbst mit IDE-Unterstützung bleibt noch die mangelnde Eleganz. Schauen wir uns nun Sequenced Collections an und wie diese nicht nur diese Aufgabenstellung, sondern auch das Anfügen und Löschen von Elementen vereinfachen.

Abhilfe: Sequenced Collections

Bislang bietet Java keinen Typ, der eine geordnete Folge von Elementen repräsentiert. Wie angedeutet, füllt Java 21 diese Lücke durch die Sequenced Collections, genauer die Einführung der Interfaces SequencedCollection, SequencedSet und SequencedMap. Sie bieten Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende der Collection sowie zur Bereitstellung einer Collection in umgekehrter Reihenfolge.

Das Interface SequencedCollection

Betrachten wir das Interface SequencedCollection (Listing 9).

interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();
  void addFirst(E);
  void addLast(E);
  E getFirst();
  E getLast();
  E removeFirst();
  E removeLast();
}

Es ist leicht ersichtlich, dass SequencedCollection das Interface Collection erweitert und Methoden zum Hinzufügen, Ändern oder Löschen von Elementen am Anfang oder Ende bietet. Die Methoden addXyz() und removeXyz() lösen für Immutable Collections eine UnsupportedOperationException aus. Die Methode reversed() ermöglicht die Verarbeitung der Elemente in umgekehrter Reihenfolge. Tatsächlich handelt es sich dabei um eine View, ähnlich wie bei subList(). Dadurch werden Änderungen in der View auch in der Original-Collection sichtbar und vice versa.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Ein kurzer Blick hinter die Kulissen zeigt, wie es mit Hilfe von DefaultMethoden möglich wurde, die Interfaces in die bestehende Interfacehierarchie einzupassen (Listing 10).

public interface SequencedCollection<E> extends Collection<E> {
  SequencedCollection<E> reversed();

  default void addFirst(E e) {
    throw new UnsupportedOperationException();
  }

  default void addLast(E e) {
    throw new UnsupportedOperationException();
    }

  default E getFirst() {
    return this.iterator().next();
  }

  default E getLast() {
    return this.reversed().iterator().next();
  }

  default E removeFirst() {
    var it = this.iterator();
    E e = it.next();
    it.remove();
    return e;
  }

  default E removeLast() {
    var it = this.reversed().iterator();
    E e = it.next();
    it.remove();
    return e;
  }
}

Mit dieser Umsetzung sehen wir die Interfacehierarchie und die drei grün markierten neuen Interfaces der Sequenced Collections in Abbildung 2.

Abb. 2: Die Interfacehierarchie (neue Interfaces sind grün markiert) (Bildquelle: [1])

Die Interfaces SequencedSet und SequencedMap

Betrachten wir nun noch die beiden Interfaces SequencedSet und SequencedMap. Beginnen wir mit SequencedSet. Es erweitert Set und basiert auch auf SequencedCollection, allerdings ohne neue Methoden zu definieren und mit einer kleinen Abweichung bei reversed(), das eine kovariante Überschreibung mit geändertem Rückgabewert ist:

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
  SequencedSet<E> reversed(); // covariant override
}

Analog zu SequencedCollection bietet SequencedMap die folgenden Methoden:

  • Entry<K, V> firstEntry() – liefert das erste Schlüssel-Wert-Paar
  • Entry<K, V> lastEntry() – liefert das letzte Schlüssel-Wert-Paar
  • Entry<K, V> pollFirstEntry() – entfernt das erste Schlüssel-Wert-Paar und gibt es zurück
  • Entry<K, V> pollLastEntry() – entfernt das letzte Schlüssel-Wert-Paar und gibt es zurück
  • V putFirst(K, V) – fügt ein Schlüssel-Wert-Paar am Anfang ein
  • V putLast(K, V) – fügt ein Schlüssel-Wert-Paar am Ende an
  • SequencedMap<K, V> reversed() – gibt eine View in umgekehrter Reihenfolge zurück

Damit ergibt sich die Interfacedefinition aus Listing 11.

interface SequencedMap<K,V> extends Map<K,V> {
  SequencedMap<K,V> reversed();
  SequencedSet<K> sequencedKeySet();
  SequencedCollection<V> sequencedValues();
  SequencedSet<Entry<K,V>> sequencedEntrySet();
  V putFirst(K, V);
  V putLast(K, V);
  Entry<K, V> firstEntry();
  Entry<K, V> lastEntry();
  Entry<K, V> pollFirstEntry();
  Entry<K, V> pollLastEntry();
}

SequencedMap-API

Das API von SequencedMap fügt sich nicht so gut in die Sequenced Collections ein. Es verwendet NavigableMap als Basis, daher bietet es statt getFirstEntry() die Methode firstEntry() und statt removeLastEntry() bietet es pollLastEntry(). Diese Namen korrespondieren nicht mit denen der SequencedCollection. Allerdings hätte der Versuch, dies zu tun, dazu geführt, dass NavigableMap vier neue Methoden erhalten hätte, die das Gleiche tun, wie die vier anderen Methoden, über die es bereits verfügt.

Sequenced Collections in Aktion

Zum Abschluss wollen wir die neuen Möglichkeiten einmal in Aktion erleben. Dazu definieren wir eine Liste mit Buchstaben und fragen im Anschluss das erste und das letzte Element ab. Danach erzeugen wir mit reversed() eine Collection mit umgekehrter Reihenfolge, die wir durchlaufen, zur Demonstration in einen Stream wandeln und die drei Elemente überspringen sowie schließlich das erste und letzte Element der umgekehrten Reihenfolge abfragen (Listing 12). Die Ausgabe zeigt Listing 13.

public static void sequenceCollectionExample() {
    System.out.println("Processing letterSequence with list");
    SequencedCollection<String> letterSequence = List.of("A", "B", "C", 
                                                         "D", "E");
    System.out.println(letterSequence.getFirst() + " / " +
                       letterSequence.getLast());

    System.out.println("Processing letterSequence in reverse order");
    SequencedCollection<String> reversed = letterSequence.reversed();
    reversed.forEach(System.out::print);
    System.out.println();
    System.out.println("reverse order stream skip 3");
    reversed.stream().skip(3).forEach(System.out::print);
    System.out.println();
    System.out.println(reversed.getFirst() + " / " + 
                       reversed.getLast());
    System.out.println();
}
Processing letterSequence with list
A / E
Processing letterSequence in reverse order
EDCBA
reverse order stream skip 3
BA
E / A

 

Variieren wir nun die Datenstruktur und nutzen Sets zur Verwaltung der Elemente. Zunächst einmal wird durch mehrmaliges Ausführen deutlich, dass die mit Set.of() erzeugten Daten keine fixe Reihenfolge besitzen, sondern dass diese von Ausführung zu Ausführung variiert. Am Beispiel eines TreeSet schauen wir uns die Möglichkeiten der Sequenced Collections, genauer des SequencedSet, an (Listing 14). Die Ausgabe zeigt Listing 15.

public static void sequencedSetExample() {
    // plain Sets do not have encounter order ... 
    // run multiple times to see variation
    System.out.println("Processing set of letters A-D");
    Set.of("A", "B", "C", "D").forEach(System.out::print);
    System.out.println();
    System.out.println("Processing set of letters A-I");
    Set.of("A", "B", "C", "D", "E", "F", "G", "H", "I").
        forEach(System.out::print);
    System.out.println();

    // TreeSet has order
    System.out.println("Processing letterSequence with tree set");
    SequencedSet<String> sortedLetters = 
                         new TreeSet<>((Set.of("C", "B", "A", "D")));
    System.out.println(sortedLetters.getFirst() + " / " + 
                       sortedLetters.getLast());
    sortedLetters.reversed().forEach(System.out::print);
    System.out.println();
}
Processing set of letters A-D
DCBA
Processing set of letters A-I
IHGFEDCBA
Processing letterSequence with tree set
A / D
DCBA

Fazit

Wir haben uns exemplarisch mit den Sequenced Collections als finalem Feature und mit den String Templates als vielversprechendem Preview Feature beschäftigt.

Sequenced Collections bereichern die ohnehin schon ausgefeilte Funktionalität des Collections-Frameworks und vereinheitlichen vor allem Zugriffe und Veränderungen des ersten und des letzten Elements sowie das Bereitstellen einer umgekehrten Reihenfolge.

Für die Konkatenation von fixen und variablen Textbestandteilen zu einem String gab es bisher schon diverse Varianten, alle mit spezifischen Stärken und Schwächen. String Templates werden die Art und Weise, wie man diese Aufgabe bewältigt, revolutionieren und stellen ein Feature dar, das Java wieder ein wenig moderner macht und zu anderen Sprachen wie Python oder Kotlin aufschließen lässt.

Java 21 bringt viele weitere Neuerungen, die die Programmiersprache voranbringen und attraktiver machen. Auch für komplette Newbies wird die Einstiegshürde gesenkt: Dank Unnamed Classes und Instance Main Methods lassen sich kleinere Java-Programme viel schneller und mit deutlich weniger Zeilen und für Anfänger schwierigen Begrifflichkeiten erstellen.

In diesem Sinne: Happy Coding mit dem brandaktuellen Java 21!


Links & Literatur

[1] https://cr.openjdk.org/~smarks/collections/SequencedCollectionDiagram20220216.png

The post Neues in Java 21: JEP 430 und 431 appeared first on JAX.

]]>
Ein Liebesbrief an die Java-Community https://jax.de/blog/ein-liebesbrief-an-die-java-community/ Fri, 01 Sep 2023 06:14:04 +0000 https://jax.de/?p=88940 Ich möchte diesen Artikel nutzen, um vielen Menschen und Unternehmen zu danken. Ein solches Projekt mit meinem Sohn ist nur möglich, weil sie fantastische Bibliotheken und Werkzeuge geschaffen haben, mit denen wir Anwendungen schnell entwickeln können.

The post Ein Liebesbrief an die Java-Community appeared first on JAX.

]]>
In meinem vorherigen Artikel [1] habe ich gezeigt, wie ich Java Enums verwende, um die Kategorien für Videos auf der Website https://4drums.media [2] zu definieren. Diese Website ist ein Lieblingsprojekt, das ich zusammen mit meinem dreizehnjährigen Sohn entwickelt habe.

Ich werde hier nicht das gesamte Projekt mit Ihnen teilen und Ihnen den kompletten Code zeigen. Nein, ich möchte die Gelegenheit vielmehr nutzen, um allen zu danken, die es uns ermöglicht haben, diese Website zu erstellen!

Abb. 1: Die verfügbaren Video- und Tutorialkategorien auf der Website 4drums.media

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Über 4drums.media

Wie viele Jugendliche heutzutage möchte auch mein Sohn „im Internet berühmt“ werden – er spielt Schlagzeug, seitdem er ein kleiner Junge ist. Um seine Ambitionen zu unterstützen, haben wir gemeinsam eine Website erstellt, auf der er Schlagzeugvideos veröffentlichen und eine Community aufbauen kann, die sich für das Schlagzeugspielen interessiert. Da er kein Budget hat, ist das Hosten und Bearbeiten von Videos keine Option. Glücklicherweise bieten YouTube und Vimeo APIs an, um mit ihren Diensten zu interagieren. In Kombination mit meinen Erfahrungen mit Spring Boot, Vaadin usw. konnten wir schnell loslegen und hatten bald die erste Version, die jetzt verfügbar ist. Werfen wir einen Blick auf die von uns verwendeten Tools, die Sie für Ihr nächstes persönliches oder berufliches Projekt inspirieren könnten.

IntelliJ IDEA

Visual Studio Code, Eclipse, NetBeans – alles großartige und kostenlose IDEs zur Erstellung von Java-Code. Aber die am häufigsten verwendete IDE ist seit vielen Jahren IntelliJ IDEA [3]. Ich benutze die (kostenpflichtige) Ultimate Edition, aber die kostenlose Community Edition ist genauso gut. Und ja, mein Sohn benutzt diese Version auf seinem Schul-PC! Am Code selbst hat er nicht mitgewirkt, aber ich konnte ihn einfach die neueste Version ziehen lassen, ein paar Farben und Symbole hinzufügen, lokal testen und sie dann wieder in das Repository einspeisen. Und dank des automatisierten Builds (siehe GitHub weiter unten) kann er die Änderungen innerhalb weniger Minuten live sehen. Ist es nicht großartig, dass jeder, vom Anfänger bis zum erfahrenen Benutzer, ein solch fantastisches Tool frei nutzen kann? Vielen Dank, JetBrains!

Spring Boot und Bibliotheken

Ich liebe Quarkus, Micronaut und andere, weil sie viel Evolution in die Java-Welt gebracht haben. Da ich jedoch bereits viel Erfahrung mit Spring Boot [4] hatte, habe ich diesen Weg weiterverfolgt. Aber es ist nicht nur Spring selbst, das dieses System zu einem fantastischen Framework macht. Es ist die Kombination mit Flyway zur Erweiterung der Datenbankstruktur, H2 für lokale Tests, Jsoup zur Validierung von Texteingaben, JUnit für Tests usw. Ich bin also nicht nur Spring dankbar, sondern auch den vielen Unternehmen und Entwicklern, die großartige Java-Bibliotheken und -Tools entwickeln!

YouTube und Vimeo

Einen Videodienst selbst zu hosten, würde den Rahmen eines Vater-Sohn-Projekts bei Weitem sprengen. Aber zum Glück müssen wir das nicht tun, denn viele Videodienste erlauben eine vollständige Integration. So können sie auf einfache Weise mehr Zuschauer anziehen, da ihre Videos in andere Websites integriert werden. Der Player unterliegt jedoch vollständig der Kontrolle des Anbieters, sodass jede Ansicht eines Videos (und der Werbung …) eine zusätzliche Ansicht für seine jeweilige Plattform darstellt.

Mit dem WebClient von Spring ist es mühelos möglich, die Beschreibung, den Kanal, das Vorschaubild und weitere Informationen z. B. von jedem YouTube-Video abzurufen. In Kombination mit Datensätzen, die der YouTube-API-Antwort-JSON [5] entsprechen, und der FasterXML-Bibliothek von Jackson ist nur minimaler Code erforderlich, um Daten anzufordern und das Ergebnis zu parsen (Listing 1).

public Optional<YouTubeApiResponse> getYouTubeVideoApi(String id) {
  String url = "https://www.googleapis.com/youtube/v3/videos"
  + "?id=" + id + 
  + "&part=snippet%2CcontentDetails%2Cstatistics"
  + "&key=" + apiKeyYouTube;
  YouTubeApiResponse video = webClient.get()
    .uri(URI.create(url))
    .retrieve()
    .bodyToMono(YouTubeApiResponse.class)
    .block();
  if (video == null) {
    logger.error("Could not get info from YouTube for {}", id);
    return Optional.empty();
  }
  logger.info("Video info from YouTube: {}", video.title());
  return Optional.of(video);
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record YouTubeApiResponse(
  String kind,
  String etag,
  List<Item> items) {}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Item(
  String kind,
  String etag,
  String id,
  Snippet snippet,
  ContentDetails contentDetails,
  Statistics statistics
) {
}

Vimeo bietet die gleiche Art von API [6], daher haben wir beide Dienste integriert. Mit dem Aufstieg des Fediverse werden Alternativen wie PeerTube [7] an Bedeutung gewinnen, und wir werden definitiv versuchen, die unterstützten Videoplattformen später zu erweitern.

SIE LIEBEN JAVA?

Den Core-Java-Track entdecken

 

Vaadin

Ich habe JavaFX schon oft für grafische Benutzeroberflächen auf dem Desktop verwendet. Mit Vaadin habe ich ein ähnlich leistungsfähiges und Java-basiertes Framework für die Websiteentwicklung gefunden. Mit reinem Java-Code können Sie sowohl das Backend als auch das Frontend innerhalb eines Maven-Projekts erstellen, das einfach zu paketieren und zu verteilen ist. Auf start.vaadin.com [8] können Sie die verschiedenen Seiten konfigurieren, die Sie benötigen (Abb. 2), das Theme gestalten (Abb. 3) und die Java- und Vaadin-Version auswählen, die Sie verwenden möchten. Wenn Sie fertig sind, können Sie ein vollständig vorbereitetes Maven-Projekt auf Basis von Spring Boot mit einem Mausklick herunterladen, entpacken und in der IDE öffnen und ausführen, um es weiter zu bearbeiten.

Abb. 2: Sie können die verschiedenen Seiten konfigurieren …

Abb. 3: … und das Theme gestalten

Jede Komponente ist auf der Vaadin-Website [9] sehr gut dokumentiert und mit viel Beispielcode versehen. Das Hinzufügen einer solchen Komponente zu den HTML-Seiten und das Verknüpfen mit einer Backend-Methode ist sehr einfach, wie Sie im Beispielcode für den Like-Button in Listing 2 sehen.

var like = new Button();
like.setIcon(VaadinIcon.THUMBS_UP_O.create());
like.setText(String.valueOf(numberOfLikes));
like.addClickListener(e -> {
  videoService.addLike(video, user);
});
add(like);

Abb. 4: Der Output von Listing 2

Vaadin bietet eine lizenzierte Version mit zusätzlichen erweiterten Komponenten (Charts, GridPro …) und Support. Dennoch bietet die freie und quelloffene Version selbst für viele Geschäftsfälle alle Tools, um eine vollständige Webanwendung zu erstellen. Ich habe sie in meinen früheren Jobs für Back-Office-Anwendungen verwendet, und die Entwicklungsgeschwindigkeit, die sich aus einer einzigen Codebasis für APIs und UI ergibt, ist enorm!

 

PostgreSQL

Gibt es heute noch Websites, die keine Datenbank verwenden? Mein Weg zur Datenbank begann vor vielen Jahren mit Microsoft Access. Obwohl es nicht für die Verwendung im Web gedacht war, konnte ich viele dynamische Websites mit einer Access-Datenbank als Kern erstellen. Später habe ich SQL Server, MySQL, NoSQL-Datenbanken und andere verwendet. Aber ich denke, wir können festhalten, dass PostgreSQL [10] den größten Teil der Welt der Onlinedatenbanken erobert hat. Ich liebe Tools, die einfach da sind, um die man sich nicht kümmern muss und die das tun, was sie tun sollen. Genau das hat PostgreSQL für mich seit vielen Jahren getan. Und auch hier ist die Kombination mit Spring Boot erstaunlich einfach und unkompliziert.

GitHub-Repository und Aktionen

Da ich nun schon seit einiger Zeit in dieser Branche tätig bin, habe ich viele Entwicklungen in der Art und Weise gesehen, wie Quelldateien verwaltet und gemeinsam genutzt werden. Von einem normalen Dateiserver mit ständigen Konflikten, über CVS, SVN bis hin zu Git. Im Foojay-Podcast „The Future of Source Control and CI/CD“ [11] sind wir zu dem Schluss gekommen, dass das aktuelle Git wahrscheinlich kein Endpunkt ist und wir weitere Entwicklungen sehen werden. Ist es nicht seltsam, dass nur die Softwareindustrie einen solchen Workflow angenommen hat? Warum verwendet z. B. der Gesetzgeber nicht denselben Ansatz, während die Erweiterung und Verbesserung von Gesetzen eigentlich sehr ähnlich ist und auf einem System von „Patches“ basiert? Könnte es sein, dass Trunks, Merge-Konflikte und zu viele mögliche Befehle Git zu kompliziert machen? Und dass nur Softwareentwickler in der Lage zu sein scheinen, mit diesen Herausforderungen fertigzuwerden?

Die Beantwortung dieser Fragen würde den Rahmen dieses Artikels sprengen, aber ich bin auf jeden Fall sehr dankbar für das, was GitLab, GitHub und Co. als kostenlosen Service für Open-Source- und Pet-Projekte anbieten. Ein zuverlässiges Versionskontrollsystem zu haben, ohne sich um Serverkosten, Back-ups, Upgrades usw. kümmern zu müssen, ist die erste Hürde, die diese Anbieter nehmen. Aber für mich ist der wichtigste Grund, sie zu lieben, die CI/CD, die sie bieten. Dank GitHub Actions führt jeder Commit in das Repository dazu, dass eine neue Websiteversion innerhalb von Minuten bereitgestellt wird, und zwar vollständig automatisiert, dank einer einzigen .yml-Datei. Ja, wir sind uns alle einig, dass YAML ätzend ist und das Erreichen einer funktionierenden Aktionsdatei nervt, aber wenn man eine funktionierende Lösung hat und sich nicht mehr um den Build- und Deployment-Prozess kümmern muss, ist das ein echter Meilenstein in jedem Projekt!

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Uberspace

Bis jetzt habe ich die meisten der Tools aufgelistet, die ich für die 4drums.media-Website verwendet habe und die alle kostenlos erhältlich sind! Jetzt bleibt nur noch ein letzter Schritt für jede Website: ein Onlinezuhause finden. Die Möglichkeiten sind endlos, von Shared Hosting über VPS bis hin zu kompletten Netzwerkumgebungen bei AWS, Azure oder jedem anderen Anbieter. Von nicht so günstig bis sehr teuer …

Da unser Budget für dieses Projekt sehr begrenzt ist (eigentlich nichtexistent), habe ich in Deutschland eine schöne Lösung bei uberspace.de [12] gefunden. Mit einem Pay-as-you-want-Modell ab 5 Euro/Monat bekommt man einen VPS, den man nach Bedarf konfigurieren kann. In unserem Fall bedeutete das, die richtige Java-Version und PostgreSQL zu installieren. Mit einer sehr detaillierten Dokumentation [13] war jeder Schritt in diesem Prozess eine schnelle und angenehme Erfahrung. Und wenn man nicht weiterkommt, antwortet der Support schnell und hilft einem oder verweist auf die richtige Dokumentation.

Das Hosting der Website (und des Domänennamens) ist also der einzige Teil dieses Projekts, bei dem „Herr Visa“ uns helfen musste, aber der Betrag ist trotzdem minimal.

Fazit

Java ist nicht nur eine Sprache und eine Laufzeitumgebung. Es ist auch eine große Gemeinschaft von Menschen und Unternehmen, die erstaunliche Dinge entwickelt, die in den meisten Fällen kostenlos genutzt werden können. Teil dieser Gemeinschaft zu sein, ist eine tägliche Freude, und ich bin wirklich dankbar, dass all das verfügbar ist! Deshalb sage ich allen, die einen Beitrag leisten und all dies möglich machen: Danke! Aus tiefstem Herzen!


Links & Literatur

[1] Delporte, Frank: „Verborgene Schönheiten“; in Java Magazin 9.2023

[2] https://4drums.media

[3] https://www.jetbrains.com/idea/

[4] https://start.spring.io

[5] https://developers.google.com/youtube/v3/

[6] https://developer.vimeo.com

[7] https://joinpeertube.org

[8] https://start.vaadin.com

[9] https://vaadin.com/docs/latest/components

[10] https://www.postgresql.org

[11] https://foojay.io/today/foojay-podcast-26/

[12] https://uberspace.de

[13] https://manual.uberspace.de

[14] https://webtechie.be/books

The post Ein Liebesbrief an die Java-Community appeared first on JAX.

]]>