1. 程式人生 > >從一道面試題開始說起 列舉、動態代理的原理

從一道面試題開始說起 列舉、動態代理的原理

本文已在我的公眾號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:

我們先下載一份,很小:

meiju01.png

命令也很簡單,執行:

./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方法,你還覺得奇怪麼~~

好了,實際上這個哥們面試距離現在挺久了,終於抽空寫完了,希望大家有一定的收穫~