泛型
引入泛型
传统编写的限制:
在Java中一般的类和方法,只能使用具体的类型,要么是基本数据类型,要么是自定义类型。如果要编写可以应用于多种类型的代码,这种刻板的限制就会束缚很多!
解决这种限制的三种方法:
1.多态:将方法的参数类型设为基类,那么该方法就可以接收从这个基类导出的任何类作为参数。
class Primary{} //定义基类 class Test() { public void f(Primary p) {...} }
2.方法的参数使用接口:任何实现了该接口的类都可以满足该方法。
interface Primary{} //定义接口 class Test() { public void f(Primary p) //实现了该接口的所有类都可以作为参数 {...} }
3.使用泛型。
泛型的说明:
泛型实现了参数化类型的概念,使代码可以应用于某种不具体的类,而不是具体的一个类或者接口。
简单说就是使代码可以适用于广泛的类型。
泛型表达式的翻译(最后看):
1.当程序调用泛型方法时,如果擦除了泛型返回类型,编译器插入类型转换。
Pair<Employee> buddies = ... Employee buddy = buddies.getFirst();
• 也就是说,编译其把这个方法调用翻译为两条虚拟指令:
◇ 对原始方法Pair.getFirest的调用。
◇ 将返回的Object类型,强制转换为Employee类型。
2.当存取一个泛型域时也要插入强制类型转换。
假设 Pair 类的first 域 和 second 域都是 公有的(这不是种好的编程风格, 但在java语法中,这是合法的)。
表达式: Employee buddy = buddies.first; 也会在结果字节码中插入强制类型转换;
简单泛型:
使用泛型预定义参数类型:
说明:基本数据类型无法作为类型参数
在使用时具体化类型参数
小结
泛型类最主要的使用是应用在集合中,代码封装了一个ArrayList,这样我们在编写类的时候,就不会受限于具体的类,因为具体使用的类型是在初始化对象的时候才指定的。
我们为什么要这样呢?如果这个实现类不用泛型,如果处理多种类型数据的时候,就要编写多个实现类,来针对处理。
元组:
简单元组实例:
通过继承机制来实现长度更长的元组
泛型接口
说明:
泛型也可以应用于接口,类和接口的类型参数应该保持一致,都是T或者其他。
泛型接口最常用的一个用法是实现Iterable接口,实现迭代方法!
实例:
import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.Random; public class RandomList<T> implements Iterable<T> { private ArrayList<T> storage = new ArrayList<T>(); private Random rand = new Random(new Date().getTime()); public void add(T item) { storage.add(item); } public T select() { return storage.get(rand.nextInt(storage.size())); } /* 实现Iterable接口 */ public Iterator<T> iterator() { return storage.iterator(); } }
泛型方法
说明:
是否拥有泛型方法和其是否是泛型类并无直接关系,也就是说泛型方法可以独立于类而产生变化!
实例:
类型说明:
显式的类型说明:
在泛型方法中,可以显式地指明参数类型,不过很少这样使用!
可变参数列表:
泛型方法和可变参数列表可以很好的共存
类型变量的限定
说明:
1.添加限定来让类型变量具有特定功能。
2.可以有多个限定(接口和类都是可以的),但要注意顺序。
为什么关键字不用implements呢?毕竟Comparable是一个接口:
<T extends BoundingType>
这句代码表示T应该是绑定类型(BoundingType)的子类型。T和绑定类型可以是类,也可以是接口。
若T是接口,那么方法调用时传进来的是T的实现类也是可以的。extends更接近于子类的概念,所以选用extends。
神秘的擦除
问题:
我们很可能会认为c1!=c2,结果为false,因为c1不能装入Integer,c2也不能装入String。
说明:
在泛型代码内部,无法获取任何有关泛型参数类型的信息,也就是你无法获得那个具体的类型是什么,你仅仅可以获知的诸如类型参数标识符和泛型边界这类的信息。
Java泛型是使用擦除来实现的:
这意味者当你在使用泛型的时候,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此问题中c1==c2,为true。这两种形式都被擦除成为它们的“原生”类型,即List.
原始类型与擦除:
虚拟机没有泛型类型对象,所有对象都属于普通类。无论何时定义了一个泛型类型,都自动提供了一个相应的原始类型,将泛型类型还原成原始类型的过程,称为擦除。
原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定类型的变量默认用Object)。
左图的原始类型变为:public class Test<Object>,所有T都变成Object 右图变为:public class Test<Comparable>,所有T都变成Object。
说明:
这样就不难理解,为什么,加上限定边界后,我们就可以调用obj.compareTo()了,因为擦除类型变量后,T都被还原成Comparable,效果就是任何实现了Comparable的子类都可以调用Test对象的compare()方法。如果泛型是在Java 1.0就出现的,那么这个特性将不会使用擦除来实现——它将使用具体化,使类型参数保持为第一类的实体,因此你就能够在类型参数上执行基于类型的语言操作和反射操作。
再一次强调:
Test<Cat> test = new Test<Cat>();
看起来class Test应该知道现在工作于Cat之上,而泛型语法也在强烈暗示,在整个类中的各个地方,类型T都在被替换。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须时刻提醒自己:它仅仅只是一个Object。
擦除的补偿。
再谈边界
1.边界使得你可以在用于泛型的类型参数上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。
2.因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只能是那些可以用Object调用的方法,那么你就可以用这些类型字节来调用方法。
通配符
说明:
1.Java泛型是强制类型检测的,泛型类型的子类型互不相关。
publicclass Test { public static void main(String[] args) throws Exception{ List<Integer> listInteger =new ArrayList<Integer>(); List<String> listString =new ArrayList<String>(); printCollection(listInteger); printCollection(listString); } public static void printCollection(Collection<Object> collection){ for(Object obj:collection){ System.out.println(obj); } } } //Integer String都是Object的子类型,但是结果会报错,这就说明了泛型不考虑继承关系 The method printCollection(Collection<Object>) in the type GernericTest is not applicable for the arguments (List<Integer>)
2.我们希望泛型能向普通类那样具有面向对象的一些特征:
-
- 向上转型为一个泛型对象。
- 向下转型为一个泛型对象。
3.为了使泛型能具有面向对象的一些继承关系,Java引入了通配符的一些概念:无界通配符 ?
为了使泛型的子类型仍然具有相关性,可以直接使用无界通配符:
使用通配符上界“? extends T”,来指定继承关系
说明:
? extends T 的本质上的实现是泛型的自动向上转型。
使用通配符下界“?super T”
使用方法和解释同上。