Moje odkazy
Obsah článku:
vydáno: 9. 8. 2014 13:24, aktualizováno: 18. 7. 2020 13:33
Přiznám se, že změn v osmé verzi Javy jsem se trochu obával. Ale nakonec, když jsem to trochu víc prozkoumal a zkusil si napsat nějaký ten kód, tak se mi to líbí. Mám z 8 radost.
Nerad bych, aby se z Javy stalo C++ nebo třeba Perl. Ne že by na nich bylo něco špatného, ale na Javě si nejvíc cením jednoduchosti jazyka. Neobsahuje žádné záludné jazykové konstrukce nebo neintuitivní operátory. Všechno je buď třída, rozhraní nebo metoda. Vždycky se člověk prokliká na definici/zdroják a podívá se na JavaDoc. Tenhle přístup je IMHO důvodem, proč je Java tak úspěšná – snadno se v ní dělají velké projekty a dá se spolupracovat ve větších týmech, kde se navíc mění lidi. Chápu, že na druhou stranu může některé geniální programátory-sólisty nudit – je tu ale řada dalších jazyků nad JVM (Scala, Clojure, Groovy, Python, PHP, Erlang, JavaScript…), takže ani kvůli tomu není potřeba opouštět platformu.
Shrnuto: žádný černý scénář se nevyplnil – je to pořád stejná Java, jen ještě trochu lepší.
Začnu trochu historickým úvodem. S Javou jsem začal v době 1.4 resp. když právě přicházela 5. Dřívější verze jsou pro mě tak trochu pravěk a změny v nich mi splývají. Zde jsou některé důležité změny verzí 5, 6 a 7:
@Override
), ale i tady se najdou jejich hodně užitečné aplikace: např. JAXB (deklarativní mapování mezi XML a objekty) nebo Lombok (nadstavba jazyka, částečně hack)enum
může obsahovat i metody, nemusí to být jen pasivní neživá struktura.Od této verze se taky vynechává „1.“ v označení verze – takže „5“ místo „1.5“.
a různá drobnější vylepšení a optimalizace, ale z hlediska jazyka nic až tak revolučního. K zásadní změně však přeci jen došlo: v této verzi vydal Sun Microsystems svoji implementaci Javy jako svobodný software pod licencí GNU GPL – vzniklo OpenJDK.
OpenJDK je referenční implementací Javy SE 7.
switch
jde použít i textové řetězcetry ( … ) { … }
blok a nemusí se o jejich zavírání explicitně starat programátornew ArrayList<>()
catch
větviLambda funkce byly odloženy ze 7 do 8.
Konec historického úvodu – jdeme na změny v 8.
Největší novinkou v Javě 8 jsou jednoznačně lambda výrazy. Takže teď můžeme napsat např.:
JButton tlačítko = new JButton();
tlačítko.addActionListener(e -> System.out.println("Tlačítko: " + e));
a stručně předat funkci, která se provede po stisku tlačítka – místo klasické anonymní třídy:
tlačítko.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Tlačítko: " + e);
}
});
„Funkční rozhraní“ je nový termín používaný pro rozhraní (interface
), která mají právě jednu abstraktní metodu.
Tato rozhraní si můžete anotovat pomocí @FunctionalInterface
, což slouží pro kontrolu stejně jako třeba anotace @Override
– v případě nesplnění podmínek vám kompilátor ohlásí chybu. Jinak to ale na funkci nemá vliv.
@FunctionalInterface
public interface MojeFunkce {
public int počítej(int vstup);
}
Funkční rozhraní představují most mezi funkcionálním a objektovým světem.
Možná vás zarazilo slovní spojení abstraktní metoda v souvislosti s rozhraním použité výše.
V Javě 8 můžou rozhraní obsahovat i neabstraktní metody, tedy metody, obsahující implementaci, ne jen hlavičku. Pro označení těchto metod se používá klíčové slovo default
.
Třída, která implementuje toto rozhraní, tak získá i tuto funkcionalitu – jedná se tak v podstatě o vícenásobnou dědičnost.
Užitečné je to mj. v souvislosti s funkčními rozhraními a lambdami. Pomocí lambda výrazu definujeme určitou funkcionalitu, ale koncový uživatel daného rozhraní může volat jinou metodu (výchozí, definovanou v rozhraní), která tu metodu z lambdy obaluje případně dělá něco úplně jiného (např. skládá více funkčních rozhraní dohromady – viz níže).
Když už víme, co jsou funkční rozhraní, nebudou pro nás lambda výrazy v Javě ničím tajemným – v zásadě je to jen jiný způsob zápisu anonymních vnitřních tříd. Což mj. znamená, že stejným (funkcionálním) stylem jste mohli programovat (a pravděpodobně jste to také dělali) už v Javě 7 a dřívějších, jen to nevypadalo tak elegantně a potřebovali jste k tomu více řádků kódu. Zásadní změnou jsou tak vlastně jen ty výchozí metody rozhraní, které ve starších verzích jen tak nenahradíte, neopíšete. Ostatní je syntaktický cukr.
Lambda výrazem definujeme vnitřek té právě jedné metody z funkčního rozhraní – nic víc psát nemusíme, protože to je zřejmé z definice rozhraní a šlo by o redundantní informaci.
Dejme tomu, že budeme chtít vytvořit funkci implementující rozhraní MojeFunkce
(viz výše). Existuje několik způsobů zápisu – od nejukecanějšího po nejstručnější:
MojeFunkce naDruhou = (int i) -> {
return i * i;
};
Informaci o typech můžeme vynechat (typová kontrola samozřejmě stále funguje). Stejně jako složené závorky a return
, pokud nám stačí jeden řádek:
MojeFunkce naDruhou = (i) -> i * i;
A když má metoda funkčního rozhraní jen jeden parametr můžeme vynechat i kulaté závorky:
MojeFunkce naDruhou = i -> i * i;
Ty se nám naopak hodí, když není parametr žádný:
Supplier<Integer> dodavatel = () -> 123;
nebo je jich víc:
BiFunction<String, Integer, String> dvojFunkce = (s, i) -> s + ": " + i;
Funkce často nebudete deklarovat jako proměnné, tudíž ve vašem kódu rozhraní jako BiFunction<String, Integer, String>
ani nebudou vidět – funkci prostě vytvoříte a hned předáte jako argument metodě nebo konstruktoru, které přijímají dané funkční rozhraní. Viz ten úplně první swingový příklad s addActionListener()
.
Když jsem poprvé v javovském kódu uviděl dvě dvojtečky ::
, tak jsem se dost zděsil, vybavily se mi nějaké úplně jiné jazyky než Java.
Po vzpamatování se z počátečního šoku ale zjistíte, že tohle je dost užitečná věc.
V Javě 8 se pomocí nich můžete odkazovat na metody i konstruktory a konvertovat je na funkce resp. funkční rozhraní.
Můžeme tedy místo:
Supplier<Long> dodavatel = () -> System.currentTimeMillis();
long čas = dodavatel.get();
napsat stručnější:
Supplier<Long> dodavatel = System::currentTimeMillis;
long čas = dodavatel.get();
a funguje to i pro instanční metody – ne jen statické:
Date datum = new Date();
Supplier<Long> dodavatel = datum::getTime;
long čas = dodavatel.get();
a dokonce i pro konstruktory:
Function<Long, Date> generátorData = Date::new;
Date datum = generátorData.apply(System.currentTimeMillis());
Metoda nebo konstruktor musí mít samozřejmě vhodný počet a typ parametrů, které odpovídají požadovanému funkčnímu rozhraní. Jakkoli to může vypadat, že to funguje nějak magicky a samo se to spojí, nejde o žádnou dynamickou alchymii – stále máte dobrou typovou kontrolu a vždycky se můžete podívat na deklaraci příslušných rozhraní a přečíst si JavaDoc.
V praxi část Supplier<Long> dodavatel =
často vynecháte, nebudete deklarovat proměnnou a jen předáte „odkaz na metodu“ někam, kde je požadováno funkční rozhraní.
Funkční rozhraní si nemusíte psát sami – řadu obvyklých už máte připravenou ve standardní knihovně v balíčku java.util.function
. Zde jsou některé z nich:
Funkce je rozhraní pro převod jedné hodnoty (definiční obor) na jinou hodnotu (obor hodnot). Díky generikům (viz Java 5) je toto rozhraní použitelné pro libovolný typ a zároveň máme typovou kontrolu.
Deklarujeme si jednoduché funkce a hned je použijeme:
Function<Integer, Integer> přičtiJedna = (a) -> a + 1;
Function<Integer, Integer> vynásobDvěma = (a) -> a * 2;
int přičteno = přičtiJedna.apply(5); // 5 + 1 = 6
int vynásobeno = vynásobDvěma.apply(5); // 5 × 2 = 10
Funkce obvykle nebudeme používat hned (to bychom mohli rovnou zavolat ten kód), ale pravděpodobně je předáme do jiné části programu, kde se budou volat – podobně jako se to dělá třeba s posluchači ve Swingu, které se pak volají při událostech z GUI.
Díky výchozím metodám rozhraní (viz výše) nemusíme funkce jen volat přímo, ale můžeme je i snadno skládat pomocí výchozí metody compose()
inplementované ve funkčním rozhraní a zděděné v konkrétní funkci:
int x = vynásobDvěma.compose(přičtiJedna).apply(1); // (1 + 1) × 2 = 4
int y = přičtiJedna.compose(vynásobDvěma).apply(1); // (1 × 2) + 1 = 3
U compose()
se nejdřív zavolá vnitřní funkce (ta předaná jako argument).
Místo compose()
můžeme pro skládání použít taky andThen()
, kde to funguje přesně naopak. V obou případech je výsledkem skládání funkcí opět funkce s patřičnými generickými typy – nemusíme ji tedy hned volat pomocí apply()
, ale můžeme si ji uložit do proměnné a používat opakovaně nebo předat někam dál.
Pokud byste náhodou potřebovali funkci, která vrací totéž, co dostala na vstupu, tak si ji vyrobíte pomocí metody identity()
:
Function<Integer,Integer> totéž = Function.identity();
int x = totéž.apply(123); // = 123
Což zrovna moc užitečně nevypadá, ale berte to zatím jako ukázku toho, že funkce si můžete generovat, nemusíte si je vždy sami psát pomocí lambda výrazů.
Predikát je zde funkce, jejímž oborem hodnot je boolean. Definiční obor je daný generickým typem a může to být cokoli.
Pomocí metody Predicate.test(T t)
vyhodnocujeme pravdivost predikátu nad zadanou hodnotu.
Vytvoříme si pár predikátů pomocí lambda výrazů nebo odkazem:
Predicate<String> neníNull = Objects::nonNull;
Predicate<String> správnáDélka = s -> s.matches(".{8,32}");
Predicate<String> obsahujeČíšla = s -> s.matches(".*[0-9].*");
Predicate<String> obsahujeVelkáPísmena = s -> s.matches(".*[A-Z].*");
Stejně jako funkce – i predikáty můžeme díky výchozím metodám skládat:
Predicate<String> složenýPredikát =
neníNull.and(správnáDélka).and(obsahujeČíšla.or(obsahujeVelkáPísmena));
// odpovídá: neníNull && správnáDélka && (obsahujeČíšla || obsahujeVelkáPísmena)
a složený predikát vyhodnotit:
boolean a = složenýPredikát.test("Abcd123"); // = false
boolean b = složenýPredikát.test("abcdefgh"); // = false
boolean c = složenýPredikát.test("abcdefgh123"); // = true
boolean d = složenýPredikát.test("Abcdefgh"); // = true
Stejně jako javovské výrazy pospojované AND a OR operátory se i predikáty vyhodnocují zleva doprava a pokud nějaká podmínka neplatí (u AND) nebo naopak platí (u OR), další už se nevyhodnocují, protože to není potřeba a na výsledek to nemá vliv. Ostatně stačí se podívat do zdrojáků třídy java.util.function.Predicate
– ty && a || tam nakonec najdete.
Stejně jako u klasických výrazů s && a || je tedy dobré i predikáty řadit podle výpočetní/paměťové náročnosti a podle pravděpodobnosti kladného vyhodnocení.
Dodavatelé jsou ještě jednodušší – funkce nemá žádné parametry a jen vrací hodnotu danou generickým typem.
Rozhraní Supplier
má jedinou metodu get()
. Není specifikováno, zda má vracet pokaždé jiný nebo stejný objekt – záleží na implementátorovi.
Když už máme statické metody jako Predicate.isEqual()
pro generování predikátů, trochu mi tu chybí metoda pro vygenerování funkcionálního obalu (dodavatele) nad jednou hodnotou. Ale můžeme si ji snadno dopsat:
public static <T> Supplier<T> generujDodavatele(T hodnota) {
return () -> hodnota;
}
Když si metodu pojmenujete vhodným krátkým názvem, tak nemusíte ani používat lambda výrazy tam, kde chcete podstrčit nějakou hodnotu na místo, kde je vyžadována funkce (dodavatel) a zase si o něco méně ošoupete klávesnici.
O Javě 8 se mluví už dost dlouho. Ostatně zde je oficiální harmonogram:
datum | milník | popis |
2012/04/26 | M1 | |
2012/06/14 | M2 | |
2012/08/02 | M3 | |
2012/09/13 | M4 | |
2012/11/29 | M5 | |
2013/01/31 | M6 | |
2013/06/13 | M7 | Feature Complete |
2013/09/05 | M8 | Developer Preview |
2014/01/23 | M9 | Final Release Candidate |
2014/03/18 | GA | General Availability |
Ale je otázka, kdy začít nové vlastnosti používat ve svých projektech a učinit svůj software závislým na nové verzi platformy. Java 8 je sice už oficiálně venku a kromě zdrojáků si můžete stáhnout i binárky jako .tar.gz (pod Early Adopter Development License Agreement for Java SE). Dokonce binární sestavení od Oraclu jsou dostupná jako .tar.gz i jako .rpm balíček (ovšem pod Oracle Binary Code License Agreement for Java SE). Ale ještě bude chvíli trvat, než půjde instalovat OpenJDK 8 standardní cestou skrze balíčkovací systém vaší distribuce – např.:
apt install openjdk-8-jdk # nebo:
yum install java-1.8.0-openjdk
U Debianu najdete Javu 8 zatím v distribuci experimental, což asi nikdo v produkci nemá. V Ubuntu ve verzi Utopic Unicorn (14.10), což už zní slibněji (vyjde letos na podzim). Ještě lepší je situace ve Fedoře, tam je 8 v už dnešní verzi. V aktuálním openSUSE 13.1 je 8 zatím mezi experimentálními balíčky. V Archu jsem 8 mezi podporovanými balíčky nenašel, ale v AURu jdk8 je.
Další věc je, kdy uživatelé přejdou na aktuální verzi své distribuce – např. takové Ubuntu 14.04 LTS je podporované až do dubna 2019. Ale třeba je vaše aplikace k dřívějšímu upgradu motivuje.
Jestliže teď začínáte nový projekt, asi bych do 8 šel – vývoj vám nějaký čas zabere a i kdybyste to stihli moc brzo, tak přinejhorším doručíte zákazníkům i vlastní balíček openjdk-8-jdk
(hlavně nedávejte JVM do jednoho balíčku s vaší aplikací).
Pokud ale máte existující produkt a vaši zákazníci jsou zvyklí, že jim to chodí na Javě 7, 6 nebo dokonce starších, tak bych se změnou příliš nespěchal – minimálně bych počkal do doby, než bude 8 v jejich distribucích.
Dobrým místem, kde si 8 můžete vyzkoušet a natrénovat už teď, jsou různé interní a vývojářské nástroje případně prototypy.
Podpora v IDE není problém, Java 8 samozřejmě funguje v Netbeans, ale i v Eclipse nebo IntelliJ IDEA.
Java 8 představuje rozšíření možností jazyka směrem k funkcionálnímu programování. Funkcionálně jste sice v Javě mohli programovat vždycky (stejně jako můžete objektově programovat v Céčku), ale teď je to mnohem hezčí a přehlednější. Převratnější – i když ne tolik nápadnou – změnou je vícenásobná dědičnost resp. výchozí metody rozhraní.
Dnes to byl takový teoreticko-historický úvod a doufám, že jste přežili to, že tady tak trochu všechno souvisí se vším (jednotlivé části se těžko vysvětlují samostatně), ale po přečtení celého textu to snad bylo stravitelné. V příštích článcích se chci věnovat praktičtějšímu využití nových vlastností a taky věcem, na které se dnes nedostalo (java.util.stream
, map-reduce a další).
Témata: [softwarové inženýrství] [Java]
Pěkný článek, všechno mi to osvětlilo. 10/10