Heim >Java >javaLernprogramm >Java-Programmiergedanken-Lernklasse (3) Kapitel 15 – Generics
Das Konzept der Generics (Generika) ist eine der wichtigsten Änderungen in Java SE5. Generics implementieren das Konzept der parametrisierten Typen (parametrisierte Typen), sodass Code auf mehrere Typen angewendet werden kann. Der Begriff „generisch“ bedeutet: „auf viele, viele Arten anwendbar.“
Es hat nichts damit zu tun, ob die Klasse, in der sich die generische Methode befindet, generisch ist, das heißt, die Klasse, in der sich die generische Methode befindet, kann ob es sich um einen generischen Klassentyp handelt oder nicht.
Generische Methoden ermöglichen eine Änderung der Methode unabhängig von der Klasse.
Ein grundlegender Leitsatz: Wann immer Sie können, sollten Sie versuchen, generische Methoden zu verwenden. Das heißt, wenn die Verwendung einer generischen Methode die Generierung der gesamten Klasse ersetzen kann, sollten Sie nur eine generische Methode verwenden, da sie die Dinge klarer macht.
Für eine static
-Methode kann nicht auf die Typparameter der generischen Klasse zugegriffen werden. Wenn die static
-Methode also generische Funktionen verwenden muss, muss sie zur generischen Methode gemacht werden.
Um eine generische Methode zu definieren, fügen Sie einfach die generische Parameterliste vor den Rückgabewert ein.
Bei Verwendung generischer Methoden ist es normalerweise nicht erforderlich, den Parametertyp anzugeben, da der Compiler den spezifischen Typ für uns ermittelt. Dies wird als Typargumentinferenz bezeichnet.
Typinferenz ist nur für Zuweisungsvorgänge gültigWenn das Ergebnis eines generischen Methodenaufrufs als
Parameter. 1.1.1 Explizite Typspezifikation
2 Das Geheimnis des Löschens
Laut der JDK-Dokumentation gibteinWoher weiß der C++-Compiler, dass dieIm generischen Code gibt es keine Möglichkeit, Informationen über den Typ des generischen Parameters zu erhalten
Class.getTypeParameters()
Array von zurück Objekte, die Typparameter mit einer generischen Deklaration darstellen ...“ Dies scheint zu implizieren, dass Sie möglicherweise Informationen zu den Parametertypen finden, aber wie Sie der Ausgabe entnehmen können, wird alles, was Sie finden können, als Parameter verwendet DerTypeVariable
Bezeichner des Platzhalters ist keine nützliche Information. Die harte Realität ist also:. Somit können Sie Dinge wie generische Parameterbezeichner und
generische Typgrenzenkennen – aber Sie können nicht wissen, wie man einen bestimmten tatsächlichen Typ erstellt Parameter der Instanz . ..., es ist das grundlegendste Problem, mit dem Sie sich bei der Arbeit mit Java-Generika befassen müssen. Java-Generika werden mithilfe von Erasure implementiert, was bedeutet, dass bei der Verwendung von Generika alle spezifischen Typinformationen gelöscht werden.
Das Einzige, was Sie wissen, ist das Sie verwenden ein Objekt. und sind also zur Laufzeit tatsächlich
Listf7e83be87db5cd2d9a8a0b8117b38cd4
vom gleichen Typ . Beide Formen werden in ihre „Listc0f559cc8d56b43654fcbe4aa9df7b4a
native Typen gelöscht, d. h. .2.1 C++-MethodeList
2.1.1 Das folgende C++-Vorlagenbeispiel:
eine Methodebesitzt. Wenn dies nicht der Fall ist, erhalten Sie ein
Um f() aufzurufen, müssen wirf()
KompilierungsfehlerManipulatorda98a08537ace02ebe5efb4a0b237f4a
, sodass Typsicherheit gewährleistet ist// Templates.cpp#include <iostream>using namespace std;template<class T> class Manipulator{ T obj;public: Manipulator(T x) { obj = x; } void manipulate() { obj.f(); } };class HasF{public: void f() { cout << "HasF::f()" << endl; } };int main(){ HasF hf; Manipulator<HasF> manipulator(hf); manipulator.manipulate(); }2. In Java übersetzt, wird es nicht kompiliertHasF
f()
Aufgrund der Löschung kann der Java-Compiler die Anforderung, dass manipuliert() in der Lage sein muss, f() für obj aufzurufen, nicht auf HasF abbilden, das f(. ) besitzt. Diese Tatsache 2.2 Generische Grenzen
generische Typklasse unterstützen, die Grenzen des Generikums Klasse
package net.mrliuli.generics.erase;/** * Created by li.liu on 2017/12/7. *//** * 由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。 * @param <T> */class Manipulator<T>{ private T obj; public Manipulator(T x){ obj = x; } // Error: Cannot resolve method 'f()' //public void manipulate(){ obj.f(); }}/** * 为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。 * @param <T> */class Manipulator2<T extends HasF>{ private T obj; public Manipulator2(T x){ obj = x; } public void manipulate(){ obj.f(); } }public class Manipulation { public static void main(String[] args){ HasF hf = new HasF(); Manipulator<HasF> manipulator = new Manipulator<>(hf); //manipulator.manipulate(); Manipulator2<HasF> manipulator2 = new Manipulator2<>(hf); manipulator2.manipulate(); } }
我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。
T
擦除到了HasF
,就好像在类的声明中用HasF
替换了T
一样。
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为迁移兼容性。
因此,擦除主要的正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。
擦除的代码是显著的。
如果编写了下面这样的代码:
class Foo<T>{ T var; }
那么看起来当你在创建Foo
的实例时:
Foo<Cat> f = new Foo<Cat>();
class Foo
中的代码应该知道现在工作于Cat
之上,而泛型语法也强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实上并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”
擦除和迁移兼容性意味着,使用泛型并不是强制的。
class GenericBase<T>{}class Derived1<T> extends GenericBase<T>{}class Derived2 extends GenericBase{} // No warning
即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。
因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。
在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”
有时必须通过引入类型标签(type tag)来对擦除进行补偿(compensating)。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
创建类型实例
泛型数组
边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。
因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。
但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。
通配符被限制为单一边界。
数组的一种特殊行为
可以将子类型的数组赋给基类型的数组引用。然后编译期数组元素可以放置基类型及其子类型的元素,即编译时不报错,但运行时的数组机制知道实际的数组类型是子类,因此会在运行时检查放置的类型是否是实际类型及其再导出的子类型,不是则抛出java.lang.ArrayStoreException
异常。
容器的类型与容器持有的类型
// Compile Error: incompatible types:List<Fruit> list = new ArrayList<Apple>();
与数组不同,泛型没有内建的协变类型。即*协变性对泛型不起作用。
package net.mrliuli.generics.wildcards;import java.util.*;/** * Created by leon on 2017/12/8. */public class GenericsAndCovariance { public static void main(String[] args){ // Compile Error: incompatible types: //List<Fruit> list = new ArrayList<Apple>(); // Wildcards allow covariance: List<? extends Fruit> flists = new ArrayList<Apple>(); // But, 编译器并不知道flists持有什么类型对象。实际上上面语句使得向上转型,丢失掉了向List中传递任何对象的能力,甚至是传递Object也不行。 //flists.add(new Apple()); //flists.add(new Fruit()); //flists.add(new Object()); flists.add(null); // legal but uninteresting // We know that it returns at least Fruit: Fruit f = flists.get(0); } }
对于 List57019040ccef885c8e3bd8f9deb31922
,set()
方法不能工作于 Apple
和 Fruit
,因为 set() 的参数也是 ? extends Furit
,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。
但是,equals()
方法工作良好,因为它将接受Object类型而并非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。
使用超类型通配符。声明通配符是由某个特定类的任何基类界定的,方法是指定bbee182344df36d3890f89a83f7ca198
,甚至或者使用类型参数:117c5a0bdb71ea9a9d0c2b99b03abe3e
。这使得你可以安全地传递一个类型对象到泛型类型中。
参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。
package net.mrliuli.generics.wildcards;import java.util.*;public class SuperTypeWildcards { /** * 超类型通配符使得可以向泛型容器写入。超类型边界放松了在可以向方法传递的参数上所作的限制。 * @param apples 参数apples是Apple的某种基类型的List,这样你就知道向其中添加Apple或Apple的子类型是安全的。 */ static void writeTo(List<? super Apple> apples){ apples.add(new Apple()); apples.add(new Jonathan()); //apples.add(new Fruit()); // Error } }
GenericWriting.java 中 writeExact(fruitList, new Apple());
在JDK1.7中没有报错,说明进入泛型方法 writeExact()
时 T
被识别为 Fruit
,书中说报错,可能JDK1.5将 T
识别为 Apple
。
package net.mrliuli.generics.wildcards;import java.util.*;/** * Created by li.liu on 2017/12/8. */public class GenericWriting { static <T> void writeExact(List<T> list, T item){ list.add(item); } static List<Apple> appleList = new ArrayList<Apple>(); static List<Fruit> fruitList = new ArrayList<Fruit>(); static void f1(){ writeExact(appleList, new Apple()); writeExact(fruitList, new Apple()); } static <T> void writeWithWildcard(List<? super T> list, T item){ list.add(item); } static void f2(){ writeWithWildcard(appleList, new Apple()); writeWithWildcard(fruitList, new Apple()); } public static void main(String[] args){ f1(); f2(); } }
原生泛型Holder
与Holder6b3d0130bba23ae47fe2b8e8cddf0195
原生
Holder
将持有任何类型的组合,而Holder6b3d0130bba23ae47fe2b8e8cddf0195
将持有具有某种具体类型的同构集合,因此不能只是向其中传递Object。
以下示例,被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。参数类型在调用
f2()
的过程中被捕获,因此它可以在对f1()
的调用中被使用。
package net.mrliuli.generics.wildcards;/** * Created by leon on 2017/12/9. */public class CaptureConversion { static <T> void f1(Holder<T> holder){ T t = holder.get(); System.out.println(t.getClass().getSimpleName()); } static void f2(Holder<?> holder){ f1(holder); // Call with captured type } public static void main(String[] args){ Holder raw = new Holder<Integer>(1); f1(raw); f2(raw); Holder rawBasic = new Holder(); rawBasic.set(new Object()); f2(rawBasic); Holder<?> wildcarded = new Holder<Double>(1.0); f2(wildcarded); } }
基本类型不能作为类型参数
由于探险,一个类不能实现同一个泛型接口的两种变体
由于擦除,通过泛型来重载方法将产生相同的签名,编译出错,不能实现重载
基类劫持了接口
我相信被称为泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。
泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型有更少的限制,因此单个的代码段能够应用到更多的类型上。
相关文章:
Das obige ist der detaillierte Inhalt vonJava-Programmiergedanken-Lernklasse (3) Kapitel 15 – Generics. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!