7. 4. 2015

Tři chytáky k typovým anotacím v Javě 8

Typové anotace patří mezi syntaktické změny přidané v Javě 8. Umožňují přidat anotace nejen k deklaraci typu jako dosud, ale i na mnoho dalších míst, kde může být použit. Oracle dokumentace v úvodním odkazu je poměrně skoupá na informace, ale pěknou představu o možnostech je možné si udělat z tohoto článku.

V porovnání s lambda funkcemi a streamy se mi tato featura zdá poměrně málo zmiňovaná. A když už zmíněna je, je často spojována jen s usecasem, který tvoří její jakousi vlajkovou loď: typová kontrola a podpora sémantické analýzy, okřídlený příklad s @Nullable nebo @NotNull. Nicméně odmyslíme-li si riziko vzniku "annotation hell", k němuž může koneckonců dojít i před verzí 8, přijde mi tato featura mnohem všestrannější. Na svém hobbyprojektu, o němž napíšu třeba někdy v budoucnu, mne napadlo využití, kvůli kterému jsem se o typové anotace začal více zajímat. Některé chování – ač je už teď považuji za logické – jsem však napoprvé nečekal a o tom bude tento článek.

Prerekvizity

  • omezím se pouze na použití v deklaraci typu
  • pro získání představy o tom, co jsme uvedenými ukázkami spáchali, je dobré vědět i o změnách v reflection API, především o hierarchii pod novým interfacem AnnotatedType.
  • anotace použité v ukázkách mají retention RUNTIME, což je podmínka viditelnosti v reflection
  • anotace použité v ukázkách mají target odpovídající příslušnému konstruktu jazyka (v opačném případě je sice anotaci možné napsat, ale také není v runtime viditelná)
  • anotace @Nullable je vybrána pouze pro srozumitelnost, nejde tu o samotnou kontrolu na null

Chyták #1: pole se anotují před hranatými závorkami


Máme vyjádřit deklaraci seznamu řetězců, samotný seznam může být null. Zápis je zřejmý:

private @Nullable List<String> field;

Teď co když místo seznamu má být pole? Zdálo by se, že stačí nahradit příslušný typ:

private @Nullable String[] field;

Omyl! Právě jsme deklarovali pole řetězců, o kterých říkáme, že jsou nullable, ale samotný typ pole je bez anotace. Tj. tomuto chybnému zápisu odpovídá ve světě kolekcí zápis:

private List<@Nullable String> field;

Správný zápis je:

private String @Nullable [] field;


Poučení: na první pohled se zdá, že díky syntaxi zápisu polí v Javě se anotace u kolekcí píšou obráceně než u polí. Abych vytěsnil tento dojem nekonzistence, pomáhám si představou, že typová anotace se uvádí těsně před to, co tvoří podstatu příslušného typu (název třídy u kolekce, hranaté závorky u pole).

Chyták #2: vícerozměrná pole se anotují zleva doprava


Co když máme vnořené kolekce nebo pole? (Pro rozlišení, co je čím anotováno, zde použiju víc různých anotací rozlišených čísly.) U kolekcí asi zápis

private @Nullable1 List<@Nullable2 List<@Nullable3 String>> field;

intuitivně pochopíme opět jako referenci na seznam, která sama může být null (1), prvky seznamu jsou opět seznamy, které mohou být null (2) a jejich prvky jsou řetězce, které také mohou být null (3).

Tipnete si, jak oanotovat pole polí řetězců? Poučeni předchozím bodem už víme, že anotace k polím máme psát před [], ale jak poznat, které závorky představují vnější a které vnitřní pole? V případě kolekcí fungovalo, že zápis deklarace anotovaného typu (např. @Nullable2 List<@Nullable3 String>) bylo možné bez roztržení použít jako typový argument v kolekci vyšší úrovně. Potřeboval jsem generovat zdroják a roztržení by generátor komplikovalo. Proto jsem se nejprve nechal svést k představě, že když např. pole řetězců dostaneme přidáním závorek za String, tj. String[] (barevně je zvýrazněn typ prvku pole), pak pole polí řetězců obdržíme přidáním závorek za String[], tj. String[][]. Dle této úvahy by zápis vypadal:

private @Nullable3 String @Nullable2 [] @Nullable1 [] field;

To je ovšem také omyl. K jeho vyvrácení nepotřebujeme ani Javu 8, stačí si představit pole s deklarovanou velikostí. Např. pole 5x10 je pole prvků String[10] délky 5 a zapíše se jako String[5][10], což potvrzuje, že roztrhnutí se nevyhneme. Správné umístění anotací je tedy:

private @Nullable3 String @Nullable1 [] @Nullable2 [] field;

Poučení: Vztah mezi hranatými závorkami a úrovněmi pole nám může být lhostejný, pouze pokud nepoužíváme anotace ani explicitní vyjádření délky pole. V případě použití alespoň jedné z uvedených featur je ale znalost této oblasti potřeba. Je dobře, že typové anotace byly navrženy konzistentně s dosavadní praxí.

Chyták #3: anotace parametru nesouvisí s anotací typu

Pozornému pohledu na reflection API neunikne, že v Javě 8 přibyla třída Parameter zprostředkující vlastnosti parametru metody, mezi něž patří i anotovaný typ. Současně však může mít anotaci samotný parametr. Uvažme metodu:

private void foo(
  @Nullable List<String> nls,
  List<@Nullable String> lns,
  String @Nullable [] nas,
  @Nullable String [] ans
);

V bodu 1 je návod, jak se pozná anotace typu. Jak ale Java rozezná, kdy anotace patří k parametru a kdy k jeho typu? Není to totéž?

Není. Pravidlo je zde jednoduché: za anotace na parametru se považují všechny zleva až po první syntaktický konstrukt, který není anotace. V případě uvedené metody má anotaci pouze první a čtvrtý parametr. Nicméně takto použité anotace jsou viditelné i od typu, pouze je uvidíme pokaždé jinde: v případě prvního parametru u typu kontejneru, v případě čtvrtého parametru u typu elementu.

Poučení: Toto uspořádání je celkem intuitivní a není na něm co řešit. Podtrhuje důraz na použití správného API na daný problém: jestliže má anotace povahu vlastnosti typu, je třeba se snažit ji získat přes typ, nikoli ji číst z parametru (a doufat, že typem nebude pole nebo se divit, že pro pole to nefunguje). A jestliže má anotace povahu vlastnosti parametru, přistupovat k ní přes anotace parametru. Pro úplnost dodávám, že tento bod se týká nejen parametrů metod, ale i fieldů tříd a samotných metod (jejichž anotace může takto proniknout i do typu fieldu resp. typu návratové hodnoty metody).

Závěr

Uvedené vlastnosti je možno si vyzkoušet na příkladu. (Pozor, v Javě 8u11 selže na ClassCastException – na řádku 120 je typ vyhodnocen jako AnnotatedTypeBaseImpl místo správného AnnotatedParameterizedType. V Javě 8u20 i 8u40 je to již v pořádku.)

Pro podrobnější seznámení odkazuji na specifikaci (upřímně, na bod 2 jsem si po vzoru strýce Františka přišel ještě před jejím objevením). Ta se zabývá i dalšími konstrukcemi jako anotací vnitřních tříd, výjimek apod. Je důležité si uvědomit, že v reflexi je s typovými anotacemi možné zacházet pouze v případech, kde to bylo dosud možné s typy. Tj. anotace na parametrech, metodách, třídách apod. jsou (za předpokladu runtime retention) přístupné přes reflexi, zatímco anotace v případech jako new @Foo Hoo() nebo String s = (@NotEmpty String)object nikoli, tyto případy spadají výlučně do kompetence nástrojů pro zpracování kódu.

Nakonec bych zde rád zmínil pěkný nástroj pro generování Java zdrojáku JavaPoet. Pouze jeho nepřipravenost na syntaktické konstrukty Javy 8 mě přiměla k zájmu o tuto oblast a napsání vlastního řešení. Pro nižší verze Javy bych se nebál ho doporučit a těším se, až po releasu verze kompatibilní s Javou 8 vlastní řešení opustím.