泛型,可以理解为参数化类型。对于参数,我们比较熟悉的是形参和实参。方法定义时,用的是形参,在调用方法时,实际传的数值或对象叫实参。参数化类型,可以理解为,在定义类或者方法时,将用到的类型参数化(类型形参),不传具体的类型,只是在使用时,才将具体的类型传进来(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。比较常见的用法是容器类,例如list,set,map。
作用
当不使用泛型,可能会在运行时产生转换异常。
// 当不限定List中存储的类型,则任意类型对象都可以加入List中
List list = new ArrayList();
list.add("abc"); // 向list中加入String
list.add(111); // 向list中加入Integer,容器类中无法储存基本类型,int会装箱后存入
for (int i = 0; i < list.size(); i++) {
// 此处在i=1时,会抛出转换异常
// ClassCastException:java.lang.Integer cannot be cast to java.lang.String
String str = (String) list.get(i);
System.out.println(str);
}
当我们不限定ArrayList中存储的类型,那么任意类型的数据都可以存入。上例中,先存入一个String类型,后存入一个Integer类型,然后都以String类型使用,程序运行时抛出转换异常。
大部分情况下,我们都会将相同类型的数据放在一起,而不是简单地将所有数据放在一个list中,所以可以通过限定类型的方式来尽快发现这种问题,在编译期间就报错,而不是在运行时才抛出异常,所以需要引入泛型来限定类型。
List<String> list = new ArrayList();
list.add("abc");
//list.add(111); 编译器此处报错
如果容器中的数据类型不确定,那么在使用前,应该先进行类型判断再使用,从而避免运行时异常。
if (list.get(i) instanceof String){
String str = (String) list.get(i);
}
主要有三种使用泛型的方式,分别是泛型类、泛型接口、泛型方法。
泛型类
泛型类:类的定义中使用泛型。常见的就是各种容器类。
泛型写法
class 类名<泛型标识> {
}
泛型类的例子:
// 此处T可写为任意标识,常见T、E、K、V等大写字母;
// 在实例化泛型类时,必须明确指定T的类型
// 只有在此处声明过的泛型标识才能在类中使用
class Generic<T> {
// 成员变量field的类型是T,由实例化泛型类时确定
private T field;
// 报错,Cannot resolve symbol 'E'
// 泛型标识E没有被声明过,不能被使用
// private E value;
// 方法返回值的类型是T,由实例化泛型类时确定
public T getField() {
return field;
}
// 方法的形参field的类型是T,由实例化泛型类时确定
public void setField(T field) {
this.field = field;
}
}
普通的字符串也可以用作为一个标识,还可以用Integer\String等,都会被识别为泛型标识,但是不建议使用除大写字母之外的作为泛型标识,降低代码可读性。
实例化上述的泛型类:
Generic<Integer> genericInt = new Generic<>();
Generic<String> genericStr = new Generic<>();
genericInt.setField(11);
genericStr.setField("abc");
System.out.println("genericInt:"+genericInt.getField());
System.out.println("genericStr:"+genericStr.getField());
// 运行结果:
// genericInt:11
// genericStr:abc
若在实例化泛型类时,不指定确定的类型,那么此实例泛型类可以存入任何类型的对象,就失去了类型限制的作用。
Generic generic = new Generic();
generic.setField(22);
System.out.println("generic:" + generic.getField());
generic.setField("def");
System.out.println("generic:" + generic.getField());
// 运行结果:
// generic:22
// generic:def
需要注意:
- 在实例化类时,传入的类型不能是基本类型。
// 报错,Type argument cannot be of primitive type
Generic<int> ge = new Generic<int>();
- 不能对具体的泛型类使用instanceof
Generic<Integer> genericInt = new Generic<>();
// 报错,Illegal generic type for instanceof
// if (genericInt instanceof Generic<Integer>) {
// }
if (genericInt instanceof Generic) {
System.out.println("is Generic");
}
// 运行结果:
// is Generic
- 泛型类中可以有多个泛型标识
public class Main {
public static void main(String[] args) {
Generic<Integer, String> generic = new Generic<>();
generic.setField(33);
generic.setKey("aaa");
}
}
// T与K可以是不同类型,也可以是相同类型
class Generic<T, K>{
private T field;
private K key;
public void setField(T field) {
this.field = field;
}
public void setKey(K key){
key = key;
}
}
泛型接口
泛型接口的定义类似泛型类的格式。
interface Generic<T> {
T next();
}
实现泛型接口的类,可以传入具体类型,此时类就是一个普通的类;也可以不传入具体类型,那么这个类就是个泛型类。
传入具体类型:虽然只创建了一个泛型接口Generic<T>,但是可以为T传入各种具体类型,从而形成各种类型的Generic接口。在传入具体类型时,实现类中所有用到泛型的地方都需要替换为传入的具体类型。
class StudentGeneric implements Generic<String> {
private String[] students = new String[]{"zhangsan", "lisi", "wangwu"};
@Override
public String next() {
return students[new Random().nextInt(3)];
}
}
不传入具体类型:实现类与泛型类定义相同,在定义类时,需要将泛型声明一起加入。
class TeacherGeneric<T> implements Generic<T> {
@Override
public T next() {
return null;
}
}
// 不在实现类中声明泛型标识会报错,Cannot resolve symbol 'T'
//class TeacherGeneric implements Generic<T> {
// @Override
// public T next() {
// return null;
// }
//}
泛型方法
相比于泛型类,泛型方法稍微复杂一些。
泛型类在实例化时,需要指定具体类型;在调用泛型方法时,需要指定泛型方法的具体类型。
泛型方法的格式如下:
访问修饰符 <泛型标识> 返回值 泛型方法名(泛型标识 形参)
普通类中的泛型方法
class Generic {
public <T> T showKey(T key){
return key;
}
}
public static void main(String[] args) {
Generic generic = new Generic();
// 与普通方法区别不大,只是参数类型变为泛型
System.out.println(generic.show(111));
System.out.println(generic.show("abc"));
}
泛型方法需要注意以下几点:
- 泛型方法可以声明在普通类中
- 此泛型方法返回值为T
- public与返回值T之间的<T>必须存在,此方法才会被声明为泛型方法
- <T>表示此方法声明了泛型类型T,方法中才可以使用T
泛型类中的泛型方法
在前面的泛型类例子中,用到泛型标识的方法并不是泛型方法。
class Generic<T>{
private T field;
// 此方法虽然将泛型标识T作为返回值,但只是一个使用了泛型标识的普通方法,不是泛型方法
// 泛型标识在泛型类中已经声明过,所以可以使用
public T getField() {
return field;
}
// 此方法也不是泛型方法
public void setField(T field) {
this.field = field;
}
// 泛型类中声明泛型方法,泛型标识E可以与泛型类中的T相同,也可以不同
public <E> E show(E key){
return key;
}
// 泛型方法中的泛型标识T可以与泛型类中的T相同,也可以不同
public <T> T showkey(T key){
return key;
}
public <T> T setKey(T key){
// 报错,Incompatible types. Found: 'T', required: 'T'
// this.field的类型T是类中声明的,key的类型T是泛型方法中声明的,
// 并不一定是同一种类型,所以不能相互赋值
// this.field = key;
return key;
}
}
public static void main(String[] args) {
Generic<Integer> generic = new Generic<>();
generic.setField(111);
// 报错,泛型类的泛型确定类型后,类中使用此泛型的地方就确定类型了
// generic.setField("abc");
Integer newInt = generic.getField();
// 泛型类中的泛型方法,不受泛型类中的泛型定义的影响
String str = generic.show("def");
}
静态泛型方法
普通类中定义静态泛型方法
class Generic {
// 普通类中的静态泛型方法
public static <T> T show(T var){
return var;
}
}
public static void main(String[] args) {
// 与使用普通的静态方法差别不大,只是参数类型变为泛型
System.out.println(Generic.show("abc"));
System.out.println(Generic.show(111));
}
泛型类中的静态泛型方法,泛型类中静态泛型方法无法使用泛型类中定义的泛型。这意味着,静态方法若是想要使用泛型,必须将静态方法定义为泛型方法。
class GenericTest<T> {
// 泛型方法中的泛型与泛型类中定义的泛型不存在关系,可以类型相同,也可以不同
public static <E> E show(E var){
return var;
}
}
public static void main(String[] args) {
System.out.println(GenericTest.show(33));
GenericTest<Integer> genericTest = new GenericTest<>();
System.out.println(genericTest.show("def"));
}
泛型擦除
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<String> strList = new ArrayList<>();
Class intClass = intList.getClass();
Class strClass = strList.getClass();
System.out.println(intClass == strClass);
}
// 运行结果:
// true
从上面的例子可以看到,尽管在使用时,List<Integer>与List<String>类型不同,分别只能添加Integer与String类型的数据,但是在运行时,两者有属于同一种类型。这是因为Java在编译后,会将所有的泛型信息擦除,在字节码层面是没有泛型的类型信息的。也就是Java的泛型只在编译期间有效,这个过程叫做泛型擦除。
泛型是后来(SE5)才加入到Java语言特性的,Java让编译器擦除掉关于泛型类型的信息,这样使得Java可以兼容之前没有使用泛型的类库和代码,在字节码层面是没有泛型概念的。
泛型(T) --> 编译器(type erasure) --> 原始类型(T被Object替换)
泛型(? extends XXX) --> 编译器(type erasure) --> 原始类型(T被XXX替换)
原始类型指被编译器擦除了泛型信息后,类型变量在字节码中的具体类型。
存在泛型擦除,无法通过不同的List类型来重载方法,因为在运行时,List<String>与List<Integer>实际上就是同一种类型。
// 报错,both methods have same erasure
public void show(List<String> list) {
System.out.println(list);
}
public void show(List<Integer> list) {
System.out.println(list);
}
既然List<String>与List<Integer>在运行时,是同一种类型,那么通过反射,List<Integer>中也可以添加String类型的数据。
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(111);
// 报错,List<Integer>不能直接添加String类型对象
// intList.add("def");
intList.getClass().getMethod("add", Object.class).invoke(intList, "abc");
System.out.println(intList);
}
// 运行结果:
// [111, abc]
可以看到,List<Integer>通过反射成功添加了String类型对象,这是因为在运行时,List<Integer>的泛型信息被擦除,只保留了原始类型(在此处是Object),所以String类型的对象也可以被添加。
通配符
对于泛型类,有一点需要注意,当确定了泛型的类型后,不只可以使用此类型,还可以使用此类型的子类。对于List<A>,可以添加A及所有A的子类对象。
List<Number> numberList = new ArrayList<>();
numberList.add(new Integer(11));
numberList.add(new Double(2.22));
// 报错
// numberList.add(new Object());
System.out.println(numberList);
// 运行结果:
// [11, 2.22]
但是当List<A>作为方法的参数类型时,只有List<A>类型的数据可以使用,List<A的子类>不可以。Integer
是Number
的子类,但是List<Integer>
并不是List<Number>
的子类。
class GenericTest {
public void show(List<Number> numList) {
System.out.println(numList);
}
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
intList = new ArrayList<>();
GenericTest genericTest = new GenericTest();
genericTest.show(numList);
// 报错,List<Integer>类型的参数不能调用List<Number>的方法
// genericTest.show(intList);
}
为了实现同样的功能,我总不能再写一个参数为List<Integer>
的方法吧。List<?>
就派上用场了。List<?>
可以视为List<Integer>
与List<Number>
的共同父类,此时,?
就是通配符,因为它不会对类型进行限制,也被称为无界通配符。
class GenericTest {
public void show(List<?> list) {
System.out.println(list);
}
}
public static void main(String[] args) {
List<Object> objList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
GenericTest genericTest = new GenericTest();
genericTest.show(objList);
genericTest.show(numList);
genericTest.show(intList);
}
?
作为通配符存在一些问题,它可以接受任何类型的参数。当我们想在一定范围内限定参数类型,但是同时又能接受多种类型的参数呢?这时候可以使用? extends A
与? super A
。? extends A
限定为A及A的子类,? super A
限定为A及A的父类。
List<Object> objectList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
GenericTest genericTest = new GenericTest();
// showExtendsNumber方法的参数类型是List<? extends Number>
// 只有List<Number>及List<Number的子类>可以作为实参传入
// genericTest.showExtendsNumber(objectList); // 此行报错
genericTest.showExtendsNumber(numberList);
genericTest.showExtendsNumber(integerList);
// showSuperNumber方法的参数类型是List<? super Number>
// 只有List<Number>及List<Number的父类>可以作为实参传入
genericTest.showSuperNumber(objectList);
genericTest.showSuperNumber(numberList);
// genericTest.showSuperNumber(integerList); // 此行报错
List、List<Object>与List<?>区别
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Object());
list.add(new Integer(11));
list.add("abc");
List<Object> objectList = new ArrayList<>();
objectList.add(new Object());
objectList.add(new Integer(22));
objectList.add("def");
List<?> newList = new ArrayList<>();
newList.add(null);
newList.remove(0);
newList.clear();
// 报错,List<?>不可添加元素(null除外)
// newList.add(new Object());
//
List<Object> objectList01 = list;
List<Integer> intList01 = list;
List<?> newList01 = list;
// 报错,不同类型List不可互相赋值
// List<Integer> intList02 = objectList;
// List<Integer> intList02 = newList;
// List<Object> objectList02 = newList;
}
- List、List<Object>可以添加任意类型的对象,List<?>除null外,不可添加任何类型的对象,主要作为通配符用于方法参数;List<Object>是存在类型约束的,只能添加Object及其子类,但因为Java中的类都直接或者间接继承自Object,所以在添加对象方面,List与List<Object>没什么区别。
- List没有泛型约束,可以向任意类型的List赋值,也可以接受任意类型的List向它赋值
- List<Object>存在泛型约束,只能赋值同类型泛型,也只能接受同类型泛型赋值
- List<?>一般作为参数来接收外部集合,可以接受任意类型的List赋值,但是只能给同类型泛型赋值。List<?>在赋值后,除null外,不可添加任何元素,但是可以remove与clear。
参考资料:
- Java泛型详解
- 《码出高效》