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

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

  上篇中介绍完泛型之后,是不是觉得泛型挺好用的?既消除了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>。

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

原文地址:https://www.cnblogs.com/mfrank/p/8821444.html

时间: 2024-11-05 16:05:58

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

【Java入门提高篇】Day5 Java中的回调(二)

Java中有很多个Timer,常用的有两个Timer类,一个java.util包下的Timer,一个是javax.swing包下的Timer,两个Timer类都有用到回调机制.可以使用它在到达指定时间间隔后发出通知,例如程序中有一个时钟,就可以每秒请求发送一个通知,以便更新时间显示.那么怎样通知Timer去完成我们的任务呢?在其他语言中,可以提供一个函数名,然后定时器周期性的调用它,但Java使用的是面向对象编程的思想,将某个类的对象传递给定时器,然后定时器再调用这个对象的方法,由于对象能携带的

【Java入门提高篇】Day16 Java异常处理(上)

当当当当当当,各位看官,好久不见,甚是想念. 今天我们来聊聊Java里的一个小妖精,那就是异常. 什么是异常?什么是异常处理? 异常嘛,顾名思义就是不正常,(逃),是Java程序运行时,发生的预料之外的事情,它阻止了程序按照程序员的预期正常执行. 异常处理,应该说异常处理机制,就是专门用来制服这个小妖精的法宝.Java中的异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰. 简而言之,Java异常处理就是能让

【Java入门提高篇】Day10 Java代理——静态代理

今天要介绍的是一个Java中一个很重要的概念--代理. 什么是代理?联系生活想想看,代理似乎并不陌生,最形象的代表便是经纪人,明星一般都有经纪人,经纪人作为中间人,负责代理明星的相关事宜,比如说,有人要请明星去唱歌表演,一般不会直接跟明星联系,而是联系他的经纪人,他的经纪人来负责安排行程,而真正唱歌表演的还是明星本人,经纪人仅仅作为一个附加物存在. 在Java中,代理也是这样的概念,来看个栗子: 先来创建一个明星类Stars: public class Stars implements ISta

【Java入门提高篇】Day11 Java代理——JDK动态代理

今天来看看Java的另一种代理方式--JDK动态代理 我们之前所介绍的代理方式叫静态代理,也就是静态的生成代理对象,而动态代理则是在运行时创建代理对象.动态代理有更强大的拦截请求功能,因为可以获得类的运行时信息,可以根据运行时信息来获得更为强大的执(骚)行(操)力(作). 我们还是以上一个例子为例,这里的IStars接口和Stars类都不需要修改,只需要修改代理类. 创建JDK动态代理需要先实现InvocationHandler接口,并重写其中的invoke方法,具体步骤如下: 1. 创建一个类

【Java入门提高篇】Day12 Java代理——Cglib动态代理

今天来介绍另一种更为强大的代理--Cglib动态代理. 什么是Cglib动态代理? 我们先回顾一下上一篇的jdk动态代理,jdk动态代理是通过接口来在运行时动态创建委托类的代理对象,但是跟静态代理一样有一个缺点,就是必须和委托类实现相同的接口,当接口数量增加时,便需要增加代理类的数量才能满足需求,而且如果委托类是别人写的,而且没有实现任何接口,那么jdk动态代理就有些力不从心了. 这时候Cglib动态代理就脱颖而出了,Cglib并不依赖接口,可以直接生成委托类的代理对象,而且可以代理委托类的任意

Java入门——(1)Java编程基础

Java入门--(1)Java编程基础 第二章 Java编程基础 JAVA 代码的基本格式: 修饰符 class 类名{ 程序代码 } 2.1关键字:赋予了特殊含义的单词. 2.2标识符:在程序中定义的一些名称. 比如:类名 a-z 0-9 _ $ 数字不可以开头,不可以使用关键字,一般首字母大写. 2.3注释:注解说明程序的文字. 作用:1.对程序进行说明. 2.调试程序. 注意:多行注释可嵌单行注释,但不能嵌套多行注释. /*多行注释*/ /**文档注释*/ //单行注释 /* 需求:写一个

学习java随笔第九篇:java异常处理

在java中的异常处理和c#中的异常处理是一样的都是用try-catch语句. 基本语法如下 try { //此处是可能出现异常的代码 } catch(Exception e) { //此处是如果发生异常的处理代码 } finally语句 try { //此处是可能出现异常的代码 } catch(Exception e) { //此处是如果发生异常的处理代码 } finally { //此处是肯定被执行的代码 } 抛出异常 使用thows和throw语句抛出异常 public static vo

Java入门 - 语言基础 - 01.Java简介

原文地址:http://www.work100.net/training/java-intro.html 更多教程:光束云 - 免费课程 Java简介 序号 文内章节 视频 1 概述 2 主要特性 3 发展历史 4 开发工具 请参照如上章节导航进行阅读 1.概述 Java 是由 Sun Microsystems 公司于1995年5月推出的 Java 面向对象程序设计语言和 Java 平台的总称.由 James Gosling 和同事们共同研发,并在1995年正式推出. Java 分为三个体系:

Java基础提高篇

一.Java中的四舍五入 public static void test(){ System.out.println(Math.round(12.5d)); System.out.println(Math.round(-12.5d)); //使用银行家算法 BigDecimal d=new BigDecimal(100000); BigDecimal r=new BigDecimal(0.0187*3); BigDecimal i=d.multiply(r).setScale(2,Roundin