写点什么

「 Java 基础 - 泛型 」一文说清 Java 泛型中的通配符 T、E、K、V、N、?和 Object 的区别和含义

作者:小刘学编程
  • 2023-02-16
    陕西
  • 本文字数:7172 字

    阅读完需:约 24 分钟

「 Java基础-泛型 」一文说清Java泛型中的通配符T、E、K、V、N、?和Object的区别和含义

前言

当我们在阅读源码的时候通常会看到如下所示代码中存在“E”、“T”或“?”,那么,这些大写字母到底有着怎样的含义呢?接下来我们具体讨论一下。


public interface Enumeration<E> {    /**     * Tests if this enumeration contains more elements.     *     * @return  <code>true</code> if and only if this enumeration object     *           contains at least one more element to provide;     *          <code>false</code> otherwise.     */    boolean hasMoreElements();
/** * Returns the next element of this enumeration if this enumeration * object has at least one more element to provide. * * @return the next element of this enumeration. * @exception NoSuchElementException if no more elements exist. */ E nextElement();}
复制代码

一、什么是泛型?

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。

可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。

1.1 泛型的语法规则

  • a、所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前;


eg 1: 比如说这是一个用来打印数组的泛型方法:


private static <E> void printArray(E[] inputArray)
复制代码


  • b、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符;


eg 2: 比如下面这个方法:


private static <E,T> void printArray(E[] inputArray, T data)
复制代码


  • c、类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符;

  • d、泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(int double char 等);

  • e、泛型的参数类型可以使用 extends 语句,例如<T extends superclass>

1.2 泛型的通配符

其实,Java 中的 **T,E,K,V,?**本质上都是通配符,常用于泛型定义的时候,E、T、K、V、N 等这些字母之间没什么区别,使用 T 的地方完全可以换成 U、S、Z 等任意字母。当然,一般我们会使用一些常用的字母,这些字符一般是一些类型的缩写,约定的定义如下:


1、T (type) 表⽰具体的⼀个 java 类型;

2、K、V (key value) 分别代表 java 键值中的 KeyValue;

3、E (element) 代表 Element

4、N Number 代表数值类型;

5、**?**表示不确定的 java 类型;

1.3 泛型类

泛型类的定义格式:


格式:修饰符 class 类名<类型>{}

范例:public class Generic<T>{}


此处 T 可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型.


public class Demo<T> {    private T t;        public void setT(T t){        this.t = t;    }    public T getT(){        return t;    }}
复制代码

1.4 泛型方法

  • 格式:修饰符<类型> 返回值类型方法名(类型 变量名){}

  • 范例:public <T> void show(T t){}


public class Demo<T> {        public void show(T t){        System.out.println(t);    }}
复制代码

1.5 泛型接口

  • 格式:修饰符 interface 接口名<类型>{ }

  • 范例:public interface Generic<T>{ }


定义接口:


public interface Demo<T> {    void show(T t);}
复制代码


定义接口实现类:


public class DemoImlp<T> implements Demo<T> {    @Override    public void show(T t){        System.out.println(t);    }}
复制代码

1.6 可变参数

1.6.1 定义格式

  • 格式:修饰符 返回值类型 方法名(数据类型... a){ }

  • 范例:public static int sum(int... a){ }

1.6.2 示例

public class Demo {    public static void main(String[] args) {        System.out.println(sum(10,20));        System.out.println(sum(10,20,30));        System.out.println(sum(10,20,30,40));        System.out.println(sum(10,20,30,40,50));        System.out.println(sum(10,20,30,40,50,60));        System.out.println(sum(10,20,30,40,50,60,70));
}
public static int sum(int... a){//a其实是一个数组,用来存储参数 int sum = 0; for(int i:a){ sum += i; } return sum; }}
复制代码


若可变参数与不可变参数在同一个方法定义中出现,则不可变参数要放置在第一个参数位置,例如:


public static int fun1(int... a,int b){//报错    ......}
public static int fun2(int b,int... a){//正常 ......}
复制代码

1.6.3 可变参数在实际中的应用

Arrays 工具类中有一个静态方法:


  • public static List asList(T… a):返回由指定数组支持的固定大小的列表

  • 注意返回的集合不能做增删操作,可以做修改操作


List 接口中有一个静态方法:


  • public static List of(E… element):返回包含任意数量元素的不可变列表

  • 注意:返回的集合不能做增删改操作


Set 接口中有一个静态方法:


  • public static Set of(E… elements):返回一个包含任意数量元素的不可变集合

  • 注意

  • a、在给元素的时候,不能给重复的元素;

  • b、返回的集合不能做增删操作,没有修改的方法;

二、泛型的好处

**1,类型安全


泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。


2,消除强制类型转换


泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。


3,潜在的性能收益


泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。

三、通配符

3.1 最常用的 T,E,K,V 和 ?

T,E,K,V,?是最常用的通配符,也是最容易理解的,它们是这样约定的:


  • ? 表示不确定的 java 类型

  • T (type) 表示具体的一个 java 类型

  • K V (key value) 分别代表 java 键值中的 Key Value

  • E (element) 代表 Element

3.2 "?" 无界通配符

有一个父类 Animal 和几个子类,如狗、猫等,现在我需要一个动物的列表,第一个想法是像这样的:


List<Animal> animalsList
复制代码


但是老板的想法确实这样的:


List<? extends Animal> animalsList
复制代码


为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。


/* * 统计几条腿 */static int countLegs1 (List<? extends Animal > animals ) {    int retVal = 0;    for ( Animal animal : animals )    {        retVal += animal.countLegs1();    }    return retVal;}/* * 统计几条腿 */static int countLegs2 (List< Animal > animals ){    int retVal = 0;    for ( Animal animal : animals )    {        retVal += animal.countLegs2();    }    return retVal;}
public static void main(String[] args) { List<Dog> dogs = new ArrayList<>(); // 不会报错 countLegs( dogs ); // 报错 countLegs1(dogs);}
复制代码


所以总结如下:


对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs1 方法中,限定了上界,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs2 就不行,当调用 countLegs2 时,程序会报类型转换异常

3.3 ?extends T 上界通配符

上界通配符:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。


在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:


  • 如果传入的类型不是 E 或者 E 的子类,编译不成功

  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用


private <K extends A, E extends B> E test(K arg1, E arg2){    E result = arg2;    arg2.compareTo(arg1);    //.....    return result;}
复制代码


类型参数列表中如果有多个类型参数上限,用逗号分开

3.4 ? super T 下界通配符

下界通配符: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object


在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。


private <T> void test(List<? super T> a, List<T> b){    for (T t : b) {        a.add(t);    }}
public static void main(String[] args) { List<Dog> dogs = new ArrayList<>(); List<Animal> animals = new ArrayList<>(); new Test3().test(animals,dogs);}
// Dog 是 Animal 的子类class Dog extends Animal {
}
复制代码


a 类型 “大于等于” b 的类型,这里的“大于等于”是指 a 表示的范围比 b 要大,因此装得下 a 的容器也就能装 b。

3.5 T 和 ?的区别

3.5.1 通过 T 来确保泛型参数的一致性

// 通过 T 来 确保 泛型参数的一致性public <T extends Number> void test(List<T> dest, List<T> src)
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型public void test(List<? extends Number> dest, List<? extends Number> src)
复制代码


像下面的代码中,约定的 T 是 Number 的子类才可以,但是申明时是用的 String ,所以就会飘红报错。


public static void main() {    Demo<String> demo = new Demo<>();    List<String> a = new ArrayList<>();    List<String> b = new ArrayList<>();    demo.test(a,b);}/** * 通过T来确保泛型参数的一致性 */public <T extends Numbers> void test(List<T> a,List<T> b) {    System.out.println(a);    System.out.println(b);}
复制代码


不能保证两个 List 具有相同的元素类型的情况


Demo demo = new Demo<>();List<String> a = new ArrayList<>();List<Number> b = new ArrayList<>();demo.testNon(a,b);
复制代码


上面的代码在编译器并不会报错,但是当进入到testNon方法内部操作时(比如赋值),对于ab而言,就还是需要进行类型转换。

3.5.2 类型参数可以多重限定而通配符不行

public calss Demo implements DemoInterfaceA,DemoInterfaceB {    /**     * 使用“&”符号设定多重边界 (Multi Bounds)     */    public static<T extends DemoInterfaceA & DemoInterfaceB> void test(T t){            }}
/* * 接口A */interface DemoInterfaceA {}
/* * 接口B */interface DemoInterfaceB {}
复制代码


使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 DemoInterfaceA 和 DemoInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。

3.5.3 通配符可以使用超类限定而类型参数不行

类型参数 T 只具有一种类型限定方式:


T extends A
复制代码


但是通配符 ? 可以进行两种限定:


? extends A? super A
复制代码

3.5.4 注意事项

// 指定结合元素只能用 T 类型List<T>  list = new ArrayList<T>();// 集合元素可以是任意类型,这种没哟意义,一般方法中,只是为了说明用法List<?> list = new ArrayList<?>();
复制代码


?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :


// 可以T t = operate();// 不可以? car = operate();
复制代码


简单总结下:


T 是一个确定的类型,通常用于泛型类和泛型方法的定义,?是一个不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

3.6 T 和 Object 的区别

Object 是 Java 的超类(所有对象的父类),在编码过程中就难免出现类型转化问题,且在编译阶段不会报错,到了运行阶段才暴露问题,大大降低了程序的安全性健壮性

3.6.1 转型的分类

  • 向上转型 -> 用父类声明一个子类对象

  • 实例:


Animal cat = new Cat();
复制代码


  • 向下转型 -> 将父类对象强转为其子类

  • 实例:


Animal cat = new Cat();Cat anotherCat = (Cat) cat;
复制代码

3.6.2 类型转换的问题

当我们使用 Object 作为泛型来使用时,不仅写起来麻烦,还要不停的进行类型转化,还很容易出现问题,如下实例:


public static void main(String[] args) {    Object number1 = 1;    Integer number2 = (Integer) number1;    String s = (String) number1;//报错}
复制代码


程序运行起来就会报类型转换异常


java.lang.Integer cannot be cast to java.lang.String
复制代码

3.7 Class<T> 和 Class<?> 区别

Class<T>Class<?> 最常见的是在反射场景下的使用,这里以用一段发射的代码来说明下。


// 通过反射的方式生成  demoA // 对象,这里比较明显的是,我们需要使用强制类型转换DemoA demoA = (DemoA)Class.forName("com.xiaoliucoding.test.DemoA").newInstance();
复制代码


对于上述代码,在运行期,如果反射的类型不是DemoA类,那么一定会报 java.lang.ClassCastException 错误。


对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题:


public class Test3 {        public static <T> T createInstance(Class<T> clazz) throws IllegalAccessException,InstantiationException {        return clazz.newInstance();    }        public static void main(String[] args) throws IllegalAccessException, InstantiationException {        A a = createInstance(A.class);        B b = createInstance(B.class);    } }
/* * A */class A {}
/* * B */class B {}
复制代码


Class<T> 在实例化的时候,T 要替换成具体类。Class<?> 它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。比如,我们可以这样做申明:


// 可以public Class<?> clazz;// 不可以,因为 T 需要指定类型public Class<T> clazzT;
复制代码


所以总结如下:


当不知道定声明什么类型的 Class 的时候可以定义一 个Class<?>,如果已经明确要反射的类型,必须让当前的类也指定 T

四、泛型的 PECS 原则

**PECS **是 Producer Extends,**Consumer Super **的缩写.

4.1 Producer Extends

现在要扩展 Stack 的功能,增加一个 pushAll 方法,我们第一版的实现可能是这样的。


public void pushAll(Iterable<E> source) {    for (E e : source) {        push(e);    }}
复制代码


上面的写法在使用时可能会报函数要求的参数类型与提供的不一致问题,如下图所示。


public static void main(String[] args) {    Stack<Number> statck = new Stack<>();    Iterable<Integer> integers = new ArrayList<>();    statck.pushAll(integers);// 报错}
复制代码


为了解决这个问题,我们需要做出如下修改,也就是把参数类型改成Iterable<? extends E>,这是一种有限制的通配符类型(bounded wildcard type),意思是"E 的某个子类型的Iterable接口"。pushAllsource参数产生 E 实例供 Stack 使用,也就是source是生产者,因此 source 的类型是Iterable<? extends E>


public void pushAll(Iterable<? extends E> source) {    for (E e : source) {      push(e);    }}
复制代码

4.2 Consumer Super

现在要扩展 Stack 的功能,增加一个与 pushAll 对应的 popAll 方法,我们第一版的实现可能是这样的。


public void popAll(Collection<E> destination) {    while (!isEmpty()) {        destination.add(pop());    }}
复制代码


上面的写法在使用时可能会报函数要求的参数类型与提供的不一致问题,如下代码所示:


public static void main(String[] args) {    Stack<Number> statck = new Stack<>();    Collection<Object> objects = new ArrayList<>();    statck.popAll(objects);// 报错}
复制代码


为了解决这个问题,我们需要做出如下修改,也就是把参数类型改成Collection<? super E>,意思是"E 的某种超类集合”。popAll 的 destination 参数通过 Stack 消费 E 实例,也就是destination是消费者,因此destination的类型是Collection<? super E>,如下所示。


public void popAll(Collection<? super E> destination) {    while (!isEmpty()) {        destination.add(pop());    }}
复制代码

4.3 PECS 原则总结

从上述 4.1 和 4.2 两方面的分析,总结 PECS 原则如下:


  • 如果要从集合中读取类型 T 的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)

  • 如果要从集合中写入类型 T 的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)

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

总结

这篇文章我们从 java 源码中的泛型通配符出发,介绍了什么是泛型、使用泛型的好处、泛型的通配符区别及含义,以及泛型的 PECS 原则,如有疏漏及错误,烦请留言补充。

参考

1、Java泛型T、E、K、V、N、和Object区别和含义

2、聊一聊-JAVA 泛型中的通配符 T,E,K,V,?

3、Java泛型中K、T、V、E、等的含义

4、Java泛型中的PECS原则

5、Java中的泛型类

感谢前人的经验、分享和付出,让我们可以有机会站在巨人的肩膀上眺望星辰大海!

发布于: 刚刚阅读数: 8
用户头像

后端工程师@代码都困了💤 2023-02-07 加入

啊呜(゚▽゚)/ . Talk is cheap (‾◡◝) ! , show me your code ! ヾ(๑╹◡╹)ノ"

评论

发布
暂无评论
「 Java基础-泛型 」一文说清Java泛型中的通配符T、E、K、V、N、?和Object的区别和含义_Java_小刘学编程_InfoQ写作社区