Innovative Sprachfunktionen für Java – Teil 2

Manifold: Erweiterungsmethoden für Java

Scott McKinney

© Shutterstock / gonin

Manifold ist eine einzigartige Open-Source-Technologie, die man in jedem Java-Projekt verwenden kann, um innovative Sprachfunktionen wie typsichere Metaprogrammierung, Erweiterungsmethoden, Templating und strukturelle Typisierung nutzen zu können. Im zweiten Teil unserer Artikelserie behandelt Scott McKinney sogenannte Extension Classes, ein innovatives Feature, mit dem man eine Klasse mit eigenen Methoden, Interfaces und weiteren Features ergänzen kann.

In der vorliegenden Artikelserie geht es um Manifold, eine brandneue Technologie zur Erweiterung von Java. In Teil 1 wurde das Type Manifold API vorgestellt, eine leistungsstarke Alternative zur gewöhnlichen Code-Generierung. In diesem Teil geht es um die sog. Extension Classes (Erweiterungsklassen), ein innovatives Feature, durch das eine Klasse mit eigenen Methoden, Interfaces und anderen Features ergänzt werden kann, ohne auf Unterklassen zurückgreifen oder Änderungen an der Originalklasse vornehmen zu müssen.

Erweiterungsmethoden für Java?!

Hopp! Schreib etwas Code, um die Inhalte von File in einem String abzubilden. Auf die Plätze, fertig, los!

Als pragmatischer Entwickler hoffst du, dass ungefähr das hier dabei rauskommt:

String contents = file.readText();

Bedauerlicherweise tippt man schnell mal file. in die IDE und stellt natürlich fest, dass eine solche Methode nicht existiert. Als nächstes sucht man dann bei StackOverflow nach einer Boilerplate-Lösung und findet ein nützliches Snippet. Da man es sich selbst und anderen ersparen will, diesen Aufwand mehrfach zu haben, packt man das Snippet in eine Util-Bibliothek:

public class MyFileUtil {
  public static String readText(File file) {
    // boilerplate code...
  }
} 

Jetzt kann man schreiben:

String contents = MyFileUtil.readText(file);

Ist das schon alles? Eigentlich verdienen wir doch ein freundlicheres und praktischeres File API – viel besser geeignet und leichter zu finden wäre readText() doch als Instanzmethode direkt auf der Methode File. An genau dieser Stelle setzt ein Feature an, das in der Community gemeinhin als Erweiterungsmethode (Extension Method) bekannt ist. Und hier führt uns Manifold weiter, wo Java schlapp macht.

Manifold implementiert Extension Methods für Java vollständig über sogenannte Extension Classes, also Erweiterungsklassen:

package extensions.java.io.File;
 
import manifold.ext.api.*;
 
@Extension
public class MyFileExtension {
  public static String readText(@This File thiz) {
    // boilerplate code...
  }
}

MyFileExtension ergänzt File mit readText() als Instanzmethode:

String contents = file.readText();

Das ist genau das, was der Pragmatiker in uns will! Die IntelliJ IDEA bietet darüber hinaus eine umfassende Unterstützung für Manifold via Plug-in. So kann man auf Features wie z.B. Code-Vervollständigung zurückgreifen, um ganz einfach Erweiterungsmethoden nutzen zu können:

Fig. 1: readText()

Fig. 1: readText()

Das folgende Video zeigt, wie man mit Manifold in IntelliJ IDEA Extension Classes erstellt und einem Refactoring unterzieht.

Die Anatomie einer Erweiterungsklasse

Erweiterungsklassen lassen sich mit relativ simplen Konventionen und Annotationen implementieren:

package extensions.java.io.File;

Der Name des Packages einer Erweiterungsklasse muss auf extensions. enden. Mit Java 8 könnte man alle Extension Classes im Package extension verankern. In Java 9 und höheren Versionen muss man, wenn man mit expliziten Modulen arbeitet, den Namen des Moduls voranstellen, etwa package foo.extensions.java.io.File – in diesem Beispiel ist foo das Modul. Um eindeutige Namen auch über mehrere Dependencys hinweg verwalten zu können, ist es in der Regel trotzdem ratsam, Erweiterungsklassen weitreichender zu qualifizieren.

@Extension
public class MyFileExtension {

Erweiterungsklassen müssen mit @Extension annotiert werden. Dies erleichtert es Manifold diese im eigenen Projekt schnell zu identifizieren.

public static String readText(@This File thiz) {

Alle Erweiterungsmethoden müssen darüber hinaus als „statisch“ deklariert sein – doch dazu später mehr. Als Empfänger eines Aufrufs, muss der erste Parameter einer Erweiterungsinstanzmethode vom gleichen Typ sein, wie die Erweiterungsklasse. In diesem Fall File. Der Name thiz ist in dem Zusammenhang zufällig gewählt, man kann jeden beliebigen dafür einsetzen. Für die gewöhnliche Nutzung von Erweiterungsklassen ist eigentlich nichts weiter nötig.

Statische Methoden

Statische Erweiterungsmethoden kann man folgendermaßen definieren:

@Extension
public static FileSystem getLocalFileSystem() {
  return FileSystems.getDefault();
}

Da statische Methoden über keinen Empfänger verfügen, muss die Methode selbst mit @Extension annotiert werden, sodass Manifold sie als solche identifizieren kann. Man kann sie dann so aufrufen, als wäre sie eine normale statische Methode namens File.

File.getLocalFileSystem()

Generics

Man kann Erweiterungen auch für generische Klassen erstellen und generische Erweiterungsmethoden definieren. Auf diese Weise arbeiten die Extension-Bibliotheken von Manifold mit Collections und anderen generischen Klassen zusammen. Im folgenden Beispiel ist first() die Erweiterungsklasse auf Iterable:

public static  T first(@This Iterable thiz, Predicate predicate) {
  for (T element: thiz) {
    if (predicate.test(element)) {
      return element;
    }
  }
  throw new NoSuchElementException();
}

Man sieht, dass die Erweiterung eine generische Methode mit der gleichen Typvariablenbezeichnung wie die erweiterte Klasse ist: T von Iterable. Da Erweiterungsmethoden statisch sind, überträgt man so Typvariablen aus der erweiterten Klasse an die Erweiterungsmethode.

Um eine generische Erweiterungsmethode zu definieren, hängt man die Typvariablen der Methode an die Liste der Typvariablen der erweiterten Klasse an. Die map()-Erweiterung von Manifold veranschaulicht dieses Format:

public static <E, R> Stream map(@This Collection thiz, Function<? super E, R> mapper) {
  return thiz.stream().map(mapper);
}

Hier ist map eine generische Erweiterungsmethode, die den Typ R hat und die Typvariable E der Collection überträgt.

W-JAX 2019 Java-Dossier für Software-Architekten

Kostenlos: 30+ Seiten Java-Wissen von Experten

Sie finden Artikel zu EnterpriseTales, Microservices, Req4Arcs, Java Core und Angular-Abenteuer von Experten wie Uwe Friedrichsen (codecentric AG), Arne Limburg (Open Knowledge), Manfred Steyer (SOFTWAREarchitekt.at) und vielen weiteren.

 

Statisches Dispatching

Eine Erweiterungsklasse ändert ihre erweiterte Klasse nicht im physischen Sinne; die in einer Extension definierten Methoden werden nicht wirklich in die erweiterte Klasse eingefügt. Stattdessen arbeiten der Java Compiler und Manifold zusammen, um einen Aufruf einer statischen Methode in einer Extension wie den Aufruf einer Instanzmethode in der erweiterten Klasse aussehen zu lassen. Daher werden Erweiterungsaufrufe statisch ausgeschickt. Somit wird im Gegensatz zu einem virtuellen Methodenaufruf der Aufruf einer Erweiterung immer basierend auf dem Kompilierzeit-Typ des Empfängers durchgeführt.

Eine weitere Folge des statischen Dispatchings ist, dass eine Erweiterungsmethode einen Aufruf empfangen kann, auch wenn der Wert des erweiterten Objekts an der Aufrufstelle null ist. Erweiterungsbibliotheken von Manifold nutzen diese Funktion, um die Lesbarkeit und null-Sicherheit zu verbessern. CharSequence.isNullOrEmpty() gleicht beispielsweise den Wert des Empfängers mit null ab, so dass man es nicht selbst machen muss:

public static boolean isNullOrEmpty(@This CharSequence thiz) {
  return thiz == null || thiz.length() == 0;
}
 
String name = null;
if (name.isNullOrEmpty()) {
  println("empty");
}

Hier prüft das Beispiel nicht auf null und verlagert die Last stattdessen auf die Erweiterung.

Zugänglichkeit und Umfang

Eine Erweiterungsmethode shadowt oder überschreibt niemals eine Class-Methode; wenn der Name und die Parameter einer Erweiterungsmethode mit einer Klassenmethode übereinstimmen, hat die Klassenmethode immer Vorrang vor der Extension. Zum Beispiel:

public class Tree {
  public void kind() {
    println("evergreen");
  }
}
 
public static void kind(@This Tree thiz) {
  println("binary");
}

Die Erweiterungsmethode gewinnt nie, ein Aufruf von kind() gibt immer "evergreen" aus. Zusätzlich warnt der Compiler vor dem Konflikt in der Erweiterungsklasse, wenn zur Kompilierungszeit Tree und die Erweiterung kollidieren.

Eine Erweiterungsmethode kann immer noch eine Klassenmethode überladen, wenn die Methodennamen identisch sind, es sich aber um unterschiedliche Parametertypen handelt:

public class Tree {
  public void harvest() {
    println("nuts");
  }
}
 
public static void harvest(@This Tree thiz, boolean all) {
  println(all ? "wood" : thiz.harvest());
}

Ein Aufruf von tree.harvest(true) gibt „wood“ aus.

Erweiterungsbibliotheken

Eine Erweiterungsbibliothek ist eine logische Gruppierung von Funktionen, die durch ein Set von Erweiterungsklassen definiert werden. Manifold enthält mehrere Erweiterungsbibliotheken für häufig verwendete Klassen, von denen viele auf Kotlin Extensions basieren. Jede Bibliothek ist als separates Modul oder JAR-Datei verfügbar, die je nach Bedarf separat zum Projekt hinzugefügt werden können.

Collections

Diese Bibliothek wird im Modul manifold-collections definiert und erweitert die folgenden Interfaces:

  • java.lang.Iterable
  • java.util.Collection
  • java.util.Collection
  • java.util.stream.Stream

Text

Diese Bibliothek wird im Modul manifold-text definiert und erweitert die folgenden Interfaces:

  • java.lang.CharSequence
  • java.lang.String

I/O

Diese Bibliothek wird im Modul manifold-io definiert und erweitert die folgenden Interfaces:

  • java.io.BufferedReader
  • java.io.File
  • java.io.InputStream
  • java.io.OutputStream
  • java.io.Reader
  • java.io.Writer

Web/Json

Diese Bibliothek wird im Modul manifold-json definiert und erweitert die folgenden Interfaces:

  • java.net.URL
  • javax.script.Bindings

Zusammenfassung

Erweiterungsmethoden bieten eine leistungsstarke Alternative zu Util-Bibliotheken. Das Feature wird in modernen Sprachen wie C#, Scala und Kotlin weitereichend unterstützt. Mit Manifold kann man nun Erweiterungsmethoden auch in Java verwenden.

Mit ihnen lässt sich die Produktivität von Entwicklern mit APIs steigern und man kann auf die vielen in Manifold eingebauten Erweiterungsbibliotheken auch in Standardklassen zurückgreifen.

Der einfachste und beste Weg, mit der Nutzung von Erweiterungsmethoden und den anderen Funktionen von Manifold zu beginnen, ist über das Manifold-Plug-in für IntelliJ IDEA.

Verwandte Themen:

Geschrieben von
Scott McKinney
Scott McKinney
Scott McKinney is the founder and principle engineer at Manifold Systems. Previously, he was a staff engineer at Guidewire where he designed and created Gosu. He currently pounds code by the truckload while listening to way too much retro synthwave.
Kommentare

Hinterlasse einen Kommentar

5 Kommentare auf "Manifold: Erweiterungsmethoden für Java"

avatar
4000
  Subscribe  
Benachrichtige mich zu:
DaniEll
Gast

Bei name == null muss name.isNullOrEmpty() zu einer NullPointerException führen.
Dieser Programmcode bedeutet (bisher) eindeutig „Führe isNullOrEmpty auf diesem konkreten name-Objekt aus“

Alles andere ist nicht nachvollziehbar und unerwartetes Verhalten ist eine nicht zu unterschätzende Fehlerquelle!

Scott
Gast

Es ist nicht jedermanns Sache, aber dieses Verhalten kann sehr nützlich sein. Die meisten modernen Sprachen wie Kotlin verwenden auch ein ähnliches Design:
https://kotlinlang.org/docs/reference/extensions.html#nullable-receiver

DaniEll
Gast

Danke für die Antwort, die Kotlin-Referenz und den interessanten Artikel.

Jost Schwider
Gast

Endlich kommt auch JAVA in den (abgespeckten) Genuss dieses mächten Konstrukts, das man in GROOVY schon seit über 10 Jahren nutzen kann. ;o)

TestP
Gast

Jau, extension methods ist ein Feature, das ich in Java vermisse. Das würde die Sprache wirklich an einigen Stellen glätten.