Connect with us

Entwicklung & Code

Core Java: Kontrollierte Aggregation – vom Collector zum Gatherer


Mit der Stream-API besitzt Java seit Version 8 einen eleganten, funktionalen Ansatz zur Verarbeitung von Datenmengen. Die Terminaloperation collect(...) stellt dabei die Brücke vom Stream zu einer zielgerichteten Aggregation dar – sei es in Form von Listen, Maps, Strings oder komplexeren Datenstrukturen. Bis Java 20 war die Verarbeitung durch Collector-Instanzen geregelt, die intern aus einem Supplier, einem Accumulator, einem Combiner und optional einem Finisher bestanden. Dieses Modell funktioniert gut für einfache Akkumulationen, kommt jedoch insbesondere bei komplexen, zustandsbehafteten oder bedingten Aggregationen schnell an seine Grenzen.


Sven Ruppert

Sven Ruppert

Seit 1996 programmiert Sven Java in Industrieprojekten und seit über 15 Jahren weltweit in Branchen wie Automobil, Raumfahrt, Versicherungen, Banken, UN und Weltbank. Seit über 10 Jahren ist er von Amerika bis nach Neuseeland als Speaker auf Konferenzen und Community Events, arbeitete als Developer Advocate für JFrog und Vaadin und schreibt regelmäßig Beiträge für IT-Zeitschriften und Technologieportale.
Neben seinem Hauptthema Core Java beschäftigt er sich mit TDD und Secure Coding Practices.

In Java 21 kommt das neue Interface java.util.stream.Gatherer hinzu, das die Semantik und Kontrolle über den Akkumulationsprozess deutlich erweitert. Während ein Collector passiv Daten sammelt, reagiert ein Gatherer aktiv auf die einfließenden Elemente – vergleichbar mit einem spezialisierten Transducer in funktionalen Programmiersprachen. Gatherer sind insbesondere dort nützlich, wo eine prozedurale oder zustandsorientierte Aggregation notwendig ist, und sie erlauben zusätzlich das Einfügen von Elementen, das Filtern, das Überspringen sowie das explizite Beenden des Sammelvorgangs – und das alles im Rahmen einer funktional komponierbaren Architektur.

Ein Gatherer beschreibt die Transformation eines Stream in ein Ergebnis vom Typ R bei enger Kontrolle über den Akkumulationsprozess. Im Unterschied zum Collector, der in gewisser Weise ein Container für Aggregationslogik ist, erlaubt der Gatherer eine regelbasierte, zustandsabhängige Verarbeitung von Eingaben – inklusive der Möglichkeit, Elemente zu überspringen (Drop), zusätzlich einzufügen (Inject) oder die Verarbeitung vorzeitig zu beenden (FinishEarly).

Dazu basiert ein Gatherer auf der Idee einer Senke, die im Kontext des Stream-Prozessors aufgerufen wird. Diese Senke erhält jedes Eingabeelement, kann darauf reagieren und beeinflusst damit aktiv den Fluss der Verarbeitung. Die eigentliche Verarbeitung wird über eine sogenannte Adapter Factory beschrieben, die die Übergänge zwischen den Zuständen der Aggregation verwaltet.

Während der herkömmliche Collector vor allem als finales Akkumulationswerkzeug dient – also dazu, die im Stream enthaltenen Elemente in eine Zielstruktur wie eine Liste, eine Map oder eine Aggregation zu überführen –, geht der Gatherer konzeptionell weit über diese Rolle hinaus. Er bietet einen eigenständigen Mechanismus, der sowohl semantisch als auch funktional neue Ausdrucksformen für die Stream-Verarbeitung eröffnet und beispielsweise neue, zuvor nicht vorhandene Elemente in den Datenstrom einspeisen kann. Das eröffnet die Möglichkeit, Initialisierungswerte am Beginn eines Streams einzuführen oder Steuerzeichen wie Header und Footer gezielt an den Anfang oder das Ende zu setzen – ohne den ursprünglichen Datenstrom dafür künstlich erweitern zu müssen.

Besonders deutlich wird diese gestalterische Freiheit beim Umgang mit Zuständen (State). Ein Gatherer kann zustandsbehaftet arbeiten und diesen Zustand auch über mehrere Elemente hinweg beeinflussen lassen. Das eröffnet neue semantische Horizonte: Beispielsweise lassen sich Fensteroperationen formulieren, bei denen eine temporale oder sequenzielle Logik angewendet wird – etwa das Aggregieren von Daten bis zu einem bestimmten „Ende“-Marker oder das Zusammenfassen von Elementgruppen, die sich nur durch eine bestimmte Reihenfolge oder inhaltliche Struktur identifizieren lassen.

Auch komplexe Entscheidungsstrukturen, wie sie in mehrstufigen Parsing-Prozessen oder beim Traversieren von Entscheidungsbäumen nötig sind, lassen sich durch zustandsbehaftete Gatherer elegant und deklarativ umsetzen. Dabei bleibt die Schnittstelle weiterhin im Geist der funktionalen Programmierung: Transformation und Aggregation bleiben getrennt beschreibbar, doch der Gatherer erlaubt es, sie auf eine Weise zu verbinden, die bislang nur durch imperative oder schwer wartbare Stream-Hacks realisierbar war.

Ein weiterer Vorteil liegt im kontrollierten Einfluss vergangener Elemente auf das aktuelle Verhalten. So kann ein Gatherer die Entscheidung treffen, ein Element zu verwerfen, weil ein vorheriges Element einen bestimmten Kontext gesetzt hat. Diese Fähigkeit zur Kontextsensitivität ist in Situationen relevant, in denen Datenströme strukturell „nicht sauber“ sind – also nicht der Definition entsprechen oder Fehler enthalten. Das betrifft etwa Protokolldateien, inkonsistente Datenexporte oder die Analyse natürlicher Sprache.

Stellen wir uns vor, wir möchten aus einem Stream von Strings nur diejenigen Elemente sammeln, die eine bestimmte Eigenschaft besitzen, und diese dann gruppieren – beispielsweise alle Wörter, die länger als fünf Zeichen sind, gruppiert nach ihrem Anfangsbuchstaben. Diese Anforderung lässt sich mit einem Collector zwar formulieren, benötigt jedoch eine Kombination aus Vorverarbeitung (zum Beispiel filter(...)) und nachgelagerter Gruppierung. Mit einem Gatherer hingegen lässt sich dieser kombinierte Prozess elegant, zustandsvoll und in einem Schritt abbilden:


Gatherer>> gatherer =
    Gatherer.ofSequential(
        () -> new HashMap>(),
        (map, element, downstream) -> {
            if (element.length() > 5) {
                char key = element.charAt(0);
                map.computeIfAbsent(
key, 
k -> new ArrayList<>()).add(element);
            }
            return true;
        }
    );


In diesem Beispiel wird für jedes Element entschieden, ob es in das Ergebnis einfließt. Die Logik ist unmittelbar in den Gatherer eingebettet. Der Rückgabewert true signalisiert, dass die Verarbeitung fortgesetzt werden soll. Würde man an dieser Stelle stattdessen false zurückgeben, würde der Stream vorzeitig beendet – ein Verhalten, das mit herkömmlichen Collectors so nicht erreichbar ist.

Das Interface Gatherer unterscheidet dafür explizit zwischen sequenzieller und paralleler Verarbeitung. Die zentrale Unterscheidung ergibt sich durch die Factory-Methoden:

Gatherer.ofSequential(...) // Nur sequenziell nutzbar

Gatherer.ofConcurrent(...) // Für parallele Streams geeignet

Ein mit ofConcurrent(...) erzeugter Gatherer darf in parallelen Streams verwendet werden, muss jedoch bestimmte Anforderungen erfüllen: Er muss thread-safe sein oder auf thread-isolierten Akkumulatoren beruhen. Dies entspricht in etwa der Logik bei parallelen Collectoren, bei denen die interne Zustandsverwaltung eine gleichzeitige Verarbeitung verschiedener Elemente in unabhängigen Threads erlaubt.



Source link

Weiterlesen
Kommentar schreiben

Leave a Reply

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Entwicklung & Code

Angular Signals: Elegante Reaktivität als Architekturfalle


Mit Angular 17 hielten Signals 2023 offiziell Einzug in das Framework. Sie versprechen eine modernere, klarere Reaktivität: weniger Boilerplate-Code, bessere Performance. Gerade im Template- und Komponentenbereich lösen sie viele Probleme eleganter als klassische Observable-basierte Ansätze.


Nicolai Wolko

Nicolai Wolko

Nicolai Wolko ist Softwarearchitekt, Consultant und Mitgründer der WBK Consulting AG. Er unterstützt Unternehmen bei komplexen Web- und Cloudprojekten und wirkt als Sparringspartner sowie Gutachter für CTOs. Fachbeiträge zur modernen Softwarearchitektur veröffentlicht er regelmäßig in Fachmedien und auf seinem Blog.

Statt Subscriptions, pipe() und komplexen Streams genügen nun wenige Zeilen mit signal(), computed() und effect(). Der Code wirkt schlanker, intuitiver und näher am User Interface (UI).

Da liegt die Idee nahe: Wenn Signals im UI überzeugen, warum nicht auch in der Applikationslogik? Warum nicht RxJS vollständig ersetzen? Ein Application Store ohne Actions, Meta-Framework und Observable: direkt, deklarativ, minimalistisch.

Ein Ansatz, der im Folgenden anhand eines konkreten Fallbeispiels analysiert und kritisch hinterfragt wird. Anschließend wird behandelt, in welchen Kontexten sich Signals sinnvoll einsetzen lassen.

Auf den ersten Blick besitzt dieses Beispiel einen klar strukturierten Architekturansatz. Doch der Wandel beginnt unauffällig. RxJS bleibt zunächst außen vor. Das UI reagiert flüssig, der Code bleibt übersichtlich. Komplexe Streams, verschachtelte Operatoren oder eigenes Subscription Handling entfallen. Stattdessen kommen Signals zum Einsatz. Es liegt nahe, diese unkomplizierte Herangehensweise auch für die Applikationslogik zu übernehmen. Im folgenden Beispiel übernimmt ein ProductStore die Zustandslogik. Signals organisieren Kategorien, Filter und Produktdaten – reaktiv und direkt.


@Injectable({ providedIn: 'root' })
export class ProductStore {
  private allProducts = signal([]);
  readonly selectedCategory = signal('Bücher');
  readonly onlyAvailable = signal(false);

  readonly productList = computed(() => {
    return this.allProducts().filter(p =>
      this.onlyAvailable() ? p.available : true
    );
  });

  selectCategory(category: string) {
    this.selectedCategory.set(category);
  }

  toggleAvailabilityFilter() {
    this.onlyAvailable.set(!this.onlyAvailable());
  }

  constructor(private api: ProductApiService) {
    effect(() => {
      const category = this.selectedCategory();
      const onlyAvailable = this.onlyAvailable();
      this.api.getProducts(category, onlyAvailable).then(products => {
        this.allProducts.set(products);
      });
    });
  }
}


Die Struktur überzeugt zunächst durch Klarheit. Die Komponente konsumiert productList direkt, ohne eigene Logik. Der Store verwaltet den Zustand, Signals sorgen für die Weitergabe von Änderungen.

Doch mit der nächsten Anforderung ändert sich das Bild: Bestimmte Produkte sollen zwar im Katalog verbleiben, aber im UI nicht mehr erscheinen. Da auch andere Systeme die bestehende API verwenden, ist eine Anpassung nicht möglich. Stattdessen liefert das Backend eine Liste freigegebener Produkt-IDs, anhand derer das UI filtert.


@Injectable({ providedIn: 'root' })
export class ProductStore {
  // [...]

  readonly backendEnabledProductIds = signal>(new Set());

  readonly productList = computed(() => {
    return this.allProducts().filter(p =>
      this.onlyAvailable() ? p.available : true
    ).filter(p => this.backendEnabledProductIds().has(p.id));
  });

  constructor(private api: ProductApiService) {
    effect(() => {
      const category = this.selectedCategory();
      const onlyAvailable = this.onlyAvailable();
      this.api.getProducts(category, onlyAvailable).then(products => {
        this.allProducts.set(products);
      });
    });

    effect(() => {
      this.api.getEnabledProductIds().then(ids => {
        this.backendEnabledProductIds.set(new Set(ids));
      });
    });
  }

  // [...]
}


Nach außen bleibt die Architektur zunächst unverändert. Die Komponente enthält weiterhin keine eigene Logik, Subscriptions sind nicht notwendig, und die Reaktivität scheint erhalten zu bleiben. Im Service jedoch nimmt die Zahl der effect()s zu, Abhängigkeiten werden vielfältiger, und die Übersichtlichkeit leidet.

Nach und nach wandert Logik in verteilte effect()s, bis ihre Zuständigkeiten kaum noch greifbar sind. Aus einem überschaubaren ViewModel entsteht ein Gebilde mit immer mehr impliziten Reaktionen – eine Entwicklung, die ein waches Auge für Architektur erfordert.

Das Setup wirkt zunächst unspektakulär. Die Produktliste wird über ein computed() erstellt, gefiltert nach Verfügbarkeit und den vom Backend freigegebenen IDs. Zwei effect()s laden die Daten.

Der Code wirkt aufgeräumt und lässt sich modular erweitern. Doch der nächste Feature-Wunsch stellt das System auf die Probe: Die Stakeholder möchten wissen, wie oft bestimmte Kategorien angesehen werden. Die Entwicklerinnen und Entwickler entscheiden sich für einen naheliegenden Ansatz. Eine Änderung der Kategorie löst ein Tracking-Event aus. Ein effect() scheint dafür perfekt geeignet – unkompliziert und ohne erkennbare Nebenwirkungen:


effect(() => {
  const category = this.selectedCategory();
  this.analytics.trackCategoryView(category);
});


Schnell eingebaut, kein zusätzlicher State, keine neue Subscription. Eine Reaktion auf das bestehende Signal, unkompliziert und ohne erkennbare Nebenwirkungen. Doch damit verlässt der Code den Bereich kontrollierter Reaktivität.

Die Annahme ist klar: Ändert sich die Kategorie, wird ein Tracking ausgelöst. Was dabei leicht zu übersehen ist: Signals reagieren nicht auf Bedeutung, sondern auf jede Mutation. Auch wenn set() denselben Wert schreibt oder zwei Komponenten nacheinander dieselbe Auswahl treffen, passiert zwar technisch etwas, semantisch aber nicht. Das Ergebnis sind doppelte Events und verzerrte Metriken, ohne dass der Code einen Hinweis darauf gibt. Alles sieht korrekt aus.

Das Tracking erfolgt unmittelbar im selben Ausführungstakt (Tick), ohne Möglichkeit zur Entkopplung. Wenn parallel ein weiterer effect() ausgelöst wird – etwa durch ein zweites Signal –, fehlt jegliche Koordination.

Die Reihenfolge ist nicht vorhersehbar, und das UI kann in einen inkonsistenten Zustand geraten: Daten werden mehrfach geladen, Reaktionen überschneiden sich, Seiteneffekte sind nicht mehr eindeutig zuzuordnen. Mit jedem zusätzlichen effect() steigt die Zahl impliziter Wechselwirkungen. Was wie ein reagierendes System wirkt, ist längst nicht mehr entscheidungsfähig.

In einem Kundenprojekt führte genau dieser Zustand dazu, dass ein effect() mehrfach pro Sekunde auslöste. Nicht wegen einer echten Änderung, sondern weil derselbe Wert mehrfach gesetzt wurde. Das UI zeigte korrekte Daten, aber das Backend war mit redundanten Anfragen überlastet.

effect() wirkt wie ein deklarativer Controller: „Wenn sich X ändert, tue Y.“ Doch in Wirklichkeit ist es ein reaktiver Spion. Er beobachtet jedes Signal, das gelesen wird, unabhängig von der semantischen Bedeutung. Er feuert sogar dann, wenn niemand es erwartet. Und er ist nicht koordiniert. Jeder effect() lebt in seiner eigenen Welt, ohne zentrale Regie.

Was als architektonische Vereinfachung begann, endet in einer Blackbox aus Zuständen, Reaktionen und Nebenwirkungen. Mit jedem weiteren Feature wächst diese Komplexität. Es gibt keinen großen Knall, aber eine zuvor elegant erscheinende Struktur driftet leise auseinander.



Source link

Weiterlesen

Entwicklung & Code

KubeSphere entfernt Open-Source-Dateien und stellt Support ein


close notice

This article is also available in
English.

It was translated with technical assistance and editorially reviewed before publication.

Die chinesische Kubernetes-Plattform KubeSphere hat auf GitHub angekündigt, die Open-Source-Version des Produkts zurückzuziehen und den kostenlosen Support einzustellen: „Ab dem Datum dieser Ankündigung werden die Download-Links für die Open-Source-Version von KubeSphere deaktiviert und der kostenlose technische Support eingestellt.“

Das Kernprojekt von KubeSphere auf GitHub bleibt jedoch Open Source unter Apache-2-Lizenz. Als Grund für den Wechsel nennt der Hersteller die Änderung der Digitalisierung mit Gen AI, wodurch auch die Infrastruktur-Branche tiefgreifende Veränderungen erfahren hat. „Um sich an die neue Ära anzupassen, die Produktkapazitäten und die Servicequalität weiter zu verbessern und sich auf die Forschung und Entwicklung von Kerntechnologien sowie die Optimierung kommerzieller Lösungen zu konzentrieren, hat das Unternehmen nach mehrjähriger Planung und sorgfältiger Prüfung beschlossen, die folgenden Anpassungen am Open-Source-Projekt KubeSphere vorzunehmen.“ Es folgt die oben genannte Ankündigung.

Welche aktuellen oder künftigen Produkte konkret nicht mehr Open Source sind, ist der Ankündigung nicht zu entnehmen. Auf der Webseite weist der Hersteller derzeit sogar noch auf die CNCF-Zertifizierung hin. Nutzern von KubeSphere rät der Diskussionsbeitrag, sich für eine kommerzielle Version an den Support zu wenden.

Der Beitrag ist auf Chinesisch, darunter findet sich eine englische Übersetzung. Wir haben mit KI-Hilfe direkt aus dem Chinesischen übersetzt.


(who)



Source link

Weiterlesen

Entwicklung & Code

JetBrains: Preissprung bei Entwicklungsumgebungen ab 1. Oktober


close notice

This article is also available in
English.

It was translated with technical assistance and editorially reviewed before publication.

Das tschechische Softwareunternehmen JetBrains hat angekündigt, seine Preise am 1. Oktober 2025 anzuziehen. Nach drei Jahren der Preisstabilität sieht sich der Hersteller beliebter Entwicklungsumgebungen (Integrated Development Environments, IDEs) aufgrund der Inflation gezwungen, die Preise für Abonnements zu erhöhen. Wer im Voraus zahlt, kann die bisherigen Preise noch für eine begrenzte Zeitdauer über den 1. Oktober hinaus beibehalten.

Betroffen sind die Abos für die JetBrains-Entwicklungsumgebungen – wie IntelliJ IDEA, WebStorm oder PhpStorm –, die .NET-Tools, das .NET-Toolkit dotUltimate und die IDE-Sammlung All Products Pack. Auf einer Webseite informiert JetBrains über die Preisänderungen. Beispielsweise erhöhen sich die Kosten der IDE IntelliJ IDEA Ultimate für den individuellen Einsatz bei jährlicher Zahlweise von 169 Euro auf 199 Euro (plus Mehrwertsteuer), bei monatlicher Zahlung von 16,90 Euro auf 19,90 Euro – jeweils auf das erste Nutzungsjahr bezogen. Für Unternehmen fallen die Steigerungen happiger aus: Das gleiche Produkt kostet pro User und Jahr derzeit 599 Euro (oder 59,90 Euro monatlich), ab dem 1. Oktober 719 Euro (oder 71,90 Euro monatlich) – eine Erhöhung um rund 20 Prozent.


Kosten für IntelliJ IDEA Ultimate für die individuelle Nutzung

Kosten für IntelliJ IDEA Ultimate für die individuelle Nutzung

Kosten für IntelliJ IDEA Ultimate für die individuelle Nutzung

(Bild: JetBrains)


Kosten für IntelliJ IDEA Ultimate für Unternehmen

Kosten für IntelliJ IDEA Ultimate für Unternehmen

Kosten für IntelliJ IDEA Ultimate für Unternehmen

(Bild: JetBrains)

Beim All Products Pack, das aus elf Entwicklungsumgebungen und weiteren Inhalten besteht, steigen die Preise für den individuellen Einsatz von 289 Euro auf 299 Euro pro Jahr an, für den Einsatz in Unternehmen pro Jahr und User von 779 Euro auf 979 Euro.

Für bestimmte Nutzergruppen wie Lehrkräfte, Schülerinnen und Schüler oder Core Maintainer von Open-Source-Projekten sind weiterhin kostenfreie Angebote aufgeführt.

JetBrains bietet seinen bestehenden sowie neuen Kundinnen und Kunden die Möglichkeit, im Voraus noch zu den derzeitigen Preisen zu bezahlen: Für individuelle Abos gilt dieser dann bis zu drei Jahre lang, für kommerzielle bis zu zwei Jahre. Dann wird die entsprechende Zahlung jedoch auf einen Schlag vor dem 1. Oktober 2025 fällig.

Weitere Details bieten der JetBrains-Blog und die Preisübersichtsseite.


(mai)



Source link

Weiterlesen

Beliebt