java 泛型
java之泛型
1.概述
泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。
直接来看一个简单的泛型类吧。
public class People<T> {
T first;
public T getFirst() {
return first;
}
public People(T first) {
this.first = first;
}
}
在这个泛型类中,与一个普通的java类的区别在:
- 类名后多了
; - 变量first的类型是T。
这里这个T就是类型参数,T可以是Java的8种基本数据类型,也可以是自己定义的类。这里使用类型参数T,可以使得People这个类与它所能操作的数据类型不在绑定在一起,这个类可以用于多种数据类型,实现了代码的复用,降低了耦合。
关于泛型类的使用大家可以自行学习,这个比较简单,网上也有很多资料,这里就不再赘述了。
2.泛型的基本原理
泛型是java 5引入的一个新特性,那么在java 5以前,java程序员是怎么表示可变类型的呢?答案是使用Object类。
public class People {
Object first;
public Object getFirst() {
return first;
}
public People(Object first) {
this.first = first;
}
}
这就是java 5以前java程序员表示可变类型的方法,同时,这也是java泛型内部的实现原理。
People people = new People(5);
Integer first = (Integer) people.getFirst();
//java5以前这样使用可变类型的类
对于泛型类,java编译器会将泛型代码转换为普通的非泛型代码,就像上面的代码这样,将类型参数T擦除,转换为Object,插入必要的类型转换。这个过程称为类型擦除。原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换T。
也就是说,java泛型是通过类型擦除实现的,类定义的类型参数T最后都会被替换为Object,在程序运行过程中,不知道泛型的实际类型参数。这么设计是为了兼容java 5以前的代码,因此java的泛型会有一些限制,这在后面再说。
3.泛型的好处
泛型的好处除了前面提到的代码重用,还有就是拥有很好的安全性,以及很好的可读性。
People people = new People("people");
int first = (Integer) people.getFirst();
如上,使用Object时,java编译器对于这样的类型转换的错误是检测不到的,类型转换错误只能在程序运行时才会被抛出。
但使用泛型后,一旦出现上面的类型转换错误后,一些开发环境就可以检测出这个错误,进而提示类型错误。或者在编译时,编译器也会提示。这称之为类型安全。除此之外,还可以省去强制类型转换,再加上明确的类型信息,使代码的可读性更好。
- 泛型方法:类可以是泛型的,那么方法也可以是泛型的,并且泛型方法与所在的类是否是泛型类无关。
public static <T> T indexOf(T[] arr){
return arr[arr.length/2];
}
类型变量放在修饰符后面,在返回类型前面。
- 泛型接口:接口也可以是泛型的,java里面的Comparable和Comparator接口都是泛型的。
public interface Comparable<T> {
public int compareTo(T o);
}
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
实现泛型接口时应该指定具体的类型。例如:
public final class Integer extends Number implements Comparable<Integer> {
//这表明Integer只能与Integer对象比较
}
4.类型参数的限定
java 支持限定类型参数的上限,也就是说参数必须是给定的上界类型或其子类型。上界的限定通过extends关键字来表示,这个上界可以是某个具体的类,或某个具体的接口,或其他的类型参数。
限定类型后,如果类型使用错误 ,编译器会提示。指定边界后,类型擦除时就不会转化为Object了,而是会转化为他的边界类型。
public class People<T extends Comparable<T>> //可以解读为T必须实现了Comparable接口
//并且可以与相同类型的元素进行比较
在java泛型中,无论T与S有什么关系,通常,People< T>与People< S>是没有联系的。因此不能因为T是S的子类,就将People< T>的对象赋值给People< S>的变量。
5.通配符
1.举个例子,Integer是Number的子类,现在要让People
public static void addAll(People<? extends Number> p)//<?extends T>这种形式称为有限定通配符
我们先来解释一下这个称为有限定的通配符吧,
用来实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体的类型是未知的(用?来表示),只知道这个具体的类型是E或者E的某个子类型。
在上面的这行代码,可以解释为:任何泛型People类型,它的类型参数是Number的子类,如People
我们来看看怎么使用通配符解决之前的问题。
People<Integer> intPeople = new People<Integer>();
People<? extends Number> numberPeople = intPeople;//这里是不会报错的
2.除此之外,还有无限定通配符<?>。先来看看它的写法,
public void swap(People<?> p ,int i,int j)//使用通配符的
public <T> void swap(People<T> p ,int i,int j)//普通的类型参数
通配符形式更为简洁,但需要注意的是,通配符不是类型变量,不能作为类型来使用,通俗来讲就是只能读,不能写。
People<Integer> intPeople = new People<Integer>();
People<? extends Number> numberPeople = intPeople;
Integer a = 200;
numberPeople.setFirst(a); //错误!
numberPeople.setFirst((Number) a); //错误!
numberPeople.setFirst((Object) a); //错误!
? extends Number 表示是Number的某个子类型,但不知道具体的类型,如果允许写入,java就无法保证类型安全性,所以干脆禁止。
大部分情况下,这种限制是好的,但这又会出现一些问题。
public void swap(People<?> p, int i, int j) {//交换索引为i,j的元素的位置
Object tem = p.get(i);
p.set(i, p.get(j));
p.set(j, tem);
}
这个代码看上去是正确的,但java编译器会报错,两行set语句都是错误的,可以借助带类型参数的泛型方法,来解决上面的这个问题。
private <T> void swapInternal(People<T> p, int i, int j) {
T tem = p.get(i);
p.set(i, p.get(j));
p.set(j, tem);
}
public void swap(People<?> p, int i, int j) {
swapInternal(p, i, j);
}
上面的这种写法在java容器类中就有类似的,公共的api是通配符形式,形式更简单,但内部调用带类型参数的方法。
3.java 还提供了一种通配符的限定<? super T> ,称为--超类型通配符,这个刚好跟<? extends T>相反。
- 表示该通配符所代表的类型是T类型及其子类。
-
表示该通配符所代表的类型是T类型及其超类。
具体的就不在这里介绍了,大家可以自行了解。
直观地讲,带有超类型限定的通配符<? super T>可以向泛型对象写入,带有子类型限定的通配符<? extends T>可以从泛型对象读取。
需要注意的是,对于<? super T>在java源码中有一种常见的应用。我们先来看一段代码。
public final class LocalDate implements ChronoLocalDate {
}
//这里我只截取java源码中与本节有关的代码
public interface ChronoLocalDate extends Comparable<ChronoLocalDate> {
}
从上面的这段代码中,可以看到LocalDate实现了ChronoLocalDate,而ChronoLocalDate扩展了Comparable
因此假设如果我们要处理类似前面这种继承关系的类的话,比如Father类实现了Comparable
那么如果我们要实现一个泛型方法来找出List中最小的Son对象,我们可以这样定义。
public static <T extends Comparable<? super T>> T min(List<T> list) {
//比较方法的逻辑
}
这意味着T可以实现Comparable<? super T>,而不仅仅是Comparable
假设类型参数是Son 的话,那声明就会是这样的<Son extends Comparable<? super Son>>,它意味着一个Son类可以实现Comparable
4.最后总结一下。
- 和 一般用于读取,使得方法可以读取T和T的任意子类型的容器对象。
- 用于写入和比较,使得对象可以写入父类型的容器;使得子类型的对象可以应用父类型的比较方法。
6.局限性
java泛型是通过类型擦除实现的,类型参数在编译的时候会被Object替换掉。这就使得Java泛型存在了一定的局限性和限制 。
1.使用泛型类、方法和接口
在使用泛型类、方法和接口时,需要注意的几点是:
- 基本类型不能用于实例化类型参数
- 运行时类型信息不适用于泛型
- 类型擦除可能会引发一些冲突
类型信息是一个对象,他的类型为Class,Class本身就是一个泛型类,每个类的类型对象可以通过类名.class的方式引用,也可以通过对象.getClass()方法获得。
但是类型对象与泛型无关,所以java不支持类似People
People<Integer> integerPeople = new People<Integer>();
People<String> stringPeople = new People<String>();
if(integerPeople.getClass() == stringPeople.getClass())//这是TRUE的,
两次getClass()都返回People类型。
2.定义泛型类、方法和接口
在定义泛型类、方法和接口时,需要注意的几点是:
- 不能通过类型参数创建对象
- 泛型类的类型参数不能用于静态变量和方法
java不允许使用类型参数来创建对象,例如下面这样的方法都是错误的。
T a = new T();
T[] arr = new T[10]; //这两个方法都是错误的
也不允许使用类型参数在静态变量和静态方法中使用。
private static T a; //错误
public static T geta(); //错误