• 泛型程序设计


    泛型,即“参数化类型”,将原来的具体类型参数化。在不创建新类型的情况下,通过泛型指定不同的类型形参,来控制实际传入实参的具体类型。换句话说,就是在使用和调用时传入具体的类型。

    为什么使用泛型?

    • 能够对类型进行限定(比如集合)
    • 将运行期错误提前到编译期错误
    • 获取明确的限定类型时无需进行强制类型转化
    • 具有良好的可读性和安全性

    泛型类

    泛型类的定义

    一个简单的泛型类,和普通类的区别是,类名后添加了<T>一个泛型标识,“T"类型参数(类型形参),传入的是类型实参,当然也可以用其他字母标识,但是"<>"左右尖括号必须存在。

    public class Generic<T> {
        private T data;
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
        public static void main(String[] args) {
            /* 1.没有传入具体的类型参数,可以存放任何类型的数据【下图】
             * 本质:虚拟机会对泛型代码进行类型擦除,类型擦除后Generic<T>会变为
             * Generic(原始类型),后面会讲到,无限定类型的域用Object代替,即擦除
             * 后T data->Object data,这也是为什么没有传入具体类型,却能存放多种
             * 类型的原因。
             */
            Generic generic = new Generic();
            generic.setData(1);
            generic.setData("String");
            generic.setData(new Object());
            Generic genericStr = new Generic<String>();//CORRECT [1]
            genericStr.setData("hello");
            genericStr.setData(1);
            /*
            * 2.参数化类型(初始化时传入具体的类型参数)【下图】
            * 本质:由编译器进行类型转化的处理,无需人为干预。
            * 当调用泛型方法时,编译器自动在调用前后插入相应的
            * 强转和调用语句。
             */
            Generic<String> genericString=new Generic<>();
            genericString.setData("hello");
    
            //Generic<int> genericInt=new Generic<>(); ERROR [2]
            Generic<Integer> genericInt=new Generic<>();
        }
    }
    

    在这里插入图片描述

    【1】原始类型可以接受任何参数化类型,即Generic generic == new Generic<String>()如[1]处所示。

    【2】泛型的类型参数只能是类类型,不能是基本类型。如[2]处,但可以使用期包装类型。

    【3】泛型参数命名规范如下:

    泛型命名规范:国际惯例,类型参数的命名采用单个大写字母。

    常见的泛型命名有:

    • T - Type:第一类通用类型参数。
    • S - Type:第二类通用类型参数。
    • U - Type:第三类通用类型参数。
    • V - Type:第四类通用类型参数。
    • E - Element:主要用于Java集合(Collections)框架使用。
    • K - Key
    • V - Value
    • N - Number
    • R - Result

    泛型接口

    泛型接口的定义

    和泛型类定义相似,如下:

    public interface GenericInterface<T> {
        T getData();
        T setData(T data);
    }
    

    类接口的实现

    类接口的实现存在三种形式,第一种无泛型,域类型用Object定义;第二种有泛型,域变量用泛型参数定义;第三种传递具体的类型参数,域变量的类型为具体的类型。

    public class GenericInterfaceImpl implements GenericInterface {
        @Override
        public Object getData() {
            return null;
        }
    
        @Override
        public Object setData(Object data) {
            return null;
        }
    }
    /*
    * 实现类的类型参数也需要声明,否则编译器会报错
    */
    class GenericInterfaceImpT<T> implements GenericInterface<T> {
    
        @Override
        public T getData() {
            return null;
        }
    
        @Override
        public T setData(T data) {
            return null;
        }
    }
    /*
    * 传入具体的类型实参
    */
    class GenericInterfaceImplStr implements GenericInterface<String> {
    
        @Override
        public String getData() {
            return null;
        }
    
        @Override
        public String setData(String data) {
            return null;
        }
    }
    

    泛型方法

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

    1、泛型方法的定义:类型变量放在修饰符前,返回类型的后面

    # 修饰符 <T> 返回值 方法名(...);
    * 示例
    class ArrayAlg {
    	/*
    	*修饰符与返回值(T)中间的<T>标识此方法为泛型方法
    	*<T>表明该方法可以使用泛型类型T,可以在形参或者方法体中声明变量
    	/
    	public static <T> T getMiddle(T...a) {
    		return a[a.length/2];
    	}
    }
    
    

    2、调用泛型方法,在方法名前的尖括号放入具体的类型:

    String middle = ArrayAlg.<String>getMiddle("John","Q","Public");
    // 类型推断:类型参数可以省略 等同于
    String middle = ArrayAlg.getMiddle("John","Q","Public");
    

    使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断

    3、泛型方法辨别真假

    /*
     * 泛型类
     * 注意:下面为了介绍,不把泛型方法归类到成员方法里,泛型方法是特指!
     */
    public class GenericMethod<T> {
        private T data;
    
        /*
        * [成员方法:非泛型方法]
        * T getData()和setData(T data) 都不是泛型方法
        * 他们只是类中的成员方法,只不过是方法的返回值类型
        * 和方法的形参类型是用的泛型类上的T所声明的。
         */
        public T getData() {
            return data;
        }
        public void setData(T data) {
            this.data = data;
        }
    
        /*
        * [泛型方法]
        * 泛型参数可以有多个,这里的T和泛型类上的T无任何关联,但是但是
        * 它和泛型类上的参数类型变量相同,这时候idea会给予一个rename提示
         */
        public <T,S> T genericMethod(S...a) {
            return null;
        }
    
         /*
        * [泛型方法]
        * 使用泛型类上的泛型变量
        * 这时候的T就和泛型类的类型相关了
         */
        public <V> T genericMethod$1(T a, V b) {
            return null;
        }
        
        /*
         * [静态方法]
         * 静态方法不能使用泛型类上的类型参数
         * 如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法
         */
        //public static T getDataStatic(T e) { } //ERROR [1]
        
        /*
        * [泛型方法]
        * 静态泛型方法
         */
        public static <E> E genericMethodS(E e) {
            return e;
        }
    }
    /*
    *普通类中的泛型方法
    */
    class OrdinaryClass {
        public <T> void sayHello() {
            T a;
            //...
        }
    }
    

    下面以苹果为例:

    public class GenericClass<T> {
    
        //成员方法,形参类型与泛型类的类型参数相关
        public void print$1(T a) {
            System.out.println(a.toString());
        }
    
        //下面三个都为泛型方法
        //--begin
        public <T> void print$2(T a) {
            System.out.println(a.toString());
        }
    
        public <S> void print$3(S a) {
            System.out.println(a.toString());
        }
    
        public static <T> void print$4(T a) {
            System.out.println(a.toString());
        }
        //--end
        public static void main(String[] args) {
            Apple apple = new Apple();
            MacBook macBook = new MacBook();
            HongFuShi hongFuShi = new HongFuShi();
            // 泛型类在初始化时限定了参数类型,成员方法中若使用泛型参数将会受限
            GenericClass<Apple> genericCls = new GenericClass<>();
            
            genericCls.print$1(apple);
            //MacBook是apple的一个子类
            genericCls.print$1(macBook);// OK 
            
            //由于初始化指定了泛型类型,print$1形参中的参数类型和泛型类的类型参数相关联
            //所以,只能打印Apple及其子类
            //genericCls.print$1(hongFuShi); ERROR
    
            //泛型方法中的泛型变量类型与泛型类中的泛型参数没有任何关联
            //所以说下面都能正常执行
            genericCls.print$2(apple);
            genericCls.<MacBook>print$2(macBook);//类型参数可以省略		[2]
            genericCls.print$2(hongFuShi);
    
            GenericClass.print$4(hongFuShi);
    
        }
    }
    class Apple {
        @Override
        public String toString() {
            return "Apple,Steve Jobs";
        }
    }
    class MacBook extends Apple {
        @Override
        public String toString() {
            return "MacBook";
        }
    }
    class HongFuShi{
        @Override
        public String toString() {
            return "HongFushi";
        }
    }
    

    小结:

    【1】泛型方法的标识:方法修饰符后返回值之前有"<...>"的声明。(判断是否为泛型方法)

    【2】泛型方法可以定义在普通类中,也可以定义在泛型类中。

    【3】静态方法不能使用泛型类上的类型参数,如[1]处

    【4】成员方法中使用的参数类型和泛型类中的声明的类型参数有关联。

    【5】泛型类中的参数类型与泛型方法中的参数类型的关联:泛型方法可以使用泛型类上的参数类型,这时候就与泛型类上的参数相关联;如果泛型方法中声明了与泛型类上相同的参数类型,那么优先使用泛型方法上的参数类型,这时候idea会给予一个rename提示。

    【6】泛型方法的使用过程中,无需对类型进行声明,它可以根据传入的参数,自动判断。[2]

    【7】方法中泛型参数不是凭空而来的,要么来自于泛型类上所定义的参数类型,要么来自于泛型方法中定义的参数类型。

    泛型方法能独立于类而发生变化,所以说在使用原则上,在能达到目的的情况下,尽量使用泛型方法。即,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。

    类型变量的限定

    对于类型变量没有限定的泛型类或方法, 它是默认继承自Object,当没有传入具体类型时,它有的能力只有Object类中的几个默认方法实现,原因就是类型擦除

    如果某个类实现Comparable接口中的compareTo方法,我们就可以通过compareTo比较两个值的大小。比如我们要计算数组中的最小元素:

    public static void main(String[] args) {
            // 传入 4 , 2 , 自动装箱成Integer类
            int r = max(4, 2);
        }
    static <T> T min(T[] a) {
        if (a == null || a.length == 0)
            return null;
        T smallest = a[0];
        for (int i = 1; i < a.length; i++) 
            if (smallest.compareTo(a[i]) > 0)// ERROR,因为编译器不知道T声明的变量是什么类型
                smallest = a[i];
        return smallest;
    }
    

    如果没有对类型进行限定,它默认只有Object能力,变量smallest类型为T,编译器不知道他是否是实现了Comparable接口(是否是Comparable类型),所以可以通过将T限定为实现了Comparable接口的类,就可以解决这一问题。

    对 类型参数进行限定,让它能够默认拥有一些类的"能力"

    static <T extends Comparable> T min(T[] a){...}
    

    类型变量限定格式:<T extends BoundingType>

    【1】T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。

    【2】一个类型变量或通配符可以有多个限定,限定类型用”&“分隔,类型变量用逗号分隔。例如:

    <T extends Comparable & Serializable>;
    <T,E extends Comparable & Serializable>;
    

    【3】在 Java 的继承中, 可以拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个。

    <T extends ArrayList & LinkedList>;//ERROR,限定中至多有一个类
    <T extends Comparable & LinkedList>;//ERROR,必须是限定列表中的第一个
    <T extends ArrayList & Comparable>;//CORRECT
    

    【4】类型限定不仅可以在泛型方法上,也可以在泛型类上,类型限定必须与泛型的声明在一起

    public <T extends Number> T compare(Generic<T extends Comparable> a) {..}//ERROR
    public <T extends Number> T compare(Generic<T> a) {..}//ERROR
    

    类型擦除

    虚拟机没有泛型类型对象—所有对象都属于普通类

    类型擦除:无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型,如果没有给定限定就用Object替换

    例如,Holder的原始类型如下:

    public class Holder {
        private Object holder;
    
        public Holder(Object holder) {
            this.holder = holder;
        }
    
        public Object getHolder() {
            return holder;
        }
    }
    // 类型擦除也会出现在泛型方法中。
    public static <T extends Comparable> T min(T[] a);
    // 擦除类型之后,只剩下
    public static Comparable min(Comparable[] a);
    

    Java泛型 转换的事实

    • 虚拟机中没有泛型,只有普通的类和方法。
    • 所有的类型参数都用它们的限定类型替换。
    • 桥方法被合成来保持多态。
    • 为保持类型安全性,必要时插入强制类型转换。

    泛型的约束与局限

    (1)不能用基本类型实例化类型参数

    其原因是当类型擦除后,Object类型的域不能存储基本类型的值。

    (2)所有的类型查询只产生原始类型

    List<Number> numbers = new ArrayList<>();
    List<Integer> integers = new ArrayList<>();
    System.out.println(numbers.getClass() == integers.getClass());//true
    if (integers instanceof List){//true
        System.out.println(true);
    }
    /*if (integers instanceof List<Integer>){//compile error
                System.out.println(true);
            }*/
    

    (3)不能创建一个确切的泛型类型的数组

    //List<Integer>[] lists = new ArrayList<Integer>[10];//ERROR
    //可以声明原始类型创建数组,但是会得到一个警告
    //可以通过@SuppressWarnings("unchecked")去除
    List<Integer>[] list = new ArrayList[10];
    //使用通配符创建泛型数组也是可以的,但是需要强制转换
    List<Integer>[] listWildcard = (List<Integer>[])new ArrayList<?>[1];
    

    下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

    List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
    Object o = lsa;    
    Object[] oa = (Object[]) o;    
    List<Integer> li = new ArrayList<Integer>();    
    li.add(new Integer(3));    
    oa[1] = li; // Correct.    
    Integer i = (Integer) lsa[1].get(0); // OK 
    

    (4)不能实例化类型变量

    static <T> Object init(Class<T> cls) throws Exception {
        //T a = new T(); // ERROR
        // 注意不存在T.class.newInstance();
        T t = cls.newInstance();//Class本身也是一个泛型类
        return t;
    }
    

    (5)不能构造泛型数组

    (6)泛型类的静态上下文中类型变量无效

    (7)不能抛出或捕获泛型类的实例

    不变 协变 逆变

    首先看一段代码

    Number[] n = new Integer[10];
    ArrayList<Number> list = new ArrayList<Integer>(); // ERROR type mismatch
    

    为什么Number类型的数组可以由Integer实例化,而ArrayList<Number>却不能被ArrayList<Integer>实例化呢?这就涉及到将要介绍的主题。

    不变协变逆变的定义:

    逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果AA、BB表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

    • f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
    • f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
    • f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

    容易证明数组是协变的即Number[] n = new Integer[10];泛型是不变的,即使它的类型参数存在继承关系,但是整个泛型之间没有继承关系 : ArrayList<Number> list = new ArrayList<Integer>();

    泛型类型的继承规则

    1. 泛型参数是继承关系的泛型类之间是没有任何继承关系的

    在java中,Number是所有数值类型的父类,任何基本类型的包装类型都继承于它。

    ArrayList<Integer> integers = new ArrayList<>();
    ArrayList<Number> numbers = new ArrayList<>();
    numbers = integers;     // ERROR,就上上面刚刚提到的泛型是不变的
    

    2. 泛型类可以扩展或实现其他的泛型类。这一点和普通类没有声明区别。比如ArrayList<T>类实现List<T>接口。这意味着,一个ArrayList<Integer>可以转换为一个List<Integer>(父类指向了子类的引用),但是,一个ArrayList<Integer>不是一个ArrayList<Number>List<Number>(泛型参数继承与类无关)。

    通配符

    Java中引入通配符?来实现逆变和协变,通过通配符之前的操作也能赋值成功,如下所示:

    List<? extends Number> number = new ArrayList<Integer>();// CORRECT
    List<? super Number> list = new ArrayList<Object>();// CORRECT 逆变的代表
    

    使用通配符的子类型关系

    ArrayList<Integer>ArrayList<? extends Number>的一个子类型。

    通配符的分类

    • ? extends T(上边界通配符 upper bounded wildcard)

      "?"是继承自T的任意子类型,表示一种约束关系。即泛型类型的范围不能超过T。

      可以取元素,不能添加元素

    • ? (无限定通配符)

    • ? super T(下边界通配符 lower bounded wildcard)

      可以取元素,但是取出的元素是Object,可以添加元素,添加的元素,必须是T类或者其子类

    记忆:上不存,下不取

    示例:类型上边界通配符为什么只能添加?

    ArrayList<Integer> integers = new ArrayList<>();
    ArrayList<Object> objects = new ArrayList<>();
    ArrayList<Number> nums = new ArrayList<>();
    /*
     * 1. 类型上边界通配符
     * 对变量numbers赋值,引用的集合类型参数只能是Number或者其子类。
     */
    ArrayList<? extends Number> numbers;
    numbers = nums;
    numbers = integers;
    //引用的类对象类型超过了泛型类型的上边界
    //numbers = objects;    ERROR
    integers.add(1);// 正常添加元素
    //numbers.add(1); ERROR numbers只能读取,不能添加[1]
    //但是可以添加null
    numbers.add(null);
    Number number = numbers.get(0);//可以读取元素
    

    为什么只能读取,不能添加?[1]
    ? extends T 表示类型的上界,类型参数是T的子类,那么可以肯定的说,get方法返回的一定是个T(不管是T或者T的子类)编译器是可以确定知道的。但是add方法只知道传入的是个T,至于具体是T的那个子类,不知道。
    转化到本例来说就是:
    理解方式一
    ? extends Number指定类型参数必须是Number的子类,get方法返回的一定是Number
    编译器确定,但是对于ArrayList的add方法为来说add(E e)->add(? extends Number e);
    调用add函数不能够确定传入add的是Number的哪个子类型。编译器不确定。
    理解方式二
    List是线性表吧【线性结构的存储】,线性表是n个具有相同类型的数据元素的有限序列。假若number能够add,因为? extends Number泛型通配符,可以添加Number的任何子类型,那么numbers在get时,极有可能引发ClassCastException,比如numbers引用了<Integer>,但是在索引0处却add了float类型的数据,取出的时候如果numbers.get(0).intValue();就会抛出异常。并且这也违背了线性表中特性,只能存放单一类型的元素。

    /*
    * 2. 类型下边界通配符
    * numbersSuper所能引用的变量必须是Number或者其父类
     */
    
    ArrayList<? super Number> numbersSuper;
    numbersSuper = objects;// 逆变
    numbersSuper = nums;
    //限定了通配符的下界,类型最低是Number,Integer达不到
    //下界,类型不匹配
    //numbersSuper = integers;  ERROR
    numbersSuper.add(1);	[2]
    numbersSuper.add(2.0f); [3]
    //numbersSuper.add(new Object()) ERROR
    Object object = numbersSuper.get(0);
    System.out.println(object);
    

    ? super T 表示类型的下界,类型参数是T的超类(包括T本身), 那么可以肯定的说,get方法返回的一定是个T的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。 编译器是可以确定知道的。对于add方法来说,编译器不知道它需要的确切类型,但是T和T的子类可以安全的转型为T。

    为什么? super Number就可以add了呢?[2、3]

    首先要明确的一点是,add的时候只能是Number[T]及其它的子类,不要和numberSuper只能引用Number的父类所混淆了。正因为numberSuper引用了<Object>,那么numberSuper在add的时候类型确定,都可以看作是Object类型,即Number的子类Integer和Float也是其Object的子类。但是相对于? extends T就add就不能调用,numbers如果限定了<Integer>,还是那句话,假若能放的话,number存放float类型的数据,取值时极易引发类型转化异常。

    泛型方法和类型通配符

    类型通配符所能解决的泛型方法一定也能解决

    # 类型通配符
    `public void func(List<? extends E> list);`
    # 泛型方法
    `public <T extends E> void func(List<T> list);`
    * 上面两种方法可以达到同样的效果,两者的主要区别还是
    i. 泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型;
    ii. 而泛型方法中的泛型参数对象是可修改的,因为类型参数T是确定的(在调用方法时确定),因为T可以用范围内任意类型指定;
    

    泛型方法和类型通配符(上界和下界)我们应该如何选择

    (1)泛型方法和通配符之间

    修改最好使用泛型方法,在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该使用通配符。

    (2)什么时候用extends什么时候用super

    PECS: producer-extends, consumer-super.

    —《Effective Java》

    • 要从泛型类取数据时,用extends
    • 要往泛型类写数据时,用super
  • 相关阅读:
    Monkeyrunner学习
    js判断字符串是否包含指定的字符
    Asp.net网站后台代码不能访问-iis部署
    css分割线 文字居中的7种实现方式
    css倒三角的几种实现方式
    前端一键切图
    html中em和px
    js日期控件demo
    网页字体样式
    html特殊符号列表
  • 原文地址:https://www.cnblogs.com/wubug/p/13707231.html
Copyright © 2020-2023  润新知