【JavaSE】----- 泛型和通配符

目录

一、泛型

🍓什么是泛型

二、泛型类

🍓引出泛型

🍓泛型语法

🍓泛型类的使用

🍎语法

🍎示例

🍎类型推导

🍎裸类型

三、泛型的编译

🍓泛型的擦除机制

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

四、泛型的上界

🍓语法

🍓示例

🍓特殊的泛型上界

五、泛型方法

🍓基本语法

🍓静态泛型方法

🍓类型推导

🍓泛型中的父子类关系

六、通配符

🍓通配符的概念

🍓通配符解决什么问题

🍓通配符的上界

🍓泛型与通配符的区别

🍓通配符的上界 --- 父子类关系

🍓通配符的上界 --- 特点

🍓通配符的下界

🍎基本语法

🍎通配符下界 --- 父子类关系

🍎通配符下界 --- 特点


一、泛型

🍓什么是泛型

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

二、泛型类

🍓引出泛型

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。

  • 我们知道数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] strs = new String[10];
  • 所有类的父类,默认为Object类。那么数组是否可以创建为Object?

🌊代码示例

class MyArray{
    public Object[] objects = new Object[10];
    //设置下标元素
    public void set(int pos ,Object val){
        objects[pos] = val;
    }
    //获取下标元素
    public Object get(int pos){
        return objects[pos];
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.set(0,"hello");
        myArray.set(1,100);
        String str = myArray.get(0); //编译报错
        System.out.println(str);
    }
}

运行结果

✨解决办法:

  • 已知 0 下标存放的是字符串,但是获取 0 下标的元素时使用String类型的变量接收返回值,还是会报错,要解决这个错误必须进行强制类型转换

通过以上代码发现:

  • Object 类型中可以存放任何类型的数据
  • 0 下标存放的就是字符串,但是编译时会报错。必须进行强制类型转换。

虽然在这种情况下,当前数组任何数据都可以存放,但是更多情况下,我们还是希望他只能够持有一种数据类型而不是同时持有这么多类型。因此就引出了泛型。

泛型的主要目的:是指定当前的容器,要持有什么类型的对象,让编译器去做检查。此时就需要把类型作为参数传递。需要什么类型,就传入什么类型。

🍓泛型语法

class 泛型类名称<类型形参列表> {
    // 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类 (这里可以使用类型参数) {
    // 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
    // 可以只使用部分类型参数
}

🍓泛型类的使用

🍎语法

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

🍎示例

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

注意:泛型只能接受类,所有的基本数据类型必须使用包装类!

🍎类型推导

MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 String

编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

🌊了解了泛型的使用后,就可以对前面的代码进行修改:

/**
 * 泛型类
 * @param <T> T:是一个占位符
 */
class MyArray<T>{
    /**
     * error 
     * 不能实例化泛型类型的数组
     */
    //public T[] objects = new T[10]; 
   public T[] objects = (T[])new Object[10]; //这种写法并不是一个好的写法,目前只是为了代码运行起来
    
    //设置下标元素
    public void set(int pos ,T val){
        objects[pos] = val;
    }
    //获取下标元素
    public T get(int pos){
        return objects[pos];
    }
}
public class TestDemo {
    public static void main(String[] args) {
        /**
         * 使用泛型类需要通过<>指定存放的数据类型
         * new MyArray<String>() -> 第二个<>里面,写不写数据类型都可以
         */
        MyArray<String> myArray = new MyArray<String>();
        myArray.set(0,"hello");
        /**
         * error
         * 在编译时会自动进行类型检查,因为是String 类型,不可以存放整型数据
         */
        //myArray.set(1,100); 
        String str = myArray.get(0); //不需要进行类型的强制转换

        //存放整型数据
        MyArray<Integer> myArray1 = new MyArray<>();
        /**
         * error
         * 基本类型不可以作为泛型类型的参数
         */
        //MyArray<int> myArray2 = new MyArray<>(); 
    }
}

💥类名后的 <T> 代表占位符,表示当前类是一个泛型类。

【规范】类型形参一般使用一个大写字母表示,常用的名称有:

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

🍎裸类型

📔说明:

  • 裸类型是一个泛型类但没有带着类型实参
  • 不去指定泛型对象持有的类型,这样的一个类型就是裸类型。

🌊代码示例

public class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.set(0,"hello");
        myArray.set(1,123);
        System.out.println(myArray.get(0));
        System.out.println(myArray.get(1));
    }
}
//运行结果:
hello
123

💥注意: 不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制

⭐小结:

  • 泛型是将数据类型参数化,进行传递
  • 使用 <T> 表示当前类是一个泛型类。
  • 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换。

三、泛型的编译

🍓泛型的擦除机制

class MyArray<T>{
   public T[] objects = (T[])new Object[10]; 

    //设置下标元素
    public void set(int pos ,T val){
        objects[pos] = val;
    }
    //获取下标元素
    public T get(int pos){
        return objects[pos];
    }
}

📔说明:

  • 通过命令:javap -c 查看字节码文件,可以看到所有的T都是Object。
  • 在编译的过程当中,将所有的T替换为Object。将这种机制称为:擦除机制
  • Java的泛型机制是在编译期实现的,而泛型机制实现就是通过擦除机制实现的,并在编译期间完成类型的检查。

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

Java中不允许实例化泛型数组,如果一定要建立一个泛型数组,正确的做法只能通过反射来实现,还有一种做法就是文章前面使用的一种(便捷)非正确的写法来创建泛型数组,这里再详细说明一下。

1、通过便捷的方法创建,大部分情况下不会出错。

class MyArrayList<T> {
    public T[] elem;
    public int usedSize;

    public MyArrayList(int capacity) {
        this.elem = (T[]) new Object[capacity];
    }
}
  • 数组和泛型之间的一个重要区别是它们如何强制执行类型检查。
  • 具体来说,数组在运行时存储和检查类型信息。然而,泛型在编译时检查类型错误。

根据擦除机制,也可以解释为什么Java当中不能实例化泛型数组:

  • 因为泛型数组前面的占位符会被擦除成Object,实际上是创建一个Object数组。而Object数组中什么类型都能放,这就导致取数据时不安全,因为不能确定数组里面存放的元素全部都是预期的类型,所以为了安全,Java不允许实例化泛型数组。

2、通过反射创建,现在只给出代码,具体为什么要这么做后续介绍反射再详细说明。

import java.lang.reflect.Array;
class MyArrayList<T> {
    public T[] elem;
    public int usedSize;

    public MyArrayList(Class<T> clazz, int capacity) {
        this.elem = (T[]) Array.newInstance(clazz, capacity);
    }
}

四、泛型的上界

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

🍓语法

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

🍓示例

public class MyArrayList <E extends Number> {
    ...
}

📔说明:

  • Number Integer,Float,Double 等相关数字类型的父类。
  • 所以 MyArrayList 泛型类的参数只能接收 Number 类以及 Number的子类,像这样就给泛型的类型传参做了约束,这个约束就是泛型的上界,泛型类被类型边界约束时,只能指定泛型类持有类型边界这个类及其子类。

🌊代码示例 

class MyArrayList<T  extends Number> {

}

public class Test {
    public static void main(String[] args) {
        MyArrayList<Integer> myArrayList1 = new MyArrayList<>();
        MyArrayList<Float> myArrayList2 = new MyArrayList<>();
        MyArrayList<Number> myArrayList3 = new MyArrayList<>();
    }
}

如果对 MyArrayList 泛型类传入的参数是 String,编译就会报错。因为String不是Number的子类。

🍓特殊的泛型上界

🌊写一个泛型类,求出数组中元素的最大值

class Alg<T extends Comparable<T>>{
    public T findMax(T[] array){
        T max = array[0];
        for (int i = 0; i < array.length; i++) {
            if(max.compareTo(array[i])<0){
                max = array[i];
            }
        }
        return max;
    }
}
public class Test {
    public static void main(String[] args) {
        Alg<Integer> alg1 = new Alg<>();
        Integer[] arr1 = {1,12,6,3};
        System.out.println(alg1.findMax(arr1));

        Alg<String> alg2 = new Alg<>();
        String[] str = {"hello","abc","qwer"};
        System.out.println(alg2.findMax(str));
    }
}

运行结果:
12
qwer

📔说明:

  • 由于引用类型的比较需要使用Comparable接口来判断大小,所以传入的类必须需要实现Comparable接口,上面这个泛型的类型参数的上界是一个特殊的上界,表示所传入的类型必须实现Comparable接口,实现了Comparable接口的类,就是Comparable的子类。
  • 如果是需要通过实现某一个接口来达到预期功能的类型,使用泛型时要指定泛型的上界,并且传入的类型必须实现该上界接口。
  • 如果没有指定类型边界 E,可以视为 E extends Object

五、泛型方法

有泛型类,那么就一定有泛型接口,泛型方法,其中泛型接口与泛型类的创建和使用是一样的,所以我们重点介绍泛型方法的创建与使用。

🍓基本语法

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

🌊代码示例:对上面求数组中最大值的代码可以写成泛型方法

class Alg<T extends Comparable<T>>{
    //泛型方法
    public <T extends Comparable<T>>T findMax(T[] array){
        T max = array[0];
        for (int i = 0; i < array.length; i++) {
            if(max.compareTo(array[i])<0){
                max = array[i];
            }
        }
        return max;
    }
}

🍓静态泛型方法

  • 如果是一个static修饰的静态方法,<类型形参列表>不可以省略
  • 因为静态方法不依赖于对象,它的使用不用实例化对象,所以必须有单独的类型参数列表来指定持有的对象类型。

🌊代码示例 

//因为静态方法不依赖与对象,所以类名后可以不用写<T extends Comparable<T>>
class Alg2{
    //静态泛型方法
    public static <T extends Comparable<T>>T findMax(T[] array){
        T max = array[0];
        for (int i = 0; i < array.length; i++) {
            if(max.compareTo(array[i])<0){
                max = array[i];
            }
        }
        return max;
    }
}

🍓类型推导

和泛型类一样,泛型方法也有类型推导的机制,如果不使用类型推导,泛型方法是这么使用的:

使用了类型推导,可以不写泛型参数:

🍓泛型中的父子类关系

public class MyArrayList<E> { ... }
// MyArrayList<Object> 不是 MyArrayList<Number> 的父类型
// MyArrayList<Number> 也不是 MyArrayList<Integer> 的父类型

在泛型类中没有父子类关系,因为泛型的擦除机制会在泛型类编译时将类后面的占位符全部擦除,其他的占位符都会被替换成 Object

🌊代码示例  

public class Test {
    public static void main(String[] args) {
        Alg<Integer> alg1 = new Alg<>();
        Alg<String> alg2 = new Alg<>();
        System.out.println(alg1);
        System.out.println(alg2);
    }
}
运行结果:
Alg@4554617c
Alg@74a14482

六、通配符

🍓通配符的概念

📔说明:

  • ? 用于在泛型的使用,即为通配符。
  • 与泛型不同的是,泛型T是确定的类型,传入类型实参后,它就确定下来了,而通配符更像是一种规定,规定一个范围,表示你能够传哪些参数。
  • 一个泛型类名尖括号之内仅含有一个 ?,获取元素时由于不能确定具体类型,只能使用Object引用接收。

🍓通配符解决什么问题

  • 通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Student 是 Person 的子类,那么 List<Student> 也应该是 List<Person> 的子类。但是泛型是不支持这样的父子类关系的。
  • 泛型 T 是确定的类型。一旦传了,那么类型就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。
  • 也可以这样理解:泛型T就像是个变量,等着你将来传一个具体的类型,而通配符则是一种规定,规定你能传哪些参数。

🌊代码示例

class Alg3{
    //使用泛型打印
    public static <T> void print1(ArrayList<T> list){
        for (T x: list) {
            System.out.println(x);
        }
    }
    //使用统配符打印
    public static void print2(ArrayList<?> list){
        for (Object x: list) {
            System.out.println(x);
        }
    }
}

📔说明:

  • 使用泛型 T 能够确定传入的类型就是 T 类型,所以使用 T 类型的变量接收。
  • 通配符?没有设置边界的情况下,默认上界是Object类型,为了包中安全,所以使用Object 类型的变量接收。

🍓通配符的上界

通配符也有上界,可以限制传入的类型必须是上界这个类或者是这个类的子类。

基本语法:

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

🌊代码示例一

class Alg3{
    public static void printAll(ArrayList<? extends Number> list) {
        for (Number n: list) {
            System.out.println(n);
        }
    }
    public static void main(String[] args) {
        printAll(new ArrayList<Integer>());
        printAll(new ArrayList<Number>());
        printAll(new ArrayList<Float>());
    }
}

📔说明:

  • printAll方法的一个形参设置了类型的上界Number,所以在遍历这个顺序表的时候,需要使用Number来接收顺序表中的对象,并且使用该方法时,只能遍历输出Number及其子类的对象。

🌊代码示例二:假设有如下关系

class Animal{}
class Cat extends Animal{}
class Dog extends Animal{}

📔说明:

  • AnimalCat,Dog的父类,使用泛型指定类型后,那么指定什么类型,那它就会输出什么类型的对象,比如你指定顺序表中放的类型是Cat,那么它调用的就是Cat对象的toString方法。

🌊代码示例 

//泛型
public static <T extends Animal> void print1(ArrayList<T>list){
    for (T s:list) {
        System.out.println(s);
    }
}

public static void main(String[] args) {
    Cat cat = new Cat();
    Dog dog = new Dog();

    ArrayList<Cat> list1 = new ArrayList<>();
    ArrayList<Dog> list2 = new ArrayList<>();
    list1.add(cat);
    list2.add(dog);
    print1(list1);
    print1(list2);
}
运行结果
Cat@4554617c
Dog@74a14482

📔说明:

  • 使用通配符是规定能够使用 Animal及其子类,无伦传入哪一个子类对象,都是父类的引用接收,但是具体哪一个子类,并不清楚。

🌊代码示例 

//通配符
public static void print2(ArrayList<? extends Animal> list){
    for (Animal s:list) {
        System.out.println(s);
    }
}

public static void main(String[] args) {
    Cat cat = new Cat();
    Dog dog = new Dog();

    ArrayList<Cat> list1 = new ArrayList<>();
    ArrayList<Dog> list2 = new ArrayList<>();
    list1.add(cat);
    list2.add(dog);
    print2(list1);
    print2(list2);
}

运行结果
Cat@4554617c
Dog@74a14482

📔说明:

  • 父类引用子类对象发生向上转型,当打印子类对象时,会优先使用子类的toString方法,所以输出结果与使用泛型是一样的。
  • 但是泛型和通配符的效果是不一样的,泛型是你传入什么类型,那这个类就会持有什么类型的对象,而通配符是规定一个范围,规定你能够传哪一些类型。

🍓泛型与通配符的区别

  • 对于泛型实现的 print1 方法, <T extends Animal> 对T进行了限制,只能是Animal的子类。比如:传入Cat,那么类型就是Cat
  • 对于通配符实现的 print2 方法,首先不用再static后使用尖括号,其次相当于对Animal进行了规定,允许你传入Animal 的子类。具体哪个子类,此时并不清楚。比如:传入了Cat,实际上声明的类型是Animal,使用多态才能调用Cat的toString方法

🍓通配符的上界 --- 父子类关系

// 需要使用通配符来确定父子类型
MyArrayList<? extends Number> 是 MyArrayList <Integer>或者 MyArrayList<Double>的父类类型
MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型

💥泛型的上界不支持上面的父子类关系,但是通配符的上界是支持的。

🍓通配符的上界 --- 特点

⭐使用通配符上界不可以写入数据

🌊代码示例

import java.util.ArrayList;
import java.util.List;
public class TestDemo {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
        ArrayList<Double> arrayList2 = new ArrayList<>();
        List<? extends Number> list1 = arrayList1;
        List<? extends Number> list2 = arrayList2;
        //此时list的引用的子类对象有很多,再添加的时候,任何子类型都可以,为了安全,java不让这样进行添加操作。
        list1.add(0,1); //error
        list2.add(1,10.5); //error
    }
}

如果对 list 中添加数据就会报错! 

📔说明:

  • 原因很简单,list中存储的可能是Number也可能是Number的子类。此时添加任何类型的数据都不可以,无法确定到底是哪种类型。

⭐使用通配符上界可以获取数据

🌊代码示例

import java.util.ArrayList;
import java.util.List;
public class TestDemo {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
        ArrayList<Double> arrayList2 = new ArrayList<>();
        List<? extends Number> list1 = arrayList1;
        List<? extends Number> list2 = arrayList2;
        list1.get(0);//可以通过
        Integer i = list1.get(1); //error 编译错误,因为在获取数据时只能确定是Number的子类,但是不能确定获取的数据就是 Integer 类型
    }
}

📔说明:

  • Number a = list.get(0);可以通过,因为此时获取的元素肯定是Number的子类。
  • 但是:Integer i = list.get(1);会出现编译错误,因为不能够确定获取到的数据就是 Integer 类型的数据。

上面的代码中 list 可以引用的子类对象有很多,编译器无法确定具体的类型。为了安全起见,只允许进行读取。

🍓通配符的下界

🍎基本语法

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

与泛型不同,通配符可以拥有下界,语法层面上与通配符的上界的区别是将关键字 extends 改为 super

🌊代码示例

public static void print(ArrayList<? super Number> list){
    for (Object s:list) {  //只能使用Object接收,因为传入的类是Number或者是Number的父类
        System.out.println(s);
    }
}
public static void main(String[] args) {
    print(new ArrayList<Number>());
    print(new ArrayList<Object>());
}

📔说明:

  • print方法的一个形参设置了类型的下界Number,所以在遍历这个顺序表的时候,只能使用Object来接收顺序表中的对象,并且使用该方法时,只能遍历输出Number及其父类的对象。
  • 例如<? super Integer>代表可以传入的实参的类型是Integer或者Integer的父类类型(如NumberObject) 

🍎通配符下界 --- 父子类关系

MyArrayList<? super Integer> 是 MyArrayList<Integer>的父类类型
MyArrayList<?> 是 MyArrayList<? super Integer>的父类类型

📔说明:

  • ? ? extends .... ? super ....的父类,通配符之间的父子类关系,是指通配符所“规定的”范围,判断父子类是根据范围来判断的。

🍎通配符下界 --- 特点

使用通配符下界可以写入数据(写入的数据的对象只能是下界类或它的子类)

🌊代码示例

import java.util.ArrayList;
class Person{}
class Student extends Person{}

public class Test {
    public static void main(String[] args) {
        ArrayList<? super Person> arrayList = new ArrayList<Person>();
       // ArrayList<? super Person> arrayList1 = new ArrayList<Student>(); //error arrayList1 只能引用Person或Person的父类类型
        //添加的元素是 person 或 person 的子类
        arrayList.add(new Person());
        arrayList.add(new Student());
    }
}

⭐使用通配符下界不可以获取数据

🌊代码示例

import java.util.ArrayList;
class Person{}
class Student extends Person{}

public class Test {
    public static void main(String[] args) {
        ArrayList<? super Person> arrayList = new ArrayList<Person>();
        arrayList.add(new Person());
        arrayList.add(new Student());
        Object o = arrayList.get(0); //在获取数据时只能使用Object类型的变量接收

        //因为构造对象时可以构造Person父类类型的arrayList,取出的对象不一定是Person或者Person的子类
        Person p = arrayList.get(0);
        Student s = arrayList.get(0);
    }
}

📔说明:

  • 在添加元素时,只能添加类型是 Person 或者 Person 的子类的元素。
  • 在获取元素时,只能使用 Object 引用接收,不能使用其他的引用接收。
  • 因为构造对象时可以构造 Person 父类类型的 arrayList,读取数据时,不能确定读取的是哪一个子类。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

潇湘夜雨.

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值