一文搞懂Java泛型概念和使用方式

107 阅读19分钟

1、为什么要有泛型

JAVA推出泛型以前,程序员可以构建一个元素类型为Object类型的集合,该集合能够存储任何类型的数据对象,而在使用该集合的过程中需要程序员明确的知道存储的每个元素的元素类型,否则很容易引发ClassCastException异常。如下例子:

public class demo1 {

    public static void main(String[] args) {
        ArrayList list = new ArrayList<String>();
        list.add("hello");
        list.add(36);
        list.add(true);

        for (int i = 0; i < list.size(); i++) {
            Object o = list.get(i);
            String str = (String) o;
            System.out.println(str);
        }
        // 抛出:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

    }
}

所以为了解决上面这种问题,在Java5引入泛型这一操作。下面使用泛型解决类型转换的问题:

public class demo1 {

    public static void main(String[] args) {

        ArrayList<String> list2 = new ArrayList<String>();
        list2.add("hello");
        list2.add("2");
        list2.add("3");
        for (int i = 0; i < list.size(); i++) {
            String str = list2.get(i);
            System.out.println(str);
        }
    }
}

2、泛型可以干什么

2.1 泛型提供了编译时类型的安全检测机制

编译器提示,这个list2列表中无法插入int类型的数据 image.png

2.2避免强制类型转换

list2中指定列表元素为String类型,所以在获取元素时,返回String类型的元素,不需要强制类型转换 image.png

3、特性

3.1类型擦除

概念

正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 List<Number> List<Integer> 等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是 Java 的泛型实现方式与C++ 模板机制实现方式之间的重要区别。

测试

/**
 * 描述:测试类型擦除
 *
 * @author xuzili
 * @date 2024/08/03
 */

@Test
public void testTypeErasure(){
    List<Number> numberList = new ArrayList<>();
    List<Integer> integerList = new ArrayList<>();
    Class<? extends List> integerListClass = integerList.getClass();
    Class<? extends List> numberListClass = numberList.getClass();

    log.info("integerListClass:{}",integerListClass);
    log.info("numberListClass:{}", numberListClass);

    if (integerListClass.equals(numberListClass)){
        log.info("类型相同");
    }else {
        log.info("类型不同");
    }
}

结果

image.png 可以看到类型都是java.util.ArrayList

结论

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上可以看成是多个不同的类型,实际上都是相同的基本类型。

3.2 泛型不是协变的

在 Java 语言中,数组是协变的,也就是说,如果 Integer 扩展了 Number,那么不仅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 Number是 Integer 的超类型,那么 Number[] 也是 Integer[]的超类型)。

但是在测试泛型擦除时,发现 List< Number> List< Integer> 都是java.util.ArrayList类型,那么可以在需要 List< Number> 的地方传递 List< Integer>。不幸的是,情况并非如此。为啥呢?这么做将破坏要提供的类型安全泛型。

3.3 泛型不支持基本数据类型

@Test
public void testNotBaseType(){
    List<int> list = new ArrayList<>();
}

image.png 因为Java中的泛型是通过编译时的类型擦除来完成的,当泛型被类型擦除后都变成Object类型。但是Object类型不能指代像int,double这样的基本类型,只能指代Integer,Double这样的引用类型。所以泛型不支持基本类型。

3.4 泛型类并没有自己独有的 Class 类对象

比如并不存在 List<String>.class 或是 List<Integer>.class,而只有 List.class,因此在运行时无法获得泛型的真实类型信息。

3.5静态变量是被泛型类的所有实例所共享的

对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过 new MyClass<String> 还是 new MyClass<Integer> 创建的对象,都是共享一个静态变量。

3.6 泛型的类型参数不能用在 Java 异常处理的 catch 语句中

因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型 MyException<String> MyException<Integer> 的。对于 JVM 来说,它们都是 MyException 类型的。也就无法执行与异常对应的 catch 语句。

4、泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

模型相关的类

食物类,所有食物(包含:水果、米饭等)的基类,

package com.xzl.newgenerics.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 描述:食物类
 *
 * @author: xuzili
 * @date: 2024/07/31
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Food {
    private String name;

}

米饭类,继承食物类,多了一个属性grainType

package com.xzl.newgenerics.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 描述:米饭类
 *
 * @author: xuzili
 * @date: 2024/07/31
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class Rice extends Food{

    /**
     * 描述:米的种类
     */
    private String grainType;

    public Rice(String name, String grainType) {
        super(name);
        this.grainType = grainType;
    }

    public Rice(String grainType) {
        this.grainType = grainType;
    }

    @Override
    public String toString() {
        return "Rice{" +
                "name='" + super.getName() + ''' +
                ", grainType='" + grainType + ''' +
                '}';

    }

}

水果类,多了一个origin属性

package com.xzl.newgenerics.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 描述:水果类
 *
 * @author: xuzili
 * @date: 2024/07/31
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Fruit extends Food{

    /**
     * 描述:产地
     */

    private String origin;

    public Fruit(String name,String origin) {
        super(name);
        this.origin = origin;
    }

    @Override
    public String toString() {
        return "Fruit{" +
                "name='" + super.getName() + ''' +
                "origin='" + origin + ''' +
                '}';
    }
}

苹果类

package com.xzl.newgenerics.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 描述:苹果类
 *
 * @author: xuzili
 * @date: 2024/07/31
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class Apple extends Fruit{

    /**
     * 描述:苹果品种
     */

    private String variety;


    public Apple(String origin, String variety) {
        super(origin);
        this.variety = variety;
    }

    public Apple(String variety) {
        this.variety = variety;
    }

    public Apple(String name, String origin, String variety) {
        super(name, origin);
        this.variety = variety;
    }


    @Override
    public String toString() {
        return "Apple{" +
                "name='" + super.getName() + ''' +
                ", origin='" + super.getOrigin() + ''' +
                ", variety='" + variety + ''' +
                '}';
    }
}

梨子类

package com.xzl.newgenerics.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 描述:梨子类
 *
 * @author: xuzili
 * @date: 2024/07/31
 */
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class Pear extends Fruit{


    private String variety;


    public Pear(String origin, String variety) {
        super(origin);
        this.variety = variety;
    }

    public Pear(String variety) {
        this.variety = variety;
    }

    public Pear(String name, String origin, String variety) {
        super(name, origin);
        this.variety = variety;
    }


    @Override
    public String toString() {
        return "Apple{" +
                "name='" + super.getName() + ''' +
                ", origin='" + super.getOrigin() + ''' +
                ", variety='" + variety + ''' +
                '}';
    }
}

模型关系

image.png

4.1泛型接口

定义

public interface Container<T> {}

例子

package com.xzl.newgenerics;

/**
 * 描述:一个 容器接口
 *
 * @author: xuzili
 * @date: 2024/07/31
 */

public interface Container<T> {

    /**
     * 描述:添加元素
     *
     * @param t 元素
     * @author xuzili
     * @date 2024/07/31
     */

    void add(T t);

    /**
     * 描述:取元素
     *
     * @return {@link T }
     * @author xuzili
     * @date 2024/07/31
     */

    T take();
}

说明

泛型接口Container容器,此时的T表示任意类型,但是当T被指定为某一个具体类型后,则put只能添加具体类型的数据,而take也只能获取具体类型的数据。就是Container此时T不是一个具体类型,这个时候就像一个容器可以往里面放水果或者米饭,当T被指定为水果这个具体类型时,这个时候Container这个容器只能放水果,不能放米饭

注意点

实现 泛型接口 不指定具体类型
package com.xzl.newgenerics;

/**
 * 描述:冰箱
 *
 * @author: xuzili
 * @date: 2024/08/02
 */

public class Icebox<T> implements Container<T>{
    /**
     * 描述:添加元素
     *
     * @param t 元素
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public void add(T t) {
        
    }

    /**
     * 描述:取元素
     *
     * @return {@link T }
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public T take() {
        return null;
    }
}

创建了一个 Icebox 冰箱,这个类是没有指定具体类型的,所以这个类是一个泛型类,后面会讲解泛型类的概念。

实现 泛型接口 指定具体类型

电饭煲指定具体类型为Rice大米,表示这个电饭煲里面只能获取或添加大米或者其子类

package com.xzl.newgenerics;

import com.xzl.newgenerics.model.Rice;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * 描述:电饭煲
 * 指定具体类型
 *
 * @author: xuzili
 * @date: 2024/07/31
 */
@Slf4j
public class RiceCooker implements Container<Rice> {

    private static final List<Rice> CONTAINER = new ArrayList<>();


    /**
     * 描述:添加元素
     *
     * @param rice 元素
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public void add(Rice rice) {
        CONTAINER.add(rice);
    }

    /**
     * 描述:获取元素
     *
     * @return {@link Rice }
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public Rice take() {
        return CONTAINER.remove(0);
    }
}

3.1泛型类

定义

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。 泛型类的最基本写法:

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

例子:

这创建一个冰箱泛型类

package com.xzl.newgenerics;

import java.util.ArrayList;
import java.util.List;

/**
 * 描述:冰箱
 *
 * @author: xuzili
 * @date: 2024/08/02
 */

public class Icebox<T> implements Container<T>{

    private final List<T> icebox = new ArrayList<>();
    /**
     * 描述:添加元素
     *
     * @param t 元素
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public void add(T t) {
        icebox.add(t);
    }

    /**
     * 描述:取元素
     *
     * @return {@link T }
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public T take() {
        return icebox.remove(0);
    }
}

测试泛型类添加和获取元素

/**
 * 描述:测试泛型类添加和获取元素
 *
 * @author xuzili
 * @date 2024/07/31
 */

@Test
public void testGenericsClass(){
    Icebox<Food> icebox = new Icebox<>();
    icebox.add(new Food("食物"));
    icebox.add(new Fruit("水果","山东烟台"));
    icebox.add(new Rice("米饭","东北大米"));

    System.out.println(icebox.take());
    System.out.println(icebox.take());
    System.out.println(icebox.take());
}

输出:

image.png

可以看到向冰箱里添加或获取食物、水果、米饭都正常,那当容器的具体类型为Fruit时,还可以添加食物和米饭吗?大家思考一下,可以尝试一下

泛型方法

定义

在Java中,泛型方法允许你在方法级别上使用泛型,这意味着你可以创建一个方法,该方法可以处理不同类型的数据,同时保持类型安全。下面是如何定义和使用泛型方法的基本语法和示例。

<类型参数列表> 返回类型 方法名(参数列表) { // 方法体 }

其中,<类型参数列表> 是一个或多个方法类型参数,用逗号 , 分隔。

  1. 修饰符与返回值类型中间的 泛型标识符 <T,E,…>,是 泛型方法的标志,只有这种格式声明的方法才是泛型方法。
  2. 泛型方法声明时的 泛型标识符 <T,E,…> 表示在方法可以使用声明的泛型类型。
  3. 与泛型类相同,泛型标识符可以是任意类型,常见的如T,E,K,V 等。
  4. 泛型方法可以声明为 static 的,并且与普通的静态方法是一样的。

泛型方法的关键点

  • 类型参数<T> 表示一个类型参数,它可以是任何有效的标识符,但通常使用单个大写字母(如 TEKV 等)。
  • 方法调用:当你调用泛型方法时,编译器会根据传入的参数类型推断出实际使用的类型。如果需要显式指定类型,则可以在方法名后面加上 <实际类型>
  • 类型通配符:你还可以使用类型通配符(如 ?)来表示未知类型,这允许你编写更灵活的泛型方法。
  • 限定类型参数:你可以通过 extends 关键字限制类型参数必须是某个类的子类或实现某个接口

例子

package com.xzl.newgenerics;

import com.xzl.newgenerics.model.Rice;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * 描述:电饭煲
 * 指定具体类型
 *
 * @author: xuzili
 * @date: 2024/07/31
 */
@Slf4j
public class RiceCooker implements Container<Rice> {

    private static final List<Rice> CONTAINER = new ArrayList<>();


    /**
     * 描述:添加元素
     *
     * @param rice 元素
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public void add(Rice rice) {
        CONTAINER.add(rice);
    }

    /**
     * 描述:获取元素
     *
     * @return {@link Rice }
     * @author xuzili
     * @date 2024/07/31
     */
    @Override
    public Rice take() {
        return CONTAINER.remove(0);
    }

    /**
     * 描述:煮饭
     *
     * @param t 参数
     * @return {@link String }
     * @author xuzili
     * @date 2024/08/02
     */

    public  <T> Rice cookRice(T t)
    {
        log.info("开始煮饭,加水");

        log.info("大米煮好了,返回米饭");
        return take();
    }


    /**
     * 描述:设置电饭煲的参数
     * 比如:时间、温度等
     *
     *
     * @param t 参数
     * @return {@link T }
     * @author xuzili
     * @date 2024/08/03
     */

    public  <T> T setUp(T t)
    {
        log.info("设置了:{}",t);
        return t;
    }
}
返回具体类型的泛型方法

前面的例子中提交了电饭煲类,当时仔细看就会发现,类缺少了一个煮饭的方法去加工大米,现在加上了cookRice煮饭方法,其返回值米饭就是一个具体类型且方法参数的类型是一个通用类型标识标识符,所以这个方法就是一个返回具体类型的泛型方法。

/**
 * 描述:煮饭
 *
 * @param t 参数
 * @return {@link String }
 * @author xuzili
 * @date 2024/08/02
 */

public  <T> Rice cookRice(T t)
{
    log.info("开始煮饭,加水");

    log.info("大米煮好了,返回米饭");
    return take();
}
返回没有具体类型的泛型方法

steUp设置方法这个就是没有返回具体类型的泛型方法,特点是返回值也一个通用的泛型标识符,这个标识符T<T>是同一种类型。

/**
 * 描述:设置电饭煲的参数
 * 比如:时间、温度等
 *
 *
 * @param t 参数
 * @return {@link T }
 * @author xuzili
 * @date 2024/08/03
 */

public  <T> T setUp(T t)
{
    log.info("设置了:{}",t);
    return t;
}

测试 泛型方法

/**
 * 描述:测试 泛型方法
 *
 * @author xuzili
 * @date 2024/08/02
 */

@Test
public void testGenericsMethod(){
    RiceCooker riceCooker = new RiceCooker();
    riceCooker.add(new Rice("米饭","东北大米"));
    // 执行泛型方法
    Rice rice = riceCooker.cookRice("水");
    log.info("返回结果:{}",rice);

    Date date = riceCooker.setUp(new Date());
    log.info("返回时间:{}",date);

    Integer heat = riceCooker.setUp(99);
    log.info("返回温度:{}",heat);
}
输出

image.png

  • 有具体返回类型
    String s = riceCooker.cookRice("水");调用了泛型方法cookRice煮饭,传递参数水,返回米饭。

  • 没有具体返回类型
    Date date = riceCooker.setUp(new Date());设置时间,返回时间; Integer heat = riceCooker.setUp(99);设置温度返回温度。可以看到setUp的返回值并不是固定的,而是根据方法的参数类型去确定的

总结

  • 代码重用:一个泛型方法可以被不同类型的对象使用。
  • 类型安全性:编译器会确保泛型方法在使用时类型匹配。
  • 灵活性:可以轻松地扩展到其他类型而不需要修改方法本身。

5、泛型通配符

什么是泛型通配符

在Java中,泛型通配符(wildcard)是一种特殊类型的类型参数,它允许你在不知道具体类型的情况下操作泛型类型。这可以增加代码的灵活性和重用性,同时保持类型安全性。

定义

泛型通配符通常由问号 ? 表示,它代表未知的类型。当使用泛型通配符时,你实际上是在告诉Java编译器:“我不知道这里应该是什么类型,但我保证不会尝试添加任何东西或从这里获取任何东西。”

为什么要用泛型通配符

开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收 List<Object> 作为形式参数,那么如果尝试将一个 List<String> 的对象作为实际参数传进去,却发现无法通过编译。

虽然从直觉上来说,Object 是 String 的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。

例子

假如现在有一个榨汁机Juice<Fruit>泛型类,这样一个容器从逻辑上来说是既然可以放水果,当然可以放苹果。但是在Java中实际情况,是不允许这样的操作,看如下例子:

@Test
public void test(){
    Juicer<Fruit> juicer = new Juicer<>();
    // 报错
    juicer = new Juicer<Apple>();
}

image.png 这因为容器里装的东西之间有继承关系,但容器之间是没有继承关系。就是之前提到的泛型不是协变的。也就是说Juicer<Apple>Juicer<Fruit>之间没有继承关系,Juicer<Apple>并不能上转型为Juicer<Fruit>,所以会出现不兼容的类型。

假设Juicer<Apple>可以转换为Juicer<Fruit>

假设成立的情况下,遇到如下操作,Juicer<Pear>这个榨汁机现在放的都是梨子,调用addFruit方法时向榨汁机中添加了一个苹果,现象就是容器声明的是梨子,却放入了一个苹果,这明显违背了类型安全的原则,也就是前面提到的隐形的类型转换问题,所以编译器禁用了这种操作。所以Juicer<Apple>不可以转换为Juicer<Fruit>

@Test
public void test2(){
    Juicer<Pear> juicer = new Juicer<>();
    addFruit(juicer);
}

private void addFruit(Juicer<Fruit> juicer){
    Fruit take = juicer.take();
    log.info("take:{}",take);
    juicer.add(new Apple("山东烟台","红富士"));
}

总结

所以为了解决上面这种情况,让泛型更加好用,引入泛型通配符?这一概念。后面详细讲解如何使用泛型通配符解决这个问题。

如何理解泛型通配符

  1. List, List, List中的< ? extends >在编译器看来所表达的并不是一个类型范围,而是一个没指定名字的具体类型,注意具体类型4个字。
  2. Object是所有类型的基类。
  3. 编译器认为null是所有类型的子类,由null可以向任何类型赋值可知。相信大家都使用过String str = null, ArrayList ,list = null诸如此类的赋值。
  4. 子类可以安全的转型为超类。
  5. 超类无法安全的转型为子类,因为子类拥有超类并不拥有的信息,例如子类有个超类没有的方法,那么超类无法安全的转型为子类。 有了这几条基础那么我们可以开始去理解通配符使用的限制了。

PECS原则

PECS 原则(Producer Extends, Consumer Super)是 Java 泛型中的一种设计模式,它指导如何使用泛型通配符来确保代码既类型安全又高效。PECS 原则适用于使用泛型容器(如 List、Set 等)的情况,特别是当容器的内容类型不明确时。

PECS 原则概述
  • Producer Extends: 如果容器是生产者(即只向容器中写入数据),应该使用上界通配符 ? extends T
  • Consumer Super: 如果容器是消费者(即只从容器中读取数据),应该使用下界通配符 ? super T

泛型通配符

使用泛型类时,既可以指定一个具体的类型,如 Juicer<Fruit> 就声明了具体的类型是 Fruit;

也可以用通配符? 来表示未知类型,如 List<?> 就声明了 List 中包含的元素类型是未知的。

通配符所代表的其实是一组类型,但具体的类型是未知的。List<?> 所声明的就是所有类型都是可以的。但是 List<?> 并不等同于 List<Object>

List<Object> 实际上确定了 List 中包含的是 Object 及其子类,在使用的时候都可以通过 Object 来进行引用。而 List<?> 则其中所包含的元素类型是不确定。其中可能包含的是 String,也可能是 Integer。如果它包含了 String 的话,往里面添加 Integer 类型的元素就是错误的。正因为类型未知,就不能通过 new ArrayList<?>() 的方法来创建一个新的 ArrayList 对象。因为编译器无法知道具体的类型是什么。但是对于 List<?> 中的元素确总是可以用 Object 来引用的,因为虽然类型未知,但肯定是 Object 及其子类。考虑下面的代码:

/**
 * 描述:测试通配符
 *
 * @author xuzili
 * @date 2024/08/03
 */

@Test
public void testWildcard(){
   List<?> list = new ArrayList<>();
   list.get(0);
   list.add(1);
}

报错:

image.png 如上所示,试图对一个带通配符的泛型类进行添加操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。

总结

泛型类使用?后,可以获取元素,不能添加元素,只能作为消费者

泛型上界通配符

定义

上界通配符使用 ? extends T 形式来表示,其中 T 是一个已知的类型。这表示通配符可以接受 T 类型或其任何子类型。

作用

  • 读取数据:你可以从使用上界通配符限定的集合中读取数据,但不能向其中添加数据,除非添加的是 null
  • 类型安全:确保你不会添加不兼容的类型到集合中。
  • 限制类型:它限制了通配符可以接受的类型范围,从而提高了代码的灵活性和可重用性。

例子

我们前面提到了Juicer<Apple>不可以转换为Juicer<Fruit>,所以我们需要使用泛型上界通配符来解决这个问题。创建addFruitExtends(Juicer<? extends Fruit> juicer)方法,一个能放水果以及一切是水果派生类的榨汁机,再直白点就是:啥水果都能放的榨汁机,这和我们人类的逻辑就比较接近了。


    /**
     * 描述:上界通配符 Juicer<? extends Fruit> juice
     *
     * @param juicer 参数
     * @author xuzili
     * @date 2024/08/03
     */

    private void addFruitExtends(Juicer<? extends Fruit> juicer){
        Fruit take = juicer.take();
        log.info("take:{}",take);
//        juicer.add(new Apple("山东烟台","红富士"));
    }

测试

    /**
     * 测试:上界通配符 Juicer<? extends Fruit> juice
     *
     * @author xuzili
     * @date 2024/07/29
     */

    @Test
    public void testExtendsOvert(){
        Juicer<Pear> juicer = new Juicer<>();
        juicer.add(new Pear("山东烟台","红富士"));
        addFruitExtends(juicer);
    }

可以直接传值Juicer<Pear>Juicer<? extends Fruit>,可以理解为Juicer<? extends Fruit>Juicer<Pear>Juicer<Fruit>的父类,关系如下图所示,所以可以正常传递。 image.png

副作用,不能添加元素

Juicer<? extends Fruit>不能添加除了null之外的任何元素,原因:如果Juicer<Apple>指向 Juicer<? extends Fruit>时,这时Juicer<? extends Fruit>添加一个Pear元素,就会出现类型转换的问题,所以编译器不允许上界通配符添加元素

泛型下界通配符

定义

下界通配符使用 ? super T 形式来表示,其中 T 是一个已知的类型。这表示通配符可以接受 T 类型或其任何父类型。

作用

  • 添加数据:你可以向使用下界通配符限定的集合中添加数据,但只能添加 T 类型或其超类型(父类型)的对象。
  • 类型安全:确保你不会从集合中取出不兼容的类型。
  • 限制类型:它也限制了通配符可以接受的类型范围,但与上界相反,它允许添加数据而不是读取数据。

示例

测试泛型下界通配符,getInfo(Icebox<? super Food> icebox)方法,表示可以传递食物以及食物父类的容器,关系如下图:

image.png

/**
 * 描述:下界通配符方法
 *
 *
 * @param icebox 参数
 * @author xuzili
 * @date 2024/08/03
 */

private void getInfo(Icebox<? super Food> icebox){
    icebox.add(new Fruit("水果"));
    icebox.add(new Apple("apple"));
    Object o = icebox.take();
    log.info("o:{}",o);

}

测试

    /**
     * 描述:测试下界通配符
     *
     * @author xuzili
     * @date 2024/08/03
     */

    @Test
    public void testSuper(){
        Icebox<Food> foodIcebox = new Icebox<>();
        getInfo(foodIcebox);

        Icebox<Fruit> fruitIcebox = new Icebox<>();
//        getInfo(fruitIcebox);

        Icebox<Apple> appleIcebox = new Icebox<>();
//        getInfo(appleIcebox);

    }

image.png Icebox<? super Food>中可以添加Food以及其子类元素。

副作用

Icebox<? super Food>获取元素时无法确定类型,只能转化为Object,这样导致元素会丢失原本的属性和方法,也相当于不能读取元素。