這是 JsonChao 的第177期分享
前段時間在微信群,一哥們出去面試,回顧面試題的時候,說問到了枚舉。
作為一名Android選手,談到枚舉,那肯定是:
Android上不應該使用枚舉,占內存,應該使用@XXXDef註解來替代,balabala…
這麼一回答,心裡美滋滋。
沒想到面試官問了句:
枚舉的原理是什麼?你說它占內存到底占多少內存呢,如何佐證?
聽到這就慌了,沒了解過呀。
下面說第一個問題(沒錯還有第二個問題)。
有篇文章:
http://blog.csdn.net/mhmyqn/article/details/48087247
寫得挺好的。
下面還是要簡述一下,我們先寫個枚舉類:
public enum Animal { DOG,CAT}看着這代碼,完全看不出來原理。不過大家應該都知道java類編譯後會產生class文件。
越接近底層,本質就越容易暴露出來了。
我們先javac搞到Animal.class,然後通過javap命令看哈:
輸出:
public final class Animal extends java.lang.Enum<Animal> { public static final Animal DOG; public static final Animal CAT; public static Animal[] values(); public static Animal valueOf(java.lang.String); static {};}其實到這裡我們已經大致知道枚舉的本質了,實際上我們編寫的枚舉類Animal是繼承自Enum的,每個枚舉對象都是static final的類對象。
還想知道更多的細節怎麼辦,比如我們的對象什麼時候初始化的。
我們可以添加-c參數,對代碼進行反編譯。
你可以使用javap -help 查看所有參數的含義。
輸出:
public final class Animal extends java.lang.Enum<Animal> { public static final Animal DOG; public static final Animal CAT; public static Animal[] values(); Code: 0: getstatic #1 // Field $VALUES:[LAnimal; 3: invokevirtual #2 // Method "[LAnimal;".clone:()Ljava/lang/Object; 6: checkcast #3 // class "[LAnimal;" 9: areturn public static Animal valueOf(java.lang.String); Code: 0: ldc #4 // class Animal 2: aload_0 3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 6: checkcast #4 // class Animal 9: areturn static {}; Code: 0: new #4 // class Animal 3: dup 4: ldc #7 // String DOG 6: iconst_0 7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 10: putstatic #9 // Field DOG:LAnimal; 13: new #4 // class Animal 16: dup 17: ldc #10 // String CAT 19: iconst_1 20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 23: putstatic #11 // Field CAT:LAnimal; 26: iconst_2 27: anewarray #4 // class Animal 30: dup 31: iconst_0 32: getstatic #9 // Field DOG:LAnimal; 35: aastore 36: dup 37: iconst_1 38: getstatic #11 // Field CAT:LAnimal; 41: aastore 42: putstatic #1 // Field $VALUES:[LAnimal; 45: return}好了,現在可以分析代碼了。
但是,這代碼看起來也太頭疼了,我們先看一點點:
static中部分代碼:
0: new #4 // class Animal3: dup4: ldc #7 // String DOG6: iconst_07: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V10: putstatic #9 // Field DOG:LAnimal;大致含義就是new Animal(String,int),然後給我們的靜態常量DOG賦值。
好了,不看了,好煩。我們轉念想一下,如果這個字節碼咱們能看懂,那就是有規則的,只要有規則,肯定有類似翻譯類的工具,直接轉成java代碼的。
確實有,比如jad:
http://www.javadecompilers.com/jad
我們先下載一份,很小:

命令也很簡單,執行:
./jad -sjava Animal.class就會在當前目錄生成java文件了。
輸出如下:
public final class Animal extends Enum{ public static Animal[] values() { return (Animal[])$VALUES.clone(); } public static Animal valueOf(String s) { return (Animal)Enum.valueOf(Animal, s); } private Animal(String s, int i) { super(s, i); } public static final Animal DOG; public static final Animal CAT; private static final Animal $VALUES[]; static { DOG = new Animal("DOG", 0); CAT = new Animal("CAT", 1); $VALUES = (new Animal[] { DOG, CAT }); }}到這,我相信你知道我們編寫的枚舉類:
public enum Animal { DOG,CAT}最終生成是這樣的類,那麼對應的我們所使用的方法也就都明白了。此外,你如果拿這樣的類,跟兩個靜態INT常量比內存,那肯定是多得多的。
其次,我們也能順便回答,枚舉對象為什麼是單例了。
並且其Enum類中對readObject和clone方法都進行了實現,看一眼你就明白了。
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new InvalidObjectException("can't deserialize enum");}protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException();}本文並不是為了去討論枚舉的原理,而是想要給大家說明的是很多「語法糖」類似的東西,都能按照這樣的思路去了解它的原理。
下面我們再看一個,聽起來稍微高端一點的:
動態代理
假設遇到這樣的一個面試情況:
問:retrofit的原理是?答:基於動態代理,然後balabal...問:那麼動態代理的原理是?答:...我們依然從一個最簡單的例子開始。
我們寫一個接口:
public interface IUserService{ void login(String username, String password);}然後,利用動態代理去生成一個代理對象,去調用login方法:
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.Arrays;public class Test{ public static void main(String[] args){ IUserService userService = (IUserService) Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("method = " + method.getName() +" , args = " + Arrays.toString(args)); return null; } }); System.out.println(userService.getClass()); userService.login("zhy","123"); }}好了,這應該是最簡單的動態代理的例子了。
當我們去調研userService.login方法,你會發現InvocationHandler的invoke方法調用了,並且輸出了相關信息。
怎麼會這麼神奇呢?
我們寫了一個接口,就能產生一個該接口的對象,然後我們還能攔截它的方法。
繼續看:
先javac Test.java,得到class文件。
然後調用:
輸出:
class com.sun.proxy.$Proxy0method = login , args = [zhy, 123]可以看到當我們調用login方法的時候,invoke中攔截到了我們的方法,參數等信息。
retrofit的原理其實就是這樣,攔截到方法、參數,再根據我們在方法上的註解,去拼接為一個正常的Okhttp請求,然後執行。
想知道原理,根據我們枚舉中的經驗,肯定想看看這個
com.sun.proxy.$Proxy0 // userService對象輸出的全路徑這個類的class文件如何獲取呢?
很簡單,你在main方法的第一行,添加:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 然後重新編譯、執行,就會在當前目錄看到了。
MacBook-Pro:tmp zhanghongyang01$ tree .├── IUserService.class├── IUserService.java├── Test$1.class├── Test.class├── Test.java└── com └── sun └── proxy └── $Proxy0.class3 directories, 6 files然後,還想通過javap -c來看麼~~

還是拿出我們剛才下載的jad吧。
執行:
./jad -sjava com/sun/proxy/\$Proxy0.class 在jad的同目錄,你就發現了Proxy0的java文件了:
package com.sun.proxy;import IUserService;import java.lang.reflect.*;public final class $Proxy0 extends Proxy implements IUserService{ public $Proxy0(InvocationHandler invocationhandler) { super(invocationhandler); } public final void login(String s, String s1) { super.h.invoke(this, m3, new Object[] { s, s1 }); } private static Method m3; static { m3 = Class.forName("IUserService").getMethod("login", new Class[] { Class.forName("java.lang.String"), Class.forName("java.lang.String") }); }}為了便於理解,刪除了一些equals,hashCode等方法。
你可以看到,實際上為我們生成一個實現了IUserSevice的類,我們調用其login方法,實際上就是調用了:
super.h.invoke(this, m3, new Object[] { s, s1 });m3即為我們的login方法,靜態塊中初始化的。剩下是我們傳入的參數。
那我們看super.h是什麼:
package java.lang.reflect;public class Proxy{ protected InvocationHandler h;}就是我們自己創建的InvocationHandler對象。
看着這個類,再想login方法,為什麼會回調到InvocationHandler的invoke方法,你還覺得奇怪麼~~

好了,實際上這個哥們面試距離現在挺久了,終於抽空寫完了,希望大家有一定的收穫~
如果還想查看更多的高頻核心面試題分享,請掃描下方的二維碼查看:

三個值得深入思考的 Android 問答分享(第 1 期)
這兩年,我打造了一份具備競爭壁壘的 Android 基礎架構 通關秘籍
(原創升級版)構建一份提升學習效率 99% 的私藏秘籍
這兩年,我打造了一份令人喜極而泣的 Framework 通關秘籍
點擊下方卡片關注JsonChao,為你構建一套
未來技術人必備的底層能力系統

▲點擊上方卡片關注JsonChao,構建一套
未來Android開發必備的知識體系
歡迎把文章分享到朋友圈
加入社群學習成長,一年頂三年
近期星球門票出售火爆,先到者先得,錯過再無。
