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

相关文章

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

Spring核心思想之浅谈IoC容器与依赖倒置(DI)

《Spring核心思想之浅谈IoC容器与依赖倒置(DI)》文章介绍了Spring的IoC和DI机制,以及MyBatis的动态代理,通过注解和反射,Spring能够自动管理对象的创建和依赖注入,而MyB... 目录一、控制反转 IoC二、依赖倒置 DI1. 详细概念2. Spring 中 DI 的实现原理三、

SpringBoot 整合 Grizzly的过程

《SpringBoot整合Grizzly的过程》Grizzly是一个高性能的、异步的、非阻塞的HTTP服务器框架,它可以与SpringBoot一起提供比传统的Tomcat或Jet... 目录为什么选择 Grizzly?Spring Boot + Grizzly 整合的优势添加依赖自定义 Grizzly 作为

Java后端接口中提取请求头中的Cookie和Token的方法

《Java后端接口中提取请求头中的Cookie和Token的方法》在现代Web开发中,HTTP请求头(Header)是客户端与服务器之间传递信息的重要方式之一,本文将详细介绍如何在Java后端(以Sp... 目录引言1. 背景1.1 什么是 HTTP 请求头?1.2 为什么需要提取请求头?2. 使用 Spr

Java如何通过反射机制获取数据类对象的属性及方法

《Java如何通过反射机制获取数据类对象的属性及方法》文章介绍了如何使用Java反射机制获取类对象的所有属性及其对应的get、set方法,以及如何通过反射机制实现类对象的实例化,感兴趣的朋友跟随小编一... 目录一、通过反射机制获取类对象的所有属性以及相应的get、set方法1.遍历类对象的所有属性2.获取

Java中的Opencv简介与开发环境部署方法

《Java中的Opencv简介与开发环境部署方法》OpenCV是一个开源的计算机视觉和图像处理库,提供了丰富的图像处理算法和工具,它支持多种图像处理和计算机视觉算法,可以用于物体识别与跟踪、图像分割与... 目录1.Opencv简介Opencv的应用2.Java使用OpenCV进行图像操作opencv安装j

java Stream操作转换方法

《javaStream操作转换方法》文章总结了Java8中流(Stream)API的多种常用方法,包括创建流、过滤、遍历、分组、排序、去重、查找、匹配、转换、归约、打印日志、最大最小值、统计、连接、... 目录流创建1、list 转 map2、filter()过滤3、foreach遍历4、groupingB