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

相关文章

Spring Boot项目部署命令java -jar的各种参数及作用详解

《SpringBoot项目部署命令java-jar的各种参数及作用详解》:本文主要介绍SpringBoot项目部署命令java-jar的各种参数及作用的相关资料,包括设置内存大小、垃圾回收... 目录前言一、基础命令结构二、常见的 Java 命令参数1. 设置内存大小2. 配置垃圾回收器3. 配置线程栈大小

SpringBoot实现微信小程序支付功能

《SpringBoot实现微信小程序支付功能》小程序支付功能已成为众多应用的核心需求之一,本文主要介绍了SpringBoot实现微信小程序支付功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作... 目录一、引言二、准备工作(一)微信支付商户平台配置(二)Spring Boot项目搭建(三)配置文件

解决SpringBoot启动报错:Failed to load property source from location 'classpath:/application.yml'

《解决SpringBoot启动报错:Failedtoloadpropertysourcefromlocationclasspath:/application.yml问题》这篇文章主要介绍... 目录在启动SpringBoot项目时报如下错误原因可能是1.yml中语法错误2.yml文件格式是GBK总结在启动S

Spring中配置ContextLoaderListener方式

《Spring中配置ContextLoaderListener方式》:本文主要介绍Spring中配置ContextLoaderListener方式,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录Spring中配置ContextLoaderLishttp://www.chinasem.cntene

java实现延迟/超时/定时问题

《java实现延迟/超时/定时问题》:本文主要介绍java实现延迟/超时/定时问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java实现延迟/超时/定时java 每间隔5秒执行一次,一共执行5次然后结束scheduleAtFixedRate 和 schedu

Java Optional避免空指针异常的实现

《JavaOptional避免空指针异常的实现》空指针异常一直是困扰开发者的常见问题之一,本文主要介绍了JavaOptional避免空指针异常的实现,帮助开发者编写更健壮、可读性更高的代码,减少因... 目录一、Optional 概述二、Optional 的创建三、Optional 的常用方法四、Optio

Spring Boot项目中结合MyBatis实现MySQL的自动主从切换功能

《SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能》:本文主要介绍SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能,本文分步骤给大家介绍的... 目录原理解析1. mysql主从复制(Master-Slave Replication)2. 读写分离3.

idea maven编译报错Java heap space的解决方法

《ideamaven编译报错Javaheapspace的解决方法》这篇文章主要为大家详细介绍了ideamaven编译报错Javaheapspace的相关解决方法,文中的示例代码讲解详细,感兴趣的... 目录1.增加 Maven 编译的堆内存2. 增加 IntelliJ IDEA 的堆内存3. 优化 Mave

Java String字符串的常用使用方法

《JavaString字符串的常用使用方法》String是JDK提供的一个类,是引用类型,并不是基本的数据类型,String用于字符串操作,在之前学习c语言的时候,对于一些字符串,会初始化字符数组表... 目录一、什么是String二、如何定义一个String1. 用双引号定义2. 通过构造函数定义三、St

springboot filter实现请求响应全链路拦截

《springbootfilter实现请求响应全链路拦截》这篇文章主要为大家详细介绍了SpringBoot如何结合Filter同时拦截请求和响应,从而实现​​日志采集自动化,感兴趣的小伙伴可以跟随小... 目录一、为什么你需要这个过滤器?​​​二、核心实现:一个Filter搞定双向数据流​​​​三、完整代码