为什么JVM上没有C#语言?浅谈Type Erasure特性

2024-03-25 19:38

本文主要是介绍为什么JVM上没有C#语言?浅谈Type Erasure特性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

0?wx_fmt=png

每次提到语言的时候我总是忍不住骂Java是一门生产力低下,固步自封的语言——这估计要一直等到Java语言被JVM上的其他语言取代之后吧。JVM上目前已经有许多语言了:JRuby,Jython;还有一些特定于JVM平台的语言,如Scala和Groovy等等。但是,为什么JVM上没有C#语言呢?按理说,这门和Java十分相似,却又强大许多的语言更容易被Java程序员接受才对。您可能会说,Sun和微软是对头,怎么可能将C#移植到JVM平台上呢?嗯,有道理,但是为什么社区里也没有人这么做呢(要知道JVM上其他语言都是由社区发起的)?其实在我看来,这还是受到了技术方面的限制。

泛型是Java和C#语言的重要特性,它使得程序员可以方便地进行类型安全的编程,而不需要像以前那样不断进行类型转换。例如,我们要在Java中写一个泛型字典的封装便可以这么做:

public class DictWrapper{    private HashMap<K, V> m_container = new HashMap<K, V>();    public V get(K key) {        return this.m_container.get(key);}    public void put(K key, V value) {        this.m_container.put(key, value);}
}

看上去和C#并没有什么区别,不是吗?不过,如果我们观察编译后生成的bytecode(类似于.NET平台上的IL),便会发现一丝奇妙之处。使用javap -c DictWrapper得到的结果是:

Compiled from "DictWrapper.java"
public class jeffz.practices.DictWrapper extends java.lang.Object{
public jeffz.practices.DictWrapper();Code:0:	aload_01:	invokespecial	#1; //Method java/lang/Object."<init>":()V4:	aload_05:	new	#2; //class java/util/HashMap8:	dup9:	invokespecial	#3; //Method java/util/HashMap."<init>":()V12:	putfield	#4; //Field m_container:Ljava/util/HashMap;15:	returnpublic java.lang.Object get(java.lang.Object);Code:0:	aload_01:	getfield	#4; //Field m_container:Ljava/util/HashMap;4:	aload_15:	invokevirtual	#5; //Method java/util/HashMap.get:(Ljava/lang/Object;)Ljava/lang/Object;8:	areturnpublic void put(java.lang.Object, java.lang.Object);Code:0:	aload_01:	getfield	#4; //Field m_container:Ljava/util/HashMap;4:	aload_15:	aload_26:	invokevirtual	#6; //Method java/util/HashMap.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;9:	pop10:	return
}

从bytecode中可以看出,其中并没有包含任何与K,V有关的信息。get/put方法的参数和返回值都是Object类型,甚至内置的HashMap也是如此。那么调用DictWrapper的代码是如何做到“强类型”的呢?例如:

public static void main(String[] args) {DictWrapper<String, String> dict = new DictWrapper<String, String>();dict.put("Hello", "World");String world = dict.get("Hello");
}

它的bytecode便是:

public static void main(java.lang.String[]);Code:0:	new	#2; //class jeffz/practices/DictWrapper3:	dup4:	invokespecial	#3; //Method jeffz/practices/DictWrapper."<init>":()V7:	astore_18:	aload_19:	ldc	#4; //String Hello11:	ldc	#5; //String World13:	invokevirtual	#6; //Method jeffz/practices/DictWrapper.put:(Ljava/lang/Object;Ljava/lang/Object;)V16:	aload_117:	ldc	#4; //String Hello19:	invokevirtual	#7; //Method jeffz/practices/DictWrapper.get:(Ljava/lang/Object;)Ljava/lang/Object;22:	checkcast	#8; //class java/lang/String25:	astore_226:	return
}

看到标号为22的那行代码没有?这条checkcast指令便是将上一句invokevirtual的结果转化为String类型——DictWrapper.get所返回的是个最普通不过的Object。

这便是Java语言的泛型实现——请注意我这里说的是Java语言,而不是JVM。因为JVM本身并没有“泛型”的概念,Java语言的泛型则完全是编译器的魔法。我们写出的泛型代码,事实上都是和Object对象在打交道,是编译器在帮我们省去了冗余的类型转换代码,以此保证了代码层面的类型安全。由于在运行时去除所有泛型的类型信息,因此这种泛型实现方式叫做Type Erasure(类型擦除)。

在.NET中则完全不同,“泛型”是真真切切落实在CLR层面上的功能。例如DictWrapper.Get方法在.NET上的IL代码便是:

.method public hidebysig instance !TValue Get(!TKey key) cil managed
{.maxstack 2.locals init ([0] !TValue CS$1$0000)L_0000: nop L_0001: ldarg.0 L_0002: ldfld class [mscorlib]...Dictionary`2 ...DictWrapper`2::m_containerL_0007: ldarg.1 L_0008: callvirt instance !1 [mscorlib]...Dictionary`2::get_Item(!0)L_000d: stloc.0 L_000e: br.s L_0010L_0010: ldloc.0 L_0011: ret 
}

您可以发现,.NET的IL便确切包含了TKey和TValue的类型信息。而在运行的时候,CLR会为不同的泛型类型生成不同的具体类型代码,这在我之前的文章中也有所提及。

那么,Java和C#两种泛型实现方式分别有什么优势和劣势呢?Java这种Type Erasure做法,最大的优势便在于其兼容性:即便使用了泛型,但最后生成的二进制文件也可以运行在泛型出现之前的JVM上(甚至JDK中不需要添加额外的类库)——因为这里的泛型根本不涉及JVM的变化。而.NET中的泛型需要CLR方面的“新能力”,因此.NET 2.0的程序集是无法运行在CLR 1.0上的——当然.NET 1.0的程序集可以直接在CLR 2.0上执行。而CLR实现方式的优势,便在于可以在运行期间体现出“模板化”的优势。.NET程序员都知道,泛型可以节省值类型的装箱和拆箱的开销,即便是引用类型也可以避免额外的类型转化,这些都能带来性能上的提高。

因此,在.NET社区经常会把Java的这种实现方式称之为“假泛型”,而同时也会有人反驳到:泛型本来就是语言上的概念,实现不同又有什么关系,凭什么说是“假”的呢?其实,由于失去了JVM的支持,一些.NET平台上常用的,非常有效的开发方式都难以运用在Java上。例如所谓的泛型字典:

public class Cache<TKey, TValue>
{    public static TValue Instance;
}public class Factory{    public static string Create<TKey>(){        if (Cache<TKey, string>.Instance == null){            Cache<TKey, string>.Instance = // some expensive computation        }        return Cache<TKey, string>.Instance;}
}

由于Cache<TKey>在运行时是个具体独立的类型,因此泛型字典是性能最高的存储方式,比O(1)时间复杂度的哈希表还要高出许多。如果说这也只是运行方面的优势,那么这段代码中的“泛型工厂”代码(即Factory.Create<SomeType>(),包括类似的Factory<T>.Create()这种)则是Java语言中无法实现的。这是因为Type Erasure的作用,在运行时JVM已经丧失了TKey这样的类型信息,而在.NET平台上,TKey则是Create<TKey>签名的组成部分。

Type Erasure造成的限制还有不少,如果您是一个C#程序员,可能难以相信以下的Java代码都是不合法的:

public class MyClass<E> {    public static void myMethod(Object item) {        if (item instanceof E) { // Compiler error...}E item2 = new E(); // Compiler errorE[] iArray = new E[10]; // Compiler error}
}

由于JVM不提供对泛型的支持,因此对于JVM上支持泛型的语言,如Scala,这方面的压力就完全落在编译器身上了。而且,由于这些语言以JVM为底,Type Erasure会影响JVM平台上几乎所有语言。以Scala为例,它的模式匹配语法可以用来判断一个变量的类型:

value match {    case x:String => println("Value is a String")    case x:HashMap[String, Int] => println("Value is HashMap[String, Int]")    case _ => println("Value is not a String or HashMap[String, Int]")
}

猜猜看,如果value变量是个HashMap[Int, Object]类型的对象,上面的代码会输出什么结果呢?如果是C#或是F#这样运行在.NET平台上的语言,最终输出的一定是“Value is not ...”。只可惜,由于JVM的Type Erasure特性,以上代码输出的却是“Value is HashMap[String, Int]”。这是因为在运行期间JVM并不包含泛型的类型信息,HashMap[K, V]即是HashMap,无论HashMap[String, Int]还是HashMap[Int, Object]都是HashMap,JVM无法判断不同泛型类型的集合之间有什么区别。不过还好,Scala编译器遇到这种情况会发出警告,程序员可以了解这些代码可能会出现的“误会”。

因此,为什么有IKVM.NET这样的项目可以将Java语言编译成.NET程序集(也可以将Java的jar包转化成.NET程序集),却没有项目将C#编译到JVM上(或是将C#程序集转化为jar包)。这是因为,JVM不足以支撑C#语言所需要的所有特性。而从运行时的中间代码角度来说,JVM Bytecode的能力也是.NET IL的子集——又有什么办法可以将超集塞入它的子集呢?

此外,如CLR的值类型可能也很难直接落实在JVM上,这也是JVM上运行C#的又一阻碍。由于这些因素存在,我想如F#这样的.NET语言也几乎不可能出现在JVM上了。

当然,如果真要在JVM上实现完整的C#也并非不可以。只要在JVM上进行一层封装(例如还是就叫做CLR,CLR Language Runtime),一定可以满足C#的全部要求。但是这个代价太高,即使实现了这点可能也没什么实际意义。而事实上,已经有人在JVM上实现了一个x86模拟器,那么又有什么是做不了的呢?实在不行,我们就在模拟器上装一个Windows操作系统,然后装一个Microsoft .NET,再……

0?wx_fmt=png

这篇关于为什么JVM上没有C#语言?浅谈Type Erasure特性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Spring Boot中WebSocket常用使用方法详解

《SpringBoot中WebSocket常用使用方法详解》本文从WebSocket的基础概念出发,详细介绍了SpringBoot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消... 目录一、WebSocket基础概念1.1 什么是WebSocket1.2 WebSocket与HTTP

C#中Guid类使用小结

《C#中Guid类使用小结》本文主要介绍了C#中Guid类用于生成和操作128位的唯一标识符,用于数据库主键及分布式系统,支持通过NewGuid、Parse等方法生成,感兴趣的可以了解一下... 目录前言一、什么是 Guid二、生成 Guid1. 使用 Guid.NewGuid() 方法2. 从字符串创建

SpringBoot+Docker+Graylog 如何让错误自动报警

《SpringBoot+Docker+Graylog如何让错误自动报警》SpringBoot默认使用SLF4J与Logback,支持多日志级别和配置方式,可输出到控制台、文件及远程服务器,集成ELK... 目录01 Spring Boot 默认日志框架解析02 Spring Boot 日志级别详解03 Sp

java中反射Reflection的4个作用详解

《java中反射Reflection的4个作用详解》反射Reflection是Java等编程语言中的一个重要特性,它允许程序在运行时进行自我检查和对内部成员(如字段、方法、类等)的操作,本文将详细介绍... 目录作用1、在运行时判断任意一个对象所属的类作用2、在运行时构造任意一个类的对象作用3、在运行时判断

java如何解压zip压缩包

《java如何解压zip压缩包》:本文主要介绍java如何解压zip压缩包问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java解压zip压缩包实例代码结果如下总结java解压zip压缩包坐在旁边的小伙伴问我怎么用 java 将服务器上的压缩文件解压出来,

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Spring WebFlux 与 WebClient 使用指南及最佳实践

《SpringWebFlux与WebClient使用指南及最佳实践》WebClient是SpringWebFlux模块提供的非阻塞、响应式HTTP客户端,基于ProjectReactor实现,... 目录Spring WebFlux 与 WebClient 使用指南1. WebClient 概述2. 核心依

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注