Java泛型总结

什么是Java泛型

  Java泛型(Java generic)是JDK 5引入的一个新特性.其本质就是参数化类型,也就是把数据类型视作为一个参数,在使用的时候再指定具体类型,这种参数化类型就是泛型.泛型可以用在类,接口,方法上,分别称之为泛型类,泛型接口,泛型方法.
  泛型的出现为程序员提供了一种编译时类型安全的监测机制,使程序员能够在编译期间找出非法类型的存在,提高开发的安全性和效率.
  Java中的泛型一种伪泛型,使用了类型擦除实现的,本质上是Java语言的语法糖.


为什么出现Java泛型

看下面的情形

1
2
3
4
5
6
7
8
9
10
11
public int add(int a, int b) {
return a + b;
}

public float add(float a, float b) {
return a + b;
}

public double add(double a, double b) {
return a + b;
}

  我们为了实现不同数据类型的add方法,就需要给每种类型都写一个重载方法,这显然不符合我们开发的需求.

  如果我们在这里使用泛型的话,就不需要给每种数据类型都增加一个重载方法.

1
2
3
public <T extends Number> double add(T a, T b) {
return a.doubleValue() + b.doubleValue() ;
}

  如果我们在这里使用泛型的话,就不需要给每种数据类型都增加一个重载方法.

再看下面的情形

step1

  在上面代码我们定义了一个List类型的集合,先向里面加入了两个String类型的值,然后又加入了一个Integer类型的值,这在Java编译期间是允许的,因为List默认的类型是Object.在后面的循环中,因为之前加入的数据类型不一样,很可能出现ClassCastException异常.

从上我们可以大概总结到泛型有以下好处
1,代码复用,增加代码拓展性
2,消除强制类型转换,类型安全

泛型的使用

  泛型可以用在类,接口,方法上,分别可以称为泛型类,泛型接口,泛型方法.

泛型类/泛型接口

  泛型类和泛型接口的定义基本相同,就是引入一个类型变量T(其他大写字母也OK,一般约定俗成的有T,E,V,K,N等),并用<>括起来,放在类名/接口名的后面,泛型类和接口允许有多个类型变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//泛型类(1)
public class GenericClass<T> {
private T data;
}
//泛型类(2)
public class GenericClass<T,K> {
private T data;
private K result;
}

//泛型接口
public interface IGeneric<T> {
T getData();
}

泛型方法

  泛型方法,是在调用方法的时候指明泛型的具体类型,泛型方法可以在任意地方任意场景中使用,包括普通类和泛型类.

1
2
3
4
5
6
7
8
9
//普通方法
public T getData() {
return data;
}

//泛型方法
public <V> V handleData(V data) {
return data;
}

  一定要注意的是,并不是方法中参数或者返回值包含了泛型的就是泛型方法,只有在调用的时候需要指明泛型的才是泛型方法.

限定类型变量

  有时候,我们需要对泛型的类型进行限定,比如说我们写一个泛型方法比较两个变量的大小,我们怎么确保传入的两个变量都一定有compareTo方法呢?这个时候我们就可以使用T extends Comparable对泛型的类型变量进行限制,
step2

  T表示应该绑定类型的子类型,Comparable表示绑定的类型,子类型和绑定类型可以试类,也可以是接口
  这个时候如果我们试图传入一个没有实现接口Comparable的实例变量,将会发生变异错误
step3

泛型类的继承规则

1,泛型参数是继承关系的,泛型类之间没有继承关系

1
2
3
4
5
6
7
8
9
public class Animal {}

public class Cat extends Animal { }

public class Pet<T> { }
public class ExtendPet<T> { }

Pet<Animal> genericClass =new Pet<Cat>(); //错误

2,泛型类是可以继承其他泛型类的,比如List和ArrayList

1
2
3
4
5
6
7
8
public class Animal {}

public class Cat extends Animal { }

public class Pet<T> { }
public class ExtendPet<T> { }

Pet<Animal> genericClass =new ExtendPet<>(); //正确

通配符使用

数组的协变

  在讲泛型通配符之前,我们先了解下数组,在Java中,数组是可以协变的,什么是协变,我们以下面的例子讲解下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Animal { }

public class Dog extends Animal { }

public class JingBa extends Dog{ }

public class Cat extends Animal { }

public class GenericClass {
public static void main(String[] args) {
Animal[] animals = new Dog[5];
animals[0] = new Dog();//可以
animals[1] = new JingBa();//可以
System.out.println(animals.getClass());
System.out.println(animals[0].getClass());
System.out.println(animals[1].getClass());
//animals[2] = new Animal();//ArrayStoreException
// animals[3] = new Cat();//ArrayStoreException
}
}

1
2
3
4
5
6
//打印结果
----------------------------------------------------
class [Lcom.company.genneric.Dog;
class com.company.genneric.Dog
class com.company.genneric.JingBa
----------------------------------------------------

  在上面的代码中,创建一个Dog数组并将他赋值给Animal数组的引用.像这种具有子父类关系的类,子类数组也是父类数组的情况就是数组协变
  不过在使用数组协变,也有些事情要注意,就是有些问题在运行的时候才能发现.还是看上面的代码.尽管Dog[]可以”向上转型”为Animal[],但是数组中实际的类型还是Dog对象,我们在上面放入Dog或者Dog的子类JingBa的对象时都是可以的,但是在放入Animal或者Cat对象的时候会在运行的时候报ArrayStoreException异常.
  泛型设计的目的之一就是将一些运行时候的错误暴露在编译期间,我们下面使用Java提供的泛型容器List,看下会发生什么.

1
2
3
public static void main(String[] args) {
ArrayList<Animal> list = new ArrayList<Dog>();
}

step7
  看到了,上面的代码根本无法编译,直接报错,当涉及到泛型的时候,尽管Dog是Animal的子类,ArrayList却不是ArrayList的子类,也就是说泛型不支持协变

通配符的使用

  如果我们要实现类似上面数组的协变怎么办呢,这时候我们就用到了通配符.Java中泛型通配符分为3种,我们依次来讲.

上边界通配符

  使用<? extends Parent>的就是上边界通配符,他指定了泛型类型的上边界,类型参数都是Parent的子类,通过这种通配符可以实现泛型的”向上转型”.

1
2
3
4
5
6
7
8
9
10
    public static void main(String[] args) {
List<? extends Animal> list = new ArrayList<Dog>();

// list.add(new Dog());//编译错误,Compile Error:cant't add any type of object
// list.add(new JingBa());//编译错误,Compile Error:cant't add any type of object
// list.add(new Cat()));//编译错误,Compile Error:cant't add any type of object
// list.add(new Object()));//编译错误,Compile Error:cant't add any type of object
list.add(null);
Animal a = list.get(0);
}

  在上面的代码中,list的类型是List<? extends Animal>,可以把list看成是一个类型的List,这个类型是可以继承Animal的,但是需要注意的是,这并不是说这个List就可以持有Animal的任意类型.通配符代表的是某种特定的类型,但是上面的list没有指定实际的类型,它可以是Animal的任何子类型,Animal是它的上边界.

  既然我们不知道这个list是什么类型,那我们如果安全的添加一个对象呢?在上面的例子中我们也看到了,无论是添加Dog,JingBa,Cat还是Object对象,编译器都会报错,唯一能通过编译的就是null.所以如果我们写了向上转型<? extends Parent>的泛型那么我们的List将失去添加任务对象的能力,及时Object对象也不行.

  另外如果我们获取返回Animal的方法,这是可以的,因为在这个list中,不管它实际的类型到底是什么,肯定可以转型成Animal的,所有向上转型返回数据是允许的.

总结:主要用于安全地访问数据,可以访问Parent及其子类型,并且不能写入非null的数据

下边界通配符

  使用<? super Child>的就是下边界通配符,他指定了泛型类型的下边界,类型参数都是Child的基类,通过这种通配符可以实现泛型的”向下转型”.

1
2
3
4
5
6
7
8
        List<? super Dog> list = new ArrayList<>();
list.add(new Dog());
list.add(new JingBa());
list.add(null);
// list.add(new Cat());//编译错误,Compile Error:cant't add any type of object
// list.add(new Object());//编译错误,Compile Error:cant't add any type of object
Object object = list.get(0);

  在上面的代码中,我们也不能确定list里的是什么类型,但是我们知道这个类型一定是Dog的基类(父类),因此我们向里面添加一个Dog对象或者Dog子类型的对象是安全的,这些对象都可以向上转型为Dog.我们在取出list里面的数据的时候,返回的一定是Dog的基类(父类),但到底是哪一个基类(父类)我们是不知道的,但在java中所有的类型都继承自Object,所有在list取出的数据,返回的一定是Object.

总结:主要用于安全地写入数据,可以写入Child及其子类型

无边界通配符

<?>无边界通配符,没有任何限定.

1
2
3
4
5
6
7
        List<?> list = new ArrayList<>();
// list.add(new Animal());//编译错误,Compile Error:cant't add any type of object
// list.add(new Dog());//编译错误,Compile Error:cant't add any type of object
// list.add(new Cat());//编译错误,Compile Error:cant't add any type of object
// list.add(new Object());//编译错误,Compile Error:cant't add any type of object
list.add(null);
Object object = list.get(0);

  List<?>表示持有某种特定类型的List,但是这种List并没有指定具体类型,这是不安全的,所以我们不能向里面添加除null以外的对象

List<?>与List的区别?

  List没有泛型参数,表明这个List持有元素的类型是Object,因此可以添加任何类型的对象,不过编译器会有警告信息.

泛型中的约束和局限性

1,不能用基本数据类型实例化类型参数
step4

2,运行时类型查询只适用于原始类型
step5

3,泛型类的静态变量或者方法不能使用泛型类型
注:静态方法本身就是泛型方法除外
step6
  不能在静态方法和变量中引用泛型类型变量,因为泛型是在对象创建的时候才知道是什么类型,而对象创建代码的执行顺序是static,构造方法,所以在对象初始化之前static的部分已经执行了.

4,不能创建泛型数组
step8

虚拟机中泛型的实现-类型擦除

  Java泛型是在Java1.5以后才出现的,在Java早期版本中并没有泛型概念,为了向下兼容,Java泛型只存在在编译期,在编译后,就会替换成原生类型,并在相应的地方插入强制转换类型的代码,因此对于运行期的Java语言来说,ArrayList和ArrayList就是一个类,这种编译后去除类型信息的方式就叫做类型擦除.

1
2
3
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1==c2);

  泛型参数会擦除到他的第一个边界,如果参数类型是单独的一个T,那么最终会擦除到Object,相当于所有使用T的地方都会被Object替换,对于虚拟机来说,最终保存的参数类型还是Object.之所以还可以取出来我们传入的参数类型,是因为编译器在编译生成字节码的过程中,插入了类型转换的代码.

 wechat
欢迎订阅微信公众号:oopanda
给作者加个鸡腿!
0%