【解密泛型:让你的代码更安全、更高效】

🌠作者:@TheMythWS.

🎇座右铭:不走心的努力都是在敷衍自己,让自己所做的选择,熠熠发光。

7bd0828d26e149cf8aaa8d2a3b80c3ec.jpeg

目录

🧾泛型的概念

🧾引出泛型

🧾自定义泛型类结构注意事项

🧾裸类型的引入

🧾泛型如何编译的?

🧾泛型的上界

🧾泛型的方法

🧾泛型参数存在继承关系的情况  

🧾通配符

🧾为什么要学习泛型?


🧾泛型的概念

        一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。
如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
                                                             ----- 来源《Java编程思想》对泛型的介绍。 
泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。

🧾引出泛型

        实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?
思路: 
1. 我们以前学过的数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] strs = new String[10]; 
2. 所有类的父类,默认为Object类。数组是否可以创建为Object?

class MyArray {
    public Object[] obj = new Object[3];
    public Object getPos(int pos) {
        return this.obj[pos];
    }
    public void setVal(int pos, Object val) {
        this.obj[pos] = val;
    }
}
public class TestDemo {
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.setVal(0, 1314);
        myArray.setVal(1, "themyth");
        myArray.setVal(0, 13.14);
        String str = (String) myArray.getPos(1);
    }
}

问题:
1. 任何类型数据都可以存放
2. 获取数据的时候都必须进行强制类型转换(很麻烦)
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。
而不是同时持有这么多类型。
所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。
此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

泛型的使用: 

class MyArray<T> {//<T>代表当前类是一个泛型类
    //public T[] obj = new T[3];错误的写法
    public T[] obj = (T[]) new Object[3];//这种写法只是不报错,不是常用的写法
    public T getPos(int pos) {
        return this.obj[pos];
    }
    public void setVal(int pos, T val) {
        this.obj[pos] = val;
    }
}
public class TestDemo {
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        //实例化对象的同时 指定当前泛型类 的指定参数类型是Integer
        //<>里面只能是引用类型,后面的<>里面的内容可以省略
        MyArray<Integer> myArray1 = new MyArray<Integer>();
        myArray1.setVal(0, 1314);
        myArray1.setVal(1, 520);
        myArray1.setVal(2, 369);
        Integer pos0 = myArray1.getPos(0);
        Integer pos1 = myArray1.getPos(1);
        Integer pos2 = myArray1.getPos(2);
        System.out.println(pos0);
        System.out.println(pos1);
        System.out.println(pos2);
        System.out.println("=======");
        MyArray<String> myArray2 = new MyArray<>();
        myArray2.setVal(0, "ws");
        myArray2.setVal(1, "themyth");
        myArray2.setVal(2, "lyd");
        String str0 = myArray2.getPos(0);
        String str1 = myArray2.getPos(1);
        String str2 = myArray2.getPos(2);
        System.out.println(str0);
        System.out.println(str1);
        System.out.println(str2);
    }
}

实现泛型之后:
1.存储数据的时候,可以帮我们自动进行类型检查
2.获取元素的时候,可以帮我们自动进行类型转换
【注意事项】
1.类名后的<T>代表占位符,表示当前类是一个泛型类
2.不能new泛型类型的数组
3.(类型的推导)当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写
4.了解:【规范】类型形参一般使用一个大写字母表示,常用的名称有: 
E 表示 Element 
K 表示 Key 
V 表示 Value 
N 表示 Number
T 表示 Type 

S, U, V 等等 - 第二、第三、第四个类型
5.泛型是编译时期的一种机制,在运行期间没有泛型的概念(擦除机制可以证明)

🧾自定义泛型类结构注意事项

【1】泛型类的定义和实例化:

/**
 * @auther: themyth
 * GenericTest就是一个普通的类
 * GenericTest<E>就是一个泛型类
 * <>就是一个参数类型,但是这个类型是什么呢?这个类型现在是不确定的,相当于一个占位
 *但是现在确定的是这个类型一定是一个引用数据类型,而不是基本数据类型
 */
public class GenericTest<E> {
    int age;
    String name;
    E sex;
    public void a(E n){
    }
    public void b(E[] m){
    }
}
class Test{
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        //GenericTest进行实例化:
        //(1)实例化的时候不指定泛型:如果实例化的时候不明确的指定类的泛型,那么认为此泛型为object类型
        GenericTest gt1 = new GenericTest();
        gt1.a("abc");
        gt1.a(17);
        gt1.a(9.8);
        gt1.b(new String[]{"a","b","c"});
        //(2)实例化的时候指定泛型:---》推荐方式
        GenericTest<String> gt2 = new GenericTest<>();
        gt2.sex = "男";
        gt2.a("abc");
        gt2.b(new String[]{"a","b","c"});
    }
}

【2】继承情况:
(1)父类指定泛型: 

class SubGenericTest extends GenericTest<Integer>{
}
class Demo{
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        //指定父类泛型,那么子类就不需要再指定泛型了,可以直接使用
        SubGenericTest sgt = new SubGenericTest();
        sgt.a(19);
    }
}

 (2)父类不指定泛型:
如果父类不指定泛型,那么子类也会变成一个泛型类,那这个E的类型可以在创建子类对象的时候确定: 

class SubGenericTest2<E> extends GenericTest<E>{
}
class Demo2{
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        SubGenericTest2<String> s = new SubGenericTest2<>();
        s.a("abc");
        s.sex="女";
    }
}

【3】应用场合:

b839642fda104dafb0956043c19b1def.png

【4】细节:
(1)泛型类可以定义多个参数类型

bbbdc4950adc456fa17724a1f1dee9b8.png

 (2)泛型类的构造器的写法:

f8b5e97aa0664046ae5d2e2fa2762017.png

(3)不同的泛型的引用类型不可以相互赋值: 

6fc36cb050a54685b7ebb9ca8afb7d05.png

(4)泛型如果不指定,那么就会被擦除,反应对应的类型为Object类型: 

7f30827030e1478c8846ff79be9ecdf1.png

(5)泛型类中的静态方法不能使用类的泛型: 

05290c4f4a0c4c5d873c667618d5a295.png

因为静态方法c先于对象创建,而A的类型必须在创建对象之后才确定,所以两个A不同步 

(6)不能直接使用E[]的创建: 

82f5770c59fc4e1695432153c142a9f3.png

总代码: 

import java.util.ArrayList;
/**
 * @auther: themyth
 */
public class TestGeneric<A,B,C> {
    A age;
    B name;
    C sex;
    /*public static int c(A a){
       return 10;
    }*/
    public void a(A m,B n,C x){
        //不可以:A[] i = new A[10];
        A[] i = (A[])new Object[10];//实际上运行时候就是一个Object类型的数组
    }
    public TestGeneric(){
    }
    /*public TestGeneric<A,B,C>(){
    }*/
    /*public void b(){
        ArrayList<String> list1 = null;
        ArrayList<Integer> list2 = null;
        list1 = list2;
    }*/
}

🧾裸类型的引入

class MyArray<E> {//<E>代表当前类的数组里面的每一个类型是E
    public E[] obj = (E[]) new Object[3];//这种写法只是不报错,不是常用的写法
    public E getPos(int pos) {
        return this.obj[pos];
    }
    public void setVal(int pos, E val) {
        this.obj[pos] = val;
    }
}
public class TestDemo {
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        /*
        下面代码为什么不报错?这是Java历史遗留的一个问题
        因为1.5以前没有泛型的概念,Java要兼容以前的版本和新特性就这样设计了
        所以下面的是属于裸类型
        裸类型是指一个泛型类,但没有带着类型实参
        裸类型是为了兼容老版本的API保留的机制
         */
        MyArray myArray = new MyArray();
        myArray.setVal(0, "themyth");
        myArray.setVal(1, 520);
        myArray.setVal(2, 13.14);
    }
    //这是一个main方法,是程序的入口:
    public static void main1(String[] args) {
        //实例化对象的同时 指定当前泛型类 的指定参数类型是Integer
        //<>里面只能是引用类型,后面的<>里面的内容可以省略
        MyArray<Integer> myArray1 = new MyArray<Integer>();
        myArray1.setVal(0, 1314);
        myArray1.setVal(1, 520);
        myArray1.setVal(2, 369);
        Integer pos0 = myArray1.getPos(0);
        Integer pos1 = myArray1.getPos(1);
        Integer pos2 = myArray1.getPos(2);
        System.out.println(pos0);
        System.out.println(pos1);
        System.out.println(pos2);
        System.out.println("=======");
        MyArray<String> myArray2 = new MyArray<>();
        myArray2.setVal(0, "ws");
        myArray2.setVal(1, "themyth");
        myArray2.setVal(2, "lyd");
        String str0 = myArray2.getPos(0);
        String str1 = myArray2.getPos(1);
        String str2 = myArray2.getPos(2);
        System.out.println(str0);
        System.out.println(str1);
        System.out.println(str2);
    }
}

🧾泛型如何编译的?

擦除机制

        那么,泛型到底是怎么编译的?这个问题,也是曾经的一个面试问题
泛型本质是一个非常难的语法,要理解好它还是需要一定的时间打磨。
通过命令:javap -c 查看字节码文件,所有的E都是Object
在编译的过程当中,将所有的E替换为Object这种机制,我们称为:擦除机制

29b767d6305b4765a57fcd762e8c1e76.png

Java的泛型机制是在编译级别实现的。
编译器生成的字节码在运行期间并不包含泛型的类型信息。

Java泛型擦除机制之答疑解惑 - 知乎 (zhihu.com) 

思考问题:

1、那为什么,T[] ts = new T[5]; 是不对的?
编译的时候,替换为Object,不是相当于:Object[] ts = new Object[5]吗? 

在 Java 中,泛型数组的创建是被禁止的,因此 T[] ts = new T[5];不合法的语法,无法通过编译。

泛型的类型擦除会导致泛型类型参数被替换为它们的上界(或者 Object,如果没有指定上界)。但是,这种替换只发生在泛型类型的边界上,而不是在数组创建时。

对于数组的创建,数组的类型必须是一个具体的、已知的类型。无法直接创建泛型数组,因为在类型擦除之后,无法确定具体的泛型类型参数。

如果你尝试编写 Object[] ts = new Object[5];,它只是创建了一个 Object 类型的数组,而不是一个泛型数组。泛型数组的创建是不合法的,因为它可能导致类型安全问题。

2、类型擦除,一定是把T变成Object吗?

不完全正确。类型擦除是 Java 泛型实现的一种特性,它会在编译时擦除泛型类型信息,将泛型类型参数替换为它们的上界(或者 Object,如果没有指定上界)。但是,类型擦除并不一定将泛型类型参数替换为 Object。

具体来说,类型擦除的规则如下:

  1. 对于泛型类或接口,类型参数将被替换为它们的上界。例如,List<T> 的类型参数 T 在类型擦除后将被替换为 Object。

  2. 对于泛型方法,类型参数将被替换为它们的上界。例如,<T> void foo(T obj) 中的类型参数 T 在类型擦除后将被替换为 Object。

  3. 如果泛型类型参数有限定(即指定了上界),则类型参数将被替换为它们的上界。例如,List<T extends Number> 中的类型参数 T 在类型擦除后将被替换为 Number。

需要注意的是,类型擦除是在编译时发生的,而不是在运行时。这意味着在运行时无法获取泛型类型参数的具体信息。

总结起来,尽管类型擦除会导致泛型类型参数被替换为它们的上界,但并不一定是替换为 Object。具体替换的类型取决于泛型类型参数的上界或限定。

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

        再简述一遍,泛型类型数组的实例化在 Java 中是不允许的,这是由 Java 语言设计的限制所决定的。

这个限制的主要原因是类型擦除。在 Java 中,泛型类型参数在编译时会被擦除,也就是说,在运行时无法获取泛型类型参数的具体信息。数组在创建时需要指定数组的元素类型,而泛型类型参数的具体类型在编译时是不可知的,因此无法创建一个确切类型的泛型数组。

另外,Java 中的数组是协变的(covariant)。这意味着,如果允许创建泛型数组,那么在数组的赋值和访问过程中就可能出现类型不一致的情况,从而违背了 Java 类型系统的安全性。

为了避免这种类型不一致的问题,Java 泛型系统限制了对泛型数组的实例化操作,推荐使用集合类型(如 ArrayList)等来代替泛型数组。

如果你需要存储一组泛型类型的元素,并且需要保持类型安全,可以考虑使用集合类(如 ArrayList)或其他数据结构来替代泛型数组。这样可以避免类型擦除和数组协变所带来的问题,并提供更好的类型安全性和灵活性。

思考:

public E[ ] obj = (E[ ]) new Object[3]; 这种写法只是不报错,不是常用的写法
而且有缺陷,看下面的代码,比如写一个方法返回当前的数组

以下代码为例子:

class MyArray<E> {
    public E[] obj = (E[]) new Object[3];
    //下面是常用写法:
    /*public Object[] obj = new Object[3];
    public E getPos2(int pos) {
        return (E) this.obj[pos];
    }*/
    public void setVal(int pos, E val) {
        this.obj[pos] = val;
    }
    public E[] getArray() {
        return this.obj;
    }
}
public class TestDemo {
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray();
        myArray.setVal(0, 1314);
        myArray.setVal(1, 520);
        Integer[] integers = myArray.getArray();
    }
}

8fde515a19cb44d2a0177205d09a6617.png

原因:替换后的方法为:将Object[]分配给Integer[]引用,类型转换异常。

public Object[] getArray() {//擦除机制:E[]替换为Object[]
    return this.obj;
}

试试向下转型:

Integer[] integers = (Integer[]) myArray.getArray();

但是还是报错,通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的,这就是为什么向下转型不安全,在这儿再怎么强转都会报错。
正确的代码:【了解即可】

import java.lang.reflect.Array;
class MyArray<E> {//<E>代表当前类的数组里面的每一个类型是E
    /*
    这种写法只是不报错,不是常用的写法
    而且有缺陷,看下面的代码,比如写一个方法返回当前的数组
     */
    public E[] obj;
    //下面是常用写法(官方自己在用的手段!):
    /*public Object[] obj = new Object[3];
    public E getPos(int pos) {
        return (E) this.obj[pos];
    }*/
    public MyArray() {
    }
    /**
     * 通过反射创建,指定类型的数组
     *
     * @param clazz
     * @param capacity
     */
    public MyArray(Class<E> clazz, int capacity) {//指定数组的类型和容量
        this.obj = (E[]) Array.newInstance(clazz, capacity);
    }
    public void setVal(int pos, E val) {
        this.obj[pos] = val;
    }
    public E getPos(int pos) {
        return (E) this.obj[pos];
    }
    public E[] getArray() {
        return this.obj;
    }
}
public class TestDemo {
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray(Integer.class, 10);
        myArray.setVal(0, 1314);
        myArray.setVal(1, 520);
        Integer[] integers = myArray.getArray();
        System.out.println(myArray.getPos(0));
        System.out.println(myArray.getPos(1));
    }
}

bf4adc0db20f47748ef7a09eacb113f6.png

🧾泛型的上界

语法:
class 泛型类名称<类型形参 extends 类型边界> { ... }
示例:
public class MyArray<E extends Number> { ... }
只接受 Number 的子类型作为 E 的类型实参

MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型

了解: 没有指定类型边界E,可以视为 E extends Object

复杂示例:

public class MyArray<E extends Comparable<E>> { ... }
E必须是实现了Comparable接口的  

🧾泛型的方法

语法:

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

示例:

public class Util {
//静态的泛型方法 需要在static后用<>声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
    E t = array[i];
    array[i] = array[j];
    array[j] = t;
}

使用示例-可以类型推导:

Integer[] a = { ... };
swap(a, 0, 9);
String[] b = { ... };
swap(b, 0, 9);

使用示例-不使用类型推导:

Integer[] a = { ... };
Util.<Integer>swap(a, 0, 9);
String[] b = { ... };
Util.<String>swap(b, 0, 9);

复杂示例:

class Array<E extends Comparable<E>> {//代表将来指定的参数类型E一定是实现了这个接口的
    public E findMax(E[] array) {
        E max = array[0];
        for (int i = 1; i < array.length; i++) {
            //E类型不能直接进行比较,而且在这儿会发生擦除机制,E->Object,但Object没有实现任何的接口,也没有相应的比较方法
            if (max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}
class Array2 {
    /*
    泛型方法:
    方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
     */
    public <E extends Comparable<E>> E findMax(E[] array) {
        E max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}
class Array3 {
    public static <E extends Comparable<E>> E findMax(E[] array) {
        E max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}
class A {
}
public class TestDemo {
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        //Array<A> a = new Array<>();
        Array<Integer> array = new Array<>();
        Integer[] integerArray = {1, 3, 5, 7, 9};
        Integer val1 = array.findMax(integerArray);
        System.out.println(val1);
        System.out.println("---");
        Array2 array2 = new Array2();
        Integer[] integer2Array = {1, 4, 2, 8, 5, 7, 3, 6, 9};
        Integer val2 = array2./*<Integer>*/findMax(integer2Array);
        System.out.println(val2);
        System.out.println("---");
        Integer[] integer3Array = {1, 4, 2, 8, 5, 7, 3, 6, 9};
        Integer val3 = Array3./*<Integer>*/findMax(integer3Array);
        System.out.println(val3);
    }
}

c7db99ad61a24a9caaef73aa0cd36547.png

练习: 

/**
 * @auther: themyth
 * 1.什么是泛型方法:
 * 不是带泛型的方法就是泛型方法
 * 泛型方法有要求:这个方法的泛型的参数类型要和当前的类的泛型无关
 * 换个角度:
 * 泛型方法对应的那个泛型参数类型 和 当前所在的这个类 是否是泛型类,类型是啥 无关
 * 2.泛型方法定义的时候,前面要加<T>
 *     原因:如果不加的话,会把T当作一种数据类型,然而代码中没有T类型那么就会报错
 * 3.T的类型实在调用方法的时候确定的
 * 4.泛型方法可否是静态方法?可以是静态方法
 */
public class TestGeneric<E> {
    //不是泛型方法 (不能直接加static)
    public /*static*/ void a(E e){//这儿不能加static!!!
    }
    //是泛型方法
    public static <T> void b(T t){//T 随着调用时候再去确定类型
    }
}
class Demo{
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        TestGeneric<String> tg = new TestGeneric<>();
        tg.a("abc");
        tg.b("abc");
        tg.b(19);
        tg.b(true);
    }
}

🧾泛型参数存在继承关系的情况  

695dbe0733b4450a82f9cf3c98bb7899.png

🧾通配符

先通过一些案例试着了解通配符:

【1】在没有通配符的时候:
下面的a方法,相当于方法的重复定义,报错

public class Test {
   /* public void a(List<Object> list){
    }
    public void a(List<String> list){
    }
    public void a(List<Integer> list){
    }*/
}

【2】引入通配符: 

public class Demo {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        List<Object> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        List<Integer> list3 = new ArrayList<>();
        List<?> list = null;
        list = list1;
        list = list2;
        list = list3;
    }
}

发现: A 和 B是子类父类的关系,G<A>和G<B>不存在子类父类关系,是并列的
加入通配符?后,G<?>就变成了 G<A>和G<B>的父类

【3】使用通配符: 

import java.util.ArrayList;
import java.util.List;
/**
 * @auther: themyth
 */
public class Test {
   /* public void a(List<Object> list){
    }
    public void a(List<String> list){
    }
    public void a(List<Integer> list){
    }*/
    public void a(List<?> list){
        //内部遍历的时候用object即可,不用?
        for (Object a:list){
            System.out.println(a);
        }
    }
}
class T{
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        Test t = new Test();
        t.a(new ArrayList<Integer>());
        t.a(new ArrayList<String>());
        t.a(new ArrayList<Object>());
    }
}

【4】查看API中应用位置: 

d2d10b66b494420a8069e94f8fae5a68.png

使用通配符后的细节:

import java.util.ArrayList;
import java.util.List;
/**
 * @auther: themyth
 */
public class Test {
    public void a(List<?> list){
        //1.遍历:
        for (Object a:list){
            System.out.println(a);
        }
        //2.数据的写入操作:
        //list.add("abc");-->出错,不能随意的添加数据
        list.add(null);
        //3.数据的读取操作:
        //String s = list.get(0);-->出错,只能用Object类型
        Object o = list.get(0);
    }
}
class T{
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        Test t = new Test();
        t.a(new ArrayList<Integer>());
        t.a(new ArrayList<String>());
        t.a(new ArrayList<Object>());
    }
}

上面我们通过了一些案例已经了解了基本的通配符使用及其注意事项,下面再次详细讲解通配符。

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

通配符解决什么问题?

示例:

//自定义泛型类
class Message<T> {
    private T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Message<String> message = new Message<>();
        message.setMessage("hello,themyth");
        fun(message);
    }

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

以上程序会带来新的问题,如果现在泛型的类型设置的不是String,而是Integer.

0b43ac0014cd42b6ab2f962986e6a8e3.png

我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符"?"来处理。
范例:使用通配符  

//自定义泛型类
class Message<T> {
    private T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Message<String> message = new Message<>();
        message.setMessage("hello,themyth");
        fun(message);//hello,themyth
        Message<Integer> message2 = new Message() ;
        message2.setMessage(99);
        fun(message2);//99
    }

    public static void fun(Message<?> temp) {
        //temp.setMessage(100);此时类型不确定 无法设置
        System.out.println(temp.getMessage());
    }
}

之前我们学习到泛型只有上限,没有下限,而通配符既有上限也有下限。

在"?"的基础上又产生了两个子通配符
? extends 类:设置通配符上限
? super 类:设置通配符下限  

通配符上界  
语法:  
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类   

4ce16c2c016445d3bc4155a42c70f376.png

示例:

class Food {
}

class Fruit extends Food {
}

class Apple extends Fruit {
}

class Banana extends Fruit {
}

class Message<T> { //设置泛型
    private T message;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Message<Apple> message = new Message<>();
        message.setMessage(new Apple());
        fun(message);
        Message<Banana> message2 = new Message<>();
        message2.setMessage(new Banana());
        fun(message2);
    }

    //此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
    public static void fun(Message<? extends Fruit> temp) {
        //temp.setMessage(new Banana()); //仍然无法修改!
        //temp.setMessage(new Apple()); //仍然无法修改!
        System.out.println(temp.getMessage());
    }
}

此时无法在fun函数中对temp进行添加元素,因为temp接收的是Fruit和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素
b8fb33f3995345819f8e7c6e1728f062.png

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

通配符下界
语法:
<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

1945b412b5964123abcb8deac2e9366d.png

示例:

class Food {
}

class Fruit extends Food {
}

class Apple extends Fruit {
}

class Plate<T> {
    private T plate;

    public T getPlate() {
        return plate;
    }

    public void setPlate(T plate) {
        this.plate = plate;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Plate<Fruit> plate1 = new Plate<>();
        plate1.setPlate(new Fruit());
        fun(plate1);
        Plate<Food> plate2 = new Plate<>();
        plate2.setPlate(new Food());
        fun(plate2);
    }

    public static void fun(Plate<? super Fruit> temp) {
        // 此时可以修改!!添加的是Fruit 或者Fruit的子类
        temp.setPlate(new Apple());//这个是Fruit的子类
        temp.setPlate(new Fruit());//这个是Fruit的本身
        //Fruit fruit = temp.getPlate(); 不能接收,这里无法确定是哪个父类
        System.out.println(temp.getPlate());//只能直接输出
    }
}

通配符的下界不能读取特定类型的数据只能写入数据

练习:

import java.util.ArrayList;
import java.util.List;
/**
 * @auther: themyth
 */
public class Test {
    //这是一个main方法,是程序的入口:
    public static void main(String[] args) {
        //a,b,c三个集合是并列关系:
        List<Object> a = new ArrayList<>();
        List<Person> b = new ArrayList<>();
        List<Student> c = new ArrayList<>();
        /*开始使用泛型受限:泛型的上限
        List<? extends Person>
        就相当于:
        List<? extends Person>是List<Person>的父类,是List<Person的子类>的父类
         */
        List<? extends Person> list1 = null;
        //list1 = a;
        list1 = b;
        list1 = c;
        /*开始使用泛型受限:泛型的下限
        List<? super Person>
        就相当于:
        List<? super Person>是List<Person>的父类,是List<Person的父类>的父类
         */
        List<? super Person> list2 = null;
        list2 = a;
        list2 = b;
        //list2 = c;
    }
}

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

为何不能写入数据?

List<? extends T> 表示可以容纳任何 T 的子类类型的数据,但在插入数据时,Java的泛型机制不能确定具体的类型,因此为了防止类型不安全的操作,禁止直接写入数据。

详细解释:

  • 当你使用 List<? extends Number> 时,? 可以是 IntegerDoubleFloatNumber 的任何子类。
  • 但是,在编译时你无法确定 listList<Integer>List<Double> 还是其他子类的 List,所以如果你尝试添加数据,比如 list.add(3.14),可能会破坏列表的类型安全性。
List<? extends Number> list = new ArrayList<Integer>();
list.add(3.14);  // 错误:编译器不知道它是List<Integer>还是List<Double>

编译器禁止你这样做,因为它不知道 list 的确切类型。如果 list 实际上是 List<Integer>,你将 3.14 (类型为 Double) 添加到其中,类型系统的安全性将被破坏。

只能读取数据:

虽然不能往 List<? extends T> 中写入数据,但可以读取它。因为读取数据时,编译器可以保证返回的类型至少是 T 类型或其子类,因此读取是安全的。

List<? extends Number> list = new ArrayList<Integer>(); 
Number num = list.get(0); // 可以读取,返回的类型至少是Number

在这个例子中,你知道 list 中的元素类型至少是 Number,所以读取时可以赋值给 Number 类型的变量。

通配符的下界不能读取特定类型的数据只能写入数据

只允许写入

相对的,List<? super T> 表示该列表可以接受 T 类型及其父类的对象,允许写入 T 类型的数据。这种情况下,因为你只写入 T 或它的子类对象,编译器可以保证类型安全。

List<? super Number> list = new ArrayList<Object>();
list.add(3.14);  // 允许:可以安全地插入Number及其子类

为何不能读取数据?

当你使用 List<? super T> 时,通配符的下界意味着 list 可能是 T 的某个父类的列表。因此,编译器不能确定具体的类型,也无法保证你读取的数据是什么类型。由于这种不确定性,编译器会限制读取操作,只允许将读取的数据视为 Object

详细解释:

  • List<? super Integer> 表示 list 可以是 Integer 或者 Integer 的父类 NumberObject 等的 List
  • 当你尝试从 list 中读取元素时,编译器无法确定具体的类型,因为它可以是 Number 类型、Object 类型甚至更上层的类型。
List<? super Integer> list = new ArrayList<Number>();
Number num = list.get(0);  // 错误:编译器无法确定list中的类型

这种情况下,list 可能是 Object 类型的 List,因此编译器无法允许将读取的元素赋值给 Number 类型。唯一可以保证的类型是 Object,因为所有类最终都继承自 Object

正确的读取方式:

Object obj = list.get(0);  // 可以读取为Object类型

这里,你只能将读取的元素赋值给 Object 类型的变量,因为这是最安全的类型推断,避免了潜在的类型转换错误。

// 通配符上界: 可以读取,但不能写入
List<? extends Number> list1 = new ArrayList<Integer>();
Number num1 = list1.get(0);  // 允许读取
// list1.add(3.14);          // 错误:不能写入

// 通配符下界: 可以写入,但不能读取特定类型
List<? super Integer> list2 = new ArrayList<Number>();
list2.add(10);               // 允许写入
// Integer num2 = list2.get(0); // 错误:无法确定读取的类型
Object obj = list2.get(0);    // 只能读取为Object

🧾为什么要学习泛型?

1.安全性:泛型提供了类型安全的编程环境。通过使用泛型,你可以在编译时捕获类型错误,而不是在运行时出现异常。
这样可以减少运行时错误,并提高代码的可靠性。
2.代码重用性:泛型使得你可以编写通用的代码,能够在不同类型之间共享和重用。通过使用泛型,你可以编写一次代码,并在不同的数据类型上使用它,而无需为每种类型编写重复的代码。
3.集合框架:Java集合框架中的类和接口都使用了泛型。通过使用泛型,你可以在集合中存储和操作特定类型的对象,
而无需进行类型转换。这样可以使代码更加清晰、简洁,并提高性能。
4.API设计:在设计和使用API(应用程序接口)时,泛型可以提供更好的灵活性和可扩展性。
通过使用泛型,API可以与不同类型的数据一起工作,并提供更通用、更强大的功能。

学习泛型可以使你的代码更安全、更灵活,并提高代码的可重用性和可扩展性。它是 Java 编程中的重要概念之一,对于理解和使用 Java 标准库和编写高质量的代码都非常有帮助。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TheMythWS

你的鼓励,我的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值