java数据结构——泛型篇

目录

一、泛型的概念

(一)什么是泛型

(二)为何引入泛型

(三)泛型在集合中的使用

二、泛型的使用(以下用代码进行讲述)

(一)泛型类

(二)泛型方法

(1)普通泛型方法

(2)静态泛型方法

(三)泛型接口

(四)泛型上界及其擦除机制

(1)泛型上界

(2)java泛型擦除机制

三、通配符

(一)通配符的上界

 (二)通配符的下界

 四、泛型总结    


一、泛型的概念

(一)什么是泛型

    在类和方法的使用中,一般只能使用具体的类型,基本类型亦或是自定义的类。但想要其适用于多种类型的代码编写。因此产生了泛型来处理这种刻板的限制对代码的束缚。简单的来讲,泛型就是适用于很多类型。从代码上讲,就是对类型实现了参数化。

(二)为何引入泛型

    想要在数据结构集合中适用于多种数据类型,但又不是所有的类型都能拿来存储,而是选取自己想要的数据类型进行存储,因此引入泛型,便能很好地处理这种情况。同时泛型的引入,能有效的检查编译中存在的错误数据,更好的体现了java语言的安全性。

public class MyArrayList {
    private final int capacity = 10;
    private int usedSize;

    //Object[]数组能存储任何类型的元素
    private Object[] elem; 
    public MyArrayList() {
        elem = new Object[capacity];
    }

    //增添元素
    public void add(Object val) {
        if(usedSize != capacity) {
            elem[usedSize++] = val;
        } else {
            System.out.println("容量溢出");
        }
    }

    //获取元素
    public Object get(int pos) {
        return elem[pos];
    }
    
    public static void main(String[] args) {

        //在自定义的集合中能够存储任何类型的元素,显然不是程序员想要的结果
        MyArrayList myArrayList = new MyArrayList();

        myArrayList.add("a");//字符串型
        myArrayList.add(1);//整型
        myArrayList.add(2.0);//double型

        //每次读取元素的时候都要强制类型转换
        String s = (String) myArrayList.get(0);
        Integer a = (Integer) myArrayList.get(1);
    }
}

(三)泛型在集合中的使用

语法:

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

class ClassName {}

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

class ClassName extends ParentClass {
// 可以只使用部分类型参数
}

    为使上述问题得到解决,使用泛型对该自定义集合类进行改进。

public class MyArrayList1 {
    private final int capacity = 10;
    private int usedSize;

    private T[] elem;    
    public MyArrayList1() {
        //报错,不能创建一个泛型数组
        //elem = new T[capacity];

        //不报错,但是存在问题,此问题留到下文讲擦除机制时讲
        elem = (T[]) new Object[capacity];
    }

    //真正正确的创建泛型数组的方式--运用反射
    public MyArrayList1(Class clazz,int capacity) {
        elem = (T[]) Array.newInstance(clazz,capacity);
    }

    //增添数据
    public void add(T val) {
        if(usedSize != capacity) {
            elem[usedSize++] = val;
        } else {
            System.out.println("容量溢出");
        }
    }

    //读取数据
    public T get(int pos) {
        return elem[pos];
    }

    public static void main(String[] args) {

        //使用泛型后自动进行编译检查和类型转换
        MyArrayList1 myArrayList = new MyArrayList1();
        myArrayList.add("a");

        //报错
        //myArrayList.add(1);
        //myArrayList.add(2.0);

        //读取元素时不需要强制类型转换
        String s =  myArrayList.get(0);
    }
}

注意:

泛型实现数据类型参数化,传入的数据类型必须是基本数据类型的包装类。

二、泛型的使用(以下用代码进行讲述)

(一)泛型类

语法:

泛型类 < 类型实参 > 变量名 ; // 定义一个泛型类引用
new 泛型类 < 类型实参 > ( 构造方法实参 ); // 实例化一个泛型类对象

代码:

//指定一个泛型类引用泛型类的对象

ArrayList list = new ArrayList();//后面的String可以省略

//自定义一个交换数据类
class Swap {}

public static void main(String[] args) {
        Swap swap = new Swap<>();
}

(二)泛型方法

(1)普通泛型方法

语法:

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

代码:

//定义交换两个数据值的类

class Swap {

    //普通泛型方法

    //(形式一)
    public void swap(T[] array, int i, int j) {
        T tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    //(形式二)
    public  void swap(T[] array, int i, int j) {
        T tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

}

(2)静态泛型方法

语法:

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

代码:

//泛型的静态方法
class Swap {

    public static void swap(T[] array, int i, int j) {
        T tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

}

注:泛型静态方法不依赖于对象,所以不在class类后面添加泛型

(三)泛型接口

语法:

interface 泛型接口名<类型形参列表> {....}

代码:

//在此用Comparable接口来举例

//此接口适用于任何类型的比较

interface Comparable {

        public int compareTo(T o);

}

(四)泛型上界及其擦除机制

(1)泛型上界

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

语法:

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

代码:

//泛型上界为Number
class MyArray {

    public static void main(String[] args) {

        //创建对象时只能是Number或Number的子类
        MyArray myArray1 = new MyArray();
        MyArray myArray2 = new MyArray();

        //报错,String不是Number的子类
        MyArray myArray = new MyArray();
    }

}

特殊实例:

//自定义一个比较类

//泛型方法
class Alg1> {

    public T findMax(T[] array) {
        T max = array[0];
        for (T elem: array) {
            if(elem.compareTo(max) > 0)
                max = elem;
        }
        return max;
    }

}

注意:

    该代码块的上界是Comparable接口,当构造该类对象时,必须是实现了Comparable接口,基本数据类型的包装类都是实现了Comparable接口。

(2)java泛型擦除机制

    擦除机制是Java5用来实现泛型的技术。一般来说,在运行时阶段,Java编译器先执行类型检查,然后执行擦除或删除泛型信息。而具体化的泛型,与此正好相反。基于具体化泛型系统的类实现在运行时作为顶级实体,它在运行时保留了类型参数,而这给基于类型和反射性的语言提供了确定的操作。

通过查看编译生成的字节码文件,更好的理解java泛型擦除机制

(a)未定义上界类型,最终擦除到Object

源代码:

java数据结构——泛型篇_第1张图片

 生成的字节码文件:

java数据结构——泛型篇_第2张图片

 (b)定义上界类型为Number,最终擦除到Number

源代码:

java数据结构——泛型篇_第3张图片

 生成的字节码文件:

java数据结构——泛型篇_第4张图片

注意: 

不能创建泛型数组,会存在隐患,以下用代码进行演示。

class MyArray {

    private final int capacity = 10;
    private int usedSize;

    private T[] elem;   
 
    public MyArrayList1() {
        //报错,不能创建一个泛型数组
        //elem = new T[capacity];

        //不报错,但是存在问题,编译的时候,替换为Object[]
        elem = (T[]) new Object[capacity];
    }

    //真正正确的创建泛型数组的方式--运用反射
    public MyArrayList1(Class clazz,int capacity) {
        elem = (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;
    }

    //编译时返回的类型是Object[]
    public T[] getArray() {
        return array;
    }


    public static void main(String[] args) {
        MyArray myArray1 = new MyArray<>();

        //使用getArray()方法,返回的是bject[]类型的数组,不是不能直接用Integer[]来接受的
        //数组和泛型的区别是数组是在运行时检查错误,而泛型是在编译时检查错误,为此该语句段未报错
        Integer[] strings = myArray1.getArray();
    }
}

运行结果:

 问题原因:

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

    返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Double等等类型,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的。

    另外,即使对返回的数组进行强制类型转换为(Integer[])也不能改变其内部元素是其他的数据类型,运行时程序任然会报错。

正确的处理方法是运用反射来创建数组

public class MyArrayList1 {
    private final int capacity = 10;
    private int usedSize;

    private T[] elem;    

    //运用反射创建泛型数组
    public MyArrayList1(Class clazz,int capacity) {
        elem = (T[]) Array.newInstance(clazz,capacity);
    }
}

三、通配符

    通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Cat 是Animal 的子类,那么 List 也应 该是 List 的子类。但是经过泛型的擦除机制,最终两个都擦除到object,所以泛型是不支持这样的父子类关系的。

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

(一)通配符的上界

语法:

extends 上界 >
extends Animal > // 可以传入的实参类型是Animal或者Animal的子类

①搞清父子类关系:

extends Animal> 是 的父类

extends Animal>的父类

//构成父子类关系
class Animal {
    
}
class Dog extends Animal {
    
}
class Dubianquan extends Dog {
    
}

②父类引用子类对象:

public class Zoon {

    public static void main(String[] args) {

        // 是 的父类
        ArrayList arrayList1 = new ArrayList();
        ArrayList arrayList2 = new ArrayList();
        ArrayList arrayList3 = new ArrayList();

        // 是 的父类
        ArrayList arrayList = arrayList1;
    }
}

③注意:

    通配符的上界只适合于读取数据,不适用于写入数据。因为通配符上界引用的是的对象,但是不能确定到底是哪个子类,不能存储确定的子类类型,java在编译时会自动查错。

class Animal {
    public String toString() {
        return "Animal :>";
    }
}
class Cat extends Animal {
    @Override
    public String toString() {
        return "Cat :>";
    }
}
class Dog extends Animal {
    @Override
    public String toString() {
        return "Dog :>";
    }
}

public class Zoon {

    //通配符的上界是不适用于写入的,只适合于读取
    public static void main0(String[] args) {

        ArrayList arrayList = new ArrayList<>();

        //报错
        //arrayList.add(new Animal());
        //arrayList.add(new Dog());

        //但是可以读取数据,用泛型的上界来读取子类的数据,属于向上转型
        Animal animal = arrayList.get(0);

        //也可以用Object,因为他是所有类的父类
        Object o = arrayList.get(0);
    }

}

 (二)通配符的下界

语法:

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

①搞清父子类关系:

super Animal> 是 的父类

super Animal>的父类

//构成父子类关系
class Animal {
    
}
class Dog extends Animal {
    
}
class Dubianquan extends Dog {
    
}

②父类引用子类对象:

public class Zoon {

    public static void main(String[] args) {

        // 下界是Dog,引用的是的对象
        //因为的上界没有限制,所以一直可以到Object。
        //因此是<任一Dog及其父类>的父类
        //符合父类引用子类对象的规则
        ArrayList arrayList1 = new ArrayList();
        ArrayList arrayList2 = new ArrayList();

        //报错,下界是Dog,不能引用下界以下的子类
//        ArrayList arrayList3 = new ArrayList();

        // 是  的父类
        ArrayList arrayList = arrayList1;

    }
}

③注意:

     通配符的下界只适合于写入数据,不适用于读取数据。因为通配符下界引用的是的对象,但是不能确定读取到的是哪个父类类型,所以引用的类型不能确定,也就不能读取,但是Object是所有类的父类,非要读取的话可以用Object来接受。因为下界存储的内容都是下界以下的数据类型,所以适合写入数据。

class Animal {
    @Override
    public String toString() {
        return "Animal{}";
    }
}
class Dog extends Animal {
    @Override
    public String toString() {
        return "Dog{}";
    }
}
class Dubianquan extends Dog {
    @Override
    public String toString() {
        return "Dubianquan{}";
    }
}

public class Zoon {

    public static void main(String[] args) {
        ArrayList arrayList1 = new ArrayList();
        
        
        ArrayList arrayList2 = new ArrayList();

        //报错,存储的数据是Dog及其子类的数据类型
//        arrayList2.add(new Animal());
        
        //添加的元素 是Dog或者Dog的子类
        arrayList2.add(new Dog());
        arrayList2.add(new Dubianquan());
        
        //ArrayList arrayList2引用的是Dog及其父类对象
        //编译器会考虑到ArrayList arrayList2引用的对象如果是 new ArrayList();
        //那么它存储的数据可能会有Animal类型的数据,那么就不能用Dog来接受Animal类型的数据。
//        Dog dog = arrayList2.get(0);//报错
        
//        Animal animal = arrayList2.get(0);//报错 原理同上

        //但是Object是所有类的父类,可以使用其来读取数据
        Object o = arrayList2.get(0);
    }
}

④拓展(运用通配符上下界存储并读取数据,实现多态)

class Animal {
    @Override
    public String toString() {
        return "Animal{}";
    }
}
class Dog extends Animal {
    @Override
    public String toString() {
        return "Dog{}";
    }
}
class Dubianquan extends Dog {
    @Override
    public String toString() {
        return "Dubianquan{}";
    }
}

public class Zoon {


    public static void main(String[] args) {
        //通配符下界存储数据
        ArrayList arrayList1 = new ArrayList();
        arrayList1.add(new Dog());
        arrayList1.add(new Dubianquan());

        //要用通配符下界读取数据,就要用Object来接受
        for (Object O: arrayList1) {
            System.out.println(O);
        }

        //利用通配符上界来读取数据
        ArrayList arrayList = (ArrayList) arrayList1;

        //因为上述存储的数据类型都是Animal的子类,所以可以用Animal来接受
        for (Animal a: arrayList) {
            System.out.println(a);
        }
    }
}

结果显示:

java数据结构——泛型篇_第5张图片

 四、泛型总结    

①归根结底java语言的安全性能:使得程序具有更好的可读性和安全性。泛型是编译时检查错误,java的数组是运行时检查错误。因此不能用泛型直接创建数组,要用反射来创建泛型数组。

②泛型有上界但是没有下界

③类型构成的父子类关系,对泛型来说,泛型因为有擦除机制,所以没有父子类关系。而通配符就是解决此类问题。

④搞清通配符上下界的父子类关系,通配符上界适合读取数据,通配符下界适合写入数据。通配符的解决了泛型不能构成父子类的关系,因此可以通过泛型来实现多态读取和写入数据。

你可能感兴趣的:(java,数据结构)