基本概念
一个简单的泛型类
public class Pair<T> {
T first;
T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
pair
就是一个泛型类,与普通的类的区别体现在一下两点:
- 类名后面多了一个
first
和second
的类型都是T
上面的T
就是类型参数,所谓的泛型其实就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入的。
比如此时使用Integer
来实例化Pair
,代码如下:
Pair<Integer> minmax = new Pair<Integer>(1, 100);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();
上面的Integer
就是传递的实际参数类型。
如果传入的是String
的话:
Pair<String> minmax = new Pair<String>("name", "张三");
传入的类型不同,处理的类型也就不一样,所以说Pair类的代码和它处理的数据类型不是绑定的,类型是可变的。
类型参数可以是多个
public class Pair<U, V> {
U first;
V second;
public Pair(U first, V second){
this.first = first;
this.second = second;
}
public U getFirst() {
return first;
}
public V getSecond() {
return second;
}
}
上面的例子中就是有不同的参数类型。
获取该类对象:
Pair<String,Integer> pair = new Pair<String,Integer>("字符串",100);
java7
开始,new
后面的类型可以不用写:
Pair<String,Integer> pair = new Pair<>("字符串",100);
实现原理
先看一下下面的代码
public class Pair {
Object first;
Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
上面的代码其实就是泛型的实现机制。将原理之前先要明白java
的两个知识点:
java
编译器
编译器的作用是将源代码转换为.class
文件,对于泛型类,java
编译器会将泛型代码转换为普通的非泛型代码(就像上面代码一样,其实就是替换为Object
,然后进行强制类型转换),这一步叫做擦除类型参数。java
虚拟机
虚拟机的的作用是加载并运行.class
文件。对于泛型类,在上面的编译器阶段就已经擦除了类型参数,所以在程序运行阶段是不知道泛型的实际类型参数的,比如说Pair<Integer>
,在程序运行时候只是知道Pair
,不知道Integer
的。
使用泛型的好处
既然泛型的实现原理就是使用Object
,那么为什么不直接使用Object
,而是要使用泛型呢,主要是有一下两个好处:
- 更好的安全性
使用Object
是需要进行强制转换到需要的类型的,但是使用Object
时在编译的时候如果转换错误也是不会提醒的,只要在运行的时候才会报ClassCastException
,但是使用泛型如果转换错误在编译阶段就会发现。 - 更好的可读性
泛型的类别
泛型类(容器类)
上面的那个例子就是一个泛型类,对于泛型类一般是用作容器类,比如ArrayList
类就是一个泛型类,同时也是一个泛型类。
泛型方法
除了泛型类还可以有泛型方法,而且一个方法是不是泛型的,与其所在的类是不是泛型没有关系。
一个参数的泛型方法
public static <T> int indexOf(T[] arr, T elm){
for(int i=0; i<arr.length; i++){
if(arr[i].equals(elm)){
return i;
}
}
return -1;
}
一个参数的泛型方法的调用
indexOf(new Integer[]{1,3,5}, 10);
//或者
indexOf(new String[]{"hello","测试1", "name")
多个参数的泛型方法
public static <U,V> Pair<U,V> makePair(U first, V second){
Pair<U,V> pair = new Pair<>(first, second);
return pair;
}
多个参数的泛型方法的调用
makePair(1,"名称");
注:与泛型类不同,调用方法时一般不需要特意指定类型参数的实际类型,java编译器可以自己推断出来。
泛型方法的格式
从上面泛型方法的案例可以看到泛型方法的格式是:
- 对于普通类型,类型参数放在返回值前面
- 对于自定义类型,除了在返回值前面加上类型参数,还需要在类后面也加上类型参数,比如上面的多个参数的方法那个案例。
泛型接口
接口定义时使用泛型
比如Comparable
接口就是泛型的。
public interface Comparable<T> {
public int compareTo(T o);
}
实现泛型接口需指定类型
接口的定义使用的是泛型,但是在实现接口时需要指定具体的类型。比如使用Integer
类实现上面的接口:
public final class Integer extends Number implements Comparable<Integer>{
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
}
类型参数的限定
在前面讲过泛型的本质是通过使用Object
,不过java还支持限定这个参数的上界。
上限为某一个类
最开始那个pair
类的代码:
public class Pair<T> {
T first;
T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
假设该类有一个子类。并且需要限定两个参数的类型为Number
public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
public NumberPair(U first, V second) {
super(first, second);
}
}
然后就可以直接使用限定的类型的方法,而且如果类型使用错误编译时就会提示。指定边界后,类型擦除时就不会转换为Object
而是转换为边界类型。
上界为某一个接口
以上界是Comparable
接口为例:
编译会有警告形式
public static <T extends Comparable>T max(T[] arr){
T max = arr[0];
for(int i=1; i < arr.length; i++){
if(arr[i].compareTo(max) > 0){
max = arr[i];
}
}
return max;
}
正确完整形式
public static <T extends Comparable<T>> T max(T[] arr){
//上面的主体代码
}
<T extends Comparable<T>>
这种形式称为递归类型限制。解释为:
T表示一种数据类型,必须实现Comparable
接口,并且必须与形同类型的元素进行比较。