文章目录
一、简介
泛型,即“参数化类型”。换句话说,我们将原先具体的类型变成参数,类似方法中的参数,在使用/调用时传入具体的类型。如果我们在一个包含泛型的类或方法上生成Java文档注释,泛型类型将作为一种参数注释(@param)生成。
综上所述,泛型类型也是一种类型,它可以被用在类(泛型类)、接口(泛型接口)和方法(泛型方法)中。
一些常见的泛型类型标识名称及含义如下:
变量标识 | 含义 |
---|---|
E | 元素(Element) |
T | 类型(Type) |
N | 数字(Number) |
K | 关键字(Key) |
V | 值(Value) |
? | 泛型通配符 |
二、泛型示例
泛型概念比较抽象,所以我们先上一个小栗子。在日常开发中最常见的泛型应用就是集合,泛型的使用将避免强制类型转换带来的类型不安全,因为编译器会在编译而非运行时检查类型。
List<String> stringList = new ArrayList<>();
stringList.add("1");
// stringList.add(2); 编译错误,因我们在声明集合时使用泛型约定集合中只能存在String类型数据。
三、泛型擦除
虽然使用泛型能够避免强制类型转换带来的类型不安全,但泛型只在编译前是有效的,在编译后的class文件中是没有泛型的,未知的泛型都将被当作Object处理。一个简单的例子可以帮我们验证这一点:
List<Integer> integerList = new ArrayList<>();
List<String> stringList = new ArrayList<>();
System.out.println(integerList.getClass() == stringList.getClass()); // true
System.out.println(integerList.getClass()); // class java.util.ArrayList
System.out.println(stringList.getClass()); // class java.util.ArrayList
这个特性称为泛型擦除,主要是为了兼容JDK1.5之前的代码。正因为泛型只在编译前有效,所以我们可以通过反射越过泛型检查,感兴趣的同学请出门左转:Java反射机制
四、泛型类
/**
* 泛型类
*
* @param <T>
*/
public class GenericTest<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
GenericTest<String> stringGenericTest = new GenericTest<>();
// stringGenericTest.setData(1); 编译错误
stringGenericTest.setData("GenericTest<String>");
System.out.println(stringGenericTest.getData()); // GenericTest<String>
GenericTest genericTest = new GenericTest();
genericTest.setData(1);
System.out.println(genericTest.getData()); // 1
genericTest.setData("GenericTest");
System.out.println(genericTest.getData()); // GenericTest
}
}
观察泛型类示例,该泛型类接受一个泛型类型参数T(T可以由任何类型标识代替),在实例化时指定泛型类型。同时,泛型类包含一个泛型类型的成员变量,它的具体类型由泛型类的泛型类型指定。
在实例化时当然可以不指定泛型类型,此时泛型类中的泛型类型在类型擦除后可以为任意类型(Object)。
五、泛型接口
(一)定义一个泛型接口,包含一个泛型类型形参的方法。
public interface IGenericTest<T> {
void print(T data);
}
(二)泛型接口的两种实现方式
1、指定泛型接口的泛型类型,此时实现类仍可以拥有自己的泛型类型。例如上文泛型类可实现本泛型接口并指定泛型类型为Integer。
/**
* 泛型类
*
* @param <T>
*/
public class GenericTest<T> implements IGenericTest<Integer> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
/**
* 实现泛型接口方法
*
* @param data
*/
@Override
public void print(Integer data) {
System.out.println(data);
}
public static void main(String[] args) {
GenericTest<String> stringGenericTest = new GenericTest<>();
// stringGenericTest.setData(1); 编译错误
stringGenericTest.setData("GenericTest<String>");
System.out.println(stringGenericTest.getData()); // GenericTest<String>
stringGenericTest.print(1); // 1
}
}
2、不指定泛型接口的泛型类型,而通过实现类实例化时指定,此时泛型接口泛型类型与实现类泛型类型相同。
/**
* 泛型类
*
* @param <T>
*/
public class GenericTest<T> implements IGenericTest<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
/**
* 实现泛型接口方法
*
* @param data
*/
@Override
public void print(T data) {
System.out.println(data);
}
public static void main(String[] args) {
GenericTest<String> stringGenericTest = new GenericTest<>();
// stringGenericTest.setData(1); 编译错误
stringGenericTest.setData("GenericTest<String>");
System.out.println(stringGenericTest.getData()); // GenericTest<String>
// stringGenericTest.print(1); 编译错误
stringGenericTest.print("IGenericTest<String>.print()"); // IGenericTest<String>.print()
}
}
六、泛型方法
大多数泛型类中都拥有使用了泛型的方法,但使用泛型的方法不一定就是泛型方法,这对初学者来说非常容易混淆。
通过上面的介绍我们知道泛型类是在类实例化时指定泛型类型,以此类推,泛型方法也是在使用/调用时才指定泛型类型。观察泛型类的示例,对于成员变量data的getter/setter方法都使用了泛型,但此时的泛型类型T与泛型类泛型相同。也就是说,在实例化泛型类时就已经指定了getter/setter方法的泛型类型,所以它们不属于泛型方法。
那什么是真正的泛型方法呢?下面我们看一个最简单的例子:
public <T> T genericMethod(T t) {
return t;
}
1、<T>声明了该方法是一个泛型方法,此时在方法中才可以使用泛型T,当然此处的T也可以由任何类型标识代替。
2、紧随<T>的T说明该方法返回一个T类型返回值。
3、参数T t说明该方法接受一个T类型形参t
/**
* 泛型类
*
* @param <T>
*/
public class GenericTest<T> implements IGenericTest<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
/**
* 实现泛型接口方法
*
* @param data
*/
@Override
public void print(T data) {
System.out.println(data);
}
/**
* 泛型方法
*
* @param t
* @param <T> 此时泛型类型T与泛型类T类型无关,它们可以是同一个类型也可以不同
* @return
*/
public <T> T genericMethod(T t) {
return t;
}
public static void main(String[] args) {
GenericTest<String> stringGenericTest = new GenericTest<>();
// 泛型方法的泛型类型只在使用/调用时指定,与泛型类泛型类型无关
System.out.println(stringGenericTest.genericMethod("GenericTest<String>.genericMethod(String)")); // GenericTest<String>.genericMethod(String)
System.out.println(stringGenericTest.genericMethod(1)); // 1
}
}
七、泛型通配符
使用具有继承关系泛型类型的泛型类之间没有继承关系。例如Integer是Number的子类,但是List<Integer>和List<Number>没有任何关系。
private static void allowNumber(Number number) {
System.out.println(number);
}
private static void allowGenericNumber(List<Number> numberList) {
System.out.println(numberList);
}
public static void main(String[] args) {
Number number = 128;
Integer integer = 128;
allowNumber(number);
allowNumber(integer);
List<Number> numberList = new ArrayList<Number>() {{
add(128);
}};
List<Integer> integerList = new ArrayList<Integer>() {{
add(128);
}};
allowGenericNumber(numberList); // [128]
// allowGenericNumber(integerList); 编译错误
}
那我们难道要还需要写一个allowGenericInteger(List<Integer>)方法处理List<Integer>么?答案当然不用,显然这违背了Java的多态思想,因此类型通配符?应运而生。类型通配符和其他类型标识符一样也是一种类型,只不过它可以代表任意类型,所以我们可以将它看作是所有类型的父类型(类似Object)。我们使用类型通配符修改上面的代码,完美通过!
private static void allowNumber(Number number) {
System.out.println(number);
}
private static void allowGenericNumber(List<?> numberList) {
System.out.println(numberList);
}
public static void main(String[] args) {
Number number = 128;
Integer integer = 128;
allowNumber(number);
allowNumber(integer);
List<Number> numberList = new ArrayList<Number>() {{
add(128);
}};
List<Integer> integerList = new ArrayList<Integer>() {{
add(128);
}};
allowGenericNumber(numberList); // [128]
allowGenericNumber(integerList); // [128]
}
八、泛型的限定(上下边界)
在使用泛型时,我们可以指定泛型类型的上下边界,例如我们可以指定泛型类型只能是某类型的父类型或者子类型。
(一)泛型类型上界<? extends Parent>
为泛型类型指定上界即传入的泛型类型只能是指定类型的子类型。
List<? extends Number> numbers = new ArrayList<Number>() {{
add(1);
add(2.0);
}};
System.out.println(numbers); // [1, 2.0]
(二)泛型类型下界<? super Child>
与上界相反,为泛型类型指定下界即传入的泛型类型只能是指定类型的父类型。
(三)无界泛型<?>
无界类型就是类型通配符<?>,它可以代表任意类型。
(四)PECS原则
使用泛型上下界虽然使得类型转换更加容易,但同时也会让泛型类容器损失部分功能。以泛型上界为例,当我们指定了泛型上界,对编译器来说只知道容器内是上界类型和其子类型。此时,由于不能确定新元素的具体类型,但容器内元素一定可以通过上界类型接收,所以容器只能读取但不能新增。下界的情况正好与之相反。
所以我们提出PECS(Producer Extends Consumer Super)原则来衡量我们如何合理使用上下界:当我们需要频繁读取容器时使用上界,反之如果我们需要频繁新增的使用下界。
九、泛型约束
1、基本类型(Eg:int)不能作为泛型类型,但包装类(Eg:Integer)可以。
2、泛型类不能继承Exception或者Throwable。
// private class MyGenericException<T> extends Exception{} 编译错误,Generic class may not extend 'java.lang.Throwable'
// private class MyGenericException<T> extends Throwable{} 编译错误,Generic class may not extend 'java.lang.Throwable'
3、静态变量或静态方法均不能引用泛型类型,但静态泛型方法可以。
/**
* 泛型方法
*
* @param e
* @param <E>
* @return
*/
public <E> E genericMethod(E e) {
return e;
}
// public static T staticField; 编译错误,静态变量不能引用泛型类型
// public static void staticMethod(T a) { 编译错误,静态方法不能引用泛型类型
// System.out.println(a);
// }
/**
* 静态泛型方法
*
* @param e
* @param <E>
* @return
*/
public <E> E staticGenericMethod(E e) {
return e;
}
4、不能使用instanceof关键字或==判断泛型类的泛型类型。
public class GenericTypeTest<T> {
public static void main(String[] args) {
GenericTypeTest<Integer> integerGenericTypeTest = new GenericTypeTest<>();
GenericTypeTest<String> stringGenericTypeTest = new GenericTypeTest<>();
System.out.println(integerGenericTypeTest instanceof GenericTypeTest); // true
// System.out.println(integerGenericTypeTest instanceof GenericTypeTest<Integer>); 编译错误,不能使用instanceof关键字判断泛型类的泛型类型
// System.out.println(integerGenericTypeTest == stringGenericTypeTest); 编译错误,不能使用==判断泛型类的泛型类型
}
}
5、由于泛型擦除机制,所以不能创建确定泛型类型的泛型数组,除非使用通配符,因为对于通配符来说,数组元素均需进行显示类型转换。
// GenericTest<String>[] ls = new GenericTest<String>[10];
GenericTest<String>[] genericTests = new GenericTest[10];
GenericTest<?>[] genericTests1 = new GenericTest<?>[10];
在日常开发中,泛型被经常用在各种开发模式中。同时泛型和反射的配合可以为代码提供更高的灵活性。对反射还不熟悉的同学请出门左转:Java反射机制