Schön anonym

Extreme Java: Wie anonyme innere Klassen dabei helfen, Tupel in Java 8 Streams zu erstellen

Dr. Heinz Kabutz

© Shutterstock/ StunningArt

Tupel können in Java 8 Streams sehr effizient mithilfe von anonymen inneren Klassen erstellt werden. In diesem Artikel werden wir uns ansehen, wie beim Refactoring alten prozeduralen Codes in das Paradigma der funktionalen Programmierung hilft.

Lebenslanges Sitzen ist ein Kreuz, das jeder Entwickler in der einen oder anderen Form zu tragen hat. Mein ehemaliger Chef hat mir daher empfohlen, mir einen sogenannten Walkdesk zu besorgen. Der vorliegende Artikel wurde also von mir verfasst, während ich über 2,5 Stunden einen 4,5 Kilometer langen Spaziergang machte. Das ist nicht besonders schnell, nicht einmal für mich, aber ich habe es mit meinem 14 km langen Powerwalk am Schreibtisch gestern übertrieben – meine Knie sind daher noch ein wenig weich. Ich bin es einfach nicht mehr gewohnt, so viel zu laufen, und zu laufen während ich schreibe, ist noch ein wenig neu für mich. Was ich allerdings sagen kann ist, dass mein Rücken sich viel besser anfühlt und ich schneller einschlafe.

Der Grund dafür, dass diese Art von Walkdesks für Entwickler so gut funktioniert, ist, dass wir beim Schreiben von Code in eine Art Trance fallen, bei der wir nichts mehr um uns herum mitbekommen. Es ist mit mittlerweile schon ein paar Mal passiert, dass ich den Tunnelblick bekam und, ohne es zu merken, 5 Kilometer zurücklegte.

Treffen Sie Dr. Heinz Kabutz auf dem Extreme Java Camp!
Das Extreme Java Camp besteht aus zwei Intensivseminaren, die umfassendes und aktuellstes Know-how zu fortgeschrittenen Java-Themen und zu Java Concurrency Performance vermitteln. Es ist ein einzigartiges Hands-on-Training mit hervorragend strukturierten Vorträgen und einer Fülle an praktischen Übungen. Auch die erfahrensten Java-Profis werden intensiv angeregt und gefordert.

Weitere Infos gibt es auf der der Webseite des Extreme Java Camps!

Anonyme Tupel

Ich versuche, jeden Tag etwas Neues zu lernen. Nachdem ich heute Morgen meine Kinder zur Schule gebracht hatte, habe ich im Auto das Hörbuch „Why We Sleep“ von Matthew Walker gehört. Nach Walker wäre es eigentlich eine gute Idee, zwischen zwei Unterrichtsstunden zu schlafen. Auf diese Weise kann unser Gehirn die gerade erhaltenen Daten besser verarbeiten. Während meiner Zeit an der Universität habe ich versucht, Seminare und Schlaf miteinander zu verbinden. Geklappt hat das allerdings nicht so gut, wie ich zugeben muss, auch wenn Walker mir in seinem Buch etwas Anderes suggeriert.

Und auch wenn ich dennoch jeden Tag etwas dazulerne, ist es meist nichts, das direkt die Programmiersprache Java betrifft. Natürlich gibt es Hunderte Java Frameworks, die ich nicht kenne, allerdings dachte ich schon, dass ich über die Sprache selbst eigentlich alles wüsste, was es sich lohnt zu wissen.

Stellen Sie sich meine Überraschung vor, als ich Henri Tremblay bei der Oracle Code One zuhörte und etwas sah, das ich noch nicht kannte. Etwas, das bereits seit Java 1.1 funktioniert, völlig ohne mein Wissen. Wussten Sie, dass der in Listing 1 dargestellte Code völlig korrekt und zulässig ist?

public class AnonymousClass {
  public static void main(String[] args) {
    new Object() {
      private void test() {
        System.out.println("anonymous test");
      }
    }.test();
  }
}

Sofort habe ich eine alte VMWare-Instanz mit Windows XP und einer Kopie vom JDK 1.1.8 gestartet, um es zu überprüfen. Komischerweise kompilierte der Code nicht im JDK 1.1, stattdessen gab es folgende Fehlermeldung:

AnonymousClass.java:7: Method test() not found in class java.lang.Object.
  }.test();
    ^
1 error

In JDK 1.2 kompilierte der Sourcecode schließlich, und ich konnte sogar als Target Version 1.1 setzen. Daher würde ich vermuten, dass eher ein Bug im Compiler des JDK 1.1 die Kompilierung verhinderte, als eine wohlüberlegte Restriktion der Sprache selbst. Sollte ich nun einen Bug loggen? Der in Listing 2 dargestellte Code würde jedenfalls in keiner Java-Version funktionieren.

public class AnonymousClassBroken {
  public static void main(String[] args) {
    Object obj = new Object() {
      private void test() {
        System.out.println("anonymous test");
      }
    };
    obj.test(); // <-- cannot find symbol
  }
}

Mit der Veröffentlichung von Java 10 können wir das neue Konstrukt var nutzen, um anonyme Objekte wie in Listing 3 dargestellt zu behandeln.

public class AnonymousClassVar {
  public static void main(String... args) {
    var obj = new Object() {
      private void test() {
        System.out.println("anonymous test");
      }
    };
    obj.test(); // works!
  }
}

Eine dynamische Typisierung ist das nicht, denn Java verifiziert während der statischen Übersetzungszeit (Compile Time) noch immer, dass die Methode test() existiert. Durch die Verwendung anonymer innerer Klassen hat Henri Tremblay diesen Trick wunderbar genutzt, um die Idee von Tupel in Java zu simulieren. Wir werden dies nun anhand des Refactorings des Apache OfBiz-Projekts illustrieren. Die Methode, die wir verbessern wollten, liegt in der Klasse ModelReader (Listing 4).

public Map<String, TreeSet> getEntitiesByPackage(
    Set packageFilterSet, Set entityFilterSet) {
  Map<String, TreeSet> entitiesByPackage = new HashMap<>();

  //put the entityNames TreeSets in a HashMap by packageName
  Iterator ecIter = this.getEntityNames().iterator();
  while (ecIter.hasNext()) {
    String entityName = ecIter.next();
    ModelEntity entity = this.getModelEntity(entityName);
    String packageName = entity.getPackageName();

    if (UtilValidate.isNotEmpty(packageFilterSet)) {
      // does it match any of these?
      boolean foundMatch = false;
      for (String packageFilter : packageFilterSet) {
        if (packageName.contains(packageFilter)) {
          foundMatch = true;
        }
      }
      if (!foundMatch) {
        continue;
      }
    }
    if (UtilValidate.isNotEmpty(entityFilterSet)
      && !entityFilterSet.contains(entityName)) {
      continue;
    }

    TreeSet entities = entitiesByPackage.get(packageName);
    if (entities == null) {
      entities = new TreeSet<>();
      entitiesByPackage.put(packageName, entities);
    }
    entities.add(entityName);
  }
  return entitiesByPackage;
}

Bitte verurteilt mich nicht – Ich habe diesen Code nicht geschrieben! Vermutlich stammt er aus der Feder vieler Entwickler, die daran über einen langen Zeitraum gewerkelt haben. Wenn Sie glauben, es besser zu können – nun, die Leute von OfBiz würden sich sicher über Hilfe bei ihrem Projekt freuen 😉

Schrittweises Refactoring

Normalerweise würde mein Refactoring dieses Codes vermutlich wie in Listing 5 gezeigt aussehen (Details zu Filtern lassen wir vorerst einmal außen vor).

public Map<String, TreeSet> getEntitiesByPackage(
    Set packageFilterSet, Set entityFilterSet) {
  return this.getEntityNames().stream()
    .map(this::getModelEntity) // Stream
    .filter(packageFilter) // Stream
    .filter(entityFilter) // Stream
    .collect(Collectors.groupingBy(ModelEntity::getPackageName,
      Collectors.mapping(ModelEntity::getEntityName,
        Collectors.toCollection(TreeSet::new))));
}

Ein paar Hinweise zum Refactoring: Wir starten mit den Namen von this.getEntityNames() als Strings. Dann suchen wir uns das entsprechende ModelEntity-Objekt. Von dort aus geht es in unserem Code zurück zum Namen, indem wir getEntityName() aufrufen. Was aber, wenn wir das nicht könnten? Wie können wir sowohl den Original-String als auch die ModelEntity behalten? Darum geht es im Kern im vorliegenden Artikel.

Lassen Sie uns gemeinsam den Code schrittweise einem Refactoring unterziehen. Zuerst sollten wir die continue-Statements loswerden und die if-Statements neu zuordnen.

 
// does it match any of these?
boolean foundMatch = false;
for (String packageFilter : packageFilterSet) {
  if (packageName.contains(packageFilter)) {
    foundMatch = true;
  }
}

Der Kommentar weist bereits darauf hin, dass man den Code wie folgt umschreiben könnte:

 
boolean foundMatch = packageFilterSet.stream()
  .anyMatch(packageName::contains);

Anschließend können wir die if/continue-Logik invertieren. Statt den in Listing 6 dargestellten Code zu verwenden, sollten wir ihn eher wie folgt schreiben:

if ((UtilValidate.isEmpty(packageFilterSet) ||
      packageFilterSet.stream().anyMatch(packageName::contains))
    && (UtilValidate.isEmpty(entityFilterSet) ||
      entityFilterSet.contains(entityName))) {
  TreeSet entities = entitiesByPackage.get(packageName);
  // ...
if (UtilValidate.isNotEmpty(packageFilterSet)) {
  boolean foundMatch = packageFilterSet.stream()
    .anyMatch(packageName::contains);
  if (!foundMatch) {
    continue;
  }
}
if (UtilValidate.isNotEmpty(entityFilterSet) &&
    !entityFilterSet.contains(entityName)) {
  continue;
}

TreeSet entities = entitiesByPackage.get(packageName);
// ...

In Listing 7 zeigt sich, wie wir die Filter in die Methoden implementieren. Die in Listing 8 gezeigte getEntitiesByPackage()-Methode wäre das Ergebnis davon.

private boolean matchesPackageFilter(
    Set packageFilterSet, String packageName) {
  return UtilValidate.isEmpty(packageFilterSet) ||
      packageFilterSet.stream()
        .anyMatch(packageName::contains);
}

private boolean matchesEntityFilter(
    Set entityFilterSet, String entityName) {
  return UtilValidate.isEmpty(entityFilterSet) ||
      entityFilterSet.contains(entityName);
}
public Map<String, TreeSet> getEntitiesByPackage(
    Set packageFilterSet, Set entityFilterSet) {
  Map<String, TreeSet> entitiesByPackage = new HashMap<>();

  //put the entityNames TreeSets in a HashMap by packageName
  Iterator ecIter = this.getEntityNames().iterator();
  while (ecIter.hasNext()) {
    String entityName = ecIter.next();
    ModelEntity entity = this.getModelEntity(entityName);
    String packageName = entity.getPackageName();

    if (matchesPackageFilter(packageFilterSet, packageName)
        && matchesEntityFilter(entityFilterSet, entityName)) {
      TreeSet entities = entitiesByPackage.get(packageName);
      if (entities == null) {
        entities = new TreeSet<>();
        entitiesByPackage.put(packageName, entities);
      }
      entities.add(entityName);
    }
  }
  return entitiesByPackage;
}

Wir sind nun gut aufgestellt, um den Code einem Refactoring zu unterziehen, sodass Java 8 Streams verwendet werden (wie ursprünglich geplant). Der einzige Haken an der Sache ist, dass wir innerhalb der Filter und der finalen Gruppierung sowohl entityName als auch packageName verwenden. In unserem Fall können wir die benötigten Informationen von der Klasse EntityModel erhalten, aber es gibt möglicherweise Fälle, in denen das nicht so ist. Genau hier setzen wir nun schließlich Henri Tremplays Idee von der Nutzung anonymer innerer Klassen als Tupel ein (Listing 9).

public Map<String, TreeSet> getEntitiesByPackage(
    Set packageFilterSet, Set entityFilterSet) {
  return this.getEntityNames().stream()
    .map(_entityName -> new Object() { // anonymous inner class
      String entityName = _entityName; // need to redeclare it
      ModelEntity entity = getModelEntity(_entityName);
      String packageName = entity.getPackageName();
    })
    .filter(tuple -> matchesPackageFilter(
      packageFilterSet, tuple.packageName))
    .filter(tuple -> matchesEntityFilter(
      entityFilterSet, tuple.entityName))
    .collect(Collectors.groupingBy(tuple -> tuple.packageName, Collectors.mapping(tuple -> tuple.entityName, Collectors.toCollection(TreeSet::new))));
}

ModelEntity enthält sowohl die getEntityName()– als auch die getPackageName()-Methode. Wir können somit von ModelEntity mappen und die benötigten Informationen der Entität erhalten. Allerdings kann es auch hier vorkommen, dass dies nicht möglich ist. Ohne die Tupel sähe der Code wie in Listing 10 aus.

public Map<String, TreeSet> getEntitiesByPackage(
    Set packageFilterSet, Set entityFilterSet) {
  Map<String, TreeSet> entitiesByPackage = new HashMap<>();

  //put the entityNames TreeSets in a HashMap by packageName
  Iterator ecIter = this.getEntityNames().iterator();
  while (ecIter.hasNext()) {
    String entityName = ecIter.next();
    ModelEntity entity = this.getModelEntity(entityName);
    String packageName = entity.getPackageName();

    if (matchesPackageFilter(packageFilterSet, packageName)
        && matchesEntityFilter(entityFilterSet, entityName)) {
      TreeSet entities = entitiesByPackage.get(packageName);
      if (entities == null) {
        entities = new TreeSet<>();
        entitiesByPackage.put(packageName, entities);
      }
      entities.add(entityName);
    }
  }
  return entitiesByPackage;
}

Kommen wir zurück zum vorherigen Refactoring mit der anonymen inneren Klasse: Vor Java 10 wäre es uns nicht möglich gewesen, eine lokale Variable des Tupel-Streams zu deklarieren. Dies geht nun dank var (Listing 11).

public Map<String, TreeSet> getEntitiesByPackage(
    Set packageFilterSet, Set entityFilterSet) {
  var tuples = this.getEntityNames().stream()
    .map(_entityName -> new Object() {
      String entityName = _entityName;
      ModelEntity entity = getModelEntity(_entityName);
      String packageName = entity.getPackageName();
    });
  var filteredTuples = tuples
    .filter(tuple -> matchesPackageFilter(
      packageFilterSet, tuple.packageName))
    .filter(tuple -> matchesEntityFilter(
      entityFilterSet, tuple.entityName));
  return filteredTuples
    .collect(Collectors.groupingBy(tuple -> tuple.packageName,
      Collectors.mapping(tuple -> tuple.entityName,
        Collectors.toCollection(TreeSet::new))));
}

Henri hat noch andere Tricks gezeigt, und ich empfehle jedem, sich seine exzellente Session anzusehen. Ich selbst habe mir den Talk nicht komplett live angesehen, denn ich schaue mir technische Talks gerne in doppelter Geschwindigkeit an.

Sollten Sie die OfBiz-Methode selbst einmal einem Refactoring unterziehen wollen, bedenken Sie bitte, dass es ein Problem mit den Checked Exceptions der Lambdas gibt, das ich in diesem Artikel nicht behandelt habe.

Verwandte Themen:

Geschrieben von
Dr. Heinz Kabutz
Dr. Heinz Kabutz
Dr. Heinz Kabutz ist regelmäßiger Referent auf allen wichtigen Java-Konferenzen mit den Schwerpunkten Java Concurrency und Performance. Kabutz schreibt den beliebten „The Java Specialists’ Newsletter“, der von Zehntausenden von begeisterten Fans in über 140 Ländern gelesen wird.
Kommentare

Hinterlasse einen Kommentar

1 Kommentar auf "Extreme Java: Wie anonyme innere Klassen dabei helfen, Tupel in Java 8 Streams zu erstellen"

avatar
4000
  Subscribe  
Benachrichtige mich zu:
Wasgeht Siedasan
Gast

Interessant dass das geht aber mE nicht praktikabel da sehr unleserlich.