28. 2. 2015

Ohlédnutí za marketingem Javy 7

Původně jsem chtěl napsat jen komentář pod článek "To nejlepší z Javy 7" na Et netera dev blogu, ale brzy mi rozsah komentáře přestal stačit. Jsem taky rád za změny, které byly v jazyce provedeny, a rozhodně nejsem příznivcem konzervativního přístupu za každou cenu (byl jsem od začátku pro i v případě lambda funkcí i modulů). Pár skutečností ale moje nadšení poněkud tlumí. Možná budete následující zamyšlení považovat za "hejt", ale pokusím se v něm tyto skutečnosti dát do souvislosti se situací, do které se Java 7 narodila.

Project Coin – vnější tvář Javy 7


Důraz na project Coin bylo něco, co mne na propagaci Javy 7 od začátku spíš zaráželo. Měl jsem dojem, že drtivá většina článků, blogů a nejrůznějších odborných komentářů prakticky ztotožňuje (nebo alespoň velmi protežuje) veškerou inovaci, kterou Java 7 přináší, s tímto projektem. Jsem si vědom, že od začátku byly změny zastřešené tímto projektem zamýšleny jako malé, které nemohou mít ambici být srovnávány s revolucemi typu generiky nebo lambda funkce. Nicméně mám dojem, že se míjejí účinkem v něčem, co je hlubší povahy. A když přemýšlím, co to je, docházím ke třem kořenům:
  • programovací styl. Neschválení původního záměru přidat do Javy Elvis operátor chápu jako snahu Javy podporovat čistý styl programování (i když osobně jsem pro jeho přidání). Nicméně pokud považuji Javu za objektový jazyk, podobnou podporu v některých nových rysech jazyka nevidím.
  • úroveň abstrakce. Nové rysy jazyka cílí na nižší úroveň abstrakce, než na jaké píšu kód při své běžné práci.
  • načasování. Nové rysy jazyka řeší problémy, které si už programátoři vyřešili pomocí externích (nebo vlastních) knihoven, a proto přicházejí poněkud pozdě.
Pojďme si ukázat pár příkladů:

Stringy ve switch


Každý, kdo přecházel z imperativního paradigmatu na objektové, si musel prožít ten aha-efekt, kdy kód s mnoha rozkopírovanými if/else
if ("dog".equals(animal)) System.out.println("woof");
else if ("cat".equals(animal)) System.out.println("miaow");
...
if ("dog".equals(animal)) eat(cat);
else if ("cat".equals(animal)) eat(mouse);

refaktoroval na jediný if/else, který je potřeba pouze v tovární metodě, neboť vše ostatní se promění na metody vzniklých tříd:

if ("dog".equals(animal)) {return new Dog();}
else if ("cat".equals(animal)) {return new Cat();}

Přidání Stringů do switch je tedy rozpačité z pohledu programovacího stylu, protože u tohoto typu případů se jeho relativní přínos lépe projeví při aplikaci imperativního, nikoli objektového paradigmatu. Kromě toho může podporovat použití Stringů tam, kde by byl vhodnější enum. A nemožnost switchovat přes null bohužel trvá i zde.

Podtržítka v literálech


Důležitější než rozplývat se nad tím, že místo 1000000 mohu psát 1_000_000, je podle mne přemýšlet: proč ten literál má hodnotu zrovna 1000000? Jak pomůžu tomu, kdo kód po mně bude číst, tento důvod co nejrychleji pochopit? Je to časový údaj? Porovnejte:
long timeInMillis = 300_000;
long timeInMillis = TimeUnit.MINUTES.toMillis(5);
Je to velikost v bajtech? Porovnejte:
long twoMB = 2_097_152;
long twoMB = 2 * 1024 * 1024;

A pro jiné případy si udělat pomocnou třídu. Takže podtržítka zčitelňují, ale k přemýšlení nad programováním zas tolik nenapomáhají. Oceňuji přidání binárních literálů (0b0000_0101_0101_1111), nicméně to zas naráží na problém úrovně abstrakce (využití bitových operací ve specializovaných oblastech) i načasování (spousta je toho již dnes napsána, kdo by opravoval na podtržítka např. algoritmy v java.lang.Math?).

Diamond operátor

"Diamond operátorem chudých" je obyčejná typová inference u generických metod. Tu využili mj. i autoři Guavy, kterým jistě také vadilo zdvojené psaní typových argumentů, jež bylo motivací vzniku diamond operátoru. Takže mám-li Guavu po ruce, pořád je mi blízké použít např. Lists.newArrayList(). I když samotní autoři Guavy už tento způsob de facto považují za deprecated, stále diamondu dobře konkuruje – minimálně z hlediska čitelnosti (ve vizuálním dojmu není tolik nepísmenkových znaků), programovacího stylu (použití factory metody) i načasování (je tu už od Javy 5). Samozřejmě má i nevýhody: typová inference není vždy překladači jasná (typicky prázdný seznam, to je zlepšeno až v Javě 8). Na diamond si postupně zvykám a určitě zvyknu, jen je třeba ho slepě neaplikovat všude. Např. nemožnost použít ho u anonymních tříd byla pro mne zklamáním.

Výjimky: AutoCloseable a multicatch

Podobná situace nastává i u featur okolo try/catch. Try-with-resources je skvělá věc, ale při řešení  běžných úloh a použití prověřených knihoven se zpravidla dostaneme na vyšší úroveň abstrakce a konstrukt nakonec nepoužijeme. Paradoxně je nám pak jedno, jestli to implementují pomocí try-with-resources nebo postaru, hlavně, že to mají dobře. Nejde pouze o moji oblíbenou Guavu (konkrétně com.google.common.io) nebo Apache commons IO, AutoCloseable je např. i trojice ResultSet, Statement, Connection z JDBC. Pokud takovou logiku už dnes každý volá přes nějakou template, pak je marginální zisk z této jazykové featury jen velmi malý. (Možná by jejímu rozšíření pomohla i větší otevřenost směrem k usecasům, které se netýkají přímo zavírání, ale pracují také s finally, např. lock/unlock, konkrétně tento usecase byl ovšem shozen ze stolu.)

O něco lepší situace se zdá u multicatche. Pro něj lze sice také najít příklad v duchu předchozího odstavce – jmenovitě reflection (uvažme, kolik existuje utilit pro pohodlné použití reflexe, které se i postarají o výjimky), nicméně v Javě 6 pro multicatch neexistuje srovnatelný konstrukt, nepočítám-li catchnutí Exception a následné rozhodování pomocí instanceof. (Je-li v takovém případě společnou obsluhou všech výjimek vyhození výš, nabízí Guava praktickou metodu propagateIfInstanceOf.)

Za nejlepší změnu v oblasti týkající se výjimek považuji more precise rethrow – ukázkový příklad, jak překladač může ušetřit přemýšlení programátorovi a zároveň ho při postupu hierarchií volání směrem výš nenutit ke zobecňování výjimek až k Exception. Malou výtku bych měl opět pouze k oblasti načasování, a to vzhledem k současnému trendu používat spíš runtime výjimky, u nichž se tento konstrukt tolik nevyužije.

Skryté inovace Javy 7


Když Oracle po akvizici Sunu zjistil, že lambdy a moduly zkrátka nezvládne v jednom releasu, došlo na rozhodnutí vydat "osekanou" Javu. K tomu se přidaly nově zjištěné bezpečnostní problémy, takže Java 7 spatřila světlo světa se značným zpožděním a po porodních bolestech (vzpomeňme na oznámení Marka Reinholda, které ve své době oblétlo Java svět). S přihlédnutím k těmto skutečnostem vnímám jako moudré rozhodnutí vydat release, který tvoří podhoubí budoucích revolučních změn.

Podíváme-li se na Javu 7 z tohoto pohledu, nevede si vůbec špatně. Rád bych zde odkázal na výborný článek Bena Evanse, který na toto téma vyšel v říjnu 2013. I když rozsahem není nijak dlouhý, přišel mi inspirativní, protože odhaluje souvislosti mezi změnami zavedenými v Javě 7 a změnami (tehdy) očekávanými do Javy 8. Dozvíme se tam například, proč není jedno, zda se diamond operátor používá na pravé či levé straně přiřazení a proč se inference nedělá zprava doleva po vzoru scalovského val. Method handles jako chytřejší a rychlejší java.lang.reflect.Method byly přichystány pro reprezentaci lambda funkcí, které jsou volány díky invokedynamic. Tato bytekódová instrukce si také v běžné propagandě získala pověst čehosi, co bylo přidané kvůli dynamickým jazykům, ale faktem je, že Java ji v případě lambd aplikuje i sama na sebe, pěkná prezentace na toto téma je zde.

Shrnutí

Javu 7 považuji za potřebný release, v němž se většina užitečných změn děje spíš "pod povrchem" a v duchu přípravy na revoluční Javu 8. Na druhou stranu nikdo, kdo má všech pět pohromadě, neřekne: "tady máte nový software, sice vypadá stejně jako předchozí, ale věřte nám, že uvnitř jsme ho vytunili!". Změny syntaxe a obecně změny, o kterých se dobře bloguje, posloužily marketingové stránce a to se taky počítá! O to cennější pak je, když se tyto dva pohledy podaří propojit.

(Upozornění: Prezentované názory nepostihují celou šíři změn, které release Javy 7 přinesl. Jsem také jistě zatížen pohledem na svoji oblast činnosti, což může pohled zkreslit. Budu proto u tohoto příspěvku obzvláště rád za nesouhlasné reakce, protože důvodně očekávám, že mohou být velmi obohacující. Díky.)

5 komentářů:

  1. Je hezke, kdyz autor primo vyzyva k nesouhlaseni :-) takze zde to je:

    Strucne receno - cele je to prave o pohledu na vasi oblast. Ja pisu desktopove programy radove desitky KLOC, kde neni moc prostor pro knihovny, takze si vse delam sam. Z toho duvodu vyuziji vetsinu novinek a jsem za ne rad.

    K jednotlivym bodum:
    Stringy ve switch jsem jeste nevyuzil a mate pravdu, ze na jejich miste je nejspis lepsi enum (coz je novinka 1.5)

    Podtrzitka - kdyz je to MB, tak to napisu jako nasobeni, ale kdyz mam milion prvku, tak proste napisu milion s podtrzitkama - drive jsem treba psal 1000 * 1000, abych se v tom vyznal, ted to usnadnuje. Vyuziju to treba pri testovani a benchmarcich. Dovedu si predstavit, ze jako init velikost nejakych poli se to take muze hodit.

    Diamond - snizuje ukecanost a redundanci, to je proste vetsinou dobre.

    Autocloseable - jednoznacne ohromny prinos! Ve starsim kodu jsem mel ponekud bordel a popravde jsem nedokazal rict, na ktere urovni se vlastne zastavi sireni IOException. Pri refactoringu me vlozeni streamu do try (...) donutilo v tom udelat poradek a dnes jednak vim, kde konci ta vrstva, druhak vim, ze to mam spravne (z hlediska leaku). A ve stejne situaci klidne mohou byt i autori tech vasich knihoven. Proste nemusi cekat, az to bude proverene, lze mnohem snaze zkontrolovat, ze je to opravdu dobre na prvni pokus...

    OdpovědětSmazat
    Odpovědi
    1. Ale jo, jestli tohle má být nesouhlas, tak s takovým nesouhlasem já souhlasím :-)
      Přesně tak, hodně jsem ve článku opíral argumentaci o knihovny, na Vašem místě bych jednal stejně.
      A případů, kdy se nové rysy využijou, je typově mnohem víc než jsme oba uvedli - u čtení parametru requestu nebo options z commandline je switch na stringy hodí. Inicializace polí i autocloseable - u obou taky uvádíte relevantní ukázky.

      Smazat
  2. Za zmínku určitě ještě stojí nové file system API (alias NIO 2), nahrazující problematickou třídu File a konečně přinášející high level metody typu Files.readAllLines() nebo Files.move() (která navíc může využít efektivních nativních operací). A kromě toho je konečně možné file systém např. pro unit testy kompletně namockovat, např. pomocí https://github.com/google/jimfs.

    Co se týče try-with-resources, tak bezpochyby jde o velmi pěknou fíčuru, ale doteď jsem se nedozvěděl uspokojivou odpověď na otázku, jak řešit situaci s vícenásobně wrapovanými zdroji:
    http://stackoverflow.com/a/12665271/1064809

    OdpovědětSmazat
    Odpovědi
    1. Díky za oba tipy, především otázku na SO! Myslím, že v praxi se to moc neřeší hlavně z důvodu, že nejčastěji používané třídy z java.io jsou i Closeable, takže se spoléhá na idempotenci.

      Smazat