【Java】集合类, 包装类和泛型

初识集合框架

集合框架, 其包含了 Java 提供的一些接口和类. 这些类实现了一些常见的数据结构, 从而使得我们可以简单的进行一些常见的数据操作.

但是, 这并不说明我们只需要会用集合框架中的这些类即可. 我们需要了解其背后的数据结构, 也就是其底层的原理, 同时了解一些它们的具体代码, 从而帮助我们了解各个集合类的优缺点, 从而在对应的场景下选择出最合适的集合类进行使用.

下面是一个集合框架的简单关系图.

在这里插入图片描述

但是在学习这些集合类前, 我们还需要对一些基础内容进行介绍, 防止我们到时候需要阅读源码时看不懂一些内容.

包装类

包装类的概念

在很多时候, 我们书写代码的时候要用到类型, 此时基本数据类型就无法使用了. 例如我此时有一个 Object 类型的对象, 然后我想要往里面装一个 int 类型, 很明显这个操作是不可行的.

这很明显不符合面向对象的思想, 明明 Object 类都是顶级父类了, 却无法容纳这些基本数据类型. 为了解决这种问题, Java 中提供了一些类, 对应着这些基本数据类型, 这些类就被称作是包装类.

下面是基本数据类型及其对应的包装类

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

此时可能有人就要问了: 那明明都有这种 Integer 这样的包装类了, 那还要 int 这种基本类型干嘛呢? 干脆直接全部用包装类不就好了吗?

实际上这可以说是一定的历史问题, Java 的诞生是受到了 C/C++ 语言的影响的, 因此那时就有了基本类型这种东西, 但是如果后面反过头去删除掉这些基本类型, 会产生一些兼容性的问题, 例如以前的代码使用了基本类型就无法在新版本 JDK 中运行这样的问题.

另外, 基本类型相较于包装类型来说, 使用效率更高, 占用内存更小, 因此也是有一定优势的.

装箱和拆箱

装箱和拆箱实际上指的就是将基本数据类型与包装类互相转换的过程, 其中主要使用的就是包装类内部提供的两个方法, 我们看简单的例子

public class Main {
    public static void main(String[] args) {
        int a = 0;
        // 通过valueOf()方法将int类型转换为Integer类型
        Integer b = Integer.valueOf(a);
        // 通过intValue()方法将Integer类型转换为int类型
        int c = b.intValue();
    }
}

但是如果每次转换都要用方法就非常的麻烦, 因此 Java 提供了自动装箱和自动拆箱, 也就是我们可以不用方法转换, 直接赋值即可

public class Main {
    public static void main(String[] args) {
        int a = 0;
        // 将a转换为Integer然后赋值给b
        Integer b = a;
        //将b转换为int然后赋值给c
        int c = b;
    }
}

Integer源码分析

下面我们来看一道题目, 我们可以看一下这个代码的输出结果是什么

public class Main {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        
        Integer c = 128;
        Integer d = 128;
        System.out.println(a == b);
        System.out.println(c == d);
    }
}

运行一下, 然后查看结果

在这里插入图片描述

此时可能有人要问了: 这不是 127 == 127 和 128 == 128 吗, 为啥后面那个是 false 呢?

首先, 如果问出了这种问题, 证明此时你对于这个包装类的理解还没有到位. 为什么这样说, 因为我们说过包装类是类, 因此这里的这些 a b c d 都是对象, 而对象如果我们通过 == 去判断, 是判断引用是否相同. 因此按照道理来说, 我们这里创建了四个不同的对象, 无论怎么比较, 都应该是两个 false才对.

但是此时我们根据结果就可以发现, 有一个是 true, 证明前面两个对象是同一个对象. 那么为什么前面的这两个是同一个对象呢? 我们此时就可以去 Integer 的源码中看看为什么.

此时又要有人问了: Integer 的源码那么多, 难道你要我一行一行看嘛?

这里需要注意的一点是, 对于一个没有阅读过源码的人, 如果需要阅读源码, 最好是先找一个切入点, 然后通过这个切入点先开辟一条逻辑链, 此时中间的具体细节我们不需要去关注, 可以通过命名/注释这样的东西进行猜测. 在梳理完一条完整的逻辑链后, 我们可以在对逻辑链中的具体细节进行逐个了解.

例如我想要知道 Integer 对象是如何创建的, 此时我们阅读源码的过程中, 一定会看到大量的初始化/校验工作, 此时这些工作我们在第一次阅读的时候我们都可以不用关注, 我们就直接看这个对象是如何被创建出来的即可.

那么接下来我们就来通过源码来看看上面的这个 a == b 为什么为 true. 首先我们就需要去找到一个切入点, 那么结合我们上面的自动装箱的知识, 我们可以知道, 实际上这里的赋值是将 int 类型自动借助 valueOf() 方法然后转为了 Integer 类型, 因此我们就可以得到一个切入点, 应该是这个 valueOf() 方法将 int类型转换为 Integer 对象的时候做了一些工作

我们通过Ctrl + 左键点击 Integer, 就可以进入源码观看其的实现

在这里插入图片描述

然后可以通过Ctrl + F来搜索到 valueOf() 方法, 此时可能会发现有好几个重载的 valueOf() 方法, 由于我们上面讨论的是涉及到的是自动装箱, 也就是将 int 类型转换为 Integer 的, 因此我们找到参数为 int 类型的即可

public static Integer valueOf(int i) {
    // 使用if判断i是否在一个范围里面, 如果在就直接一个数组中存储的对象
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    // 不在就创建一个新的对象
    return new Integer(i);
}

看到源码, 我们会发现, 当我们输入的值 i 在一个范围内的时候, 它是直接从一个现存的 Integer 数组里面返回对象. 那此时我们就可以进行猜测, 前面我们创建 127 的时候, 这个数据应该是正好还在范围里, 而这个 128 应该就是正好就在范围外了. 所以 128 的对象是新建的, 因此两个对象引用不同. 而 127 的对象则都是从数组里面获取的, 因此两个对象的引用是同一个.

接下来我们可以继续深入, 来看看这个范围的确切数字是多少. 下面是这个内部类 IntegerCache 的源码

private static class IntegerCache {
    // 直接定义 low 为 -128
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        // 这里有一个源码自带的注释, 意思是这个high可以通过配置来进行更改
        int h = 127;
        
        // 这里就是对配置项的一些处理, 我们可以直接忽略
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        // 如果配置项不为空 进入if处理这个配置项
        if (integerCacheHighPropValue != null) {
            try {
                // 配置读取出来是一个字符串, 需要转换为int
                int i = parseInt(integerCacheHighPropValue);
                // 在默认127和配置值中取max
                i = Math.max(i, 127);
                // 保证这个配置值不能超过int最大值
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
                // 如果配置项无法转为int, 无视
            }
        }
        // 将 h 赋值给high
        high = h;

        // 创建数组, 给里面创建Integer对象
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // 确保high一定 >= 127, 否则报错
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

此时我们就可以得知这个 IntegerCache 的默认范围是 [-128, 127] , 当然我们也可以通过配置项来更改这个 high 来增加范围, 我们这里就不介绍如何修改了, 感兴趣的可以自行了解

泛型

初识泛型

泛型, 我们可以将其理解为是一种参数, 但是这个参数定义的是类型. 例如我想要一个方法适用于多种类型, 我可以不用重载很多参数类型不同的方法, 而是用一个泛型参数, 在使用这个方法的时候把类型传过来就行

但是泛型也可以用于类和接口, 比如我们之前使用 Comparable 接口的时候就接触过泛型. 并且泛型一般常用于类和接口中, 在方法中用的不多, 因此我们这里先看泛型类的使用

定义的语法我们通过例子来看, 下面是两个创建泛型类的例子

//一个泛型参数的泛型类
class Test1<T> {

}
//多个泛型参数的泛型类
class Test2<T1, T2> {

}

这里的 T 以及 T1, T2 这样的, 都可以理解为是参数名称, 可以自定义, 但是这里也有一些规范, 我们可以简单了解一下

泛型参数的名称一般使用单个大写字母表示, 常用的名称如下所示

E 表示 Element

K 表示 Key

V 表示 Value

N 表示 Number

T 表示 Type

S, U, V 等等 - 第二、第三、第四个类型

创建泛型对象的语法, 我们这里就以上面的这两个类型为例子, 创建两个泛型对象.

public class Demo {
    public static void main(String[] args) {
        // new Test1<>() 也可以写为 new Test1<Integer>(), 但是这里省略了
        Test1<Integer> integerTest1 = new Test1<>();
        Test2<Integer, Double> integerDoubleTest2 = new Test2<>();
    }
}

这里需要注意的是, 此时提供的类型不能是基本数据类型, 可以看到我们的例子中, 也是用的是基本类型对应的包装类.

泛型的使用

首先我们来看一个例子

实现一个类, 类中包含一个数组和两个方法.

数组可以存放任何类型的数据, 两个方法分别是设置对应下标的元素和获取对应下标的元素

首先我们看到这个类的需求, 要求的是里面的数组能够存放任何类型的数据, 那么此时就只能令这个数组的数据类型为 Object 类. 因为无论是什么类, 都一定会继承 Object 类, 那么无论我们存储什么数据进去, 他都会向上转型

既然确定了数组的元素类型, 那么写另外两个方法也是很简单的事情了.

class MyArray{
    // Object类型数组
    Object[] array = new Object[100];

    public void set(int index, Object input){
        this.array[index] = input;
    }

    public Object get(int index){
        return this.array[index];
    }
}

这个时候我们可以尝试一下往里面加入各种数据, 但是会发现取出的时候如果我们直接取出则会报错, 因为从父类往子类转型是要我们手动进行强制类型转换

public class Demo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        // 在1位置放入int类型
        myArray.set(1, 10);
        // 在2位置放入String类型
        myArray.set(2, "123");

        //下面两条都会报错
        int a = myArray.get(1);
        String b = myArray.get(2);
    }
}

在这里插入图片描述

实际上, 这个类如果像上面这样设定, 那么在实际运作中效果就不是很好. 比如我们假设这个数组中有100个数据, 如果我们需要取出每一个元素, 那么我们需要强制类型转换, 而强制类型转换就要求我们清晰的知道每一个数据是什么类型.

那此时可能有人说: 那我直接全部使用 Object 类型接受不就好了吗?

这个确实解决了当前的问题, 但是下一个问题又出现了, 就是我们不知道这个对象是什么类型的, 那么我们如何的去操作他呢?

因此在这个 MyArray 的数组中最好就不要去一个对象中的数组装各种各样类型的数据, 而是一个对象中数组就装一个类型, 同时允许不同的 MyArray 对象中的数组装不同的类型. 那么此时就可以用上泛型, 在创建对象的时候传递类型参数, 从而让我们每一个对象的数组都只能存放刚开始创建时指定的类型的数据, 而不可以存放任意类型的数据

修改代码后如下

class MyArray<T>{
    
    Object[] array = new Object[100];

    public void set(int index, T t) {
        this.array[index] = t;
    }

    public T get(int index) {
        return (T) this.array[index];
    }
}

在上面这段代码我们可以看到, 我们在类名的旁边设置了泛型参数, 然后除了在初始化成员数组的时候直接初始化了 Object 类型的数组, 其他操作我们都将 Object 换成了泛型参数 T . 此时, 这个泛型参数就可以帮我们进行对应的类型操作.

例如, 当我们创建对象的时候传递Integer类型过去, 那么我们在放入元素就只能放入Integer类型. 同时在拿出元素时, 它也会自动转换为Integer类型, 而不需要我们手动转换

public class Demo {
    public static void main(String[] args) {
        //传入一个Integer类型参数
        MyArray<Integer> test1 = new MyArray<>();
        test1.set(1, 10);
        
        //不能传入String类型, 类型不匹配
        test1.set(2, "123");
        
        //可以直接拿出来而不再需要进行类型转换
        int a = test1.get(1);
    }
}

这里我们也可以看出泛型的一个核心功能就是提供了类型的检查, 从而防止将各种各样的数据都装进来.

但是此时看了上面的代码, 有人可能会产生一些奇妙的想法: 我能不能直接使用泛型数组呢? 那下面我们先来尝试一下

首先如果我们像下面的这样写, 甚至是无法编译通过的

在这里插入图片描述

也就是说我们无法直接通过泛型参数来去 new 出一个泛型数组, 因为它毕竟不是一个具体的类型. 但是我们可以通过另外的一种方式获取一个泛型数组的引用, 就是通过强制类型转换. 如下所示

class MyArray2<T>{
	T[] array = (T[])new Object[100];

    public void set(int index, T t) {
        this.array[index] = t;
    }

    public T get(int index) {
        return this.array[index];
    }
}

但是, 这种写法并不推荐. 为什么?

我们可以看一下如下代码

public class Demo {
    public static void main(String[] args) {
        // 使用MyArray2创建一个String数组
        // 理论上此时里面的array成员就是一个String类型的
        MyArray2<String> stringArray = new MyArray2<>();

        // 将array成员赋值给一个String数组
        String[] tmp = stringArray.array;
    }
}

此时在编译器中看着似乎没有问题, 尝试运行, 发现报错, 原因是类型转换异常
在这里插入图片描述

这就很奇怪了, 我们的 array 成员不应该在我们泛型的指引下, 强转为了一个String类型的数组吗? 为什么这里却说这个成员是 Object 类型的呢?

那么这就不得不提到泛型的擦除机制.

擦除机制

我们就直接看上面写的代码的反编译结果

public class Demo {
    public static void main(String[] args) {
        // 使用MyArray2创建一个String数组
        // 理论上此时里面的array成员就是一个String类型的
        MyArray2<String> stringArray = new MyArray2<>();

        // 将array成员赋值给一个tmp的String数组
        String[] tmp = stringArray.array;
    }
}

class MyArray2<T>{
    T[] array = (T[])new Object[100];


    public void set(int index, T t) {
        this.array[index] = t;
    }

    public T get(int index) {
        return this.array[index];
    }
}

在这里插入图片描述

可以发现我们在 MyArray2 中使用的泛型参数根本就不存在, 而是直接被替换为了 Object . 实际上, 这正是由于 Java 中泛型的擦除机制所导致的, 泛型的擦除机制指的就是在将代码编译为字节码文件的过程中, 会将所有的泛型参数直接替换掉. 因此 Java 中的泛型只能在我们的编译期间提供一个类型检查, 当代码编译完后生成的字节码文件就已经没有泛型类的任何信息了.

这也说明了, 为什么上面我们的代码没有在编译器中报错, 因为确实它在编译时期是一个String类型的数组, 但是编译完后, 泛型参数被擦除为 Object, 此时代码变为 (Object[])new Object[10], 那么一旦运行代码, 就会由于 Object 类型的数组无法转换为String类型的数组而抛出异常.

这也是为什么我们这里不推荐使用 T[] array = (T[])new Object[100] 这种方式创建泛型数组的原因之一. 因为它这种写法就类似于一种只存在编译期间的面具, 让编译器认为这个类型是没有问题的. 到了运行时期后, 脱下了面具, 才会暴露出问题.

此时我们也可以回去尝试一下, 如果使用的是 MyArray 会发生什么.

public class Demo {
    public static void main(String[] args) {
        // 使用MyArray创建一个String数组
        MyArray<String> stringArray = new MyArray<>();

        // 此时这里直接报错, Object类型数组无法转换为String类型数组
        String[] tmp = stringArray.array;
    }
}

可以看到这里编译器就直接报错了

在这里插入图片描述


实际上这里真的希望创建泛型数组, 那么则需要通过反射来进行创建.

由于这里暂时并不想涉及过多这部分知识, 因此我们这里暂时仅仅是演示, 不做讲解, 感兴趣的可以自行了解一下

我们首先修改一下 MyArray2 的代码

class MyArray2<T>{
    T[] array;
	
    // 通过反射创建泛型数组
    // 相当于直接通过具体类型创建
    public MyArray2(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }

    public void set(int index, T t) {
        this.array[index] = t;
    }

    public T get(int index) {
        return this.array[index];
    }
}

然后再次尝试运行如下代码

public class Demo {
    public static void main(String[] args) {
        // 使用MyArray创建一个String数组
        MyArray2<String> stringArray = new MyArray2<>(String.class, 5);

        // 将array成员赋值给一个String数组
        String[] tmp = stringArray.array;
    }
}

此时没有报错, 代码正常结束

在这里插入图片描述


回到擦除机制, 我们上面看到了擦除机制会将我们的泛型参数擦除为 Object 类型, 但是实际上擦除机制并不一定是将泛型类擦除为 Object 类, 而是擦除为泛型的上界类型, 如果没有指定泛型的上界那么就默认擦除为 Object 类, 那么什么是泛型的上界呢?

泛型的上界

有时候对于一些泛型类, 我需要它传入的类型能够有一些限制. 比如我希望传入的类能够进行比较, 而根据我们之前的学习, 如果希望实现对象的比较, 则需要通过实现 Comparable 接口来进行, 那么此时就可以借助泛型的上界来对泛型类做出限制

例如我们这里就限制泛型类要实现 Comparable 接口, 那么我们就可以这样写

// 使用extents关键字定义上界
class Test<T extends Comparable<T>> {
	// Comparable接口的泛型参数也提供T
    // 因为我们这里定义上界就是要去让T是能够比较的
    // 而Comparable的泛型参数实际上就是告诉它你要比较的东西是什么类型的
    // 那么自然给Comparable接口提供的就是T
}

这个上界并不是说只能是接口, 也同样的可以是一个类. 但是要求都类似的. 如果是一个类, 那么就要求传入的这个类必须要是对应类本身或子类, 如果是一个接口, 那么就要求传入的这个类必须实现对应的接口.

此时假设我们尝试去传一个没有实现 Comparable 接口的类, 那么就会报错提示

public class Demo {
    public static void main(String[] args) {
        // 创建Test对象, 尝试传入Student类型
        Test<Student> t = new Test<>();
    }
}

class Test<T extends Comparable<T>> {

}

// 一个学生类
class Student{
    
}

在这里插入图片描述

这里可能有人要问了: extends 不是用来继承的吗? 这里怎么又用来给泛型类做限制了?

实际上, 随着我们学习的深入, 我们会发现在计算机领域中的非常多的术语, 会重复的出现, 但是意思却都不一样, 所以如果想要理解一个词/术语的用处或者是意思, 则一定要结合具体的情景去理解.


下面我们就通过一个例子, 来帮助我们更好的理解这个泛型上界的使用

实现一个类, 在这个类中写一个方法求任意类型数组中的最大值

写一个求数组中所有元素的最大值的方法, 我们这里先按照传统思路简单实现一个代码.

class Test<T> {
    public T getMax(T[] array){
        T ret = array[0];
        for(int i = 1; i < array.length; i++){
            //下面这一行报错
            if((T)array[i] > ret){
                ret = array[i];
            }
        }
        return ret;
    }
}

这段代码都不用运行, 就知道肯定有问题, 因为直接编译报错: T 类型不能用>比较.
在这里插入图片描述

那么有了我们上面的介绍, 实际上要如何修改这个代码也是非常简单的, 给这个泛型定义一个 Comparable 上界, 然后这里的比较就使用 compareTo() 来进行即可, 代码修改后如下

// 定义泛型上界
class Test<T extends Comparable<T>> {
    public T getMax(T[] array){
        T ret = array[0];
        for(int i = 1; i < array.length; i++){
            // 改为使用compareTo()方法进行比较
            if(ret.compareTo(array[i]) < 0){
                ret = array[i];
            }
        }
        return ret;
    }
}

此时可能有人要问一个问题: 我老是记不住这个 compareTo() 的返回值代表的具体是什么意思怎么办? 例如< 0代表的是哪个大, > 0又代表的是哪个大.

实际上, 这个也没有必要去死记硬背的, 我们是写代码的人, 不是背代码的人. 如果不确定我们就直接当场试试就好了, 毕竟人脑的记忆力是有限的, 与其去记一些这种没有很大意义的信息, 不如去学习更多的知识. 记忆的这种活, 我们就交给计算机去做就好了.

接下来我们就通过一个 Integer 类型的数组来试验一下我们这个代码能否正常找出最大值. 至于为什么使用 Integer, 当然是因为它自己就实现了 Comparable 接口, 就不需要我们去额外写代码了.

public class Demo {
    public static void main(String[] args) {
        Integer[] arr = new Integer[]{1, 7, 8, 9, 4, 5, 6, 2, 3};
        Test<Integer> integerTest = new Test<>();
        System.out.println(integerTest.getMax(arr));
    }
}

class Test<T extends Comparable<T>> {
    public T getMax(T[] array) {
        T ret = (T) array[0];
        for (int i = 1; i < array.length; i++) {
            if (ret.compareTo(array[i]) < 0) {
                ret = (T) array[i];
            }
        }
        return ret;
    }
}

最后得出结果是9, 没有问题

在这里插入图片描述

通配符

无界通配符

现在我们假设有一个信息类, 其通过泛型实现了能够接纳各种各样的的类型

class Message<T> {
    private T message;

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


    public T get() {
        return message;
    }
}

此时我需要一个打印机类, 我需要你在这个打印机类里面实现一个方法, 用于打印 Message 对象中的 message 成员. 那么假设我们先按照我们上面学习过的东西来实现一个代码, 如下所示

class Printer<T>{
    public void print(Message<T> message){
        System.out.println(message.get());
    }
}

虽然这个写法是可以的, 但是不够优雅, 如果我们给这个类加上 T , 则证明我们对于每一个泛型参数不同的 Message<T> 对象, 都要创建对应的 Print<T> 对象才可以打印, 如下所示

public class Demo {
    public static void main(String[] args) {
        // 如果要打印类型参数为 String 的 Message, 则需要 String 类型的 Printer
        Message<String> stringMessage = new Message<>();
        stringMessage.set("hello");
        Printer<String> stringPrinter = new Printer<>();
        stringPrinter.print(stringMessage);

        // 如果要打印类型参数为 Integer 的 Message, 则需要 Integer 类型的 Printer
        Message<Integer> integerMessage = new Message<>();
        integerMessage.set(123);
        Printer<Integer> integerPrinter = new Printer<>();
        integerPrinter.print(integerMessage);
    }
}

这样很明显是非常繁琐的, 那么有没有什么方法可以使得这个 print() 方法中的 Message<> 参数, 可以容纳各种各样带有不同泛型参数的对象呢?

那么此时我们就可以使用通配符来进行这个操作, 代码修改后如下所示

// 此时类不再需要提供泛型参数
class Printer{
    // 使用通配符, 声明这个方法的参数接收任意泛型对象
    public void print(Message<?> message){
        System.out.println(message.get());
    }
}

那么此时我们的代码就可以简化很多了, 如下所示

public class Demo {
    public static void main(String[] args) {
        // 创建 String 类型 Message
        Message<String> stringMessage = new Message<>();
        stringMessage.set("hello");
        // 创建 Integer 类型 Message
        Message<Integer> integerMessage = new Message<>();
        integerMessage.set(123);

        // 使用一个 Printer 打印两个类型的 Message
        Printer printer = new Printer();
        printer.print(integerMessage);
        printer.print(stringMessage);
    }
}

上界通配符

上面我们介绍了通配符本身, 也就是 ? 的使用. 但是实际上这个 ? 也可以像泛型参数一样, 配合 extends 关键字一起使用.

例如我现在希望上面的这个 Message 类, 只能是 Number 类及其子类. 那么此时我们就可以使用 extends 关键字来修饰. 这里的使用方法和泛型上界十分类似, 代码如下

class Printer{
    public void print(Message<? extends Number> message){
        System.out.println(message.get());
    }
}

此时我们上面的传入 Message<String> 的地方就报错了, 提示类型不匹配

在这里插入图片描述

下界通配符

除了上面介绍的通配符本身和上界通配符这种和泛型上界十分相似的用法之外, 通配符还有一种独特的用法, 就是下界通配符. 与上界通配符相反, 上界通配符要求传入的类必须是类本身或者是其子类, 下界通配符则是要求传入的类必须是类本身或者是其父类

我们先将 Printer 中的 print() 方法修改为使用下界通配符的形式

class Printer{
    // 使用 super 关键字指定下界为 Number 类
    public void print(Message<? super Number> message){
        System.out.println(message.get());
    }
}

然后我们看下面代码的效果

public class Demo {
    public static void main(String[] args) {
        // 创建 Object 类型 Message
        Message<Object> objectMessage = new Message<>();
        // 创建 Integer 类型 Message
        Message<Integer> integerMessage = new Message<>();

        // 使用一个 Printer 打印两个类型的 Message
        Printer printer = new Printer();
        printer.print(integerMessage);
        printer.print(objectMessage);
    }
}

在这里插入图片描述

可以看到这里我们将 Message<Integer> 类型传入进去就报错了, 因为 Integer 类型是 Number 类型的子类. 并不符合我们的类型要求, 这里要求的是 Number 类本身或者其父类. 可以看到下面我们将 Object 类型传入进去, 就没有问题了, 因为 Object 类型肯定是 Number 类的父类.

泛型方法

泛型方法实际上就是不借助类的泛型参数, 而是直接自己声明一个泛型参数来使用泛型, 下面这个代码是我们前面写过的获取数组最大值的方法

class Test<T extends Comparable<T>> {
    public T getMax(T[] array) {
        T ret = array[0];
        for (int i = 1; i < array.length; i++) {
            if (ret.compareTo(array[i]) < 0) {
                ret = array[i];
            }
        }
        return ret;
    }
}

我们这里就将其修改为通过泛型方法来实现相同操作, 从而帮助我们理解泛型方法的使用

class Test {
    // 这里在方法修饰符的后面, 声明泛型参数, 声明方式和类的相同
    public <T extends Comparable<T>> T getMax(T[] array){
        // 内部代码并没有什么变化
        T ret = array[0];
        for(int i = 1; i < array.length; i++){
            if (ret.compareTo(array[i]) < 0){
                ret = array[i];
            }
        }
        return ret;
    }
}

泛型方法有一个很好的点, 就是我们甚至都不用告诉他是是什么类型, 它会自己从我们传递参数中推导出是什么类型. 如下所示

public class Demo {
    public static void main(String[] args) {
        Integer[] arr = new Integer[]{1, 7, 8, 9, 4, 5, 6, 2, 3};
        Test integerTest = new Test();
        // 我们这里并没有显示的将泛型参数传过去, 它会自动进行类型推断
        System.out.println(integerTest.getMax(arr));
    }
}

当然, 我们也可以选择性的显示声明一下, 如下所示

public class Demo {
    public static void main(String[] args) {
        Integer[] arr = new Integer[]{1, 7, 8, 9, 4, 5, 6, 2, 3};
        Test integerTest = new Test();
        // 在方法名前显示的声明泛型参数
        System.out.println(integerTest.<Integer>getMax(arr));
    }
}

另外, 对于静态方法这里有一些注意点. 首先静态方法是无法使用类的泛型参数的, 如下代码

public class Demo01<T> {
    // 参数报错
    public static void print(T t){
        
    }
}

在这里插入图片描述

这也很好理解, 为什么呢? 因为我们的泛型参数是在我们实例化对象的时候才会传入的, 而静态方法是属于类的, 和对象无关, 自然无法使用到创建对象时才会传入的泛型参数.

也就是说, 如果我们希望在静态方法中使用泛型, 那么久只能通过泛型方法的使用方式去使用泛型, 如下所示

public class Demo01<T> {
    // 此时这个静态方法使用的是自己泛型方法定义的泛型参数, 而不是类的泛型参数
    public static <V> void print(V v){

    }
}
  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值