Java: tento způsob užití lambdy zdá se mi poněkud nešťastný

Nedá mi to, musím střelit do vlastních řad. Dostal se mi do rukou zdroják, který už na první pohled prozrazoval, že je na něm něco špatně. Udělám proto pokus o přeprogramování a zkusím také změřit efektivitu.

Aktualizace 13.2.2019: dostal jsem tip na vyzkoušení ještě jedné varianty a zjistil jsem, že jsem měření asi dělal špatně. Takže znovu, aktualizovat výsledky a přepsat závěr.

Původní implementace

Když jsem dříve psal lambda pokud možno jednořádková, rozhodně jsem neměl na mysli tuhle špagetu:

private Vector<Measurement> measList = new Vector<>();

public Vector<Integer> getMeasItemIdList() {
	Vector<Integer> idList = new Vector<>();
	measList.forEach(m -> m.getMeasItems().forEach(i -> { if( !idList.contains(i.getId()) ) { idList.add(i.getId()); } })); 
	return idList;
}

Dva vnořené Collection.forEach(), do nich vnořená konstrukce if. Čitelnost zdrojáku dostala na frak. Teprve až po delším hackeření jsem pochopil, jaké vlastně bylo zadání: máme seznam měření, každé měření obsahuje změřené položky. Úkolem je vytvořit unikátní seznam identifikátorů změřených položek a z nějakých historických důvodů ho vrátit jako Vector.

Pokusím se proto o lepší implementaci.

Varianta 1: vnořené cykly for

Nejprve zkusíme oldschool implementaci pomocí klasických for cyklů.

public Vector<Integer> getMeasItemIdList() {
	Vector<Integer> idList = new Vector<>();
	for(Measurement m : measList) {
		for(MeasItem i : m.getMeasItems()) {
			if( !idList.contains(i.getId()) ) { 
				idList.add(i.getId()); 
			}
		}
	}
	return idList;
}

Zdroják na první pohled ukecanější, ale čitelnější.

Varianta 2: LinkedHashSet

Pojďme ale ještě dál. Pokud chceme unikátní seznam, bude lepší kolekci typu List nahradit kolekcí typu Set. A pokud potřebujeme zachovat pořadí vložených prvků, můžeme využít LinkedHashSet. Nejprve ho zkusíme zaimplementovat do původní verze s lambdami.

public Vector<Integer> getMeasItemIdList() {
	Set<Integer> idSet = new LinkedHashSet<>();
	measList.forEach(m -> m.getMeasItems().forEach(i -> idSet.add(i.getId()) )); 
	return new Vector<>(idSet);
}

Vida, hned je špageta kratší a čitelnější.

Varianta 3: vnořené cykly for plus LinkedHashSet

Pro úplnost zaimplementujme LinkedHashSet i do verze s vnořenými for cykly.

public Vector<Integer> getMeasItemIdList() {
	Set<Integer> retSet = new LinkedHashSet<>();
	for(Measurement m : measList) {
		for(MeasItem i : m.getMeasItems()) {
			retSet.add(i.getId());
		}
	}
	return new Vector<>(retSet);
}

I zde došlo ke zlepšení čitelnosti. Posuďte sami, jestli se vám líbí více tahle varianta, nebo varianta s lambdami.

Varianta 4: implementace pomocí streamu

Když jsem měřil efektivitu jednotlivých řešení, napadlo mě ještě vyzkoušet implementaci pomocí streamu. Bude čitelnější nebo efektivnější?

public Vector<Integer> getMeasItemIdList() {
	List<Integer> retList = measList.stream()
                .flatMapToInt(m -> m.getMeasItems().stream().mapToInt(MeasItem::getId))
                .distinct()
                .boxed()
                .collect(Collectors.toList());
	return new Vector<>(retList);
}

Když pomineme menší špagetu u metody flatMapToInt(), kterou potřebujeme pro sloučení měřených položek do jednoho streamu, je zdroják vcelku čitelný. Ale zatímco u předchozích čtyř implementací bude pořadí prvků ve vráceném seznamu stejné, u této implementace jsem pořadí netestoval.

Varianta 5: implementace pomocí streamu

Aktualizace 13.2.2019: Varianta odstranit distinct() a použít Collectors.toSet() mě sice také napadla, ale až Vojtěch Hordějčuk mě svým komentářem na LI přiměl udělat exaktní měření. Dosáhneme vyšší efektivity?

public Vector<Integer> getMeasItemIdList() {
	Set<Integer> retSet = measList.stream()
                .flatMapToInt(m -> m.getMeasItems().stream().mapToInt(MeasItem::getId))
                .boxed()
                .collect(Collectors.toSet());
	return new Vector<>(retSet);
}

Vyhodnocení efektivity

Čitelnost zdrojáku je jedna věc, nás však zajímá také efektivita výsledného kódu. Jak dlouho bude trvat zpracování dat jednotlivými variantami implementace? Měření času jsem prováděl takto:

  • Pro měření času jsem použil profiler z NetBeans.
  • Jako vstupní seznam jsem použil postupně: 1000 měření po 1000 položkách, 2000 měření po 2000 položkách a 5000 měření po 5000 položkách. ID položek odpovídalo pořadí položky v seznamu.
  • Před každým měřením byl seznam vytvořen znovu.
  • Před každým novým vytvořením seznamu byl spuštěn garbage collector.
  • Aktualizace 13.2.2019: Zjistil jsem, že nejkratší čas vykazovalo vždy poslední měření bez ohledu na to, o kterou variantu se jednalo. Proto jsem po posledním měření ještě jednou spustil vytvoření seznamu. Pak začaly být výsledky nezávislé na pořadí měření.

A výsledek?

Varianta 1000 x 1000
2000 x 2000
5000 x 5000
lambdy plus Vector 5,862 s 39,363 s 9 min 16,733 s
for plus Vector 5,259 s 39,016 s 9 min 32,712 s
lambdy plus LinkedHashSet 0,218 s 0,718 s 5,751 s
for plus LinkedHashSet 0,205 s 0,827 s 6,391 s
stream distinct() toList() 0,256 s 0,930 s 5,848 s
stream toSet() 0,240 s 0,975 s 6,724 s

Jednoznačně nejméně efektivní jsou obě implementace využívající Vector. Mezi nimi a ostatními implementacemi je skokový rozdíl. Profiler navíc ukázal, že největší brzdou je volání metody Vector.contains(): více než 99 % času zabrala právě tato metoda - viz následující tabulka.

Varianta Celkový čas
Vector.contains()
%
lambdy 1000 x 1000 5,862 s 5,846 s 99,7 %
lambdy 2000 x 2000 39,363 s 39,209 s 99,6 %
lambdy 5000 x 5000 9 min 16,733 s 9 min 15,817 s 99,8 %
for 1000 x 1000 5,259 s 5,197 s 98,8 %
for 2000 x 2000 39,016 s 38,939 s 99,8 %
for 5000 x 5000 9 min 32,712 s 9 min 31,156 s 99,7 %

Když porovnáme implementace cyklu jako Collection.foreach() plus lambda s klasickým cyklem for, výsledné časy se liší pouze nepatrně. Vypadá to však, že s rostoucí vstupní množinou vzrůstá efektivita implementace lambdou.

Varianta lambdy
for
%
Vector 1000 x 1000 5,862 s 5,259 s -10 %
Vector 2000 x 2000 39,363 s 39,016 s -1 %
Vector 5000 x 5000 9 min 16,733 s 9 min 32,712 s +2 %
LinkedHashSet 1000 x 1000 0,218 s 0,205 s -6 %
LinkedHashSet 2000 x 2000 0,718 s 0,827 s +15 %
LinkedHashSet 5000 x 5000 5,751 s 6,391 s +11 %

Implementace streamem kupodivu nebyla nejefektivnější. Když ji srovnáme s implementací pomocí lambda výrazů a kolekce LinkedHashSet, byla až o čtvrtinu pomalejší.

Velikost vstupu lambdy plus LinkedHashSet
stream toList()
%
1000 x 1000 0,205 s 0,256 s +24 %
2000 x 2000 0,827 s 0,930 s +12 %
5000 x 5000 5,751 s 5,848 s +2 %

Také je zajímavá vyšši efektivita u streamu s distinct() a toList(). Stream s toSet() byl efektivnější při menší vstupní množině, s rostoucí velikostí vstupu však začala efektivita klesat.

Velikost vstupu stream toList()
stream toSet()
%
1000 x 1000 0,256 s 0,240 s -6 %
2000 x 2000 0,930 s 0,975 s +5 %
5000 x 5000 5,848 s 6,724 s +15 %

Závěr

To, že je něco moderní, ještě neznamená, že je to i efektivnější. Implementace cyklu pomocí Collection.foreach() a pomocí for jsou zhruba rovnocenné. Při volbě implementace proto musíme brát v potaz čitelnost zdrojáku. Cit pro zdroják je zde zcela na místě.

Také je zajímavé, že vyšší efektivitu nepřinesly ani streamy, dokonce byly pomalejší než cykly.

Zcela na místě je však volba správného typu kolekce. Pokud chceme unikátní seznam, dostaneme výrazně lepší efektivitu použitím kolekce typu Set.

Tagy: Java, Programování

Tento web bude tebe a tvůj počítač krmit piškotkami, jelikož a protože je to slušný web a jako takový ví, že je potřeba návštěvu řádně pohostit, aby se u nás cítila dobře. Užíváním tohoto webu potvrzuješ, že netrpíš mentální anorexií, nedržíš žádnou obskurní dietu a že můžeš piškotki do sebe cpát kdykoli a v jakémkoli množství. Více informací...