【Java进阶笔记】编译期处理(java语法糖)

  • Post author:
  • Post category:java





1. 语法糖

语法糖,指java编译器把

.java

源码文件编译为

.class

字节码文件的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利(给糖吃)。



1.1. 默认构造方法

如果没有写任何构造方法,编译器会自动生成一个默认的无参构造方法

public class Test {
}

编译成 class 后的代码:

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



1.2. 自动拆装箱

包装类型和基本类型,在 JDK5 之后能自动转换。

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

编译成 class 后的代码:

public class Test {
	public static void main(String[] args) {
		Integer x = Integer.valueOf(1);	// 自动装箱
        int y = x.intValue();			// 自动拆箱
    }
}



1.3. 泛型取值

JDK5 在编译泛型代码时,会执行

类型擦除

操作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了

Object

类型来处理。

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

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

// 需要将Object转为Integer
Integer x = (Integer)list.get(0);

如果前面的

x

变量类型修改为

int

基本类型那么最终生成的字节码是:

//需要将Object转为Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

虽然会有类型擦除,但是会在 class 文件中的

LocalVariableTypeTable

中,保存泛型信息,用来做自动类型转换。因此可以使用反射获取泛型信息。

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
Method test = Test.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
	if (type instanceof ParameterizedType) {
		ParameterizedType parameterizedType = (ParameterizedType) type;
		Ѕуѕtеm.оut.рrіntln("原始类型 - " + раrаmеtеrіzеdТуре.gеtRаwТуре());
		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



1.4. 可变参数

可变参数

String... args

会在编译期转换为

String[] args

public class Test {
	public static void main(String[] args) {
        // 实际上是 test(new String[]{"Hello", "World!"});
        test("Hello", "World!");
        // 实际上是 test(new String[]{});
        test();
    }
    
    public static void test(String... args) {
		String[] array = args;
        System.out.рrіntln(array);
    }
}



1.5. foreach 循环

public class Test {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        for (int e : array) {
            System.out.println(e);
        }

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer e : list) {
            System.out.println(e);
        }
    }
}

编译后:

public class Test {
    public Test() {
    }

    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);
        }
		// 对于集合,则使用迭代器迭代
        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);
        }
    }
}



1.6. switch 字符串

从 JDK7 开始,

switch

语句可以作用于字符串和枚举类。

public class Test {
    public void test(String s) {
        switch (s) {
            case "你好": {
                System.out.println("你好");
                break;
            }
            case "世界": {
                System.out.println("世界");
                break;
            }
        }
    }
}

编译后:

public class Test {
    public Test() {
    }
    
    public void test(String s) {
        byte var2 = -1;
        // 先计算字符串的hash值,再匹配得到一个变量值
        switch(s.hashCode()) {
            case 649718:
                if (s.equals("世界")) var2 = 1;
                break;
            case 652829:
                if (s.equals("你好")) var2 = 0;
        }
		// 再匹配变量值
        switch(var2) {
            case 0:
                System.out.println("你好");
                break;
            case 1:
                System.out.println("世界");
        }
    }
}

执行了两遍

switch

语句,第一遍是根据字符串的

hashCode



equals

将字符串的转换为相应

byte

类型,第二遍才是利用

byte

执行进行比较。


hashCode

是为了提高效率,减少可能的比较;而

equals

为了防止

hashCode

冲突。

例如

BM



C.

这两个字符串的 hashCode 值都是 2123。

public class Test {
    public void test(String s) {
		switch (s) {
            case "BM": {
                System.out.println("BM");
                break;
            }
            case "C.": {
                System.out.println("C.");
                break;
            }
        }
    }
}

编译后:

public class Test {
    public Test() {
    }

    public void test(String s) {
        byte var2 = -1;
        switch(s.hashCode()) {
        case 2123:	// hashcode 值可能相同,需要进一步比较
            if (s.equals("C.")) var2 = 1;
            else if (s.equals("BM")) var2 = 0;
        default:
            switch(var2) {
                case 0:
                    System.out.println("BM");
                    break;
                case 1:
                    System.out.println("C.");
            }
        }
    }
}



1.7. switch 枚举

public enum Sex {
    MALE, FEMALE
}

public class Test {
    public void test(Sex sex) {
        switch (sex) {
            case MALE: {
                System.out.println("男");
                break;
            }
            case FEMALE: {
                System.out.println("女");
                break;
            }
        }
    }
}

编译后:

public class Test {
    public Test() {
    }
    
    // 会生成一个合成的内部类(JVM可见,我们不可见)
    static class $MAP {
        // 数组大小即为枚举元素个数,里面存储case用来对比的数字
		static int[] map = new int[2];
		static {
			map[Sex.MALE.ordinal()] = 1;
			map[Sex.FEMALE.ordinal()] = 2;
    	}
    }
    
    public void test(Sex sex) {
        // 从数组中取出对应的值
        int x = $MAP.map[sex.ordinal()];
        switch(x) {
            case 1:
                System.out.println("男");
                break;
            case 2:
                System.out.println("女");
        }
    }
}



1.8. 枚举

public enum Sex {
    MALE, FEMALE
}

编译后:

// 本质上是一个class
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);
    }
}



1.9. try-with-resources

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

try-with-resources

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

其中资源对象需要实现

AutoCloseable

接口,例如

Inputstream



OutputStream



Connection



Statement



ResultSet

等接口都实现了 AutoCloseable,使用

try-with-resources

可以不用写

finally

语句块,编译器会帮助生成关闭资源代码,例如:

public class Test {
    public void test() {
        try (InputStream is = new FileInputStream("D:\\test.txt")) {
            System.out.println(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

编译后:

public class Test {
    public void test() {
        try {
            FileInputStream is = new FileInputStream("D:\\test.txt");
            try {
                System.out.println(is);
            } catch (Throwable e1) {
                // 我们自己的代码出现异常
                try {
                    // 尝试关闭
                    is.close();
                } catch (Throwable e2) {
                    // close出现异常,作为被压制的异常添加
                    e1.addSuppressed(e2);
                }
                // 抛出异常
                throw e1;
            }
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}



1.10. 方法重写时的桥接方法

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

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

编译后:

class B extends A {
	public Integer m() {
		return 2;
	}
	// 此方法オ是真正重写了父类m方i,JVM可见,我们不可见
	public synthetic bridge Number m() {
		// 调用 public Integer m()
		return m();
	}
}



1.11. 匿名内部类

public class Test {

    // 匿名内部类实现接口并重写接口方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
        }
    };

    // 匿名内部类重写类方法
    Object object = new Object() {
    };

    // 静态常量 + 匿名内部类实现接口并重写接口方法
    static final Runnable runnable2 = new Runnable() {
        @Override
        public void run() {
        }
    };
}

编译后会生成三个内部类 class:


Test$1.class



new Runnable() {}

的内容:

import com.company.Test;
class Test$1 implements Runnable {
    // 持有外部类的引用
    final Test this$0;
    // 外部类的引用通过构造方法传入
    Test$1(Test this$0) {
        this.this$0 = this$0;
    }

    public void run() {}
}


Test$2.class



new Object() {}

的内容:

import com.company.Test;
class Test$2 {
    // 持有外部类的引用
    final Test this$0;
    // 外部类的引用通过构造方法传入
    Test$1(Test this$0) {
        this.this$0 = this$0;
    }
}


Test$3.class



static final Runnable runnable2 = new Runnable() {}

的内容:

class Test$3 implements Runnable {
    public void run() {}
}

前两种写法,内部类会持有外部类的引用。如果内部类的生命周期比外部类长,则外部类容易导致内存泄漏。



版权声明:本文为ROX0008原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。