Java 集合详解 (超详细)

目录

Java集合是什么?

Java集合的分类 

Collection接口

Map接口

Collection子接口--List接口

ArrayList(JDK 8为例)

LinkedList

Vector

Collection子接口--set接口

HashSet

LinedHashSet

TreeSet

Collection中的常用的方法

List接口中常用的方法

Set接口中常用方法:

Map接口

HashMap(JDK 8为例)

LinkedHashMap

Hashtable

Properties

TreeMap

Map接口中常用的方法

Collections


  • Java集合是什么?

集合可以说是一种保存对象的容器,他可以将我们需要的多个对象进行存储,来方便我们对多个对象可以进行的一些操作。我们最先接触的Java容器时数组,但是为什么现在又要接触集合?这就要说明二者的区别了。我们在声明一个数组的时候,就需要确定数组的长度和数组所能存放元素的类型,然后才可以进行其他操作,这样数组的局限性也就体现出来了:当我们存储的数据在增多并且超过我们原先声明的长度时,我们需要扩展数组的长度才可以继续往里添加数据;而且数组中提供给我们操作数据的方法比较少;最后就是数组存储的数据只能是有序可以重复的,数据的特点比较单一,无法应对现在我们的复杂需求。所以集合就出现了,他可以很好的解决数组存在的问题。

  • Java集合的分类 

Java集合分为俩类:单列数据集合Collection和双列数据集合Map。在这俩个接口下又有他们具体的实现类,我们可以根据自己的具体业务需求来创建适合我们自己的集合。

Collection接口

  • List接口:存储有序、可重复的数据。
    • ArrayList(实现类)、LinkedList(实现类)、Vector(实现类)
  • set接口:存储无序、不可重复的数据。
    • HashSet(实现类)、LinkedHashSet(实现类)、TreeSet(实现类)

Map接口

  • HashMap(实现类)
    • LinkedHashMap(实现类)
  • TreeMap(实现类)
  • HashTable(实现类)
    • Properties(实现类)

Collection子接口--List接口

List是用来存储有序、可重复的数据。他的三个实现类分别的ArrayList、LinkedList、Vector。List集合我们常常可以认为是“动态数组”的概念,因为其底层实现也是数组,但是增加了自动扩容的机制。

ArrayList(JDK 8为例)

ArrayList作为List的主要实现类存在,它的底层是用一个object类型的数组来存储数据。(下图为ArrayList源码)

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

在我们new一个ArrayList时,集合初始化的时候,ArrayList不会先创建这个集合。(下图为ArrayList源码)


    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

而是等我们调用add()方法对数据进行添加到时候,ArrayList才会去为我们创建一个长度为10的集合。(下图为ArrayList源码)

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

ArrayList线程不安全,但是带来的好处就是效率高。

当添加的数据大小超过初始化大小,ArrayList会进行自动扩容操作,然后将旧的数据拷贝到新扩容好的集合中,在进行add新数据。默认情况下,集合会扩容之前集合大小的1.5倍。(下图为ArrayList源码)

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    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);
    }

LinkedList

LinkedList底层实现是使用双向链表实现,在初始化时,不仅创建一个数据节点,还维护着节点俩端的一对指针,指向该数据节点的前一个和后一个数据节点。

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

在调用add()方法添加数据时,LinkedList需要从链表的最后一个元素开始的后面开始添加,将最后一个元素的尾指针指向添加的元素,然后将添加的元素的头指针指向链表最后的元素,即可完成添加操作。

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    /**
     * Links e as last element.
     */
    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更适合做插入和删除操作,只需要将元素的指针移动即可完成操作,但是对于查询操作,也需要从头开始遍历,效率反而不如ArrayList。

Vector

vector伴随着jdk 1.0而诞生,底层也是用object类型的数组对数据进行保存。

 * @author  Lee Boynton
 * @author  Jonathan Payne
 * @see Collection
 * @see LinkedList
 * @since   JDK1.0
 */
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    /**
     * The array buffer into which the components of the vector are
     * stored. The capacity of the vector is the length of this array buffer,
     * and is at least large enough to contain all the vector's elements.
     *
     * <p>Any array elements following the last element in the Vector are null.
     *
     * @serial
     */
    protected Object[] elementData;

Vector初始化时,默认创建一个长度为10的数组进行存储数据。

    /**
     * Constructs an empty vector so that its internal data array
     * has size {@code 10} and its standard capacity increment is
     * zero.
     */
    public Vector() {
        this(10);
    }

Vector底层方法都有synchronized关键字修饰,所以线程安全,但是效率比较低。

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

在扩容方面,Vector默认扩容原来数组长度的2倍。

Collection子接口--set接口

set集合存储无序、不可重复的数据。set中的无序性表现为非索引顺序,set中的元素需要根据自身的hashCode值进行计算元素所存放的位置。set中的不可重复性表现为添加元素.equals()返回为false才可以添加成功。

HashSet

hashSet作为set接口的主要实现类,其线程不安全,效率较高。新添加的元素与已在位置上的元素用链表形式存放,在jdk8中,hashSet底层是用数组+链表+红黑树

  • HashSet添加元素的过程(向set中添加数据时,需要将添加数据所在的类重写hashCode和equals方法

          在我们new一个HashSet的时候,底层其实为我们new了一个HashMap。(HashMap实现原理往下看

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

         在我们往HashSet中add数据时,底层其实将数据存放在了map中存放的key的位置,value的位置是一个常量,可以           理解为没有数据。 


    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
  1. 在添加数据时,首先会调用被添加元素的hashCode值计算其的hash值,然后通过底层的算法将该元素的存放位置进行计算得到

     2. 如果该索引位置上没有元素,就直接添加成功。

     3. 如果该索引位置上有其他元素(不同的hash值经过计算可能得到相同的索引位置),就比较其俩个的hash值,如果hash值不同则添加成功。

     4. 如果二者的hash值相同,就需要调用被添加元素的equals方法进行比较,返回false即可添加成功。

LinedHashSet

linkedHashSet是HashSet的子类,在HashSet的基础上增加了一对指针,遍历其内部数据时,可以按照添加的顺序遍历(这不能说明LinkedHashSet是存放有序的数据)。对于频繁遍历的操作,LinkedHashSet效率要高于HashSet。

TreeSet

TreeSet底层是用红黑树对数据进行存储,它的查询效率很高。存储时需要对指定对象的指定属性进行排序操作,所以treeSet中添加的数据必须是相同类的对象并且不可以添加相同的数据。TreeSet可以确保集合的元素处于一种排好序的状态。 

Collection中的常用的方法

  1. add
  2. addAll
  3. size
  4. isEmpty
  5. clear
  6. contains
  7. remove
  8. removeAll
  9. retainsAll
  10. equals

List接口中常用的方法

  1. add
  2. addAll
  3. get
  4. indexOf
  5. set
  6. remove
  7. subList
  8. size
  9. iterator

Set接口中常用方法:

Set接口中没额外定义新的方法,使用的都是Collection中声明过的方法。

Map接口

map存储的是双列数据,存储的是k-v键值对类型的数据。在map中保存的k-v对,key不可以重复并且无序,value可以重复,底层保存这对k-v使用的是Node对象来进行保存的。Node保存数据的特点是:无序、不可重复。在使用map存储数据时, map对象所在的类要重写equals()和hashCode()方法。

HashMap(JDK 8)

作为Map接口的主要实现类,线程不安全,但是效率比较高,可以存储null值的key和value。HashMap底层是利用数组+链表+红黑树进行存储。

1. 我们在new 一个HashMap的时候,底层没有立刻为我们创建出一个存放数据的数组,在jdk7中,底层是为我们创建了一个长度为16的Entry数组。

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

 2. 在我们往HashMap中首次put数据时,才会为我们创建一个长度为16的Node[ ]数组,在JDK 7中,为我们创建的是Entry[ ]数组。

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

3. 我们调用put传想要存放的数据,HashMap底层进入putVal方法中,如果首次添加,tab ==null, 就会进入resize()方法中,resize()方法中在首次就是为我们造好了长度为16的Node[ ]数组,同时也声明了一个newThr的临界值变量。

 

4. 如果数组不为空,不是第一次添加就不会进入resize()方法中,就会执行下面的操作。首先HashMap会对传入数据的key做一个运算,求出数据的key存放在数组中的位置,如果此位置上为null,也就是此位置上没有数据,所以就把该值存放在此位置上,即添加成功。

5. 如果不为null,则表明存放该数据的位置上已经有值,HashMap底层会将待存储的数据与已经在位置上的数据进行hash的比较, 如果hash值不同就要调用待存储数据的equals方法比较二者,如果返回结果为false就进入循环,继续比较在该位置上的其他元素,如果都返回false则按照链表的方式在该位置的链表末尾添加该数据,如果返回true就进入替换逻辑,将待存储数据的value替换旧的数据的value。

6. 在此期间还有一段逻辑,就是在你添加数据时会进行判断该位置上存在的数据节点的长度,如果长度大于8会将此位置保存数据的方式转变为 红黑树存储。

7. 在treeifyBin这个方法中我么可以看到,并不是链表长度到8就立马变为红黑树存储,另一个条件就是数组长度要大于64,考虑资源情况,可能数组长度不够64,可以使用扩容数组长度来达到数据平衡存储,所以底层选择扩容resize(),当两个条件都满足就会转变为红黑树存储。

 

8. 在我们不断地存储数据,总会有数据放不下的情况,所以HashMap也会面临自动扩容的问题,但是相对于ArrayList装满了才去扩容,HashMap选择了一个叫做临界值的常量来控制扩容时机:默认临界值=容量大小*加载因子=16*0.75 = 12。为什么会提前扩容?因为HashMap中的数据不是按顺序存放,而是随机放,所以可能某些位置始终没有数据,而某些位置链表或者树形结构很长,导致集合利用率不均匀,所以选择提前扩容,达到集合均匀的使用率。

 

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        return newTab;
    }

LinkedHashMap

在HashMap的基础上增加了一对指针,存储时根据元素key的hashCode值来决定元素存放的位置。在遍历时可以按照添加顺序进行遍历集合,因为每个数据元素都记录着自己的前后元素的位置。对于频繁遍历集合的操作,效率要比HashMap高。

LinkedHashMap是HashMap的子类,在添加数据时,也是调用父类中的putVal()方法进行添加操作。而LinkedHashMap重写了父类中的newNode()方法,在new一个数组的时候,LinkedHashMap维护了数据的一对前后指针。

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

Hashtable

Hashtable伴随着jdk 1.0 所诞生,他线程安全,但是效率不高,不可以存储null的key和value值。

Properties

是HashTable的子类,一般用来处理配置文件。properties中的key和value都是字符串类型的。

TreeMap

底层实现是使用红黑树,也就是排序二叉树。对于存储数据时,数据添加的位置需要根据自定义比较规则来判断数据key的大小,再由红黑树进行存储。

Map接口中常用的方法

  1. put
  2. putAll
  3. remove
  4. replace
  5. clear
  6. get
  7. containsKey
  8. containsValue
  9. size
  10. isEmpty
  11. equals

Collections

collections是一个工具类,用来操作Set,List,Map集合的。collections中提供了一些方法可以很高效的对集合中的元素进行操作,例如排序、查询、修改,反转等等操作。

collections常用方法:

  1. reverse
  2. shuffle
  3. sort
  4. swap
  5. min
  6. max
  7. list
  8. copy
  9. replaceAll

猜你喜欢

转载自blog.csdn.net/qq_45648512/article/details/106169920
今日推荐