「JVM 编译优化」Java 语法糖(泛型、自动装箱/拆箱、条件编译)

news/2024/11/7 7:44:40/

「JVM 编译优化」Java 语法糖(泛型、自动装箱/拆箱、条件编译)

语法糖可以看做事前端编译期的一些小把戏;虽不会提供实质性的功能改进,但它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会;不过语法糖也并不一定都是有益的,大量添加和使用含糖的语法,容易让程序员产生依赖,无法看清语法糖背后代码的真实面目(编译层面);

Java 的语法糖有泛型、自动装箱、自动拆箱、遍历循环、变长参数、条件编译、内部类、枚举类、断言语句、数值字面量、对枚举和字符串的 switch 支持、try-resource(JDK 7)、Lambda 表达式(JDK 8,不算单纯的语法糖,但前端编译器做了大量转换工作)等;

要了解小把戏背后的真实实现,才能最好的利用好它;

文章目录

      • 1. 泛型
      • 2. 自动装箱、拆箱、遍历循环
      • 3. 条件编译

1. 泛型

泛型本质是参数化类型(Parameterized Type)或参数化多态(Parametric Polymorphism)的应用,对泛化的数据类型编写相同的算法(抽象);将操作的数据类型指定为方法签名中的一种特殊参数,参数类型可用在类、接口、方法的创建中分别构造泛型类、泛型接口、泛型方法;

Java 泛型 vs. C# 泛型

  • 类型擦除式泛型Type Erasure Generics),Java 的实现方式,泛型只存在于程序源码,在编译后的字节码中所有泛型都变成原裸那类型(Raw Type),并插入相应的强制转型代码;在运行期的 ArrayList<Int>ArrayList<String> 是同一种类型;
  • 具现化式泛型Reified Generics),C# 的实现方式,泛型在程序源码、编译后的中间语言表示(Intermediate Language,泛型是一个占位符)、运行期的 CLR 里都是切实存在的;在运行期的 List<Int>List<String> 是两种不同类型,它们由系统在运行期生成;

Java 不合法的泛型用法

public class TypeErasureGenerics<E> {public void doSomething(Object item) {if (item instanceof E) {    // 不合法,无法对泛型进行实例判断;// ...}E newItem = new E();        // 不合法,无法使用泛型创建对象;E[] itemArray = new E[10];  // 不合法,无法使用泛型创建数组;}
}

相比 C# 的泛型,除了使用层面上需要更多代码、更多类型参数来编写使用;Java 的泛型在执行性能方面是难以用应用编码弥补的(需要大量拆箱装箱、构造容器,引入复杂度高的代码,降低了复用性,几乎丧失了泛型本身存在的价值);

Java 的泛型实现只需在 javac 编译器上做出改进,不需要改动字节码、JVM,保障了以前无泛型的库直接运行在新版 JDK 环境;

泛型的历史背景

《Java 语言规范》严肃承诺二进制向后兼容(Binary Backwards Compatibility);

// 协变(Covariant)演示
Object[] array = new String[10];
array[0] = 10;                      // 编译正常、运行报错;ArrayList things = new ArrayList();
things.add(Integer.valueOf(10));    // 编译、运行皆正常;
things.add("hello world");
  • C# 版)需要泛型化的类型(容器类型),以前有的保持不变,并平行添加一套泛型化版本的新类型;C# 新增一组 System.Collections.Generic 容器,原 System.Collections 和 System.Collection.Specialized 容器依旧存在;
  • Java 版)直接把所有需要泛型化的已有类型原地泛型化,不添加任何平行泛型版本;Java 也尝试了引入新的集合类,但因遗留代码规模和流行度较大、设计实现时间不足等原因,最终选择了类型擦除式实现;

类型擦除

  • 裸类型Raw Type),所有该类型泛型化的共同父类型(Super Type);
ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list;     // 裸类型
list = ilist;
list = slist;
  • 在运行期由 JVM 自动真实的构造 ArrayList 的类型,病自动实现从 ArrayList 派生自 ArrayList 的继承关系;
  • 在编译期把 ArrayList 还原会 ArrayList,只在元素反问、修改时自动插入一些类型强转和检查的指令;

泛型擦除示例

public static void main(String[] args) {Map<String, String> map = new HashMap<String, String>();map.put("hello", "你好");map.put("how are you?", "吃了吗?");System.out.println(map.get("hello"));System.out.println(map.get("how are you?"));
}

擦除后(编译后)的效果

public static void main(String[] args) {Map map = new HashMap();map.put("hello", "你好");map.put("how are you?", "吃了吗?");System.out.println((String) map.get("hello"));System.out.println((String) map.get("how are you?"));
}

擦除法的缺陷

  • 使用擦除法实现泛型时原始类型(Primitive Type)数据的支持很麻烦,因为基础类型与 Object 之间无法强转;因此 Java 的泛型直接不支持原始类型;
  • 运行期无法取到泛型类型信息;如需知道泛型类型信息,需额外通过 Class<T> 类型的参数传递进来;
  • 擦除发实现泛型导致一些面向对象思想变得模糊;
// 重载 1,无法编译通过;编译后的裸类型相同;
public class GenericTypes {public static void method(List<String> list) {System.out.println("invoke method(List<String> list)");}public static void method(List<Integer> list) {System.out.println("invoke method(List<Integer> list)");}
}// 重载 2,可以编译通过;Class 文件允许描述符不完全一致的两个方法共存;
public class GenericTypes {public static String method(List<String> list) {System.out.println("invoke method(List<String> list)");return "";}public static int void method(List<Integer> list) {System.out.println("invoke method(List<Integer> list)");return 1;}

Java 泛型的引入在 JVM 解析、反射等场景下的方法调用带来了新的需求,如泛型类中获取传入的参数化类型等;JCP 为此引入了诸如 Signature、LocalVariableTypeTable 等新的属性用于解决伴随泛型而来的参数类型识别问题;
擦除法实际仅仅对方法的 Code 属性中字节码进行了擦除,元数据中还是保留着泛型信息,反射手段是可以取到参数化类型的;

值类型与未来的泛型

Oracle 在 2014 年建立了 Valhalla 语言改进项目,用于改进 Java 语言中各种缺陷(泛型的缺陷是主要目标之一);

Valhalla 对新泛型实现规划了多种方案,如 Model 1 和 Model 3;其中泛型可能被具现化,也可能继续维持类型擦除(不完全擦除)以保兼容性;

目前比较明确的是未来 Java 会提供值类型Value Type)的语言层面支持;值类型可以与引用类型一样具有构造函数、方法、属性字段等,区别在于它的赋值通常是整体赋值,而不像引用类型的传递引用;这样值类型实例更容易实现在调用栈上分配,可以随着退出方法自动回收,从而减轻 GC 压力;

Valhalla 中的值类型方案被称为内联类型,通过一个新的关键字 inline 来定义,字节码层面则以与原生类型对应的 Q 开头的新操作码(如 iload 对应 qload)来支撑;

即时编译场景下,可以使用逃逸分析优化来处理内联类型;通过编码时标注和内联类型实例的不可变性,可以很好的解决逃逸分析面对传统引用类型时难以判断对象是否逃逸的问题;

2. 自动装箱、拆箱、遍历循环

自动装箱、自动拆箱、循环遍历(for-each 循环)等语法糖是 Java 中被使用最多的语法糖;

自动装箱、自动拆箱、循环遍历演示

public static void main(String[] args) {List<String> list = Arrays.asList(1, 2, 3, 4;int sum = 0;for (int i : list) {sum += i;}System.out.println(sum);
}

编译后的效果

public static void main(String[] args) {// 1. 泛型被擦除;// 2. 自动装箱、拆箱被转化成对应的包装盒还原方法;// 3. 变长参数变成数组类型的参数;List list = Arrays.asList(new Integer[] {Integer.valueOf(1),Integer.valueOf(2),Integer.valueOf(3),Integer.valueOf(4)});int sum = 0;// 4. 循环遍历被还原成了迭代器的实现;这是遍历循环中被遍历的实力类需要实现 Iterable 接口的原因;for (Iterator localIterator = list.iterator(); localItertor.hasNext(); ) {int i = ((Integer) localIterator.next()).intValue();sum += i;}System.out.println(sum);
}

自动装箱的陷阱

public static void main(String[] args) {Integer a = 1;Integer b = 2;Integer c = 3;Integer d = 3;Integer e = 321;Integer f = 321;Long g = 3L;System.out.println(c == d);             // true,Integer 的享元模式实现方式让 -128 ~ 127 之间的实例复用;System.out.println(e == f);             // false,不在 Integer 享元范围,不是共享;System.out.println(c == (a + b));       // true,== 运算在遇到算术运算时自动拆箱;System.out.println(c.equals(a + b));    // true,类型与值皆相同System.out.println(g == (a + b));       // true,== 运算在遇到算术运算时自动拆箱;System.out.println(g.equals(a + b));    // false,值相同,但类型不同,equals() 方法不处理数据转型;
}

建议在实际编码中尽量避免自动装箱与拆箱;

3. 条件编译

Java 语言天然的编译方式不需使用预处理器(编译器并非一个个编译 Java 文件,而是通过编译单元构建语法树顶级节点待处理列表,再行编译,各个文件可以相互提供符号信息);

Java 语言的条件编译以使用条件为常量的 if 语句实现;跟进 boolean 值真假,编译器会将不成立的分支代码消除掉(编译器的解语法糖阶段完成);只支持方法体内部的语句基本块(Block)级别的条件编译,不支持整个 Java 类的控制;

public static void main(String[] args) { if (true) {System.out.println("block 1"); } else {System.out.println("block 2"); }
}

编译后的效果


public static void main(String[] args) { // 编译出来的结果只会保留 true 的分支System.out.println("block 1");
}

若其他带有条件判断能力的控制语句与常量搭配使用,可能会被拒绝编译;

public static void main(String[] args) { // 编译器将会提示“Unreachable code” while (false) {System.out.println(""); }
}

上一篇:「JVM 编译优化」javac 编译器源码解读

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》

http://www.ppmy.cn/news/26061.html

相关文章

2022黑马Redis跟学笔记.实战篇(六)

2022黑马Redis跟学笔记.实战篇 六4.7.达人探店功能4.7.1.分享探店图文1. 达人探店-发布探店笔记2. 达人探店-查看探店笔记4.7.2.点赞功能4.7.3.基于List实现点赞用户列表TOP104.7.4.基于SortedSet实现点赞排行榜4.8.关注列表4.8.1.关注列表实现原理4.8.2.添加关注1. 好友关注-关…

aws eks 集群访问ecr仓库拉取镜像的认证逻辑

本文主要讨论三个问题 ecr帮助程序在docker上如何配置eks集群访问ecr仓库的逻辑kubelet授权ecr的源码分析 ecr帮助程序 在docker环境下&#xff0c;可以通过在$HOME/.docker/config.json中指定凭证管理程序 docker login aws同样提供了证书助手&#xff0c;避免手动执行ecr认…

云环境渗透测试的重要性

&#x1f315;写在前面 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; ✉️今日分享&#xff1a; “在这个世上&#xff0c;除了极稀少的例外&#xff0c;我们其实只有两种选择&#xff1a;要么是孤独&#xff0c;要么就是庸俗。” 随着云计…

2023年前端面试知识点总结(JavaScript篇)

近期整理了一下高频的前端面试题&#xff0c;分享给大家一起来学习。如有问题&#xff0c;欢迎指正&#xff01; 1. JavaScript有哪些数据类型 总共有8种数据类型&#xff0c;分别是Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt Null 代表的含义是空对象…

【算法数据结构体系篇class06】:堆、大根堆、小根堆、优先队列

一、堆结构1&#xff09;堆结构就是用数组实现的完全二叉树结构2&#xff09;完全二叉树中如果每棵子树的最大值都在顶部就是大根堆3&#xff09;完全二叉树中如果每棵子树的最小值都在顶部就是小根堆4&#xff09;堆结构的heapInsert与heapify操作5&#xff09;堆结构的增大ad…

idea同时编辑多行-winmac都支持

1背景介绍 idea编辑器非常强大&#xff0c;其中一个功能非常优秀&#xff0c;很多程序员也非常喜欢用。这个功能能够大大大提高工作效率-------------多行代码同时编辑 2win 2.1方法1 按住alt鼠标左键上/下拖动即可 这样选中多行后&#xff0c;可以直接多行编辑。 优点&a…

【郭东白架构课 模块一:生存法则】11|法则五:架构师为什么要关注技术体系的外部适应性?

你好&#xff0c; 我是郭东白。 前四条法则分别讲了目标、资源、人性和技术周期&#xff0c;这些都与架构活动的外部环境有关。那么今天我们来讲讲在架构活动内部&#xff0c;也就是在架构师可控的范围内&#xff0c;应该遵守哪些法则。今天这节课&#xff0c;我们就先从技术体…

win10电脑性能优化设置

win10电脑性能优化设置 目录win10电脑性能优化设置1.桌面图标显示2.wini2.1 “系统”2.1.1专注助手 关2.1.2 电源和睡眠 设置为从不2.1.3 存储 开2.2 网络和Internet2.3 个性化2.4 应用2.5 账户2.6 游戏2.7 隐私墨迹书写和键入个性化&#xff1a;关活动历史记录&#xff1a;全部…