1. 程式人生 > >專治Java集合面試回答以及觀察者模式解讀

專治Java集合面試回答以及觀察者模式解讀

Java集合的見到使用介紹我們不詳細說,都是一帶而過,不然就偏離主題了。本人在面試的時候,被問到Java集合的概率是基本在百分之八十以上,有的甚至因為你懂集合的原理實現直接pass掉你,雖然有點難以理解這種行為,但是,在我以及我身邊人身上確實發生過。本文主要從以下幾個方面介紹: 1、集合面試通常問什麼,該怎麼回答? 2、List集合的實現原理以及使用場景。 3、HashMap的使用場景以及使用原理。 4、高併發的Set集合介紹。 5、遺留的集合介紹。 6、觀察者模式的介紹(Java API以及自定義的)。 7、EventBus的介紹以及原理實現解析。 1、集合面試通常問什麼,該怎麼回答? 做Android做久了的程式設計師,很多時候都會忽略這一塊,集合,用的最多的就是ArrayList,最多使用HashMap。Set,幾乎從來沒用過。這就造成我們很多時候不知道app的優化,不僅僅是記憶體,還有程式碼的執行效率,換句話說,這就是資料結構的使用。在金立的一次面試中,我就是因為這玩意被掛掉的,雖然可惜,但是也認同。這也就是為什麼我不再找獨立開發的公司了。那麼回到正題,關於集合,很多面試官是怎麼問的呢? 問題1:你對三種集合的使用場景怎麼理解? 問題2:如何實現List集合? 問題3:說說HashMap的實現原理, 問題4:關於Android對集合有哪些優化? 我是做Android的,所以,Java基礎也就是Android語法基礎,捎帶Android不要覺得奇怪哈!對於上面四個問題,看完這後面的六個介紹,我想應該就知道怎麼回答了,如果還不知道,說明你們還是要去複習下基本語法了。上面的問題是我遇到的,當然還會有其他的,印象深刻,並且比較抽象的能記住的,也就這麼幾個了。 2、List集合的實現原理以及使用場景。 這回答上面的問題2。不呼叫系統的List,我們該如何實現呢?記得剛碰到這個問題的時候,我也矇蔽了,因為在這之前就被問道一個很操蛋的問題:怎麼實現sinx,自己封裝實現。然後突然扔出這麼個問題,我直接蒙了,好吧,我心裡素質確實太差了。好了,實現方式:ArrayList的肯定使用陣列了,LinkedList,使用連結串列,但是Java的連結串列怎麼實現呢?我們完全可以仿造C裡面的組成結構構造出來。比如,單鏈表結構:Node 類裡面就一個data 和next屬性,自己嘗試實現一下。還有,不要忘了既然是資料結構,Java肯定少不了,那麼佇列和堆疊人家已經封裝好了,我們為什麼不直接使用呢?比如使用佇列Queue這個類,還有Stack這個類,都是系統的API,完全按照C的思路就可以做,方法都實現好了。比如:stack.peek(); stack.pop();stack.push();自己實現的方式是不是瞬間思路開闊了?Queue也是一樣,具體操作直接看名字的意思就可以了!List本身是個介面,所以不能新建例項,必須要使用它的具體實現類。我們下面主要看ArrayList。 首先看下List介面的原始碼,提供了通用的這些操作。只要記住名字就行,熟悉的就再溫習一遍。實現的具體程式碼肯定不會在接口裡面的,看看就行,沒什麼要研究的。
public interface List<E> extends Collection<E> {
    int size();

    boolean isEmpty();

    boolean contains(Object var1);

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] var1);

    boolean add(E var1);

    boolean remove(Object var1);

    boolean containsAll(Collection<?> var1);

    boolean addAll(Collection<? extends E> var1);

    boolean addAll(int var1, Collection<? extends E> var2);

    boolean removeAll(Collection<?> var1);

    boolean retainAll(Collection<?> var1);

    default void replaceAll(UnaryOperator<E> var1) {
        Objects.requireNonNull(var1);
        ListIterator var2 = this.listIterator();

        while(var2.hasNext()) {
            var2.set(var1.apply(var2.next()));
        }

    }

    default void sort(Comparator<? super E> var1) {
        Object[] var2 = this.toArray();
        Arrays.sort(var2, var1);
        ListIterator var3 = this.listIterator();
        Object[] var4 = var2;
        int var5 = var2.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Object var7 = var4[var6];
            var3.next();
            var3.set(var7);
        }

    }

    void clear();

    boolean equals(Object var1);

    int hashCode();

    E get(int var1);

    E set(int var1, E var2);

    void add(int var1, E var2);

    E remove(int var1);

    int indexOf(Object var1);

    int lastIndexOf(Object var1);

    ListIterator<E> listIterator();

    ListIterator<E> listIterator(int var1);

    List<E> subList(int var1, int var2);

    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 16);
    }
}
接下來看看ArrayList的實現方式。看名字就知道離不開陣列,原理大致的描述為:它實現List介面、底層使用陣列儲存所有元素。其操作基本上是對陣列的操作,是List介面的可變陣列的實現。實現了所有可選列表操作,並允許包括 null 在內的所有元素。除了實現 List 介面外,此類還提供一些方法來操作內部用來儲存列表的陣列的大小。每個ArrayList例項都有一個容量,該容量是指用來儲存列表元素的陣列的大小。它總是至少等於列表的大小。隨著向ArrayList中不斷新增元素,其容量也自動增長。自動增長會帶來資料向新陣列的重新拷貝,因此,如果可預知資料量的多少,可在構造ArrayList時指定其容量。在新增大量元素前,應用程式也可以使用ensureCapacity操作來增加ArrayList例項的容量,這可以減少遞增式再分配的數量。
注意,此實現不是同步的。如果多個執行緒同時訪問一個ArrayList例項,而其中至少一個執行緒從結構上修改了列表,那麼它必須保持外部同步。最好的原理就是看原始碼的實現方式:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
這可以看出是實現List介面的。其他的暫時不管。看構造方法:
 public ArrayList(int var1) {
        if(var1 > 0) {
            this.elementData = new Object[var1];
        } else {
            if(var1 != 0) {
                throw new IllegalArgumentException("Illegal Capacity: " + var1);
            }

            this.elementData = EMPTY_ELEMENTDATA;
        }

    }

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(Collection<? extends E> var1) {
        this.elementData = var1.toArray();
        if((this.size = this.elementData.length) != 0) {
            if(this.elementData.getClass() != Object[].class) {
                this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
            }
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }

    }
解釋:第一個構造方法可以看出,它是可以設定起始大小的,這操作和陣列基本是一樣的。後面兩個構造方法就有個預設的大小,具體的值是多少呢?以前用ES的時候總要看API,然後還要看Doc文件,AS可以直接點進去看看,就一個常量。
還是直接看下屬性值吧:
 private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
    transient Object[] elementData;
    private int size;
    private static final int MAX_ARRAY_SIZE = 2147483639;
不用解釋,一目瞭然,有沒有發現一個從來沒注意過的問題。ArrayList大小是有限制的,最大的個數不能超過2147483639。看第三個構造方法,直接就可以看出,當超過起始大小的時候,就會出現複製陣列和擴容的現象。
 public void ensureCapacity(int var1) {
        int var2 = this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA?0:10;
        if(var1 > var2) {
            this.ensureExplicitCapacity(var1);
        }

    }
原理其實還算簡單吧,因為自己就能實現一個比較簡單點的ArrayList有木有!那麼使用場景,實在是太多了。當需要顯示列表,需要記錄資料的時候,都可以用到。
3、HashMap的使用場景以及使用原理。 這個就比較難以理解了,這個原理設計到對映表,還涉及到hash演算法的雜湊,以及擴容之後的資料處理等等。不過沒關係,我們慢慢來學習吧。之所以說List集合的實現原理以及使用場景,原理放前面,場景放後面,因為大家都能掌握,這個場景放前面,原理放後面,是因為原理太複雜了,我智慧推薦博文讓親們自己去看了,我也怕我自己說不明白。Java HashMap工作原理及實現,這個篇博文總結的相當到位,有圖有真相的。看不懂沒關係,因為實際開發中不可能用到,只要知道個大概,我相信面試的時候,面試官讓你來寫出實現方式來,如果有那就出門左拐下一家公司吧! 4、高併發的Set集合介紹。 這個也有好幾個實現類,HashSet和TreeSet,一個有序一個無序。Set其實和Map是非常類似的,這裡就挑HashSet解釋。因為HashSet就是用HashMap實現的,直接看原始碼:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E, Object> map;
    private static final Object PRESENT = new Object();

    public HashSet() {
        this.map = new HashMap();
    }

    public HashSet(Collection<? extends E> var1) {
        this.map = new HashMap(Math.max((int)((float)var1.size() / 0.75F) + 1, 16));
        this.addAll(var1);
    }

    public HashSet(int var1, float var2) {
        this.map = new HashMap(var1, var2);
    }

    public HashSet(int var1) {
        this.map = new HashMap(var1);
    }

    HashSet(int var1, float var2, boolean var3) {
        this.map = new LinkedHashMap(var1, var2);
    }

    public Iterator<E> iterator() {
        return this.map.keySet().iterator();
    }

    public int size() {
        return this.map.size();
    }

    public boolean isEmpty() {
        return this.map.isEmpty();
    }

    public boolean contains(Object var1) {
        return this.map.containsKey(var1);
    }

    public boolean add(E var1) {
        return this.map.put(var1, PRESENT) == null;
    }

    public boolean remove(Object var1) {
        return this.map.remove(var1) == PRESENT;
    }

    public void clear() {
        this.map.clear();
    }

    public Object clone() {
        try {
            HashSet var1 = (HashSet)super.clone();
            var1.map = (HashMap)this.map.clone();
            return var1;
        } catch (CloneNotSupportedException var2) {
            throw new InternalError(var2);
        }
    }

    private void writeObject(ObjectOutputStream var1) throws IOException {
        var1.defaultWriteObject();
        var1.writeInt(this.map.capacity());
        var1.writeFloat(this.map.loadFactor());
        var1.writeInt(this.map.size());
        Iterator var2 = this.map.keySet().iterator();

        while(var2.hasNext()) {
            Object var3 = var2.next();
            var1.writeObject(var3);
        }

    }

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        int var2 = var1.readInt();
        if(var2 < 0) {
            throw new InvalidObjectException("Illegal capacity: " + var2);
        } else {
            float var3 = var1.readFloat();
            if(var3 > 0.0F && !Float.isNaN(var3)) {
                int var4 = var1.readInt();
                if(var4 < 0) {
                    throw new InvalidObjectException("Illegal size: " + var4);
                } else {
                    var2 = (int)Math.min((float)var4 * Math.min(1.0F / var3, 4.0F), 1.07374182E9F);
                    this.map = (HashMap)(this instanceof LinkedHashSet?new LinkedHashMap(var2, var3):new HashMap(var2, var3));

                    for(int var5 = 0; var5 < var4; ++var5) {
                        Object var6 = var1.readObject();
                        this.map.put(var6, PRESENT);
                    }

                }
            } else {
                throw new InvalidObjectException("Illegal load factor: " + var3);
            }
        }
    }

    public Spliterator<E> spliterator() {
        return new KeySpliterator(this.map, 0, -1, 0, 0);
    }
}
serialVersionUID 是序列化需要的,我們這裡不用關心。如果前面看的懂HashMap的話,再回頭看這個應該是很輕鬆的事情。底層使用HashMap來儲存HashSet中所有元素,接著定義一個虛擬的Object物件作為HashMap的value,將此物件定義為static final。實際底層會初始化一個空的HashMap,並使用預設初始容量為16和載入因子0.75。也就說,當容量達到上一次設定的0.75大小時就會擴容,不是非要等到超過,這裡筆試題就會經常性的來考,擴容的次數等等。
5、遺留的集合介紹。 不多,如果問道這樣的問題,出門左拐,下一家公司。但是作為一個Java 接觸過的程式設計師,總要知道有這麼回事。 Hashtable。Hashtable和HashMap類的作用一樣,實際上,這兩個類擁有相同的介面。與Vector類一樣,Hashtable的方法也是同步的,這是與HashMap不同的地方。如果對同步性或與遺留程式碼的相容性沒有任何要求,就應該使用HashMap。 Enumeration 遺留集合使用Enumeration介面對元素進行遍歷。Enumeration介面有兩個方法,即hashMoreElements和nextElement。這兩個方法與Iterator介面的hashNext和next方法十分類似。 Properties BitSet 具體的自行搜尋,我也不是很精通,也是瞭解個大概,因為在這之後你還能碰到或者談論到這個遺留的東西都是很小的概率,我要是不是看了兩三遍《java 核心技術I》我也不會知道這些遺留的集合原來有這麼多,偏偏,我在一家小公司面試的時候被面試官剛好問到了,我直接握手想走人,剛好人家說沒事,技術面通過,只是由於人事方面或者其他的什麼原因最終沒有給我offer。 6、觀察者模式的介紹(Java API以及自定義的)。 為什麼觀察者模式放在這裡一起說呢,因為在隨手科技面試的時候,集合和觀察者模式是被一起問到的,而細節,就是這些集合的考察。 先看觀察者模式的實現。其實Java內部的API已經有實現的,只是儘管如此還是會自己去自定義Observer,Observable等等。 Observer.java
public interface Observer {
    void update(Observable var1, Object var2);
}
Observable.java
public class Observable {
    private boolean changed = false;
    private Vector<Observer> obs = new Vector();//兼顧執行緒安全,所以沒用list

    public Observable() {
    }

    public synchronized void addObserver(Observer var1) {
        if(var1 == null) {
            throw new NullPointerException();
        } else {
            if(!this.obs.contains(var1)) {
                this.obs.addElement(var1);
            }

        }
    }

    public synchronized void deleteObserver(Observer var1) {
        this.obs.removeElement(var1);
    }

    public void notifyObservers() {
        this.notifyObservers((Object)null);
    }

    public void notifyObservers(Object var1) {
        Object[] var2;
        synchronized(this) {
            if(!this.changed) {
                return;
            }

            var2 = this.obs.toArray();
            this.clearChanged();
        }

        for(int var3 = var2.length - 1; var3 >= 0; --var3) {
            ((Observer)var2[var3]).update(this, var1);
        }

    }

    public synchronized void deleteObservers() {
        this.obs.removeAllElements();
    }

    protected synchronized void setChanged() {
        this.changed = true;
    }

    protected synchronized void clearChanged() {
        this.changed = false;
    }

    public synchronized boolean hasChanged() {
        return this.changed;
    }

    public synchronized int countObservers() {
        return this.obs.size();
    }
}
唉,有了這兩個,我們只要實現具體的實現類就行了。是不是有點恍然大悟的趕腳,不過由此也可以看出,觀察者模式的重要性!如果自己連這模式的UML吐都不知道是什麼的,自行腦補,推薦 head first,生動形象的講解了各種模式以及使用場景等等。實在看不懂的,歡迎和我私下交流! 面試中肯定會被問到Android原始碼或者框架裡面的使用。那場景第一個想到的,肯定是廣播,這是眾所周知的,框架的話建議回答EventBus,因為這個框架的實現確實想到可以,使用也相當簡介,資料類都不需要實現訂閱,內部直接幫你實現好了,簡單的幾行程式碼就可以讓你輕鬆搞定時間的傳輸,雖然檔案有點多,註釋多寫點,可以彌補維護難度的問題!
7、EventBus的介紹以及原理實現解析。 建議去看騰訊課堂的原始碼解析,視訊講解的非常清晰明瞭,裡面的資料獲取,當然還有EventBus使用到的建造者模式的問題都有很好的解釋,比我者拙劣的文字有吸引力多了。如果不會的,或者看原始碼很類的,歡迎私聊,不用考慮後果! 不要覺得寫的太少,明明說了這麼多個方面介紹,但是隻是推薦文章或者視訊,者樣寫覺得沒什麼意義。其實不是,很多時候就算是自己研究的原始碼,還是會通過搜尋或者翻譯等等工具來解讀,我這水平我還是知道的,沒有大牛說的那麼詳細和通俗易懂,但是我可以提供好點的文章和途徑,讓你們少走彎路,搜尋的關鍵字是個很頭痛的問題,有時候其實有人已經幫你解決了的問題,但是你關鍵字不對,一直找不到,結果就自己研究很久或者最後不了了之這種情況是經常發生的。