1. 为什么要使用泛型?
设想你有很多数字要进行排序,你选择把数字先放到下面的集合中
ArrayList list = new ArrayList();
你当然可以往里面加数字,但你添加字符串编译时也不报错,可是list里面有字符串显然不是你想见到的。
list.add(“hello”);
JVM虚拟机在执行排序时当然无法把数字与字符串排序,会产生一个运行时错误(运行时才能发现),这使你无法完成对数字的排序。要是能在编译时就发现错误就好了,怎么实现呢?面对这个问题jdk1.5提出的泛型发挥了作用,如下创建集合list即可。限制了集合中只能存放Integer类型的数据,此时你添加字符串编译就会报错。
ArrayList list = new ArrayList<>();
可以说,使用java泛型的动机就是在编译时检测出错误。
\1. 下面来谈谈怎样使用泛型类、泛型接口、泛型方法。一般用表示泛型,T可以是广泛的任意类型的数据。
2. 泛型类
泛型类声明看起来像非泛型类声明,除了类名后跟一个类型参数部分。我觉得类型参数部分主要是为了给类中的变量和方法传递参数的。泛型类的类型参数部分可以有一个或多个用逗号分隔的类型参数,通常大写字母表示。 它表示通用类型,只有外界传入具体类型时才能确定。
public class Generate<E>{
private T name;
}
我理解的泛型类就是一种可接收参数的类,它可以进行实例化。比如上面的例子,你可以传入泛型参数得到Generate或者Generate。
我理解的是Generate是类,Generate是Generate的实例对象,所以Generate和Generate是不同的。但实际上这两者都是Generate这种类型。下面的代码可以验证上述事实
public class GeneTest {
public static void main(String[] args) {
Generate<Integer> integerGenerate = new Generate<>();
Generate<String> stringGenerate = new Generate<>();
Class gene1 = integerGenerate.getClass();
Class gene2 = stringGenerate.getClass();
System.out.println(gene1.equals(gene2));
}
}注意T绝对不是泛型类,它只是一个泛型参数。
3. 泛型接口
泛型接口和泛型类的关系,类似于类与接口的关系。
public interface Male<T,E>{
public T eat();
}
4. 泛型方法
泛型方法这一概念比较抽象,下面的get和构造方法不是泛型方法。只有标记了的方法才是泛型方法。
class Generate<T>{
private T name;
public Generate(){
}
//构造器参数由外部指定
public Generate(T name) {
this.name = name;
}
public T getName( ){ //泛型方法getName的返回值类型为T由外部指定
return this.name;
}
public <T> T method1(T t){
List<Integer> list = new ArrayList<>();
return t;
}
//泛型方法
public <K,P> P method2(K k,P p){
List<Integer> list = new ArrayList<>();
return p;
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show1(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show2(T t){
System.out.println(t.toString());
}
}
泛型方法中的方形标记与泛型类无关,我觉得方法中的标记<K, E>这些大写字母就代表了入参和出参的范围。它表示该方法可以接受任何类型的参数,并且可以返回任何类型的参数。非泛型类中也可以有泛型方法。
5. 泛型通配符
class Fruit{
public void call() {
System.out.println("这是一个水果");
}
}
class Banana extends Fruit{
@Override
public void call() {
System.out.println("这是一个香蕉");
}
}
class Apple extends Fruit{
@Override
public void call() {
System.out.println("这是一个苹果");
}
}
现在我定义一个“水果盘子”,逻辑上水果盘子当然可以装苹果。但是变量Plate不能指向Plate,尽管Apple是子类。因为plate从表面看也可以装Banana,这与它实际指向的类型不一致,下面的代码会报编译错误。
Plate<Fruit> plate = new Plate<Apple>();
<? extends T>上界通配符
上面的问题可以用泛型通配符来解决。使用extends的变量fruits1 可以指向任意类型参数为T子类的泛型类。
Plate<? extends Fruit> fruits1 = new Plate<Apple>();
当你指定了一个Plate< ? extends Fruit>,add的参数也变成了“? extends Fruit”。因此编译器并不能了解这里到底需要哪种Fruit的子类型,因此他不会接受任何类型的Fruit。因此add无法执行,get可以执行。
<? super T>下届通配符
使用super的变量可以指向任意类型参数为T的父类的泛型类,?代表T的父类,不符合这一情况的会编译失败。
Plate<? super Apple> list = new Plate<Apple>();
Plate<? super Apple> list2 = new Plate<Fruit>();
// Plate<? super Apple> list3 = new Plate<Banana>(); //编译失败
此处list可以添加元素,但是只能加Apple的子类。正是因为?代表Apple的父类,但是编译器不知道你要添加哪种Apple的父类,因此不能安全地添加。
list.setItem(new Apple()); //成功list.setItem(new Banana()); //编译失败
对于super,get返回的是Object,因为编译器不能确定列表中的是Apple的哪个子类,所以只能返回Object。
PECS原则
如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符。