專治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使用到的建造者模式的問題都有很好的解釋,比我者拙劣的文字有吸引力多了。如果不會的,或者看源碼很類的,歡迎私聊,不用考慮後果!
不要覺得寫的太少,明明說了這麼多個方面介紹,但是隻是推薦文章或者視頻,者樣寫覺得沒什麼意義。其實不是,很多時候就算是自己研究的源碼,還是會通過搜索或者翻譯等等工具來解讀,我這水平我還是知道的,沒有大牛說的那麼詳細和通俗易懂,但是我可以提供好點的文章和途徑,讓你們少走彎路,搜索的關鍵字是個很頭痛的問題,有時候其實有人已經幫你解決了的問題,但是你關鍵字不對,一直找不到,結果就自己研究很久或者最後不了了之這種情況是經常發生的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章