在定义类、接口和方法时,泛型使类型(类和接口)成为参数。与方法声明中使用的形参非常相似,类型参数为您提供了一种方法,可以用不同的输入重用相同的代码。不同之处在于形式参数的输入是值,而类型参数的输入是类型。
使用泛型有许多好处:
1、在编译时加强类型检测
通过使用泛型,可以在编译时捕获和修复类型错误,从而避免在运行时出现 ClassCastException 等类型转换异常。这提高了代码的可靠性和稳定性。
2、消除类型转换
使用泛型可以避免一些操作的强制类型转换
3、提高代码重用性
泛型可以使代码更加通用,可以编写一次代码来处理多种类型的数据。
4、提高性能
泛型是在编译时进行类型检查的,因此可以避免在运行时进行类型转换,从而提高了程序的性能。
泛型定义
泛型可以定义在类,接口和方法上。泛型使用 < >来指定泛型类型。
public class Aminal<T> {private T flag;public T getFlag() {return flag;}public Aminal(T x){this.flag = x;}public static void main(String[] args) {Aminal<String> a1 = new Aminal<>("a");Aminal<Integer> a2 = new Aminal<>(2);System.out.println(a1.getFlag());System.out.println(a2.getFlag());}
}
如上,类变量flag在class定义时候指定为泛型,在对应使用泛型变量flag的地方都需要使用泛型进行接收和传递。
常见的泛型类型标识:
E - Element(表示元素,常见于JDK的集合框架中)
K - Key(键)
V - Value(值)
N - Number(数字)
T - Type (类型)
S, U, V等 - 第2个、第3个、第4个类型
上面这些泛型类型标识只是一种约定,不会强制进行校验,你也完全可以自定义,如下
public class Fruit<XXX> {private XXX price;XXX getPrice(){return price;}void setPrice(XXX price){this.price = price;}public static void main(String[] args) {Fruit<Double> f1 = new Fruit<>();f1.setPrice(2.1d);Fruit<Integer> f2 = new Fruit<>();f2.setPrice(5);}
}
上面使用XXX来表示类型,一样可以正常编译使用。
多个泛型
定义泛型时,可以指定多个泛型类型,多个之间使用,隔开
public class Pair<K,V> {K key;V value;public Pair(K k,V v){this.key = k;this.value = v;}public static void main(String[] args) {Pair<Integer,String> pair = new Pair<>(666,"泛型");System.out.println(pair.value);}
}
泛型方法
泛型方法相比于普通方法的声明,还会在返回值前使用 < >来声明使用的泛型参数列表
public static <T> List<T> fromArrayToList(T[] a) {return Arrays.stream(a).collect(Collectors.toList());
}
这里需要主要一点类上的泛型在类方法上都可以直接使用(注意是非静态方法),不用在方法上声明。
泛型类型限定
可以限定泛型为某个类的子类或实现了某个接口,使用extends来指定父类。
public static <T extends Number> float plus(T a ,T b){
return a.floatValue() + a.floatValue();
}
如果要限定多个条件可以使用 &来连接。
<T extends Number & Comparable>
通配符限定
在泛型代码中,问号(?)被称为通配符,用来表示未知类型。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;有时作为返回类型(尽管最好的编程实践是更加具体)。通配符永远不会被用作泛型方法调用的类型参数,泛型类的实例创建,或者超类型。
通配符限定只能使用在引用类型上,是是对泛型的限定。可以限定泛型的上界和下界。
<? extends Foo>
<? super Foo>
上界:? extends Foo表示泛型最高类型是Foo,只能是Foo及其子类。
下界:? super Foo表示泛型最低类型是Foo,只能是Foo及其父类。
无界:? 表示没有类型限制
例如:
public void printFruits(List<? extends Fruit> fruits) {for (Fruit fruit : fruits) {System.out.println(fruit);}
}
这里入参约束成Fruit的上界,也就是入参只能是实现了Fruit接口的类,这样在方法体中就可以调用Fruit接口统一的方法来完成逻辑操作。这里一定要理解 ?extends和 T extends的区别。?extends是针对引用类型,也就是实际参数,而T extends是方法或类的定义上。
类型擦除(Type Erasure)
Java 中的泛型在编译时会进行类型擦除(Type Erasure)。类型擦除是 Java 泛型实现的一种机制,它允许你在编译时使用泛型类型(也就是在编码是进行检测),但在运行时使用的是原始类型。在编译时,泛型类型参数被擦除并替换为其边界或 Object 类型。
验证泛型擦除可以使用反射来操作class。
如下定义类
public class ErasureTest<T,X extends Number> {T t;X x;public void setT(T t){this.t = t;}
}
通过反射打印类信息:
Class c = ErasureTest.class;
for (Field field : c.getDeclaredFields()) {System.out.println(field.getName()+":"+field.getType());
}
for (Method method : c.getDeclaredMethods()) {System.out.println(method.getName()+":");Class[] params = method.getParameterTypes();for (int i = 0; i < params.length; i++) {System.out.println("参数"+params[i].getName()+",类型:"+params[i].getTypeName());
}
/**
输出内容:
t:class java.lang.Object
x:class java.lang.Number
setT:
参数java.lang.Object,类型:java.lang.Object
*/
这里可以看到X extends Number转换成了其上界Number,T转换成了其原始类型Object。
擦除带来的问题
由于擦除,通过反射在对方法进行调用时可以跳过类型约束:
List<String> list = new ArrayList<>();
list.add("haha");
Method addMethod = list.getClass().getDeclaredMethod("add", Object.class);
addMethod.invoke(list,Integer.valueOf(1));
System.out.println(list);
如上代码,定义了一个String型的list,但是我们通过反射成功的往list添加了一个Integer类型的,上面的代码可以正常执行。这就可以绕过泛型限定。