Java编程思想学习课时(三)第15章-泛型
泛型(generics)的概念是Java SE5的重大变化之一。泛型实现了参数化类型(parameterized types)的概念,使代码可以应用于多种类型。“泛型”这个术语的意思是:“适用于许多许多的类型”。
1 泛型方法
泛型方法与其所在的类是否是泛型没在关系,即泛型方法所在的类以是泛型类也可以不是泛型类。
泛型方法使得该方法能够独立于类而产生变化 。
一个基本指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。
对于一个
static
方法而言,无法访问泛型类的类型参数,所以,如果static
方法需要使用泛型能力,就必须使其成为泛型方法。要定义泛型方法,只需将泛型参数列表置于返回值之前。
1.1 类型参数推断
使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。
类型推断只对赋值操作有效。
如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行类型推断。
1.1.1 显式的类型说明
在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内,即显式的类型说明。
2 擦除的神秘之处
根据JDK文档的描述,
Class.getTypeParameters()
将“返回一个TypeVariable
对象数组,表示有泛型声明的类型参数…..”,这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中看到,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。因此,残酷的现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息。
因此,你可以知道诸如泛型参数标识符和泛型类型边界这类信息——你却无法知道创建某个特定实例的实际的类型参数。……,在使用Java泛型工作时它是必须处理的最基本的问题。
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此
List<String>
和List<Integer>
在运行时事实上是相同的类型。这两种形式都被擦除成它们的“原生类型,即List
。
2.1 C++的方式
2.1.1 以下C++模板示例:
它怎么知道
f()
方法是为类型参数T而存在的呢?当你实例化这个模板时,C++编译器将进行检查,因此在Manipulator<HasF>
被实例化的这一刻,它看到HasF
拥有一个方法f()
。如果情况并非如此,就会得到一个编译期错误,这样类型安全就得到了保障。
// 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.1.2 翻译成Java,将不能编译。
由于有了擦除,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
2.2 泛型边界
为了调用f(),我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。由于有了边界,下面的代码就可以编译了。
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(); } }
2.3 擦除
我们说泛型类型参数将擦除到它的第一个边界(它可能会有多个边界),我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样。
T
擦除到了HasF
,就好像在类的声明中用HasF
替换了T
一样。
2.4 擦除的问题
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为迁移兼容性。
因此,擦除主要的正当理由是从非泛化的代码到泛化的代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入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
2.5 边界处的动作
即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。
因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。
在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”
3 擦除的补偿(Compensating for erasure)
有时必须通过引入类型标签(type tag)来对擦除进行补偿(compensating)。这意味着你需要显示地传递你的类型的Class对象,以便你可以在类型表达式中使用它。
创建类型实例
泛型数组
4 边界(bound)
边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。
因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用Object调用的方法。
但是,如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。
通配符被限制为单一边界。
5 通配符(wildcards)
数组的一种特殊行为
可以将子类型的数组赋给基类型的数组引用。然后编译期数组元素可以放置基类型及其子类型的元素,即编译时不报错,但运行时的数组机制知道实际的数组类型是子类,因此会在运行时检查放置的类型是否是实际类型及其再导出的子类型,不是则抛出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); } }
5.1 编译器有多聪明
对于
List<? extends Fruit>
,set()
方法不能工作于Apple
和Fruit
,因为 set() 的参数也是? extends Furit
,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。但是,
equals()
方法工作良好,因为它将接受Object类型而并非T类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。
5.2 逆变(Contravariance)
使用超类型通配符。声明通配符是由某个特定类的任何基类界定的,方法是指定
<? super MyClass>
,甚至或者使用类型参数:<? super T>
。这使得你可以安全地传递一个类型对象到泛型类型中。参数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(); } }
5.3 无界通配符(Unbounded wildcards)
原生泛型Holder
与Holder<?>
原生
Holder
将持有任何类型的组合,而Holder<?>
将持有具有某种具体类型的同构集合,因此不能只是向其中传递Object。
5.4 捕获转换
以下示例,被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。参数类型在调用
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); } }
6 问题
基本类型不能作为类型参数
由于探险,一个类不能实现同一个泛型接口的两种变体
由于擦除,通过泛型来重载方法将产生相同的签名,编译出错,不能实现重载
基类劫持了接口
7 总结
我相信被称为泛型的通用语言特性(并非必须是其在Java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。
泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型有更少的限制,因此单个的代码段能够应用到更多的类型上。
相关文章:
Atas ialah kandungan terperinci Java编程思想学习课时(三)第15章-泛型. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Alat AI Hot

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool
Gambar buka pakaian secara percuma

Clothoff.io
Penyingkiran pakaian AI

AI Hentai Generator
Menjana ai hentai secara percuma.

Artikel Panas

Alat panas

Notepad++7.3.1
Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1
Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6
Alat pembangunan web visual

SublimeText3 versi Mac
Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Topik panas



Panduan Nombor Sempurna di Jawa. Di sini kita membincangkan Definisi, Bagaimana untuk menyemak nombor Perfect dalam Java?, contoh dengan pelaksanaan kod.

Panduan untuk Penjana Nombor Rawak di Jawa. Di sini kita membincangkan Fungsi dalam Java dengan contoh dan dua Penjana berbeza dengan contoh lain.

Panduan untuk Weka di Jawa. Di sini kita membincangkan Pengenalan, cara menggunakan weka java, jenis platform, dan kelebihan dengan contoh.

Panduan untuk Nombor Smith di Jawa. Di sini kita membincangkan Definisi, Bagaimana untuk menyemak nombor smith di Jawa? contoh dengan pelaksanaan kod.

Dalam artikel ini, kami telah menyimpan Soalan Temuduga Spring Java yang paling banyak ditanya dengan jawapan terperinci mereka. Supaya anda boleh memecahkan temuduga.

Java 8 memperkenalkan API Stream, menyediakan cara yang kuat dan ekspresif untuk memproses koleksi data. Walau bagaimanapun, soalan biasa apabila menggunakan aliran adalah: bagaimana untuk memecahkan atau kembali dari operasi foreach? Gelung tradisional membolehkan gangguan awal atau pulangan, tetapi kaedah Foreach Stream tidak menyokong secara langsung kaedah ini. Artikel ini akan menerangkan sebab -sebab dan meneroka kaedah alternatif untuk melaksanakan penamatan pramatang dalam sistem pemprosesan aliran. Bacaan Lanjut: Penambahbaikan API Java Stream Memahami aliran aliran Kaedah Foreach adalah operasi terminal yang melakukan satu operasi pada setiap elemen dalam aliran. Niat reka bentuknya adalah

Panduan untuk TimeStamp to Date di Java. Di sini kita juga membincangkan pengenalan dan cara menukar cap waktu kepada tarikh dalam java bersama-sama dengan contoh.

Java ialah bahasa pengaturcaraan popular yang boleh dipelajari oleh pembangun pemula dan berpengalaman. Tutorial ini bermula dengan konsep asas dan diteruskan melalui topik lanjutan. Selepas memasang Kit Pembangunan Java, anda boleh berlatih pengaturcaraan dengan mencipta program "Hello, World!" Selepas anda memahami kod, gunakan gesaan arahan untuk menyusun dan menjalankan program, dan "Hello, World!" Pembelajaran Java memulakan perjalanan pengaturcaraan anda, dan apabila penguasaan anda semakin mendalam, anda boleh mencipta aplikasi yang lebih kompleks.
