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

简介: Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。

前言

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

publicinterfaceEnumeration<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.*/booleanhasMoreElements();
/*** 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.*/EnextElement();
}

一、什么是泛型?

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

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

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

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

1.1 泛型的语法规则

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

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

privatestatic<E>voidprintArray(E[] inputArray)

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

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

privatestatic<E,T>voidprintArray(E[] inputArray, Tdata)

  • 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等形式的参数常用于表示泛型.

publicclassDemo<T> {
privateTt;
publicvoidsetT(Tt){
this.t=t;
    }
publicTgetT(){
returnt;
    }
}

1.4 泛型方法

  • 格式:修饰符<类型> 返回值类型方法名(类型 变量名){}
  • 范例:public <T> void show(T t){}
publicclassDemo<T> {
publicvoidshow(Tt){
System.out.println(t);
    }
}

1.5 泛型接口

  • 格式:修饰符 interface 接口名<类型>{ }
  • 范例:public interface Generic<T>{ }

定义接口:

publicinterfaceDemo<T> {
voidshow(Tt);
}

定义接口实现类:

publicclassDemoImlp<T>implementsDemo<T> {
@Overridepublicvoidshow(Tt){
System.out.println(t);
    }
}

1.6 可变参数

1.6.1 定义格式

  • 格式:修饰符 返回值类型 方法名(数据类型... a){ }
  • 范例:public static int sum(int... a){ }

1.6.2 示例

publicclassDemo {
publicstaticvoidmain(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));
    }
publicstaticintsum(int... a){//a其实是一个数组,用来存储参数intsum=0;
for(inti:a){
sum+=i;
        }
returnsum;
    }
}


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

publicstaticintfun1(int... a,intb){//报错    ......
}
publicstaticintfun2(intb,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类型KV (keyvalue) 分别代表java键值中的KeyValueE (element) 代表Element

3.2 "?" 无界通配符

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

List<Animal>animalsList

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

List<?extendsAnimal>animalsList

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

/** 统计几条腿*/staticintcountLegs1 (List<?extendsAnimal>animals ) {
intretVal=0;
for ( Animalanimal : animals )
    {
retVal+=animal.countLegs1();
    }
returnretVal;
}
/** 统计几条腿*/staticintcountLegs2 (List<Animal>animals ){
intretVal=0;
for ( Animalanimal : animals )
    {
retVal+=animal.countLegs2();
    }
returnretVal;
}
publicstaticvoidmain(String[] args) {
List<Dog>dogs=newArrayList<>();
// 不会报错countLegs( dogs );
// 报错countLegs1(dogs);
}

所以总结如下:

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

3.3 ?extends T 上界通配符

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

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

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
private<KextendsA, EextendsB>Etest(Karg1, Earg2){
Eresult=arg2;
arg2.compareTo(arg1);
//.....returnresult;
}

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

3.4 ? super T 下界通配符

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

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

private<T>voidtest(List<?superT>a, List<T>b){
for (Tt : b) {
a.add(t);
    }
}
publicstaticvoidmain(String[] args) {
List<Dog>dogs=newArrayList<>();
List<Animal>animals=newArrayList<>();
newTest3().test(animals,dogs);
}
// Dog 是 Animal 的子类classDogextendsAnimal {
}

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

3.5 T 和 ?的区别

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

// 通过 T 来 确保 泛型参数的一致性public<TextendsNumber>voidtest(List<T>dest, List<T>src)
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型publicvoidtest(List<?extendsNumber>dest, List<?extendsNumber>src)

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

publicstaticvoidmain() {
Demo<String>demo=newDemo<>();
List<String>a=newArrayList<>();
List<String>b=newArrayList<>();
demo.test(a,b);
}
/*** 通过T来确保泛型参数的一致性*/public<TextendsNumbers>voidtest(List<T>a,List<T>b) {
System.out.println(a);
System.out.println(b);
}

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

Demodemo=newDemo<>();
List<String>a=newArrayList<>();
List<Number>b=newArrayList<>();
demo.testNon(a,b);

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

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

publiccalssDemoimplementsDemoInterfaceA,DemoInterfaceB {
/*** 使用“&”符号设定多重边界 (Multi Bounds)*/publicstatic<TextendsDemoInterfaceA&DemoInterfaceB>voidtest(Tt){
    }
}
/** 接口A*/interfaceDemoInterfaceA {}
/** 接口B*/interfaceDemoInterfaceB {}

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

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

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

TextendsA

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

? extendsA

? superA

3.5.4 注意事项

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

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

// 可以Tt=operate();
// 不可以car=operate();

简单总结下:

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

3.6 T 和 Object 的区别

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

3.6.1 转型的分类

  • 向上转型 -> 用父类声明一个子类对象
    实例:
Animalcat=newCat();
  • 向下转型 -> 将父类对象强转为其子类
    实例:
Animalcat=newCat();
CatanotherCat= (Cat) cat;

3.6.2 类型转换的问题

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

publicstaticvoidmain(String[] args) {
Objectnumber1=1;
Integernumber2= (Integer) number1;
Strings= (String) number1;//报错}

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

java.lang.Integercannotbecasttojava.lang.String

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

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

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

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

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

publicclassTest3 {
publicstatic<T>TcreateInstance(Class<T>clazz) throwsIllegalAccessException,InstantiationException {
returnclazz.newInstance();
    }
publicstaticvoidmain(String[] args) throwsIllegalAccessException, InstantiationException {
Aa=createInstance(A.class);
Bb=createInstance(B.class);
    } 
}
/** A*/classA {}
/** B*/classB {}

 

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

// 可以publicClass<?>clazz;
// 不可以,因为 T 需要指定类型publicClass<T>clazzT;

所以总结如下:

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

四、泛型的PECS原则

PECS Producer ExtendsConsumer Super 的缩写.

4.1 Producer Extends

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

publicvoidpushAll(Iterable<E>source) {
for (Ee : source) {
push(e);
    }
}

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

publicstaticvoidmain(String[] args) {
Stack<Number>statck=newStack<>();
Iterable<Integer>integers=newArrayList<>();
statck.pushAll(integers);// 报错}

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

publicvoidpushAll(Iterable<?extendsE>source) {
for (Ee : source) {
push(e);
    }
}

4.2 Consumer Super

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

publicvoidpopAll(Collection<E>destination) {
while (!isEmpty()) {
destination.add(pop());
    }
}

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

publicstaticvoidmain(String[] args) {
Stack<Number>statck=newStack<>();
Collection<Object>objects=newArrayList<>();
statck.popAll(objects);// 报错}

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

publicvoidpopAll(Collection<?superE>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中的泛型类

  1. 感谢前人的经验、分享和付出,让我们可以有机会站在巨人的肩膀上眺望星辰大海!
相关文章
|
6天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
29天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
68 14
|
25天前
|
Java
java中面向过程和面向对象区别?
java中面向过程和面向对象区别?
22 1
|
1月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
56 8
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
135 4
|
1月前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别
|
2月前
|
Java
通过Java代码解释成员变量(实例变量)和局部变量的区别
本文通过一个Java示例,详细解释了成员变量(实例变量)和局部变量的区别。成员变量属于类的一部分,每个对象有独立的副本;局部变量则在方法或代码块内部声明,作用范围仅限于此。示例代码展示了如何在类中声明和使用这两种变量。
|
4天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
6天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。