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.
Methode | Dauer* |
---|---|
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: