Java泛型有点难

一、对泛型有一个初步理解

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
“泛型”这个术语的意思是:“适用于许多许多的类型”。

先看一段代码:

class MyArray {
    public Object[] objects = new Object[10];

    public void set(int pos, Object val) {
        objects[pos] = val;
    }
    public Object get(int pos) {
        return objects[pos];
    }
}

对于这样一个类来说,里面的数组什么样的数据都可以放,因为objects里面元素的类型是Object,是所有类的父类。这样确实达到了一个通用的效果,但这样也会带来许多风险

在这里插入图片描述

当我们在0下标处存放了一个字符串(“hello”),但在获取时却遇到了问题,因为get方法返回的类型是Object,无法直接赋值给String,因此需要进行强制类型转换,编译器才不会报错。

因此在这种情况下,就会导致存放元素时,什么都可以存,但获取元素时,需要进行强制类型转换,非常麻烦。为了解决这样的问题,就引出了泛型。

对代码稍作修改:

class MyArray<T> {
    public T[] objects = (T[])new Object[10];
    public void set(int pos, T val) {
        objects[pos] = val;
    }
    public T get(int pos) {
        return objects[pos];
    }
}

现在这个MyArray就是一个泛型类。这里的<T>可以理解为一个占位符。
当需要实例化数组时,不能采用 **public T[] objects = new T[10];这样的方式,因为这样做不能实例化泛型类的数组,编译器会报错。使用public T[] objects = (T[])new Object[10];**这样的方式,虽然编译器不会报错,但也不是最佳的选择,后面会提到。

public class Test12 {
    public static void main(String[] args) {
        MyArray<String> myArray = new MyArray<String>();
        myArray.set(0,"hello");
        //myArray.set(1,0);//Error
        String str = myArray.get(0);
    }
}

使用泛型时,需要使用<>指明类型,但new MyArray<String>() 等价于new MyArray<>() ,因为编译器会自动推导出类型。存放元素时,编译器会检测元素的类型和指定的类型是否匹配,如果不匹配,则会报错。获取元素时,也不需要我们进行类型转换。

注意:简单类型,基本类型不能作为泛型的参数

泛型的语法

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, …, Tn> {
}

class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, …, Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}

裸类型
裸类型是一个泛型类但没有带着类型实参
例如:

MyArray<String> myArray = new MyArray<>();//本应该这样写
MyArray myArray = new MyArray<>();//但实际上写成这样,这就是一个裸类型

小结

  1. 泛型是将数据类型参数化,进行传递
  2. 使用<T>表示当前类是一个泛型类
  3. 泛型目前未知的优点:数据类型参数化,编译时自动进行类型检测和转换

二、类型的擦除机制

ArrayList <String>和ArrayList <Integer>很容易被认为是不同的类型。因为不同的类型在行为方面肯定不同,例如,如果尝试着将一个Integer放 入ArrayList<String>,所得到的行为(将失败)与把一个Integer放ArrayList<Integer> (将成功)所得到的行为完全不同。但是事实真就如此?

public class Test12 {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }
}

在这里插入图片描述

事实却和我们所期望的不同,程序认为ArrayList <String>和ArrayList <Integer>是相同类型。

再看这段代码:

class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION , MOMENTUM> {}

public class Test12 {
    public static void main(String[] args) {
        List<Frob> list=new ArrayList<Frob>();
        Map<Frob, Fnorkle> map = new HashMap<Frob,Fnorkle>();
        Quark<Fnorkle> quark=new Quark<Fnorkle>();
        Particle<Long, Double> p =new Particle<Long,Double>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
    }
}

在这里插入图片描述

根据JDK文档的描述,Class.getTypeParameters0将 “ 返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数…”这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中所看到的,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。
因此,残酷的现实是:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。

用javap -c Test12反编译这个类,就能看到以下内容,从底层真正的了解为什么是这样

在这里插入图片描述
在底层编译的时候,我们发现无论是ArrayList <String>还是ArrayList <Integer>,都被擦除成为ArrayList,对于这种机制,我们称之为擦除机制。

Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含任何泛型信息

现在来解决一下前面遗留的一个问题:为什么不能实例化泛型类数组,public T[] objects = new T[10];
我们再来看一下这段代码的反汇编

class MyArray<T> {
    //public T[] objects = new T[10];
    public T[] objects = (T[])new Object[10];
    public void set(int pos, T val) {
        objects[pos] = val;
    }
    public T get(int pos) {
        return objects[pos];
    }
}

public class Test12 {
    public static void main(String[] args) {
        MyArray<String> myArray = new MyArray<String>();
        myArray.set(0,"hello");
        //myArray.set(1,0);//Error
        String str = myArray.get(0);
    }
}

在这里插入图片描述

通过这个反汇编,知道了T都会替换为Object,那为什么就不能实例化泛型类数组。
可以利用反证法:
假设可以实例化泛型数组:public T[] objects = new T[10]

我们再给MyArray提供一个方法:public T[ ] getArray() { return objects; }
因为擦除机制,T[ ] 最终会被替换为 Object[ ],因此我们在调用getArray()方法时,返回的是Object[ ]

此时,我们在mian函数中,做这样一件事:

public class Test12 {
    public static void main(String[] args) {
        MyArray<String> myArray = new MyArray<String>();
        myArray.set(0,"hello");
        String[] strings = myArray.getArray();
    }
}

因为在使用MyArray<T>时,指定的是String类型,因此正常来说,在调用getArray()方法时,也使用String[] 来接受返回值。虽然这段代码编译器检测不到错误,但是实际运行就会出错

在这里插入图片描述

为什么编译器检测不到错,而运行就报错?
因为编译器检测时,我们指定的类型是String,调用getArray()返回的是String[ ],所以编译器认为没错。
由于擦除机制,public T[ ] getArray() { return objects; } 会变为 public Object[ ] getArray() { return objects; } 。 既然是Object[ ],那就意味着不仅能存放Sting,还能存放Integer等其他类型的元素,那你凭什么用String[ ] 接受呢?代码在运行时,编译器就会报错,因为不确定 objects 里面都是String。

因此不能实例化泛型类数组——>public T[] objects = new T[10]

这样做也是不可以的——>public T[] objects = (T[])new Object[10],因为Object[ ]里面可能放的任意类型元素

最终我们需要通过反射来实例化数组

public MyArray(Class<T> clazz, int capacity) {
    array = (T[]) Array.newInstance(clazz, capacity);
}

这里我们指定了泛型数组的类型,通过Array.newInstance()方法,传入类型(clazz)和容量(capacity),此时才能创建一个T类型的数组

如果看过ArrayList源码的同学可能知道,ArrayList中的数组也是Object[ ],那为什么可以?

在这里插入图片描述

因为它有它自己的处理方式,比如看一下get()方法
在这里插入图片描述
虽然可以new Object[ ],但是获取元素时也要进行相应的类型转换

三、泛型的上界

public class MyArray<E extends Number>{
    ...
}

这段代码就叫做泛型的上界。
这代表啥意思呢?
将来你传给MyArray的类型参数E是Number的子类或者Number自己

MyArray<Integer> myarray1;//正确,因为Integer是Number的子类
MyArray<String> myarray2;//错误,因为String不是Number的子类

在这里插入图片描述

因为存在上界,代码编译时,擦除机制默认擦除到上界,如果没有上界,默认擦除到Object

例题:写一个泛型类,求出数组中的最大值

class ALg<T> {
    public T findMax(T[] array) {
        T max = array[0];
        for(int i = 0; i < array.length; ++i) {
            if(max < array[i]) {
                max = array[i];
            }
        }
        return max;
    }
}

但实际情况下,if(max < array[i])会报错

在这里插入图片描述

为什么这里会报错?
因为我们知道泛型在传递参数的时候都是传递的类类型,也就是引用类型,引用类型不能通过“>、=、<”比较,只能通过实现Comparable接口或者实现Comparator接口来比较大小
注意:equals只能判断true或false,不能比较大小关系

这里指定的是T可以是任意类型(String,Person,Student等),所以需要用到Comparable或者Comparator,假如这里我们用Comparable,但是此时我们也不知道T类型是否实现了Comparable,所以需要引用特殊的边界Comparable

class ALg<T extends Comparable<T>> {
    public T findMax(T[] array) {
        T max = array[0];
        for(int i = 0; i < array.length; ++i) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}

这里的ALg<T extends Comparable<T>>和MyArray<E extends Number>是一样的,也属于泛型的上界。但是此时传入的T一定要实现Comparable这个接口,因为T最终也会被擦除到Object,Object没有实现Comparable
除此之外:public T findMax(T[ ] array) 也可以写成 public <T extends Comparable<T>> T findMax(T[ ] array)

在这里插入图片描述

这里的Integer是实现了Comparable接口的

注意:泛型没有下界,并且ALg<T extends Comparable<T>>中的extends不能理解为继承,应该理解为拓展

现在这个findMax方法有一个不好的地方,每次调用这个方法,就需要new ALg这个对象,那能不能不new,直接通过类来调用呢?当然也可以,将这个方法改为静态方法

在这里插入图片描述

此时问题又来了,因为这个findMax它是一个静态方法,所以不依赖于对象,也就导致了Alg后面的<T extends Comparable>写了和没写一样,就类似这样

在这里插入图片描述

最终做这样的处理就可以让静态方法泛型化

class ALg {
    public static <T extends  Comparable> T findMax(T[] array) {
        T max = array[0];
        for(int i = 0; i < array.length; ++i) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}

在调用这个方法时,既可以指定参数,也可以不指定参数,如果不指定参数,编译器是可以通过参数array推导出它的类型的

在这里插入图片描述

所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。要定义泛型方法,只需将泛型参数列表置于返回值之前。

使用泛型方法时应该遵循一个基本的指导原则:
无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。

四、通配符及其上下届

通配符是用来解决泛型无法协变的问题的,协变指的是如果Apple是Fruit的子类,那么List<Apple>也应该是List<Fruit>的子类。但是泛型显然是不支持这样的父子类关系的。

例如:
在这里插入图片描述

尽管你在第一次阅读这段代码时会认为:“不能将一个Apple容器赋值给一个Fruit容器”。但正确的理解应该是:“不能把一个涉及Apple的泛型赋给一个涉及Fruit的泛型”。

初学者都会以为这是编译器拒绝向上转型,然而实际上这根本不是向上转型一Apple的List不是Fruit的List。 Apple的List将持有Apple和Apple的子类型,而Fruit的List将持有任何类型的Fruit,这包括Apple在内,但是它不是一个Apple的List,它仍旧是Fruit的List。 Apple的List在类型上不等价于Fruit的List,即使Apple是一种Fruit类型。

此时我们就需要用到通配符,来指定上界

在这里插入图片描述

flist类型现在是List<? extends Fruit>,? 表示通配符,这句代码你可以理解为 ? 表示 Fruit或者Fruit的子类。但是,这实际上并不意味着这个List将持有任何类型的Fruit。通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”。因此这个被赋值的List必须持有诸如Fruit或Apple这样的某种指定类型。

在这里插入图片描述

这里报错的原因就是Animal并不是Fruit或者Fruit的子类

再看一个例子:

在这里插入图片描述

这里list1去引用了list,用list1去调用add方法添加一个double类型元素,这里报错还是能理解的,因为list里面元素的类型是Integer,但是添加int元素也不行,这里就让人感到疑惑。
因为list1使用的是通配符,指定了list1应该引用Number或者Number的子类,但具体是哪一个类,编译器并不知道,为了安全,编译器不让这么做。因此通配符的上界不适合写入数据,但适合读取数据

在这里插入图片描述

list1引用的是Number或者Number的子类,可以用类型Number去获取list1中存放的元素,但是不能用Number的子类类型(例如Integer)去获取,这样会报错,因为list1可能引用Double类型

通配符的下界

在这里插入图片描述

ArrayList<? super Fruit> 表示引用的类型必须是Fruit或者Fruit的父类

在这里插入图片描述

在添加元素时,可以添加Fruit的父类或Fruit本身,因为arrayList1引用的至少是Fruit,将一个子类赋给一个父类再正常不过

对于读取元素,只能用Object去接受
在这里插入图片描述

arrayList1引用的一定是Fruit本身或者它的子类,为什么这里不能用Fruit类去接受呢?

用下界通配符修饰的容器对象不可以直接调用ge方法,这恰与上界通配符相反除非你用Object类型的引用指向它,这也正是编译器的看法,但是这种做法没有意义因为你还是无法知道它是一个什么类型对象,它的一切方法,包括继承过来的方法还是特有的方法都无法调用,除了Objec对象的几个方法它已经完全失去了一切身份信息造成这种情况的原因是执行get()方法时编译器只知道获得的元素是基类类型或是它的超类类型,但并不知道应是哪种具体类型,不同于上界通配符的做法,它不能视之为基类类型而进行向下的类型转换,而进行向上的类型转换又无法确定这个类型是否真的是你获得的类型的超类类型,除非你一 直向上转换成Object类型这也就是为何只有用Objec类型引用才能指向get()方法获得的对象的原因

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值