1. 为什么使用泛型
2. 泛型是什么
3. 泛型方法
4. 泛型接口
5. 泛型类
6. 泛型擦除
7. 通配符
8. 泛型经典实例
ONE:为什么使用泛型
好程序的一个指标就是通用性。java可以使用多态机制,将方法参数设置为基类,从而调用方法时可以接受该基类和其子类。由于单继承受限太多,可以将方法参数设为接口,正是所谓的面向接口编程,从而再次提高程序的通用性。
通用性还有更好的做法就是让方法接收不具体的参数类型,也就是说参数不是一个具体的类或接口或常见类型,一份代码可以根据不同的需求,在实际使用中使用不同的类。具体而言就是将类型作为一个参数,使用的时候将这个参数用具体的类型来代替。这种需求就要依靠泛型机制来实现 ---- 编写更加通用的代码。
为容器提供更加安全的机制,在没有泛型前,容器类,如ArrayList是接受任何一个对象的,因为任何对象放进来之后都会向上转型为Object,而在取出的时候在向下转型为期待的类型,如果接受的对象类型不一致,在向下转型的时候就会出现错误,这个原因是由于当初没有检查机制造成的,现在推出了泛型,同时增加了这种检测机制,从而避免了这种事情的发生 ---- 提供更加安全的机制。
TWO : 泛型是什么
泛化的类型,也就是使用于很多类型的类型,也就是参数化类型的概念,即一个类型是一个参数,在使用过程中用<>括住类型参数,如<T>,T就代表类型参数,然后使用T来泛指类型做各种事情。
THREE : 泛型方法
对一个方法应用泛型和对一个类应用泛型没什么区别。
public static <T> void methodTest (T t){
System.out.println(t);
}
public static void main(String[] args) {
methodTest("hello world");
methodTest(1);
}
首先泛型方法都是先声明后使用,申明要在public static final 后边,同事必须在返回值前边,这样之后才能在方法中使用类型参数。
public static <T> void getThing(T[] numbers){
for (T t: numbers) {
System.out.print(t + " ");
}
}
泛型方法不需要像泛型类一样需要明确指定类型参数是什么,编译器会自动进行类型推断,
FOUR : 泛型接口
泛型接口的用法与泛型类的用法一样。
public interface Testt<T>{
T test();
}
FIVE : 泛型类
参数化类型,这里指的是将一个类中的参数参数化类型,即这个类中的成员变量不是一个具体的类型,在使用这个类的时候才指定他。例如我们经常用到的ArrayList<Student> students 就是在我们使用ArrayList的时候才将ArrayList中的成员变量类型指定为User。
public class PageObject<T>{
private T t;
}
在类名后边申明一个类型参数<T>,这个T在整个类中都可以使用,如果不申明一个类型参数,也可以使用<T,K,...,V>来申明多个。
public class PageObject<T>{
protected T t;
public T getT(){
return t;
}
public void setT(T t){
this.t = t;
}
}
PageObject<Integer> pageInteer = new PageObject<Integer>();
pageInteger.setT(1);
SIX : 泛型擦除
java泛型机制是采用擦除的形式来实现的。
擦除的机制实现泛型是在编译器这个层次实现的。也就是说在生成字节码中是不包含泛型中的类型信息的,ArrayList<Integer> list,和ArrayList list这两个类,编译之后都会生成ArrayList.class这一个类文件。我们单单从类文件上是看不出这两个类的不同的,因为他们就是一个类文件。这就是擦除,擦除了我们使用泛型的证据。这样也导致了一些比较困难的事情,比如我们在运行期是没办法得到泛型的信息的。我们没法得知ArrayList<Integer> list中,list是Integer约束的。
SEVEN : 通配符
上述的几种泛型用法都是在声明泛型的时候的使用方法,而通配符是针对使用泛型的时候。
public class Test {
public static void main(String[] args) {
List<A> OList = new ArrayList<B>()
}
这样写,编译器就会报错,应为泛型是没有继承的,即使B类是A类的子类,但是在泛型上List<A>和Lisb<B>然而并不等价。
要实现这种向上转型,就需要通配符?
public class Test {
public static void main(String[] args) {
List<?> OList = new ArrayList<B>()
}
}
这里的?如果不加限制,默认指的任何类型,当然我们也可以给这个通配符添加限制。同样使用到extends
代码改写后如下:
public class Test {
public static void main(String[] args) {
List<? extends A> OList = new ArrayList<B>()
}
}
这里extends 限定的意义是通配符的上界。表示类型参数可以是指定类型或者指定类型的子类。这样List<B> 就是List<?>的子类,就可以实现向上转型。这样是有弊端的。如果我们调用Olist的add方法,我们会发现我们无法往OList中添加任何一个值,即使这个对象是new B()得到,或者是new A()。这是因为编译器知道OList容器接受一个类型A的子类型,但是我们不知道这个子类型究竟是什么,所以为了保证类型安全,我们是不能往这个类型里面添加任何元素的,即使是A。从一个角度来看,我们知道这个容器里面放入的是A的子类型,也就是我们通过这个容器取出的数据都是可以调用A的方法的,得到的数据都是一个A类型的实例。下面这种写法是成功的。
public class Test {
public static void main(String[] args) {
List<? extends B> OList;
ArrayList<B> BList = new ArrayList<>();
BList.add(new B());
OList = BList;
OList.get(0).f();
}
}
使用通配符的容器则代表了一个确切的类型参数的容器。,我们可以把这个类型参数的容器用到定义方法上面。比如下面的例子。
public class Test<E>{
public static void main(String[] args) {
List<? extends B> OList;
ArrayList<B> BList = new ArrayList<>();
BList.add(new B());
OList = BList;
new Test<A>().getFirstElement(OList).f();
}
public E getFirstElement(List<? extends E> list) {
return list.get(0);
}
}
在getFirstElement方法中,我们接受一个E类型或者其子类的容器,我们知道这个容器中取出的元素一定是E类型或者其子类。当我们给Test绑定泛型参数的时候,我们就可以根据泛型参数调用其相应的方法。
上面的代码,如果我们用泛型方法也是可以做到的。其代码如下:
public class Test<E>{
public static void main(String[] args) {
List<? extends B> OList;
ArrayList<B> BList = new ArrayList<>();
BList.add(new B());
OList = BList;
new Test<A>().getFirstElement(OList).f();
}
public <T extends E > E getFirstElement(List<T> list) {
return list.get(0);
}
}
这两者都是可以达到同样的效果。
我们回到之前泛型通配符的问题上来,因为使用了通配符。我们无法向其中加入任何元素,只能读取元素。如果我们一定要加入元素,我们要怎么办?这个时候,需要使用通配符的 下界。? super T表示类型参数是T或者T的父类。这样,我们就可以向容器中加入元素。比如下面的代码:
public class Test {
public class Test{
public static void main(String[] args) {
List<? super A> OList = new ArrayList<>();
OList.add(new B());
}
? super A 表示类型是A或者A的子类。根据向上转型一定安全的原则,我们的类型参数为A的容器中加入其子类B一定是成功的,因为B可以向上转型成A,但是这个面临和上面一样的问题,我们无法加入任何A的父类,比如Object对象。
然而在引用的时候,我们可以把OList引用到一个Object类型的ArrayList中。
List<? super A> OList = new ArrayList<Object>();
就和下面代码的引用会成功一样。
List<? extends A> Olist = new ArrayList<A>();
第一个通配符下界表明Olist是具有任何是A父类的列表。
第二个通配符上界表明OList是具有任何是A子类的列表。
我们把一个符合要求的列表引用给他是成功。但是他们在读取和写入数据是不同的。拥有下界的列表表明我们可以把A类或者子类的数据写入到这个列表中,但是我们无法调用任何A类的方法,因为编译器只知道返回的是A的父类或者A,但是具体是哪一个,他不知道,自然也无法调用除Object以外的任何方法。拥有上界的列表表明我们可以从列表中读取数据,因为数据一定是A类或者A的子类。但是由于无法确定具体是哪一个类型,我们自然也无法向其加入任何类型。
如果我们要读取数据,则用拥有上界的通配符,如果我们要写入数据,则用拥有下界的通配符。既读又写则不要用通配符。