1. Java泛型概述
1.1 Object实现参数类型任意化
-
考虑自定义list,支持不同类型数据的add、get、getSize操作
-
JDK 1.5之前,由于不支持泛型,所以需要使用
Object
创建数组作为容器。 -
获取item时,需要显式地进行强制类型转换。
class MyList { public static final int LENGTH = 10; private Object[] list = new Object[LENGTH]; private int size = 0; public void add(Object object) { list[size++] = object; } public Object get(int index) { return list[index]; } public int size() { return size; } } // 使用方式 String str = (String) list.get(1);
-
基于自定义的list创建一个Integer列表,向list中添加元素时,错误添加了String类型的参数
-
get时,仍然按照Integer进行强制类型转换
public static void main(String[] args) { MyList integerList = new MyList(); // 基于Object的list,可以添加任意类型的参数 integerList.add(12); integerList.add("24"); integerList.add(36); // 访问元素 for (int i = 0; i < integerList.getSize(); i++) { // 显式进行强制类型转换 Integer integer = (Integer) integerList.get(i); System.out.println(integer + " + 1 = " + (++integer)); } }
-
上述代码,java编译时不会检查非法的强制类型转换,在运行时现
ClassCastException
使得程序崩溃
Object实现参数类型任意化的优缺点
- Object可以实现参数任意化,在一定程度上提高了程序的灵活性
- 随之而来的要求,想要访问Object类型的属性,必须显式地进行强制类型转换
- 错误的强制类型转换,在编译时无法发现,只有运行时才会抛出
ClassCastException
,降低了代码的健壮性
1.2 泛型的引入
-
在JDK源码中,我们经常看到类似代码,他们使用大写字母去限定自己的参数类型。这就是泛型!
public interface List<E> extends Collection<E> public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
-
泛型的意思就是泛指的类型(参数化类型),即所操作的数据类型可以以参数的形式进行指定。
class Printer<T>{ private T item; public Printer(T item){ this.item=item; } public void print(){ System.out.println(item.getClass().getName()); } }
泛型(generics)是 JDK 5 中引入的一个新特性,:
- 泛型提供了编译时类型安全检测机制,该机制将运行时非法类型转换的check提前到编译时,提升了代码的健壮性。
- 泛型会进行隐式地、自动地强制类型转换,提高了代码的可读性、可复用性。
参考文档:
2. 泛型的类型
2.1 泛型类
泛型类的定义
-
类名后面使用
<>
声明类中使用的泛型,可以包含一个或多个泛型通配符 -
在类中使用泛型,若不使用IDE会有提示,但不影响编译和运行
① 使用泛型通配符定义成员变量
② 使用泛型通配符定义方法的入参或返回值class ClassName<T>{ // 泛型通配符可以是A-Z的任意大写字母以及'?' private T item; // 使用泛型通配符定义成员变量 public T getItem() { // 返回泛型值的方法,并非是泛型方法 return item; } ... }
- 泛型类在Java的容器(
Collection
和Map
)中最常见,通过泛型实现了数据类型的任意化,使得代码灵活、安全、易维护。
泛型类的简单示例:
-
实例化泛型类不指定泛型类型,就与使用Object实现参数类型任意化的效果一样:无编译时的类型安全检测,需要对数据进行显式类型转换,
-
实例化泛型类指定泛型类型,才能开启编译时的类型安全检测,增加代码的健壮性
class MyGeneric<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } } // 使用泛型类 public static void main(String[] args) { // 创建泛型类对象时不指定类型,使用时与Object定义的类一样,需要进行显式转换 MyGeneric generic1 = new MyGeneric(); generic1.setItem(new Double(12.5)); Double number = (Double) generic1.getItem(); System.out.println("number: " + number); // 创建泛型类对象是指定类型,可以充分利用泛型的优势 MyGeneric<Integer> generic2=new MyGeneric<>(); // generic2.setItem(12.3); // 编译无法通过,提示double类型无法转换成Integer类型 generic2.setItem(12); if (generic2.getItem() instanceof Integer){ System.out.println("Item is Integer"); } }
2.2 泛型方法
泛型方法的定义
-
在权限修饰符和返回之间,通过
<>
指定方法将使用到的一个或多个泛型通配符 -
使用泛型通配符(非强制):使用声明的泛型通配符定义入参或返回值
// 有参数的泛型方法 public <E> void printInfo(E input){ System.out.println(input); } // 调用泛型方法 obj.printInfo("sunrise"); // 无参数的泛型方法 public <E> List<E> createList(){ return new ArrayList<>(); } // 调用泛型方法 List<String> list = obj.createList(); // 创建的是String类型的list
泛型方法的注意事项
-
只有在权限修饰符和返回值之间增加泛型通配符的方法,才是泛型方法。
public T getItem()
这样的方法,不是泛型方法 -
使用泛型通配符时,必须是声明过的泛型通配符,否则编译器会报错:
Cannot resolve symbol 'E'
public <T> void fanMethod(E data) { System.out.println("泛型方法,入参: " + data); }
-
泛型类与泛型方法是独立的:
(1)泛型类和泛型方法都使用通配符T
,但二者的泛型类型是独立的
(2)实际使用时,泛型类传入的泛型类型为String
,而泛型方法的参数类型任意,不受泛型类的限制
(3)泛型方法可以在泛型类中定义,也可以在普通类中定义,泛型类不一定包含泛型方法public class FanTest1<T> { private T data; /** * 省略构造方法、getter、setter方法 * 泛型方法,使用的泛型在返回值和权限修饰符之间定义 * 泛型类型不受类的泛型类型限制,即使都是使用相同的泛型符号 */ public <T> void print(T data) { System.out.println("静态方法,定义为泛型方法: " + data); } public static void main(String[] args) { FanTest1<Integer> test = new FanTest1<>(1); System.out.println(test.getData()); // 访问泛型方法,传入的泛型类型可以不是Integer test.print("hello"); } }
-
泛型类中,使用泛型的静态方法,必须定义为泛型方法
(1)泛型类只有实例化后,才会传入泛型类型
(2)静态方法属于类,并非对象,因此无法获取实例化后传入的泛型类型
(3)此时,必须将静态方法定义为泛型方法
泛型方法与可变参数
-
Java方法中,通常使用
...
来定义可变长度的参数(>= 0
个参数)。 -
编译后的可变参数将转为数组,使用方法与数组的使用方法一致
public void printNames(String... names) { for (String name : names) { System.out.print(name + " "); } } // 传入任意数量的参数 generic.printNames("lucy", "grace", "john", "张三");
-
使用可变参数有一些限制
(1)若不使用Object
, 则参数类型是固定的
(2) 可变参数必须位于方法入参的最后一个,且只能使用一个可变参数 -
将泛型方法与可变参数结合,可以充分利用泛型优势:
(1)编译时类型安全检查、可读性、代码复用性
(2)不传入泛型类型时,参数类型的任意化public <T> void printNumbers(T... args) { for (T arg : args) { System.out.print(arg + " "); } } // 方法的调用 generic.printNumbers("12.3", 24, 24.5, 2.4f);
2.3 泛型接口
泛型接口的定义
-
与泛型类的定义基本一致:接口名后使用
<>
声明接口中使用到的泛型,可以是一个或多个泛型 -
在接口中使用泛型:
① 使用泛型通配符定义public static final
类型的成员变量
② 使用泛型通配符定义方法的入参或返回值:方法默认为public abstract
,JDK 1.8开始支持定义default方法,使得接口不再是完全抽象类//定义一个泛型接口 public interface Generator<T> { public T next(); }
实现泛型接口
- 实现泛型接口时,传入泛型类型:
① 实现类无需声明泛型,类中所有使用到对应泛型的地方都会被自动替换成传入的泛型类型
② 这时,实现类就是一个普通类// 已传入泛型参数,实现类中泛型的类型则被固定,会自动将泛型替换成传入的泛型类型 public class StringImplement implements GenericInterface<String> { @Override public String next() { return RandomStringUtils.random(8, false, true); } }
- 实现泛型接口,未传入泛型类型:实现类需要声明泛型,这时实现类仍是一个泛型类
class GenericImplement<T> implements GenericInterface<T>{ @Override public T next() { return null; } }
参考文档
3. 泛型擦除
3.1 概述
3.1.1 泛型擦除
- Java泛型JDK 1.5开始引入的,为了兼容之前的代码,泛型成为了伪泛型
- 所谓的伪泛型,个人理解:
- 从Java代码上看,
GenericImplement<T>
中的T在字节码中也应该是一个泛型 - 只有实例化传入具体的泛型类型后,才会转为具体的类,如
GenericImplement<String>
编译后T
被String
替代 - 实际上,
GenericImplement<T>
的T
被Object
替代,泛型的相关信息都不存在了 - 这根本不是我们理解的泛型
- 从Java代码上看,
- 编译后泛型被原始类型替代(泛型类变普通类),使得泛型信息被擦除,这就是泛型擦除
- 如果使用
T
声明泛型,则被Object
类型替代;如果使用<? extends xxx>
,则被xxx
类型替代 —— 替代类型,是同一父类的最小级
- 如果使用
3.1.2 原始类型
- 上面的
Object
类型和xxx
类型就是编译后的原始类型,是擦除泛型后,类型变量在字节码中的原始类型 - 泛型类
TypeRemoveTest
如下public class TypeRemoveTest<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
- 其原始类型如下
public class TypeRemoveTest { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
泛型方法中的泛型变量
- 泛型方法中,泛型的具体类型需要分情况讨论
- 不为泛型方法指定泛型时,泛型类型为所有参数同一父类的最小级,即所有参数的最近祖先类
- 为泛型方法指定泛型时,泛型类型为指定的类型或子类型
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { // 不指定泛型 int value1 = selectOne(12, 24); // 同为Integer,拆包为int Number value2 = selectOne(12, 2.4); // Integer和Double的最小父类为Number Object value3 = selectOne("hello", 12); // String和Integer的最小父类为Object // 指定泛型 int value4 = TypeRemoveTest.<Integer>selectOne(12, 24); // 均为Integer Number value5 = TypeRemoveTest.<Number>selectOne(12, "hello"); // 编译报错,指定泛型类型后,要求必须为Number类型或其子类型 Number value5 = TypeRemoveTest.<Number>selectOne(12, 2.2); // Integer和Double,是Number的子类 } // 泛型方法 public static <T> T selectOne(T x, T y) { return y; }
3.1.3 疑问一:get时为何不用进行显式类型转换?
-
疑问: 既然泛型信息被擦除,为何通过get获取的值不是原始类型,而是我们传入的泛型类型?或者说:为何get时不用进行显式类型转换
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { TypeRemoveTest<String> stringType = new TypeRemoveTest<>(); stringType.setValue("Hello"); // 如果泛型擦除,获取的应该是Object类型,需要显式类型转换成String类型 // 这样使用并未报错,为何? String value = stringType.getValue(); }
-
通过查看字节码发现:获取到的值最开始确实是Object类型,后来被隐式地转为了String类型
3.1.3 疑问二:泛型类不指定泛型类型,泛型类型将会是什么?
- 需要具体分析:
- 对于无限定的泛型
<T>
,泛型类不指定泛型类型,泛型类型默认为Object,与使用Object实现参数任意化的效果一样 - 对于有限定的泛型
<T extends xxx>
,泛型类不指定泛型类型,泛型类型默认为xxx
,与使用xxx
实现参数任意化的效果一样
- 对于无限定的泛型
- 以无限定的泛型为例:
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { // 不指定泛型类型,与Object的参数任意化效果一样 TypeRemoveTest type = new TypeRemoveTest(); type.setValue("Hello"); String value = type.getValue(); // 编译报错: 不兼容的类型: java.lang.Object无法转换为java.lang.String String str = (String) type.getValue(); type.setValue(12); // 不会进行隐式的强制类型转换 type.getValue(); type.setValue(2.4); }
- 通过查看字节码发现,第二次的get没有进行隐式的强制类型转换
3.2 证明泛型擦除
3.2.1 原始类型相等
-
传入不同泛型类型的实例对象,通过
getClass()
获得的信息一致 —— 类型擦除变为原始类型public static void main(String[] args) { TypeRemoveTest<String> stringType = new TypeRemoveTest<>(); TypeRemoveTest<Integer> integerType = new TypeRemoveTest<>(); System.out.println(stringType.getClass() + " == " + integerType.getClass() + " ? " + (stringType.getClass() == integerType.getClass())); }
3.2.2 反射调用获取原始类型(反射绕过编译时类型安全检测)
-
反射调用获取的是原始类型,可以添加其他类型的元素
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { TypeRemoveTest<String> stringType = new TypeRemoveTest<>(); TypeRemoveTest<Integer> integerType = new TypeRemoveTest<>(); // 编译时类型安全检测,不允许添加其他类型元素 stringType.setValue("hello"); // integerType.setValue("2"); // 可以通过反射获取原始类型,从而添加其他类型的元素 Method method = integerType.getClass().getMethod("setValue", Object.class); method.invoke(integerType, "hello"); System.out.println(integerType.getValue()); }
3.3 泛型擦除引起的问题
3.3.1 类型检查 —> 泛型擦除 —> 编译成字节码
编译前的类型检查
- 在3.1.3中,我们回答了泛型使用原始类型替代,在get时却无需进行显式类型转换的原因
- 现在,也有一个类似的问题:set时,为何会进行类型安全检查,泛型不是已经被真实类型替代了吗?
- 按理说,无论我传入
String
还是Integer
,只要是真实类型Object
的子类,都是ok的 - 原因: 类型检查是在编译前
- 注意: 这里的get和set是指获取值、传入值,并非局限于成员变量的getter/setter操作
类型检查的依据是什么?
-
引入泛型前,容器类的实例化如下:
- 可以向list中加入各种类型的值,编译器不会报错
- 获取值时,如果我们单纯地认为值为某种类型,很可能出现
ClassCastException
- 这也是为什么需要引入泛型,通过其编译时类型安全检测,提高代码的健壮性
ArrayList list = new ArrayList();
-
引入泛型后,参数类型的任意化通过泛型类型去指定:实例化时指定泛型类型,引用中也指定对应的泛型类型
ArrayList<String> arrayList = new ArrayList<String>();
-
细心的你会发现,IDE会提示你:
new ArrayList<String>
中的String
可以移除
-
原因: 类型检查是基于引用的,
new ArrayList<>()
只是开辟一块空间-
get或set的值类型,需要与引用传入的泛型类型一致
-
如果引用未传入泛型类型,则泛型类与Object实现参数类型任意化一样
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { ArrayList<String> list1 = new ArrayList<>(); list1.add("hello"); // 引用指定的泛型类型为String,传入Integer无法通过检查 // list1.add(12); // 引用指定的泛型类型为String,获取的值为String类型, // Integer number = list1.get(0); String str = list1.get(0); // 未指定泛型类型,将使用真实类型Object ArrayList list2 = new ArrayList<String>(); // 可以添加任意类型的值 list2.add("hello"); list2.add(2.4); // 获取的值为Object类型 Object value = list2.get(1); }
-
特殊的: 未显式指定引用,新建对象后立即使用。如果指定了泛型类型,会基于此类型进行安全检测
public static void main(String[] args) { new ArrayList<String>().add("Hello"); new ArrayList<String>().add(10); // 编译错误:对于add(int), 找不到合适的方法 new ArrayList<>().add("Hello"); }
-
为什么不支持泛型类型的继承?
- 下面的代码编译错误:提示:不兼容的类型
ArrayList<String> list1 = new ArrayList<Object>(); ArrayList<Object> list2 = new ArrayList<String>();
- 第一种写法错误,本人倒是能理解:
- 相当于将
ArrayList<Object>
引用传递给ArrayList<String>
引用,已经存储的Object值需要转为String类型 - 这样很容易出现
ClassCastException
,因此Java不予许出现这样的引用传递
- 相当于将
- 第二种写法:
- 将String向上转型为Object明明是可以的
- 泛型出现的意义,就是为了实现类型安全检测
- 到头来,还需要自己进行强制类型检查,违背了泛型的设计初衷
- 所以,Java中将以上写法都视为错误写法
3.3.2 泛型类型不能是基本数据类型
ArrayList<int>
是不允许的,要想存储整数必须使用ArrayList<Integer>
- 原因: 泛型擦除后,Object不能存储int类型的值,只能引用Integer类型的值
3.3.3 不能进行类型判断
getClass()
获取泛型擦除后的class对象
-
泛型擦除后,无法通过
getClass()
获取类似ArrayList<Number>
这样的带泛型信息的class对象, -
转为原始类型的类,就是一个普通类,获取到的就是一个普通类的class对象
ArrayList<Number> list1 = new ArrayList<>(); System.out.println(list1.getClass());
-
上述代码的执行结果:
list instanceof ArrayList<String>
编译无法通过,更无法知道list对应的泛型类型是什么
- 下面的写法是错误的,更无法知道list的泛型类型是什么
3.3.4 桥方法
setter方法是重写还是重载?
-
基于上面的
TypeRemoveTest
这个泛型类,我们想生成一个指定泛型类型的子类class StringRemoveTest extends TypeRemoveTest<String> { @Override public String getVal() { System.out.println("执行子类的get方法"); return "子类"; } @Override public void setVal(String val) { System.out.println("执行子类的set方法"); } }
-
疑问来了:针对setter方法,泛型擦除后,父类中的setter方法为
setVal(Object val)
。 -
这明显不满足重写的定义:子类重新定义父类的方法,方法名和参数列表都相同
-
这更像是子类继承了父类的
setVal(Object val)
,然后又重载为setVal(String val)
-
如果通过反射调用到了继承得到的
setVal(Object val)
,最终的执行结果就不是我们所期待的 -
编译器自动在子类中生成了重写的
setVal(Object val)
,其内实际调用子类的setVal(String val)
方法,避免了泛型擦除与多态之间的矛盾
-
个人理解:
- 子类中定义没有
setVal(Object val)
方法,通过父类(指向子类对象)调用setVal
时,实际应该调用父类的setVal(Object val)
方法 - 编译器自动为子类生成了桥方法
setVal(Object val)
,实现了对父类方法的重写,解决了泛型擦除对父类与子类间多态造成的影响 - 桥方法十分巧妙:内部实际调用的是子类重载的
setVal(String val)
方法 - 看起来就像是
setVal(String val)
重写了父类的setVal(Object val)
,也允许我们为其添加@Override
注解 - 总之,利用编译器自动生成的桥方法,子类实现了对setter方法的重写
- 子类中定义没有
get方法
- 子类中的
String getVal()
满足重写的定义,因此可以实现子类和父类间的多态性 - 通过查看字节码发现,编译器也为getter方法生成了桥方法
- 质疑:在子类中出现方法名和参数列表相同的getter方法,这根本就是同一个方法,是不允许这样编写代码的!!
- 实际上,这俩个方法的返回值是不同的,在jvm看来就是两个不同的方法
- jvm判断方法时,是方法明、参数列表、返回值综合判断的
- 但是,编写代码时却不允许程序员定义这样的相同方法
- 疑惑: 子类都实现getter方法重写了,干嘛编译器还需要定义一个桥方法
- 欢迎讨论
3.4 絮絮叨叨
3.4.1 一些理解
- 关于泛型擦除,自己也是似懂非懂
- 只知道为了与之前的JDK代码兼容,Java中的泛型是伪泛型
- 编译后的字节码中,泛型不是使用泛型通配符表示,而是被原始类型替换
- 看起来就像是泛型信息被擦除了,称作泛型擦除
- 原始类型:
- 无限定时,使用Object替换;有限定
<T extends xxx>
时,使用xxx
替换 - 不管是泛型类,还是泛型方法,泛型类型都满足最近祖先类原则(同一父类的最小级)
- 通过get获取值时,字节码中会存在隐式地强制类型转换
CHECKCAST
,而不是返回被擦除后的原始类型 - 泛型类实例化时,不指定泛型类型,泛型类型也会满足最近祖先类原则
- 无限定时,使用Object替换;有限定
一些问题(及解决方法)
- 泛型类型安全检查的时机和依据
- 类型安全检查 —> 泛型擦除 —> 编译成字节码,set方法传入的值类型不正确,IDE会有提示
- 类型安全检查的依据是引用中的泛型类型:
new ArrayList<>()
无需显式指定泛型类型
- 泛型类型不支持继承,下面的两种写法,均是错误的:
第一种写法,可能会导致ClassCastException
;
第二种写法,没有充分利用泛型的优势ArrayList<String> list1 = new ArrayList<Object>(); ArrayList<Object> list2 = new ArrayList<String>(); ```
- 泛型类型必须为引用类型,不能是基本数据类型:Object不能存储int值,只能引用Integer对象
- 无法进行类型判断
- 泛型擦除后,
getClass()
获取的不是传入的泛型类型,而是原始类型
- 泛型擦除后,
参考文档