2554 2017-09-11 2020-06-25

前言:面试中不可避免的会被问到Java容器类,这次从全局上过一遍,体会一下作者的设计思路以及了解一些需要注意的地方。

一、Java容器类图

网上大部分的类图都是来源于《Java编程思想》,这里借用一下网上的(这里有个错误,LinkIterator应为ListIterator),如下:

Java集合框架类图

二、Collection接口

可以理解为一个独立元素的序列,我们抽取里面部分方法进行讲解:

方法说明
比较常用的抽象方法(这些方法不做特别说明)int size()、boolean isEmpty()、boolean contains(Object o)、boolean add(E e)、boolean remove(Object o)、void clear()、boolean equals(Object o)
Iterator iterator()这个方法表明只要是Java类库中的序列,或者说实现Collection接口的类(以下简称实现类)都可以通过xxx.iterator获得一个Iterator接口类型的迭代器
Object[] toArray()这个方法表明实现类可以向数组转换
T[] toArray(T[] a)这个方法说明实现类可以向指定类型数组转换,形如toArray(new Integer(){1, 2})
int hashCode()这个方法说明实现类有哈希值

下面是一些测试代码

private static void testCollection() {
    Collection<Integer> collection = Arrays.asList(1, 2, 3, 4);
    Integer[] temp = collection.toArray(new Integer[collection.size()]);
    Iterator<Integer> it = collection.iterator();
    while (it.hasNext()) {
        Integer i = it.next();
        System.out.println(i + "(hashCode=" + i.hashCode() + ")");
    }
    // 此步会抛出UnsupportedOperationException
    // collection.clear();
    System.out.println(collection.size());
    // 输出结果略
}

// 这个ArrayList并不是我们认为的那个ArrayList,他只是Arrays的一个静态内部类而已
private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable {
	// 注意并没有重写clear方法,其他略
}

// 下面是异常信息,不难看出结果了吧
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.AbstractList.remove(AbstractList.java:161)
	at java.util.AbstractList$Itr.remove(AbstractList.java:374)
	at java.util.AbstractList.removeRange(AbstractList.java:571)
	at java.util.AbstractList.clear(AbstractList.java:234)

public E remove(int index) {
    throw new UnsupportedOperationException();
}

1、List接口

List接口维护一个有序列表,在Collection的基础上添加了大量针对元素位置操作的方法。还是抽取部分,如下:

方法说明
比较常用的抽象方法(这些方法不做特别说明)E get(int index)、E set(int index, E element)、void add(int index, E element)、E remove(int index)
int indexOf(Object o)返回目标对象第一次出现的位置
int lastIndexOf(Object o)返回目标最后一次出现的位置
ListIterator listIterator()返回Iterator接口的一个实现类
ListIterator listIterator(int index)返回Iterator接口的一个实现类
List subList(int fromIndex, int toIndex)返回一个子列表

下面是一些测试代码

private static void testList() {
    List<String> one = new LinkedList<>();
    List<String> two = new ArrayList<>();
    for (int i = 100000; i > 0; i--) {
        one.add(String.valueOf(i));
        two.add(String.valueOf(i));
    }
    long startTime = System.currentTimeMillis();
    one.sort(null);
    System.out.println("LinkedList排序耗时" + (System.currentTimeMillis() - startTime) + "ms");
    two.sort(null);
    System.out.println("ArrayList排序耗时" + (System.currentTimeMillis() - startTime) + "ms");
    startTime = System.currentTimeMillis();
    for(String s : one) {
        System.out.print(s);
    }
    System.out.println("\nLinkedList遍历耗时" + (System.currentTimeMillis() - startTime) + "ms");
    startTime = System.currentTimeMillis();
    for(String s : one) {
        System.out.print(s);
    }
    System.out.println("\nArrayList遍历耗时" + (System.currentTimeMillis() - startTime) + "ms");
}
// 部分结果如下
LinkedList排序耗时55ms
ArrayList排序耗时84ms
LinkedList遍历耗时248ms
ArrayList遍历耗时213ms

结合两者的具体实现,由上面的输出结果来看,LinkedList在添加、删除、插入方面速度快,而ArrayList遍历较快。其原因是LinkedList底层是链表,对节点的操作很方便,增删改很方便,而ArrayList底层是数组实现,物理位置连续,所以遍历较快。

2、Set接口

Set接口维护一个无序的列表,Collection接口中有19个方法,而Set接口中只有16个方法。这说明,大部分Collection中的操作就是Set接口中的操作了,理解为接口规范(注释需要)即可。

下面是一些测试代码

private static void testSet() {
    Set<String> one = new HashSet<>();
    Set<String> two = new LinkedHashSet<>();
    for (int i = 0; i < 6; i++) {
        one.add(String.valueOf(i * 31));
        two.add(String.valueOf(i * 31));
    }
    for (String s : one) {
        System.out.print(s + " ");
    }
    System.out.println();
    for (String s : two) {
        System.out.print(s + " ");
    }
}
// 输出结果如下
0 155 124 93 62 31 
0 31 62 93 124 155 

其实HashSet和LinkedHashSet的区别就是HashMap和LinkedHashMap的区别,这个我们后面再讲。

3、Queue接口

队列维护一个先进先出的有序列表,它在Collection的基础上添加了自己的6个方法,而这6个方法又分为两部分,这里引用官方文档说明,如下:

方法说明
boolean add(E e)将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException
boolean offer(E e)将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常
E remove()获取并移除此队列的头
E poll()获取并移除此队列的头,如果此队列为空,则返回 null
E element()获取,但是不移除此队列的头
E peek()获取但不移除此队列的头;如果此队列为空,则返回 null

下面是一些测试代码

private static void testQueue() {
    Queue<Integer> queue = new LinkedList<>();
    Deque<Integer> deque = new LinkedList<>();
    Deque<Integer> stack = new LinkedList<>();
    for (int i = 0; i < 4; i++) {
        queue.add(i);
        deque.offer(i);
        stack.push(i);
    }
    System.out.println(queue.poll());
    System.out.println(deque.pollLast());
    System.out.println(stack.pop());
}
// 输出结果0 3 3

值得注意的是有个双端队列接口Deque,这个到LinkedList再讲。

4、AbstractCollection

Collection有AbstractCollection,List有AbstractList,Set有AbstractSet,Map有AbstractMap,它们都是对接口一种半实现,留下一些抽象方法由子类重写以完成子类独有的特征。以AbstractCollection为例

public abstract class AbstractCollection<E> implements Collection<E> {
    protected AbstractCollection() {
    }
    public abstract Iterator<E> iterator();
    public abstract int size();
    public boolean isEmpty() {
        return size() == 0;
    }
    public boolean contains(Object o) {
        Iterator<E> it = iterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return true;
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return true;
        }
        return false;
    }
    public Object[] toArray() {
        // Estimate size of array; be prepared to see more or fewer elements
        Object[] r = new Object[size()];
        Iterator<E> it = iterator();
        for (int i = 0; i < r.length; i++) {
            if (! it.hasNext()) // fewer elements than expected
                return Arrays.copyOf(r, i);
            r[i] = it.next();
        }
        return it.hasNext() ? finishToArray(r, it) : r;
    }
    public <T> T[] toArray(T[] a) {
        // Estimate size of array; be prepared to see more or fewer elements
        int size = size();
        T[] r = a.length >= size ? a :
                  (T[])java.lang.reflect.Array
                  .newInstance(a.getClass().getComponentType(), size);
        Iterator<E> it = iterator();

        for (int i = 0; i < r.length; i++) {
            if (! it.hasNext()) { // fewer elements than expected
                if (a == r) {
                    r[i] = null; // null-terminate
                } else if (a.length < i) {
                    return Arrays.copyOf(r, i);
                } else {
                    System.arraycopy(r, 0, a, 0, i);
                    if (a.length > i) {
                        a[i] = null;
                    }
                }
                return a;
            }
            r[i] = (T)it.next();
        }
        // more elements than expected
        return it.hasNext() ? finishToArray(r, it) : r;
    }    
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;    
    private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
        int i = r.length;
        while (it.hasNext()) {
            int cap = r.length;
            if (i == cap) {
                int newCap = cap + (cap >> 1) + 1;
                // overflow-conscious code
                if (newCap - MAX_ARRAY_SIZE > 0)
                    newCap = hugeCapacity(cap + 1);
                r = Arrays.copyOf(r, newCap);
            }
            r[i++] = (T)it.next();
        }
        // trim if overallocated
        return (i == r.length) ? r : Arrays.copyOf(r, i);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError
                ("Required array size too large");
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    // 其他略
}

下面是一些测试代码

private static void testAbstractCollection() {
    List<String> one = new ArrayList<>();
    List<String> two = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        one.add(String.valueOf(i));
        two.add(String.valueOf(i));
    }
    String[] str1 = (String[]) one.toArray();
    String[] str2 = two.toArray(new String[two.size()]);
    for (String t : str1) {
    }
    for (String t : str2) {
    }
}
// 直接运行是会抛出异常的,信息如下
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
	at site.xiaokui.common.hk.blog.CollectionTest.testAbstractCollection(CollectionTest.java:22)
	at site.xiaokui.common.hk.blog.CollectionTest.main(CollectionTest.java:12)
                                                                                     // 强制转换只能针对单个对象,对于数组还是要一个一个转过来,推荐使用第二种           

三、Map接口

可以理解为一组键值对对象,Map接口是同Collection接口一样,处于最顶层。代码确实不少,我们抽取部分方法来看下:

方法说明
比较常用的抽象方法(这些方法不做特别说明)int size()、boolean isEmpty()、boolean containsKey(Object key)、boolean containsValue(Object value)、V get(Object key)、V put(K key, V value)、V remove(Object key)
Set keySet()返回一个键Set
Collection values()返回一个值Collection
Set<Map.Entry<K, V>> entrySet()返回内部接口Entry中的一个Set集合
其他剩下的大部分是default方法,暂时不管

对于Map可以展开的有太多太多,这一部分我们留到后面再继续。

四、具体子类的实现

上面提到的都是些顶级接口或抽象类,没有具体的实现,这里我们看一下具体的子类实现。列表如下(自底向上,从左往右):

类名说明
Stack栈是Vector的一个子类,它实现了一个标准的后进先出的栈
Vector该类和ArrayList非常相似,但是该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍
ArrayList该类也是实现了List的接口,实现了可变大小的数组,随机访问和遍历元素时,提供更好的性能。该类也是非同步的,在多线程的情况下不要使用。ArrayList 增长当前长度的50%,插入删除效率低
LinkedList该类实现了List接口,允许有null(空)元素。主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个List,则必须自己实现访问同步,解决方法就是在创建List时候构造一个同步的List
LinkedHashSet具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现
HashSet该类实现了Set接口,不允许出现重复元素,不保证集合中元素的顺序,允许包含值为null的元素,但最多只能一个
TreeSet该类实现了Set接口,可以实现排序等功能
SortedSet继承于Set保存有序的集合
WeakHashMap继承AbstractMap类,使用弱密钥的哈希表
LinkedHashMap继承于HashMap,使用元素的自然顺序对元素进行排序
HashMapHashMap 是一个散列表,它存储的内容是键值对(key-value)映射
TreeMap继承了AbstractMap,并且使用一颗树
SortedMap继承于Map,使Key保持在升序排列

具体的分析将在后面的篇章进行讲解。

总访问次数: 314次, 一般般帅 创建于 2017-09-11, 最后更新于 2020-06-25

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!