所谓的语法糖,其实就是指java编译器把*.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们第一个额外福利
以下代码的分析,借助了javap工具,idea的反编译功能,idea插件jclasslib等工具。 另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的java源码方式,并不是编译器还会转换出中间的java源码。
public class Candy1{
}
编译成class后的代码
public class Candy1{
// 这个无参构造是编译器帮助我们加上的
public Candy1(){
super();// 即父类Object的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
这个特性是JDK5开始加入的,代码片段1:
public class Candy2{
public static void main(String[] args){
Integer x = 1;
int y = x;
}
}
这段代码在JDK5之前是无法编译通过的,必须改写为如下:
public class Candy2{
public static void main(String[] args){
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在JDK5以后都由编译器在编译阶段完成。
泛型也是在JDK5开始加入的特性,但java在编译泛型代码会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当作了Object类型来处理:
public class demo4 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
}
所以在取值时,编译器真正生成字节码中,还要额外做一个类型转换的操作:
// 需要将Object转为Integer类型
Integer x = (Integer)list.get(0);
如果前面的x变量类型修改为int基本类型那么最终生成的字节码是:
// 需要将Object转为Integer,并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
泛型擦除,在字节码中,所有的对象类型都被转为了Object了 所幸,这些麻烦事不用我们自己做
擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息
Classfile /E:/Java/学习案例/JVM/JVM/src/test/java/Class/demo4.class
Last modified 2024年10月29日; size 478 bytes
MD5 checksum 4d2758e590977925b85935c072dc7242
Compiled from "demo4.java"
public class Class.demo4
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // Class/demo4
super_class: #9 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/util/ArrayList
#3 = Methodref #2.#18 // java/util/ArrayList."<init>":()V
#4 = Methodref #7.#20 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#5 = Methodref #2.#21 // java/util/ArrayList.add:(Ljava/lang/Object;)Z
#6 = Methodref #2.#22 // java/util/ArrayList.get:(I)Ljava/lang/Object;
#7 = Class #23 // java/lang/Integer
#8 = Class #24 // Class/demo4
#9 = Class #25 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 demo4.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = Utf8 java/util/ArrayList
#20 = NameAndType #26:#27 // valueOf:(I)Ljava/lang/Integer;
#21 = NameAndType #28:#29 // add:(Ljava/lang/Object;)Z
#22 = NameAndType #30:#31 // get:(I)Ljava/lang/Object;
#23 = Utf8 java/lang/Integer
#24 = Utf8 Class/demo4
#25 = Utf8 java/lang/Object
#26 = Utf8 valueOf
#27 = Utf8 (I)Ljava/lang/Integer;
#28 = Utf8 add
#29 = Utf8 (Ljava/lang/Object;)Z
#30 = Utf8 get
#31 = Utf8 (I)Ljava/lang/Object;
{
public Class.demo4();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
17: pop
18: aload_1
19: iconst_0
20: invokevirtual #6 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
23: checkcast #7 // class java/lang/Integer
26: astore_2
27: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 18
line 10: 27
}
SourceFile: "demo4.java"
可变参数也是JDK5开始加入的新特性:
public class demo5 {
public static void foo(String... args){
String[] arr = args;// 直接赋值
System.out.println(arr);
}
public static void main(String[] args) {
foo("hello","world");
}
}
可变参数String… args 其实是一个String[] args, 从代码的赋值语句中就可以看出来
同样java编译器会在编译期间就将上述代码转为:
public class demo5 {
public static void foo(String... args){
String[] arr = args;// 直接赋值
System.out.println(arr);
}
public static void main(String[] args) {
foo(new String[]{"hello","world"});
}
}
如果调用了foo()则等价代码为foo(new String[]{}),创建了一个空的数组,而不会传递null进去
仍是jdk5开始引入的语法糖,数组的循环:
public class demo6 {
public static void main(String[] args) {
int[] array = {1,2,4,5}; // 数组赋值的简化写法,也算是语法糖
for (int i : array) {
System.out.println(i);
}
}
}
编译器编译后,转换为:
public class demo6 {
public static void main(String[] args) {
int[] array = new int[]{1,2,4,5};
for (int i=0;i<array.length;++i) {
int x = array[i]; // x 就是自己定义接受的变量 i
System.out.println(x);
}
}
}
集合的循环:
public void test1_List(int[] args){
List<Integer> list = Arrays.asList(1,2,3,4,5,6);
for (Integer i : list) {
System.out.println(i);
}
}
实际被编译器转换为迭代器的调用:
public class demo6 {
public static void main(String[] args) {
}
public void test1_List(int[] args){
List<Integer> list = Arrays.asList(1,2,3,4,5,6);
Iterator list = list.iterator(); // 获取迭代器
while(iter.hasNext()){// 对迭代器中包含的对象进行循环
Integer e = (Integer)iter.next();// Object类型转为Integer类型
System.out.println(e);
}
}
}
foreach循环写法,能够配合数组,以及所有实现了iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器(Iterator)
从JDK7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
package Class;
public class demo7 {
public static void main(String[] args) {
}
public static void cc(String str){
switch (str){
case "hello":{
System.out.println("h");
break;
}
case "world":{
System.out.println("w");
break;
}
}
}
}
switch配合String和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清除
会被编译器转换为:
public static void cc(String str){
byte x = -1;
switch (str.hashCode()){
case 99162322: // hello 的哈希值
if (str.equals("hello")){
x = 0;
}
break;
case 113318802: // world的哈希值
if (str.equals("world")){
x=1;
}
break;
}
switch (x){
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
可以看到执行了两边switch,第一遍根据字符串的hashCode和equals将字符串的转换为相应byte类型,第二遍才是利用byte执行进行比较。
为什么第一遍时必须即比较hashCode,又利用equals比较呢?hashCode是为了提高效率,减少可能的比较;而equals是为了防止hashCode冲突,例如BM
和C.
这两个字符串的hashCode值都是2123。
switch枚举的例子,原始代码:
enum Sex{
MALE,FEMALE
}
public class demo8 {
public static void foo(Sex sex){
switch (sex){
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
break;
}
}
}
/**
* 定义一个合成类(仅JVM使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal() = 1
*/
static class $MAP{
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex){
int x = $MAP.map[sex.ordinal()];
switch (x){
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
JDK 7 新增了枚举类,以前面的性别枚举为例:
enum Sex{
MALE,FEMALE
}
转换后的代码:
public final class Sex extends Enum<Sex>{
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE",0);
FEMALE = new Sex("FEMALE",1);
$VALUES = new Sex[]{MALE, FEMALE};
}
private Sex(String name,int ordinal){
super(name,ordinal);
}
public static Sex[] values(){
return $VALUES.clone();
}
public static Sex ValueOf(String name){
return Enum.valueOf(Sex.class,name);
}
}
本质上枚举还是创建了一个对象,只不过继承了一个枚举类型的对象 枚举中的值是对象中的成员变量
JDK7开始新增了对需要关闭的资源处理的特殊语法try-with-resources:
try(资源变量=创建资源对象){
}catch(){
}
其中资源对象需要实现 AutoCloseable 接口,例如:InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了AutoCloseable,使用try-with-resources可以不用谢finally语句块,编译器会帮助生成关闭资源代码
public class demo9{
public static void main(String[] args){
try(InputStream is = new FileInputStream("./1.text")){
System.out.println(is);
}catch(IOException e){
e.printStackTrace();
}
}
}
转换为:
public class demo10 {
public static void main(String[] args) {
try{
FileInputStream is = new FileInputStream("./1.text");
Throwable t = null;
try{
System.out.println(is);
}catch (Throwable e1){
t = e1;
throw e1;// t 就是代码出现的异常,
}finally {
if (is!=null){
if (t!=null){
try {
is.close();
}catch (Throwable e2){
// 如果close出现异常,那么会作为被压制异常添加
t.addSuppressed(e2);
}
}else {
// 当代码并未出现异常,close出现的异常就是最后catch块中的e
is.close();
}
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
为什么要设计一个addSuppresed(Throwable e)(添加被压制异常)的方法呢?
都知道,方法重写时对放回值分为两种情况:
class A{
public Number m(){
return 1;
}
}
class B extends A{
// 子类m 方法返回值是 Integer 是父类 m 方法返回值 Number 的子类
@Override
public Integer m(){
return 2;
}
}
对于子类,编译器会做如下处理
class B extends A{
public Integer m(){
return 2;
}
// 此方法才是真正重写了父类public Number m() 方法
public synthetic bridge Number m(){
// 调用 public Integer m()
return m();
}
}
其中桥接方法比较特殊,仅对java虚拟机可见,并且与原来的public Integer m() 没有命名冲突,可以用下面的反射代码来验证:
for(Method m: B.class.getDeclaredMethods()){
System.out.println(m);
}
输出:
public java.lang.Integer 方法地址;
public java.lang.Number 方法地址;
public class demo12{
public static void test(final int x){
Runnable runnable = new Runnable(){
@Override
public void run(){
System.out.println("ok: "+x);
}
};
}
}
转换后
final class demo12$1 implements Runnable{
int val$x;
demo12$1(int x){
this.val$x=x;
}
public void run(){
System.out.println("ok: " + this.val$x);
}
}
在编译后,会创建一个新的类,并实现Runnable接口,以此来实现run方法
在创建 demo121 对象时,将x的值赋值给 demo1 对象的valx属性,x不应该再发生改变,如果改变了那么valx属性没有机会再跟着一起变化,这解释了为什么匿名内部类引用局部变量时,局部变量必须时final的原因
(TieredCompliation)
例:
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.println("触发数:"+i+",耗时:"+(end-start));
}
}
JVM将执行状态分成 5 个层次:
profiling 是指在运行过程中收集一些程序执行状态的数据,例如:
这种优化手段称之为 逃逸分析 ,发现新建的对象是否可以逃逸,可以使用如下选项关闭逃逸分析
-XX: -DoEscapeAnalysis
即时编译器(JIT)与解释器的区别
对于占据大部分的不常用代码,我们无需耗费事件将其编译成机器码,而是采用解释执行的方式运行。
另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率上简单比较一下 Interpreter < C1 < C2 ,总的目标是发现热点代码(hotspot名称的由来,优化之)
/**
* 方法内联
*/
@Test
public void test1(){
int x = 0;
for (int i = 0; i < 500; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
x = square(9);
}
long end = System.nanoTime();
System.out.println("触发数:"+i+",耗时:"+(end-start)+",\t 相乘数值:"+x);
}
}
private static int square(final int a){
return a * a;
}
如果发现square是热点方法,并且长度不太长时,会进行内联。
内联操作:把方法内代码拷贝、粘贴到调用者的位置 这种方法可以大大降低对方法的解析速度
public class demo2 {
public static void foo(){
System.out.println("foo...");
}
public static void main(String[] args) throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException {
Method foo = demo2.class.getMethod("foo");
for (int i =0; i<=16;i++){
System.out.printf("%d\t",i);
// 调用反射
foo.invoke(null);
}
System.in.read();
}
}
在进行反射时,底层代码会进行判断,当调用超过15次时,JVM会直接使用类进行调用
private static int inflationThreshold = 15;
static int inflationThreshold() {
return inflationThreshold;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// 从工厂中拿取反射次数阈值(默认15)
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !method.getDeclaringClass().isHidden()
&& generated == 0
&& U.compareAndSetInt(this, GENERATED_OFFSET, 0, 1)) {
try {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator(). // 方法访问器生成器 在这里就会直接生成调用方法
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
} catch (Throwable t) {
// Throwable happens in generateMethod, restore generated to 0
generated = 0;
throw t;
}
}
return invoke0(method, obj, args);
}
故而,在超出反射阈值后,JVM会给你使用使用类调用方法
通过查看 ReflectionFactory源码可知: