Liigu peamise sisu juurde

Sisseehitatud funktsionaalsed liidesed

Sissejuhatus

Eelmine peatükk näitas, kuidas ise luua funktsionaalseid liideseid ning tutvustas nende tagamaid. Java standardteek pakub nelja üldotstarbelist funktsionaalset liidest, mis asuvad java.util.function pakis ning katavad enamiku kasutusjuhtudest ära. Nende tundmine säästab programmeerijat oma funktsionaalsete liideste loomisest ning aitab paremini mõista ka seda, kuidas näiteks Stream API töötab.

Predicate<T>

Predicate<T> võtab argumendiks tüüpi T väärtuse ja tagastab boolean-väärtuse. See vastab oma argumendi kohta jah/ei küsimusele.

Predicate<String> isLong = s -> s.length() > 10;

isLong.test("Hi"); // false
isLong.test("Hello world"); // true

Selle ainus abstraktne meetod on boolean test(T t).

Predikaatide kombineerimine

Predikaate on võimalik omavahel kombineerida .and(), .or(), ja .negate() meetoditega:

Predicate<Employee> isSenior    = e -> e.getYearsOfExperience() >= 5;
Predicate<Employee> isActive = e -> e.isActive();

Predicate<Employee> isSeniorActive = isSenior.and(isActive);

employees.stream()
.filter(isSeniorActive)
.collect(Collectors.toList());

// Equivalent to writing the condition directly:
employees.stream()
.filter(e -> e.getYearsOfExperience() >= 5 && e.isActive())
.collect(Collectors.toList());

Liittingimuse muutmine nimega predikaatideks teeb koodi eesmärgi selgemaks ning võimaldab iga osa eraldi testida.

Function<T, R>

Function<T, R> võtab sisendiks T tüüpi väärtuse ning tagastab R tüüpi väärtuse.

Function<Employee, String> toName = e -> e.getName();

String name = toName.apply(someEmployee);

Selle ainus abstraktne meetod on R apply(T t).

Funktsioonide aheldamine

Antud funktsioone on omavahel võimalik aheldada .andThen() meetodiga:

Function<Employee, String> toName  = Employee::getName;
Function<String, String> toUpper = String::toUpperCase;

Function<Employee, String> nameUpper = toName.andThen(toUpper);

.andThen(f) tagastab uue funktsiooni, mis esmalt rakendab esialgset funktsiooni ning siis rakendab sellele järgneva funktsiooni tulemuse selle peale.

Consumer<T>

Consumer<T> võtab sisendiks T tüüpi väärtuse ning ei tagasta midagi. Seda kasutatakse siis, kui eesmärgiks on saavutada mingi kõrvalmõju (logimine, printimine, salvestamine) arvutuse läbiviimise ja tulemuse tagastamise asemel.

Consumer<Employee> print = e -> System.out.println(e.getName() + " — " + e.getDepartment());

print.accept(someEmployee); // Alice — Engineering

Selle ainus abstraktne meetod on void accept(T t).

Sarnaselt funktsioonidele on ka Consumer-eid võimalik aheldada omavahel .andThen() meetodiga:

Consumer<Employee> log  = e -> logger.info("Processing: " + e.getId());
Consumer<Employee> save = e -> repository.save(e);

Consumer<Employee> logThenSave = log.andThen(save);

Supplier<T>

Supplier<T> ei võta sisendina mitte midagi sisse ning tagastab T tüüpi tulemuse. Selle mõte on koodi käivitamist viivitada ehk funktsiooni väärtust ei arvutata enne, kuniks Supplier-i ainsat abstraktset, .get(), on välja kutsutud.

Supplier<LocalDate> today = () -> LocalDate.now();

LocalDate date = today.get(); // computed only when get() is called

Supplier on kasulik siis, kui väärtuse arvutamine on kulukas ja võib osutada ka mitte vajalikuks olla:

// HTTP requests, Database requests etc. are expensive by nature
// The HTTP request is not made when the Supplier is created — only when get() is called
Supplier<Response<Criteria>> fetchCriteria = () -> budgetApi.fetchCriteria();

Sellise võtte nimi on lazy evaluation ehk arvutuse edasi lükkamine kuniks selle tulemust tegelikult vaja läheb.

Vahekokkuvõte

Siin on lühike koondtabel eelnevast jutust:

LiidesSisendVäljundMeetodMilleks tavaliselt kasutatakse?
Predicate<T>Tbooleantest(T)filtreerimiseks, valideerimiseks
Function<T, R>TRapply(T)uue tulemuse loomiseks
Consumer<T>Tnothingaccept(T)kõrvalmõjude tekitamiseks
Supplier<T>nothingTget()tulemuse arvutamise viivitamiseks

Kui eelnevalt tutvusime nelja põhilise funktsionaalse liidesega, siis järgnevalt laiendame pilti: vaatame ka mitme argumendiga varianti BiFunction ning praktilist erijuhtu Runnable, mis kuigi ei kuulu java.util.function pakki, kuid järgib sama mustrit.

BiFunction<T, U, R>

BiFunction<T, U, R> on kahe argumendiga vaste Function<T, R>-ile. Nagu selle nimi ütleb, siis seda kasutatakse juhul kui funktsioon vajad ühe väärtuse teiseks muutmisel ka mingit teist sisendit.

BiFunction<Double, Double, Double> power = (base, exp) -> Math.pow(base, exp);

double result = power.apply(2.0, 10.0); // 1024.0

Meetodiviitena:

BiFunction<Double, Double, Double> power = Math::pow;

Selle ainus abstraktne meetod on R apply(T t, U u).

Runnable

Runnable tähistab koodiplokki, mis ei võta sisendit ega tagasta mingit väärtust. See on üks Java vanimatest liidesest (vanem kui java.util.function pakk ja selle sisu), kuid see järgib funktsionaalsete liidestega sama mustrit ning seda saab kasutada lambda-avaldisena. Näiteks:

Runnable greet = () -> System.out.println("Hello");

greet.run(); // Hello

Selle ainus abstraktne meetod on void run(). Runnable kasutatakse kõige sagedamini töö edasi lükkamiseks, näiteks ülesande edastamiseks teisele lõimele, tööde ajastajale (scheduler) või täiturile (executor).

Muud variatsioonid

java.util.function pakk sisaldab ka liideseid nagu BiPredicate<T, U>, BiConsumer<T, U> ning primitiivtüüpidele spetsialiseeritud variante (IntPredicate, ToIntFunction<T>), mille eesmärk on vältida auto boxing'ule kuluvat lisakulu. Need järgivad samu põhimõtteid ja kasutatakse samal viisil nagu eelnevalt läbi käidud liideseid.