专治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使用到的建造者模式的问题都有很好的解释,比我者拙劣的文字有吸引力多了。如果不会的,或者看源码很类的,欢迎私聊,不用考虑后果!
不要觉得写的太少,明明说了这么多个方面介绍,但是只是推荐文章或者视频,者样写觉得没什么意义。其实不是,很多时候就算是自己研究的源码,还是会通过搜索或者翻译等等工具来解读,我这水平我还是知道的,没有大牛说的那么详细和通俗易懂,但是我可以提供好点的文章和途径,让你们少走弯路,搜索的关键字是个很头痛的问题,有时候其实有人已经帮你解决了的问题,但是你关键字不对,一直找不到,结果就自己研究很久或者最后不了了之这种情况是经常发生的。

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