JE_18: Dávejte přednost statickým členským třídám nestatickými

Vnořená třída je třída definovaná v jiné třídě. Smylsem existence vnořené třídy by mělo být pouze posluhování její obklopující třídě. Pokud by byla taková vnořená třída užitečná i v nějakém jiném kontextu, pak by se mělo jednat o třídu nejvyšší úrovně. Existují čtyři druhy vnořených tříd: statické členské třídy, nestatické členské třídy, anonymní třídy a lokální třídy. Všechny kromě toho posledního druhu se označují za vnitřní třídy. Tato rada vám vysvětlí, kdy je zapotřebí použít jednotlivé druhy vnořených tříd a proč.

Nejjednoduším druhem vnořené třídy je statická členská třída. Nejlepší je považovat ji za obyčejnou třídu, která je náhodou deklarovaná uvnitř jiné třídy a má přístup ke všem členům své obklopující třídy, i k těm deklarovaným jako soukromé. Statická členská třída je statickým členem obklopující třídy a platí pro ni stejná pravidla ohledně přístupnosti, jako pro ostatní statické členy. Je-li delarovaná jako soukromá, pak je přístupná pouze v rámcí obklopující třídy atd.

Jedním obecným využitím statické členské třídy je její využití jako veřejné pomocné třídy použitelné pouze ve spojení s její vnější třídou. Zvažme například typově zabezpečený výčet popisující operace podporované nějakým kalkulátorem (rada 21). Třída Operation by měla být veřejným statickým členem třídy Calculator. Klienti třídy Calculator se pak mohou odkazovat na operace pomocí názvů jako Calculator.Operation.PLUS a Calculator.Operation.MINUS. Toto využití je demonstrováno dále v této řadě.

Syntakticky spočívá jediný rozdíl mezi statickými a nestatickými členskými třídami v tom, že statické členské třídy mají ve svých deklaracích modifikátor static. I přes svou syntatktickou podobnost se tyto dva druhy vnořených tříd výrazně odlišují. Každá instance nestatické člnské třídy je implicitně asociována s obklopující instancí její zahrnující třídy. Uvnitř metod instance nestatické členské třídy je možné volat metody v obklopující instanci nebo získat odkaz na obklopující instanci pomocí kvalifikované konstrukce this [JLS, 15.8.4]. Může-li nějaká instance vnořené třídy existovat izolovaně od instance své obklopující třídy, pak nemůže být vnořená třída nestatickou členskou třídou: není možné vytvořit instanci nestatické členské třídy bez obklopující instance.

Asociace mezi instancí nestatické členské třídy a její obklopující instancí se vytváří v okamžiku tvorby první uvedené instance; později už ji nelze změnit. Normálně se tato asociace ustanovuje automaticky voláním konstruktoru dané nestatické členské třídy z nějaké metody instance obklopující třídy. Je také možné, třebaže jen výjimečně, vytvořit tuto asociaci manuálně pomocí výrazu enclosingInstance.newMemberClass(args). Jak asi očekáváte, tato asociace zabírá prostor v instanci nestatické členské třídy a prodlužuje dobu její konstrukce.

Jedním obvyklým použitím nestatické členské třídy je definování adaptéru [Gamma95, str. 139], který umož�uje dívat se na instanci vnější třídy jako na instanci nějaké nesouvisející třídy. Například implementace rozhraní Map typicky používají nestatické člesnké třídy k implementování svých pohledů na kolekce, které se vracejí metodami keySet, entrySet a values třídy Map. Podobně implementace rozhraní kolekcí, jakými jsou Set a List, typicky používají nestatické členské třídy k implemetnování svých iterátorů:

// Typické použití nestatické členské třídy
public class MySet extends AbstractSet {
	. . .

	public Iterator iterator() {
		return new MyIterator();
	}

	private class MyIterator implements Iterator {
		. . .
	}
}

Deklarujete-li členskou třídu, která nevyžaduje přístup k obklopujíc instanci, nezapome�te uvést v její deklaraci modifikátor static, čímž z ní učiníte statickou a nikoli nestatickou člesnkou třídu. Vynecháte-li modifikátor static, bude každá instance obsahovat nadbytečný odkaz na obklopující objekt. Správa takového odkazu stojí čas a prostor a neposkytuje žádné výhody. Budete-li někdy náhodou potřebovat alokovat nějakou instanci bez obklopující instance, pak to nebudete moci učinit, protože instance nestatických členských tříd musejí mít obklopující instanci.

Soukromé statické členské třídy se obvykle používají k reprezentování komponent objektu představovaného jejich obklopující třídu. Vezměme si například instanci třídy Map, která přiřazuje klíče k hodnotám. Instance Map mají typicky interní objekt Entry pro každou dvojici klíč - hodnota v mapě. Třebaže je každé zadání asociováno s mapou, metody položek (getKey, getValue a setValue) nemusejí přistupovat k mapě. Proto by bylo plýtváním používat k reprezentaci položek nějakou nestatickou člesnkou třídu; nejlepší je využít soukromou statickou členskou třídu. Jestliže náhodou zapomenete na modifikátor static v deklaraci zadání, bude mapa sice fungovat, ale každé zadání bude obsahovat nedbytečný odkaz na mapu, což je plýtvání prostorem a časem.

Je dvojnásobně důležité správně si vybrat mezi statickou a nestatickou členskou třídou, pokud je daná třída veřejným nebo chráněným členem nějaké exportované třídy. V takovém případě je daná členská třída elementem exportovaného API a nelze ji změnit z nestatické na statickou členskou třídu v následující verzi, aniž by došlo k narušení binární komaptibility.

Anonymní třídy se nepodobají ničemu jinému v programovacím jazyce Java. Jak asi tušíte, anonymní třída nemá žádný název. Není členem své obklopující třídy. Není deklarována spoelčně s dalšími členy, ale deklaruje se a inicializuje najednou v okamžiku svého použití. Anonymní třídy lze používat kdekoli v kódu, kde se může vyskytnout nějaký výraz. Anonymní třídy se chovají jako statické nebo nestatické členské třídy podle toho, kde se vyskytují: mají obklopující instance, vyskytují-li se v nestatickém kontextu.

Použitelnost anonymních tříd je omezena z několika hledisek. Protože se současně deklaruje i inicializuje, lze anonymní třídu použít, pouze pokud má být vytvořena její instance v jediném místě kódu. Jelikož nemají anonymní třídy žádný název, lze je použít, jen když je zapotřebí odkazovat se na ně po vytvoření jejich instance. Anonymní třídy obvykle implementují pouze metody ve svém rozhraní nebo ve své nadtřídě. Nedeklarují žádné nové metody, protože neexistuje žádný pojmenovatelný typ umož�ující přístup k těmto metodám. Protože se anonymní třídy vyskytují uprostřed výrazů, měly by být velmi krátké, třeba dvaceti řádkové nebo menší. Delší anonymní třídy by narušovaly čitelnost programu.

Anonymní třídy se obvykle používají k vytvoření funkčního objektu, jako je například instance Comparator. Kupříkladu následující volání metody řadí pole řetězců podle jejich délky:

	// Typické použití anonymní třídy
	Arrays.sort(args, new Comparator() {
		public int compare(Object o1, Object o2) {
			return ((String)o1).length() - ((String)o2).length();
		}
	});

Dalším možným využitím anonymní třídy je vytváření procesních objektů, jakými jsou například instance Thread, Runnable nebo TimerTask. Třetí využití spočívá ve statických továrních metodách (viz. metoda intArrayAsList v radě 16). Čtvrté obvyklé použití najdeme v inicializátorech veřejných statických finálních atributů složitých, typově zabezpečených výčtů, které vyžadují samostatnou podtřídu pro každou instanci (viz. třída Operation v radě 21. Je-li třída Operation statickou členskou třídou Calculator, jak bylo doporučeno výše, pak jsou jednotlivé konstanty Operation dvojitě vnořenými třídami:

// Typické použití veřejné statické člesnké třídy
public class Calculator {
	public static abstract class Operation {
		private final String name;

		Operation(String name) {
			this.name = name;
		}

		public String toString() {
			return this.name;
		}

		// Vykonat aritmeetickou operaci představovanou touto konstantou
		abstract double eval(double x, double y);

		// Dvojitě vnořené anonymní třídy
		public static final Operation PLUS = new Operation("+") {
			double eval(double x, double y) {
				return x + y;
			}
		};

		public static final Operation MINUS = new Operation("-") {
			double eval(double x, double y) {
				return x - y;
			}
		};

		public static final Operation TIMES = new Operation("*") {
			double eval(double x, double y) {
				return x * y;
			}
		};

		public static final Operation DIVIDE = new Operation("/") {
			double eval(double x, double y) {
				return x / y;
			}
		};
	}

	// Vrácení výsledku zadaného výpočtu
	public double calculate(double x, Operation op, double y) {
		return op.eval(x, y);
	}
}

Lokální třídy se ze všech druhů vnořených tříd používají asi nejméně. Lokaální třída může být deklarována kdekoli, kde se může nahcázet dekalarace lokální porměnné, a chová se podle stejných pravidel oboru platnosti (rozsahu). Lokální třídy mají některé atributy společné s každým ze tří dalších druhů vnořených tříd. Podobně jako členské třídy mají názvy a lze je používat opakovaně. podobně jako anonymní třídy mají obklopující instance jen tehdy a právě tehdy, když jsou použity v nestatickém kontextu. Podobně jako anonymní třídy by měly být krátké, aby nesnižovaly čitelnost obklopující metody nebo inicializátoru.

V rámci rekapitulace tedy můžeme říci, že existují čtyři různé druhy vnořených tříd, z nichž každá má své místo. Má-li být nějaká vnořená třída viditelná mimo jedinou metodu nebo je-li příliš dlouhá a nevejde se so nějaké metody, pak použijte členskou třídu. Musí-li každá instance takové členské třídy obsahovat odkaz na svou obklopující instanci, uči�te ji nestatickou; jinak ji udělejte statickou. Jestliže daná třída patří do nějaké metody, pak pokud potřebujete vytvářet instance jen na jediném místě a přitom máte k dispozici již existující typ charakterizující danou třídu, udělejte z ní třídu anonymní; jinak použijte lokální třídu.