Žolíci (wildcards)

V předchozích částech jsme se seznámili s hlavní myšlenkou generics a její realizací, a sice nahrazení konkrétního nadtypu (většinou Object) typem obecným. Nicméně tohle samo o sobě je velmi omezující a nedostačující. Nyní se tedy ponoříme hlouběji do tajů generics.

Představme si následující situaci. V programu chceme mít seznam, kde budou jako prvky různé jiné seznamy. První nápad, jak jej nadeklarovat může být třeba tento:


      
      List<List<Object>> seznamSeznamu;
      
    

Na první pohled se to zdá být bez chyby. Máme seznam, kam budeme vkládat jiné seznamy a jelikož každý seznam musí obsahovat instance třídy Object, můžeme tam vložit libovolný seznam, tedy třeba i náš List<Number>. Nicméně tato úvaha je chybná. Uvažujme následující kód:


      
      List<Number> cisla = new ArrayList<Number>();
      List<Object> obecny = cisla;
      obecny.add("Ja nejsem cislo");      
      
    

Jak vidíme, „něco je špatně.“ To, že se pokoušíme přiřadit do seznamu objektů obecny řetězec "Ja nejsem cislo" je přece naprosto v pořádku, do seznamu objektů můžeme skutečně vložit cokoliv. V tom případě ale musí být špatně přiřazení na druhém řádku. To znamená, že seznam čísel není seznamem objektů! Zde je vidět rozdíl oproti „klasickému“ uvažování v mezích dědičnosti. Přečtěte si pozorně následující větu a pokuste se pochopit její význam.

Do seznamu, který obsahuje nejvýše čísla lze vkládat pouze objekty, které jsou alespoň čísly.

Z toho vyplývá, že je nelegální přiřazovat objekt „seznam čísel“ do objektu „seznam objektů.“ Tedy, vrátíme-li se k našemu příkladu se seznamem seznamů, vidíme, proč byla naše úvaha chybná. Do námi definovaného seznamu totiž lze ukládat pouze seznamy objektů a ne libovolné seznamy. Jak tedy docílíme kýženého jevu? K tomuto účelu nám generics poskytují nástroj zvaný žolík, anglicky wildcard, který se zapisuje jako ?. Vraťme se nyní k předchozímu příkladu:


      
      List<Number> cisla = new ArrayList<Number>();
      List<?> obecny = cisla;        // tohle je OK
      obecny.add("Ja nejsem cislo"); // tohle nelze prelozit
      
    

Jak je již v komentáři kódu naznačeno, poslední řádek neprojde překladačem. Proč? Protože pomocí List<?> říkáme, že obecny je seznamem neznámých prvků. A jelikož nevíme, jaké prvky v seznamu jsou, nemůžeme do něj ani žádné prvky přidávat. Jedinou výjimkou je „žádný“ prvek, totiž null, který lze přidat kamkoliv. Mírně filosoficky řečeno, null není ničím a tak je zároveň vším.

Naopak, ze seznamu neznámých objektů můžeme samozřejmě prvky číst, neboť každý prvek je určitě alespoň instancí třídy Object. Ukážeme si praktické použití žolíku.


      
      public static void tiskniSeznam(List<?> seznam) {
        for (Object e : seznam) {
          System.out.println(e);
        }
      }
      
    

Nyní si představme, že chceme metodu, která udělá z nějakého seznamu čísel jeho sumu. Uvažujme tedy následující (a pomiňme možné přetečení nebo podtečení rozsahu double):


      
      public static double suma(List<Number> cisla) {
        double result = 0;
        for (Number e : cisla) {
          result += e.doubleValue()
        }
        return result; 
      }
        
    

Opět, metoda se jeví jako bezproblémová. Nic ale není tak jednoduché, jak by se mohlo zdát. Nyní zkusíme uvažovat bez příkladu. Představme si, že máme seznam celých čísel, u kterého chceme provést sumu. Jistě není sporu o tom, že celá čísla jsou zároveň obecná čísla a přesto seznam List<Integer> nelze použít jako parametr výše deklarované metody z naprosto stejného důvodu, kvůli kterému nešlo říci, že seznam objeků je seznam čísel.

Samozřejmě je tu opět řešení. Zkusme nejdříve uvažovat selským rozumem. Výše jsme říkali, že místo seznamu objektů chceme seznam neznámých prvků. Nyní jsme v podobné situaci, pouze se nacházíme na jiném místě v hierarchii tříd. Zkusme tedy obdobnou úvahu použít i zde. Nechceme seznam čísel nýbrž seznam neznámých prvků, které jsou nejvýše čísly. Nyní je již pouze třeba ozřejmit syntaxi takové „úvahy“.


      
      public static double suma(List<? extends Number> cisla) {
        ...
      }
        
    

Toto použití žolíku má uplatnění i v samotném rozhraní List<E> a sice v metodě „přidej vše“. Zamyslete se nad tím, proč tomu tak je.


      
      boolean addAll(Collection<? extends E> c); 
      
    

Uvědomte si prosím následující -- prostý žolík je vlastně „zkratka“ pro „neznámý prvek rozšiřující Object“.

Ač by se tak mohlo zdát, možnosti wildcards jsme ještě nevyčerpali. Představme si situaci, kdy potřebujeme, aby možnou hodnotou byla instance třídy, která je v hierarchii mezi třídou specifikovanou naším obecným prvkem E a třídou Object. Pokud přemýšlíte, k čemu je něco takového dobré, představte si, že máte množinu celých čísel, které chcete setřídit. Jak lze taková čísla třídit? Například obecně podle hodnoty metody hashCode(), tedy na úrovni třídy Object. Nebo jako obecné číslo, tj. na úrovni třídy Number. A konečně i jako celé číslo na úrovni třídy Integer. Skutečně, níže již jít nemůžeme, protože libovolné zjemnění této třídy například na celá kladná čísla by nemohlo třídit obecná celá čísla.

Následující příklad demonstruje syntaxi a použití popsané konstrukce


      
      public TreeMap(Comparator<? super K> c);
      
    

Jedná se o konstruktor stromové mapy, tj. mapy klíč/hodnota, která je navíc setříděna podle klíče. Nyní opět trochu odbočíme a podíváme se, jak vypadá deklarace obecného rozhraní setříděné mapy.


      
      public interface SortedMap<K,V> extends Map<K,V> {...
      
    

Máme zde nový prvek -- je-li třeba použít více nezávislých obecných typů, zapíšeme je opět do „zobáčků“ jako seznam hodnot oddělených čárkou. Povšimněte si opět mnemotechniky -- K je key (klíč), V je value (hodnota). Je-li to třeba, je možné použít i žolíků. Viz následující příklad konstruktorů naší staré známé stromové mapy.


      
      public TreeMap(Map<? extends K, ? extends V> m);
      public TreeMap(SortedMap<K, ? extends V> m);