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í