Oříšek v reflexní analýze generik
Minulý týden jsem řešil zajímavý problém s reflexí a došel jsem k závěru, že generiky v reflexním API jsou opravdu velká legrace. Prototypoval jsem myšlenku automatického generování implementací nad obecným kontejnerem – dejme tomu Map
Problém mohu ukázat na příkladě:
Chtěl bych mít jednotné API pro přístup k primárnímu klíči.
Pak mám obecného předka Album:
A konkrétní implementace (PhotoAlbum, VideoAlbum, AudioAlbum) atd.:
V AOP proxy mám následně přístup k reflexnímu obrazu metody getFeaturedMediaItem a rád bych vrátil dynamickou proxy třídy, která typově odpovídá deklaraci. Volání:
mi však vrátí třídu Media. V kontextu třídy PhotoAlbum bych ale rád získal specifický typ Media a to je Photo – to je přeci z deklarace Java třídy krásně vidět. Tak jak se k této informaci dostat pomocí reflexe? Na rovinu říkám, že čím víc o generikách vím, tím víc mi připadají jako velmi komplexní rébus. Připadám si trošku jako když účetní nakoukne do učebnic kvantové fyziky.
Když odhlédnu od toho, že mě zajímá obecný princip jak se dostat k informaci o konkrétní třídě odpovídající zástupnému generickému symbolu, mohu se v tomto případě k cílovému typu dostat takto:
Legrační že? Mě tedy trvalo docela dlouho, než jsem do toho trošku pronikl. Teď ještě vymyslet způsob, jak tuto analýzu provést obecně na libovolné hierarchii tříd. V reflexním API se pracuje s obecným interfacem Type, který může být reprezentován několika různými podtypy:
- Class: klasický jednoduchý typ jako třeba String nebo Integer[] – to je v podstatě to, co hledáme
- TypeVariable: proměnný typ – např. T – to je něco co potřebujeme vyhodnotit
- ParameterizedType: parametrizovaný typ, např. List
nebo Set extends Number> – to je něco, co nám dokáže poskytnout informace o přiřazení TypeVariable na Class, minimálně jsme schopni přes to získat tzv. “upper bounds” - GenericArrayType: generické pole – tedy List>[] nebo T[]
- WildcardType: wildcard, např. ? extends Number nebo ? super Long – tady na tomhle objektu jsme schopní se zeptat na “upper bounds” a “lower bounds”
Pouze v případě ParameterizedType se dokážeme zjistit, jestli některému proměnnému typu nebyla přiřazena konkrétní class – díky volání metody getActualTypeArguments(). Problémem zůstává jak zjistit, co bylo čemu vlastně přiřazeno. Tady musíme porovnávat původní typy získané přes getTypeParameters() s naším hledaným typem v TypeVariable. Celé je to dost zamotané, protože vyhodnocené typy a původní typy získáváme přes různá volání – tj. getSuperClass() a getGenericSuperClass(). Porovnání typů se navíc nesmí dělat porovnáním referencí (==), ale přes volání metody equals(). Zkrátka a prostě, je to taková dobrá mentální rozcvička. Po pár hodinách zkoušení jsem ve svém kódu došel k implementaci, která mi dokázala vyhodnotit konkrétní typy přes celou hierarchii nadřízených tříd a všech odkazovaných interfaců (ke stažení zde).
Nejvtipnější na tom celém je to, že když jsem do celé záležitosti jakž tak pronikl, tak jsem našel hledanou utilitu hotovou a vyladěnou ve Spring Frameworku. Pro příklad uvádím jednoduchý test, který získá informace, které jsem hledal (plus další test na extenzi generické HashMap pro ukázku resolvování generického typu v argumentu metody):
Tím, že jsem si prošel delší a trnitější cestou, jsem se minimálně donutil vztahům v reflexi generik trošku porozumět (i když na rovinu říkám, že si pořád nejsem moc jistý v kramflecích). Na druhou stranu se čím dál víc přesvědčuji o tom, že knihovna Springu v sobě skrývá nečekané poklady – jen je umět najít. Neuplyne půl roku abych nějaký podobný poklad neobjevil.
Jen mě stále trápí otázka, proč tak jednoduché API pro dotazování generik jaké má Spring – zopakujme si:
UŽ NENÍ K DISPOZICI V ZÁKLADNÍCH BALÍCÍCH JAVY !!!
Zdroje ze kterých jsem čerpal
- Velmi pěkně popsané pozadí analýzy generik pomocí reflexe
- Tohle je tak trošku předskokan prvně uvedeného článku – s tímhle bych doporučoval začít
- Základ metody v mém článku v podstatě vznikl na základě tohoto článku
- FAQ dotazy ohledně generik a reflexe




(3 hlasů, průměrně: 4.67 z 5)
Ahoj,
to co tady pises se mi bude casem hodit. Ale nejdrive musim vyresit jiny orisek, myslim ze je to jeste o level obtiznejsi.
Delam po vecerech http://jni4net.sf.net, objektovy bridge mezi Javou a C#. Tak jako ty pouzivam reflection a z ni generuju proxy. Rozdil je v tom ze k Javovske tride generuju C# proxy. A ted ten orisek.
1) Pro ArrayList<E> na strane C# potrebujeme zaroven ArrayList<E> a zaroven ArrayList.
Pri cemz ArrayList je idealne zdedeny z ArrayList<Object>, co se signatur metod tyce.
Ale na druhou stranu, prirazeni ArrayList x = new ArrayList<String>(); musi byt ok.
Da se to asi prechytracit pomoci implicitnich konverzinch operatoru v C#. Ale pouze pro tridy, pro interfacy je to prohrane.
2) Java ma wildcard “?”. Ten znamena neco ve smyslu, dej si tam co chces, na runtime to bude stejne Object. Z toho vyplyva nasledujici:
a) Existuje bounded wildcard. V C# existuje pouze upper bound. S lower bound je to horsi Collection<? super Person>. Ja doufam ze se v prirode vyskytuje vzacne a proto ho muzu zanedbat.
Tady se da genericky wildcard ? vynechat na deklaraci tridy a pridat na deklaraci metodygenericky parametr T, to resi nektere zapeklitosti. Nevim jeste jestli vsechny.
b) Existuje dedicnost z wildcard typu, a to je mazec. Vubec nevim co s tim.
WeakClassKey extends WeakReference<Class<?>>{
Class<?> getClass(){
}
}
Zatraceny type erasure, jakakoliv dobra rada je mi cenna.
Pavel
Ono je vôbec zaujímavé, že sa to dá takto “hacknúť’.
Zmyslom erasure je aj to, aby to nešlo. Kvôli spätnej kompatibilite.
Vďaka za tip!
ad Pavel Savara) přiznám se, že jsem se snažil proniknout do tvých problémů, ale moc se mi to nezadařilo. Potřeboval bych asi hlubší poznání toho problému, abych dokázal reagovat. Navíc v C# jsem úplný analfabet, takže nevím, jaké možnosti / problémy jsou na druhé straně – nicméně věřím, že se snažit implementovat konverzi generik z dvou, takto oddělených světů musí být oříšek na druhou.
Díky za komentář, ale za mě se bohužel žádné cenné reakce asi nedočkáš
ad Marián) Pokud by bylo záměrem informace o typech zcela skrýt, pak by pravděpodobně neexistovaly metody jako Class.getGenericSuperclass(). Smyslem erasure je zpětná kompatibilita a ta těžko může být možností inspekce prostřednictvím reflexe nějak dotčena.
Tento problem sem taky resil. Nakonec sem rezignoval na genericky primarni klic apouzivam String. Pokud z nejakeho duvodu je databaze schopna pracovat rychleji s klicem jineho typu (napr. celociselnym, nebo Key v GAE) tak neni problem prevod mezi klicem a stringem naimplementovat primo v metode getId() a setId() a mapovat ho do jineho fieldu s typem vhodnym pro tu kterou databazi.
Zjistis ze si tak usetris strasnou spoustu problemu. Hlavne strasnou spoustu psani. Po nekolika hroznych mesicich straveny spodobnymi pokusy sem prisel na to ze generiky na tohle fakt nejsou vhodne.
ad benzin) musím říct, že to byla trochu škola, ale mám pocit že se mi to podařilo protunelovat – knihovnu na řadě instalací už používáme a funguje to poměrně intuitivně a hlavně bez dalšího přemýšlení … složitý kód je v AOP advicách ale klientský kód už je velmi přehledný a čitelný
ad Pavel Savara) O C# nevím vůbec nic, nicméně k těm bounded wildcards – upper a lower bound tvoří logickou dvojici třeba ve vztahu operátor/operand. Například pro setřídění List mohu použít Comparator. Prostě podle toho, co dělám, se na hierarchii dědičnosti dívám z jedné nebo druhé strany. Je to dost přirozený požadavek, těžko říct, zda se “v přírodě” vyskytuje vzácně.
Ha zmizely špičaté závorky… No ale snad je to jasné, prostě cokoliv typu MyType nebo jeho podtypu mohu předhodit čemukoliv co umí pracovat s typem MyType nebo jeho nadtypem.