從一道面試題開始說起 列舉、動態代理的原理
本文已在我的公眾號hongyangAndroid原創釋出。
轉載請標明出處:
本文出自:漲鴻洋的部落格
前段時間在dota群,一哥們出去面試,回顧面試題的時候,說問到了列舉。
作為一名Android選手,談到列舉,那肯定是:
Android上不應該使用列舉,佔記憶體,應該使用@XXXDef註解來替代,balabala…
這麼一回答,心裡美滋滋。
沒想到面試官問了句:
- 列舉的原理是什麼?你說它佔記憶體到底佔多少記憶體呢,如何佐證?
聽到這就慌了,沒了解過呀。
下面說第一個問題(沒錯還有第二個問題)。
列舉的本質
有篇文章:
寫得挺好的。
下面還是要簡述一下,我們先寫個列舉類:
public enum Animal {
DOG,CAT
}
看著這程式碼,完全看不出來原理。不過大家應該都知道java類編譯後會產生class檔案。
越接近底層,本質就越容易暴露出來了。
我們先javac搞到Animal.class,然後通過javap命令看哈:
javap Animal.class
輸出:
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 檢視所有引數的含義。
javap -c Animal.class
輸出:
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 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;
大致含義就是new Animal(String,int),然後給我們的靜態常量DOG賦值。
好了,不看了,好煩。我們轉念想一下,如果這個位元組碼咱們能看懂,那就是有規則的,只要有規則,肯定有類似翻譯類的工具,直接轉成java程式碼的。
確實有,比如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方法都進行了實現,看一眼你就明白了。
本文並不是為了去討論列舉的原理,而是想要給大家說明的是很多“語法糖”類似的東西,都能按照這樣的思路去了解它的原理。
下面我們再看一個,聽起來稍微高階一點的:
- 動態代理
動態代理
這個比較出名的就是retrofit了。
問: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檔案。
然後呼叫:
java Test
輸出:
class com.sun.proxy.$Proxy0
method = 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.class
3 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方法,你還覺得奇怪麼~~
好了,實際上這個哥們面試距離現在挺久了,終於抽空寫完了,希望大家有一定的收穫~