泛型
写在前面:泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。
目录
1 泛型(generic)
Generic在J2E1.5引入的,中引入,简而言之,泛型在定义类,接口和方法时使类型(类和接口)成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种使用不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。
JDK是在编译期对类型进行检查,提供了编译时类型的安全性。它为集合 框架增加了编译时类型的安全性,并消除了繁重的类型转换工作 。
2 为什么使用泛型
在JDK5.0 以前,如果一个方法返回值是 Object ,一个集合里装的是 Ob ject ,那么获取返回值或元素只能强转,如果有类型转换错误,在编译器无法觉察,这就大大加大程序的错误几率,必须通过不断的测试来保障质量,如果在编译期就可以解决岂不美哉?
3 泛型的可重用性
显然,我们都知道Java代码的重用性高,但如何实现所谓的重用性呢???
这个例子,也可先跳过,看完后面通配符、上界、下界再回头看。
对于泛型的可重用性,我们直接例子一枚:
就拿一个简单的例子来说吧,对数组进行冒泡排序,那么我们很容易写出一个如下的冒泡排序:
//N个数字冒泡排序,总共要进行N-1趟比较,每趟的排序次数为(N-i)次比较
public static void bubbleSort(int[] arr){
//一定要记住判断边界条件,很多人不注意这些细节,面试官看到你的代码的时候都懒得往下看,你的代码哪个项目敢往里面加?
if(arr==null||arr.length<2){
return;
}
//需要进行arr.length趟比较
for(int i = 0 ;i<arr.length-1;i++){
//第i趟比较
for(int j = 0 ;j<arr.length-i-1;j++){
//开始进行比较,如果arr[j]比arr[j+1]的值大,那就交换位置
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
很显然,这个函数可以应对整形数组的排序,但是如果针对一个浮点型数组,那么该函数就无法处理,初步改进如下:
也许你会说,可以再改成一个浮点型的,那么就既可以处理整形也可以处理浮点型了
public static void bubbleSort(double[] arr) {
boolean flage = true;
for (int i = 0; flage && i < arr.length - 1; i++) {
flage = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
double temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flage = true;
}
}
}
}
再次改进:
- 那么如果针对的是一个字符型的数组呢????
- 如果说针对的是一个字符串型的数组呢???
- 如果我既要从小到大排序又要从大到小排序呢???
- 如果我要根据人的重量对人进行排序呢????
很显然我们需要的是一个可以处理数组元素是任意的,可以自己决定比较的属性的一个方法:
public static <T> void bubbleSort(T[] arr, Comparator cmp) {
boolean flage = true;
for (int i = 0; flage && i < arr.length - 1; i++) {
flage = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (cmp.compare(arr[j], arr[j + 1]) > 0) {
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flage = true;
}
}
}
}
那么现在就可以处理数组元素是任意的情况了,只需要根据自己的需求创建不用的比较器即可
import java.util.Comparator;
public class FirstMin implements Comparator<Integer> {
@Override
public int compare(Integer i1, Integer i2) {
return i2 - i1;
}
}
import java.util.Comparator;
public class FirstMax implements Comparator<Integer> {
@Override
public int compare(Integer i1, Integer i2) {
return i1-i2;
}
}
最后:
因为中间牵扯到一个问题就是,“工人”,“学生”都是“人”,二者很显然可以比较体重。
但是比较器针对的对象只能是一个
所以必须对函数进行修改
public static <T> void bubbleSort(T[] arr, Comparator<? super T> cmp) {
boolean flage = true;
for (int i = 0; flage && i < arr.length - 1; i++) {
flage = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (cmp.compare(arr[j], arr[j + 1]) > 0) {
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flage = true;
}
}
}
}
到这里,可能大家,不太懂,后面详细介绍泛型的上界与下界。
4 泛型的不允许子类型化
答案是会的。为什么呢?
假设允许,那么是不是可以改成以下的情况,在JDK 里所有的类都是 Object 的子类,如果允许子类型化, 那么 ls 里不就可以存放任意类型的元素了吗,这就和泛型的类型约束完全相悖,所以 JDK 在泛型的校验上有很严格的约束。
5 泛型的规则
5.1 类型擦除
5.1.1 类擦除、方法擦除
随意写两段代码,一段用泛型,一段不用。而经过编译后,生成的.class 文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样。(具体代码不再赘述)
jdk1.5之前的代码
public class Node{
private Object obj;
public Object get(){
return obj;
}
public void set(Object obj){
this.obj=obj;
}
public static void main(String[] argv){
Student stu=new Student();
Node node=new Node();
node.set(stu);
Student stu2=(Student)node.get();
}
}
使用泛型的代码
public class Node<T>{
private T obj;
public T get(){
return obj;
}
public void set(T obj){
this.obj=obj;
}
public static void main(String[] argv){
Student stu=new Student();
Node<Student> node=new Node<>();
node.set(stu);
Student stu2=node.get();
}
}
两个版本生成的.class文件
public Node();
Code:
0: aload_0
可以看到泛型就是在使用泛型代码的时候,将类型信息传递给具体的泛型代码。而经过编译后,生成
的.class 文件和原始的代码一模一样,就好像传递过来的类型信息又被擦除了一样。
方法擦除案例
编译器桥接
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public java.lang.Object get();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void set(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
}
// 第二个文件
public class Node<T> {
public Node();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public T get();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
}
5.1.2 编译器桥接
桥接方法是 JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法。
package com.naixue.vip.p6.bridging;
/**
* @Description
* @Author
**/
public class Node<T> {
public T data;
public void setData(T data) {
this.data = data;
}
public Node(T data) {
System.out.println("Node.setData");
this.data = data;
}
public static class MyNode extends Node<Integer>{
public MyNode(Integer data) {
super(data);
}
@Override
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
//模拟编译器产生的桥接方法
// public void setData(Object data){
// setData((Integer)data);
// }
}
public static void main(String[] args) {
MyNode mn=new MyNode(5);
Node n=mn;
//java.lang.ClassCastException: java.lang.String cannot be cast to
java.lang.Integer
n.setData("Hello");
Integer x=mn.data;
System.out.println(x);
}
}
我们可以通过Method.isBridge()方法来判断一个方法是否是桥接方法,在字节码中桥接方法会被标记为ACC_BRIDGE和ACC_SYNTHETIC,其中ACC_BRIDGE用于说明这个方法是由编译生成的桥接方法,ACC_SYNTHETIC说明这个方法是由编译器生成,并且不会在源代码中出现。可以查看jvm规范中对这两个access_flag的解释http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6。
有如下3个问题:
- 什么时候会生成桥接方法
- 为什么要生成桥接方法
- 如何通过桥接方法获取实际的方法
a那什么时候编译器会生成桥接方法呢?可以查看JLS中的描述http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.4.5。
就是说一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法(当然还有其他情况会生成桥接方法,这里只是列举了其中一种情况)。
b因为泛型在编译时编译器会检查往集合中添加的对象的类型是否匹配泛型类型,如果不正确会在编译时就会发现错误,而不必等到运行时才发现错误。因为泛型是在1.5引入的,为了向前兼容,所以会在编译时去掉泛型(泛型擦除),但是我们还是可以通过反射API来获取泛型的信息,在编译时可以通过泛型来保证类型的正确性,而不必等到运行时才发现类型不正确。由于java泛型的擦除特性,如果不生成桥接方法,那么与1.5之前的字节码就不兼容了。
c 我们在通过反射进行方法调用时,如果获取到桥接方法对应的实际的方法呢?可以查看spring中org.springframework.core.BridgeMethodResolver类的源码。实际上是通过判断方法名、参数的个数以及泛型类型参数来获取的。
参考原文链接:https://blog.youkuaiyun.com/mhmyqn/article/details/47342577
5.1.3 堆污染
Heap pollution(堆污染), 指的是当把一个不带泛型的对象赋值给一个带泛型的变量时, 就有可能发生堆污染。
而Heap Pollution有可能导致更严重的后果 : ClassCastException, 我们在刚才的varagMethod方法中添加几行代码如下:
public static void varagMethod(Set<Integer> objects) {
objects.add(new Integer(10));
}
然后修改main方法:
public static void main(String[] args){
Set set = new TreeSet();
varagMethod(set);
Iterator<String> iter = set.iterator();
while (iter.hasNext())
{
String str = iter.next(); // ClassCastException thrown
System.out.println(str);
}
}
可以看到,程序报了一个ClassCastException(类型转换错误),这是因为Java允许我们将一个无泛型参数的Set对象传递给varagMethod方法,并且我们在varagMethod方法中,给传入的Set对象添加了一个Integer对象,之后我们遍历set集合时,将Set中第一个元素强转成String,很明显Integer不能转化为String对象
5.2 翻译泛型表达式
Pair<Parson> pair=new Pair<Parson>();
pair.getFirst();
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。
编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用。
- 将返回的Object类型强制转换为Parson类型。
5.3 子类型规则
5.3.1 类型边界
泛型T在最终会擦除为Object类型,只能使用Object的方法。
5.3.2 通配符
无界通配符
在上述的泛型示例中,我们都是指定了特定的类型,至少也是Object ,假设有一种场景,你不知道这个类型是啥,它可以是 Object ,也可以是 Person 那咋办?这种场景就需要用到通配符,如下所示,通常采用一个 来表示
public void addAll(Collection<?> col){
...
}
上界通配符
基于上述的场景,加入我想限制这个类型为 Person 的子类,只要是 Person 的子类就都可以,如果泛型写成 <Person> 那么只能强转如下所示,那么就失去了泛型的意义,又回到了最初的起点。
针对这种情况于是有了有界通配符的推出。在泛型中指定上边界的叫上界通配符 <? extends XXX>。
上界通配符,本身都不行,
上界用于方法的入参:入参子类可
Add可子类,上界限定的容器,只能get不能add
下界通配符
原理同上界通配符,下界 通配符将未知类型限制为特定类型或该 类型 的 超 类型,下限通配符使用通配符(‘?’)表示,后跟 super 关键字, 后跟下限:<? super A> 。
同理,方法入参,入参为父类可,只get 不 add
6 泛型的限制
jdk定义了7种泛型的使用限制:
1、不能用简单类型来实例化泛型实例
class Pair<K, V> {
private K key; private V value;
public Pair(K key, V value) {
this.key = key; this.value = value;
}
public static void main(String[] args) {
//编译时会报错,因为int、char属于基础类型,不能用于实例化泛型对象
Pair<int,char> p = new Pair(8,'a');
//编译不会报错
Pair<Integer,String> p2 = new Pair<>(8,"a");
} }
2、不能直接创建类型参数实例
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error list.add(elem);
}
//作为解决办法,可以通过反射来创建
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
3、不能申明静态属性为泛型的类型参数
public class MobileDevice<T> {
//非法
private static T os;
//...
}
4、不能对参数化类型使用cast或instanceof
因为java 编译器在编译器会做类型檫除,于是在运行期就无法校验参数的类型
public static <E> void rtti(List<E> list) {
//编译期会提示异常
if (list instanceof ArrayList<Integer>) {
// ...
}
}
解决方法可以通过无界通配符来进行参数化
public static void rtti(List<?> list) {
//编译不会报错
if (list instanceof ArrayList<?>) { // ... } }
在有些场景下,编译器知道类型参数总是有效,这时是允许强转的
在有些场景下,编译器知道类型参数总是有效,这时是允许强转的
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // OK
5、不能创建数组泛型
6、不能create、catch、throw参数化类型对象
7、重载的方法里不能有两个相同的原始类型的方法
7 小结
7.1 两个维度:
- flag
- Inheritance
7.2 ArrayList中addAll方法
上界通配符,抽象list中 addAll
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}