泛型的概念
泛型,也就是把类型参数化。比如定义一个方法,他需要对几种不同的数据类型做相同的处理,如果不用泛型,那我们就需要定义好几个方法,每个方法对应每种数据类型。这几个方法只是参数类型不同,方法体都是一样的,那么就显得很冗余。
拿比较常用的 List
举例,假设他不支持泛型,那我们就需要一个 StringList
来存放字符串,一个 IntegerList
来存放整型,一个 DoubleList
来存放双精度浮点型等等。另外,对于我们自己定义的类,也需要再定义很多 List
来分别存放他们。而这些 List
提供的功能都是一样的:添加数据、读取数据。
List
里面可以存放任意类型的数据,这里我们创建一个 List
,并添加一个字符串和一个整型数据。
List myList = new ArrayList();
myList.add("hello");
myList.add(23);
在需要从 myList
中读取数据时,对于不同的数据类型,就要做对应的类型转换了。
String strElem = (String) myList.get(0);
Integer intElem = (Integer) myList.get(1);
显然,我们需要记住数组中每个元素是什么类型的,如果在读取数据时搞乱了,比如第 0 个元素是字符串,我们却记成了整型
Integer intElem = (Integer) myList.get(0);
自然就会抛出一个类型转换的错误,导致程序异常。泛型的出现很好地解决了这个问题。
List<String> myStrList = new ArrayList<>();
List<Integer> myIntList = new ArrayList<>();
myStrList.add("hello");
myIntList.add(23);
这样就规定了 myStrList
中只能存放字符串类型,myIntList
中只能存放整型。如果你在 myStrList
中添加了一个非字符串的类型,是不会编译通过的。
类型擦除
泛型信息只存在于编译阶段,编译完成后,所有的泛型信息都会被擦除,也就是说在 JVM 执行代码阶段,是不存在泛型这个概念的,这就是类型擦除。
System.out.println(myStrList.getClass() == myIntList.getClass());
System.out.println(myStrList.getClass());
执行这段代码,会输出 true
,表示在 JVM 中,myStrList
和 myIntList
的 Class
是同一个,并且在输出的第二行打印出了 class java.util.ArrayList
,这里并不包含泛型信息。
既然泛型信息被擦除了,那么 JVM 在执行阶段是怎么确定不同的 List
中存放的是什么数据类型呢?
我们定义一个泛型类,并通过反射看一下其中的泛型在编译后是什么样的
class MyGeneric<T> {
T object;
public MyGeneric(T object) {
this.object = object;
}
public static void main(String[] args) {
MyGeneric<String> myGeneric = new MyGeneric<>("hello");
Class myClz = myGeneric.getClass();
Field[] fields = myClz.getDeclaredFields();
for (Field f : fields) {
System.out.println("field: " + f.getName() + ", type: " + f.getType().getName());
}
}
}
输出为:
field: object, type: java.lang.Object
可以看到,其中的泛型类型变成了 Object
。我们改一下 MyGeneric
的泛型部分
class MyGeneric<T extends String> {
// ...省略
}
再次编译运行,输出为:
field: object, type: java.lang.String
这样我们就可以确定,在对泛型做类型擦除时,如果没有指定类型的上限,那么这个类型会被编译为 Object
类型;如果指定了上限,那么他会被编译为这个上限的类型。
既然在编译后类型会被擦除,那么我们就可以通过反射来绕过在泛型中的类型限制。比如定义了一个 List<Integer>
类型,就可以通过反射调用他的 add()
方法向其中添加非 Integer
类型的数据。
还有一个问题:
List<Integer>[] myList = new ArrayList<>[];
这里 myList
是一个数组,他的每个元素是 List<Integer>
类型的。由于会有类型擦除,导致程序无法分辨元素的类型,这行代码是无法编译通过的。
泛型的使用
泛型有三种使用方式:泛型类,泛型方法,泛型接口。
泛型类
我们来看泛型类的写法:
class 类名 <泛型标识> {
private 泛型标识 变量;
public 类名(泛型标识 形参) {
}
public void 方法名(泛型标识 形参) {
}
}
其中泛型标识可以是一个,比如 <T>
,也可以是多个,比如 <T, E>
。上文的 MyGeneric
就是一个泛型类,再比如 HashMap
使用了两个泛型标识,第一个指定键的类型,第二个指定值的类型,用法 new HashMap<String, Integer>
。
实例化泛型类:
泛型类<具体类型> 变量名 = new 泛型类<>(具体类型 参数);
第二个 <>
中的具体类型是可以省略的,因为编译器从第一个 <>
中已经获得了具体类型。
当然,实例化泛型类时,也可以不用指定具体类型,比如我们在 List
中可以添加任意类型的数据。
泛型方法
public <泛型标识> void method1(泛型标识 参数) {}
public <泛型标识> 泛型标识 method2() {
// value 是泛型标识限定的类型
return value;
}
比如在上文的 MyGeneric
类中,添加一个获取 object
数据的方法:
public <T> T getObject() {
return object;
}
泛型方法与泛型类可以共存,也可以只定义其中一个。
泛型接口
泛型接口的定义跟泛型类的定义类似:
public interface 接口名<泛型标识> {
泛型标识 方法名();
}
实现泛型接口时,可以指定具体的泛型类型,也可以不指定:
public interface MyInterface<T> {
T method1();
}
// 不指定具体类型
class MyClassA implements MyInterface<T> {
@Override
T method1() {
}
}
// 指定具体类型
class MyClassB implements MyInterface<String> {
@Override
String method1() {
}
}
泛型通配符
常用的泛型通配符有:T, E, K, V, ?
其中:
- T (type) 表示具体的一个 Java 类型
- K, V (key, value) 表示键和值
- E (element) 表示元素
- ? 表示任意的类型
?
通配符可以指定类型范围,有三种形式:
<?>
任意类型<? extends T>
指定类型上限,表示类型 T 及其子类<? super T>
指定类型下限,表示类型 T 及其超类(父类)