FK~

Moje odkazy

Ostatní odkazy

Close Windows
Nenajdete mě na Facebooku ani Twitteru
Rozpad EU
Jsem členem FSF
There Is No Cloud …just other people's computers.
Sane software manifesto / Manifest příčetného softwaru

Java 8: novinky jazyka

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.

Logo OpenJDK (průhledné 1)

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ší.


Předchozí verze

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:

Java 5 (září 2004)

  • generika – osobně je považuji za jednu z nejzásadnějších změn, došlo k posílení typové kontroly a zjednodušení práce s kolekcemi (a nejen s nimi, generické můžou být i jiné třídy).
  • anotace – největší význam mají pro Javu EE (např. EJB jsou díky nim skvělý nástroj, zatímco dřív to muselo být dost utrpení; taky vznikla spousta dalších frameworků, které na anotacích staví), v Javě SE jsou spíš na ozdobu nebo pro kontrolu (např. @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 – výčtové typy jsou teď oficiální součástí jazyka a není potřeba je emulovat pomocí tříd (návrhového vzoru), ale v zásadě to třídy jsou a 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“.

Java 6 (prosinec 2006)

  • skriptování – API pro skriptovací jazyky: Scripting Language Support (JSR 223) a vestavěná implementace JavaScriptu
  • Java Compiler API (JSR 199) – možnost volat kompilátor z programu

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.

Java 7 (červenec 2011)

OpenJDK je referenční implementací Javy SE 7.

  • invokedynamic – podpora dynamických jazyků v JVM, obecně se z JVM stává platforma pro běh různých jazyků, ne jen Javy
  • switch podle Stringů – v konstrukci switch jde použít i textové řetězce
  • AutoCloseable – zdroje implementující toto rozhraní může automaticky zavírat try ( … ) { … } blok a nemusí se o jejich zavírání explicitně starat programátor
  • tzv. diamantový operátor (pěkně blbý název) – při definici generické proměnné nemusíme uvádět generické typy dvakrát – můžeme napsat třeba new ArrayList<>()
  • catch-multiple – zachytávání více typů výjimek v jedné catch větvi
  • rozšířené atributy – API pro práci s rozšířenými atributy souborů a symbolickými odkazy
  • SCTP – podpora nového síťového protokolu (Stream Control Transmission Protocol)

Lambda 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í

„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.

Výchozí metody rozhraní

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).

Lambda výrazy

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().

Odkazy na metody a konstruktory

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í.

Standardní 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

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

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áty

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é

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.

Už je čas na Javu 8?

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.

Závěr

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ší).

Přílohy:

Odkazy a zdroje:

Témata: [softwarové inženýrství] [Java]

Komentáře čtenářů


AAAAAAAAAAAAAAAAAAAAAAA, 23. 4. 2017 09:16, Paráda! [odpovědět]

Pěkný článek, všechno mi to osvětlilo. 10/10


Marek, 27. 7. 2017 11:33 [odpovědět]

Paráda, díky moc za souhrn

Přidat komentář

reagujete na jiný komentář (zrušit)
jméno nebo přezdívka
název příspěvku
webová stránka, blog
e-mailová adresa
nápověda: možnosti formátování
ochrana proti spamu a špatným trollům

Náhled komentáře