Java泛型的中庸之道

2023-10-24 08:59
文章标签 java 泛型 中庸之道

本文主要是介绍Java泛型的中庸之道,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。—— 《Java编程思想》

面向对象的程序设计语言都有一种共有的复用方式——依赖继承体系的多态,来实现一种纵向复用。除此以外,诸如C++和C#还拥有不依赖继承体系的泛型,来实现一种横向复用。而Java,这个在其1.5版本才加入泛型特性的语言,为了平滑兼容已有的非泛型代码,其泛型依旧充满了继承结构的味道,只能说是一个瞻前顾后、遵循中庸之道的无奈的伪泛型。

这里写图片描述

以下我们只讲Java的泛型。

一、泛型的创建和使用

1.1、泛型创建和使用之间的矛盾

对于Java泛型,编写泛型类和泛型方法是相当简单的,但是编写出能够操作其泛型类型的泛化代码就需要额外的努力了,这些努力需要类创建者和类消费者共同付出,他们必须理解适配器设计模式的概念和实现。——《Java编程思想》

如上所言,创建泛型很容易:

class Holder<T> {T t;void set(T t){//...}T get(){return t;}
}

如上Holder类就是一个泛型类,但是因为类型擦除的原因,在set()方法的实现中,你只能将变量t当作Object来使用,只能调用Object类的方法,因此类创建者的设计将会无比受限。

在创建泛型时,可以设置一个泛型上界,从而在一定程序上减轻泛型实现时的限制:

class Holder<T extends Fruit> {T t;void set(T t){//...}T get(){return t;}
}

这时,泛型擦除到Fruit类型,在set()的方法体中你便可以将变量t当作Fruit来使用。但是,这个类的使用者,就需要让自己的类都继承于Fruit类,才能塞到Holder中去,这对类使用者来说是一定程度上的限制,该泛型的泛化程度也因此被限制了。

这就像一个天平,天平的左端是“泛化程度”,天平的右端是“泛型实现”,根据类的实际情况,两者取其平衡,是在Java的泛型中必须权衡的问题。

1.2、泛型类/泛型接口的创建和使用

简单的泛型类创建上一小节已举过例子,还有几种稍微复杂的情形:

//在继承时创建泛型
class MyHolder<T> extends Holder<T>{
}//在继承时创建泛型,并对泛型参数做进一步扩充
class MyHolder<T,U> extends Holder<T>{
}//泛型类型参数除了必须是Fruit的子类外,还必须实现了eatable接口和tasty接口
class Holder<T extends Fruit & eatable & tasty>{
}

而泛型类的使用有如下两种情形:

  • 在new时,直接在<>中声明类型。 (若只在赋值语句的左侧的<>符号中有类型,说明用到了类型参数推断技术,该技术在1.7版本引入,在1.8版本进一步优化)
  • 在继承已声明类型的泛型类时,如下:
//重写父类方法时,可直接使用已参数化的方法形参
class MyHolder extends Holder<Fruit> {@Overridevoid set(Fruit t){//...}
}//直接在类中调用父类已参数化的方法
class MyHolder extends Holder<Fruit> {void callFatherMedthod(){//get()是父类Holder的方法,并且具有了Fruit类的参数信息Fruit t = this.get();}
}

泛型接口的创建和使用和泛型类类似,不再赘言。

1.3、泛型方法的创建和使用

如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。——《Java编程思想》

//泛型方法创建
class GenericMedthodWrapper{static <T> void genericMedthod(List<T> list){//...}
}//泛型方法使用:不必指明参数类型,会自动进行类型参数推断,就好像该方法被无限次的重载过
List<User> users = new ArrayList<>();
GenericMedthodWrapper.genericMedthod(users)

二、类型擦除和擦除补偿

2.1、类型擦除

在C#中,List<int>和List<String>就是两个不同的类型,它们在运行期生成,有自己的虚方发表和类型数据,是真实泛型。

Java泛型说到底只不过是编译期的一个语法糖,所谓语法糖就是让你少写点代码也能达到相同的效果,而这你偷懒少写的代码,编译器帮你补上了,它是编译器实现的一些小把戏。所以Java泛型语法只对编译器生效,故而也就只在你自己的源码中存在,在编译后的字节码中,已不存在任何的泛型语义信息,它已经被编译器擦除了。

在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的值的转型。有且仅有边界是发生动作的地方。

在泛型代码内部,无法获得任何有关泛型参数类型的信息

2.2、擦除补偿

在泛型代码内部确实无法直接获取泛型参数类型信息,但是可以通过一些额外的补偿手段,间接地获取类型信息。

1、在泛型类中保存一个对应泛型参数类型的Class对象,通过Class对象的isInstance()和newInstance()方法。
2、在泛型类中保存一个对应泛型参数类型的对象,获取该对象后从该对象身上挖掘类型信息。
3、通过java.lang.reflect.ParameterizedType接口。

三、通配符

Java的泛型通配符,实际上是在泛型不变性的前提下,解决了泛型的协变和逆变问题。

关于协变逆变,以及涉及到的里氏替换原则,可参考下面两篇博文:
设计模式六大原则(2):里氏替换原则
Java中的逆变与协变

先厘清一点,泛型通配符是在使用泛型类的时候使用的,而不是在创建泛型时使用的。以下讨论的都是在使用泛型类时通配符的用法:

class Fruit {
}
class Apple extends Fruit {
}//对类来说,这样引用是没毛病的
Fruit fruit = new Apple();
//但这样引用就不行了,会报错
List<Fruit> fruitList = new ArrayList<Apple>()

3.1、<? extends Fruit> 协变

真正的问题是我们在谈论容器的类型,而不是容器持有的类型。但如果真的就想这样引用,该怎么做呢?如下这样就可以了:

List<? extends Fruit> fruitList = new ArrayList<Apple>();Fruit fruit = fruitList.get(i);
fruitList.add(new Apple()); //error
fruitList.add(new Object()); //error

但是,通配符引用的是明确的类型,但是这个 ? extends Fruit 意味着fruitList要持有某种具体的Fruit或其子类型,也就意味着编译器根本无法确定 ? extends Fruit 引用的是什么类型,所以编译器很决绝,直接不让你再通过fruitList.add()添加任何对象了,对,是任何,Object也不行。而通过fruitList.get()出来的对象即是Fruit类型。

3.2、<? super Fruit> 逆变

然而我就是像添加对象怎么办呢?可如下这样:

List<? super Fruit> fruitList = new ArrayList<Apple>();Object obj = fruitList.get(i);
fruitList.add(new Fruit());
fruitList.add(new Apple());  

? super Fruit 意味着fruitList要持有某种具体的Fruit或其父类型,所以此时就可以通过fruitList.add()添加Fruit类型或其子类了,但是通过fruitList.get()出来的只能是Object类型。

以上 ? extends Fruit 和 ? super Fruit 对于add()和get()的限制,让人初看有点匪夷所思,但仔细想想还是有道理的,需要稍加品悟。

3.3、<?>

还有最后一个更让人匪夷所思的通配符 —— 无界通配符<?>。使用无界通配符好像等价于使用原生类型,但其实二者还有有所区别的。还是那句话:“通配符引用的是明确的类型”,List实际上表示“持有任何Object类型的原生List”,而List<?>表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么”。

List<?> list = new ArrayList<>();
list.add(new Object()); //error

不能向这个list加入除了null以外的任何对象,这个道理和之前的 ? extends Fruit 是类似的,? extends Fruit 都不能加入任何对象,就更别说 ? 了。

无界通配符有一个重要应用:当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定的类型。比如声明一个 Map<String, ?>

四、泛型的真正内涵

先厘清几个概念(个人见解,理性看待),且只抓核心要害:

第一个范畴:

  1. 静态语言: 又可称“静态编译语言”,指从源码转为目标代码/中间代码的动作发生在程序编译阶段。
  2. 动态语言:又可称“动态编译语言”,指从源码转为目标代码/中间代码的动作发生在程序运行阶段。

第二个范畴:

  1. 静态类型语言:类型的区分标志仅在于你赋予这个类型的独特名字,而跟这个类型中含有的属性和方法签名无关。
  2. 动态类型语言:所有类型的名字都一样,继而也就不会再关心类型的名字,类型的区分标志仅在于类型中的属性和方法签名。

第三个范畴:

  1. 强类型语言:变量需指定类型信息,只有确是该类型的值才可赋给该变量。
  2. 弱类型语言:变量无需指定类型信息,任何类型的值都可赋给该变量。

不同范畴之间的概念,没有直接关联,比如动态类型语言不一定是动态语言,也不一定是弱类型语言。但是动态类型语言通过运行期再编译的方式确实更易实现且合理,所以大多数的动态类型语言都是动态语言,比如javascript、groovy,所以人们常把动态类型语言说成动态语言。但是,C++的泛型实现却是在编译期间,就通过动态类型语言特性完成的。

一门语言设计成动态语言还是静态语言,没有必然的因果逻辑关系,仅仅是语言规范中的人为规定,除去性能因素,动态语言和静态语言在最终的使用效果上可以说没有区别。但是一门语言是动态类型语言还是静态类型语言,对于程序员的日常编程的设计和实施都会影响甚大。

4.1、泛型的本质

所谓泛型,就是在静态类型语言中去渴求动态类型语言的特性。你也会发现真正的动态类型语言中,不会有,也没有必要有泛型这种东西,泛型只会出现在静态类型中。

泛型的目的就是:
(1)通过某种途径来放宽对我们的代码将要作用的类型所作的限制。
(2)同时不丢失静态类型检查的好处。

4.2、Java对缺乏动态类型的补偿

(1)Java中没有动态类型的特性,但是可以通过反射,达到和动态类型相似的效果。可以调用Class类的getMethod()方法和Method类的invoke方法。
(2)迎合基于继承的类型擦除,让我们的类来继承泛型上界,或者通过适配器设计模式来继承泛型上界。

4.3、Java的泛型究竟带来了什么

Java的泛型带来了一定程度的泛化效果,但更多地是带来了程序语言的可表达性,提升了语义准确性,以及将曾经的运行期检查提前到了编译期。

五、参考书籍

《Java编程思想》—— Bruce Eckel
第15章 泛型

《深入理解Java虚拟机》—— 周志明
第10章 10.3 Java语法糖的味道

《Groovy程序设计》—— Venkat Subramaniam
第3章 动态类型

这篇关于Java泛型的中庸之道的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/274117

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows