IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 集合的前奏---泛型 -> 正文阅读

[Java知识库]集合的前奏---泛型

?目录?

  • 🍗什么是泛型
  • 🍗引出泛型
  • 🍗泛型类的语法
  • 🍗裸类型
  • 🍗泛型如何编译的
  • 🍗泛型的上界
  • 🍗泛型方法
  • 🍗通配符

1.什么是泛型???

1.1泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化

看到标红的这句话,有些兄弟可能会有疑惑,我们以前传参,传得是一个整形数据,传得是一个引用,我从来没传过类型嘛,类型难道还能作为参数传递???对,没错,接下来,我们就来看一下类型作为参数传递的实现方式,,,

?¨è?é??¥????è?°


??2.引出泛型

我们以前学过继承和多态,我们知道所有类的父类默认是Object类,我们现在目标是:数组中可以存放任何类型的数据,这时候我们就可以new一个Object类型的数组,,,

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

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

    public void setPos(int pos, Object value) {
        array[pos] = value;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.setPos(0,1);
        myArray.setPos(1,"hello");
    }
}

当我们写出这样一个代码的时候,我们发现数组中什么元素都能放了,可以放整形数据,可以放字符串,还可以放小数,这里面能放的东西太多了,你能知道某个下标的元素是什么样的类型吗??当我们取元素的时候,会发现你的代码编译都不能通过,,,

?为什么会出现这样的情况呢,我相信大部分兄弟们都知道,我们观察getPos的返回值是Object,也就意味着我们拿出来的可能是任意元素,但编译器知道吗???它只知道返回的是一个Object:这就很明显了,父类给子类,,这不纯纯的向下转型吗,向下转型就必须要强转类型,否则你的代码就不可能通过。

那么问题又来了,,我这里只是放了两个元素,肉眼还能看的过来,但是这里面的类型时刻在发生改变,就算你知道类型,你每次都需要强转吗,我是动态的啊,我代码是写死的啊,所以这个地方如果用Object去做的话,会显得格外麻烦,而且局限性非常的大,,,

?我们现在的困境有两个:

1.任何类型都可以放,不好控制,,,

2.每次取元素的时候,都得进行强制类型转换,,,


这时候我们得搞一手希望工程,我们的希望是:

1.我们能不能自己指定类型

2.我们能不能,不再进行类型转换呀,能不能把这一步省略呀,兄弟!!!

?为了解决这一问题,我们java就引入了这个东西----》泛型

那到底怎么指定类型呢,来看代码:

class MyArray <T>{
    //public Object[] array = new Object[10];
    public T[] array = (T[]) new Object[10];//这样写也不好,待会说为什么??

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

    public void setPos(int pos, T value) {
        array[pos] = value;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        //我指定放整形
        MyArray<Integer> myArray = new MyArray<>();
        //后面的尖括号里的类型可以省略,在实例化对象的时候根据前面可以做出判断
        //MyArray<Integer> myArray = new MyArray<Integer>();
        
        myArray.setPos(0,1);
        myArray.setPos(1,2);
        
        Integer ret = myArray.getPos(1);
        System.out.println(ret);

        //我指定放字符串类型
        MyArray<String> myArray2 = new MyArray<>();

        myArray2.setPos(0,"abc");
        myArray2.setPos(1,"bit");

        String ret2 = myArray2.getPos(0);
        System.out.println(ret2);

    }
}

这个代码有几个注意事项:

1.尖括号<>里面只能放引用类型;

2.<T>是一个占位符,代表当前类是一个泛型类?;

3.MyArray<Integer> array = new MyArray<>();后面的尖括号可以省略Integer不写,实例化对象的? ? ?时候编译器根据前面能做出判断。

🤔🤔🤨🤨😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯😯

看完上面的代码,我相信兄弟们已经明白是怎么一回事了吧,,,

我们刚刚的希望工程也已经实现了,,

1.<Integer>,指定当前类中,使用的类型是Integer类型

2.泛型帮我在编译期间做了2件事情(目前为止的优点):

? ? ? ? 2.1?存放元素的时候,进行类型的检查

????????2.2 取元素的时候,帮我进行类型的转换


3.泛型类的语法

有了刚刚知识的铺垫,这一块咱就形式化一下啦,,当然,语法还是很重要滴!!

语法:

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

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

栗子:

MyArray<Integer> list = new MyArray<Integer>();

??4.裸类型(了解即可)

说明:

兄弟们,咱学了新东西,这旧东西,咱也得了解一下是不咯,,万一以后跟别人聊到了,自己好像从来没听过就尴尬了,,

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型
MyArray list = new MyArray();
注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制

5.泛型如何编译的

5.1 擦除机制
我用的是Powershell窗口查看的字节码文件,当然用idea的兄弟们也可以去下载一个展示字节码的插件,,接下来我们看看泛型到底是怎么编译的,,javap? -c查看字节码

我们发现在编译期间,所有的T都被擦除为了Object,那既然所有的T都变成了Object,那我们为什要指定类型呢?注意这里不是编译的时候替换,指定类型只是为了在编译的时候帮我们进行类型的检查和转换,并不是替换!!!最终字节码编译完成的时候,所有的T变成了Object,而这就是我们Java当中泛型所谓的擦除机制?!!!

提问:那既然T在编译的时候被擦除为了Object,那为啥 T[]? array = new? T[]? 会报错呢?

5.2会给你答案😎😎😎

5.2.泛型数组为什么不能实例化???

先看一段代码和运行结果:

class MyArray<T> {
    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 T[] getArray() { return array; }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>();
        Integer[] strings = myArray1.getArray();//这里运行时会报错

        String[] strings1 = (String)myArray1.getArray();//这里编译都不能通过
    }
}

?出现这种情况的原因何在呢?

这里最大的问题就是数组,java中的数组是非常特殊的,它是在堆上分配内存的,它有自己的一套特殊的机制,而T[]? array = new? T[]? 这个数组它没有指定具体的类型,肯定是不让这样做的;再者,get方法的返回类型是Object类型,如果用整形数组来接受,父类给子类,编译器是直接不让你做的。

那问题来了???既然 T[]? array = new? T[] 这种写法是错误的,T[] array = (T[])new Object[10] 这种写法又不好,那正确的写法应该是怎么的呢???

正确写法:(了解即可)

/**
 * 通过反射创建指定类型的数组
 */
public MyArray(Class<T> clazz, int capacity) {
    array = (T[]) Array.newInstance(clazz, capacity);
}

看起来很复杂,对吧,但是没关系,java官方都不这么用,所以我们也只需要了解即可。

我们可以看下ArrayList的源码来确认一下(以免存在忽悠的成份):


?6.泛型的上界

什么叫泛型的上界呢??先给你们看一下语法:

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

这一模块我将举两个栗子来和大家一起认识或温习一下这个知识点,,,

简单示例:

class Test<T extends Number> {

}
public class TestDemo {
    public static void main(String[] args) {
        //Number的子类
        Test<Integer> test = new Test<>();
        Test<Double> test1 = new Test<>();
        Test<Float> test2 = new Test<>();
        //Number本身
        Test<Number> test3 = new Test<>();
    }
}

<T? extends? Number>中的 extends可不敢理解为继承喔,可以理解为T擦除为了Number的子类或自己本身,也就是说在主函数中用这个类实例化的时候,可以指定的类型一定要 <= Number ,,差不多就这意思,,,简单的你们都能理解,我就不多废话了,给你们来点稍微有难度的(我觉得难),,

进阶示例:

class Alg<T extends Comparable<T>> {
    //如果不设置上界为Comparable,就用不了compareTo这个方法,因为T如果不指定的话,
    // 在编译的时候被擦除成了Object,而Object并没有实现这个接口

    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;
    }
}
class Student implements Comparable<Student> {
    public String name;
    public int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
    @Override
    public int compareTo(Student o) {
        return 0;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        //指定类型为整形
        Alg<Integer> alg = new Alg<>();
        Integer[] array = {1,2,3,4,5};
        Integer max = alg.findMax(array);
        System.out.println(max);

        //指定类型为Student
        Alg<Student> alg2 = new Alg<>();
        Student[] students = new Student[3];
        students[0] = new Student("毛毛",15);
        students[1] = new Student("吉吉",13);
        students[2] = new Student("毛二",16);
        Student student = alg2.findMax(students);
        System.out.println(student);
    }
}

<T? extends? Comparable<T>>意为擦除到实现了这个接口,简而言之就是这里面接收的类型一定是实现了Comparable接口的,给你们证明一下,我那里面Student类型毋庸置疑,自己实现的Comparable接口,我们来看看Integer的源码:

?再者,我们观察一下? Alg? 这个类中的? ?findMax? 方法,我们都知道引用类型比较大小要用比较器或者compareTo方法,如果 Student 这个类没有实现 Comparable 接口,我们在写代码的时候,会发现点都点不出来,比较一下,就通透了,,

没有实现Comparable接口:

?实现了Comparable接口:

?当然,又有人会问,我能不能不实例化这个方法,直接调用这个方法,这时候我们把这个方法搞成静态的来看一下:

发现它报错了,,,别急哈,咱们一步一步是爪牙,,哦,不对,是一步一步来揭开面纱,,

那如何将这个代码改成正确的呢??细心的兄弟会发现,既然这个方法是静态的, 那我们只需要通过类名来方法,如果还按照之前的写法,类后面跟一个? <T>,那我们啥时候给它指定类型??不知道吧,这时候我们要将代码改成这样,在 static 的后面跟一个? <T>,这样你的代码就不会报错了,,,

?

当然,我们也可以指定边界,这里跟类有着异曲同工之妙,就不细说了,花两秒钟看一下代码,你就能发出:? ?哦~~~~~的声音😯😯😯,,中国人不骗中国人,,,

class Alg2 <T>{
    public static<T extends Comparable> 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;
    }
}

最后,你们可能会问,那它既然有上界,那它有下界吗??

那肯定是没有的啊,咱学习编程多辛苦啊 !!是不是兄弟们😎😎😎

?

?


?7.通配符

兄弟们,最后两个知识点,咱泛型就告一段落了,,

什么是通配符??我还是换一行吧

?这个就是通配符,对,没错,就是这个问号? "?"

7.1.通配符能用来干嘛

JAVA中引入通配符这一玩意是用来解决泛型无法协变这一问题的,那什么是协变呢?

协变这东西怎么说呢,类似于,继承当中的父类引用子类的对象,但是泛型中不支持协变类型,而JAVA的数组是支持的,数组中明确了它所允许存储的对象类型,并且会在运行时做类型检查(这也是为什么不能创建泛型数组的原因,数组创建时必须知道确切类型)。

下面看具体代码,看看是如何解决协变问题的,,,

class Message<T> {
    private T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}
public class TestDemo2 {
    public static void main(String[] args) {
        Message<Integer> message = new Message<>();
        message.setMessage(1);
        fun(message);//编译不通过
    }

    public static void fun(Message<String> temp) {
        System.out.println(temp.getMessage());
    }
}

首先,上面这一段代码编译是不能通过的,很明显我? fun? 方法中只能接收? String? 类型的数据,而你给我一个整形数据,肯定会报错吗,当我把它改成这样的时候,,你会发现代码不报错了:

class Message<T> {
    private T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}
public class TestDemo2 {
    public static void main(String[] args) {
        Message<Integer> message = new Message<>();
        message.setMessage(1);
        fun(message);
    }

    public static void fun(Message<?> temp) {
        //temp.setMessage(1);//是不能往里面加元素的,所以源码当中通配符用的比较多
        System.out.println(temp.getMessage());
    }
}

?这里尖括号里面放?意味着可以接收任何类型,人家用户你可不知道你这是什么类型,所以通配符在这一块就起了非常大的作用;再者,既然可以接收所有类型,就不能让用户随便修改。具体是怎么做到的呢?也就是说当你用通配符在函数中接收任意类型之后,你不能调用? set? 方法往里面设值,你根本不知道你接收到的是什么类型,,,所以,我们也看到源码当中通配符用的比较多就是这么回事,,,


7.2.通配符的上界(读数据)

通配符的上界,这东西跟刚刚的泛型上界很相似,有了刚刚的只是铺垫,应该不难理解了,,语法这一块咱就不演示了,,

class Food {}

class Fruit extends Food {}

class Apple extends Fruit {}

class Banana extends Fruit {}

class Plate<T> {
    private T pear;

    public T getPlate() {
        return pear;
    }
    public void setPlate(T a) {
        this.pear = a;
    }
}
public class TestDemo3 {
    public static void main(String[] args) {
        Plate<Apple> plate = new Plate<>();
        Plate<Banana> plate1 = new Plate<> ();

        fun1(plate);
        fun1(plate1);
    }
    public static void fun1(Plate<? extends Fruit> temp) {
        //temp.setPlate(new Apple());这里不知道temp引用的子类是谁,所以不能往里边放元素

        //可以接收元素,父类接收子类,顶多就是向上转型
        System.out.println(temp.getPlate());
        //Fruit fruit = temp.getPlate();
    }
}

我们这里给了它香蕉/苹果等一些水果类和 Fruit 这个父类,<? extends Fruit>这里可以接收任何Fruit 的子类类型,同理,既然可以接收任何类型,就不适合写数据,只适合读数据,,


7.3.通配符的下界(写数据)

通配符不同于泛型,它是有下界的,来看看它的下界,,,

语法大概就这样子,简单了解一下:<? super 下界>---重点在代码

public class TestDemo3 {
    public static void main(String[] args) {
        Plate<Fruit> plate = new Plate<>();
        fun(plate);

        Plate<Food> plate2 = new Plate<>();
        fun(plate2);
        
        Plate<Apple> plate1 = new Plate<>();
        //fun(plate1);
    }
    public static void fun(Plate<? super Fruit> temp) {
        //写数据,但是不能写父类
        temp.setPlate(new Apple());
        temp.setPlate(new Banana());

        //编译不通过:Fruit的父类有很多,不一定就是某一个
        //temp.setPlate(new Food());

        //不安全--它的父类有很多,不知道是哪一个父类发生了向下转型
        //Fruit fruit = (Fruit)temp.getPlate();
    }
}

通配符的下界呢,,我们可以看到它是用了一个关键字super,字面意思,大家都懂,就是说能接收的类型至少是 Fruit ,以及 Fruit 的父类,所以,在读写数据这一块,它就不同于别人,它只喜欢写数据,就像咱人一样,总得有些偏执的爱好,我就喜欢敲代码,是吧,,,为啥不能读数据,这里还是得解释一下,因为 Fruit 的父类太多了,你根本不知道读谁的数据,再者就算你强转类型,编译器也会认为这是不安全的,所以我们在读数据和写数据这一块,?的上界和?的下界要搞清楚什么时候用哪一个,,,

下图能帮助你理解读写数据,到底适合哪一个》》》🤔🤔🤔

?本来还有一个装箱和拆箱的类容要给兄弟们分享的,但是现在已经凌晨两点了(小命要紧,建议大家不要像我这样,身体是内卷的本钱,我只是一时兴起,不敢瞒着兄弟们偷偷卷),小编我要为那些目前还在渴望这一块知识的年轻小伙子着想,就把那些内容单独抽到下一期去了,,,下期见,兄弟们!!!

?

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-11 16:17:11  更:2022-05-11 16:19:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:17:15-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码