什么是泛型?
Java泛型(generics)是JDK5中引入的一个新特性。泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数类型,也就是说所操作的数据类型被指定为一个参数。泛型不存在于JVM虚拟机中。
泛型的基本常识
类型参数和类型变量
Generic<T> 中的T被称为类型参数
Generic<String> 中的String被称为类型变量
常用的泛型
常用的泛型有 T, K, V, E 等,分别对应Type,Key,Value,Element。关于使用哪个字母没有明确的规定,只不过这是一种约定俗成的用法。定义泛型的时候尽量能让使用者知道大概的意思。
原始类型
缺少实际类型变量的泛型就是一个原始类型。例如:
class Generic<T> { }
如果在使用的过程中 Generic generic = new Generic() 不用泛型类型来使用这个类,那么这个Generic 就是 Generic<T> 的原始类型。
使用泛型的好处
1 安全性
本来虚拟机会在运行的时候进行类型检测,如果类型不对,则会抛出 ClassCastException 。引入了泛型机制之后,就可以在编译期间发现错误,相当于把发现错误的时间提前了。
List list = new ArrayList();
list.add("张无忌");
list.add(1);
for (int i = 0; i < list.size(); i++) {
String str = (String) list.get(i);
System.out.println(str);
}

上述代码在编译期是没有问题的,但当真正运行加载到内存的时候就会报类转换异常。
如果我们加上泛型之后,在代码编写的时候编译器就会提示我们类型错误:

从而就可以避免运行期抛出异常,是我们的程序更加安全。
2 复用性
假如我们现在有一个需求是需要打印一个泛型类型为String的集合的所有元素,那么我们可以这样定义:
public void printStringList(List<String> list) {
if (list == null || list.isEmpty()) return;
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
如果以后需求变了,需要打印的泛型类型为 Integer,甚至是我们自己定义的类,那我们岂不是要写很多逻辑类似的代码。所以为了避免我们做这些无用功,我们可以借助泛型来实现一个模版方法来增加我们代码的复用性:
public <T> void printListGeneric(List<T> list) {
if (list == null || list.isEmpty()) return;
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
Java中的泛型
1 泛型类
public class Box<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public void print() {
System.out.println("这个箱子装的是:" + data.toString());
}
}
下面是泛型类的一个简单使用:
public class Fruit {
String name;
public Fruit(String name) {
this.name = name;
}
@NonNull
@Override
public String toString() {
return name;
}
}
public class Apple extends Fruit{
public Apple(String name) {
super(name);
}
}
public class Orange extends Fruit{
public Orange(String name) {
super(name);
}
}
Box<Apple> appleBox = new Box<>();
appleBox.setData(new Apple("苹果"));
appleBox.print();
Box<Orange> orangeBox = new Box<>();
orangeBox.setData(new Orange("橙子"));
orangeBox.print();

2 泛型接口
interface Generator <T>{
T next();
}
泛型接口的实现有两种方式:
// 泛型接口指明具体类型
public class Mask implements Generator<String> {
@Override
public String next() {
return null;
}
}
// 泛型类实现泛型接口的类型参数
public class Cap<T> implements Generator<T> {
@Override
public T next() {
return null;
}
}
3 泛型方法
public <T> void genericMethod(T t) {
}
只有在修饰符和返回值之间有 <> 的才是泛型方法。不管是上面泛型类还是泛型接口中的任何带有泛型类型参数的方法(简而言之就是带参数中带有泛型 T )都不是泛型方法。泛型方法只有这一种定义方式。
public class GenericMethod<T> {
public static class Fruit { }
public static class Apple extends Fruit { }
public static class Person { }
public void show(T t) { }
public <T> void show2(T t) { }
}

show()方法就是一个普通的方法,它只不过使用了泛型类中的类型参数,所以它要受到在创建对象时泛型的约束。而show2()是一个标准的泛型方法。它的 T 和 泛型类当中的 T 没有任何关系。因此不受约束。
限定类型变量
public static <T> T max (T a,T b){
if (a.compareTo(b)>0)return a ; else return b;
}
上面这段代码在编译器上是不通过的。这是因为 T 是一种未知的类型。而想要使用compareTo()方法则必须实现了 Comparable 接口。反过来说,只有实现了 Comparable 接口的类才能使用compareTo() 来进行比较。
public static <T extends Comparable> T max (T a,T b){
if (a.compareTo(b)>0)return a ; else return b;
}
extends表示继承,派生。用在泛型中后面可以跟接口,也可以使用类。表示泛型 T 必须是它们的子类或实现类。上面代码中的T extends Comparable 就限定了传入的类型变量则必须实现 Comparable 接口。同时还可以进行多重限定:
public static <T extends Comparable & Serializable> T max (T a,T b){
if (a.compareTo(b)>0)return a ; else return b;
}
上面的代码则表示传入的 T 要同时实现Comparable 和 Serializable接口才能被使用。现在假设再限定 T 必须是 A 类的子类该怎么办呢?根据上面的例子使用 & 符号连接起来就可以了。其实不然。如果extends 后面的限定参数是类类型的话,那么类必须放在第一个的位置:
public static <T extends A & Comparable & Serializable> T max (T a,T b){
if (a.compareTo(b)>0)return a ; else return b;
}
其实这是符合我们Java代码的习惯的。我们写的类也只能是先继承某个类,然后再实现其他的接口。另外类类型只能使用一个,这是因为Java是单继承的。
泛型的约束和局限性


通配符
extends 为泛型添加上边界,即传入的类型实参必须是指定类型及子类型
首先定义四个普通的类:
public class Animal{ }
public class Cat extends Animal{ }
public class Dog extends Animal{ }
public class SpottedDog extends Dog{ }
两个测试方法:
// Box 是上面介绍泛型类时定义的
public void print(Box<Animal> box){ }
// 使用了通配符 ? 通配符通常用在方法上
public void print2(Box<? extends Animal> box){ }

根据使用情况来看,虽然 Dog 和 Cat 在继承关系上都属于 Animal 的子类,但是Box<Dog> 和 Box< Cat> 与Box<Animal> 是没有任何继承关系的,只不过它们都是同一个泛型类罢了。

可以看到,这四个对象都可以使用。这里使用通配符 ?extends 限制了传入的泛型参数只能是Animal以及Animal的子类。假如说Animal有一个父类A,那么Box<A>是不能传入print2()的。
那么下面再看一个例子:

可以看到在setData()的时候是不可以的,而getData()的时候却只能获得父类 Animal,子类是不行的。因为在setData()的时候,编译器只知道传入的数据是Animal的子类,但是到底是哪一个子类编译器是不清楚的,所以setData()不允许使用。而getData()的时候不管之前里面有什么样的数据,但是它们的父类都是Animal,都是可以向上转型的,这是安全的,所以被允许。
总结一下就是extends设定了上边界,可被用于安全的访问数据。
super 为泛型添加下边界,即传入的类型实参必须是指定类型的类型及父类型
添加一个新的测试的方法:
public void print3(Box<? super Dog> box){ }

根据使用结果来看使用了 super 之后,只能传入Dog以及它的父类Animal。也就是说 super 限定了传入参数的下边界。

可以看到在setData()的时候Dog以及他的子类SpottedDog可以被使用,平级的Cat不行是很正常的,因为它不是Dog的超类,那Animal为什么也不行呢?我们知道Object是所有类的父类,编译器无法知道确切的是哪一个父类。从下面的getData()便可以证明这一点。只有Dog以及它的子类能安全的转型为Dog,故可以被set进去。
总结一下就是 super限定了传入参数的下界,用于安全的设置数据。
网友评论