Tohle byl pro mě nějakou dobu oříšek, než jsem narazil na pár článků s překvapivým – ne dokonalým, ale přeci jen nějakým řešením.
Problém je jednoduchý, chtěl bych aby bylo možné v nějaké abstraktní třídě definovat cosi jako:
/** poznámka: toto je nesmysl, ale vyjadřuje
moji snahu o vyjádření vazeb **/
abstract class AbstractClass<T is this> {
T getMe();
}
Což jsem potřeboval z důvodu získání reference na AOP proxy obalující moji třídu – v níže uvedených odkazech podobná potřeba vznikla při implementaci builder patternu.
Hezké řešení pro tento problém neexistuje, ale je možné napsat generickou vazbu, která podobný zápis umožní.
public abstract class AbstractParent<SelfType
extends AbstractParent<SelfType>> {
protected final StringBuilder data = new StringBuilder();
@SuppressWarnings("unchecked")
protected SelfType getMe() {
return (SelfType) this;
}
public SelfType addText(String something) {
data.append(something);
return getMe();
}
public String buildString() {
return data.toString();
}
}
/**
* PRVNÍ POTOMEK - DEFINUJE NOVOU VLASTNÍ METODU, ZBYTEK DĚDÍ
**/
public class Bar extends AbstractParent<Bar> {
public Bar makeLowerCase() {
//nasbíraná data jen lowercasneme
data.replace(0, data.length(), data.toString().toLowerCase());
return getMe();
}
}
/**
* DRUHÝ POTOMEK JE ANALOGIE K PRVNÍMU, JEN DEFINUJE JINOU METODU</pre>
**/public class Foo extends AbstractParent<Foo> {
public Foo makeUpperCase() {
//nasbíraná data jen uppercasneme
data.replace(0, data.length(), data.toString().toUpperCase());
return getMe();
}
}
Dosažení mého původního cíle mohu dokumentovat na tomto testu, který se podaří zkompilovat (stejně tak funguje správně i auto-completion v IDE):
@Test
public void tryGenerics() {
assertEquals(
"hello world!",
new Bar().addText("Hello ").addText("world!")
.makeLowerCase().buildString()
);
assertEquals(
"HELLO WORLD!",
new Foo().addText("Hello ").addText("world!")
.makeUpperCase().buildString()
);
}
Duležitá pasáž ve výše uvedeném příkladě je toto (pokud byste sami nepostřehli):
class AbstractParent<SelfType extends AbstractParent<SelfType>>
Což říká, že SelfType musí být potomek stejné třídy jako je ta, kterou právě definujeme. Je to poměrně obskurní zápis, který by mne (přiznám se) rozhodně nenapadl. Důležité je, že potomci musí ve své hlavičce uvést také poměrně zvláštní deklaraci (bez níž správného vyhodnocení generik nedosáhneme):
class Bar extends AbstractParent<Bar>
Možná tenhle trik znáte, ale já jsem o něm docela dlouho nevěděl – nedařilo se mi správně zeptat Googlu, jelikož jsem se motal pořád okolo klíčového slova this, protože intuitivnější by mě skutečně přišlo použití tohoto slova. Klíčem k rozluštění je hledat řetězec java generics selftype.
Třeba se Vám můj „objev“ bude také hodit.



Poznámka: jazyk Kotlin od JetBrains bude mít generickou podporu This zabudovanou – viz. diskuse zde: http://confluence.jetbrains.net/display/Kotlin/Generics?focusedCommentId=44237386&#comment-44237386
Je dobré si pamatovat, že přesně touhle konstrukcí je definováno java.lang.Enum. Pak už se to hledá snadno
Taky to na pár místech používáme. Je to fajn trik. Akorát když pak vedle této hierarchie vznikne nějaká paralelní (např. AbstractParentFactory, FooFactory, BarFactory), zatáhnou se generiky i do ní a pak jsou všechny třídy z obou hierarchií parametrizovány dvěma typovými parametry, jedním z každé hierachie. Což je někdy na škodu čitelnosti, ale pořád je zachována typová bezpečnost.
Parada, zrovna dnes se mi to hodilo
Častěji používám generickou metodu, protože je jednodušší:
@SuppressWarnings(„unchecked“)
T getMe() {
return (T) this;
}
Hodí se to však pouze pro případy, kdy potomci o sobě nevědí, protože je možné toto:
public class Bar extends AbstractParent {
public Bar makeLowerCase() {
return getMe();
}
public Foo m() {
return getMe();
}
}
Volání m() pak pochopitelně skončí výjimkou.
Ad) Zdeněk Troníček – z příkladu úplně nechápu, jak zajistíš to odvození typu na potomka – tj. aby bez castování prošel zápis:
new Bar().addText(„Hello „).addText(„world!“).makeLowerCase().buildString();
Ta volání addText by se v tvém případě vyhodnotila na AbstractParent, kde ta metoda makeLowerCase() chybí.
Nebo jsem to jen špatně pochopil? Nezmizely ti generiky při přidávání komentáře?
No jo, zmizely. Tak ještě jednou:
@SuppressWarnings(„unchecked“)
<T extends AbstractParent> T getMe() {
return (T) this;
}
Jinak ten příklad, který uvádíš, neprojde. V tomto ohledu to samozřejmě na Tvoje řešení nemá.
Neco podobneho jsem resil pri vytvareni obecneho nodu ve strome, kdy jsem chtel zajistit, aby deti byly stejneho typu jako definovany node (selftype), a take, aby parent byl stejneho typu. Zaroven zachovat typovou bezpecnost a dedeni (genericky typ nahradit potomkem, ktery ma navic nejake atributy):
Druhy pokus:
http://stackoverflow.com/questions/6864315/java-tree-node-recursive-generics
Tak tohle už je šílenství na druhou
.. díky moc za odkaz.
Bude to asi hloupy dotaz, ale proc je class AbstractParent deklarovana abstraktni? Ma to nejaky vyznam v tom prikladu, nebo nad tim premyslim zbytecne?
IMHO – Abstract je možné klidně vynechat a generiky to neovlivní. V příkladu je to hlavně proto, aby čtenář očekával, že funkce této třídy je pouze v tom, že má sloužit jako předek pro nějaké potomky, který sám o sobě nemá valný význam.
Pravda je, že ve svých použitích to mám podobně (včetně abstraktní třídy), protože je to poměrně typický use-case – tímto zápisem generik umožňujeme vynutit nějaké chování na úrovni potomků a tím pádem by naše třída měla být pouze jako abstraktní předek, ze kterého se dědí. Ale určitě mohou být i další usecasy, kdy chceme mít už i předka neabstraktního.