数据结构之泛型总结

目录

一、泛型类的引出

二、泛型类的定义和使用

三、泛型的编译

四、泛型的上界

五、泛型方法

六、通配符

1.什么是通配符:

2.通配符可以解决的泛型问题:

3.通配符的上界

4.通配符的下界


一、泛型类的引出

1.什么是泛型

一般的类和方法,只能使用具体的类型 : 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。泛型是在JDK1.5 引入的新的语法,通俗讲,泛型: 就是适用于许多类型 。从代码上讲,就是对类型实现了参数化。
2. 对于泛型的引入和初步认识,我在“数据结构之List”中已经总结过,这里不再重复啰嗦
但是我们要知道一件事情: 泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译 器去做检查。 此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

二、泛型类的定义和使用

1.泛型类的定义

class 泛型类名称<类型形参列表> { 
    // 这里可以使用类型参数 
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ { 
    // 这里可以使用类型参数 
}

举个例子:

class MyArray {
    public T[] array = (T[])new Object[10];//这样写也不好,我们一会儿进行解释
    public T getPos(int pos) { 
        return this.array[pos]; 
    }
    public void setVal(int pos,T val) { 
        this.array[pos] = val; 
    } 
}
public class TestDemo {
    public static void main(String[] args) { 
        MyArray myArray = new MyArray<>();//第12行代码
        myArray.setVal(0,10);
        myArray.setVal(1,12); 
        int ret = myArray.getPos(1);
        System.out.println(ret);
        //myArray.setVal(2,"bit");//代码编译报错,此时因为在注释2处指定类当前的类型,此时在注释4处,编译器会在存放元素的时候帮助我们进行类型检查。
    } 
}

 (1)类名后的 代表占位符,表示当前类是一个泛型类,类型形参一般使用一个大写字母表示,常用的名称有:

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

(2)不能new泛型类型的数组

即:T[] ts = new T[5];//error

(3)第12行代码处:类型后加入 指定当前类型。泛型只能接受类,所有的基本数据类型必须使用包装类!所以error!

2.泛型类的使用

泛型类<类型实参> 变量名; // 定义一个泛型类引用 

new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

注意:当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写,如:

MyArray list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 String

3.裸类型(Raw Type)

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型,如:
MyArray list = new MyArray();

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

三、泛型的编译

在Powershell窗口中查看“二”中的代码是如何编译的,如图所示:

数据结构之泛型总结_第1张图片

 通过命令:javap -c 查看字节码文件,所有的T都是Object

在编译的过程当中,将所有的 T 替换为 Object 这种机制,我们称为: 擦除机制 。 Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。(即泛型只存在于编译的时候)

那么现在问题来了,为什么T[] ts = new T[5]; 是不对的,编译的时候替换为Object,不是相当于Object[] ts = new Object[5]吗?

如下代码所示:

class MyArray {
    //public T[] objects = new T[10];//error   
    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 T[] getArray(){
        return objects;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray<>();
        String[] ret = myArray.getArray();//第20行代码
    }
}
编译并运行该代码,输出如下:(编译没有报错,运行时报错了)

 数组和泛型之间的一个重要区别是它们如何强制执行类型检查。具体来说,数组在运行时存储和检查类型信息。然而,泛型在编译时检查类型错误。

也就是说返回的 Object 数组里面,可能存放的是任何的数据类型,可能是 String ,可能是 Person ,运行的时候,直接转给String类型的数组,编译器认为是不安全的。(所以可以把第20行的String[]换成Object[])
我们可以通过反射来new一个泛型数组,我会在后续博客中总结,这里简单的写一下如何new这么的一个泛型数组
import java.lang.reflect.Array;
class MyArray {
    public T[] array;
    public MyArray() {}

    public MyArray(Class clazz, int capacity) { 
        array = (T[])Array.newInstance(clazz, capacity); 
    }
    public T getPos(int pos) { 
        return this.array[pos]; 
    }
    public void setVal(int pos,T val) { 
        this.array[pos] = val; 
    }
    public T[] getArray() { 
        return array; 
    } 
}
public static void main(String[] args) { 
    MyArray myArray1 = new MyArray<>(Integer.class,10); 
    Integer[] integers = myArray1.getArray(); 
}

四、泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
class 泛型类名称<类型形参 extends 类型边界> {
    ...
}

例如:

class MyArray {//T只能是Number或者Number的子类
    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 T[] getArray(){
        return objects;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray<>();//第18行代码
        MyArray myArray2 = new MyArray<>();
    }
}

第18行代码从编译开始就出错了,因为String 不是 Number 的子类型,但是Integer是。

注意:

(1)泛型只有上界,没有下界

(2)没有指定边界时,默认边界为Object

class MyArray{//默认边界为Object
......
}

一个比较复杂的例子:写一个泛型类,求出数组当中的最大值

class Alg>{//此时传入的T一定要实现该接口   此处不是继承
    public T findMax(T[] array){
        T max = array[0];
        for(int i = 1; i < array.length; i++){
            if(max.compareTo(array[i]) < 0){//①max<=比较
                                            //②也不能用equals方法,它只能比较true和false,比较不了大小关系。两个引用比较大小,要用CompareTo方法
                max = array[i];
            }
        }
        return max;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Alg alg = new Alg<>();
        Integer[] array = {1,22,3,4};
        System.out.println(alg.findMax(array));
    }
}

编译并运行该代码,输出如下:

22

注意Integer是实现了Comparable<>接口的,如图所示:

该代码有一个不好的地方,我们每次想调用findMax方法时,必须要new一个对象;我们对其进行优化一下

五、泛型方法

1.

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

优化如下:

class Alg2/*>*/{// "/* */"的这一部分写和不写都是一样的
    public static> T findMax(T[] array){//我们发现当我们加一个static后,整个代码都报错了,这是因为加了static后,我们的方法不再依赖
                                       //于对象,我们在使用该方法时不再new对象,但是我们的泛型传参在new对象时传参,所以如果加了一个static的
                                       //的话,相当于没有传参;因此我们在static处加上,但是要用compareTo方法,还是要实现Comparable接口
        T max = array[0];
        for(int i = 1; i < array.length; i++){
            if(max.compareTo(array[i]) < 0){
                max = array[i];
            }
        }
        return max;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Integer[] array = {1,22,3,4};
        System.out.println(alg./**/findMax(array));//"/* */"的这一部分写和不写都是一样的,它会通过array的类型来推导方法中的T是什么类型【类型推导】
    }
}

2.泛型中的父子类关系

public class MyArrayList { 
    ... 
} 
// MyArrayList Object不是 MyArrayList 中的Number的父类类型 
// MyArrayList Number也不是 MyArrayList 的Integer父类类型 
  

因为在编译的时候它们都被擦除了

举个例子:

class Alg>{
    public T findMax(T[] array){
        T max = array[0];
        for(int i = 1; i < array.length; i++){
            if(max.compareTo(array[i]) < 0){
                max = array[i];
            }
        }
        return max;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Alg alg1 = new Alg<>();
        Alg alg2 = new Alg<>();
        System.out.println(alg1);
        System.out.println(alg2);
    }
}

编译并运行该代码,输出如下:

我们发现打印的内容中不存在<>,这说明它们被擦除了

六、通配符

1.什么是通配符:

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

2.通配符可以解决的泛型问题:

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

泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围( 或者我们可以这样理解:泛型T就像是个变量,等着你将来传一个具体的类型,而通配符则是一种规定, 规定你能传哪些参数 ),如下代码所示:
class Alg3{
    public static void print(ArrayList list){
        for(T x:list){
            System.out.println(x);
        }
    }
    //此时代码的参数是T,此时的T一定是将来指定的一个泛型参数
    public static void print2(ArrayList list){
        for(Object x:list){
            System.out.println(x);
        }
    }
    //代码中使用了统配符,和代码1相比,此时传入printList2的,具体是什么数据类型,我们是不清楚的。这就是通配符。
}

3.通配符的上界

(Ⅰ)语法

与泛型上界类似,;如://可以传入的实参类型是Number或者Number的子类

举个例子:假设有如下关系:

Animal
Cat extends Animal
Dog extends Animal   根据该关系,写一个方法,打印一个存储了 Animal 或者 Animal 子类的 list
(1)
public static void print(List list) { 
    ......
}
这样不可以解决问题,因为 print 的参数类型是 List list ,就不能接收 List list
(2)
public static  void print2(List list) { 
    for (T animal : list) { 
        System.out.println(animal); 
    } 
}
此时 T 类型是 Animal 的子类或者自己。该方法可以实现
(3)
public static void print3(List list) { 
    for (Animal ani : list) { 
        System.out.println(ani);//传过来是谁,就调用谁的toString方法 (发生了向上转型)
    } 
}
通配符实现,该方法也可以达到效果

区别如下:

①对于泛型实现的print2方法, 对T进行了限制,只能是Animal的子类 比如:传入Cat,那么类型就是Cat

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

(Ⅱ)通配符上界的父子类关系

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

(Ⅲ)通配符的上界的特点

通配符的上界不适合写入数据

public class TestDemo {
    public static void main(String[] args) {
        ArrayList arrayList1 = new ArrayList<>();
        ArrayList arrayList2 = new ArrayList<>();
        List list = arrayList1;
        list.add(0,1);
        list.add(1,10.9);
    }
}

这样子写编译时是不会通过的,因为此时list的引用的子类对象有很多,再添加的时候,任何子类型都可以,为了安全,java不让这样进行添加操作。

(添加任何类型的数据都不可以,无法确定到底是哪种类型。)

但是通配符的上界适合读取数据:

public class TestDemo {
    public static void main(String[] args) {
        ArrayList arrayList1 = new ArrayList<>();
        ArrayList arrayList2 = new ArrayList<>();
        List list = arrayList1;
        Number o = list.get(0);
        //Integer i = list.get(1);//error     类型太多,不一定是Integer(你怎么知道,获取的就是Integer呢?),所以读取时一定要用Number类型来接收
    }
}

4.通配符的下界

(Ⅰ)语法

 

//代表 可以传入的实参的类型是Integer或者Integer的父类类型

(Ⅱ)通配符下界的父子类关系

MyArrayList 是 MyArrayList的父类类型
MyArrayList 是 MyArrayList的父类类型
(Ⅲ)通配符的下界的特点
public class TestDemo {
    public static void main(String[] args) {
        ArrayList arrayList1 = new ArrayList<>();
        //ArrayList arrayList2 = new ArrayList<>(student);//error  arrayList2只能引用Person或者Person父类类型的list
        arrayList1.add(new Person());//为添加元素的时候,我们知道list引用的对象肯定是Person或者Person的父类的集合,我们能够确定此时存储元素的最小粒度比Person小的都可以。
        //Student s = arrayList1.get(0);//error   读取的时候,我们不知道是读取到的是哪个子类
        Object s2 = arrayList1.get(0);//可以
        //Person s3 = arrayList1.get(0);//error
    }
}

通配符的下界适合写入数据,不适合读取数据

你可能感兴趣的:(JavaSE,java)