Java : Lambda Ausdrücke

Die Lambdas oder auch Closures genannt, sind mit der Version 8 von Oracle Java dazu gekommen. Und bieten dem Entwickler einen zusätzlichen Funktionsumfang, womit dieser noch weniger Code in noch kürzerer Zeit schreiben kann. Ob jedoch dieser Quellcode lesbarer und performanter als vorher ist, schauen wir uns am Ende des Beitrages an.

Beispiel mit einer ForEach Schleife

Hier nun ein paar kleine Beispiele anhand einer einfachen ForEach Schleife.

Gegeben ist immer folgende Liste:

 protected static List<String> names = new ArrayList<>();

  static {
       names = Arrays.asList(new String[] { 
                  "Stefan", "Udo", "Andrea", 
                  "Melanie", "Luca", "Elena", 
                  "Mia", "Hannah", "Florian", 
                  "Thomas", "Samuel", "Alexander",
                  "Daniel","Udo" });
  }

Vor der Version 5 von Java musste die Schleife wie folgt aussehen:

@Test
public void simpleForEachBeforeJava5() {    
   for (int i = 0; i < names.size(); i++) {
     logger.info(names.get(i));
   }
}

Ab der Version 5 konnte man dieses mit der erweiterten ForEach Schleife deutlich kürzer schreiben:

@Test
public void simpleForEachAfterJava5() {
   for (String name : names) {
     logger.info(name);
   }
}

Seit der Einführung der Lambdas könnte der Ausdruck wie folgt aussehen:

@Test
public void simpleForEachLambda() {
   names.forEach(name->{
     logger.info(name);
   });
}

Performancevergleich

Wie performant ist das ganze nun im Vergleich zu einem „normalen“ Ausdruck?
Im nachfolgenden möchte ich nun die Performance mit einem einfachen Mittel messen, ich nehme vor dem Starten der Methode einen Timestamp und danach einen Timestamp, der Rest ist simple Mathematik.

MethodeDauer*
 simpleForEachBeforeJava5() 8,6ms.
 simpleForEachAfterJava5() 1,4ms.
 simpleForEachLambda() 36,6ms.

*Durchschnittswert nach 10 Durchläufen.

Neue Operationen an Listen

Neben der Methode „forEach“ existieren noch weitere Operationen, dabei unterscheidet man zwischen einer „intermediate operation“ und einer „terminal operation“.

Intermediate Operation

Eine „intermediate operation“ hat immer einen Rückgabewert in Form eines modifizierten Streams auf diesen können dann weitere Operationen ausgeführt werden.

filter

Mit der Methode „filter“ kann man wie es sich erahnen lässt einen Stream nach einem bestimmten Kriterum (Predicate) filtern.
Da die Methode „filter“ einen manipulierten Stream zurückliefert kann man nun mit der Methode „findAny“ ein Optional zurückliefern. Wenn die Methode jedoch nichts findet, so wird kein NULL zurückgegeben, sondern ein Optional jedoch ohne Value. D.h. entweder man prüft am Optional ob dieses einen Wert hat oder aber man liefert ein NULL zurück.

Folgender Test liefert immer ein Optional zurück.

@Test
public void filterForStringPositiv() {
  Optional<String> name = names.stream().filter(n -> "Stefan".equalsIgnoreCase(n)).findAny();
  assertTrue(name.isPresent());
  logger.info(name.get());
}

Im nun folgenden Test wird nach einem Namen gesucht, welcher nicht in der Liste enthalten ist.
Im „echten“ Leben kennt man jedoch die Liste nicht, daher wird dieses programmatisch wie folgt gelöst:

@Test
public void filterForStringNegativNull() {
  String name = names.stream().filter(n -> "Robin".equalsIgnoreCase(n)).findAny().orElse(null);
  assertNull(name);
  logger.info(name);
}

Mit der Methode „orElse“ am Ende des Ausdrucks wird sichergestellt, dass, wenn kein Treffer gefunden wurde, NULL zurückgegeben wird.
Hier könnte auch ein beliebig anderes Objekt zurückgeliefert werden.

@Test
public void filterForStringNegativNull() {
  String name = names.stream().filter(n -> "Robin".equalsIgnoreCase(n)).findAny().orElse("Melanie");
  assertNotNull(name);
  logger.info(name);
}
Mehrfachprüfung

Möchte man in einem Ausdruck mehr als eine Prüfung durchführen, so erweitert man den Ausdruck, um eine geschweifte Klammer um darin die Prüfungen durchzuführen. Als Rückgabewert wird ein Boolean erwartet, „true“ wenn der Ausdruck zutrifft und „false“ wenn nicht.

@Test
public void filterForStringPositivMultiple() {
  Optional<String> name = names.stream().filter(n -> {
    if(n.equalsIgnoreCase("Stefan") && n.length()>5) {
      return true;
    }
    return false;
  }).findAny();
  assertTrue(name.isPresent());
  logger.info(name.get());
}

map

Mit der Methode „map“ kann man aus einem Ergebnis der filter Methode oder aber aus der gesamten Liste (mit „stream“) ein neues Objekt erstellen und zurückliefern.

Es soll nun für jeden Namen aus der Liste ein Objekt Person erstellt werden:

private class Person{
    
  private String name;

  public Person(String name) {
    super();
    this.name = name;
  }

  public String getName() { return name; }

  public void setName(String name) { this.name = name; }   
}

Bevor es die Methode map gab, wurde dieses wie folgt gelöst:

@Test
public void testMappingBeforeJava8() {
  logger.info("testMappingBeforeJava8()");
  List<Person> personen = new ArrayList<>();
  for(String name: names) {
    personen.add(new Person(name));
  }
  assertFalse(personen.isEmpty());
}

Mit der Methode map ist daraus nun ein einzeiliger Ausdruck geworden, welcher wie folgt, aussieht:

@Test
public void testMappingWithJava8Map() {
  logger.info("testMappingWithJava8Map()");
  List<Person> personen = names.stream().map(name -> new Person(name)).collect(Collectors.toList());
  assertFalse(personen.isEmpty());
}

sorted

Die Methode „sorted“ gibt einem die Möglichkeit eine Collection oder Map nach einem oder mehrere Kriterien zu sortieren. Maps können auch nach dem Key oder Value sortiert werden.
Wollte man „damals“ eine einfache Liste mit String Werten sortieren, so brauchte man nur

@Test
public void testSortedBeforeJava8() {
  logger.info("sortierte Ausgabe");
  Collections.sort(names);
  for(String name:names) {
    logger.info(name);
  } 
}

Ein String ist hier am einfachsten zu sortieren, da der Comparator welcher für die Methode „sort“ herangezogen wird die Methode toString() aufruft.

Seit Java8 kann man dieses nun eleganter schreiben.

@Test
public void testSortedWithJava8() {
  logger.info("unsortierte Ausgabe");
  names.stream().forEach(name -> logger.info(name));

  List<String> sortedNames = names.stream().sorted().collect(Collectors.toList());
  assertFalse(sortedNames.isEmpty());
  logger.info("sortierte Ausgabe");
  sortedNames.stream().forEach(name -> logger.info(name));
}

Es ist nun auch wieder ein einzeiliger Ausdruck geworden. Des Weiteren haben wir die ursprüngliche Liste nicht geändert und somit eine neue Liste mit Werten erzeugt.

Eine Liste ist jedoch ein sehr einfaches Beispiel, wie das mit einer Map aussieht, möchte ich nun an einem kleinen Beispiel erläutern:

@Test
public void testSortedMapWithJava8() {
  final String formatPattern = "Key: [%s] \t Value: [%d]";
  
  //Map mit den Beispielwerten erzeugen.
  final Map<String,Integer> personen = new HashMap<>();
  names.stream().forEach(name->{
    personen.put(name, getRandomAge());
  });
  logger.info("unsortierte Ausgabe");
  personen.entrySet().stream().forEach(entry ->{
    logger.info(String.format(formatPattern, entry.getKey(),entry.getValue()));
  });
    
  //Sortieren der Map nach entry.getValue()
  Map<String,Integer> sortedbyValue = new LinkedHashMap<>();
  personen.entrySet().stream().sorted(Map.Entry.<String,Integer>comparingByValue()).forEachOrdered(person -> sortedbyValue.put(person.getKey(), person.getValue()));
  logger.info("sortierte Ausgabe - (sortedByValue)");
  sortedbyValue.entrySet().stream().forEach(entry ->{
    logger.info(String.format(formatPattern, entry.getKey(),entry.getValue()));
  });

  //Sortieren der Map nach entry.getKey()
  Map<String,Integer> sortedByKey = new LinkedHashMap<>();
  personen.entrySet().stream().sorted(Map.Entry.<String,Integer>comparingByKey()).forEachOrdered(person -> sortedByKey.put(person.getKey(), person.getValue()));
  logger.info("sortierte Ausgabe - (sortedByKey)");
  sortedByKey.entrySet().stream().forEach(entry ->{
    logger.info(String.format(formatPattern, entry.getKey(),entry.getValue()));
  });    
}

Terminal Operation

Eine Terminal-Operation steht immer am Ende der Pipeline und erzeugt bzw. liefert einen Wert. Wir haben bereits  sollche Terminal Operations im ersten Kapitel verwendet Bsp. collect(Collectors.toList());“ oder auch „findAny();“.

collect

Mit der Methode „collect“ wird das Ergebnis einer Intermediate Operation gesammelt und Bsp. in eine java.util.Collection geschrieben.

@Test
public void testCollectwithJava8() {
  List<String> filteredNames = names.stream().filter(n -> { return n.equalsIgnoreCase("Stefan") && n.length()>5;}).collect(Collectors.toList());
  filteredNames.stream().forEach(name -> logger.info(name));
}

Es gibt auch die Möglichkeit die Werte zu einer Zeichenkette zusammen zuführen, dieses ist ein beliebtes Beispiel denn in einer For Schleife müsste man zuletzt immer prüfen ob der Zähler sich am Ende befindet und dann das Trennzeichen nicht anzeigen.

@Test
public void testCollectwithJavaJoin() {
  StringBuilder builder = new StringBuilder();
  for (int i = 0; i < names.size(); i++) {
    builder.append(names.get(i));
    if ((i + 1) < names.size()) {
      builder.append(",");
    }
  }
  logger.info(builder.toString());
}

Dieses wird nun deutlich vereinfacht:

@Test
public void testCollectwithJava8Join() {
  String filteredNames = names.stream().filter(n -> {return n.length() > 5;}).collect(Collectors.joining(","));
  logger.info(filteredNames);
}

Hier hat Google eine sehr gute Bibliothek erschaffen welche das noch mehr vereinfacht, zu finden ist diese im Maven Repository com.google.guava

findFirst

Wie die Bezeichnung es vermuten lässt, gibt die Methode findFirst den ersten Wert zurück. Als Rückgabe Wert erhält man ein Optional.

Gegeben sei also folgende java.util.List:

 protected static List<String> names = new ArrayList<>();

  static {
    names.add("Stefan");
    names.add("Udo");
    names.add("Andrea");
    names.add("Melanie");   
    names.add("Luca");   
    names.add("Elena");   
    names.add("Mia");   
    names.add("Hannah");   
    names.add("Florian");   
    names.add("Thomas");   
    names.add("Samuel");   
    names.add("Alexander");   
    names.add("Daniel");
    names.add("Udo");   
  }

Der Name „Udo“ ist hier 2-mal vorhanden, wir möchten jedoch nur den ersten Wert erhalten.

Mit der Methode FindFirst würde dieses wie folgt gelöst werden:

@Test
public void testFindFirstJava8() {
  Optional<String> name = names.stream().filter(n -> {
      return n.equalsIgnoreCase("Udo");
    }).findFirst();
  logger.info(name.get());
}

Mit den „Boardmitteln“ von Java würde dieses nicht viel weniger Quellcode werden.

@Test
public void testFindFirstJava() {
  for (String name : names) {
    if (name.equalsIgnoreCase("Udo")) {
      logger.info(name);
      break;
    }
  }
}

findAny

Die Methode findAny liefert in einem NICHT parallelen Stream den ersten Eintrag aus der Liste. Dieses ist jedoch nicht garantiert.

Mit Java8 wird dieses wiefolgt gelöst:

@Test
public void testFindAnyWithJava8() {
  Optional<Person> optional = persons.stream().findAny();
  assertTrue(optional.isPresent());
  logger.info(optional.get().toString());
}

Wen der Stream leer ist (nicht NULL) dann liefert diese Methode ein „null“ zurück d.h. man muss einen NULL Check auf das erwartete Optional durchführen.

@Test
public void testFindAnyWithJava8EmptyList() {
  List<Person> nullList = new ArrayList<>();
  Optional<Person> optional = nullList.stream().findAny();
  assertFalse(optional == null);
  logger.info("Der Wert ist NULL!");
}

Oder man „hängt“ hier wieder die Methode „orElse“ an das Ergebnis um ein definiertes Objekt zu erhalten.

@Test
public void testFindAnyWithJava8orElse() {
  List<Person> nullList = new ArrayList<>();
  Person person = nullList.stream().findAny().orElse(new Person("Steve","Jobs"));
  assertTrue(person.getFirstname().equalsIgnoreCase("Steve"));
  logger.info(person.toString());
}

min & max

Mit den Methoden min & max kann man den kleinsten bzw. den größtmöglichen Zahlenwert aus einem Stream ermitteln.

Gegeben sei also folgende Liste mit ganzen Zahlen:

private List<Integer> numbers = Arrays.asList(new Integer[] {10,6,1,0,4,5,3,15,2});

Die Methode „min“ erwartet einen Comparator welcher den Vergleich der Werte vornimmt.

private final Comparator<Integer> comparator = (num1, num2) -> Integer.compare(num1, num2);

Nun suchen wir den kleinsten Wert:

@Test
public void testMinJava8() {
  Optional<Integer> minValue = numbers.stream().min(comparator);
  assertTrue(0 == minValue.get());
}

Das Ermitteln des maximalen Werts erfolgt ähnlich:

@Test
public void testMaxJava8() {
  Optional<Integer> maxValue = numbers.stream().max(comparator);
  assertTrue(15 == maxValue.get());
}

Wollte man in einer Liste den kleinsten bzw. größten Wert ohne die Verwendung von Lambda suchen, so konnte man dieses wie folgt lösen:

@Test
public void testMinJavaBefore8() {
  int index = numbers.indexOf(Collections.min(numbers));
  Integer number = numbers.get(index);
  assertTrue(0 == number);
}
  
@Test
public void testMaxJavaBefore8() {
  int index = numbers.indexOf(Collections.max(numbers));
  Integer number = numbers.get(index);
  assertTrue(15 == number);
}

Download

Das gesamte Eclipse Projekt mit allen JUnit Testfällen gibt es hier zum Herunterladen:

Kommentar hinterlassen

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