0


【Java数据结构】玩转泛型-泛型的手术刀剖析

文章目录


一、泛型是什么?

“泛型”的意思是:适用于许许多多的类型。

泛型是在JDK1.5引入的新的语法,从代码上讲,就是对类型实现了参数化。

我们在这里先实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。

  1. 我们以前学过的数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] strs = new String[10];
  2. 所有类的父类,默认为Object类。那么数组是否可以创建为Object?

我们来看一下下面代码:

classMyArray{public Object[] objects =newObject[10];publicvoidset(int pos, Object val){
        objects[pos]= val;}public Object get(int pos){return objects[pos];}}publicclassTestDemo{publicstaticvoidmain(String[] args){
        MyArray myArray =newMyArray();
        myArray.set(0,"haha");
        myArray.set(1,11);
        myArray.get(0);// String str = myArray.get(0);//问题在于我们的0下标数为字符串,所以我们在这里用的是字符串接受,可是会报错。//为什么呢?因为我们上面的返回方法是Object//所以这里我们只能进行强制类型转换
        String str =(String) myArray.get(0);}}

在这里插入图片描述那么这样的做到就会导致一个很尴尬的局面。

  1. 一方面,我们希望这个类能接受所有的类型,并且需要什么类型,就传入什么类型。而不是像这样什么都可以传进来。
  2. 另一方面,这个看似能接受所有类型,可是在接受所存类型的时候往往需要强制类型转化才不会报错。

因此,这里我们就引入了泛型。

泛型语法规则如下:

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

类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 等等 - 第二、第三、第四个类型

那泛型的作用呢?

  1. 编译的时候,自动进行类型的检查
  2. 不需要手动进行类型的强制转换,会自动帮我们进行类型转换

所以刚刚的代码用泛型来实现的话,就很ok啦

/**
 *
 * @param <T>  这个<T>代表当前类是一个泛型类,T:是一个占位符
 */classMyArray<T>{//public T[] objects = new T[10];//这里不是new一个泛型数组//换句话说就是不能实例化泛型数组public T[] objects =(T[])newObject[10];//我们在这里实例化一个Object,强制转换成T类型publicvoidset(int pos, T val){
        objects[pos]= val;}public T get(int pos){return objects[pos];}}publicclassTestDemo{publicclassTestDemo{publicstaticvoidmain(String[] args){
        MyArray<String> myArray =newMyArray();
        myArray.set(0,"haha");//myArray.set(1,11); //这里会显示报错,因为存放的类型不是我们指定的String
        myArray.get(0);

        String str = myArray.get(0);}}

我们可以对照着看,二者代码的区别:

在这里插入图片描述因此,泛型的实用就帮助我们解决了一开始的问题:

  1. 创建一个通用的类型,指定当前的容器,要持有什么类型的对象。让编译器去做检查。
  2. 可以存放指定的类型,把类型,作为参数传递。需要什么类型,就传入什么类型,从而使程序具有更好的可读性和安全性

以后,当我们需要用什么类型时,可以直接指定:
在这里插入图片描述【注意】简单类型或者基本类型不能作为泛型类型的参数。

有关基本类型的介绍可以查看我以前的文章:
【Java系列】玩转Java——Java中的数据类型与运算符!

此外,我们这里还有一个裸类型的东西:

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型。

我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制

MyArray list =newMyArray();

以上内容小结:

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

二、泛型是如何编译的

这里必须首先提到的就是我们常说的擦除机制

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

换句话说,就是我们的擦除机制只存在于编译的时候。

无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased) 类型变M , 并替换为限定类型 (无限定的变量用 Object)。

这里借用网上的一幅图:
在这里插入图片描述我们通过javap -c 查看字节码文件,可以看到,我们的T类型在编译的时候会自动转为Object类型。

在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制

这里有一篇由Java SE5的主要开发人员之一Neal Gafte以前所写有关擦除机制的文章,大家想进一步了解的话可以去看看。
Java泛型擦除机制之答疑解惑

三、泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。

语法:

class 泛型类名称<类型形参 extends 类型边界> {

}

举个例子:

publicclassMyArray<E extendsNumber>{...}

我们传给MyArray的参数E类型,它是我们Number子类或者是Number本身。从而限定了E的类型。

我看一下下面的例子:
在这里插入图片描述
看到这里,有的老铁可能会想:泛型是否有下界呢?

答案是没有,泛型只有上界,没有下界。

如果一个泛型没有指定边界,则默认是Object。因为在编译时,它会帮你全都擦除成Object。( E extends Object)

一个例子:
假如我们要通过泛型实现找最大值,为什么圈住的地方会报错?
在这里插入图片描述刚刚提到过:

  • 泛型在传递的时候都是通过类类型,也就是通过引用类型,但是引用类型不能通过>< 等符号比较,因此会报错。

那怎么办?难道我们就没有其他办法了吗?
肯定不是,这里我们可以通过E 实现Comparable接口来解决这个问题。

classAlg<T extendsComparable<T>>{public  T findMax(T[] array){
        T max = array[0];for(int i =1; i < array.length; i++){//if(max < array[i]) { //为什么这个地方会报错?if(max.compareTo(array[i])<0){
                max = array[i];}}return max;}}

四、泛型方法

相对于泛型是什么,Java中的泛型方法就比较复杂了。

语法:

方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { … }

这里借鉴一下这篇文章:
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
以下代码摘取自上面的链接:

publicclassGenericTest{//这个类是个泛型类,在上面已经介绍过publicclassGeneric<T>{private T key;publicGeneric(T key){this.key = key;}//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。//所以在这个方法中才可以继续使用 T 这个泛型。public T getKey(){return key;}/**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
        public E setKey(E key){
             this.key = keu
        }
        */}/** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }
     */public<T> T showKeyName(Generic<T> container){
        System.out.println("container key :"+ container.getKey());//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();return test;}//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。publicvoidshowKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is "+ obj.getKey());}//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类publicvoidshowKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is "+ obj.getKey());}/**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
    public <T> T showKeyName(Generic<E> container){
        ...
    }  
    *//**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
    public void showkey(T genericObj){

    }
    */publicstaticvoidmain(String[] args){}}

泛型方法相对复杂和困难,不过我们也不必纠结太多,这部分我们只要求能够读懂就行,没必要太沉迷于琢磨语法这个东西。

更重要的是,我们写代码的能力。

五、通配符

1.概念

? 用于在泛型的使用,即为通配符

通配符类型中, 允许类型参数变化。

我们通过一个例子来感受以下:

classAlg3{publicstatic<T>voidprint1(ArrayList<T> list){//参数是T,此时的T一定是将来指定的一个泛型参数for(T x:list){
            System.out.println(x);}}publicstaticvoidprint2(ArrayList<?> list){//这里使用了统配符?,和代码1相比,此时传入printList2的,具体是什么数据类型,我们是不清楚的。这就是通配符for(Object x:list){
            System.out.println(x);}}}

两种代码异同:

  1. 从效果上来看:两者效果是一样的。
  2. 不同在于print1 的T 一定是指定了某种数据类型的,然后用T去接受。
  3. 在print2中则 使用了通配符 ?,并且指定了上界,扩充了参数的范围,同时也导致不能确定类型。因此,可以使用 Object 接收数据。

总结起来:

  1. 泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩 充参数的范围.
  2. 或者我们可以这样理解:泛型T就像是个变量,等着你将来传一个具体的类型,而通配符则是一种规定, 规定你能传哪些参数

2.通配符上界

边界使得你可以在用于泛型的参数类型上设置限制条件。尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个更重要的效果是你可以按照自己的边界类型来调用方法。

语法:

<? extends 上界>

这里把T改成?了

<? extends Number>//可以传入的实参类型是Number或者Number的子类
// 这里指定了上界,可以传入类型实参是 Number 子类的任意类型的 MyArrayListpublicstaticvoidprintAll(MyArrayList<?extendsNumber> list){...}// 以下调用都是正确的
printAll(newMyArrayList<Integer>());printAll(newMyArrayList<Double>());printAll(newMyArrayList<Number>());// 以下调用是编译错误的printAll(newMyArrayList<String>());printAll(newMyArrayList<Object>());

假设有如下关系:

Animal//父类
Cat extendsAnimal//Cat继承 父类
Dog extendsAnimal//Dog 继承父类

然后我们根据以上的关系,写一个方法,打印一个存储了Animal或者Animal子类的list。

publicstaticvoidprint(List<Animal> list){}

但是如果这样写,会存在问题,因为print的参数类型是 List list ,就不能接收 List list 。

因此,我们采用下面的解决方案:

publicstatic<T extendsAnimal>voidprint2(List<T> list){for(T animal : list){
System.out.println(animal);}}

此时T类型是Animal的子类或者自己,该方法可以实现。

除此之外,我们还可以采用通配符的形式实现:

publicstaticvoidprint3(List<?extendsAnimal> list){for(Animal ani : list){
System.out.println(ani);//调用谁的toString 方法?}}

不过这里有一个问题,就是我们对这两者都设置了上界,当打印不同的对象时调用谁的toString 方法?

答案:

  1. 对于泛型实现的print2方法, 对T进行了限制,只能是Animal的子类. 比如:传入Cat,那么类型就是Cat
  2. 对于通配符实现的print3方法,首先不用再static后使用尖括号,其次相当于对Animal进行了规定,允许你传入Animal 的子类。具体哪个子类,此时并不清楚. 比如:传入了Cat,实际上声明的类型是Animal,使用多态才能调用Cat的toString方法

通配符的上界-父子类关系:
通配符的上界是支持如下的父子类关系的,而泛型的上界不支持

/需要使用通配符来确定父子类型
MyArrayList<? extends Number> 是 MyArrayList 或者 MyArrayList的父类类型 MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型

这里还有个问题:对于下面这个代码,是否可以对这个List进行写入?

ArrayList<Integer> arrayList1 =newArrayList<>();
ArrayList<Double> arrayList2 =newArrayList<>();
List<?extendsNumber> list = arrayList1;
list.add(1,1);//1.这里是否可以进行写入?
Number a = list.get(0);//2.是否可以通过
Integer i = list.get(0);//3.是否可以?

答案:不可以
因为list可以引用arrayList1,或者arrayList2。

  1. 报错,此时list的引用的子类对象有很多,在添加的时候,任何子类型都可以,因为不能确定list所持有的对象具体是什么,为了安全,java不让这样进行添加操作。
  2. 可以通过
  3. 编译错误,因为只能确定是Number子类,但是不能确定list所持有的对象具体是什么

因此这里可以得出一个重要结论:

  • 由于不能确定类所持有的对象具体是什么,因此通配符上界可以读取数据,但是并不适合写入数据。

3.通配符下界

泛型没有下界,而通配符有下界

语法是这样的:

<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

一个例子:

// 可以传入类型实参是 Integer 父类的任意类型的 MyArrayListpublicstaticvoidprintAll(MyArrayList<?super Integer> list){...}// 以下调用都是正确的
printAll(newMyArrayList<Integer>());printAll(newMyArrayList<Number>());printAll(newMyArrayList<Object>());// 以下调用是编译错误的printAll(newMyArrayList<String>());printAll(newMyArrayList<Double>());
MyArrayList<?super Integer> 是 MyArrayList<Integer>的父类类型
MyArrayList<?> 是 MyArrayList<?super Integer>的父类类型

结论:
通配符下界与上界对传入类的规定是相反的,也就是规定一个泛型类只能传入下界的这个类类型或者这个类的父类类型。

通配符下界-父子类关系

MyArrayList<?super Integer> 是 MyArrayList<Integer>的父类类型
MyArrayList<?> 是 MyArrayList<?super Integer>的父类类型

我们来思考一个问题:是否可以对下面这个List进行读取?

ArrayList<?super Person> list =newArrayList<Person>();//ArrayList<? super Person> list2 = new ArrayList<Student>();//编译报错,list2只能引用Person或者Person父类类型的
list
list.add(newPerson());//添加元素时,只要添加的元素的类型是Person或者Person的子类就可以
list.add(newStudent());
Student s = list.get(0);//error
Object s = list.get(0);//可以

答案不可以!
因为添加元素的时候,我们知道list引用的对象肯定是Person或者Person的父类的集合,我们能够确定此时存储的元素的最小粒度比Person小的都可以。但是,你读取的时候,你知道是读取到的是哪个子类吗?


最后

泛型类的语法相对来说比较难,这里我们以能看得懂代码为主,也就是我们抱着以能阅读 java 集合源码为目标来学习泛型。写完这篇博客,不容易,真不容易,确实难写,害。希望老铁们能点个赞吧!

标签: java 数据结构 list

本文转载自: https://blog.csdn.net/weixin_46913665/article/details/123202057
版权归原作者 波风张三 所有, 如有侵权,请联系我们删除。

“【Java数据结构】玩转泛型-泛型的手术刀剖析”的评论:

还没有评论