一、什么是泛型?
泛型,即“参数化类型”。
比如定义一个变量A,我们可以通过如下方式将这个变量定义为字符串类型或者整形。
String A;
Integer A;
当然这是在变量类型已知的情况下,如果有一种情况,我们在定义变量的时候不知道以后会需要什么类型,或者说我们需要兼容各种类型的时候,又该如何定义呢?
鉴于以上这种情况,我们可以引入“泛型”,将String和Integer类型进行参数化。在使用的时候,再传入具体的参数类型。
泛型的本质是为了参数化类型(通过传入的不同类型来决定形参的具体类型)。也就是说在泛型使用过程中,数据的类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
二、泛型类
Computer类,这个类中包含一个属性t,类型为String。
public class Computer {
private String t;
public void set(String t) {
this.t = t;
}
public String get() {
return this.t;
}
}
泛型类Computer
//这里的"T"并不是固定写法,也可以用V或M等字符
public class Computer<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return this.t;
}
}
//可以传入多种泛型参数
public class Computer<T, V> {
private T t;
private V v;
public void set(T t, V v) {
this.t = t;
this.v = v;
}
public T getT() {
return this.t;
}
public V getV() {
return this.v;
}
}
定义泛型类的好处就是,我们可以在需要的时候,再去指定属性t的类型,增强了通用性。
Computer<String> computer1= new Computer<String>();
Computer<Integer> computer2= new Computer<Integer>();
Computer<Double> computer3= new Computer<Double>();
三、泛型接口
//定义接口Calculatable
public interface Calculatable<T> {
T execute();
}
//传入String类型的实参
public class Computer implements Calculatable<String> {
@Override
public String execute() {
return "ok";
}
}
//传入Integer类型的实参
public class Calculator implements Calculatable<Integer> {
@Override
public Integer execute() {
return 100;
}
}
//未传入具体实参,继续抛出,由下层传入
public class Phone<V> implements Calculatable<V> {
private V v;
@Override
public V execute() {
return this.v;
}
}
四、泛型方法
public class Computer<T> {
private T t;
//不是泛型方法
public void set(T t) {
this.t = t;
}
//不是泛型方法
public T get() {
return this.t;
}
//泛型方法
//首先public与返回值类型之间的<V>必不可少,只有声明了<V>的方法才是泛型方法
//可以声明多个泛型,如 public <V,M> genericMethod(V v,M m)
public <V> V genericMethod(V v){
return v;
}
}
Computer<String> computer = new Computer<String>();
Integer v= 100;
System.out.println(computer.genericMethod(v).getClass().toString());
Double v2= 100d;
System.out.println(computer.genericMethod(v2).getClass().toString());
输出结果:
class java.lang.Integer
class java.lang.Double
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。
泛型方法,可以通过传入的实参,判断V的具体类型。
五、静态方法与泛型
如果静态方法不可以使用类中定义的泛型,如果静态方法想要使用泛型,需要将自身声明为泛型方法。
public class Computer<T> {
public static <V> void get(V v){
//T t;报错,不能使用类中定义的泛型
}
}
六、通配符及上下边界
举一个简单的例子
//水果类
public class Fruit {
private String name;
public Fruit(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
fsadfdsfd
//苹果类
public class Apple extends Fruit {
public Apple(String name) {
super(name);
}
}
//装水果的袋子
public class GenericHolder<T> {
private T obj;
public GenericHolder() {
}
public GenericHolder(T obj) {
this.obj = obj;
}
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
测试类:
public class AppTest extends TestCase {
@Test
public void test() {
//这是一个贴了水果标签的袋子贴了
GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
//这是一个贴了苹果标签的袋子
GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
//这是一个水果
Fruit fruit = new Fruit("水果");
//这是一个苹果
Apple apple = new Apple("苹果");
//现在我们把水果放进去
fruitHolder.setObj(fruit);
//调用一下吃水果的方法
eatFruit(fruitHolder);
//贴了水果标签的袋子放水果当然没有问题
//现在我们把水果的子类——苹果放到这个袋子里看看
fruitHolder.setObj(apple);
//同样是可以的,其实这时候会发生自动向上转型,apple向上转型为Fruit类型后再传入fruitHolder中
//但不能再将取出来的对象赋值给redApple了,因为袋子的标签是水果,所以取出来的对象只能赋值给水果类的变量
//无法通过编译检测 redApple = fruitHolder.getObj();
//调用一下吃水果的方法
eatFruit(fruitHolder);
//放苹果的标签,自然只能放苹果
appHolder.setObj(apple);
//报错,这时候无法把appHolder传入eatFruit,因为GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型,GenericHolder<Fruit>只允许传入水果类的袋子
// eatFruit(appHolder);
}
public static void eatFruit(GenericHolder<Fruit> fruitHolder){
System.out.println("我正在吃 " + fruitHolder.getObj().getName());
}
}
执行结果:
我正在吃 水果 我正在吃 苹果
GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型,所以无法通过编译。
从Java继承的角度上可以分析:
苹果 IS-A 水果
装苹果的袋子 NOT-IS-A 装水果的袋子
那么问题来了,如果我想让eatFruit方法能同时处理GenericHolder<Fruit> 和 GenericHolder<Apple>两种类型怎么办?而且这也是很合理的需求,毕竟Apple是Fruit的子类,能吃水果,为啥不能吃苹果???如果要把这个方法重载一次,未免也有些小题大做了。
这个时候,泛型的边界符就有它的用武之地了。我们先来看效果:
public class AppTest extends TestCase {
@Test
public void test() {
//这是一个贴了水果标签的袋子
GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
//这是一个贴了苹果标签的袋子
GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
//这是一个水果
Fruit fruit = new Fruit("水果");
//这是一个苹果
Apple apple = new Apple("苹果");
//现在我们把水果放进去
fruitHolder.setObj(fruit);
//调用一下吃水果的方法
eatFruit(fruitHolder);
//放苹果的标签,自然只能放苹果
appHolder.setObj(apple);
// 这时候可以顺利把appHolder 传入eatFruit
eatFruit(appHolder);
//这种泛型 ? extends Fruit不能存放Fruit,但是可以使用 ? extends Fruit的变量指向GenericHolder<Fruit>的对象
GenericHolder<? extends Fruit> fruitHolder22 = fruitHolder;
// fruitHolder22.setObj(fruit);//报错,不能存放fruit
System.out.println(fruitHolder22.getObj().getName()+"----");
//这种泛型 ? extends Fruit不能存放apple,但是可以使用 ? extends Fruit的变量指向GenericHolder<apple>的对象
fruitHolder22 = appHolder;
// fruitHolder22.setObj(apple);//报错不能存放apple
System.out.println(fruitHolder22.getObj().getName()+"^^^^");
}
public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){
System.out.println("我正在吃 " + fruitHolder.getObj().getName());
Fruit fruit1 = new Fruit("水果1");
//报错,不能存入fruit1 。因为引入传入的参数fruitHolder是Fruit的子类。
//Error:(35, 28) java: 不兼容的类型: Fruit无法转换为capture#1, 共 ? extends Fruit
//fruitHolder.setObj(fruit1);
}
}
运行结果:
我正在吃 水果
我正在吃 苹果
水果----
苹果^^^^
这就是泛型的边界符,用<? extends Fruit>的形式表示。边界符的意思,自然就是定义一个边界,这里用?表示传入的泛型类型不是固定类型,而是符合规则范围的所有类型,用extends关键字定义了一个上边界。
有上边界,自然有下边界,泛型里使用形如<? super Fruit>的方式使用下边界。
这两种方式基本上解决了我们之前的问题,但是同时,也有一定的限制(PECS原则)。
1.上界<? extends T>不能往里存,只能往外取
不要太疑惑,其实很好理解,因为编译器只知道容器里的是Fruit或者Fruit的子类,但不知道它具体是什么类型,所以存的时候,无法判断是否要存入的数据的类型与容器种的类型一致,所以会拒绝set操作。注意:取出来的是实际的类型,例如上面指向的是GenericHolder<Fruit>,那么取出的就是Fruit,如果指向的是GenericHolder<Apple>,那么取出的就是Apple。
2.下界<? super T>往外取只能赋值给Object变量,不影响往里存
因为编译器只知道它是Fruit或者它的父类,这样实际上是放松了类型限制,Fruit的父类一直到Object类型的对象都可以往里存,但是取的时候,就只能当成Object对象使用了。
3.如果既要存又要取,那么就不要使用任何通配符
所以如果需要经常往外读,则使用<? extends T>,如果需要经常往里存,则使用<? super T>。
如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}
这种就我看来,应该是相当于:
public class Collections {
public static <T> void copy(List<T> dest, List<T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}