Java泛型

作者: AC编程 | 来源:发表于2022-09-06 17:43 被阅读0次

    一、基础

    1.1 什么是泛型

    • 泛型是 JDK5 的一个新特性,是将类型明确的事情延后到创建对象或调用方法时,再去明确的特殊的类型;

    • 泛型的本质是参数化类型,它提供了编译时类型安全监测机制,通过这个机制,我们可以在编译时检查类型安全,泛型所有的强制转换都是自动、隐式的,只要编译时没有问题,运行时就不会出现 ClassCastException(类型转换异常),极大地提高代码的可读性、复用性及安全性;

    • 泛型可以用在类、接口和方法的创建中,被称为泛型类、泛型接口、泛型方法。

    1.2 泛型的作用
    • 没有泛型前,我们通过对类型 Object 的引用,来实现参数的“任意化”。缺点是:要做显式的强制类型转换,开发者需要对实际参数类型在可以预知的情况下进行。

    • 引入泛型后,我们可以在使用时,再确定具体的类型。在调用方法时,也不用强转对象、写很多重复方法了。

    1.3 泛型的优点
    • 代码可复用:一套代码可支持不同的类型;
    • 可读性更高:不用强制转换,代码更加简洁;
    • 程序更安全:在编译时就检查类型安全,如在编译时没有警告,运行时就不会出现 ClassCastException (类型转换异常),降低 crash 率;
    • 稳定性更强:在创建集合时,就限定了集合元素的类型。因此,在取出元素时,不需要强制类型转换了。
    1.4 泛型的使用场景
    • 使用在不想写多个重载函数的场景;
    • 使用在用户希望返回他自定义类型的返回值场景。例如:Json 返回 Java bean ;
    • 在使用反射的应用中,也经常使用泛型。例如:Class<T> ;
    • 使用在约束对象类型的场景,用来定义边界(T extends ...)。例如:JDK 集合 List,Set ;
    • 使用在网页、资源分析或返回的场景。
    1.5 泛型的设计原则

    编译时没有出现警告,运行时就不会出现 ClassCastException 异常。

    1.6 泛型的实现原理

    Java 中的泛型,基本上都是在编译器这个层次来实现的。在生成的 Java 字节码里面,没有包含泛型中的类型信息。我们在使用泛型的时候添加的类型参数,将在编译时被擦除掉。这也是 Java 的泛型也被称作为“伪泛型”的原因。

    1.6.1 类型擦除(Type Erasure)

    测试代码:

    public class GenericType {
        public static void main(String[] args) {  
          ArrayList<String> arrayString=new ArrayList<String>();
          ArrayList<Integer> arrayInteger=new ArrayList<Integer>();
    
          System.out.println(arrayString.getClass());
          System.out.println(arrayString.getClass()==arrayInteger.getClass());
        }  
    }
    

    输出:

    class java.util.ArrayList
    
    true
    

    注意:是类型相同,而不是对象相同。

    在这个示例中,我们定义了两个 ArrayList 数组:

    数组一:ArrayList<String> 泛型类型,只可以存储字符串;
    数组二:ArrayList<Integer> 泛型类型,只可以存储整型。
    最后,我们通过 ArrayString 对象、和 ArrayInteger 对象的 GetClass 方法,获取它们的类信息并比较,发现结果为 true

    这是为什么呢,明明我们定义了两种不同的类型呀?这是因为:
    在编译期,所有的泛型信息都会被擦除,List<Integer> 和 List<String> 类型在编译后,都会变成 List 类型(原始类型)。

    1.7 泛型的特性

    Java 中的泛型,只在编译的时候有效。

    • 我们在编译时,先检验泛型结果,再将泛型信息擦除掉,最后在对象进入和离开方法的边界处,添加上类型检查和类型转换的方法。
    • 在编译后,程序会采取去泛型化的措施,即泛型信息不会进入到运行时阶段。
    1.8 泛型的规则限制
    • 不可以使用泛型地形参创建对象;
    • 在泛型类中,不可以给静态成员变量定义泛型;
    • 泛型类不可以继承 java.lang.Throwable 类;
    • Java 泛型不可以使用基本类型;
    • 泛型类不可以初始化一个数组;
    • Java 泛型不可以进行实例化;
    • Java 泛型不可以直接进行类型转换;
    • Java 泛型不可以直接使用 instanceof 运算符,进行运行时类型检查;
    • Java 泛型不可以创建确切类型的泛型数组;
    • Java 泛型不可以定义泛型异常类、或 catch 异常;
    • Java 泛型不可以作为参数进行重载。
    1.9 泛型常用术语
    1
    1.10 泛型的类型表示
    2

    二、为什么要使用泛型

    1、在 JDK5 之前,集合中没有泛型,我们是通过继承来实现泛型的程序设计的,使用继承实现有两个弊端

    • 取值时,需要强制类型转换,反之,得到的都是 Object。
    • 编译时,不会检查错误。

    2、在 JDK5 之后,集合中有了泛型,我们就可以使用泛型来实现了,泛型提供了编译时类型安全检测机制,该机制:

    • 允许我们在编译时检查类型安全,且所有的强制转换都是自动和隐式的,只要编译时不出现问题,运行时就不会出现ClassCastException(类型转换异常)。
    • 可以极大地提高代码的可读性、复用性及安全性。
    2.1 实例

    电商系统中,有普通用户、商户用户两种类型。当用户获取信息详情时,系统要先将其中的敏感信息设置为“空”,再返回给用户。此时,我们需要写一个通用方法,将敏感字段设置为“空”。具体怎么实现呢?

    我们可能想到的三种实现方式:继承、方法重载机制、泛型机制

    2.1.1 第一种实现方式:继承

    在 Java 中,所有的类都继承了 Object ,我们可以将 Object 作为传入参数,再使用反射去操作字段,设置 password 为空。

    public class Client {
        public static void main(String[] args) {
            // 初始化
            ShopUser shopUser = new ShopUser(0L, "shopUser", "123456");
            ClientUser clientUser = new ClientUser(0L, "clientUser", "123456");
    
            // 输出原始信息
            System.out.println("过滤前:");
            System.out.println("          " + shopUser);
            System.out.println("          " + clientUser);
    
            // 执行过滤
            shopUser = (ShopUser) removeField(shopUser);
            clientUser = (ClientUser) removeField(clientUser);
    
            // 输出过滤后信息
            System.out.println("过滤后:");
            System.out.println("          " + shopUser);
            System.out.println("          " + clientUser);
        }
    
        public static Object removeField(Object obj) throws Exception {
            Set<String> fieldSet = new HashSet();
            fieldSet.add("password");
    
            Field[] fields = obj.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (fieldSet.contains(field.getName())) {
                    field.setAccessible(true);
                    field.set(obj, null);
                    return obj;
                }
            }
            return obj;
        }
    }
    

    运行结果

    过滤前:
          ShopUser{id=0, username='shopUser', password='123456'}
          ClientUser{id=0, username='clientUser', password='123456'}
    过滤后:
          ShopUser{id=null, username='shopUser', password='null'}
          ClientUser{id=null, username='clientUser', password='null'}
    

    通过继承实现,运行结果没问题,但要强制转换对象,代码不够简洁。

    2.1.2 第二种实现方式:方法重载机制

    Java 不是有方法重载机制吗,我们再来试试这个方法。

    业务方法:

    public static ShopUser removeField(ShopUser user) throws Exception {
    
         // 强转,并返回对象
         return (ShopUser) remove(user);
     }
     public static ClientUser removeField(ClientUser user) throws Exception {
    
         // 强转,并返回对象
         return (ClientUser) remove(user);
     }
    

    核心方法:

    // 把敏感字段设置为空
        public static Object remove(Object obj) throws Exception {
            // 需要过滤的敏感字段
            Set<String> fieldSet = new HashSet<String>();
            fieldSet.add("password");
    
            // 获取所有字段:然后获取这个类所有字段
            Field[] fields = obj.getClass().getDeclaredFields();
    
            // 敏感字段设置为空
            for (Field field : fields) {
                if (fieldSet.contains(field.getName())) {
                    // 开放字段操作权限
                    field.setAccessible(true);
                    // 设置空
                    field.set(obj, null);
                }
            }
    
            // 返回对象
            return obj;
        }
    }
    

    通过方法重载机制实现,重复方法会很多,我们每添加一个供应商用户,就要再写一个方法、修改源码,简直太繁琐了,还极容易出错。

    2.1.3 第三种实现方式:泛型
    public static <T> T removeField(T obj) throws Exception {
        Set<String> fieldSet = new HashSet();
        fieldSet.add("password");
    
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (fieldSet.contains(field.getName())) {
                field.setAccessible(true);
                field.set(obj, null);
                return obj;
            }
        }
        return obj;
    }
    

    通过泛型实现,我们就不用把参数的类型代码写死,并且可以在使用的时候,再去确定具体的类型;在调用方法的时候,也不用强转对象、写很多重复方法,代码更加简洁、安全。

    三、哪些情况下需要使用泛型

    当接口、类及方法中操作的引用数据类型不确定时,就可以用泛型来表示,这样能避免强转,将运行问题转移到编译期。

    需要注意的是:

    • 泛型实际代表什么类型,取决于调用者传入的类型,如果没传,默认是 Object 类型。
    • 使用带泛型的类创建对象时,等式两边指定的泛型类型必须一致。原因是:编译器检查对象调用方法时只看变量;而程序在运行期间调用方法时,就要考虑对象具体类型了。
    • 等式两边,可以在任意一边使用泛型,在另一边不使用(考虑向后兼容)。

    四、泛型类、泛型接口、泛型方法

    4.1 泛型类
    4.1.1 泛型类的定义

    泛型类,是在实例化类时指明泛型的具体类型,是拥有泛型特性的类,本质还是一个 Java 类,可以被继承。泛型类是最常见的泛型使用方式,最常见的运用就是各种集合类和接口,例如List、ArrayList 等。

    4.1.2 泛型类的格式

    格式:public class 类名<数据类型,…> { }
    示例:public class Generic<T>{ }

    关于数据类型:

    • 可以用任意字母来代表,例如 T ,或者 T,E,K,V等形式的参数。
    • 也可以用多个英文字母,如果用多个英文字母,需要用逗号隔开,例如 public class Generic<T,K,V>{ }
    4.1.3 泛型类的使用
    • 定义一个类,在该类名后面添加类型参数声明部分,由尖括号分隔。
    • 每一个类型参数声明部分,可以包括一个或多个类型参数,参数间用逗号隔开。
    • 使用 <T> 来声明一个类型持有者名称,将 T 当作一个类型来声明成员、参数、返回值类型(此处T可以写为任意标识)。
    4.1.4 泛型类的示例代码
    public class GenericsBox<T> {
        private T t;
    
        public void add(T t) {
            this.t = t;
        }
    
        public T get() {
            return t;
        }
    }
    
    4.1.5 泛型类的使用注意事项
    • 泛型的类型参数(包括自定义类),只能是类类型(String、Integer),不能是简单类型(int,double)。
    • 不能对确切的泛型类型使用 instanceof 操作,会导致编译时出错。
    • 使用泛型时,如果传入泛型实参,会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用;如果不传入泛型类型实参,在泛型类中,使用泛型的方法或成员变量定义的类型,可以是任何的类型。
    • 若泛型类型已确定,则只能是其本身,其子类不能使用。
    4.2 泛型接口
    4.2.1 泛型接口的定义

    将泛型用在接口 interface 上,被称为泛型接口,泛型接口经常被用在各种类的生产器中。

    4.2.2 泛型接口的格式

    格式:修饰符 interface 接口名 <类型>{ }
    示例:public interface ShangHaiJieKou<T> { }

    泛型接口格式,类似于泛型类的格式;接口中的方法的格式,类似于泛型方法的格式。

    以下有两个类,和一个接口,我们以接口类为例:

    1. Genericimpl(接口实现类)
    
    2. Generic_泛型接口<T>(interface接口类)
    
    3. GenericDemo_泛型接口(main方法实现类)
    
    4.2.3 泛型接口的使用
    • 定义一个接口,在该接口名后面,添加类型参数声明部分,用尖括号分隔。
    • 每一个类型参数声明部分,可以包括一个或多个类型参数,参数间用逗号隔开。
    4.2.4 泛型接口的示例代码
    public interface GenericsInterface<T> {
    
        public abstract void genericsInterface1(T element);
    
        public abstract <T> void genericsInterface2();
    }
    
    4.2.5 泛型接口的使用注意事项

    实现泛型的类,必须传入泛型实参,然后在实现类中用该实参来替换 T 。

    4.3 泛型方法
    4.3.1 泛型方法的定义

    泛型方法,是在调用方法时指明泛型的具体类型,既可用在类和接口上,还可用在方法上。

    4.3.2 泛型方法的格式
    • 格式:修饰符 <类型> 返回值类型方法名(类型变量名){}
    • 示例:public <T> void show(T t){}
    4.3.3 泛型方法的使用
    • 所有泛型方法声明,都有一个类型参数声明部分,由尖括号分隔。该类型参数的声明部分,在方法返回值类型之前。
    • 每一个类型参数声明部分,都包括一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
    • 类型参数可被用来声明返回值类型,作为泛型方法得到的实际参数类型的占位符。
    4.3.4 泛型方法的示例代码

    打印各种类型的数组中的元素

    public static <T> void printLog(T[] inputArray) {
        for (T element : inputArray) {
             LogUtil.e("打印数组中的元素", element + "");
        }
    }
    
    4.3.5 编程原则

    泛型方法是为了调高代码的重用性和程序安全性,尽量设计泛型方法去解决问题,如果设计泛型方法,可以取代泛型整个类,就选择泛型方法。另外,泛型方法可以搞定的,就不建议使用泛型类了。

    五、泛型类的继承

    泛型类被继承的情况有两种:子类明确泛型类的类型参数变量、子类不明确泛型类的类型参数变量。

    5.1 子类明确泛型类的类型参数变量

    把泛型定义在接口上

    public interface Inter<T> {
        public abstract void show(T t);
    }
    

    定义一个子类来实现接口

    public class InterImpl implements Inter<String> {//子类明确泛型类的类型参数变量为String:
        @Override
        public void show(String s) {
            System.out.println(s);
    
        }
    }
    

    测试代码

    public class mingquefanxingcanshu {
        public static void main(String[] args) {
            Inter<String> i = new InterImpl();
            i.show("hello java!");
        }
    }
    
    5.2 子类不明确泛型类的类型参数变量

    把泛型定义在接口上

    public interface Inter<T> {
        public abstract void show(T t);
    }
    

    定义一个子类来实现接口

    public class InterImpl<T> implements Inter<T> {//实现类不明确泛型类的类型参数变量,实现类也要定义出<T>类型的
        @Override
        public void show(T s) {//子类方法也要继承类的数据类型T
            System.out.println(s);
    
        }
    }
    

    测试代码

    public class mingquefanxingcanshu {
        public static void main(String[] args) {
            Inter<String> i = new InterImpl();
            i.show("hello java!");
        }
    }
    
    5.3 泛型类的继承注意事项
    • 实现类要的是重写父类的方法,返回值的类型需要同父类一样。
    • 类上声明的泛形,只对非静态成员有效。

    六 、泛型通配符

    6.1 泛型通配符的基本概述

    泛型通配符用 ? 表示,代表不确定的类型,是泛型的一个重要组成。

    相关术语及范例:

    • ArrayList<E>,称为泛型类型,此处的 E ,称为类型参数变量。
    • ArrayList<Integer>,称为参数化的类型 ParameterizedType ,此处的Integer,称为实际类型参数。
    6.2 为什么要用泛型通配符

    泛型通配符的引入,是为了解决类型被限制死之后,不能动态根据实例来确定的缺点。先来看代码示例,我们现在有这样一个函数

    public void test(List<Number> data) {
    
    }
    

    根据泛型规则,这个函数只能传进来 List<Number> 这一种类型, List<Object> 和 List<Integer> 是传不进来的。如果我们既要泛型,又想把这两个类型的子类、或者父类的泛型传进去,就需要使用到通配符泛型。

    6.3 泛型通配符的优点

    可以在保证运行时类型安全的基础上,提高参数类型的灵活性。

    6.4 泛型通配符的作用

    泛型本身不支持协变和逆变,通配符能解决协变和逆变的问题。

    在 Java 中:

    • 数组可以协变。例如:Dog extends Animal , Animal[] 与 Dog[] 是可以兼容的。
    • 集合不可以协变。List<Animal> 不是 List<Dog> 的父类,为了建立两个集合之间的联系,此时需要引用通配符来解决。

    泛型 T 是确定的类型,而通配符更加灵活(不确定),更多用于扩充参数的范围。泛型 T就像是变量,将传来的一个具体的类型拿来使用,而通配符则是一种规定,规定你能传哪些参数,就像是一个特殊的实际类型参数。

    6.5 泛型通配符的使用场景
    • 用于类型参数中。
    • 用于实例变量,或者局部变量中。
    • 有时也可作为返回类型,例如Object的getClass方法。

    注意:泛型通配符只能用于泛型类的使用(声明变量、方法的参数),不能用于泛型定义、New 泛型实例。

    6.6 泛型通配符的三种形式
    • <?> :无边界通配符(基本使用)
    • <? extends T> :固定上边界通配符(又称有上限的通配符)
    • <? super T> :固定下边界通配符(又称有下限的通配符)
    6.6.1 无边界通配符 <?>

    无边界通配符的概述:

    • 英文全称 Unbounded Wildcards ,代表任意类型,是万能通配符 ;
    • 采用 <?> 的语法形式,来声明使用该类通配符。例如: List<?> ,表示元素类型未知的 List ,它的元素可以匹配任何的类型。这种带通配符的 List ,仅表示它是各种泛型 List 的父类,并不能把元素添加到其中。

    无边界通配符的作用:可以让泛型能够接受未知类型的数据。

    6.6.2 固定上边界通配符 <? extends T>

    固定上边界通配符的概述:

    • 英文全称 Upper Bounded Wildcards 。
    • 采用 <? extends 类型> 的语法形式,来声明使用该类通配符。例如:List<?extends Number> ,表示 Number 或者其子类型。需要注意的是,此处虽然使用了 extends 关键字,但却不仅限于继承了父类 类型 的子类,也可以代指实现了接口 类型 的类。
    • 代表类型变量的范围有限,只能传入某种类型、或者它的子类,适合频繁往外读取数据的场景。

    固定上边界通配符的作用:
    当我们不希望 List<?> 是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类时,我们就使用固定上边界通配符的泛型,来接受指定类、以及其子类类型的数据。

    6.6.3 固定下边界通配符 <? super T>

    固定下边界通配符的概述:

    • 英文全称 Lower Bounded Wildcards。
    • 采用 <? super 类型> 的语法形式 ,来声明使用该类通配符。例如: List<?super Number> ,它表示的类型是 Number 或者其父类型。
    • 代表类型变量的范围有限,只能传入某种类型、或者其父类,适合频繁插入数据的场景。

    固定下边界通配符的作用:
    我们可以使用固定下边界通配符的泛型,来接受指定类及其父类类型的数据。需要重点注意的是,一个泛型可以单独指定类型通配符的上边界、或下边界,但不能同时指定上边界、下边界。

    6.7 泛型通配符的使用

    我们确定类型变量时,如果不能明确类型变量,就可以使用泛型通配符(泛型通配符代表不确定的类型)。反之,如果我们能明确地知道类型变量,就不需要使用泛型通配符。

    6.7.1 代码示例1 :不需要使用泛型通配符

    我们明确地知道 count() 方法用在哪儿,在编码时,可以直接指明这是一个 Integer 集合。

    public static Integer count(List<Integer> list) {
        int total = 0;
        for (Integer number : list) {
            total += number;
        }
        list.add(total);
        return total;
    }
    

    在调用方法时,如果不传指定的数据进来,编译会报错:

    public static void main(String[] args) {
        // 不传指定数据,编译报错
        List<String> strList = Arrays.asList("0", "1", "2");
        int totalNum = count(strList);
    }
    

    即便我们绕过了编译,程序也很可能无法正常运行:

    public static void main(String[] args) {
        // 绕过了编译,运行报错
        List strList1 = Arrays.asList("0", "1", "2");
        int totalNum = count(strList1);
    }
    

    所以,如果你已经明确自己要做什么,清楚地知道类型变量,就没必要使用泛型通配符。

    6.7.2 代码示例2 :使用泛型通配符,实现一些通用算法

    但是,在一些通用方法中,什么类型的数据都能传进来,不能确认类型变量,这时候该怎么办呢?此时,我们就可以使用泛型通配符,在不用确认类型变量的情况下,来实现一些通用算法。例如:我们需要写一个通用方法,把传入的 List 集合输出到控制台。

    public static void print(List<?> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
    

    Integer 集合,可以运行:

    public static void main(String[] args) {
        //Integer 集合,可以运行
        List<Integer> intList = Arrays.asList(0, 1, 2);
        print(intList);
    }
    

    String 集合,可以运行:

    public static void main(String[] args) {
        //String 集合,可以运行
        List<String> strList = Arrays.asList("0", "1", "2");
        print(strList);
    }
    

    List<?> list 指不确定 List 集合装的是什么类型,有可能是 Integer、或 String、或其他。但这不用理会,我们只需要传一个 List 集合进来,这个方法就能正常运行了。这就是泛型通配符的作用所在。

    我们还可以使用泛型通配符,来实现一些特殊算法。泛型通配符也适用于一些特殊算法,但可使用的范围不大。例如:用户分为普通用户、商家用户,但用户有一些特殊功能,其它角色都没有。这时候,又该怎么办呢?我们可以通过给泛型通配符设定边界,来限定类型变量的范围。

    6.8 PECS原则

    泛型通配符的用法有些复杂,为了更好的帮助我们理解、及正确使用 Java 泛型通配符。Joshua Bloch 在《Effective Java 》第 3 版中提出了 PECS 原则。PECS 的英文全称是 Producer Extends , Consumer Super ,字面意思是“读取时使用 extends ,写入时使用 super ”。

    也就是说:

    • 参数化类型表示一个生产者,我们就使用 <? extends T> ;
    • 参数化类型表示一个消费者,我们就使用 <? super T> 。
    6.9 通配符与类型参数的区别

    一般而言,通配符能干的事情都可以用类型参数替换。例如:

    public void testWildCards(Collection<?> collection){}
    

    可以用以下来取代:

    public <T> void test(Collection<T> collection){}
    

    值得注意的是,如果用泛型方法来取代通配符,上面代码中的 collection 是能进行写操作的,但要进行强制转换。

    public <T> void test(Collection<T> collection){
        collection.add((T)new Integer(12));
        collection.add((T)"123");
    }
    

    需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。

    public class Test2 <T,E extends T>{
        T value1;
        E value2;
    }
    

    如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。

    public T test1(T t){
        return value1;
    }
    
    6.10 泛型通配符和泛型方法的使用选型

    在实际应用场景中,有很多时候,我们可以使用泛型方法、来替代泛型通配符。使用通配符:

    public static void test(List<?> list) {
    
    }
    

    使用泛型方法:

    public <T> void test2(List<T> t) {
    
    }
    

    既然通配符和泛型方法都可用,具体该怎样选型呢?
    通配符和泛型方法的选型参考:

    • 参数之间的类型有依赖关系的,又或者返回值与参数之间有依赖关系的,使用泛型方法。
    • 参数之间的类型没有依赖关系的,使用通配符。

    七、Java 泛型中的 T,E,K,V

    本质上都是通配符,只是一种编码约定俗成。我们可以将 T 换成 A-Z 之间的任意字母,不会影响程序的正常运行。但是,如果换成其他的字母代替 T ,可读性可能会差一些。通常情况下,T,E,K,V,?是这样约定的:

    3

    资料来源:
    Java泛型基础最全详解,超级详细

    为什么要使用泛型,什么时候使用泛型?

    泛型的3种使用方式:泛型类、泛型接口、泛型方法

    泛型通配符超详解,一文彻底搞懂

    一线互联网面试真题,最全最新整理

    相关文章

      网友评论

        本文标题:Java泛型

        本文链接:https://www.haomeiwen.com/subject/fexbnrtx.html