Liigu peamise sisu juurde

Kompleksemad vood

Sissejuhatus

Eelmises peatükis käsitlesime vooge ning nende põhioperatsioone. Selles (lisa)peatükis tutvune erinevate mustrite/võtetega, mis võivad esineda kompleksemates töötlusahelates.

Kujutisse kogumine

Collectors.toMap

toMap kogub voo elemendid kujutisse, rakendades igale elemendile võtme- ja väärtuseekstraktorit:

Map<Long, Employee> employeesById = employees.stream()
.collect(Collectors.toMap(
Employee::getId, // key: the employee's id
employee -> employee // value: the employee itself
));

Function.identity() on selgem alternatiiv employee -> employee-le, kui väärtuseks on element ise:

Map<Long, Employee> employeesById = employees.stream()
.collect(Collectors.toMap(Employee::getId, Function.identity()));

Kui kaks elementi annavad sama võtme, viskab toMap vaikimisi IllegalStateException-i. Konfliktide lahendamiseks saab kolmanda argumendina anda ühendamisfunktsiooni:

// If two employees share a name, keep the one with more experience
Map<String, Employee> byName = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Function.identity(),
(existing, replacement) -> existing.getYearsOfExperience() >= replacement.getYearsOfExperience()
? existing : replacement
));

Collectors.groupingBy

groupingBy rühmitab elemendid klassifitseerimisfunktsiooni alusel, luues Map<K, List<V>> tüüpi tulemuse:

Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));

Antud näites kogutakse kokku töötajad nende osakonna järgi.

Argumendina saab kaasa anda ka allavoolu koguja (downstream collector), et iga rühma täiendavalt agregeerida:

// Count employees per department
Map<String, Long> countByDepartment = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));

flatMap

map teisendab iga elemendi üheks tulemuseks. flatMap teisendab iga elemendi tulemuste vooks ning seejärel koondab need üheks vooks

See on kasulik, kui iga element sisaldab kollektsiooni. Kui map loob Stream<List<String>>, siis flatMap koondab kõik üheks tasaseks Stream<String>-iks:

// Each employee has a list of skills — collect all skills across all employees into one list
List<String> allSkills = employees.stream()
.flatMap(employee -> employee.getSkills().stream())
.collect(Collectors.toList());

Ilma flatMap-ita oleks map tulemus Stream<List<String>>, flatMap-iga on see tasane Stream<String>.

Kuupäeva põhine filtreerimine LocalDate-ga

LocalDate sobitub loomulikult töötlusahelatesse. Selle meetodid võimaldavad hõlpsalt filtreerida üksikute kuupäevakomponentide alusel:

// All employees who started in January, regardless of year or day
employees.stream()
.filter(e -> e.getStartDate().getMonth() == Month.JANUARY)

// All employees who started on a Monday
employees.stream()
.filter(e -> e.getStartDate().getDayOfWeek() == DayOfWeek.MONDAY)

// Employees hired within a date range (both endpoints inclusive)
employees.stream()
.filter(e -> !e.getStartDate().isBefore(fromDate)
&& !e.getStartDate().isAfter(toDate))

Kuupäevade puhul on võimalik kasutada ka Period.between meetodit, mis arvutab välja vahemiku:

LocalDate today = LocalDate.now();  // computed once, before the pipeline

List<Integer> tenures = employees.stream()
.map(e -> Period.between(e.getStartDate(), today).getYears())
.collect(Collectors.toList());
hoiatus

LocalDate.now() peaks välja kutsuma ainult korra enne töötlusahela algust. Ahela sees selle kutsumine käivitaks selle meetodi iga elemendi puhul, mis tekitab ebaüthsust ning ona aeglane ja tarbetu.

Laisk hindamine (lazy evaluation) Supplier-iga

Supplier<T> lükkab arvutuse edasi kuni .get()-i väljakutsumiseni. See on eriti oluline, kui väärtuse loomine on kulukas, näiteks HTTP-päring, ja puudub põhjus seda iga töötlusahela elemendi jaoks korrata.

Vaatleme töötajate filtreerimist kriteeriumide alusel, töötajate andmed saadakse välise API käest:

public static List<Employee> findEligibleEmployees(
List<Employee> employees,
Supplier<Response<BudgetCriteria>> httpRequestExecutor) {

Response<BudgetCriteria> response = httpRequestExecutor.get(); // one request

if (response.getHttpStatus() != 200) {
throw new HttpException(response.getHttpStatus());
}

BudgetCriteria criteria = response.getBody();

return employees.stream()
.filter(e -> meetsBudgetCriteria(e, criteria))
.collect(Collectors.toList());
}

Päring tehakse täpselt üks kord: enne töötlusahela algust kutsutakse Supplier välja, et saada kriteeriumid, ning sama criteria objekti kasutatakse iga töötaja jaoks uuesti. Kui kutsuda httpRequestExecutor.get() töötlusahelas, tehtaks iga töötaja kohta eraldi päring. Selline lähenemine on vale, sest HTTP-päringud on loomult kallid.

teade

Supplier-i parameetrina tavalise Response-i asemel teeb selgeks, millal HTTP-päring tehakse. Väljakutsuja annab tehase, meetod otsustab, millal (ja kas) seda välja kutsuda.

BigDecimal-iga summeerimine

Raha esindamiseks koodis ei tohiks ealeski kasutada kas int või double andmetüüpe, viimast eriti ujukomaarvude ümardusvigade tõttu. Õige andmetüüp selleks oleks BigDecimal.

Voogudel on sisseehitatud arvulised vähendused int, long ja double jaoks, kuid mitte BigDecimal-i jaoks. Selleks kasuta reduce-i:

BigDecimal totalPayroll = employees.stream()
.map(Employee::getSalary)
.reduce(BigDecimal.ZERO, BigDecimal::add);

BigDecimal.ZERO on identiteetelement (tulemus, kui voog on tühi), BigDecimal::add on akumulaator.

Operatsioonide kombineerimine

Levinud muster on kombineerida erinevaid voo elemente, nagu näiteks sorted, limited ja reduce, et agregeerida n-arv suurimad väärtused kokku:

BigDecimal topThreeBudget = employees.stream()
.map(Employee::getSalary)
.sorted(Comparator.reverseOrder())
.limit(3)
.reduce(BigDecimal.ZERO, BigDecimal::add);

Töötlusahel sorteerib palgad kahanevalt, võtab esimesed kolm ja summeerib need. Iga operatsioon lisab tulemuse kirjeldusse ühe selge sammu.

Paralleelsed vood

Vaikimisi töötavad vood ühel protsessori lõimel. .parallel() lisamine suvalises töötlusahela punktis annab käitussüsteemile märku jaotada töö automaatselt mitme lõime vahel:

long count = IntStream.range(0, 200_000)
.parallel()
.filter(n -> isPrime(n))
.count();

See on üks puhaste töötlusahelate otseseid eeliseid. Kuna igat elementi töödeldakse sõltumatult ja puudub jagatud muudetav olek, saab käitussüsteem tööd turvaliselt jaotada ilma lisapingutuseta programmeerija poolt.

hoiatus

Paralleelsed vood lisavad töö jaotamise ja ühendamise tõttu lisakulu. Need tasuvad end ära ainult CPU-mahukate operatsioonide korral suurte andmehulkadega. Väikeste kollektsioonide või I/O-mahukate tööde puhul on jadavood kiiremad.

Paralleelne töötlus peab teadlik otsus olema ning selle jaoks alatihti võib vajadus puududa.