• JAVA基础知识|泛型


    一、什么是泛型?

    泛型,即“参数化类型”。

    比如定义一个变量A,我们可以通过如下方式将这个变量定义为字符串类型或者整形。

    String A;
    Integer A;

    当然这是在变量类型已知的情况下,如果有一种情况,我们在定义变量的时候不知道以后会需要什么类型,或者说我们需要兼容各种类型的时候,又该如何定义呢?

    鉴于以上这种情况,我们可以引入“泛型”,将String和Integer类型进行参数化。在使用的时候,再传入具体的参数类型。

    泛型的本质是为了参数化类型(通过传入的不同类型来决定形参的具体类型)。也就是说在泛型使用过程中,数据的类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

     二、泛型类

    Computer类,这个类中包含一个属性t,类型为String。

        public class Computer {
    
            private String t;
    
            public void set(String t) {
                this.t = t;
            }
    
            public String get() {
                return this.t;
            }
        }

    泛型类Computer

        //这里的"T"并不是固定写法,也可以用V或M等字符
        public class Computer<T> {
    
            private T t;
    
            public void set(T t) {
                this.t = t;
            }
    
            public T get() {
                return this.t;
            }
        }
    
        //可以传入多种泛型参数
        public class Computer<T, V> {
    
            private T t;
            private V v;
    
            public void set(T t, V v) {
                this.t = t;
                this.v = v;
            }
    
            public T getT() {
                return this.t;
            }
    
            public V getV() {
                return this.v;
            }
        }

    定义泛型类的好处就是,我们可以在需要的时候,再去指定属性t的类型,增强了通用性。

            Computer<String> computer1= new Computer<String>();
            Computer<Integer> computer2= new Computer<Integer>();
            Computer<Double> computer3= new Computer<Double>();

    三、泛型接口

        //定义接口Calculatable
    public interface Calculatable<T> { T execute(); }
    //传入String类型的实参
    public class Computer implements Calculatable<String> { @Override public String execute() { return "ok"; } }
    //传入Integer类型的实参
    public class Calculator implements Calculatable<Integer> { @Override public Integer execute() { return 100; } }
    //未传入具体实参,继续抛出,由下层传入
    public class Phone<V> implements Calculatable<V> { private V v; @Override public V execute() { return this.v; } }

     四、泛型方法

        public class Computer<T> {
    
            private T t;
    
            //不是泛型方法
            public void set(T t) {
                this.t = t;
            }
    
            //不是泛型方法
            public T get() {
                return this.t;
            }
    
            //泛型方法
            //首先public与返回值类型之间的<V>必不可少,只有声明了<V>的方法才是泛型方法
            //可以声明多个泛型,如 public <V,M> genericMethod(V v,M m)
            public <V> V genericMethod(V v){
                return v;
            }
        }
            Computer<String> computer = new Computer<String>();
            Integer v= 100;
            System.out.println(computer.genericMethod(v).getClass().toString());

    输出结果:

    class java.lang.Integer

    泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。

    泛型方法,可以通过传入的实参,判断V的具体类型。

    五、静态方法与泛型

    静态方法不可以使用类中定义的泛型,如果静态方法想要使用泛型,需要将自身声明为泛型方法。

    public class Computer<T> {    
        public static <V> void get(V v){
            //T t;报错,不能使用类中定义的泛型
        }
    }

    六、通配符及上下边界

    举一个简单的例子

    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 AppTest extends TestCase {
    
        @Test
        public void test() {
    
            //这是一个贴了水果标签的袋子
            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);
            //调用一下吃水果的方法
            eatFruit(fruitHolder);
    
            //放苹果的标签,自然只能放苹果
            appHolder.setObj(apple);
            // eatFruit(appHolder);//报错,这时候无法把appHolder传入eatFruit,因为GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型
        }
    
        public static void eatFruit(GenericHolder<Fruit> fruitHolder){
            System.out.println("我正在吃 " + fruitHolder.getObj().getName());
        }
    }

    执行结果:

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

    GenericHolder<Fruit> 和 GenericHolder<Apple>是两种不同的类型,所以无法通过编译。

    从Java继承的角度上可以分析:

     苹果 IS-A 水果

    装苹果的袋子 NOT-IS-A 装水果的袋子

    么问题来了,如果我想让eatFruit方法能同时处理GenericHolder<Fruit> 和 GenericHolder<Apple>两种类型怎么办?而且这也是很合理的需求,毕竟Apple是Fruit的子类,能吃水果,为啥不能吃苹果???如果要把这个方法重载一次,未免也有些小题大做了。

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

    public class AppTest extends TestCase {
    
        @Test
        public void test() {
    
            //这是一个贴了水果标签的袋子
            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);
        }
    
        public static void eatFruit(GenericHolder<? extends Fruit> fruitHolder){
            System.out.println("我正在吃 " + fruitHolder.getObj().getName());
    Fruit fruit1 = new Fruit("水果1");
    //fruitHolder.setObj(fruit1);//报错,不能存入新的内容。Error:(35, 28) java: 不兼容的类型: Fruit无法转换为capture#1, 共 ? extends Fruit
    } }

    运行结果:

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

    这就是泛型的边界符,用<? extends Fruit>的形式表示。边界符的意思,自然就是定义一个边界,这里用?表示传入的泛型类型不是固定类型,而是符合规则范围的所有类型,用extends关键字定义了一个上边界。

    有上边界,自然有下边界,泛型里使用形如<? super Fruit>的方式使用下边界。

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

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

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

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

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

    3.如果既要存又要取,那么就不要使用任何通配符

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

     如果阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:

    public class Collections {
        public static <T> void copy(List<? super T> dest, List<? extends T> src) {
            for (int i=0; i<src.size(); i++)
                dest.set(i, src.get(i));
        }
    }

     参考网址:

    http://www.importnew.com/24029.html

    https://blog.csdn.net/sunxianghuang/article/details/51982979

  • 相关阅读:
    编译安装
    yum history使用详解(某次为解决误卸载软件的回退实验)
    centos7了解
    码云仓库中获取单个文件的超链接
    常用软件及安装目录有链接的
    rsync备份服务器部署详情
    snmpwalk命令详解
    vmware迁移虚拟机
    ngrinder脚本请求头中添加cookie后仍报未登录或者401错误问题解决
    python多进程和多线程效率比较,ProcessPoolExecutor,ThreadPoolExecutor
  • 原文地址:https://www.cnblogs.com/maikucha/p/8821544.html
Copyright © 2020-2023  润新知