@
编译器处理就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利,故·称之为语法糖(给糖吃嘛)。
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
1.默认构造器
public class Candy1 {
}
经过编译的代码,可以看到在编译阶段,如果我们没有添加构造器。那么Java编译器会为我们添加一个无参构造方法。
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
2.自动拆装箱
在JDK5以后,Java提供了自动拆装箱的功能。
如以下代码:
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
在Java5以前会编译失败,必须该写为以下代码:
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
以上的转换,在JDK5以后都会由Java编译器自动完成。
3.泛型与类型擦除
泛型延时在JDK5以后加入的特性,但Java中的泛型并不是真正的泛型。因为Java中的泛型只存在于Java的源码中,在经过编译的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类型,可以认为是被Object替换)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说, ArrayList < int>与ArrayList< String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
如以下代码:
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);
}
}
在从list集合中取值时,在编译器真正的字节码文件中还需要一个类型转换的动作
// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0);
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
不过因为语法糖的存在,所以以上的动作都不需要我们自己来做。
不过,虽然编译器在编译过程中,将泛型信息都擦除了,但是并不意味着,泛型信息就丢失了。泛型的信息还是会存储在LocalVariableTypeTable 中:
{
public wf.Candy3();
descriptor: ()V
flags: 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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lwf/Candy3;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: 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: 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
LineNumberTable:
line 8: 0
line 9: 8
line 10: 20
line 11: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
31 1 2 x Ljava/lang/Integer;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
}
SourceFile: "Candy3.java"
我们可以通过反射的方式获得被擦除的泛型信息。不过只能获取方法参数或返回值上的信息。
public class Candy3 {
List<String> str = new ArrayList<>();
public static void main(String[] args) throws Exception {
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
fs();
}
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
return null;
}
private static void fs() throws Exception {
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
", i, arguments[i]);
}
}
}
Field list = Candy3.class.getDeclaredField("str");
Class<?> type = list.getType();
System.out.println(type.getName());
}
}
输出:
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
java.util.List
4.可变参数
可变参数也是JDK5新加入的特性。其具体形式如下:
public class Test3 {
public static void main(String[] args) {
foo("hello","world");
}
private static void foo(String... args){
String[] str = args;
for (int i = 0; i < str.length; i++) {
System.out.println(str[i]);
}
}
}
其结果由一个字符串数组直接接受,程序能够正常执行。
注意:如果调用方法时没有参数如foo(),那么传入方法的不是null,而是一个空数组foo(new String[]{})。
5.foreach
依旧是JDK5引入的语法糖。简化了for循环的写法。
示例:
public class Test4_1 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
for (int i : arr) {
System.out.print(" " + i);
}
}
}
在对其字节码反编译后:
public class Test4_1 {
public Test4_1() {
}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
int[] var2 = arr;
int var3 = arr.length;
for(int var4 = 0; var4 < var3; ++var4) {
int i = var2[var4];
System.out.print(" " + i);
}
}
}
此处包含两个语法糖
- {1,2,3,4,5}转为数组才进行复制
- foreach循环被转换为了简单的for循环。
foreach循环还可以对集合进行遍历:
public class Test4_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer integer : list) {
System.out.print(integer + " ");
}
}
}
其编译后字节码的反编译出的代码为:
public class Test4_2 {
public Test4_2() {
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator var2 = list.iterator();
while(var2.hasNext()) {
Integer integer = (Integer)var2.next();
System.out.print(integer + " ");
}
}
}
编译器先获取集合的迭代器对象,在通过while循环对迭代器对象进行遍历。其中还包含泛型擦除的语法糖。
foreach循环写法,配合数组,及实现了Iterable接口的集合类使用,Iterable来获取迭代器对象(Iterator)
6.switch支持case使用字符串及枚举类型
JDK7开始,Java的switch支持字符串和枚举类型,而其中也包含了语法糖。
switch字符串
示例:
public class Test5_1 {
public static void main(String[] args) {
switch ("hello"){
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
}
}
}
注意:在使用String时,不能传入一个null,会发生空指针异常。因为以上代码会被编译器转换为:
public class Test5_1 {
public Test5_1() {
}
public static void main(String[] args) {
String var1 = "hello";
byte var2 = -1;
switch(var1.hashCode()) {
case 99162322:
if (var1.equals("hello")) {
var2 = 0;
}
break;
case 113318802:
if (var1.equals("world")) {
var2 = 1;
}
}
switch(var2) {
case 0:
System.out.println("hello");
break;
case 1:
System.out.println("world");
}
}
}
可以看到,switch支持字符实际上是把对象,获取其哈希值进行一次比较在确定了,之后再用一个switch来实现代码逻辑。
为什么第一次既要进行一次哈希比较,又要进行一次equals()?使用hashcode是为了提高比较的效率,而equals是为了防止哈希冲突。如BM和C.两个字符串的哈希值相同都为2123,如果有以下代码:
public class Test5_2 {
public static void main(String[] args) {
switch ("BM"){
case "BM":
System.out.println("hello");
break;
case "C.":
System.out.println("world");
}
}
}
经过反编译后:
public class Test5_2 {
public Test5_2() {
}
public static void main(String[] args) {
String var1 = "BM";
byte var2 = -1;
switch(var1.hashCode()) {
case 2123://哈希值相同需要进一步比较
if (var1.equals("C.")) {
var2 = 1;
} else if (var1.equals("BM")) {
var2 = 0;
}
default:
switch(var2) {
case 0:
System.out.println("hello");
break;
case 1:
System.out.println("world");
}
}
}
}
switch枚举
代码如下:
public class Test5_3 {
public static void main(String[] args) {
Sex sex = Sex.MALE;
switch (sex){
case MALE:
System.out.println("男");
break;
case FEMALE:
System.out.println("女");
}
}
}
enum Sex{
MALE,FEMALE;
}
转换后:
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
* 该转换需要使用其他工具进行转换,idea转换不出来
*/
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;
}
}
}
7.枚举
JDK7以后Java引入了枚举类,它也是一个语法糖。
以上一个性别类型为例:
enum Sex {
MALE, FEMALE
}
转换后(idea依旧不能转换):
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};
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is
assigned
*/
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);
}
}
8.try-with-resourcs
JDK7加入对需要关闭资源处理的特殊语法。
try(资源大小 = 创建对象资源){
}catch(){
}
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、
Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:
public class Test6 {
public static void main(String[] args) {
try(InputStream stream = new FileInputStream("F://test.txt")) {
System.out.println(stream);
}catch (Exception e){
e.printStackTrace();
}
}
}
会被转换为;
public class Test6 {
public Test6() {
}
public static void main(String[] args) {
try {
InputStream stream = new FileInputStream("F://test.txt");
Throwable var2 = null;
try {
System.out.println(stream);
} catch (Throwable var12) {
//var2是可能出现的异常
var2 = var12;
throw var12;
} finally {
//判断资源是否为空
if (stream != null) {
//如果代码出现异常
if (var2 != null) {
try {
stream.close();
} catch (Throwable var11) {
//关闭资源时出现异常,作为被压制异常添加
var2.addSuppressed(var11);
}
} else {
//如果代码没有异常,close出现的异常就是catch中var12
stream.close();
}
}
}
} catch (Exception var14) {
var14.printStackTrace();
}
}
}
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):
public class Test6_1 {
public static void main(String[] args) {
try(Myresource myresource = new Myresource()) {
int a = 1/0;
}catch (Exception e){
e.printStackTrace();
}
}
}
class Myresource implements AutoCloseable{
@Override
public void close() throws Exception {
throw new IOException("close异常");
}
}
其输出为;
java.lang.ArithmeticException: / by zero
at wf.test.Test6_1.main(Test6_1.java:9)
Suppressed: java.io.IOException: close异常
at wf.test.Myresource.close(Test6_1.java:20)
at wf.test.Test6_1.main(Test6_1.java:10)
TWR将两个异常信息都保留了下来。
9.方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
class A{
public Number m(){
return 1;
}
}
class B extends A{
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() 没有命名冲突,可以用下面反射代码来验证:
public class Test7 {
public static void main(String[] args) {
for (Method m :B.class.getDeclaredMethods()) {
System.out.println(m);
}
A a = new B();
System.out.println(a.m());
}
}
输出结果:
public java.lang.Integer wf.test.B.m()
public java.lang.Number wf.test.B.m()
2
也可以验证该方法重写起作用了。
10.匿名内部类
代码:
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello ");
}
};
}
}
转码后:
// 额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}
当匿名内部类引用外部类变量时
private static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello " + x);
}
};
runnable.run();
}
转换后:
// 额外生成的类
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 static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
注意:这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的:因为在创建Candy11$1对象时,将x的值赋值给了Candy11$1 对象的val$x属性,所以x不应该再发生变化了, 如果变化,那么valx属性没有机会再跟着一起变化