Performance beim Ermitteln von Elementen aus einer Liste

Gestern schrieb ich einen Artikel über die Möglichkeiten ein Element aus einer Liste unter Java zu ermitteln. Dort wurde unter anderem eine Lösung mittels der Stream-API aufgezeigt. In einem Kommentar zu dem Artikel kam die Frage nach der Performance dieser Methode auf. Aus diesem Grund habe ich einen kleinen Benchmark geschrieben, welcher die Unterschiede in der Performance ermitteln sollte. Der Testfall bestand daraus ein Element aus einer Liste zu ermitteln. Dazu wird eine Liste mit knapp 125.000 Elementen erzeugt. Nun wurde mit unterschiedlichen Methoden versucht das entsprechende Element zu ermitteln. Das gesuchte Element befindet sich in den Testfällen immer an der Position 75.004 der Liste. Erzeugt wird die Liste mit der Methode getElements:

private List<Element> getElements() {

	List<Element> elements = new ArrayList<>();

	// Add 75000 elements
	for (int i = 0; i < 75000; i++) {
		elements.add(new Element(String.valueOf(i), String.valueOf(i)));
	}

	elements.add(new Element("Suppe", "Löffel"));
	elements.add(new Element("Wasser", "Flüssigkeit"));
	elements.add(new Element("Käse", "Gelb"));
	elements.add(new Element("Huhn", "Ei"));

	// Add 50000 elements
	for (int i = 0; i < 50000; i++) {
		elements.add(new Element(String.valueOf(i), String.valueOf(i)));
	}

	return elements;
}

Vom Gefühl her hätte ich vermutet, das die Stream-API immer langsamer ist als die klassische Iteration durch die Liste. Vier unterschiedliche Methoden wurden für das Benchmark implementiert. Beim Benchmark Iterate list wird die Liste über eine foreach-Schleife iteriert und beim entsprechenden Element abgebrochen:

for (Element element : elements) {

	if ("Huhn".equals(element.Key)) {
		specificElement = element;
		break;
	}
}

Die nächste Variante iteriert die Liste ebenfalls durch, nutzt aber die klassische Variante über den Index:

for (int j = 0; j < elements.size(); j++) {

	Element element = elements.get(j);

	if ("Huhn".equals(element.Key)) {
		specificElement = element;
		break;
	}
}

Anschließend folgt eine Variante über die Stream-API, bei welcher die Methode findFirst genutzt wird:

specificElement = elements.stream()
		.filter(element -> "Huhn".equals(element.Key))
		.findFirst()
		.orElse(null);

Bei der letzten Variante wird ebenfalls die Stream-API genutzt, nur diesmal wird findAny genutzt:

specificElement = elements.stream()
		.filter(element -> "Huhn".equals(element.Key))
		.findAny()
		.orElse(null);

Die Idee bei der Nutzung der Methode findAny ist, dass diese schneller ist, da die Suche theoretisch parallelisiert werden kann. Im JavaDoc zu der Methode wird das Ganze so beschrieben:

The behavior of this operation is explicitly nondeterministic; it is
* free to select any element in the stream. This is to allow for maximal
* performance in parallel operations; the cost is that multiple invocations
* on the same source may not return the same result.

Der Benchmark selber führt für jeden Testfall 75.000 mal durch, damit sich Ungenauigkeiten bei einzelnen Läufen wegmitteln und etwaige Optimierung zum tragen kommen können. Wird die Ausführungszeit über alle 75.000 Durchläufe betrachtet ergibt sich folgendes Bild:

Die Durchführungszeiten über alle Durchläufe

Bei der Betrachtung der einzelnen Durchläufe ergibt sich ein ähnliches Bild:

Die Durchführungszeiten pro Durchlauf

Die ermittelten Werte sehen wie folgt aus:

Iterate list
Time in seconds (total): 58.621323501
Time in seconds (per run): 0.00078161764668

Iterate list (without for each)
Time in seconds (total): 51.9264994
Time in seconds (per run): 0.0006923533253333333

Stream list and find first
Time in seconds (total): 55.3019915
Time in seconds (per run): 0.0007373598866666667

Stream list and find any
Time in seconds (total): 90.196209799
Time in seconds (per run): 0.0012026161306533333

Die schnellste Variante scheint die klassische Iteration über den Index zu sein, anschließend folgt die Variante mit der Stream-API und der Methode findFirst. Danach kommt die Iteration mittels einer foreach-Schleife und am Ende folgt weit abgeschlagen die Stream-API-Variante mit der Methode findAny. Das diese so schlecht abschneidet hat mich überrascht. Natürlich sollten Zahlen aus einem Benchmark immer mit Vorsicht genoßen werden, da es sich immer um eine künstliche Gegenüberstellung handelt. Das komplette Benchmark befindet sich auf GitHub und ist unter der MIT-Lizenz lizenziert und damit freie Software.

Element einer Liste unter Java mittels des Stream-API ermitteln

Unter Java kommt es öfter vor, das ein bestimmter Eintrag in einer Liste gesucht werden soll. Gegeben sei in diesem Beispiel folgende Liste:

// List of elements
List elements = new ArrayList();

elements.add(new Element("Suppe", "Löffel"));
elements.add(new Element("Wasser", "Flüssigkeit"));
elements.add(new Element("Käse", "Gelb"));
elements.add(new Element("Huhn", "Ei"));

Die Klasse, welche die Elemente hält ist wie folgt definiert:

class Element {
    public final String Key;
    public final String Value;

    public Element(String key, String value) {
        Key = key;
        Value = value;
    }
}

Soll nun ein bestimmtes Element der Liste ermittelt werden, so kann dies auf dem klassischen Weg wie folgt erledigt werden:

// Get element
Element specificElement = null;

for (Element element : elements) {

    if ("Huhn".equals(element.Key)) {
        specificElement = element;
        break;
    }
}

Mit der seit Java 8 eingeführtes Stream-API kann das Problem eleganter gelöst werden:

// Get element via stream api
Element specificElement = elements.stream()
        .filter(element -> "Huhn".equals(element.Key))
        .findFirst()
        .orElse(null);

In diesem Fall werden die Elemente durchgegangen und überprüft ob in element.Key die Zeichenkette Huhn enthalten ist. Ist dies der Fall, wird das entsprechende Element zurückgegeben. Wird das Element in diesem Fall nicht gefunden, so ist die Variable specificElement null.

Separates Watches-Fenster in IntelliJ IDEA aktivieren

In den neueren Versionen der Java-IDE IntelliJ IDEA wurde die Ansicht des Variables– und des Watches-Fensters standardmäßig zusammengelegt. Damit können alle aktuell aktiven Variablen und die selbstdefinierten Watches in einem Fenster betrachtet werden.

Standardmäßig ist das Variablen- und das Watch-Fenster zu einem Fenster zusammengefasst

Dem ein oder anderen mag diese neue Ansicht zu unübersichtlich sein. Die alte Ansicht ist nicht aus der IDE verschwunden, so das diese wieder aktiviert werden kann. Dazu muss dann Brillensymbol in der vertikalen Symbolleiste des Fensters ausgewählt werden.

Die alte Ansicht kann wieder aktiviert werden

Damit wird die alte Ansicht, in der die aktiven Variablen und die Watches wieder getrennt sind, aktiviert.

Historic Tale Construction Kit

Wer kennt es nicht, es ist Freitag Nachmittag und plötzlich wird ein historisch akurater Comic aus dem Mittelalter benötigt. Damit hier niemand im Regen stehen muss, gibt es das Historic Tale Construction Kit.

Mit dem Historic Tale Construction Kit können entsprechende Comics erstellt werden

Mit diesem ist es möglich solche Comics problemlos aus den vorgefertigten Elementen zu generieren. Ist der Comic fertiggestellt, kann er als PNG Datei exportiert werden. Ausprobiert werden kann das Ganze auf der Seite des Projektes. Der entsprechende Quelltext ist unter MIT-Lizenz lizenziert und damit freie Software. Er kann über GitHub bezogen werden.

Maps unter Java mit computeIfAbsent nutzen

Wenn eine Map unter Java etwas komplexer wird, wie z.B. folgende Map:

Map<String, List<String>> testMap = new HashMap<>();

wird das befüllen derselbigen interessant. In diesem Beispiel soll ein Key und ein neuer Value für die Liste vom Typ String hinzugefügt werden. Damit dies funktioniert muss überprüft werden ob der Key bereits existiert und wenn dies nicht der Fall ist, ein neuer Key angelegt werden. Im kompletten Beispiel sieht das Ganze so aus:

String key = "ABC";
String valueForList = "DEF";

Map<String, List<String>> testMap = new HashMap<>();

if(!testMap.containsKey(key)) {
    testMap.put(key, new ArrayList<>());
} 

testMap.get(key).add(valueForList);

Einfacher und unkomplizierter ist es die Methode computeIfAbsent zu nutzen, welche seit Java 8 in der Definition des Map-Interfaces enthalten ist. Mit der Nutzung der Methode verkürzt sich das Beispiel wie folgt:

String key = "ABC";
String valueForList = "DEF";

Map<String, List<String>> testMap = new HashMap<>();
testMap.computeIfAbsent(key, s -> new ArrayList<>()).add(valueForList);

Damit wird automatisch eine neue ArrayList angelegt, wenn der entsprechende Schlüssel noch nicht hinterlegt ist. Anschließend kann der entsprechende Wert für die Liste hinzugefügt werden.