Java - aserce

Aserce je příkaz v programovacím jazyce Java který vám umožní otestovat vaše předpoklady v programu. Například, když napíšete metodu která vypočítává rychlost částice, můžete tvrdit že vypočítaná rychlost je menší než rychlost světla.

Každá aserce obsahuje booleovský výraz o němž si myslíte že bude pravdivý po provedení aserce. Pokud není pravdivý, systém vyhodí chybu. Ověřením že booleovský výraz je pravdivý, aserce posiluje vaše předpoklady o chování vašeho programu, čímž zvyšuje vaši důvěru v to že program je bezchyb.

Zkušenost ukázala že psaní aserce při programování je jedním z nejrychlejších a nejefektivnějších způsobů jak zjistit a opravit chyby. A navíc aserce slouží k dokumentaci vnitřní funkčnosti vašeho programu, čímž zlepšují udržovatelnost.

Tento tutoriál vám ukáže jak programovat s tvrzeními. Zabývá se těmito tématy:

Úvod

Příkaz aserce má dva tvary. První, jednodušší tvar je:

	assert výraz1;

kde výraz1 je proměnná typu boolean. Když systém provádí aserce, vyhodnocuje výraz1 a pokud je false vyhodí výjimku AssertionError.

Druhý tvar příkazu aserce je:

	assert výraz1 : výraz2;

kde:

Použití této verze příkazu assert vypíše detailní správu pro výjimku AssertionError. Systém předá hodnotu výraz1 příslušnému konstruktoru AssertionError, který použije její řetězcovou reprezentaci k detailnímu popisu chyby.

Účelem detailní správy je zachytit a zprostředkovat detaily selhání aserce. Tato zpráva by vám měla umožnit a zjistit a nakonec opravit chybu která vedla k selhání aserce. Všimněte si že detailní zpráva není určena uživatelům, takže je obvykle nutné upravit tyto zprávy zvlášť nebo je internacionalizovat. Detailní zpráva je určena k interpretaci v kontextu s výpisem zásobníku, ve spojení se zdrojovým kódem obsahující selhání aserce.

Stejně jako všechny nezachycené výjimky, selhání aserce najdete ve výpisu zásobníku spolu se jménem třídy a číslem řádku ze kterého byly vyhozena. Druhý typ aserce by měl být preferován pouze v případě že program obsahuje nějaké dodatečné informace které mohou pomoci určit příčinu selhání. Například, pokud výraz1 určuje vztah mezi dvěma proměnnými x a y, měli byste použít druhý typ. Za těchto podmínek by rozumný kandidát na výraz2 mohl vypadat takto: "x: "+ x +", y: "+ y

V některých případech může být nákladné vyhodnotit výraz1. Například, předpokládejte že píšete metodu pro nalezení nejmenšího prvku v netříděném seznamu, a přidáte aserce abyste ověřili zda je označený prvek skutečně nejmenším. Práce vykonaná tvrzením bude přinejmenším stejně nákladná jako metoda sama. Abychom zajistili že aserce nebudou výkonostní zátěží aplikací, mohou být povolena nebo zakázána při startu programu, a defaultně i zakázána. Zákaz aserce úplně eliminuje jejich výkonostní zátěž. Po zákazu jsou ekvivalentní prázdnému příkazu. Viz. Povolení a zákaz aserce.

Přidání klíčového slova assert má za následek ovlivnění už existujícího kódu. Viz. Kompatibilita s existujícími programy.

Použití aserce ve vašem kódu

Je mnoho situací kde je vhodné použít aserce, včetně:

Jsou také situace, kde byste je neměli používat:

	// Porušení pravidla! akce je vykonána v aserce
	assert names.remove(null);
	// Opraveno - operace předchází aserce
	boolean nullsRemoved = names.remove(null);
	assert nullsRemoved; // Funguje nezávisle na stavu aserce

Vnitřní invarianty

Předtím než byly k dispozici aserce, mnoho programátorů používalo komentáře k označení svých předpokladů ohledně chování programu. Například, mohli jste napsat k vysvětlení vašeho předpokladu o příkazu else:

	if (i % 3 == 0) {
		. . .
	} else if (i % 3 == 1) {
		. . .
	} else { // Víme že (i % 3 == 2)
		. . .
	}

Teď byste měli používat aserce kdykoli byste napsali komentář který potvrzuje invariant. Například, měli byste přepsat předchozí kód takto:

	if (i % 3 == 0) {
		. . .
	} else if (i % 3 == 1) {
		. . .
	} else { 
		assert i % 3 == 2 : i;
		. . .
	}

Všimněte si, že aserce uvedené výše může selhat pokud je i záporné, protože % operátor není pravý operátor dělení, ale počítá jen zbytek, který může být záporný.

Jiným vhodným kandidátem pro aserce je příkaz switch bez příkazu default. Absence default značí že programátor věří že jeden z případů se vždy vykoná. Předpoklad že jedna z proměnných bude spadat do rozsahu hodnot jako může být považována za invariant který můžeme ošetřit tvrzením. Například, předpokládejte že se následující příkaz switch objeví v programu který představuje hrací karty:

	switch(suit) {
		case suit.CLUBS;
			. . .
		break;
		case Suit.DIAMONDS;
			. . .
		break;
		case Suit.HEARTS;
			. . . 
		break
		case Suit.SPADES;
			. . .
	}

Pravděpodobně to znamená, že proměnná suit bude mít pouze jednu ze čtyř hodnot. K otestování othoto aserce můžete použít tento defaultní příkaz:

	default:
		assert false : suit;

Pokud proměnná suit nabyde jiné hodnoty a aserce jsou povolena, aserce selže a vyhodí se výjimka AssertionError.

Přijatelnou alternativou je:

	default:
		throw new AssertionError(suit);

Tato alternativa nabízí ochranu pokud jsou aserce zakázána, ale nestojí vás nic navíc: příkaz throw se nevykoná dokud program neselže. Kromě toho, tato alternativa je povolena i za jiných podmínek kdy příkaz assert povolen není. Když obklopující metoda vrací hodnotu každý případ v příkazu switch obsahuje příkaz return, a žádný příkaz return nenásleduje příkaz switch, pak to může způsobit syntaktickou chybu když se pokusíme přidat za defaultní případ aserce (metoda by měla navracet i když nebyl proveden žádný případ a aserce byla zakázána).

Invarianty řízení toku

Předchozí příklad nejen že testuje invariant, ale také kontroluje předpoklad o řízení toku aplikace. Autor původního bloku switch pravděpodobně předpokládal že nejen že proměnná suit bude mít vždy jednu ze čtyř hodnot, ale také že jeden ze čtyř případů bude vždy vykonán. To poukazuje na další oblast kde můžeme použít aserce: používejte aserce kdekoli si myslíte že bude nedosažitelné. Použití aserce:

	assert false;

Například, předpokládejte že máte matodu jako je tato:

	void foo() {
		for (...) {
			if (...)
				return;
		}
		// Program by nikdy neměl dojít až sem!!!!
	}

Komentář můžeme doplnit tímto:

	void foo() {
		for (...) {
			if (...)
				return;
		}
		assert false; // Program by nikdy neměl dojít až sem!!!!
	}

Poznámka: používejte tuto techniku jen opatrně. Pokud je příkaz nedosažitelný, pak, jak je definováno ve specifikaci jazyka Java, bude následovat chyba překladu pokud se budete snažit tvrdit něco čeho není nikdy dosaženo. A opět, přijatelnou alternativou je vyhodit výjimku AssertionError.

Vstupní podmínky, výstupní podmínky a invarianty třídy

I když konstrukce assert plně rozvinutou schopností návrhu dle kontraktu, může napomoci neformálnímu stylu návrhu dle kontraktu. Tato sekce jak můžete použít aserce pro:

Vstupní podmínky

Dle konvence, vstupní podmínky pro veřejné metody jsou vynuceny explicitně tím, že vyhazují příslušné výjimky. Například:

	
	/**
	  * Nastavuje frekvenci obnovování
	  *
	  * @param  rate obnovovací frekvence, počet snímků za sekundu.
	  * @throws IllegalArgumentException pokud je rate <= 0 nebo
	  * rate > MAX_REFRESH_RATE
	*/
	
	public void setRefreshrate(int rate) {
		// Vynucení vstupní pomínky ve veřejné metodě
		if (rate <= 0 || rate > MAX_REFRESH_RATE)
			throw new IllegalArgumentException("Illegal rate: "+ rate);
		setRefreshInterval(1000 / rate);
	}

Tato konvence zůstane po přidání konstrukce assert nedotknuta. Nepoužívejte aserce ke kontrole parametrů veřejné metody. Aserce je nevhodné protože metoda se zaručuje že si vždy vynutí kontrolu argumentů. Musí kontrollovat argumenty nezávisle na tvrzeních. Navíc, assert nevyhozuje výjimku příslušného typu. Může vyhodit pouze výjimku AssertionError.

Avšak můžete použít aserce k otestování vstupní podmínky neveřejné metody o které si myslíte že bude pravdivá ať už udělá klient s třídou cokoli. Například, aserce je vhodné použít v následující "pomocné metodě" která je volána předchozí metodou.

	
	/**
	 * Nastavuje obnovovací interval (který musí souhlasit s počtem snímků za sekundu)
	 *
	 * @param interval obnovovací interval v milisekundách
	*/
	
	private void setRefreshInterval(int interval) {
		// Potvrzuje platnost vstupní podmínky v neveřejné metodě
		assert interval > 0 && interval <= 1000 / MAX_REFRESH_RATE : interval;
		. . . // Nastavíme obnovovací interval
	}

Všimněte si, že aserce selže jestliže je MAX_REFRESH_RATE větší než 1000 a klient nastavuje obnovovací frekvenci větší než 1000. Takže se v podstatě jedná o chybu v knihovně!

Vstupní podmínky závislé na stavu zámku

Třídy navržené por vícevláknové operace mají často neveřejné metody se vstupními podmínkami související s tím zda je nebo držen nějaký zámek. Například, není neobvyklé setkat se s něčím takovým:

	private Object[] a;
	public synchronized int find(Object key) {
		return find(key, a, 0, a.length);
	}

	// Pomocná rekurzivní metoda - volána vždy se zámkem na objektu
	private int find(Object key, Object[] arr, int start, int len) {
		. . .
	}

Do třídy Thread byla přidána statická metoda holdsLock která testuje zda dané vlákno drží zámek nad určitým objektem. Tato metoda může být použita s příkazem assert k nahrazení komentáře popisujícího vstupní podmínku závislou na stavu zámku, jak můžete vidět v následujícím příkladu:

	// pomocná rekurzivní metoda - volána vždy se zámkem
	private int find(Object key, Object[] arr, int start, int len) {
		assert Thread.holdsLock(this);
		. . .
	}

Všimněte si že je možné napsat aserce o stavu zámku když daný zámek není držen.

Výstupní podmínky

Výstupní podmínky můžete testovat tvrzeními ve veřejných i neveřejných metodách. Například, následující veřejná metoda používá příkaz assert ke kontrole výstupní podmínky:

	
	/**
	 * Vrací BigInteger jehož hodnota je (this-1 mod m)
	 *
	 * @param  m modul
	 * @return this-1 mod m
	 * @throws ArithmeticException 
	*/
	
	public BigInteger modInverse(BigInteger m) {
		if (m.signum <= 0)
			thorw new ArithmeticException("Modulus not positive: "+ m);
		. . . // Provedení výpočtu
		assert this.multpily(result).mod(m).equals(ONE) : this;
		return result;
	}

Občas je nezbytné uložit nějaká data před provedením výpočtu abychom mohli zkontrolovat výstupní podmínku. To můžete udělat pomocí dvou příkazů assert a jednoduché vnitřní třídy která ukládá stav jedné nebo více proměnných aby mohly být zkontrolovány (nebo překontrolovány) po provedení výpočtu. Například, předpokládejte že máte kód jako tento:

	void foo(int[] array) {
		// Operace s polem
		. . .
		// V tomto bodě bude pole obsahovat přesně ty samé prvky
		// jako před operací, v tom stejném pořadí
	}

Tady je způsob jak byste mohli pozměnit metodu aby používala aserce výstupní podmínky:

	void foo(final int[] array) {
		// Vnitřní třída která ukládá stav a provádí konečnou kontrolu stavu
		class DataCopy {
			private int[] arrayCopy;
			DataCopy() {
				arrayCopy = (int[]) array.clone();
			}

			boolean isConsistent() {
				return Arrays.equals(array, arrayCopy);
			}
		}

		DataCopy = null;	
		// Vždy projde, jako vedlejší efekt ukládá kopii pole
		assert ((copy = new DataCopy()) != null);
		. . . // Operace s polem
		// Ujistíme se že pole obsahuje ty samé hodnoty v tom samém pořadí
		assert copy.isConsistent();
	}

Tento idiom můžete snadno zobecnit bay ukládal více než jedno pole, a aby testoval nějaké komplexnější aserce týkající se hodnot před před a po výpočtu.

Možná máte nutkání nahradit první aserce (které se vykonává pouze kvůli svému vedlejšímu efektu) následujícím, více výmluvným příkazem:

	copy = new DataCopy();

Nedělejte to. Tento příkaz by zkopíroval pole ať už by byla aserce povolena nebo ne, čímž by porušil princip podle kterého aserce nemají žádné náklady když jsou zakázána.

Invarianty třídy

Invariant třídy je typem vnitřního invariantu který platí vždy pro všechny instance třídy, kromě toho když je instance v přechodu z jednoho konzistentního stavu do jiného. Invariant třídy specifikuje vztahy mezi mnoha atrinuty, a měl by být pravdivý po vykonání jakékoli metody. Například, předpokládejte že jste implementovali souměrnou seřazenou stromovou datovou strukturu. Invariantem by mohlo být to že strom je souměrný a správně seřazený.

Mechanismus aserce si nevynucuje žádný určitý styl pro kontrolu invariantů. Někdy je obvyklé zkombinovat výrazy které kontrolují požadované meze do jedné vnitřní metody která může být volána tvrzeními. V našem příkladu stromu by mohlo být vhodné implementovat soukromou metodu které zkontroluje zda je strom opravdu souměrný:

	// Vrací true pokud je strom souměrný
	prvate boolean balanced() {
		...
	}

Protože tato metoda kontroluje mez která by měla být pravdivá před a po vykonání jakékoli metody, každá veřejná metoda a konstruktor by měla obsahovat ihned před návratovým typem toto:

	assert balanced();

Obecně je nezbytné vložit podobnou kontroly do hlavičky každé veřejné metody pokud je datová struktura implementována nativními metodami. V takovém případě je možné že by chyba poškození paměti mohla zničit "nativní vrstvu" datové struktury mezi voláními metod. Selhání aserce v hlavičce takové metody může indikovat že se takové poškození paměti vyskytlo. Podobně je doporučeno začlenit kontrolu invariantů do hlaviček metod ve třídách které jsou měnitelné jinými třídami (ještě lépe, navrhujte třídy tak aby jejich stav nebyl viditelný jinými třídami!).

Pokročilá témata

Následující sekce hovoří o tématech která jsou platná jen pro zařízení s omezenými zdroji a systémy ve kterých nesmí být aserce zakázána. Pokud vás tato témata nezajímají, můžete přejít k další sekci Překlad souborů obsahujících aserce.

Odstranění všech stop po tvrzeních ze zdrojových souborů tříd

Programátoři vyvíjející aplikace pro zařízení s omezenými zdroji si mohou přát oprostit soubory tříd od aserce. Přestože to činí nemožným povolit aserce, tak to také zmenšuje velikost souborů tříd, což může vést ke zvýšení výkonu rychlejším načítáním tříd. Při absenci vysoce výkonného JIT to může vést ke snížení velikosti paměťového otisku a zvýšené výkonnosti za běhu.

Aserce samotná nenabízejí žádnou přímou podporu pro jejich odstranění ze souborů tříd. Avšak příkaz aserce může být použit ve spojení s idiomem "podmíněného překladu" popsaného v JLS 14.20., který umož�uje eliminovat jejich stopu v souborech tříd které vygeneruje.

	static final boolean asserts = . . .; // selhání povolit aserce
	if (asserts)
		assert <výraz>;

Požadavek povolení aserce

Programátoři některých rizikových systémů si mohou přát vynutit si povolení aserce. Následující idiom staického inicializátoru chrání třídu před inicializací bez povolení aserce:

	static {
		boolean assertsEnabled = false;
		assert assertsEnabled = true; // Záměrný vedlejší efekt!!!
		if (!assertsEnabled)
			throw new RuntimeException("Asserts must be enabled!!!");
	}

Umístěte tento statický inicializátor na začátek vaší třídy.

Překlad souborů obsahujících aserce

Aby překladač javac bral v úvahu kód obsahující aserce, musíte použít parametr -source 1.4 takto:

	javac -source 1.4 MyClass.java

Tento parametr je nezbytný pro zpětnou kompatiblitu.

Povolení a zákaz aserce

Defaultně jsou aserce za běhu zakázána. Dva parametry příkazového řádku vám dovolí povolit nebo zákázat aserce.

Povolení aserce různého rozsahu dosáhnete použitím parametru -enableassertions nebo -ea. K zákazu aserce různého rozsahu použijte -disableassertions nebo -da. Rozsah můžete určit pomocí dalších argumentů:

Například následující příkaz spustí program batTutor, s tvrzeními povolenými jen v balíčku com.wombat.fruitbat a jeho podbalíčcích:

	java -ea:com.wombat.fruitbat... BatTutor

Pokud příkazový řádek obsahuje více takovýchto argumentů, provedeje v daném pořadí před načtením jakýchkoli tříd. Například následující příkaz spustí program BatTutor s povolenými tvrzeními v balíčku com.wombat.fruitbat ale zakazuje je ve třídě com.wombat.friutbat.Brickbat:

	java -ea:com.wombat.fruitbat... -da:com.wombat.fruitbat.Brickbat BatTutor

Tyto přříkazy platí pro všechna načítání tříd. S jednou výjimkou, platí také pro systémové třídy (které nemají explicitní načítání). Výjimka se týká také příkazů bez argumentů, které (jak je naznačeno výše) neplatí pro systémové třídy. Toto chování umož�uje lehce povolit aserce ve všech třídách kromě těch systémových, což je žádoucí.

K povolení aserce ve všech systémových třídách použijte jiný příkaz -enablesystemassertions nebo -esa. podobně k zákazu aserce v systémových třídách použijte -disablesystemassertions nebo -dsa.

Například následující příkaz spustí program BatTutor s tvrzeními povolenými v systémových třídách a stejně tak v balíčku com.wombat.fruitbat a v jeho podbalíčcích:

	java -esa -ea:com.wombat.fruitbat...

Stav aserce ve třídě (jestli je povoleno nebo zakázáno) se nastavuje v čase inicializace a nemění se. Existuje však jeden případ, který vyžaduje speciální postup. Je možné, ael obecně nežádoucí, provádět metody nebo konstruktory před inicializací. To se může stát pokud třídní hierarchie obsahuje kruhovou závislost ve své statické inicializaci.

Pokud se příkaz assert vykoná předtím než je třída inicializována, musí se vše proběhnout tak jako kdyby byla aserce ve třídě povolena. Detaily viz. JLS.

Kompatibilita s existujícími programy

Přidání klíčového slova assert do programovacího jazyka Java nezpůsobuje žádné problémy s předešlými binárními soubory (.class soubory). Avšak pokud se budete snažit přeložit aplikaci obsahující assert jako identifikátor, obdržíte varování nebo chybovou hlášku. Abychom vám usnadnili přechod ze světa kde assert je platným identifikátorem do světa kde není, překladač podporuje dva módy:

Dokud nespecifikujete svůj požadavek použití módu 1.4 pomocí parametru -source 1.4, překladač bude pracovat v módu 1.3. Pokud zapomenete použít tento parametr, pak programy které používají nový příkaz assert se nepřeloží. Ponechání překladač používat starou sémanitku jako defaultní chování (to jest, používat assert jako identifikátor) bylo učiněno pro maximální kompatibilitu zdrojových kódů. Tento mód časem vymizí.