• 【Java SE】认识泛型


    篮球哥温馨提示:编程的同时不要忘记锻炼哦!

    天青色等烟雨,而我在......


    目录

    1、如何创建可以存放各种类型的数组?

    2、泛型的概念

    2.1 浅聊泛型

    2.2 泛型的简单语法

    2.3 类型形参列表的命名规范

    2.4 使用泛型知识创建数组

    2.5 什么是裸类型?

    3、泛型是如何编译的?

    3.1 泛型的擦除机制

    3.2 再谈为什么不能实例化泛型数组?

    3.3 什么是泛型的上界?

    3.4 再谈擦除机制

    4、包装类的知识

    4.1 基本数据类型和包装类

    4.2 装箱和拆箱

    4.3 自动装箱和拆箱

    4.5 一道面试题

    5、泛型方法

    5.1 普通泛型方法

    5.2 静态泛型方法

    6、通配符

    6.1 引出通配符 

    6.2 认识通配符

    6.3 通配符的上界

    6.4 通配符的下界


    1、如何创建可以存放各种类型的数组?

    通过前面JavaSE的语法知识储备,如果现在让你们创建如标题一样的数组,你会怎么创建呢?

    答案是:使用 Object 类来定义数组,因为 Object 是所有类的父类, 可以接收任意子类对象,也即实现了向上转型,于是我们就写出了这样的代码:

    private Object[] array = new Object[3];

    那么这种方法可取吗?显然是可取的,但只是使用起来会很不方便,具体不方便在哪,我们接着往后看,在这里我们要写一个类,里面提供了获取array指定下标的数据,和设置array指定下标的数据,于是写出了这样的代码:

    1. public class DrawForth {
    2. private Object[] array = new Object[3];
    3. public void setPosArray(int pos, Object o) {
    4. this.array[pos] = o;
    5. }
    6. public Object getPosValue(int pos) {
    7. return this.array[pos];
    8. }
    9. }

    代码到这里仍然是正确的,那我们就要去使用这个类,也就是在main方法中用这个类实例对象,去操作里面的数组,所以main方法的代码就是这个样子:

    1. public static void main(String[] args) {
    2. DrawForth draw = new DrawForth();
    3. draw.setPosArray(0, 123);
    4. draw.setPosArray(1, "hello");
    5. draw.setPosArray(2, 12.5);
    6. int a = (int)draw.getPosValue(0);
    7. String str = (String)draw.getPosValue(1);
    8. double d = (double)draw.getPosValue(1);
    9. }

    看到这里,你是不是就发现这样做很不方便呢?当我们往数组里面设置数据的时候开心了,想设置成什么类型就是什么类型,但是!当我们要获取对应位置的元素就麻烦了,我们必须知道他是什么类型,然后进行强制类型转换才能接收,(返回是Object类型所以需要强转),难道往后每次取数据的时候我还得看一看是什么类型吗?


    2、泛型的概念

    2.1 浅聊泛型

    泛型是在JDK1.5引入的新的语法,通过上面的例子,由此我们就引出了泛型,泛型简单来说就是把类型当成参数传递,指定当前容器,你想持有什么类型的对象,你就传什么类型过去,让编译器去做类型检查!从而实现类型参数化(不能是基本数据类型,后面讲)

    2.2 泛型的简单语法

    1. class Test1<类型形参列表> {
    2. }
    3. class Test2<类型形参1, 类型形参2, ...> {
    4. }

    2.3 类型形参列表的命名规范

    类名后面的 <类型形参列表> 这是一个占位符,表示当前类是一个泛型类,形参列表里面如何写?通常用一个大写字母表示,当然,你也可以怎么开心怎么来,但是小心办公室谈话警告哈(dog),这里有几个常用的名称:

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

    2.4 使用泛型知识创建数组

    这里就来修改一下刚开始的代码,使用到泛型的知识,那么我们就可以这样修改:

    1. public class DrawForth {
    2. //private T[] array = new T[3]; error
    3. private T[] array = (T[])new Object[3];
    4. public void setPosArray(int pos, T o) {
    5. this.array[pos] = o;
    6. }
    7. public T getPosValue(int pos) {
    8. return this.array[pos];
    9. }
    10. public static void main(String[] args) {
    11. DrawForth draw = new DrawForth<>();
    12. draw.setPosArray(0, 123);
    13. //draw.setPosArray(1, "hello"); error
    14. //draw.setPosArray(2, 12.5); error
    15. draw.setPosArray(1, 1234);
    16. draw.setPosArray(2, 12345);
    17. int a = draw.getPosValue(0);
    18. int b = draw.getPosValue(1);
    19. int c = draw.getPosValue(2);
    20. }
    21. }

    如上修改之后的代码,我们可以得到以下知识点:

    • 是一个占位符,仅表示这个类是泛型类
    • 不能 new 泛型数组(原因后面讲),此代码的写法也不是最好的方法!
    • 实例化泛型类的语法是:类名<类型实参>变量名 = new 泛型类<类型实参>(构造方法实参);
    • 注意:new 泛型类<>尖括号中可以省略类型实参,编译器可以根据上下文推导!
    • 编译时自动进行类型检查和转换。

    2.5 什么是裸类型?

    裸类型就是指在实例化泛型类对象的时候,没有传类型实参,比如下面的代码就是一个裸类型:

    DrawForth draw = new DrawForth();

    我现在可以告诉你,这样做编译完全正常,但我们不要去使用裸类型,因为这是为了兼容老版本的 API 保留的机制,毕竟泛型是 Java1.5 新增的语法。


    3、泛型是如何编译的?

    3.1 泛型的擦除机制

    如果我们要看泛型是如何编译的,可以通过命令 javap -c 字节码文件 来进行查看:

    如上代码是 2.4 段落中的代码,奇怪,明明传的实参是 Integer 类型,最后所有的 T 却变成了 Object 类型,这就是擦除机制,所以在Java中,泛型机制是在编译级别实现的,运行期间不会包含任何泛型信息。

    提示:类型擦除,不一定是把 T 变成 Object(泛型的上界会提到)

    3.2 再谈为什么不能实例化泛型数组?

    知道了擦除机制后,那么 T[] array = new T[3]; 是不对的,编译的时候,替换为Object,不是相当于:Object[] array = new Object[3]吗?

    在Java中,数组是一个很特殊的类型,数组是在运行时存储和检查类型信息, 泛型则是在编译时检查类型错误。而且Java设定擦除机制就只针对变量的类型和返回值的类型,所以在编译时候压根不会擦除 new T[3]; 这个 T ,所以自然编译就会报错!

    我们前面通过强制类型转换的方式创建了泛型数组,说过那样写并不好,正确的方式是通过反射创建指定类型的数组,由于现在没学习到反射,这里先放着就行。

    3.3 什么是泛型的上界?

    有了擦除机制的学习,泛型在运行时都会被擦除成 Object 但是并不是所有的都是这样,泛型的上界就是对泛型类传入的类型变量做一定的约束,可以通过类型边界来进行约束。

    语法:

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

    这里我们来举两个例子:

    例1:

    这里简单分析一下,Student 继承了 Person 类,而 Teacher 没有继承 Person 类,接着 Test 类给定了泛型的上界, 那么 Test 类中 <> 里面是什么意思呢?表示只接收 Person 或 Person 的子类作为 T 的类型实参。

    通过 main 方法中的例子也可也看出,类型传参只能传 Person 或 Person 的子类。

    例2:

    还是简单分析一下,Student 类实现了 Comparable 接口,而 Teacher 类并没有实现, 接着 Test 类给定了泛型的上界, 那么 Test 类中 <> 里面是什么意思呢?表示 T 接收的类型必须是实现 Comparable 这个接口的!

    通过 main 方法中的例子也可也看出,类型传参只能传实现了 Comparable 接口的类 。

    注意:如果泛型类没有指定边界,则可以默认视为 T extends Object。

    3.4 再谈擦除机制

    如果给泛型设置了上界,则会擦除到边界处,也就不会擦除成 Object!

    1. class Person {}
    2. class Student extends Person {}
    3. public class Mainextends Person> {
    4. T array[] = (T[])new Object[10];
    5. public static void main(String[] args) {
    6. Main main = new Main<>();
    7. }
    8. }

    这里 Main 方法中设定了泛型的上界,传的类型实参必须是Person的子类,所以编译时会不会被擦除成 Person呢?下面我们查看一下对应的字节码文件: 

    显而易见,确实被擦除成了泛型的上界! 


    4、包装类的知识

    4.1 基本数据类型和包装类

    在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了 一个包装类型。

    基本数据类型包装类
    byteByte
    shortShort
    intInteger
    longLong
    floatFloat
    doubleDouble
    charCharacter
    booleanBoolean

    4.2 装箱和拆箱

    装箱和拆箱也可也被称为装包和拆包。

    装箱:将一个基本数据类型值放入对象的某个属性中。

    拆箱:将一个包装类型中的值取出放到一个基本数据类型中。

    这里我们举例来更清楚的认识装箱和拆箱:

    1. public class Test {
    2. public static void main(String[] args) {
    3. int a = 10;
    4. Integer integer1 = new Integer(a); //手动装箱
    5. Integer integer2 = Integer.valueOf(100); //手动装箱
    6. int b = integer1.intValue(); //手动拆箱
    7. }
    8. }

    4.3 自动装箱和拆箱

    由上面的例子我们可以看出,手动装箱和拆箱会带来不少的代码量,为了减少开发者的负担,Java中提供了自动转换机制,比如:

    1. public class Test {
    2. public static void main(String[] args) {
    3. Integer integer = 100; //自动装箱
    4. int a = integer; //自动拆箱
    5. }
    6. }

    4.5 一道面试题

    以下代码输出什么? 

    1. public class Test {
    2. public static void main(String[] args) {
    3. Integer a1 = 100;
    4. Integer a2 = 100;
    5. System.out.println(a1 == a2);
    6. Integer a3 = 200;
    7. Integer a4 = 200;
    8. System.out.println(a3 == a4);
    9. }
    10. }

    结果是:true false 

    为什么是这样的答案?这里我们去看一下对应的字节码文件再分析:

    通过观察字节码文件,我们可以看到,在自动装箱的过程中,调用了 Integer.valueOf 方法,那么我们就去看一看 valueOf 方法中做了一件什么事:

    通过查看源码,我们也能看出此方法将始终缓存 -128到127范围内的值, 通过查看对应的 low 和 high 值也可也发现 low为 -128,high为127,cache 是一个缓存数组。

    接着我们来阅读下这段代码的操作,如果传入的值是介于 -128和127 之间,则直接返回缓存数组对应下标的值,比如传入的值是 -127 也就返回 chache[-127+(-(-128))],也即1下标位置的值!

    如果超出了 -128到127 的范围则是新 new 一个对象返回,只要是 new 就一定是一个新对象,地址也是唯一的。

    而且引用类型用 == 比较,比较的是引用的对象的地址,看完上面的介绍,你能弄明白为什么输出 true 和 false 吗?


    5、泛型方法

    定义泛型方法的语法:

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

            //...code

    }  

    5.1 普通泛型方法

    这里我们就举一个很简单的例子:

    1. public class Test {
    2. public T getValue(T value) {
    3. return value;
    4. }
    5. public static void main(String[] args) {
    6. Test test = new Test();
    7. int ret = test.getValue(150); //不使用类型推导
    8. System.out.println(ret);
    9. double d = test.getValue(12.5); //使用类型推导
    10. System.out.println(d);
    11. }
    12. }

    这就是泛型方法,这里面有个关键词,类型推导,什么是类型推导呢?

    类型推导就是编译器会根据你传参的数据,自动推断出你要传递的类型实参,你也可以不使用类型推导,他们的效果都是一样的。

    5.2 静态泛型方法

    既然有普通泛型方法,同理,也有静态的泛型方法,也就是在修饰符后面加上 static,静态泛型方法跟普通静态方法一样,都是通过类名访问,不依赖于对象:

    1. public class Test {
    2. public static T getValue(T value) {
    3. return value;
    4. }
    5. public static void main(String[] args) {
    6. int ret = Test.getValue(150); //不使用类型推导
    7. System.out.println(ret);
    8. double d = getValue(12.5); //使用类型推导(静态方法可以直接访问同类中静态方法,可以不借助类名)
    9. System.out.println(d);
    10. }
    11. }

    6、通配符

    6.1 引出通配符 

    我们先来看这样的一段代码:

    1. class Message {
    2. private T message ;
    3. public T getMessage() {
    4. return message;
    5. }
    6. public void setMessage(T message) {
    7. this.message = message;
    8. }
    9. }
    10. public class TestDemo {
    11. public static void fun(Message temp){
    12. System.out.println(temp.getMessage());
    13. }
    14. public static void main(String[] args) {
    15. Message message = new Message<>();
    16. message.setMessage("欢迎来到篮球哥的博客!");
    17. fun(message);
    18. }
    19. }

    如果你仔细观察,TestDemo 类中的 fun 方法是有局限性的,他的形参就限制了传过来的 Missage类的类型必须是String,也就是说,形参能接收的对象的类型参数必须是String类型。

    所以如果我们 new Missage对象时,类型实参传的是 Integer 呢?fun方法就会报错:

    所以为了解决以上的问题,就有了通配符的概念!

    6.2 认识通配符

    泛型T是确定的类型,一旦传类型了,就定下来了,而通配符的出现,就会使得更灵活,或者说更不确定,就好像他是一个垃圾箱,可以接收所有的泛型类型,但又不能让用户随意更改!

    通配符:? 

    现在我们就把上面的代码更改一下,运用上通配符:

    1. public class TestDemo {
    2. public static void fun(Message temp){
    3. System.out.println(temp.getMessage());
    4. }
    5. public static void main(String[] args) {
    6. Message message1 = new Message<>();
    7. message1.setMessage(123);
    8. fun(message1);
    9. Message message2 = new Message<>();
    10. message2.setMessage("欢迎来到篮球哥的博客!");
    11. fun(message2);
    12. }
    13. }

    这样我们的代码就不会出错,但是,你不能通过 fun 方法去修改你传递对象的内容,为什么呢?

    站在 fun 的角度,他使用了 接收可以任意泛型类,所以他不能确定自己接收了什么对象的!也就无法对对象的值进行更改! 

    这样代码还是不够好,如果真的什么泛型类都能接收,那不是乱套了,所以在此基础上,又增加了通配符的上界和下界!

    6.3 通配符的上界

    语法:   例如:

    表示只能接收的实参类型是 Person 或者 Person的子类

    图例:

    这里我们写一段伪代码,更改上面用例的方法:

    1. public static void fun(Message temp){
    2. //temp.setMessage(new Student()); //仍然无法修改!
    3. //temp.setMessage(new Person()); //仍然无法修改!
    4. Person person = temp.getMessage();
    5. System.out.println(person);
    6. }

    为什么还是不能修改对象的属性呢?因为 temp 接收的是 Person 或 Person的子类,此时接收的是哪个子类无法确定,也就无法设置对象的属性。

    因为我们知道只能接收 Person以及他的子类,所以我们就可以拿 Person 类型来接收 getMessage 的对象,因为 Person是他们的父类,获取的是子类对象就可以实现向上转型,是安全的。

    总结: 通配符的上界,不能进行写入数据,只能进行读取数据。

    6.4 通配符的下界

    语法:   例如: 

    表示只能接收的实参类型是 Person 或者 Person的父类

    图例: 

    这里我们写一段伪代码,更改上面用例的方法:

    1. public static void fun(Messagesuper Person> temp){
    2. temp.setMessage(new Student()); //可以修改,因为添加的是他的子类
    3. temp.setMessage(new Person()); //可以修改,因为添加的是他本身
    4. //Person person = temp.getMessage(); // 不能接收,不知道获取的是哪个父类
    5. System.out.println(temp.getMessage()); //只能输出
    6. }

    为啥下界就可以设置对象的属性呢?因为只能接收本身以及父类的类型,所以我们可以setMessage 传子类对象,但是不能传递父类,因为修改成子类对象是向上转型是安全的,如果 setMessaget 传父类对象的话就是向下转型则不安全!

    为啥不能 getMessage呢?因为你不知道形参接收的类型是哪个父类,只能去输出内容!

    总结:通配符的下界,不能进行读取数据,只能写入数据。 


    下期预告:【Java 数据结构】手撕顺序表

  • 相关阅读:
    10驾校科目一考试系统——窗口交互
    Android网络安全配置:允许明文HTTP通信的正确姿势20240418
    运营业务指标
    ubuntu22.04安装公司安全VPN的方案
    PMP怎么补考?补考费用是多少?
    vue中babel-plugin-component按需引入和element-ui 的主题定制,支持发布,上线
    Springboot进行登录校验
    vue3+vite+ts配置多个代理并解决报404问题
    【Android 构建新工具】Bazel 构建Android项目
    (17)不重启服务动态调整RabbitMQ消费者数量
  • 原文地址:https://blog.csdn.net/m0_61784621/article/details/127159301