关于对Java泛型的解释和思考
持续坚持原创输出,点击蓝字关注我吧
作者:软件质量保障
知乎:https://www.zhihu.com/people/iloverain1024
理解泛型
先看下维基百科给出的定义:
Generics are a facility of generic programming that were added to the Java programming language in 2004 within version J2SE 5.0. They were designed to extend Java's type system to allow "a type or method to operate on objects of various types while providing compile-time type safety".
翻译过来就是一句话,即可以理解为泛型就是 参数化 参数类型。
1. 泛型的作用
Java 5中添加了泛型,使开发者在使用集合类时提供编译时类型检查并消除ClassCastException异常。集合框架使用泛型来保证类型安全。下面举个例子,看看泛型如何帮助我们安全地使用集合类。
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
for(Object obj : list){
//type casting leading to ClassCastException at runtime
String str=(String) obj;
}
上面的代码写的没毛病,但在运行时就会抛出 ClassCastException,原因是我们试图将List中的 Object 转换为 String,而其中一个元素是 Integer 类型。在 Java 5 引入泛型之后,我们可以声明如下的集合类。
// java 7 ? List<String> list1 = new ArrayList<>();
List<String> list1 = new ArrayList<String>();
list1.add("abc");
//list1.add(new Integer(5)); //compiler error
for(String str : list1){
//no type casting needed, avoids ClassCastException
}
在创建List时,指定List中可接受元素类型为String。因此,如果我们尝试在List中添加任何其他类型的对象,程序将抛出编译时错误。在 for 循环中,我们不需要对List中的元素进行类型强制转换,因此泛型的引入消除了代码运行时的 ClassCastException。
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
2.泛型类
泛型类型用于类的定义中,被称为泛型类。泛型类是在类型上参数化的类或接口。我们使用尖括号 <>来指定类型参数。
为了理解这样做的好处,下面同样从应用的角度举个例子说明下:
public class GenericsTypeOld {
private Object t;
public Object get() {
return t;
}
public void set(Object t) {
this.t = t;
}
public static void main(String args[]){
GenericsTypeOld type = new GenericsTypeOld();
type.set("Pankaj");
String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
}
}
可以注意下,在使用此类时,我们必须使用类型转换,否则它会在运行时报 ClassCastException异常。现在我们将使用泛型重写相同的类,如下所示。
public class GenericsType<T> {
private T t;
public T get(){
return this.t;
}
public void set(T t1){
this.t=t1;
}
public static void main(String args[]){
GenericsType<String> type = new GenericsType<>();
type.set("Pankaj"); //valid
GenericsType type1 = new GenericsType(); //raw type
type1.set("Pankaj"); //valid
type1.set(10); //valid and autoboxing support
}
}
注意在 main 方法中使用了 GenericsType 类。我们不需要进行类型转换,因此消除了代码中的ClassCastException。
3.泛型接口
泛型接口与泛型类的定义及使用基本相同。可以看一个例子:
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
我们也可以有多个类型参数,就像在 Map 接口中一样。同样,我们也可以为参数化类型提供参数化值,例如new HashMap<String, List<String>>()。
4.泛型的表示方式
类型命名约定有助于我们轻松理解代码。所以泛型也有自己的表示方式。通常,类型参数名称是单个大写字母,以便与 Java变量区分开来。最常用的表示方式如下:
-
E – 元素(Java 集合框架广泛使用,例如 ArrayList、Set 等)
-
K - 键(用于Map)
-
N - Number
-
T - 类型
-
V – 值(用于Map)
5. 泛型方法
有时我们不希望整个类都被参数化,在这种情况下,我们可以创建 Java 泛型方法。由于构造函数是一种特殊的方法,我们也可以在构造函数中使用泛型类型。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
public class GenericsMethods {
//Java Generic Method
public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2){
return g1.get().equals(g2.get());
}
public static void main(String args[]){
GenericsType<String> g1 = new GenericsType<>();
g1.set("Pankaj");
GenericsType<String> g2 = new GenericsType<>();
g2.set("Pankaj");
boolean isEqual = GenericsMethods.<String>isEqual(g1, g2);
//above statement can be written simply as
isEqual = GenericsMethods.isEqual(g1, g2);
//This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets.
//Compiler will infer the type that is needed
}
}
isEqual方法显示了在方法中使用泛型类型的语法。另外,请注意如何在我们的 java 程序中使用这些方法。我们在调用这些方法时指定类型,也可以像普通方法一样调用它们,而Java 编译器可以确定要使用的变量的类型。
6. 泛型有界类型参数
假设我们想要限制在参数化类型中使用的对象类型,例如定义一个方法实现两个对象的比较,如果我们想要确保接受的对象是 Comparable,就要声明有界类型参数,方法如下:
public static <T extends Comparable<T>> int compare(T t1, T t2){
return t1.compareTo(t2);
}
如果我们在方法调用中尝试传入任何非 Comparable 类,则会引发编译时错误。有界类型参数可以与方法以及类和接口一起使用。
Java 泛型也支持多个边界,即<T extends A & B & C>。在这种情况下,A 可以是接口或类。如果 A 是类,那么 B 和 C 应该是一个接口。
7. 泛型通配符
问号 (?) 是泛型中的通配符,表示未知类型。通配符可用作参数或局部变量的类型,有时还可用作返回类型。但我们不能在调用泛型方法或实例化泛型类时使用通配符。
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
7.1) 泛型上界通配符
上界通配符用于放宽对方法中变量类型的限制。假设我们要编写一个方法来返回List中数字的总和,那么我们的实现将是这样的。
public static double sum(List<Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
现在,上述实现的问题在于它不适用于整型List或双精度List,而 List<Integer> 和 List<Double> 不相关,这时候上限通配符就可以起作用了。我们使用带有extends关键字的泛型通配符和上限类或接口,这将允许我们传递上限或其子类型的参数。
上面的实现可以修改如下:
import java.util.ArrayList;
import java.util.List;
public class GenericsWildcards {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
ints.add(3); ints.add(5); ints.add(10);
double sum = sum(ints);
System.out.println("Sum of ints="+sum);
}
public static double sum(List<? extends Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
}
就像我们在接口上写代码一样,在上面的方法中我们可以使用上界类 Number 的所有方法。请注意,对于有界List,我们不允许将任何对象添加到List中,但 null 除外,如果尝试在 sum 方法中向List中添加一个元素,程序将无法编译。
7.2) 泛型无界通配符
有时我们希望我们的泛型方法适用于所有类型,在这种情况下,可以使用无界通配符。
public static void printData(List<?> list){
for(Object obj : list){
System.out.print(obj + "::");
}
}
我们可以为printData方法提供 List<String> 或 List<Integer> 或任何其他类型的 Object List参数。与上限List类似,我们不允许向List中添加任何内容。
7.3) 泛型下界通配符
为泛型添加下边界,即传入的类型实参必须是指定类型的父类型,使用带有super关键字和下界类的泛型通配符 (?) 来实现。
public static void addIntegers(List<? super Integer> list){
list.add(new Integer(50));
}
思考:关于泛型的类型擦除
由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
下面通过例子说明一下:
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
泛型只在编译阶段有效
看下面的代码:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
Log.d("泛型测试","类型相同");
}
输出结果:泛型测试: 类型相同。
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
喜欢的话,就点个赞和在看再走吧 👍👍
- END -
下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!
往期推荐