泛型的定义
泛型:参数化的类型。很简单的一句话,那么什么叫做“参数化的类型”呢?。。。。。
为什么需要泛型?
假设现在有这样一个需求:把两个整数进行相加并返回计算结果。我们很轻松的就可以写出如下代码来完成此功能:
public int addInt(int x, int y) {
return x + y;
}
后来有一天业务拓展,需要支持浮点数进行相加并返回计算结果。于是我们可以新增一个新的如下方法来完成新需求:
public float addFloat(float x, float y) {
return x + y;
}
日子一天天的过去,又有了新的需求,需要支持double类型的相加,那么我们依然可以依葫芦画瓢的再增加一个新的方法来完成double类型的数据相加。诶?等等。。。观察上面的两个方法除了参数的类型和返回值不同之外,其他的都相同(方法名是可以相同的,称之为重载,此处为了区分类型,故写作不同名称)。于是为了解决这个问题,java就引入了泛型机制,可以很好的解决重复代码。
我们平常用的最多的数据结构恐怕就是List了。例如:
List list = new ArrayList();
list.add("element0");
list.add("element1");
list.add(100);
这段代码无论在编写阶段还是运行阶段都是不会报错的。
for (int i = 0; i < list.size(); i++) {
String value = (String) list.get(i);
System.out.println("第" + (i + 1) + "个元素的值是: " + value);
}
可是当我们需要用上面的代码遍历输出集合中元素的时候,却会报出ClassCastException。这时因为最后添加的一个元素是 int 类型,强转为 String 类型肯定是会报错的。于是这里就体现出了泛型的第二个好处:安全。于是我们经常如下去创建一个List:
List<String> list = new ArrayList<>();
list.add("element0");
list.add("element1");
list.add(100); // 编译器会提示此行代码报错
并且加了泛型之后在取值的时候也不需要强制类型转换了。
综上所述:1.适用于多种数据类型执行相同的代码。2.在编译期间就发现数据类型安全问题。这也就是我们为什么要使用泛型的原因。
泛型的使用
- 泛型类
public class GenericClass<T> {
private T data;
public GenericClass(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
- 泛型接口
public interface GenericInterface<T> {
T next();
}
关于泛型接口的实现有两种:
public class GenericInterfaceImpl1 implements GenericInterface<String>{
@Override
public String next() {
return null;
}
}
public class GenericInterfaceImpl2<T> implements GenericInterface<T>{
@Override
public T next() {
return null;
}
}
这两种方式的区别就是一个是在使用的时候才确定具体类型,一个在声明类的时候就确定了类型。
- 泛型方法
public <T> T genericMethod(T... t) {
return t[t.length / 2];
}
其中<T>是定义泛型方法所必须的,如果没有的话,即使这个方法带有 T 、或者 E 这种常见的泛型定义,那么它依然不是一个泛型方法。例如上面泛型类中的 getData() 或者 setData(T data),前面没有<T>,它们依旧是普通的方法,只不过他们是定义在了泛型类中罢了。
类型变量的限定-用于方法上
public static <T extends Comparable> T min(T a, T b) {
if (a.compareTo(b) > 0) return a;else return b;
}
extends 关键字从面相对象上严格来说是叫派生。那么 T extends Comparable 就可以理解为派生自 Comparable 这个接口的子类。故下面可以使用Comparable 接口中的 compareTo() 方法。假如尖括号中只有一个 T,那么参数a是无法调用compareTo()方法的。这就是所谓的类型变量的限定。
关于限定类型的使用有如下规则:
- 可以有多个限定类型,中间用 & 符号隔开
- 可以是类,也可以是接口。如果是类的话需要放在第一个的位置
- 有且只能有一个类,接口则没有限制。因为java是单继承,多实现。
下面是简单示例:
public static <T extends View & Comparable & Serializable> T min(T a, T b) {
if (a.compareTo(b) > 0) return a;else return b;
}
此时传入的参数需要满足是 View 的子类,且同时实现了Comparable 和 Serializable 接口才能使用。否则无法通过编译。
类型变量的限定-用于类上
public class GenericClass<T extends Comparable> {
private T data;
public GenericClass(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public T min(T t) {
if (this.data.compareTo(t) > 0) {
return t;
} else {
return this.data;
}
}
}
这里是把上面泛型类进行了一个改造,这时泛型的具体类型只能是 Comparable 的实现类。
泛型的约束和局限性
- 不能实例化类型变量
还是以上面的泛型类为例。这时我们给它添加一个无参的构造方法。
public class GenericClass<T> {
private T data;
public GenericClass() {
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
假如我们想要在构造方法中初始化 T 的实例是不允许的。编译器会直接报错:
-
静态域或静态方法不能引用类型变量
编译器会直接报错。这个问题的答案其实很简单:泛型的具体类型是在创建出具体的泛型类的对象的时候才确定的,而静态代码的执行是早于对象的创建的。那么虚拟机根本就不知道静态的 T 是什么。但如果是静态泛型方法则是可以的:
public static <E> void print(E e) {
System.out.println(e.toString());
}
- 基本数据类型不能作为泛型
GenericClass<double> genericClass1 = new GenericClass<>(); // 报错 基本类型不可以
GenericClass<Double> genericClass2 = new GenericClass<>(); // 需要用其包装类
- 不能使用 instanceof 关键字
通常我们需要判断一个对象是不是某种类型的时候,都会用 instanceof 关键字来判断。可是判断某个对象是不是某个泛型类的类型时却不可以。例如:
GenericClass<Float> genericFloat = new GenericClass<>();
if (genericFloat instanceof GenericClass<Float>){ // 这一行编译器会报错
}
-
不能实例化泛型数组
可以声明泛型数组,但是却不能实例化。这是 java 语法规定。是不是很奇葩?
-
泛型类不能继承 Exception 或 Throwable
非常简单粗暴,编译器直接提示泛型类不能派生自 Throwable。
-
不能捕获泛型类对象
但是下面这种写法确实允许的:
通配符
假设现在有两个如下类,且存在继承关系:
public class Animal {
}
public class Dog extends Animal{
}
Dog 是 Animal 的子类,那么问题来了:
GenericClass<Animal> animalGeneric = new GenericClass<>();
GenericClass<Dog> dogGeneric = new GenericClass<>();
请问GenericClass<Animal> 和 GenericClass<Dog> 之间存在继承关系吗?答案显然是否定的。但是泛型类可以继承或扩展其他泛型类,例如 List 和 ArrayList 之间的关系:
public class GenericClassChild<T> extends GenericClass<T>{
}
再假设现在有如下一个方法:
public static <T> void set(GenericClass<Animal> genericClass) {
}
那么下面两种调用可以吗?
set(animalGeneric); // 1
set(dogGeneric); // 2
很显然,第一种是肯定可以的。第二种就不可以了。于是为了解决这种问题就有了通配符的概念。
-
? extends
现在有如下4个类,且有如下继承关系:
假设现在有如下4个对象和对应的打印方法:
GenericClass<Fruit> fruit = new GenericClass<>();
GenericClass<Apple> apple = new GenericClass<>();
GenericClass<RedApple> redApple = new GenericClass<>();
GenericClass<Orange> orange = new GenericClass<>();
public static <T> void printExtends(GenericClass<? extends Apple> genericClass) {
}
下面我们来看一下调用结果:
从上图中我们可以得知 ?extends 限定了传入参数的上边界。那这样使用有没有什么限制呢?
可以看到当我们尝试着设置数据的时候是不允许的,当我们要取数据的时候却只可以用基类 Fruit 去接收。其实这也很好解释。上面我们说了?extends 限定了上边界,也就是说当我们取数据的时候,不管这个时候这个对象里有什么,但是一定是 Fruit 的子类,根据多态的性质,可以用父类来接收子类。至于设置数据的时候为什么不可以也就很好解释了。因为编译器只知道传入的是 Fruit 的子类,而至于传入的具体是哪一个子类,编译器是不知道的。至此我们可以总结一下:?extends 限定了传入参数类型的上界,用于安全的访问数据
- ?super
GenericClass<Fruit> fruit = new GenericClass<>();
GenericClass<Apple> apple = new GenericClass<>();
GenericClass<RedApple> redApple = new GenericClass<>();
GenericClass<Orange> orange = new GenericClass<>();
public static <T> void printSuper(GenericClass<? super Apple> genericClass) {
}
下面我们来看一下调用结果:
可以看到?super正好是限定了传入参数的下边界。下面我们来看一下有什么限制:
不是说 super 是限定了下边界吗?怎么在设置数据的时候父类 Fruit 反而不行了呢?而子类 RedApple 却可以呢?这是因为所有类的父类都是Object,而编译器无法确定传入的是什么类型,但是 Apple 和 Apple 的子类是可以安全的转型为Apple的,可以满足最下边界,所以可以安全传入。这也就是为什么最后获取的时候直接得到的是 Object ,而不是 Fruit 。至此我们可以总结一下:?super 限定了传入参数类型的下界,用于安全的设置数据