JAVA集合框架探究(一)

JAVA集合框架探究(一)

集合框架是日常开发中使用最多的,但是我对它还一知半解。在具体应该选择哪个容器使用时往往不能
确定,因为对它的实现细节不够了解。所以准备通过查看文档和源码的方式对每个集合框架加深理解。
首先会从总体框架上进行梳理,然后再具体到每个集合类进行分析。

一、概述

一般存放一系列相同的已知对象时,我们会使用数组,但开发过程中,大部分情况下对象的数量都无法确定。所以无法使用数组的形式去存储,这时就需要使用集合的形式存储对象,集合总体来说就是存储一系列相同对象的容器,根据不同的特性(如顺序、是否重复等)又分为不同的类型。

集合和数组的区别

  • 数组长度是固定的,需要在初始化时指定。集合不需要指定长度。
  • 数组可以保存对象和基本数组类型,集合只能保存对象的引用,基本数据类型需要经过装箱后才能存储。

JAVA的集合类根据用途,大致可以分为两种。一种是保存元素的序列,并且提供了序列中增删元素的方法,另一种是保存键值对对象,像字典的形式一样可以通过一个对象来查找映射表中的另一个对象。这两种类型也分别是由两个接口派生,Collecion和Map。相关类图比较复杂,为了方便理解,一个接口一个接口来分析。

1、Collection

先看一张Collection接口。

可以看到Collection接口里面的方法:

Collection接口是最基本的集合接口。我们平时使用的ArrayList、HashSet等集合类分别实现了List、Set、Queue接口,而Collection是这些接口的抽象,包含了它们的通用方法。

对Collection的遍历有三种方式,第一种是foreach,第二种因为Collection继承了迭代接口Iterable,所以可以使用迭代器进行遍历。第三种是JDK8的新特性,对集合的流式操作(Stream),这是对集合功能的增强,写法使用lambda表达式,可以以非常简单的语法对集合进行各种聚合和批量处理操作,感兴趣的话可以了解一下。

Collection接口没有实现类,只有派生的三个接口:Set、Queue、List。

  • List:List顾名思义,可以存放有序的元素,它可以通过索引去查找某个元素。
  • Set:Set与List的区别是其中的元素是不可重复的,也是无序的。

2、List

List相关类图。

List接口除了继承的Collection接口里的方法,又添加了一些方法,主要可以分为以下几类。

  • 搜索相关方法。

     // Search Operations
    
    int indexOf(Object o);
    
    int lastIndexOf(Object o);
    

    在集合中查找指定元素的位置。

  • 位置相关方法。

    // Positional Access Operations
        
    E get(int index);
    
    E set(int index, E element);
    
    void add(int index, E element);
    
    E remove(int index);	
    

    如get、set、add、remove方法,参数都是index,因为list是有序集合,可以像数组一样根据位置进行元素操作。

  • ListIterator。List迭代器。

    // List Iterators
    
    ListIterator<E> listIterator();
    

    List迭代器比Iterator增加了添加元素、向前遍历、定位索引、修改元素等方法,在List子类中有相关实现。

  • 范围操作。

    List<E> subList(int fromIndex, int toIndex);
    

3、Set

Set相关类图。

Set接口方法没有对Collection接口进行扩展,在此不再赘述。

4、Queue

Queue相关类图。

Queue是队列,是一种先入先出的结构(FIFO),在数据结构学习中经常使用。
Queue接口除了继承Collection的方法,还定义了队列的常规操作方法。如下:

	offer(E e);  //入队操作
  
    E remove(); //移除队头元素  
  
    E poll();  //移除队头元素  
  
    E element(); //获取队头元素  
  
    E peek();  //获取队头的元素

可以看到remove/poll、element/peek方法作用是相同的,那有什么区别呢?在注释里写了这点,This method differs from {@link #poll poll} only in that it throws an exception if this queue is empty.当队列为空时remove会抛出异常,poll会返回空。element/peek的区别也是这样。element()方法会抛出异常。

5、Map

Map相关类图。

Map接口中除了我们经常使用的put、get、remove等方法,有几个方法比较有意思。

	// Views

    Collection<V> values();

其中,values()返回所有的value,使用了Collection,所以我们在使用values()方法时返回的是个Collection。值是可重复的。

 Set<K> keySet();

keySet()返回值是所有的key,这样可以遍历Set获取key,再通过get获取所有的value。因为使用Set存储,所以key是不可重复的。

Set<Map.Entry<K, V>> entrySet();

entrySet()在遍历Map时经常使用,它返回的是包含映射关系的Set。其中Entry是Map接口中的一个内部接口,有如下方法:

        K getKey();

        V getValue();

        V setValue(V value);

        boolean equals(Object o);

entry是存放在Set中,所以entry是不可重复的。

另外,还有HashTable继承于Dictionary类,也实现了Map接口,但Dictionary类已经标记为废弃了,所以不再研究。

二、AbstractCollection源码解析

从上面List和Set的类图中可以看出,所有实现类几乎都继承自AbstractCollection这个抽象类,AbstractCollection是Collection唯一的直接实现类,实现了Collection接口里的大部分方法。在此列举几个比较重要的方法。

1. contains(Object o)

    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;
    }

可以看到遍历方式是使用迭代器,并且在元素为空时也可以查找。

2. toArray()

    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;
    }

可以看到需要先调用size()方法创建一个数量和集合数量相同的数组,然后遍历将集合元素引用复制到数组,如果集合中元素比数组大小少,则调用Arrays.copyOf()方法来截取数组并返回新数组,如果集合中元素比数组大小多,则会调用finishToArray调整数组大小并返回新数组。这里对数组大小和集合数量进行比较是为了考虑在toArray()期间修改了原集合元素的情况。

    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);
    }

此方法的核心是对数组扩容,当数组元素满了时使用Arrays.copyOf方法对数组扩容。扩容的大小为newCap = cap + (cap >> 1) + 1。使用移位运算符,相当于增加了原大小的一半再加一,并且如果调整后带下超过MAX_ARRAY_SIZE数组最大值时,要使用hugeCapacity()方法调整大小。
数组最大值为:

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

数组最大值是int最大值-8的原因在注释里写明了,有些虚拟机会在数组中保留一些头关键字,为了防止内存溢出所以小一些。
再看一下hugeCapacity()方法

    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;
    }

遍历完成之后再使用Arrays.copyOf截取数组。

3. toArray(T[] a)

	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;
    }

这个方法和无参toArray()方法的区别是有一个数组作为参数,作用是将集合中的元素填入数组中。

  • 首先比较参数数组a和集合大小,使用其中较大地值来构造数组r(a长度较大时直接使用a)。
  • 使用迭代器进行遍历,将元素转为数组类型存入数组。
  • 在集合元素和数组长度不一致时,可以看到有两种情况(一和三是一种结果)。如果数组长度较大,则剩余位置都设置null。如果集合长度大于数组,则Arrays.copyOf进行截取。
  • 最后如果数组已满,则和上面的方法一样,通过finishToArray()进行扩容。

4.add()

	public boolean add(E e) {
        throw new UnsupportedOperationException();
    }

add方法会抛出异常。

5.抽象方法

    public abstract Iterator<E> iterator();

    public abstract int size();

二、AbstractList源码解析

AbstractList实现了List接口,继承自AbstractCollection,是所有List集合类的父类,我们来看一下它有哪些方法。

1、没有实现的方法

    public boolean add(E e) {
        add(size(), e);
        return true;
    }
	
	 public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    abstract public E get(int index);

    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }

这几个方法都是直接抛出异常。

2、indexOf()

    public int indexOf(Object o) {
        ListIterator<E> it = listIterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return it.previousIndex();
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return it.previousIndex();
        }
        return -1;
    }

    public int lastIndexOf(Object o) {
        ListIterator<E> it = listIterator(size());
        if (o==null) {
            while (it.hasPrevious())
                if (it.previous()==null)
                    return it.nextIndex();
        } else {
            while (it.hasPrevious())
                if (o.equals(it.previous()))
                    return it.nextIndex();
        }
        return -1;
    }

indexOf和lastIndexOf,都使用了listIterator进行遍历,并且可以查询Null的元素。
因为listIterator是双向迭代器,所以lastIndexOf可以从后向前进行遍历。

3、addAll()

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        boolean modified = false;
        for (E e : c) {
            add(index++, e);
            modified = true;
        }
        return modified;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index < 0 || index > size())
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

addAll方法首先检查index是否越界,然后再挨个添加元素。

4. iterator()

	  public Iterator<E> iterator() {
	        return new Itr();
	  }
     private class Itr implements Iterator<E> {
        /**
         * Index of element to be returned by subsequent call to next.
         */
        int cursor = 0;

        /**
         * Index of element returned by most recent call to next or
         * previous.  Reset to -1 if this element is deleted by a call
         * to remove.
         */
        int lastRet = -1;

        /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            checkForComodification();
            try {
					int i = cursor;//当前标记位                
					E next = get(i);//通过列表的get函数获得元素                
					lastRet = i;//记录老的标记位                
					cursor = i + 1;//标记位+1
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

iterator()是AbstractCollection中没有实现的抽象方法,在AbstractList中实现了该方法,Itr是单向迭代器,继承了Iterator接口。其中

  • cursor指迭代器当前位置。
  • lastRet指迭代器上次位置。
  • expectedModCount指迭代期间集合被修改的次数。
  • next()方法中首先通过get方法获取值,然后cursor标志加一,以此进行迭代。
  • 发生IndexOutOfBoundsException异常时,首先会通过checkForComodification()检查迭代期间集合是否发生了变化,如果是则会先抛出并发修改异常。
  • remove()方法中会先判断lastRet是否小于0,所以remove只能删除已经迭代过的元素。

5.listIterator()

 	public ListIterator<E> listIterator() {
        return listIterator(0);
    }

    public ListIterator<E> listIterator(final int index) {
        rangeCheckForAdd(index);

        return new ListItr(index);
    }
    
    private class ListItr extends Itr implements ListIterator<E> {
    	// 通过索引初始化迭代器
        ListItr(int index) {
            cursor = index;
        }
		 // 是否有前一个元素
        public boolean hasPrevious() {
            return cursor != 0;
        }

		// 返回前一个元素
        public E previous() {
            checkForComodification();
            try {
                int i = cursor - 1;
                E previous = get(i);
                lastRet = cursor = i;
                return previous;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.set(lastRet, e);
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                AbstractList.this.add(i, e);
                lastRet = -1;
                cursor = i + 1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    }

listIterator是Iterator的功能增强版,从方法上就可以看出。listIterator除了无参还有一个有参方法,以index为参数,提供的是通过索引位置初始化的迭代器。

  • 以索引位置初始化迭代器的方式是把cursor初始为index.
  • 判断有没有前一个元素的方式是判断cursor是否为0。
  • add新元素后会使lastRet失效。

6.subList()

subList方法是返回原列表的一部分,也就是list的子列表.

    public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
    }

代码中可以看到,如果List实现了RandomAccess接口,则返回RandomAccessSubList,否则返回SubList。这两个类的源码都在AbstractList类中,后面会进行分析。其中会说明为什么sublist方法只是返回原list的一部分的映射。

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