JVM第十三篇(类加载和字节码技术四)

语法糖

本篇主要讲解 JVM 在编译期间的处理。
语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利。

默认构造函数

当创建一个java类,没有自定义一个构造方法时,编译器会自动生成一个无参构造函数。

public class Candy1 {
}
// 这个无参构造是编译器帮助我们加上的
public class Candy1 {
	public Candy1() {
		super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
	}
}

自动装拆箱

在java中 ,基本类型转换为包装类型称为装箱,把包装类型转换为基本类型称为拆箱。
在JDK5以后,编译器会自动帮我们完成装箱和拆箱工作。

public class Candy2 {
	public static void main(String[] args) {
		Integer x = 1;
		int y = x;
	}
}

上面的代码在编译器中会优化为下面的代码:

public class Candy2 {
	public static void main(String[] args) {
		Integer x = Integer.valueOf(1);
		int y = x.intValue();
	}
}

泛型擦除

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息
在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理。

public class Candy3 {
	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>();
		list.add(10); 				// 实际调用的是 List.add(Object e)
		Integer x = list.get(0); 	// 实际调用的是 Object obj = List.get(int index);
	}
}

查看对应的字节码指令

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: invokeinterface #5, 2 // InterfaceMethod java/util/List.add (Ljava/lang/Object;)Z
	19: pop
	20: aload_1
	21: iconst_0
	22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get (I)Ljava/lang/Object;
	27: checkcast #7 // class java/lang/Integer
	30: astore_2
	31: return
LocalVariableTypeTable:
	Start Length Slot Name Signature
		8     24    1 list  Ljava/util/List<Ljava/lang/Integer;>;

11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
将 10 自动装箱为 Integer 类型
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add (Ljava/lang/Object;)Z
在第14行 真正调用方法时的类型参数是 Object 类型。
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get (I)Ljava/lang/Object;
在22行 调用get方法 ,返回对象的类型为 Object 类型
27: checkcast #7 // class java/lang/Integer
在27行 又将 Object类型 进行类型装换为 Integer类型

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息
如果方法的参数和返回值中存在泛型,则可以使用反射拿到泛型信息。

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
	if (type instanceof ParameterizedType) {
		ParameterizedType parameterizedType = (ParameterizedType) type;
		System.out.println("原始类型 - " + parameterizedType.getRawType());
		Type[] arguments = parameterizedType.getActualTypeArguments();
		for (int i = 0; i < arguments.length; i++) {
			System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
		}
	}
}

输出

原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

可变参数

可变参数也是 JDK 5 开始加入的新特性:

public class Candy4 {
	public static void foo(String... args) {
		String[] array = args; // 直接赋值
		System.out.println(array);
	} 
public static void main(String[] args) {
		foo("hello", "world");
	}
}

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。
同样 java 编译器会在编译期间将上述代码变换为:

public class Candy4 {
	public static void foo(String[] args) {
		String[] array = args; // 直接赋值
		System.out.println(array);
	} 
	public static void main(String[] args) {
		foo(new String[]{"hello", "world"});
	}
}

foreach 循环

public class Candy5_1 {
	public static void main(String[] args) {
		int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
		for (int e : array) {
			System.out.println(e);
		}
	}
}

会被编译器转换为:

public class Candy5_1 {
	public Candy5_1() {
	}
	public static void main(String[] args) {
		int[] array = new int[]{1, 2, 3, 4, 5};
		for(int i = 0; i < array.length; ++i) {
			int e = array[i];
			System.out.println(e);
		}
	}
}

而集合的循环:

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

实际被编译器转换为对迭代器的调用:

public class Candy5_2 {
	public Candy5_2() {
	} 
	public static void main(String[] args) {
		List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
		Iterator iter = list.iterator();
		while(iter.hasNext()) {
			Integer e = (Integer)iter.next();
			System.out.println(e);
		}
	}
}

switch

字符串

从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

public class Candy6_1 {
	public static void choose(String str) {
		switch (str) {
			case "hello": {
				System.out.println("h");
				break;
			} 
			case "world": {
				System.out.println("w");
				break;
			}
		}
	}
}

会被编译器转换为:

public class Candy6_1 {
	public Candy6_1() {
	}
	public static void choose(String str) {
		byte x = -1;
		switch(str.hashCode()) {
			case 99162322: // hello 的 hashCode
				if (str.equals("hello")) {
					x = 0;
				} 
				break;
			case 113318802: // world 的 hashCode
				if (str.equals("world")) {
					x = 1;
				}
			} 
		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 冲突。

枚举

switch 枚举的例子,原始代码:

enum Sex {
	MALE, FEMALE
}
public class Candy7 {
	public static void foo(Sex sex) {
		switch (sex) {
			case MALE:
				System.out.println("男"); break;
			case FEMALE:
				System.out.println("女"); break;
		}
	}
}

转换后代码:

public class Candy7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
	static class $MAP {
	// 数组大小即为枚举元素个数,里面存储case用来对比的数字
		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;
		}
	}
}

枚举

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);
	}
}

将枚举类装换为了一个 final 类,将构造方法私有,枚举为单例模式。

try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

try(资源变量 = 创建资源对象){

} catch( ) {

}

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

public class Candy9 {
	public static void main(String[] args) {
		try(InputStream is = new FileInputStream("d:\\1.txt")) {
			System.out.println(is);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

会被转换为:

public class Candy9 {
	public Candy9() {
	} 
	public static void main(String[] args) {
		try {
			InputStream is = new FileInputStream("d:\\1.txt");
			Throwable t = null;
			try {
				System.out.println(is);
			} catch (Throwable e1) {
				// t 是我们代码出现的异常
				t = e1;
				throw e1;
			} 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();
		}
	}
}

方法重写时的桥接方法

方法重写时对返回值分两种情况:

  1. 父子类的返回值完全一致
  2. 子类返回值可以是父类返回值的子类
class A {
	public Number m() {
		return 1;
	}
} 
class B extends A {
	@Override
	// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
	public Integer m() {
		return 2;
	}
}

对于子类,java 编译器会做如下处理:

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 test.candy.B.m()
public java.lang.Number test.candy.B.m()

匿名内部类

引用局部变量的匿名内部类,源代码:

public class Candy11 {
	public static void test(final int x) {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println("ok:" + x);
			}
		};
	}
}

转换后代码:

// 额外生成的类
final class Candy11$1 implements Runnable {
	int val$x;
	Candy11$1(int x) {
		this.val$x = x;
	} 
	public void run() {
		System.out.println("ok:" + this.val$x);
	}
}
public class Candy11 {
	public static void test(final int x) {
		Runnable runnable = new Candy11$1(x);
	}
}

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建Candy11$1 对象时,将 x 的值赋值给了 Candy11 1 对象的 v a l 1 对象的 val 1对象的valx 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值