Vererbung in Java austricksen mit Manifold: Structural Interfaces

Zuvor hatten wir bereits über Manifold berichtet. Dabei ging es in einem kurzen Artikel um die Grundlagen von Manifold. Anschließend hatten wir gezeigt, wie sich mit relativ wenig Aufwand JSON-Schnittstellen über Manifold integrieren lassen. Außerdem ging es darum, wie man fremde Klassen erweitern kann. Heute zeigen wir, wie man mit Manifold fremde Klassen nachträglich von einem neuen Interface erben lassen kann, sofern sie die passenden Methoden haben. Dieses Konzept wird auch Structural Interface genannt.

Java ist eine sogenannte nominal-typisierte Programmiersprache. Das bedeutet, dass die Typen normalerweise explizit angegeben werden müssen. Dazu ein Beispiel.

Angenommen, wir hätten folgende triviale Klasse Customer:

public class Customer {
    private long id;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }
}

Ein Customer-Objekt hat eine ID, die es eindeutig identifiziert. Diese ID kann über die Methode getId() abgerufen werden.

Angenommen, wir hätten außerdem das folgende Interface:

public interface HasId {
    long getId();
}

Wie wir sehen, spezifiziert das Interface HasId die Methode getId(), welche ein Objekt vom Datentyp long zurückgeben soll. Unsere Customer-Klasse hat zwar diese vorgegebene Methode, implementiert aber nicht das Interface HasId.

Somit ließe sich der folgende Java-Code zwar kompilieren, dies würde aber beim Ausführen eine ClassCastException auslösen:

Customer c = new Customer();
c.setId(1);
HasId withId = (HasId)c;
System.out.println("CustomerId: " + withId.getId());

Dies liegt daran, dass die Klasse Customer zwar die Methode hat, aber eben nicht das Interface erbt. Da Java nur nominale Typen unterstützt, versteht der Interpreter an dieser Stelle nicht, dass die getId()-Methode des Customer-Objektes auf die getId()-Methode des HasId-Interfaces zu mappen wäre. Hier unterscheiden sich nominal typisierte von strukturell typisierten Programmiersprachen.

Der Laufzeitfehler lässt sich mit Manifold beheben. Dazu wird das HasId-Interface mit @Structural annotiert:

@Structural
public interface HasId {
    long getId();
}

Und siehe da, das Resultat:

CustomerId: 1

Durch die @Structural-Annotation wird für das Interface HasId eine strukturelle Typisierung emuliert und die fehlende Vererbung ignoriert. Beim Aufruf der getId()-Methode auf das Interface wird automatisch die korrekte getId()-Methode des Objektes im Hintergrund aufgerufen. Natürlich bleibt Java nach wie vor nominal typisiert, effektiv wird im Hintergrund die strukturelle Typisierung nominal durch den Aufruf statischer Factorymethoden abgebildet.

Dadurch können beispielsweise fremde Klassen einfach nachträglich von einem eigenen Interface erben, ohne dass die betroffene Programmbibliothek manuell gepatcht werden müsste oder aufwendige Converter in statischen Utililty-Klassen programmiert werden müssten.

Short URL for this post: https://wp.me/p4nxik-3kM
Steffen Jacobs

About Steffen Jacobs

Java Consultant & Developer at Orientation in Objects GmbH. Follow me on Twitter and find me on LinkedIn and Xing. Some of the source code associated with the blog articles can be found on GitHub.
This entry was posted in Java and Quality and tagged , , , . Bookmark the permalink.

Leave a Reply