Generika v Javě - Generics in Java

Generika jsou nástrojem generického programování, které byly přidány do programovacího jazyka Java v roce 2004 ve verzi J2SE 5.0. Byly navrženy tak, aby rozšířily systém typů Java tak, aby umožňoval „typ nebo metodu pracovat na objektech různých typů a současně zajišťovat bezpečnost typu při kompilaci“. Aspekt kompilace-time bezpečnostní typ nebylo plně dosaženo, protože to bylo uvedeno v roce 2016, že není zaručena ve všech případech.

Rámec kolekcí Java podporuje generika k určení typu objektů uložených v instanci kolekce.

V roce 1998 vytvořili Gilad Bracha , Martin Odersky , David Stoutamire a Philip Wadler Generic Java, rozšíření jazyka Java na podporu generických typů. Obecná Java byla začleněna do Javy s přidáním zástupných znaků.

Hierarchie a klasifikace

Podle specifikace jazyka Java :

  • Typové proměnné je bez výhrad identifikátor. Proměnné typu jsou zavedeny deklaracemi generických tříd, deklaracemi generických rozhraní, deklaracemi generických metod a deklaracemi generických konstruktorů.
  • Třída je druhový, pokud prohlásí jeden nebo více proměnných typu. Tyto typové proměnné jsou známé jako typové parametry třídy. Definuje jednu nebo více typových proměnných, které fungují jako parametry. Deklarace obecné třídy definuje sadu parametrizovaných typů, jednu pro každé možné vyvolání sekce parametru typu. Všechny tyto parametrizované typy sdílejí za běhu stejnou třídu.
  • Rozhraní je druhový, pokud prohlásí jeden nebo více proměnných typu. Tyto typové proměnné jsou známé jako typové parametry rozhraní. Definuje jednu nebo více typových proměnných, které fungují jako parametry. Obecná deklarace rozhraní definuje sadu typů, jeden pro každé možné vyvolání sekce parametru typu. Všechny parametrizované typy sdílejí za běhu stejné rozhraní.
  • Metoda je obecná, pokud prohlásí jeden nebo více proměnných typu. Tyto typové proměnné jsou známé jako formální parametry typu metody. Forma formálního seznamu parametrů je shodná se seznamem parametrů typu třídy nebo rozhraní.
  • Konstruktor může být deklarován jako obecný, a to nezávisle na tom, zda třída konstruktor je udávána v, je sama o sobě obecný. Konstruktor je obecný, pokud deklaruje jednu nebo více typových proměnných. Tyto typové proměnné jsou známé jako formální parametry typu konstruktoru. Forma formálního seznamu parametrů je shodná se seznamem parametrů typu obecné třídy nebo rozhraní.

Motivace

Následující blok kódu Java ukazuje problém, který existuje, když nepoužíváte generika. Nejprve deklaruje ArrayListtyp Object. Potom se přidává Stringk ArrayList. Nakonec se pokusí načíst přidané Stringa přenést je na Integer- chybu v logice, protože obecně není možné přetypovat libovolný řetězec na celé číslo.

List v = new ArrayList();
v.add("test"); // A String that cannot be cast to an Integer
Integer i = (Integer)v.get(0); // Run time error

Přestože je kód kompilován bez chyby, java.lang.ClassCastExceptionpři spuštění třetího řádku kódu vyvolá výjimku za běhu ( ). Tento typ logické chyby lze zjistit během kompilace pomocí generik a je primární motivací pro jejich použití.

Výše uvedený fragment kódu lze přepsat pomocí generik takto:

List<String> v = new ArrayList<String>();
v.add("test");
Integer i = (Integer)v.get(0); // (type error)  compilation-time error

Parametr typu Stringv hranatých závorkách deklaruje, že ArrayListmá být tvořen String(potomkem ArrayListgenerických Objectsložek '). U generik již není nutné přetypovat třetí řádek na jakýkoli konkrétní typ, protože výsledek v.get(0)je definován Stringkódem generovaným kompilátorem.

Logická chyba ve třetím řádku tohoto fragmentu bude detekována jako chyba při kompilaci (s J2SE 5.0 nebo novější), protože kompilátor zjistí, že v.get(0)se Stringmísto toho vrátí Integer. Podrobnější příklad viz odkaz.

Zde je malý výňatek z definice rozhraní Lista Iteratorv balíčku java.util:

public interface List<E> { 
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> { 
    E next();
    boolean hasNext();
}

Zadejte zástupné znaky

Argument typu pro parametrizovaný typ není omezen na konkrétní třídu nebo rozhraní. Java umožňuje použití zástupných znaků typu jako argumentů typů pro parametrizované typy. Zástupné znaky jsou argumenty typu ve tvaru " <?>"; volitelně s horní nebo dolní mezí . Vzhledem k tomu, že přesný typ reprezentovaný zástupným znakem není znám, jsou na typ metod, které lze volat na objekt, který používá parametrizované typy, kladena omezení.

Zde je příklad, kde je typ prvku a Collection<E>parametrizován zástupným znakem:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // compile-time error
c.add(null); // allowed

Protože nevíme, co cznamená typ prvku , nemůžeme do něj přidávat objekty. add()Metoda přebírá argumenty typu E, typ prvku z Collection<E>generické rozhraní. Když argument skutečného typu je ?, znamená to nějaký neznámý typ. Jakákoli hodnota argumentu metody, kterou metodě předáme, add()by musela být podtypem tohoto neznámého typu. Protože nevíme, co je to za typ, nemůžeme předat nic. Jedinou výjimkou je null ; který je členem každého typu.

K určení horní hranice zástupného znaku typu se extendsklíčové slovo používá k označení, že argument typu je podtyp ohraničující třídy. Takže List<? extends Number>prostředky, které daný seznam obsahuje objekty neznámého typu, která rozšiřuje Numbertřídu. Seznam může být například List<Float>nebo List<Number>. Čtení prvku ze seznamu vrátí a Number. Přidání nulových prvků je opět také povoleno.

Použití výše uvedených zástupných znaků zvyšuje flexibilitu, protože mezi jakýmikoli dvěma parametrizovanými typy s konkrétním typem jako argumentem typu neexistuje žádný dědičný vztah. Ani List<Number>není List<Integer>podtyp toho druhého; přestože Integerje podtypem Number. Takže jakákoli metoda, která bere List<Number>jako parametr, nepřijímá argument z List<Integer>. Pokud ano, bylo by možné do něj vložit a, Numberkterý není Integer; který porušuje bezpečnost typu. Zde je příklad, který ukazuje, jak by byla narušena bezpečnost typů, pokud by List<Integer>šlo o podtyp List<Number>:

List<Integer> ints = new ArrayList<Integer>();
ints.add(2);
List<Number> nums = ints;  // valid if List<Integer> were a subtype of List<Number> according to substitution rule. 
nums.add(3.14);  
Integer x = ints.get(1); // now 3.14 is assigned to an Integer variable!

Řešení se zástupnými znaky funguje, protože zakazuje operace, které by narušovaly bezpečnost typu:

List<? extends Number> nums = ints;  // OK
nums.add(3.14); // compile-time error
nums.add(null); // allowed

K určení nižší třídy ohraničení zástupného znaku typu superse použije klíčové slovo. Toto klíčové slovo označuje, že argument typu je nadtypem ohraničující třídy. Tak List<? super Number>by mohl představovat List<Number>nebo List<Object>. Čtení ze seznamu definovaného jako List<? super Number>vrací prvky typu Object. Přidání do takového seznamu vyžaduje buď prvky typu Number, jakýkoli podtyp Numbernebo hodnotu null (což je člen každého typu).

Mnemotechnická pomůcka PECS (Producer Extends, Consumer Super) z knihy Efektivní Java od Joshua Blocha poskytuje snadný způsob, jak si zapamatovat, kdy v Javě používat zástupné znaky (odpovídající kovarianci a kontravarianci ).

Definice obecných tříd

Zde je příklad generické třídy Java, kterou lze použít k reprezentaci jednotlivých položek (mapování mapování hodnot) na mapě :

public class Entry<KeyType, ValueType> {
  
    private final KeyType key;
    private final ValueType value;

    public Entry(KeyType key, ValueType value) {  
        this.key = key;
        this.value = value;
    }

    public KeyType getKey() {
        return key;
    }

    public ValueType getValue() {
        return value;
    }

    public String toString() { 
        return "(" + key + ", " + value + ")";  
    }

}

Tuto generickou třídu lze použít například následujícími způsoby:

Entry<String, String> grade = new Entry<String, String>("Mike", "A");
Entry<String, Integer> mark = new Entry<String, Integer>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

Entry<Integer, Boolean> prime = new Entry<Integer, Boolean>(13, true);
if (prime.getValue()) System.out.println(prime.getKey() + " is prime.");
else System.out.println(prime.getKey() + " is not prime.");

Výstupy:

grade: (Mike, A)
mark: (Mike, 100)
13 is prime.

Diamantový operátor

Díky odvození typu umožňuje Java SE 7 a novější programátorovi nahradit prázdnou dvojici úhlových závorek ( <>nazývaných diamantový operátor ) dvojicí úhlových závorek obsahujících jeden nebo více parametrů typu, které implikuje dostatečně blízký kontext . Výše uvedený příklad kódu pomocí Entrylze tedy přepsat jako:

Entry<String, String> grade = new Entry<>("Mike", "A");
Entry<String, Integer> mark = new Entry<>("Mike", 100);
System.out.println("grade: " + grade);
System.out.println("mark: " + mark);

Entry<Integer, Boolean> prime = new Entry<>(13, true);
if (prime.getValue()) System.out.println(prime.getKey() + " is prime.");
else System.out.println(prime.getKey() + " is not prime.");

Definice obecných metod

Zde je příklad generické metody používající výše uvedenou generickou třídu:

public static <Type> Entry<Type, Type> twice(Type value) {
    return new Entry<Type, Type>(value, value);
}

Poznámka: Pokud <Type>ve výše uvedené metodě odstraníme první , dostaneme chybu kompilace (nelze najít symbol 'Typ'), protože představuje deklaraci symbolu.

V mnoha případech uživatel metody nemusí uvádět parametry typu, jak lze odvodit:

Entry<String, String> pair = Entry.twice("Hello");

V případě potřeby lze parametry explicitně přidat:

Entry<String, String> pair = Entry.<String>twice("Hello");

Použití primitivních typů není povoleno a místo toho je nutné použít krabicové verze:

Entry<int, int> pair; // Fails compilation. Use Integer instead.

Existuje také možnost vytvářet obecné metody na základě daných parametrů.

public <Type> Type[] toArray(Type... elements) {
    return elements;
}

V takových případech nemůžete použít ani primitivní typy, např .:

Integer[] array = toArray(1, 2, 3, 4, 5, 6);

Obecná klauzule v hodech

Ačkoli samotné výjimky nemohou být obecné, obecné parametry se mohou objevit v klauzuli throws:

public <T extends Throwable> void throwMeConditional(boolean conditional, T exception) throws T {
    if (conditional) {
        throw exception;
    }
}

Problémy s vymazáním typu

Generika se kontroluje při kompilaci na správnost typu. Informace o obecném typu jsou poté odstraněny v procesu zvaném vymazání typu . Například List<Integer>budou převedeny na negenerický typ List, který obvykle obsahuje libovolné objekty. Kontrola při kompilaci zaručuje, že výsledný kód je typově správný.

Z důvodu vymazání typu nelze za běhu určit parametry typu. Když ArrayListje například zkoumán soubor za běhu, neexistuje obecný způsob, jak určit, zda před vymazáním typu šlo o an ArrayList<Integer>nebo an ArrayList<Float>. Mnoho lidí je s tímto omezením nespokojeno. Existují dílčí přístupy. Jednotlivé prvky mohou být například prozkoumány, aby se určil typ, ke kterému patří; například pokud an ArrayListobsahuje an Integer, který ArrayList může být parametrizován Integer(ale může být parametrizován s jakýmkoli rodičem Integer, jako Numbernebo Object).

Po předvedení tohoto bodu vygeneruje následující kód „Equal“:

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) { // evaluates to true
    System.out.println("Equal");
}

Dalším účinkem vymazání typu je, že obecná třída nemůže Throwable třídu žádným způsobem rozšířit, přímo ani nepřímo:

public class GenericException<T> extends Exception

Důvod, proč to není podporováno, je kvůli vymazání typu:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

Kvůli vymazání typu modul runtime neví, který blok catch má provést, takže je kompilátor zakázán.

Generika Java se liší od šablon C ++ . Generika Java generuje pouze jednu kompilovanou verzi generické třídy nebo funkce bez ohledu na počet použitých typů parametrizace. Prostředí Java run-time navíc nemusí vědět, jaký parametrizovaný typ se používá, protože informace o typu jsou ověřeny v době kompilace a nejsou zahrnuty v kompilovaném kódu. V důsledku toho není možné vytvořit instanci třídy Java parametrizovaného typu, protože instance vyžaduje volání konstruktoru, který je v případě neznámého typu nedostupný.

Následující kód například nelze zkompilovat:

<T> T instantiateElementType(List<T> arg) {
     return new T(); //causes a compile error
}

Protože za běhu existuje pouze jedna kopie na generickou třídu, jsou statické proměnné sdíleny mezi všemi instancemi třídy bez ohledu na jejich parametr typu. V důsledku toho nelze typový parametr použít v deklaraci statických proměnných ani ve statických metodách.

Projekt generik

Project Valhalla je experimentální projekt na inkubaci vylepšených generik a jazykových funkcí Java pro budoucí verze potenciálně od Java 10 a dále. Mezi možná vylepšení patří:

Viz také

Reference

  1. ^ Programovací jazyk Java
  2. ^ A ClassCastException lze vyvolat i při absenci sesílání nebo null. „Systémy typu Java a Scala jsou neslušné“ (PDF) .
  3. ^ GJ: Obecná Java
  4. ^ Specifikace jazyka Java, třetí vydání James Gosling, Bill Joy, Guy Steele, Gilad Bracha - Prentice Hall PTR 2005
  5. ^ Gilad Bracha (5. července 2004). „Generika v programovacím jazyce Java“ (PDF) . www.oracle.com .
  6. ^ Gilad Bracha (5. července 2004). „Generika v programovacím jazyce Java“ (PDF) . www.oracle.com . p. 5.
  7. ^ Bracha, Gilad . „Zástupné znaky> Bonus> Generika“ . Návody Java ™ . Věštec. ... Jedinou výjimkou je null, což je člen každého typu ...
  8. ^ http://docs.oracle.com/javase/7/docs/technotes/guides/language/type-inference-generic-instance-creation.html
  9. ^ Gafter, Neal (2006-11-05). „Opravená generika pro Javu“ . Citováno 2010-04-20 .
  10. ^ "Specifikace jazyka Java, oddíl 8.1.2" . Oracle . Citováno 24. října 2015 .
  11. ^ Goetz, Briane. „Vítejte ve Valhalle!“ . Archiv pošty OpenJDK . OpenJDK . Citováno 12. srpna 2014 .