@
編譯器處理就是指 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\n", 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屬性沒有機會再跟着一起變化