Java泛型是JDK 1.5才引入的,为了兼容之前的版本,Java的泛型机制实际上是一种“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”,将所有泛型的表示(尖括号中的内容)都替换为具体的类型(其对应的指定类型),就像完全没有泛型一样。也就是说,泛型对于JVM是透明的。
为什么会引入泛型?
泛型的本质是为了参数化类型(在不创建新类的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。
- 参数类型的多态化,方便代码复用
先看下列的代码:
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
是不是觉得有点那么的怪异,其实逻辑是一样的,但是我们却写了三个重载的方法。
有了泛型,我们就可以使用泛型方法:
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
现在是不是简洁了很多。
- 泛型的类型在使用的时候指定,方便编译器检查
List<String> list = new ArrayList<>();
如果我们向这里列表中插入了非String类型的对象,那么是不会通过编译的。
泛型的基本使用
总的来说,就三种:泛型类、泛型接口、泛型方法
泛型类
class Test<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
多个泛型参数也是可以的:
public class Test<K,V> {
private K key;
private V value;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
泛型接口
public interface Test<T> {
T test();
}
class TestImpl<T> implements Test<T> {
private T value;
@Override
public T test() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
泛型方法
public class Test {
public static <T> T test(Class<T> c) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException{
return c.getDeclaredConstructor().newInstance();
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
String string = test(String.class);
System.out.println(string);
}
}
泛型的上下界限
class A {}
class B extends A{}
public class Test { // 不会报错
public static void funA(A a) {
}
public static void funB(B b) {
funA(b);
}
}
public class Test { // 会报错,具体原因请看下文
public static void funC(List<A> listA) {
}
public static void funD(List<B> listB) {
funC(listB);
}
}
解决方式如下:
public static void funC(List<? extends A> listA) {
}
public static void funD(List<B> listB) {
funC(listB);
}
在泛型中指定参数类型的上边界:<? extends A>,表示传入func的参数类型只能是A或者A的子类。
有上边界,就会有下边界:<? super A>,表示传入func的参数类型只能是A或者A的父类。
当然,还以使用 & 符号:
//工资低于2500元的上斑族并且站立的乘客车票打8折
public static <T extends Staff & Passenger> void discount(T t){
if(t.getSalary()<2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
深入理解泛型
文章的开头我们就提到了一个很重要的概念,那就是泛型擦除。
类型擦除是怎么进行的呢?
原则:
消除类型参数声明,即删除
<>
及其包围的部分。根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
为了保证类型安全,必要时插入强制类型转换代码。
自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”
// 源代码
class Test<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// 泛型擦除后
class Test {
private Object value;
public Object getValue() {
return value;
}
public Object setValue(Object value) {
this.value = value;
}
}
// 源代码
class Test<T extends Number> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// 泛型擦除后
class Test {
private Number value;
public Number getValue() {
return value;
}
public void setValue(Number value) {
this.value = value;
}
}
证明一下类型擦除?
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass()); // true
}
}
最终的结果是true,所以确实进行了泛型擦除,否则不可能相等。
既然会被擦除,那为什么我们平时在使用的过程中,比如说ArrayList<\String>,如果向尝试向其中添加其他类型的元素,为什么会报错?
泛型的编译期检查
这里直接给答案了,编译器是通过变量的类型来进行检查的,与引用的对象无关。
(其实,仔细回想一下多态机制,父类变量引用了子类对象,能调用子类对象的扩展方法嘛?)
看例子:
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
}
考虑以下这个好玩的例子:
ArrayList<String> list1 = new ArrayList<Object>(); //编译错误
ArrayList<Object> list2 = new ArrayList<String>(); //编译错误
为什么?反正擦除之后都是Object嘛。想法是好的,对于第一行代码,请问泛型的意义在哪里?对于第二行代码,非常容易出现运行时错误,比如:ClassCastException。
泛型的桥接方法
类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。
先看这种情况:
// 源代码
public class Test<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
// 泛型擦除后
public class Test {
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
一切都很好,不过嘛,看下面:
class Test02 extends Test<String> {
@Override
public String getData() {
return super.getData();
}
@Override
public void setData(String data) {
super.setData(data);
}
}
类型擦除后,父类中的是Object,而子类的是String,这不是继承,是重载。
这里就不反编译查看字节码了。你只需要知道所谓的桥接方法就是复写经过泛型擦除后的方法,在内部调用我们写好的方法:
// 伪代码
@Override
public Object getData() { // 这里想表示的是这个桥接方法去调用下面那个我们复写的方法
return getData();
}
@Override
public String getData() {
return super.getData();
}
2024.10.22
writeBy kaiven