一、什么是泛型?
泛型,即“参数化类型”。
比如定义一个变量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());
输出结果:
class java.lang.Integer
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。
泛型方法,可以通过传入的实参,判断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; } }
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); //调用一下吃水果的方法 eatFruit(fruitHolder); //放苹果的标签,自然只能放苹果 appHolder.setObj(apple); // eatFruit(appHolder);//报错,这时候无法把appHolder传入eatFruit,因为GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型 } 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); } public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){ System.out.println("我正在吃 " + fruitHolder.getObj().getName());
Fruit fruit1 = new Fruit("水果1");
//fruitHolder.setObj(fruit1);//报错,不能存入新的内容。Error:(35, 28) java: 不兼容的类型: Fruit无法转换为capture#1, 共 ? extends Fruit } }
运行结果:
我正在吃 水果
我正在吃 苹果
这就是泛型的边界符,用<? extends Fruit>的形式表示。边界符的意思,自然就是定义一个边界,这里用?表示传入的泛型类型不是固定类型,而是符合规则范围的所有类型,用extends关键字定义了一个上边界。
有上边界,自然有下边界,泛型里使用形如<? super Fruit>的方式使用下边界。
这两种方式基本上解决了我们之前的问题,但是同时,也有一定的限制(PECS原则)。
1.上界<? extends T>不能往里存,只能往外取
不要太疑惑,其实很好理解,因为编译器只知道容器里的是Fruit或者Fruit的子类,但不知道它具体是什么类型,所以存的时候,无法判断是否要存入的数据的类型与容器种的类型一致,所以会拒绝set操作。
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)); } }
参考网址:
http://www.importnew.com/24029.html
https://blog.csdn.net/sunxianghuang/article/details/51982979