編譯期處理(語法糖)
所謂的 語法糖 ,其實就是指 java
編譯器把 *.java
源碼編譯爲 *.class
字節碼的過程中,自動生成 和轉換的一些代碼,主要是爲了減輕程序員的負擔,算是 java 編譯器給我們的一個額外福利(給糖吃嘛)
注意,以下代碼的分析,藉助了 javap
工具,idea
的反編譯功能,idea 插件 jclasslib
等工具。另外, 編譯器轉換的結果直接就是 class 字節碼,只是爲了便於閱讀,給出了 幾乎等價 的 java 源碼方式,並不是編譯器還會轉換出中間的 java 源碼,切記。
1 默認構造器
public class Candy1 { }
編譯成class後的代碼:
public class Candy1 {
// 這個無參構造是編譯器幫助我們加上的
public Candy1() {
super(); // 即調用父類 Object 的無參構造方法,即調用 java/lang/Object." <init>":()V
}
}
2 自動拆裝箱
這個特性是 JDK 5 開始加入的, 代碼片段1 :
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
這段代碼在 JDK 5 之前是無法編譯通過的,必須改寫爲 代碼片段 2 :
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
顯然之前版本的代碼太麻煩了,需要在基本類型和包裝類型之間來回轉換(尤其是集合類中操作的都是 包裝類型),因此這些轉換的事情在 JDK 5 以後都由編譯器在編譯階段完成。即 `代碼片段1` 都會在編 譯階段被轉換爲 `代碼片段2`
3 泛型集合取值
泛型也是在 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);
}
}
所以在取值時,編譯器真正生成的字節碼中,還要額外做一個類型轉換的操作:
// 需要將 Object 轉爲 Integer
Integer x = (Integer)list.get(0);
如果前面的
x 變量類型修改爲 int 基本類型那麼最終生成的字節碼是:
// 需要將 Object 轉爲 Integer, 並執行拆箱操作
int x = ((Integer)list.get(0)).intValue();
還好這些麻煩事都不用自己做。
擦除的是字節碼上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法參數泛型的信息
public cn.itcast.jvm.t3.candy.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 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/candy/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
-------new ArrayList對象----------
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;
//add方法的參數類型實際是Object類型
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
//get方法的參數類型實際是Object類型
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;
LocalVariableTypeTable:
Start Length Slot Signature
8 24 1 Ljava/util/List<Ljava/lang/Integer;>;
-
list.add 和 get 方法在bycecode層面上的參數類型都是Object類型,最後會執行checkcast指令進行類型轉換爲真實類型
-
方法體內部的泛型在虛擬機執行時會進行泛型擦除
-
使用反射,能夠獲得方法參數和返回值中的泛型信息:
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
4 可變參數
可變參數也是 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"});
}
}
注意 如果調用了 foo()
則等價代碼爲 foo(new String[]{})
,創建了一個空的數組,而不會 傳遞 null
進去
5 foreach 循環
仍是 JDK 5 開始引入的語法糖,數組的循環:
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);
}
}
}
注意 foreach
循環寫法,能夠配合數組,以及所有實現了 Iterable
接口的集合類一起使用,其 中 Iterable
用來獲取集合的迭代器( Iterator
)
6 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;
}
}
}
}
注意
switch
配合 String
和枚舉使用時,變量不能爲null
,原因分析完語法糖轉換後的代碼應當自 然清楚
會被編譯器轉換爲:
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
衝突,例如 BM
和 C
. 這兩個字符串的hashCode
值都是 2123 ,如果有如下代碼:
public class Candy6_2 {
public static void choose(String str) {
switch (str) {
case "BM": {
System.out.println("h");
break;
}
case "C.": {
System.out.println("w");
break;
}
}
}
}
會被編譯器轉換爲:
public class Candy6_2 {
public Candy6_2() { }
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 2123: // hashCode 值可能相同,需要進一步用 equals 比較
if (str.equals("C.")) {
x = 1;
} else if (str.equals("BM")) {
x = 0;
}
default:
switch (x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}
7 switch 枚舉
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 {
/**
* 定義一個合成類,$MAP這個名字自己取的,jvm中不是這樣(僅 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;
}
}
}
8 枚舉類
以前面的性別枚舉爲例:
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};
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier 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);
}
}
9 匿名內部類
源代碼:
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
轉換後代碼:
// 額外生成的類
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();
}
}
引用局部變量的匿名內部類,源代碼:
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
對象的 val$x
屬 性 ,所以x
不應該再發生變化了,如果 變化 , 那 麼 val$x 屬性沒有機會再跟着一起變化 .