【Java入门提高篇】Day15 Java泛型再探——泛型通配符及上下边界

简介:   上篇文章中介绍了泛型是什么,为什么要使用泛型以及如何使用泛型,相信大家对泛型有了一个基本的了解,本篇将继续讲解泛型的使用,让你对泛型有一个更好的掌握和更深入的认识。   上篇中介绍完泛型之后,是不是觉得泛型挺好用的?既消除了Object的不安全类型转化,又可以很方便的进行类型对象的存取,但是,等一下,有没有考虑到这样的情况。

  上篇文章中介绍了泛型是什么,为什么要使用泛型以及如何使用泛型,相信大家对泛型有了一个基本的了解,本篇将继续讲解泛型的使用,让你对泛型有一个更好的掌握和更深入的认识。

  上篇中介绍完泛型之后,是不是觉得泛型挺好用的?既消除了Object的不安全类型转化,又可以很方便的进行类型对象的存取,但是,等一下,有没有考虑到这样的情况。

  我们先定义一个水果类:

public class Fruit {
    private String name;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

  然后再定义一个苹果类:

public class Apple extends Fruit{
    public Apple(String name) {
        super(name);
    }
}

  接下来定义一个泛型容器:

public class GenericHolder<T> {
    private T obj;

    public GenericHolder(){}

    public GenericHolder(T obj){
        this.obj = obj;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
}

  接下来开始我们的测试:

public class Test {

    /**
     * 吃水果
     * @param fruitHolder
     */
    public static void eatFruit(GenericHolder<Fruit> fruitHolder){
        System.out.println("我正在吃 " + fruitHolder.getObj().getName());
    }

    public static void main(String args[]){
        //这是一个贴了水果标签的袋子
        GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
        //这是一个贴了苹果标签的袋子
        GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
        //这是一个水果
        Fruit fruit = new Fruit("水果");
        //这是一个苹果
        Apple apple = new Apple("苹果");

        //现在我们把水果放进去
        fruitHolder.setObj(fruit);
        //调用一下吃水果的方法
        eatFruit(fruitHolder);

        //贴了水果标签的袋子放水果当然没有问题
        //现在我们把水果的子类——苹果放到这个袋子里看看
        fruitHolder.setObj(apple);
        //同样是可以的,其实这时候会发生自动向上转型,apple向上转型为Fruit类型后再传入fruitHolder中
        //但不能再将取出来的对象赋值给redApple了
        //因为袋子的标签是水果,所以取出来的对象只能赋值给水果类的变量
        //无法通过编译检测 redApple = fruitHolder.getObj();
        eatFruit(fruitHolder);

        //放苹果的标签,自然只能放苹果
        appHolder.setObj(apple);
        // 这时候无法把appHolder 传入eatFruit
        // 因为GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型
        // eatFruit(appHolder);
    }
}

  运行结果:

我正在吃 水果
我正在吃 苹果

  在这里,我们往eatFruit方法里传入fuitHolder的时候,是可以正常编译的,但是如果将appHolder传入,就无法通过编译了,因为作为参数时,GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型,所以无法通过编译,那么问题来了,如果我想让eatFruit方法能同时处理GenericHolder<Fruit> 和 GenericHolder<Apple>两种类型怎么办?而且这也是很合理的需求,毕竟Apple是Fruit的子类,能吃水果,为啥不能吃苹果???如果要把这个方法重载一次,未免也有些小题大做了(而且事实上也无法通过编译,具体原因之后会有说明)。 

  在代码的逻辑里:

  • 苹果 IS-A 水果
  • 装苹果的盘子 NOT-IS-A 装水果的盘子

  这个时候,泛型的边界符就有它的用武之地了。我们先来看效果:

public class Test {

    /**
     * 吃水果
     * @param fruitHolder
     */
    public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){
        System.out.println("我正在吃 " + fruitHolder.getObj().getName());
    }

    public static void main(String args[]){
        //这是一个贴了水果标签的袋子
        GenericHolder<Fruit> fruitHolder = new GenericHolder<Fruit>();
        //这是一个贴了苹果标签的袋子
        GenericHolder<Apple> appHolder = new GenericHolder<Apple>();
        //这是一个水果
        Fruit fruit = new Fruit("水果");
        //这是一个苹果
        Apple apple = new Apple("苹果");

        //现在我们把水果放进去
        fruitHolder.setObj(fruit);
        //调用一下吃水果的方法
        eatFruit(fruitHolder);

        //放苹果的标签,自然只能放苹果
        appHolder.setObj(apple);
        // 这时候可以顺利把appHolder 传入eatFruit
        eatFruit(appHolder);
    }
}

  运行结果:

我正在吃 水果
我正在吃 苹果

  这里我们只是使用了一点小小的魔法,把参数类型改成了GenericHolder<? extends Fruit>,这样就能将 GenericHolder<Apple>类型的参数顺利传入了,怎么样?很好用吧,这就是泛型的边界符,用<? extends Fruit>的形式表示。边界符的意思,自然就是定义一个边界,这里用?表示传入的泛型类型不是固定类型,而是符合规则范围的所有类型,用extends关键字定义了一个上边界,也就是说这里的?可以代表任何继承于Fruit的类型,你也许会问,为什么是上边界,好问题,一图胜千言:

  从这个图可以很好的看出这个“上边界”的概念了吧。有上边界,自然有下边界,泛型里使用形如<? super Fruit>的方式使用下边界,此时,?只能代表Fruit及其父类。

  (这两个图是抠过来的,不要骂我懒。)

  这两种方式基本上解决了我们之前的问题,但是同时,也有一定的限制。

   1.上界<? extends T>不能往里存,只能往外取

  不要太疑惑,其实很好理解,因为编译器只知道容器里的是Fruit或者Fruit的子类,但不知道它具体是什么类型,所以存的时候,无法判断是否要存入的数据的类型与容器种的类型一致,所以会拒绝set操作。

  2.下界<? super T>往外取只能赋值给Object变量,不影响往里存

  因为编译器只知道它是Fruit或者它的父类,这样实际上是放松了类型限制,Fruit的父类一直到Object类型的对象都可以往里存,但是取的时候,就只能当成Object对象使用了。

  所以如果需要经常往外读,则使用<? extends T>,如果需要经常往外取,则使用<? super T>。

  至此,本篇讲解完毕,欢迎大家继续关注!

 

真正重要的东西,用眼睛是看不见的。
相关文章
|
2天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
88 60
【Java并发】【线程池】带你从0-1入门线程池
|
1月前
|
自然语言处理 Java
Java中的字符集编码入门-增补字符(转载)
本文探讨Java对Unicode的支持及其发展历程。文章详细解析了Unicode字符集的结构,包括基本多语言面(BMP)和增补字符的表示方法,以及UTF-16编码中surrogate pair的使用。同时介绍了代码点和代码单元的概念,并解释了UTF-8的编码规则及其兼容性。
113 60
|
2月前
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
111 7
Spring Boot 入门:简化 Java Web 开发的强大工具
|
2月前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
3月前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
3月前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
3月前
|
Java 程序员 数据库连接
Java中的异常处理:从入门到精通
在Java编程的海洋中,异常处理是一艘不可或缺的救生艇。它不仅保护你的代码免受错误数据的侵袭,还能确保用户体验的平稳航行。本文将带你领略异常处理的风浪,让你学会如何在Java中捕捉、处理和预防异常,从而成为一名真正的Java航海家。
|
13天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
72 14
|
16天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
48 13
|
17天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。

热门文章

最新文章