什么是泛型
泛型就是广泛的类型,同一套代码可以在多种类型中使用,使代码的可重用性更高。泛型是JDK1.5加的新特性。
为什么使用泛型
加入现在有对int类型数值求和的需求,那我们可能会这样写:
public int sumInt(int x, int y){
return x + y;
}
这样写法没有任何问题,但是如果又来了一个新需求是需要对float类型的数值进行求和,那我们是需要再写一个sumFloat方法吗?
public float sumFloat(float x, float y){
return x + y;
}
虽然写法上没有什么错误,但是在代码扩展性和优雅方面是不好的。
虽然在求和这个需求上对于不同的类型分别写不同的求和方法,这在功能实现上是没有问题的。再来看看接下来的需求。
将全班同学的姓名全部添加到一个List中,在JDK1.5之前是没有泛型的,实现方式是这样的:
List list = new ArrayList();
list.add("张三");
list.add("李四");
// 不小心填入了一个int型
list.add(10);
// 获取元素
String s0 = (String) list.get(0);
String s1 = (String) list.get(1);
String s2 = (String) list.get(2);
在给List存String类型的时候,如果不小心存入了非String类型的数据,编译器在编译期的时候不会报错,但是在运行期获取到List的元素的时候会报错。上面代码在获取s2
时报出java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
的错误,因为存进去的时候是一个int类型,但是取出来的时候需要强转成String类型,但是int不是String的子类,所以会强转失败。
所以,使用泛型有以下几大好处:
- 一套代码可以多处重用
- 填写错误编译器可以在编译期就可以暴露出错误
- 存进去是什么类型,取出来就是什么类型,无需类型强转
泛型可以使用在类、接口和方法上,他们分别称为泛型类、泛型接口和泛型方法,那应该怎么定义泛型呢?
泛型类
定义泛型类和泛型接口是一样的,只需要在类名后面增加
,那么这个类就是泛型类了(接口就是泛型接口),T
是自己自定义的,可以随便换成其他的。
public class Bag { // 包、袋子
}
泛型也可以指定多个类型
public class MyMap {
}
继承/实现一个泛型类有两种方式,因为泛型类和泛型接口方式基本一样,这里就以泛型类来举例:
-
子类不确定其泛型
public class FruitBag
extends Bag { // 水果袋 } 水果袋也没有确定该装哪种水果,因此也需要在
FruitBag
后面增加
。在实例化的时候就需要指明其类型FruitBag
appleBag = new FruitBag<>();
-
子类确定其泛型
public class FruitBag extends Bag
{ // 水果袋 } 水果袋子指明了只能装苹果,只需要在父类后面增加
即可。在实例化的时候和普通的类一样FruitBag appleBag = new FruitBag();
泛型方法
在方法中含有
的方法就是泛型方法(不包含参数中的
),要区分泛型方法和普通方法的区别。需要注意的是,声明泛型方法的类不一样是泛型类,泛型类中也可以不声明泛型方法。
接下来看看泛型方法和普通方法的区别:
// 只演示泛型语法,忽略一些可能带来的异常问题
public class Bag {
private List list = new ArrayList<>();
public void put(T t) {
list.add(t);
}
public T get(int index) {
return list.get(index);
}
public void set(Bag bag) {
}
}
put
和get
方法中没有
,因此是它们都是普通方法,而set
方法中虽然有
,但是它是在参数中的,因此它依旧是一个普通方法。
public class Bag {
public T set(T t) {
return t;
}
}
上面的set
方法才是一个泛型方法,在public
后面带有
,这里要注意的是,set
方法中的T
和Bag
的T
没有关系,它们是独立的类型,只是碰巧都是用T
来表示了。
限定类型变量
假设我们现在有一个需求是比较水果的重量,Fruit类有一个weight
属性来表示水果的重量,我们要通过比较weight
属性来比,因此需要实现Comparable
接口并重写其compareTo
方法,在该方法中写判断的逻辑。
// Fruit.java
public class Fruit implements Comparable {
private float weight;
public Fruit(float weight) {
this.weight = weight;
}
@Override
public int compareTo(Fruit o) {
float result = this.weight - o.weight;
if (result > 0) {
return 1;
} else if (result == 0) {
return 0;
} else {
return -1;
}
}
}
// GenericTest.java
public class GenericTest {
public static void main(String[] args) {
Fruit a = new Fruit(10);
Fruit b = new Fruit(11);
Fruit max = max(a, b); // 返回较重的水果
}
private static Fruit max(Fruit a, Fruit b) {
if (a.compareTo(b) >= 0) return a;
else return b;
}
}
上面的max
方法固定了只能比较Fruit
的重量,但是现在如果我想比较苹果、香蕉或者非水果类的重量呢?如果定义成以下泛型:
会提示泛型T没有compareTo
方法。这时就需要对泛型进行类型限定了,让泛型T
继承自Comparable
接口
private static > T max(T a, T b) {
if (a.compareTo(b) >= 0) return a;
else return b;
}
给泛型增加了类型限定,现在只要是实现了Comparable
接口的类都可以作为参数传进来进行比较。Comparable
表示绑定类型,T
表示绑定类型的子类型,子类型和绑定类型可以是类也可以是接口。
子类型和绑定类型都可以是一个或者多个,但是绑定类型的类只能有一个且只能放在第一个,多个的话后面只能是接口,多个绑定类型用&
连接。
public class A {}
public interface B {}
private static void void min(T a, T b){
}
min
方法定义了两个子类型,其中子类型T
继承类A & 接口B,需要注意的是子类型K只是一个普通的类型,并没有继承。
泛型的约束与局限性
泛型也有一些约束和限制。
- 泛型不能使用基本类型
不能使用基本类型,应该使用类类型和基本类型的包装类型
- 运行时类型判断不能携带泛型
- 不能使用
static
修饰泛型
因为泛型是在对象创建的时候才确定,而对象创建会先执行静态修饰的部分然后才是构造器等,所以在对象初始化之前就已经执行了static
部分,所以虚拟机就不知道泛型具体是指什么类型了。
- 不能初始化泛型数组
```java
public class Fruit implements Comparable {}
public class Apple extends Fruit {}
public class Bag {}
public class FruitBag extends Bag {}
public class AppleBag extends FruitBag {}
```
- 不能实例化泛型变量
- 泛型类不能继承
Exception
和Throwable
或其子类
- 不能捕获泛型
根据上面的代码应该可以大致得出:泛型类型不能被捕获,但是可以抛出泛型类型异常。
泛型的继承规则
现有Fruit
和Apple
两个类,Apple
派生自Fruit
。Apple
是Fruit
的子类,但是List
却不是List
的子类。但是泛型类可以继承或实现其它泛型类,如:
public class ArrayList extends AbstractList implements List
泛型通配符
泛型有2中通配符,一个是上界通配符? extends Class
,另一种是下界通配符? super Class
。
-
? extends Class
上界通配符,表示传入的类型必须是
Class
的子类或者是Class
本身。举个例子:public class Bag
{ private T t; public T get() { return t; } public void set(T t) { this.t = t; } } public class Fruit {} public class Apple extends Fruit {}
定义了三个类,一个泛型类Bag
,一个水果类Fruit
,一个苹果类Apple
,Apple
继承Fruit
。使用上界通配符定义了一个包对象,代表包里面可以装水果,只要是水果都可以装,然后有创建了一个水果对象和苹果对象。但是在调用bag.set
方法的时候编译器不通过,但是在bag.get
的时候可以返回一个Fruit
对象。这是为什么呢?
因为? extends Fruit
已经确定了上界是Fruit
,set
的时候可以传入Fruit
对象,也可以传入Apple
对象,这样编译器就不确定具体是哪个类的对象了,而泛型的意义就是要确定某一种类型,这与泛型的设计初衷相违背了;get
的时候编译器可以明确它取出来就是一个Fruit
。所以不允许set
,但是可以get
。
主要用于安全地访问数据,可以访问数据但是不能写入数据。
-
? super Class
下界通配符,表示传入的类型必须是
Class
本身或者是Class
的父类。类的定义还是和上面一样,这里再增加一个Apple
的子类GreenApple
public class GreenApple extends Apple {}
现在的bag
对象用的是下界通配符,表示包里只能装Apple
以及Apple
的父类。创建了一个水果对象,一个苹果对象,一个青苹果对象。在分别调用bag.set
时,设置fruit
的时候报错,添加apple
和greenApple
就可正常,bag.get
返回的却是Object
。
因为? super Apple
已经确定了下界是Apple
,只有Apple
和Apple
的子类才能安全的转成Apple
,因此能设置下界类和下界类的子类(下界类此处表示的是Apple)。而其父类可以有多个但是并不能安全的转换成Apple
,因此它只能set下界类本身及其子类。在get
获取的时候虚拟机并不知道具体是什么父类,但是有一点很明确的是,最终父类一定是Object
。
无限定通配符
还有一种通配符叫做无限定通配符,用?
表示,它表示没有限制,可以看成任意类型,并没有什么实质性的作用。
虚拟机是如何实现泛型的
泛型是从JDK1.5才有的,那么为了兼容以前的JDK,Java采用了泛型擦除来兼容。那什么是泛型擦除呢?简单的来说就是Java代码在编译之后是不会保留泛型的类型的。比如List
在编译后,字节码中变成了List
,再也没有了泛型类型了。也就是说在运行时,List
和List
其实是一种类型。写个代码验证一下:
public static void main(String[] args) {
List listString = new ArrayList<>();
List listDouble = new ArrayList<>();
System.out.println(listString.getClass());
System.out.println(listDouble.getClass());
}
// 输出
class java.util.ArrayList
class java.util.ArrayList
虽然定义的是两个类型不同的List
,但是获取到的类型都是ArrayList
,这很好地 证明了泛型擦除的机制。
通过使用ASM Bytecode Viewer
插件将代码转成字节码来看,
从椭圆形框中可以看出,左边Java代码中的第10行和第11行,泛型类型都变成了Object,而不是定义时候的类型。
与 extends X>的区别
-
前者可用于定义泛型类,而后者不行
extends X>
-
前者在运行时确定了具体的一种类型,该类型是X或者X的子类中的某一个,而后者是X后者X的子类都可以
class Bag
{} class Food {} class Fruit extends Food {} class Apple extends Fruit {} extends X> 前者在声明泛型类、泛型接口、泛型方法时使用,后者在声明变量、方法形参时使用