JavaBeans samt Serialisierung und Mechanismus für Property-Change Handling

JavaBeans sind ein wichtiger Bestandteil der Java-Spezifikation. Ein Einstigspunkt zu sehr vielen greundlegenden Java-Spezifikationen (hier für Java 15) ist einsehbar unter Java® Platform, Standard Edition & Java Development Kit Specifications Version 15. Darin Sind auch die Spezifikationen zur Serialisierung finden.

Interessanterweise ist die JavaBeans-Spezifikation darin nicht (vielleicht irgendwo versteckt) zu finden. Diese ist nachfolgend angeführt.

Die Spezifikationen sind großteils ohne extremes Fachwissen zumindest überblicksweise verständlich und teils sogar sehr anschaulich.

1. Was sind JavaBeans

Hier der Einstieg zur original JavaBeans Dokumentation (beginnend ca. 1997): Oracle JavaBeans Spec

Kurz und salopp zusammengefasst: JavaBeans ist eine Spezifikation, keine formal/syntaktisch zwingende Regelung. Allerdings gibt es viele Entwicklungswerkzeuge, Java-Frameworks, Bibliotheken und Komponenten, die auf dieser Spezifikation beruhen und damit die Spezifikation in verschiedenen Situationen faktisch zwingend machen.

Ursprünglich war eine starke Ausrichtung auf GUI-Komponenten vorhanden, wie sich an manchen Stellen in Ansatz und Formulierung erkennen lässt. Mittlerweile ist der Bereich viel breiter, Beans sind im JDK und sehr vielen anderen Java-Frameworks (z.B. für DB-Zugriff, im Web-Bereich) in Verwendung.

Die wichtigen festgelegten Charakteristiken einer JavaBean sind:

  • Erzeugung – zwingend ein Default-Konstruktor (parameterlos), bei Bedarf natürlich auch weitere mit Parametern. Damit lassen sich Objekte bei Bedarf einfach automatisch instanzieren, was vor allem für intelligente IDE-Funktionen und auch für die unten besprochene (De-)Serialisierung wichtig ist

  • Eigenschaften (Properties) – sind durch die bekannten Zugriffsmethoden "Getter" und "Setter" definiert (dahinter muss nicht zwingend eine Instanzvariable stehen!):

    • "ReadOnly" (nur Getter)

    • "WriteOnly" (nur Setter)

    • "ReadWrite" (Getter und Setter).

    Getter- und Setter-Festlegungen gibt es auch für Kollektionen (Array, Listen, etc.): hier ist als erster Parameter der Index oder Schlüssel anzugeben.

  • Objekt-Speicherung, Transport und Wiederherstellung inklusive aller Assoziationen (Serialisierung) – Speicherung einzelner Objekte – z.B. für Transport über Netzwerk, etc. zu einer anderen Applikation oder Applikations-Instanz – und anschließendes "Auspacken" als wieder voll verwendbares Objekt samt allen Objekten, wohin gerade Assoziationen bestehen. Detais siehe nachfolgend.

  • Änderungs-Ereignisse – Möglichkeit der JavaBean-Objekte, Eigenschaftswert-Änderungen an interessierte andere Objekte zu melden (Property-Change Handling).

  • Introspektion (mittels des Reflection-API) – das ist ein Mechanismus, eine Bean auf ihre Eigenschaften, Ereignisse (Events) und Operationen zu analysieren. Die API bietet dabei Möglichkeiten, die eine zusätzliche Unterstützung zur Introspektion von Seiten des Bean-Entwicklers unnötig machen. Beans können per Reflexion untersucht werden, wenn sie sich an die in der Spezifikation definierten Konventionen halten (teils Zitat aus Wikipedia).

  • Internationalisierung (mit Property-Dateien o.ä.) Zusätzlich zu obigen notwendigen Grundpfeilern lässt sich durch Internationalisierung die Entwicklung internationaler Komponenten vereinfachen. Verwendet eine Bean länderspezifische Ausdrücke, wie etwa Währungs- oder Datumsformate, kann der Bean-Entwickler mit länderunabhängigen Bezeichnern arbeiten, die dann in die jeweilige Landessprache übersetzt werden (teils Zitat aus Wikipedia).

2. Serialisierung Überblick

  • Implementation des Interface java.io.Serializable (Marker-Interface ohne jede formal nötige Methode – es gibt nur einige optionale) bzw. des davon abgeleiteten Interface java.io.Externalizable (mit 2 formal erforderlichen Methoden void writeExternal(…​) throws …​, void readExternal(…​) throws …​)

3. Property-Change Handling

3.1. Prinzip

Bei Änderungen von Zuständen bzw. Eigenschaftwerten in einem Programmbereich entsteht in fast allen komplexeren Applikationen der Bedarf, in anderen Programmteilen ebenfalls Aktivitäten zu setzen bzw. Anpassungen durchzuführen.

Ein einfaches Beispiel ist eine nach einer Eigenschaft sortierte Liste: Sobald diese Eigenschaft in einem "Listenmitglied" geändert wird, bricht potentiell die Sortierung zusammen. Die Liste sollte also informiert werden, dass die Sortierung zu kontrollieren und gegebenenfalls neu vorzunehmen ist.

Im Bereich von GUIs ist ständig eine Aktualisierung von Teilen des UI nötig, sobald sich Selektionen ändern, Werte ändern etc. – hier ist in so einem Fall die Benachrichtigung vieler anderer Komponenten nötig.

Bei dynamischer Programmstruktur muss auch ein dynamisches Benachrichtigungskonzept vorhanden sein. Dazu gibt es folgendes Konzept:

  • Jedes "interessante" Objekt hat in einem zusätzlichen Objekt-Attribut vom Typ PropertyChangeSupport (oft pcs genannt) eine Liste von Interessenten, die bei Änderungen (ausschließlich per Aufruf des Setters oder anderer dafür passend ausgestatteter Methoden) "interessanter" Eigenschaften mit den nötigen Details informiert werden.

  • damit sich Intessenten (Listeners) anmelden (und auch wieder abmelden) können, stellt das pcs-Objekt zwei dafür passende Methoden bereit.

  • Um Mitglied solcher Interessenten-Listen werden zu können, muss die Klasse des interessierten Objekts ein bestimmtes Interface implementieren, das sicherstellt, dass jeder Interessent eine festgelegte Methode hat, die zur Verständigung aufgerufen wierden kann.

  • Die Details der Änderung (wer, was, alter Wert, neuer Wert sowie bei Bedarf weitere Infos) werden in sogenannten Event-Objekten (erben von einer bestimmten Klasse) bereitgestellt.

3.2. Umsetzung einfacher Fall

3.2.1. Klasse unter Beobachtung

Zusatz-Attribut:
private final PropertyChangeSupport pcs;

Im Konstruktor:
pcs = new PropertyChangeSupport(this);

Typischer Setter:

    public void setName(String newName) throws BadParamException {
        if (newName == null || newName.isBlank()) {
            throw new BadParamException(String.format("new '%s' null or blank: '%s'",
                   PROP_NAME, newName));
        }
        String oldName = this.name;
        this.name = newName;
        pcs.firePropertyChange(PROP_NAME, oldName, newName);
    }

Interessent aufnehmen (mit optionaler Doublettenprüfung):

    public void addPropertyChangeListener(PropertyChangeListener aspirant) {

        for (PropertyChangeListener listener : pcs.getPropertyChangeListeners()) {
            if (listener.equals(aspirant)) {
                System.out.format("WARNING: listener %s already included", aspirant);
                return;
            }
        }
        pcs.addPropertyChangeListener(aspirant);
    }

Nicht-mehr-Interessent entfernen:

    public void removePropertyChangeListener(PropertyChangeListener leaver) {
        pcs.removePropertyChangeListener(leaver);
    }

3.2.2. Interessenten-Klasse

muss Interface PropertyChangeListener implementieren:

    public void add(Employee employee) throws BadParamException {
        if (employee == null) {
            throw new BadParamException("mitarb null");
        }
        if (employeesMap.containsKey(employee.getSocSecNr())) {
            throw new BadParamException(String.format("mitarb with SozVersNr %s already in map",
                    employee.getSocSecNr()));
        }
        employeesMap.put(employee.getSocSecNr(), employee);
        employee.addPropertyChangeListener(this);
    }

    public Employee remove(SocSecNr ssnr) {
        Employee removed = null;
        if (employeesMap.containsKey(ssnr)) {
            removed = employeesMap.remove(ssnr);
            removed.removePropertyChangeListener(this);  //removed is not member if Company any more
        }
        return removed;
    }

    // ...

    @Override    // von interface 'PropertyChangeListener'
    public void propertyChange(PropertyChangeEvent evt) {
        System.out.format("changed object: %s%n", evt.getSource());
        System.out.format("  changed Prop: %s%n", evt.getPropertyName());
        System.out.format("  oldValue:     %s%n", evt.getOldValue());
        System.out.format("  newValue:     %s%n", evt.getNewValue());
        System.out.println();

        //NOW e.g. do new sorting, since sort criteria could be affected by change, etc.
        if (evt.getSource() instanceof Employee) {  // if more subclasses or interfaces ...
            if (evt.getPropertyName().equals(Employee.PROP_BIRTHDATE)) {
                printEmployeesSorted("new list after birthDate change", allEmployees(),
                        new Employee.AgeComparator());
            }

            Employee emp = (Employee) evt.getSource(); // much easier with Java-17 !!
            if (emp.getGender() == Gender.FEMALE
                    && emp.getBirthDate().isBefore(LocalDate.parse("1950-01-01"))) {
                System.out.format("ALARM! great-grandma found?: %s%n%n", emp);
            }
        }
        // More actions depending on object, propname, values, ...

    }

3.3. Umsetzung für Listen

3.4. Umsetzung für komplexere Fälle

import java.util.Scanner;

public class ScannerDemo {

    public void scannerIntValDemo1() {
        Scanner scanner = new Scanner(System.in);
        String stopToken = null;
        System.out.println("===== Demo Ganzzahl-Eingabe =====");
        boolean repeat = true;
        while (repeat) {
            System.out.print("  Ganzzahl (Abbruch, wenn ungültige Eingabe): ");
            if (scanner.hasNextInt()) {
                int intVal = scanner.nextInt();
                String negPosInfo = ">=0";
                if (intVal < 0) {
                    negPosInfo = "negativ";
                }
                System.out.println("    Eingegeben: " + intVal + " (" + negPosInfo + ")");
            } else {
                stopToken = scanner.next();
                repeat = false;
            }
        }
        System.out.println("    ## Beendet durch \"" + stopToken + "\"");
    }
}