Funktsionaalsed liidesed ja lambda-avaldised
Sissejuhatus
Eelmises peatükis kirjeldati funktsionaalset programmeerimist stiilina, kus funktsioone käsitletakse väärtustena, mida on võimalik teistele meetoditele/funktsioonidele kasutuseks edasi anda. Javas saavutatakse seda funktsionaalsete liideste ja lambda-avaldiste abil.
Funktsionaalsed liidesed
Funktsionaalne liides on liides, millel on täpselt üks abstraktne meetod ning mis on märgistatud @FunctionalInterface annotatsiooniga.
@FunctionalInterface
interface Greeting {
String greet(String name);
}
@FunctionalInterface annotatsioon ei ole kohustuslik.
Iga liides, millel on täpselt üks abstraktne meetod kvalifitseerub funktsionaalseks liideseks, kuid antud annotatsioon annab kavatsustest selgelt märku.
Lisaks on kompilaator suuteline ka seda "lepingut" tagama ehk kui liidesel on rohkem kui üks abstraktne meetod, siis tekib järgnev kompileerimisviga:
Multiple non-overriding abstract methods found in Greeting
Kuna funktsionaalsel liidesel on ainult üks meetod, siis ei teki segadust selles osas, millist käitumist antud tüüpi väärtus esindab. See võimaldabki kas funktsiooni salvestada muutujana või anda see mõnele meetodile argumendina ette.
Lambda-avaldised
Kuna funktsionaalne liides sisaldab täpselt ühte abstraktset meetodit, tuleb selle kasutamiseks anda sellele meetodile ka konkreetne teostus. Enne lambda-avaldisi sai seda teha ainult anonüümse klassi abil:
Greeting polite = new Greeting() {
@Override
public String greet(String name) {
return "Good day, " + name;
}
};
Lambda-avaldis on selle sama mustri lühivorm:
Greeting polite = name -> "Good day, " + name;
Lambda-avaldis moodustab greet meetodi keha - polite.greet("Alice") väljakutsumisel täidetaksegi täpselt see lambdas kirjeldatud kood.
Kompilaator tuletab nii parameetri tüübi (String) kui ka tagastustüübi konteksti põhjal - täpsemalt selle järgi, et sihttüübiks on Greeting.
Lambda süntaks
Tutvume lähemalt lambda-avaldiste süntaksiga. Lambda-avaldiste süntaksil on mitu kuju, sõltuvalt parameetrite arvust ja keha keerukusest:
// No parameters
Runnable r = () -> System.out.println("Hello");
// One parameter — parentheses optional
Greeting g = name -> "Hello, " + name;
// Multiple parameters — parentheses required
Comparator<Integer> cmp = (a, b) -> a - b;
// Block body — explicit return required
Greeting formal = name -> {
String title = name.length() > 3 ? "Mr/Ms" : "";
return "Dear " + title + " " + name;
};
Lambda-avaldiste puhul kehtivad järgmised reeglid:
- Parameetrite ümber ei pea sulge kirjutama, kui tegemist on ühe parameetriga ilma tüüpi välja kirjutamata. Muudel juhtudel on need kohustuslikud.
- Ühe avaldisega keha tagastab vaikimisi mingi väärtuse.
{}-ga ümbritsetud ploki puhul tulebreturnselgesõnaliselt kirjutada.
Funktsionaalse liidese loomine ja kasutamine
Samuti tutvume lähemalt ka sellega, kuidas ise luua funktsionaalseid liideseid ning kuidas neid kasutada. Toome näiteks funktsionaalse liidese, mis viib läbi ettevõtte töötaja valideerimist:
@FunctionalInterface
interface EmployeeValidator {
boolean validate(Employee employee);
}
Seda liidest saab kasutada parameetrina meetodites, kus valideerimisreeglid peavad olema paindlikud:
public boolean checkEmployee(Employee employee, EmployeeValidator validator) {
return validator.validate(employee);
}
Meetodi väljakutsuja peab validaatori teostuse lambda-avaldisena ise kirjutama. See võimaldab valideerimist teha paindlikumalt ning hoiab meetodit ennast üldotstarbelisena:
// Check that the employee has an assigned department
boolean ok = checkEmployee(employee, e -> e.getDepartment() != null && !e.getDepartment().isBlank());
// Check that the salary is within a valid range
boolean valid = checkEmployee(employee, e -> e.getSalary().compareTo(BigDecimal.ZERO) > 0);
Nagu näha, rakendatakse siin kahte erinevat reeglit läbi ühe meetodi. Valideerimise loogika määrab meetodi väljakutsuja.
See ongi funktsionaalsete liideste põhiline eelis. Loogika jäigalt sissekodeerimise asemel võtab meetod vastu funktsiooni ja rakendab seda.
Lambda-avaldised ja muutujatele viitamine
Lambda-avaldis võib viidata muutujatele, mis on sellega samas skoobis tingimusel, et see muutuja on efektiivselt lõplik ehk sellele ei omistata enam uut väärtust:
String department = "Engineering"; // effectively final
EmployeeFilter inDepartment = employee ->
employee.getDepartment().equals(department); // OK
department = "Marketing"; // this would make the lambda above a compile error
Selline piirang on tehtud kuna lambda-avaldise eluiga võib olla pikem kui selle skoop või meetod, kus see loodi. Nii on tagatud, et lambda-avaldis töötab alati sama väärtusega, millega see loodi.
Meetodiviited
Lambda-avaldised võimaldavad funktsionaalsete liideste teostusi kirjutada lühidalt. Sageli juhtub aga, et lambda ei sisalda uut loogikat, vaid lihtsalt delegeerib töö juba mõnele olemasolevale meetodile. Sellisel juhul saab kasutada veelgi kompaktsemat vormi, milleks on meetodiviidet (method reference).
Kui lambda-avaldis ei tee midagi muud kui kutsub välja olemasolevat meetodit, on meetodiviide lühem ja loetavam alternatiiv:
// Lambda
employees.forEach(e -> System.out.println(e));
// Equivalent method reference
employees.forEach(System.out::println);
Meetodiviiteid on nelja liiki:
| Liik | Süntaks | Samaväärne lambda-avaldis |
|---|---|---|
| Staatiline meetod | ClassName::staticMethod | x -> ClassName.staticMethod(x) |
| Konkreetse objekti isendimeetod | object::method | x -> object.method(x) |
| Argumendi isendimeetod | ClassName::method | x -> x.method() |
| Konstruktor | ClassName::new | () -> new ClassName() |
Meetodiviiteid kasutatakse sageli Stream API töötlusahelates.