# Nulový objekt

Ako v tejto chvíli už určite viete, v Jave máme len niekoľko primitívnych dátových typov, pričom všetko ostatné sú tzv. referenčné typy. Tie sa správajú ako smerníky v C - v skutočnosti len nesú odkaz - referenciu - na objekt v pamäti. Čo však, ak chceme povedať, že premenná aktuálne neukazuje na žiadny objekt? Pre tento účel máme tzv. nulový ukazovateľ - null. Jeho autorom je britský počítačový vedec Sir Tony Hoare, ktorý ho v roku 1965 zakomponoval do jazyka ALGOL W. A ako sa na svoj výtvor pozerá pol storočia dozadu?

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

- Sir Tony Hoare, QCon London 2009

NullPointerException, teda chyba, kedy sa snažíme zavolať nad nulovou referenciou metódu, resp. sa dostať k jej atribútu, je jedna z najčastejších chýb, ktorá je úplne bežná aj vo veľkých a profesionálnych projektoch. Ak chceme túto chybu ošetriť tradičným spôsobom, môžeme buď zahltiť kód vetvením, alebo musíme na vstupe hneď overiť dáta a pokračovať s vykonávaním kódu len v prípade, že všetky dáta sú čisté (angl. sanitized).

Naskytá sa preto otázka - prečo potom máme null? Nevieme sa ho zbaviť? Nanešťastie, nakoľko je hlboko zakorenený v objektovo orientovanej paradigme, musíme jeho existenciu akceptovať, no ak ho nechceme používať, budeme potrebovať alternatívne spôsoby na vyjadrenie absencie hodnoty.

Veľmi populárny prístup, ktorý sme už v knihe ukázali, je použitie obaľovacej triedy Optional. Tá následne umožňuje funkcionálny prístup k vnútornej hodnote, pričom ak tá neexistuje, vaše operácie budú odignorované. Porovnajme si to na nasledujúcom príklade:















 
 
 


public int sumWithIf(List<Integer> list) {
	if (list == null) {
		return 0;
	}
    return list.stream().mapToInt(Integer::intValue).sum();
}

public int sumWithTernary(List<Integer> list) {
    return list == null
            ? 0
            : list.stream().mapToInt(Integer::intValue).sum();
}

public int sumWithOptional(List<Integer> list) {
    return Optional.ofNullable(list)
            .map(numbers -> numbers.stream().mapToInt(Integer::intValue).sum())
            .orElse(0);
}

Posledný prístup si síce vyžaduje napísať trošku viac kódu, no celý čas pracujeme s objektami a nestaráme sa o null. Len vyjadríme: Spočítaj všetky čísla, no ak náhodou nebudeme mať žiadnu hodnotu, vráť nulu'.

Tento prístup nie je zlý, ale bohužiaľ má svoje nevýhody. Optional nie je serializovateľný - účel tejto triedy je len riešiť absenciu hodnoty v rámci kódu (napr. ako návratová hodnota metódy), no nemá sa používať so žiadnym atribútom v triede. Taktiež jeho excesívne používanie znižuje čitateľnosť - a to aj zápismi ako napr. HashMap<Integer, Optional<String>>.

Alternatíva, o ktorej sa budeme v tejto kapitole rozprávať, je nulový objekt. Jeho princíp je jednoduchý - namiesto používania nulovej referencie budeme používať objekt, ktorý ju bude reprezentovať. Týmto prístupom dosiahneme to, že vždy budeme pracovať s objektom, ktorý len bude mať tú vlastnosť navyše, že buď je to platná hodnota, alebo prázdna. Na modelovej situácii si ukážeme, ako to vyzerá v praxi.

# Modelová situácia

V modelovej situácii si ukážeme 2 príklady - funkcionálny zoznam (po vzore z jazyka Scala) a ukážku toho, ako môžeme brať absenciu ako platnú hodnotu.

# Funkcionálny zoznam

Najčastejšie používaná implementácia zoznamu v Jave je ArrayList. Ako názov napovedá, vo vnútri si drží hodnoty v jednoduchom poli. Ďalšia populárna alternatíva, ktorú poznáme, je spájaný zoznam (angl. linked list). Ten je postavený na inom princípe - zoznam je reprezentovaný uzlom, ktorý v sebe obaľuje hodnotu a smerník na ďalší uzol. Tu sa častokrát používa práve null na označenie faktu, že zoznam je prázdny, resp. že uzol je posledný (ďalší uzol je null). V našej implementácii si ukážeme, ako môžeme nahradiť v takom prípade null za nulový objekt.

Poznámka

Pri spájanom zozname sú nové hodnoty pridávané na začiatok zoznamu, nie na koniec. Ide o Last-In-First-Out prístup. Tento poznatok je dôležitý najmä pri dizajnovaní algoritmov, ktoré s touto štruktúrou pracujú.

Náš prvý krok bude spočívať vo vytvorení abstraktnej triedy List, ktorá bude podporovať 3 operácie - dostať hlavičku (hodnotu), chvost (ďalší uzol) a taktiež overenie, či je uzol prázdny. Navyše, keďže nevieme, aké hodnoty bude zoznam obsahovať, musíme triedu urobiť generickú:

public abstract class List<T> {

	public abstract T head();
	public abstract List<T> tail();
	public abstract boolean isEmpty();
}

Abstraktná trieda je nám sama o sebe málo platná, ak nemáme konkrétne impementácie. Vytvoríme si teda privátnu vnorenú triedu NonEmpty, ktorá bude reprezentovať uzol s hodnotou. Dôvod, prečo ju skrývame, si vysvetlíme neskôr:











 



















public abstract class List<T> {
	// rest of the class omitted for brewity
	
	private static class NonEmpty<T> extends List<T> {

		private final T head;
		private final List<T> tail;

		private NonEmpty(T head, List<T> tail) {
			this.head = head;
			this.tail = Objects.requireNonNull(tail, "Tail cannot be null!");
		}

		@Override
		public T head() {
			return head;
		}

		@Override
		public List<T> tail() {
			return tail;
		}

		@Override
		public boolean isEmpty() {
			return false;
		}
	}
}

Keď si všimneme zvýraznený riadok, zistíme, že momentálne je nemožné vytvoriť akýkoľvek zoznam - nemáme ako definovať posledný prvok! Preto potrebujeme ešte jednu triedu - nulový objekt - Empty:








 




 









public abstract class List<T> {
	// rest of the class omitted for brewity
	
	private static class Empty<T> extends List<T> {

		@Override
		public T head() {
			throw new IllegalStateException("Head on empty");
		}

		@Override
		public List<T> tail() {
			throw new IllegalStateException("Tail on empty");
		}

		@Override
		public boolean isEmpty() {
			return true;
		}
	}
}

Ako sme očakávali, metóda isEmpty nám bude signalizovať, že uzol je prázdny. Pozrime sa však na zvýraznené implementácie metód head a tail. Ak by sme postupovali tradične, tieto hodnoty by pravdepodobne vrátili null, no pri nulovom objekte to nie je potrebné. Jednoducho budeme signalizovať, že programátor niekde urobil chybu, lebo si neskontroloval, či nepracuje s prázdnym zoznamom a pravdepodobne neadekvátne ošetril daný prípad.

V tejto chvíli môžeme konečne vytvoriť inštancie nášho zoznamu!

List<Integer> empty = new Empty<>();
List<Integer> oneElement = new NonEmpty<>(42, empty);
List<Integer> twoElements = new NonEmpty<>(314, oneElement);

Takáto práca s zoznamom je však veľmi nepekná, preto sme interné implementácie uzlov skryli. Musíme preto pridať dostatočnú funkcionalitu na to, aby vytváranie zoznamov bolo čo najpohodlnejšie. Začneme s najjednoduchším - prázdnym zoznamom. Keďže v matematike existuje práve jedna prázdna množina, dáva zmysel, aby bol prázdny uzol tiež len jeden - urobíme z neho singleton:

public abstract class List<T> {

	private static final List<?> NIL = new Empty<>();

	public static <T> List<T> nil() {
		//noinspection unchecked
		return (List<T>) NIL;
	}

	// rest omitted for brevity
}

To nám umožní dostať sa k prázdnemu uzlu pomocou statického volania factory metódy List.nil(). Názov Nil sme zvolili preto, lebo vo funkcionálnych jazykoch je to štandardné označenie pre zoznam, ktorý nič neobsahuje.

Teraz je na rade vytváranie neprázdnych zoznamov. Pre vytvorenie jednoprvkového budeme potrebovať ďalšiu factory metódu, ktorú si nazveme of:

public abstract class List<T> {

	public static <T> List<T> of(T element) {
		return new NonEmpty<>(element, nil());
	}

	// rest omitted for brevity
}

Vytváranie bude teda vyzerať ako List.of(42). Ostáva nám zabezpečiť, aby sme vedeli pridávať do zoznamu nové prvky. Na to vytvoríme už členskú metódu cons. Cons znamená construct a vo funkcionálnych zoznamoch sa toho označenie používane práve na vytvorenie nového zoznamu pridaním ďalšieho prvku:

public abstract class List<T> {
	
	public List<T> cons(T element) {
		return new NonEmpty<>(element, this);
	}

	// rest omitted for brevity
}

Nakoľko sme túto metódu dali do abstraktnej triedy List, je možné ju volať nad neprázdnym aj prázdnym zoznamom. Nasledujúce 2 zápisy sú ekvivalentné:

List<Integer> firstList = List.of(42).cons(314);
List<Integer> secondList = List.nil().cons(42).cons(314);

A tu sa ukazuje sila nulového objektu. Nie je absolútne nutné sa starať, aký typ zoznamu máme - všetky sa správajú rovnako. Ak chceme pridať nový prvok, jednoducho zavoláme list.const(element). Porovnajme si túto jednoduchosť s klasickým imperatívnym prístupom, kde sa spoliehame na nulovú referenciu:

List<Integer> list; // Assume we don't know where this value came from


if (list == null) {
	list = new List<>(42);
} else {
	list.cons(42);
}

# Absencia ako hodnota

Keď hovoríme o prázdnote medzi telesami vo Vesmíre, nehovoríme, že medzi napr. dvoma planétami nič nie je, ale že je medzi nimi vákuum. Samotnú absenciu častíc vnímame ako hodnotu, pretože časopriestor v tých miestach stále existuje (a rovnako aj kvantové polia).

V tejto ukážke si skúsime vymodelovať podobnú reprezentáciu nášho súkromného Vesmíru, ktorý bude mať pre jednoduchosť len 2 rozmery. Základná trieda Universe vyzerá nasledovne:




 



















public class Universe {

	private final int size;
	private final Quantum[][] universe;

	public Universe(int size, Quantum[][] universe) {
		this.size = size;
		this.universe = universe;
	}

	public int getSize() {
		return size;
	}

	public Quantum[][] getUniverse() {
		return universe;
	}

	public Quantum getQuantum(int x, int y) {
		return universe[x][y];
	}
}

Všimnime si zvýraznený riadok. Náš Vesmír sa skladá z menších častí Quantum. Kvantá predstavujú základné stavebné bloky, ktoré majú svoju hmotnosť, energiu a stav (tuhý, kvapalný, plynný a plazma). Ich implementácia by preto mohla vyzerať nasledovne:

public class Quantum {

	private final int mass;
	private final int energy;
	private final State state;

	public Quantum(int mass, int energy, State state) {
		this.mass = mass;
		this.energy = energy;
		this.state = state;
	}

	public int getMass() {
		return mass;
	}

	public int getEnergy() {
		return energy;
	}

	public State getState() {
		return state;
	}

	public enum State {
		SOLID,
		LIQUID,
		GAS,
		PLASMA,
		UNKNOWN
	}
}

Realisticky však bude väčšina priestoru prázdna - vyplnená vákuom. Ten by nemal mať žiadnu hmotnosť, žiadnu energiu a neurčitý stav. Takéto vákuum si môžeme vytvoriť ako singleton, podobne, ako sme to urobili s prázdnym zoznamom v prechádzajúcej ukážke. Tentokrát však nebudeme vytvárať novú triedu, len jednoducho inicializujeme objekt Quantum. Zároveň doplníme jednoduchú metódu, ktorá nám skontroluje, či dané kvantum je alebo nie je vákuum:



 












public class Quantum {

	private static final Quantum VACUUM = new Quantum(0, 0, State.UNKNOWN);

	public static Quantum vacuum() {
		return VACUUM;
	}

	public boolean isVacuum() {
		return mass == 0 && energy == 0 && state == State.UNKNOWN;
	}

	// rest omitted for brevity
}

A máme hotovo! Ak by sme v kóde chceli vytvoriť konkrétny Vesmír, vyzeralo by to nasledovne:

Quantum[][] quanta = {
	{ Quantum.vacuum(), new Quantum(1, 8, Quantum.State.GAS) },
	{ Quantum.vacuum(), new Quantum(3, 5, Quantum.State.LIQUID) }
};

Universe universe = new Universe(2, quanta);

# Best practices

  • Nulové objekty používajte ako zmysluplné predvolené hodnoty
  • Je vhodné, aby váš nulový objekt bol Singleton

# Úlohy

# 1. Doplňte základné operácie do funkcionálneho zoznamu

Náš funkcionálny zoznam síce dokáže vytvárať ľubovoľné zoznamy, ale to je bohužiaľ všetko. V tejto úlohe si ho preto rozšírime.

Začneme klasikou - toString, hashCode, equals. Zaručte, aby fungovali nasledovne:

  • toString by mal vypisovať (napr. pre čísla) List(), List(42) či List(42, 314)
  • hashCode musí zahrnúť hlavičku aj chvost. Nezabudnite ošetriť prípady prázdneho a jednoprvkového zoznamu
  • equals musí porovnávať nielen prázdny/neprázdny stav, ale aj hodnoty, ktoré uzly obaľujú

Následne pridajte komplikovanejšie metódy. Ich funkcionalitu nájdete popísanú v JavaDoc-u. Poznámka: metódu of nahraďte pôvodnou, cons pridajte ako alternatívu k už existujúcej:

/**
 * Creates a new list from given elements in specified order.
 *
 * Example:
 * List.of(42, 314) == List(42, 314)
 *
 * @param elements elements to be wrapped into list
 * @return new instance of a functional list
 */
public static <T> List<T> of(T... elements);

/**
 * Prepends all elements in front of this list
 *
 * Example:
 * List.of(42, 314).cons(List.of(161, 271)) == List(161, 271, 42, 314)
 *
 * @param that list to be prepended
 * @return new instance of a functional list
 */
public List<T> cons(List<T> that);

/**
 * Creates new list from the same elements with reversed order
 *
 * Example:
 * List.of(42, 314).reverse() == List(314, 42)
 *
 * @return new instance of a function list
 */
public List<T> reverse();

/**
 * Applies given callback to all elements.
 *
 * Example:
 * List.of(42, 314).forEach(number -> System.out.println(number))
 * > 42
 * > 314
 *
 * @param consumer callback that will consume elements from this list
 */
public void forEach(Consumer<T> consumer);

/**
 * Maps this list to a new one using given function
 *
 * Example:
 * List.of(42, 314).map(number -> number * 2) == List(84, 628)
 *
 * @param function function that will be used to map elements from this list to the new list
 * @return new instance of a function list
 */
public <R> List<R> map(Function<T, R> function);

# Riešenia úloh

# Pozri tiež

# Diskusia