JAVA 持有对象——容器初探

引言

    若是一个程序只包含固定数量的且其生命周期都是已知对象,那么这是一个很是简单的程序——《think in java》java

    了解容器前,先提出一个问题,ArrayList和LinkedList谁的处理速度更快呢?编程

 

一 持有对象的方式

    在Java中,咱们能够使用数组来保存一组对象。可是,数组是固定大小的,在通常状况下,咱们写程序时并不知道将须要多少个对象,所以数组固定大小对于编程有些受限。数组

    java类库中提供了一套至关完整的容器类来解决这个问题,其中基本类型有List,Queue,Set,Map,这些对象类型被称为集合类。可是,Java类库中使用了Collection来指代集合类中的子集{List,Queue,Set},因此集合类也被称为容器。容器提供了完善的方法来保存对象。安全

 

二 类型安全的容器

    java采用泛型保证咱们不会向容器中插入不正确的类型,可是java的泛型只存在于程序源码中,在通过编译器编译就会将类型擦除。举一个例子:函数

//通过编译前
List<String> list = new ArrayList<>();
list.add("ok");
System.out.println(list.get(0));

//通过编译后
List list = new ArrayList();
list.add("ok");
System.out.println((String)list.get(0));

这样作的好处是:在编写程序的时候,不会将其余非导出类型的对象添加到容器中。优化

 

三 List

    数组存储多个对象的缘由是它提早声明了能存储多少对象。那容器又是如何实现存储不定多对象的呢?this

//ArrayList部分源码

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};

private transient Object[] elementData;
private int size;

public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
}
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

    咱们能够看到,在ArrayList类中有一个elementData数组。当使用无参构造函数时数组长度默认为空,当向arraylist加入对象时,会调用一个方法来判断数组是否能放下这个对象。当数组为空时设置数组长度为10并申请相应大小空间,当数组已满时,最少从新申请原数组大小1.5倍的空间(除非达到int类型最大值-8)。而在LinkedList中却没有采用这种方式,而是采用链表方式。spa

//LinkedList add方法
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

    在LinkedList中,他的add方法调用了linkLast方法,直接在链表后边加入一个新的节点。操作系统

 

四 Set

    Set类型不保存重复的元素。判断对象元素是否相等采用的是equals方法,因此在存入自定义的对象时,若是重写equals方法依赖于可变属性,将会致使一些问题。线程

 

五 Map

    map类型是可以将对象映射到其余对象的一种容器,有区别于list的get方法。hashset类中包含了一个hashmap对象,hashset的实现依靠hashmap。

    hashmap的实现采用了数组链表的方式,即数组的每个位置都存放的是链表头。查找会先经过key的hash找到对应数组下标,再在该数组下标所对应的链表中找到是否有对应对象,查找方式为equals方法。

 

六 Queue

    队列是一种典型的先进先出的容器,LinkedList实现了Queue接口。PriorityQueue实现了优先级队列。ArrayDeque是一个用数组实现双端队列的类,咱们来看一下ArrayDeque类中的一些方法。

    public ArrayDeque() {
        elements = (E[]) new Object[16];
    }    
    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }    
    private void allocateElements(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    elements = (E[]) new Object[initialCapacity];
}

上边的代码是ArrayDeque的构造方法,能够看到,当没有定义大小时,ArrayDeque默认数组大小为16,而定义大小后,会调用allocateElements方法,这个方法的做用是:当给定长度小于最小长度8时,使用最小长度。若大于等于最小长度,则找到比给定长度大的最小的2的幂数。为何要是2的幂数呢?缘由有如下两点:

  1. 操做系统分配内存的方法使用伙伴系统的话,每一块的大小都是2的幂数,若是分配的内存大小为2的幂数,能够减小内存分配的时间。伙伴系统在百度百科中的解释:http://baike.baidu.com/view/4935190.htm

  2. 在ArrayDeque的addFirst方法中不固定将头放在数组的第一位,而是循环移位。使用2的幂数可以有效判断头部所在的地址。

  3. 一样在第二点中,若是队列满了,数组扩充是将容量capacity值左移一位便可扩充一倍。

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = (E[])a;
    head = 0;
    tail = n;
}

 

七 List的选择

    在文章开头提出了一个问题,数组实现的List快仍是链表实现的List快。模拟一下试试:   

public static void add()
{
    long start = 0;
    long end = 0;

    List<Integer> alist = new ArrayList<>();
    List<Integer> llist = new LinkedList<>();

    System.out.println("ArrayList添加1000万数据所需毫秒数");
    start = System.currentTimeMillis();
    for (int i=0; i<10000000; i++)
    {
        alist.add(i);
    }
    end = System.currentTimeMillis();
    System.out.println(end-start);

    System.out.println("LinkedList添加1000万数据所需毫秒数");
    start = System.currentTimeMillis();
    for (int i=0; i<10000000; i++)
    {
        llist.add(i);
    }
    end = System.currentTimeMillis();
    System.out.println(end-start+"\n");

    System.out.println("ArrayList从1000万数据删除数据所需毫秒数");
    start = System.currentTimeMillis();
    alist.remove(0);
    alist.remove(2000000);
    alist.remove(4000000);
    alist.remove(6000000);
    alist.remove(8000000);
    alist.remove(9999994);
    end = System.currentTimeMillis();
    System.out.println(end - start);

    System.out.println("LinkedList从1000万数据删除数据所需毫秒数");
    start = System.currentTimeMillis();
    llist.remove(0);
    llist.remove(2000000);
    llist.remove(4000000);
    llist.remove(6000000);
    llist.remove(8000000);
    llist.remove(9999994);
    end = System.currentTimeMillis();
    System.out.println(end - start+"\n");

    System.out.println("ArrayList从1000万数据查找数据所需毫秒数");
    start = System.currentTimeMillis();
    alist.contains(0);
    alist.contains(2000000);
    alist.contains(4000000);
    alist.contains(6000000);
    alist.contains(8000000);
    alist.contains(10000000);
    end = System.currentTimeMillis();
    System.out.println(end - start);

    System.out.println("LinkedList从1000万数据查找数据所需毫秒数");
    start = System.currentTimeMillis();
    llist.contains(0);
    llist.contains(2000000);
    llist.contains(4000000);
    llist.contains(6000000);
    llist.contains(8000000);
    llist.contains(10000000);
    end = System.currentTimeMillis();
    System.out.println(end - start+"\n");
}

    能够看到,不管在何种状况下,数组实现的list都比链表快。当我在ArrayList构造方法中设置数组初始大小1000万时,ArrayLIst添加数据的速度慢了下来,降到5000毫秒左右,因此通常状况下不须要优化。

 

总结 

简单容器类图:

    Java的容器分为两类,一类是Collection,一类是Map。collection中包含三种集合类型:Set,List,Queue。

    若是想要set中的数据有序,请使用TreeSet。

    HashTable和Vector是线程安全的,可是不建议使用,请使用java.util.concurrent包下的容器。

    HashMap容许key/value值为null。

 

更多文章:http://blog.gavinzh.com

相关文章
相关标签/搜索