数据结构之泛型

在这里插入图片描述

写在前面:
博主主页:戳一戳,欢迎大佬指点!
博主秋秋:QQ:1477649017 欢迎志同道合的朋友一起加油喔
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个小菜鸟嘿嘿
-----------------------------谢谢你这么帅气美丽还给我点赞!比个心-----------------------------

在这里插入图片描述


数据结构之泛型

  • 一,什么是泛型
  • 二,引出泛型
  • 三,泛型的使用
    • 3.1,语法
    • 3.2,裸类型
  • 四,泛型的编译
  • 五,泛型的上界
    • 5.1,语法及示例
    • 5.2,特殊的上界
  • 六,泛型的方法
    • 6.1,定义语法
    • 6.2,定义静态的泛型方法
  • 七,通配符
    • 7.1,通配符存在的目的
    • 7.2,通配符的上界
    • 7.3,通配符的下界
  • 八,包装类
    • 8.1,基本数据类型及其包装类
    • 8.2,装箱和拆箱


一,什么是泛型

对于一般的类和方法,其参数就只能是特定的类型,那如果要实现同时适用于多种类型的代码,那么这种束缚的影响就会很大。所以,就有了泛型,泛型就是适用于很多很多类型,它的核心就在于将类型实现了参数化。


二,引出泛型

问题:假设我们要设计一个类,这个类的实际成员是一个数组,要求这个数组中能够放任何类型的数据,可以设置和返回下标的值,请问你如何实现?

首先,既然要实现存放任意的值,那我们这个数组的类型只能是Object类型,则代码如下:

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

    public Object getArray(int pos) {
        return array[pos];
    }

    public void setArray(int pos,Object num) {
        this.array[pos] = num;
    }
}
public class TestDemo220619 {
    public static void main(String[] args) {
        MyArray ay = new MyArray();
        ay.setArray(0,100);
        ay.setArray(1,"hello");

        String s = (String)ay.getArray(1);//必须要强转
        System.out.println(s);
    }
}

但是我们写完会发现一个问题,就是当我们要返回某个下标的值的时候,我们需要进行强转,因为原数组元素是Object类型的,只是因为这是我们自己写的,我知道这个下标下放的是什么类型的数据,那如果是在内存中,或者用户输入,我怎么知道某个下标的值的类型,那在返回值的接受的时候你怎么进行强转,所以这就是一个问题,这里的解决方法就是需要用到泛型。


对于上面的问题,主要就是虽然一个数组里面确实可以放任何类型的数据,但是每一次它有可能放的都不一样,导致数组里面的元素类型种类太多。而对于泛型,我们将类型作为参数后,我指定的容器对象里面它确实能够放任何类型,这个类型是根据你传入的参数决定的,并且一旦这个类型一确定,那么这个容器里面就只能放这一种类型。做到了可以接收任何类型的同时,每次又只接纳一种。


上述代码改进:

class MyArray<T>{//T是一个占位符,标识当前类是一个泛型类
    //<>里面指定了类型,那么这个时候就确定了这个类里面就只能放这个类型的数据
    public T[] array = (T[])new Object[10];

    public T getArray(int pos) {
        return array[pos];
    }

    public void setArray(int pos,T num) {
        this.array[pos] = num;
    }
}
public class TestDemo220620 {
    public static void main(String[] args) {
//        现在数组里面只能放String类型
        MyArray<String> ay = new MyArray<String>();
        ay.setArray(0,"abcd");
        ay.setArray(1,"hello");
        
//        现在数组里面只能放int类型
        MyArray<Integer> ay1 = new MyArray<Integer>();
        ay1.setArray(0,100);
        ay1.setArray(1,200);
    }
}

注意,<>里面传入的是类类型,字符串就是String,整形就是Integer,对于基本数据类型而言,就是其对应的包装类。


定义泛型类的时候,可以传入多个参数:

class MyArray<T,K>{
    public T[] array = (T[])new Object[10];
    public K[] array1 = (K[])new Object[10];
    public T getArray(int pos) {
        return array[pos];
    }

    public void setArray(int pos,T num) {
        this.array[pos] = num;
    }

    public K getArray1(int pos) {
        return array1[pos];
    }

    public void setArray1(int pos,K num1) {
        this.array1[pos] = num1;
    }
}
public class TestDemo220620 {
    public static void main(String[] args) {
        MyArray<String,Integer> ay = new MyArray<String,Integer>();
        ay.setArray(0,"abcd");
        String s = ay.getArray(0);
        System.out.println(s);
        
        ay.setArray1(0,100);
        int ret = ay.getArray1(0);
        System.out.println(ret);
    }
}

表示我这个类里面可以放两种数据类型,但是类里面就需要有两个容器了,因为对于一个容器而言,它要么就是T类型,要么就是K类型,这个确定的,这种情况下就是不需要像上面一样再创建一个新的类对象,然后再传入新的类型参数,就只用一个类对象就好,只不过在操作的时候对应的容器不同而已。


✅关于泛型的注意点:

1,关于泛型的占位符,没有强制要求,一般为T,E,K之类的。

2,不能new 一个泛型数组,因为你在定义的时候你new的数组的类型就是不明确的,编译没法通过。

public T[] array = (T[])new Object[10];
//public T[] array = new T[10];这是错误的

三,泛型的使用

3.1,语法

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

✅注意:泛型在传参的时候,只能接收类类型,所有的基本数据类型必须使用包装类。


【类型推导:】

//完整的写法是:
MyArray<String> ay = new MyArray<String>();
//类型推导的写法
MyArray<String> ay = new MyArray<>();//后面的<>里面可以不用写

3.2,裸类型

裸类型也是一个泛型,但是这个泛型它没有类型实参,其实也就是相当于我们之前的Object类型,就是这个类里面的容器中任何类型的数据都可以放,同时可以放多种不同的·类型。

class MyArray<T>{//泛型类
    ......
}

MyArray ay = new MyArray();
ay.setArray(0,"abcd");
ay.setArray(1,100);

注意,我们自己不要去使用裸类型,裸类型的存在是为了兼容老版本中的API保留的机制下面的类型擦除部分。


四,泛型的编译

通过查看我们的字节码文件,可以看到我们的占位符T都会被替换成Object。

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

这其实就是我们的擦除机制,把我们的泛型T,在编译的时候,擦除为了Object。Java的泛型也只是在编译时期的实现的,在编译时期被擦除后,运行时就不再有泛型这个概念。


那既然说将泛型T会替换成Object,那么就会有几个问题了?

我们之前说,不能new 一个泛型数组(但是可以声明),也就是 T[] array = new T[10],那么既然都会被替换成Object,那不就是定义了一个Object类型的数组吗,为什么不行呢?因为这就违反了泛型设计的初衷,你如果能定义泛型数组,那么擦除直接不就直接变成了Object类型的数组了吗,那岂不是又是任何类型的数据都可以放,如何还能确保数组可以放很多类型但是每次的类型只能一致的这个点呢。


public T[] array = (T[])new Object[10];

这是我们最初的定义有关泛型的数组的方法,虽然说大多数的情况下这个没有问题,但是实际上也是不好的,假设我们要返回这个数组。

class MyArray<T>{
    public T[] array = (T[])new Object[10];
    public T getArray(int pos) {
        return array[pos];
    }

    public void setArray(int pos,T num) {
        this.array[pos] = num;
    }

    public T[] getArrays(){
        return this.array;
    }
}
public class TestDemo220621 {
    public static void main(String[] args) {
        MyArray<String> ay = new MyArray<String>();
        ay.setArray(0,"abcd");
        String[] tmp = ay.getArrays();

    }
}

程序运行结果:

在这里插入图片描述


按理说,我们利用泛型修饰后,这个array数组里面就只是一个String类型的数组,那么返回之后用一个String类型的数组接收不会有任何的问题的。但实际上因为擦除机制的存在,其类型其实被擦除为了Object类型,只是里面都是放的字符串,只不过你返回出去,编译器认为这是一个Object类型的数组,里面可能会存放着各种类型的数组,你将他赋值给一个String类型的数组,这是不安全的,所以它给你报了一个数组的类型转换异常。


所以,最为正确的定义数组的方法如下:

class Myarray2<T>{
    public T[] array;//只进行声明
    public Myarray2(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);//clazz是你的数组的类型,后面是数组的大小
    }
    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 class TestDemo220621 {
    public static void main(String[] args) {
        Myarray2<String> ay = new Myarray2<String>(String.class,10);
        ay.setVal(0,"abcd");
        String[] tmp = ay.getArray();
        System.out.println(Arrays.toString(tmp));
    }
}

程序运行截图:

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


现在你再返回数组接收就没有任何的问题了,通过这种方式定义出来的数组它的类型就是指定了的,不再是Object类型的数组。我们泛型指定类型其实就是两个作用:

1,在编译的时候会进行类型检查,保证容器中只能存放指定类型的数据

2,可以使得我们在取元素的时候可以不用进行类型转换。


五,泛型的上界

我们在定义泛型的时候,需要对于传入的类型做出一定的约束,所以就有了泛型的上届来作为约束条件。注意,存在泛型的上界,但没有泛型的下界这种说法。

5.1,语法及示例

class 泛型类名称<类型形参 extends 类型边界> {
...
}
//注意这里的extends不是继承的意思

示例:

class Myarray2<T extends Number>{
    public T[] array2;//只进行声明
    public Myarray2(Class<T> clazz, int capacity) {
        array2 = (T[]) Array.newInstance(clazz, capacity);//clazz是你的数组的类型,后面是数组的大小
    }
    public T getPos(int pos) {
        return this.array2[pos];
    }
    public void setVal(int pos,T val) {
        this.array2[pos] = val;
    }

    public T[] getArray() {
        return array2;
    }
}


public class TestDemo220622 {
    public static void main(String[] args) {
        Myarray2<Integer> ay = new Myarray2<Integer>(Integer.class, 10);
        ay.setVal(0, 10);

        Myarray2<Double> ay1 = new Myarray2<Double>(Double.class,10);
        ay1.setVal(0,1.68);
        
    }
}

Number是数字类,在这里规定了泛型的上界之后,你传入的类类型参数就只能是Number类及其子类,不能是其他类,比如现在你再传入String就是错误的。当我们不写extends后面的内容是,我们默认的上界其实就是Object。


5.2,特殊的上界

假设现在我们要定义一个方法去找出一个泛型数组里面的最大值,如下:

//写一个函数,可以进行一个泛型数组找出最大值
class Alg<T extends Comparable<T>>{
    public T[] array;
    public Alg(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }

    public void setArray(int pos,T num) {//num是你设置的数组元素的值的引用
        this.array[pos] = num;
    }

    public T findMax(){
        T max = array[0];
        for(int i = 1;i < array.length;i++){
            if(array[i].compareTo(max) > 0){
                max = array[i];
            }
        }
        return max;
    }
}
public class TestDemo220622 {
    public static void main(String[] args) {
        Alg<Integer> alg = new Alg<Integer>(Integer.class,3);
        alg.setArray(0,10);
        alg.setArray(1,5);
        alg.setArray(2,20);
        Integer ret = alg.findMax();
        System.out.println(ret);
        
    }
}//输出结果20

可以看到,我们的上界是这样写的 T extends Comparable ,意思是只要是实现了Comparable接口的类就行,所以我们的上界不仅仅可以直接是一个类类型,还可以是这种以是否实现了某个接口为标准作为上界的判断标准。Comparable也是一个泛型接口。


注意,我们的泛型传入的参数都是类类型,也就是引用类型,所以说我们定义出来的数组也是引用类型的数组,所以我们在比较数组元素的时候,是不能直接比较大小的,只能用ComparTo方法来进行比较


六,泛型的方法

6.1,定义语法

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

6.2,定义静态的泛型方法

还是以上面的找出数组元素的最大值为例,我们如何不利用对象去调用,直接定义一个静态的泛型方法呢?

class Alg2{
    public static<T extends Comparable<T>> T findMax(T[] array){
        T max = array[0];
        for(int i = 1;i < array.length;i++){
            if(array[i].compareTo(max) > 0){
                max = array[i];
            }
        }
        return max;
    }
}
public class TestDemo220622 {
    public static void main(String[] args) {
//        这个时候就不需要定义对象了
        Integer[] array = {1,2,3,4,5};
        //Integer ret = Alg2.findMax(array);//T会根据你传入的变量进行类型的推导
        Integer ret = Alg2.<Integer>findMax(array);
        System.out.println(ret);
    }
}

主要就是注意我们定义的时候类型形参列表的写法与位置就好。也可以定义实例的泛型方法,原理是差不多的,只是调用的时候还是要依赖与对象才行。


七,通配符

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

7.1,通配符存在的目的

通配符的存在主要是为了解决泛型无法协变的问题,协变指的是假设A是B的子类,那么List 也应该是 List 的子类,但是这在泛型里面是不成立的。

class Test<T>{

}
public class TestDemo220622 {
    public static void main(String[] args) {
        Test<Integer> t1 = new Test<Integer>();
        System.out.println(t1);
        Test<String> t2 = new Test<String>();
        System.out.println(t2);
    }
}
//运行结果:
//Test@1b6d3586
//Test@4554617c

由运行的结果而言,我们的<>括号里面的内容是不参与类型的组成的,所以正是因为这一点,我们的父子类关系在泛型里面才不成立。

那为了实现协变,于是有了我们的通配符。

class Message<T>{
    private T title;

    public T getTitle() {
        return title;
    }

    public void setTitle(T title) {
        this.title = title;
    }
}

public class TestDemo {
    public static void func(Message<String> tmp){
        System.out.println(tmp.getTitle());
    }
    public static void main(String[] args) {
        Message<String> mes1 = new Message<String>();
        mes1.setTitle("helloworld~");
        func(mes1);//正常输出
        
        Message<Integer> mes2 = new Message<Integer>();
        //func(mes2);编译会出错

    }
}

我们可以看到,func(mes1)正常执行不会有任何问题,但是对于func(mes2)就会有问题了,因为mes2的类型与Messagetmp
的类型不匹配,我们之前说泛型不参与类型的组成,但是类型的检查还是会参与的,所以,这个时候就需要用到通配符了。

//将上面的代码改为
class Message<T>{
    private T title;

    public T getTitle() {
        return title;
    }

    public void setTitle(T title) {
        this.title = title;
    }
}

public class TestDemo {
    public static void func(Message<?> tmp){
        System.out.println(tmp.getTitle());
    }
    public static void main(String[] args) {
        Message<String> mes1 = new Message<String>();
        mes1.setTitle("helloworld~");
        func(mes1);

        Message<Integer> mes2 = new Message<Integer>();
        mes2.setTitle(100);
        func(mes2);

    }
}

程序运行结果:

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


public static void func(Message tmp) 其中的?就是通配符,现在tmp就可以匹配任何的泛型的类型。


✅注意:

public static void func(Message<?> tmp){
       // tmp.setTitle(100);编译出错
        System.out.println(tmp.getTitle());
}

这一点我们要注意,在运用这个通配符的方法里面,你是无法去修改泛型类里面成员的值的,因为通配符匹配任何类型,所以现在你根本就不知道可能会传入什么类型,所以你怎么能去设置它的值呢。


7.2,通配符的上界

class Food{

}
class Apple extends Food{

}
class Peach extends Food{

}

class EatFood<T>{
    private T food;

    public T getFood() {
        return food;
    }

    public void setFood(T food) {
        this.food = food;
    }
}
public class TestDemo2 {

    public static void func2(EatFood<? extends Food> tmp){
        System.out.println(tmp.getFood());
    }
    public static void main(String[] args) {
        EatFood<Apple> e1 = new EatFood<Apple>();
        e1.setFood(new Apple());
        func2(e1);

        EatFood<Peach> p1 = new EatFood<Peach>();
        p1.setFood(new Peach());
        func2(p1);

    }
}

“” 通配符的上界,表示其类型只能是 Food 及其子类才可以匹配。

我们之前说,在通配符的方法里面你不能去修改泛型类成员的值,但是我们在通配符上界里面,我们还是可以用父类的引用来接收取到的值的。通配符的上界一般用来获取元素。

public static void func2(EatFood<? extends Food> tmp){
        Food food = tmp.getFood();//用父类引用接收

        System.out.println(tmp.getFood());
}

7.3,通配符的下界

class Food{

}
class Fruit extends Food{

}
class Apple extends Fruit{

}
class Peach extends Fruit{

}

class EatFood<T>{
    private T food;

    public T getFood() {
        return food;
    }

    public void setFood(T food) {
        this.food = food;
    }
}
public class TestDemo2 {
    public static void func3(EatFood<? super Fruit> tmp){
        System.out.println(tmp.getFood());
    }
    public static void main(String[] args) {
        EatFood<Fruit> e1 = new EatFood<Fruit>();
        e1.setFood(new Fruit());
        func3(e1);

        EatFood<Food> p1 = new EatFood<Food>();
        p1.setFood(new Food());
        func3(p1);
    }
}

“” 通配符的下界,表示传入的类型是能是 Fruit及其父类才可以匹配。

在通配符的下界我们可以去修改元素。

public static void func3(EatFood<? super Fruit> tmp){
        tmp.setFood(new Apple());
        tmp.setFood(new Peach());

        System.out.println(tmp.getFood());
}

现在这是没有任何的问题的,因为Fruit是下界,那么只要是fruit的子类你去设置都不会有任何的事情,因为不管你传入的是Fruit还是Food的引用类型,都可以接收你的设置的值。一般通配符的下界用来修改值。


八,包装类

在Java中,每个基本数据类型都会对应一个包装类。

8.1,基本数据类型及其包装类

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


注意一下int以及char类型的包装类,不是简单的把首字母大写。


8.2,装箱和拆箱

【装箱:将基本数据类型转换为包装类类型】

public class TestDemo3 {
    public static void main(String[] args) {
        int ret = 10;
        Integer integer1 = 10;//自动装箱
        Integer integer2 = Integer.valueOf(ret);//显式装箱
        Integer integer3 = new Integer(ret);//显式装箱
    }
}

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


我们虽然说的是自动装箱,其实底层上还是调用的valueOf方法。


【拆箱:将包装类类型转换为基本数据类型】

public class TestDemo3 {
    public static void main(String[] args) {
        int ret = 10;
        Integer integer1 = 10;//自动装箱 ,底层调用的也是 intvalue方法

        int val1 = integer1;//自动拆箱
        System.out.println(val1);

        int val2 = integer1.intValue();//显示拆箱,拆为int类型
        System.out.println(val2);

        double val3 = integer1.doubleValue();//显示拆箱,拆为double类型
        System.out.println(val3);
    }
}

程序运行截图:

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


【面试题:】

public class TestDemo3 {
    public static void main(String[] args) {
        Integer integer1 = 127;
        Integer integer2 = 127;
        System.out.println(integer1 == integer2);//输出true

        Integer integer3 = 128;
        Integer integer4 = 128;
        System.out.println(integer3 == integer4);//输出false

    }
}

请解释一下为什么输出结果是true,false?

可能很多会猜想这是不是因为数值的范围的原因,那么现在我们看看你源码:

我们都知道,我们在进行赋值的时候,都会发生自动装箱,但是自动装箱底层上调用的是valueOf方法,那问题可能就在这里:

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


由源码可知,凡是数值范围在[-128,127]之内的,都会缓存到数组里面,所以在这个范围内的值我们访问到的都是同一份,所以两个引用里面存的地址是一样的,那么就是相等的。但是对于不是在这个范围内的值,就是直接 new 了一个Integer对象,那自然两个引用的值就不一样了,不相等。


最后,今天的文章分享比较简单,就到这了,如果大家觉得还不错的话,还请帮忙点点赞咯,十分感谢!
在这里插入图片描述

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