共计 5065 个字符,预计需要花费 13 分钟才能阅读完成。
泛型是 Java 的高级个性之一,如果想写出优雅而高扩展性的代码,或是想读得懂一些优良的源码,泛型是绕不开的槛。本文介绍了什么是泛型、类型擦除的概念及其实现,最初总结了泛型应用的最佳实际。
前言
想写一下对于 Java 一些高级个性的文章,尽管这些个性在平时实现一般业务时不用应用,但如果想写出优雅而高扩展性的代码,或是想读得懂一些优良的源码,这些个性又是不可避免的。
如果对这些个性不理解,不相熟个性的利用场景,应用时又因为语法等起因困难重重,很难让人克服惰性去应用它们,所以身边总有一些共事,工作了很多年,却从没有用过 Java 的某些高级个性,写出的代码总是差那么一点儿感觉。
为了防止几年后本人的代码还是十分 low,我筹备从当初开始深刻了解一下这些个性。本文先写一下利用场景最多的泛型。
泛型是什么
首先来说泛型是什么。泛型的英文是 generic,中文意思是通用的、一类的,联合其利用场景,我了解泛型是一种 通用类型。但咱们个别指泛型都是指其实现形式,也就是 将类型参数化
对于 Java 这种强类型语言来说,如果没有泛型的话,解决雷同逻辑不同类型的需要会十分麻烦。
如果想写一个对 int 型数据的疾速排序,咱们编码为(不是配角,网上轻易找的 =_=):
public static void quickSort(int[] data, int start, int end) {int key = data[start];
int i = start;
int j = end;
while (i < j) {while (data[j] > key && j > i) {j--;}
data[i] = data[j];
while (data[i] < key && i < j) {i++;}
data[j] = data[i];
}
data[i] = key;
if (i - 1 > start) {quickSort(data, start, i - 1);
}
if (i + 1 < end) {quickSort(data, i + 1, end);
}
}
可是如果需要变了,当初须要实现 int 和 long 两种数据类型的快排,那么咱们须要利用 Java 类办法重载性能,复制以上代码,将参数类型改为 double 粘贴一遍。可是,如果还要实现 float、double 甚至字符串、各种类的疾速排序呢,难道每增加一种类型就要复制粘贴一遍代码吗,这样未必太不优雅。
当然咱们也能够申明传入参数为 Object,并在比拟两个元素大小时,判断元素类型,并应用对应的办法比拟。这样,代码就会恶心在类型判断上了。不优雅的范畴小了一点,并不能解决问题。
这时,咱们思考应用通用类型(泛型),将快排办法的参数设置为一个通用类型,无论什么样的参数,只有实现了 Comparable 接口,都能够传入并排序。
public static <T extends Comparable<T>> void quickSort(T[] data, int start, int end) {T key = data[start];
int i = start;
int j = end;
while (i < j) {while (data[j].compareTo(key) > 0 && j > i) {j--;}
data[i] = data[j];
while (data[i].compareTo(key) < 0 && i < j) {i++;}
data[j] = data[i];
}
data[i] = key;
if (i - 1 > start) {quickSort(data, start, i - 1);
}
if (i + 1 < end) {quickSort(data, i + 1, end);
}
}
那么,能够总结一下泛型的利用场景了,当遇到以下场景时,咱们能够思考应用泛型:
当参数类型不明确,可能会扩大为多种时。
想申明参数类型为 Object,并在应用时用 instanceof 判断时。
须要留神,泛型只能代替 Object 的子类型,如果须要代替根本类型,能够应用包装类,至于为什么,会在下文中阐明。
应用
而后咱们来看一下,泛型怎么用。
申明
泛型的申明应用 < 占位符 [, 另一个占位符] > 的模式,须要在一个中央同时申明多个占位符时,应用 , 隔开。占位符的格局并无限度,不过个别约定应用单个大写字母,如 T 代表类型(type),E 代表元素 *(element)等。尽管没有严格规定,不过为了代码的易读性,最好应用前检查一下约定用法。
泛型指代一种参数类型,能够申明在类、办法和接口上。
咱们最常把泛型申明在类上:
class Generics<T> { // 在类名后申明引入泛型类型
private T field; // 引入后能够将字段申明为泛型类型
public T getField() { // 类办法内也能够应用泛型类型
return field;
}
}
把泛型申明在办法上时:
public [static] <T> void testMethod(T arg) {// 拜访限定符[静态方法在 static] 后应用 < 占位符 > 申明泛型办法后,在参数列表后就能够应用泛型类型了
// doSomething
}
最初是在接口中申明泛型,如下面的快排中,咱们应用了 Comparable<T> 的泛型接口,与此类似的还有 Searializable<T> Iterable<T> 等,其实在接口中申明与在类中申明并没有什么太大区别。
调用
而后是泛型的调用,泛型的调用和一般办法或类的调用没有什么大的区别,如下:
public static void main(String[] args) {String[] strArr = new String[2];
// 泛型办法的调用跟一般办法雷同
Generics.quickSort(strArr, 0, 30);
// 泛型类在调用时须要申明一种准确类型
Generics<Long> sample = new Generics<>();
Long field = sample.getField();}
// 泛型接口须要在泛型类里实现
class GenericsImpl<T> implements Comparable<T> {
@Override
public int compareTo(T o) {return 0;}
}
类型擦除
讲泛型不可不提类型擦除,只有明确了类型擦除,才算明确了泛型,也就能够避开应用泛型时的坑。
由来
严格来说,Java 的泛型并不是真正的泛型。Java 的泛型是 JDK1.5 之后增加的个性,为了兼容之前版本的代码,其实现引入了类型擦除的概念。
类型擦除指的是:Java 的泛型代码在编译时,由编译器进行类型查看,之后会将其泛型类型擦除掉,只保留原生类型,如 Generics<Long> 被擦除后是 Generics,咱们罕用的 List<String> 被擦除后只剩下 List。
接下来的 Java 代码在运行时,应用的还是原生类型,并没有一种新的类型叫 泛型。这样,也就兼容了泛型之前的代码。
如以下代码:
public static void main(String[] args) {List<String> stringList = new ArrayList<>();
List<Long> longList = new ArrayList<>();
if (stringList.getClass() == longList.getClass()) {System.out.println(stringList.getClass().toString());
System.out.println(longList.getClass().toString());
System.out.println("type erased");
}
}
后果 longList 和 stringList 输入的类型都为 class java.util.ArrayList,两者类型雷同,阐明其泛型类型被擦除掉了。
实际上,实现了泛型的代码的字节码内会有一个 signature 字段,其中指向了常量表中泛型的真正类型,所以泛型的真正类型,还能够通过反射获取失去。
实现
那么类型擦除之后,Java 是如何保障泛型代码执行期间没有问题的呢?
咱们将一段泛型代码用 javac 命令编译成 class 文件后,再应用 javap 命令查看其字节码信息:
咱们会发现,类型里的 T 被替换成了 Object 类型,而在 main 办法里 getField 字段时,进行了类型转换(checkcast),如此,咱们能够看进去 Java 的泛型实现了,一段泛型代码的编译运行过程如下:
- 编译期间编译器查看传入的泛型类型与申明的泛型类型是否匹配,不匹配则报出编译器谬误;
- 编译器执行类型擦除,字节码内只保留其原始类型;
- 运行期间,再将 Object 转换为所须要的泛型类型。
也就是说:Java 的泛型实际上是由编译器实现的,将泛型类型转换为 Object 类型,在运行期间再进行状态转换。
实际问题
由上,咱们来看应用泛型时须要留神的问题:
具体类型须为 Object 子类型
上文中提到实现泛型时申明的具体类型必须为 Object 的子类型,这是因为编译器进行类型擦除后会应用 Object 替换泛型类型,并在运行期间进行类型转换,而根底类型和 Object 之间是无奈替换和转换的。
如:Generics<int> generics = new Generics<int>(); 在编译期间就会报错的。
边界限定通配符的应用
泛型尽管为通用类型,但也是能够设置其通用性的,于是就有了边界限定通配符,而边界通配符要配合类型擦除才好了解。
<? extends Generics> 是上边界限定通配符,避开 上边界 这个比拟含糊的词不谈,咱们来看其申明 xx extends Generics,XX 是继承了 Generics 的类(也有可能是实现,上面只说继承),咱们依照以下代码申明:List<? extends Generics> genericsList = new ArrayList<>();
Generics generics = genericsList.get(0);
genericsList.add(new Generics<String>()); // 编译无奈通过
咱们会发现最初一行编译报错,至于为什么,能够如此了解:XX 是继承了 Generics 的类,List 中取出来的类肯定是能够转换为 Generics,所以 get 办法没问题;而具体是什么类,咱们并不知道,将父类强制转换成子类可能会造成运行期谬误,所以编译器不容许这种状况;
而同理 <? super Generics> 是下边界限定通配符,XX 是 Generics 的父类,所以:
List<? super Generics> genericsList = new ArrayList<>();
genericsList.add(new Generics()); // 编译无奈通过
Generics generics = genericsList.get(0);
应用前须要依据这两种状况,思考须要 get 还是 set,进而决定用哪种边界限定通配符。
最佳实际
当然,泛型并不是一个万能容器。什么类型都往泛型里扔,还不如间接应用 Object 类型。
什么时候确定用泛型,如何应用泛型,这些问题的解决不仅仅只依附编程教训,咱们应用结尾快排的例子整顿一下泛型的实际形式:
- 将代码逻辑拆分为两局部:通用逻辑和类型相干逻辑;通用逻辑是一些跟参数类型无关的逻辑,如快排的元素地位整顿等;类型相干逻辑,顾名思义,是须要确定类型后能力编写的逻辑,如元素大小的比拟,String 类型的比拟和 int 类型的比拟就不一样。
- 如果没有类型相干的逻辑,如 List 作为容器不须要思考什么类型,那么间接欠缺通用代码即可。
如果有参数类型相干的逻辑,那么就须要思考这些逻辑是否已有独特的接口实现,如果已有独特的接口实现,能够应用边界限定通配符。如快排的元素就实现了 Compare 接口,Object 曾经实现了 toString() 办法,所有的打印语句都能够调用它。 - 如果还没有独特的接口,那么须要思考是否能够形象出一个通用的接口实现,如打印人类的衣服色彩和动物的毛皮色彩,就能够形象出一个 getColor() 接口,形象之后再应用边界限定通配符。
如果无奈形象出通用接口,如输入人类身高或动物体重这种,还是不要应用泛型了,因为不限定类型的话,具体类型的办法调用也就无从谈起,编译也无奈通过。
我将以上步骤整顿了一个流程图,依照这个图,咱们能够疾速得出能不能用泛型,怎么用泛型。
小结
好好理了一下泛型,感觉播种颇多,Java 迷雾被拨开了一些。这些个性的确挺难缠,每当本人感觉曾经了解得差不多的时候,过些日子又感觉当初了解得还不够。重要的还是要实际,在应用时会很容易发现纳闷的中央。
起源 | https://zhenbianshu.github.io/