Java中Collection和Map框架详解

一. Collection框架

Collection是所有集合的顶级接口,包括常用的一些集合:如List、Set、Queue。下面详细介绍Collection框架体系。
转自https://blog.csdn.net/qq_38366063/article/details/91817119

0. Collection

List, Set, Queue的上层接口,但注意Map并没有依赖于Collection实现。

方法名说明
int size()返回元素数量
boolean isEmpty()判断集合是否为空
boolean contains(Object)判断是否包含元素
boolean containsAll(Collection<?>)判断是否包含输入集合内的所有元素
Iterator iterator()返回集合的迭代器
Object[] toArray()、: T[] toArray(T[])将元素转化为数组
boolean add(E)、boolean addAll(Collection<?>)添加元素
boolean remove(Object)、boolean removeAll(Collection<?>)移除元素
void clear()清空集合
boolean retainAll(Collection c)两个集合交集的元素给原集合,并判断原集合是否改变,改变则true,不变则false

1. List

List是接口,不能被实例化。主要有三个实现类:ArrayList, Vector, LinkedList。
List是一个有序并且可重复的集合,集合中每一个元素都有对应的顺序索引。
List接口对Collection接口进行了扩展,在Collection原有的方法基础上增加了以下方法。

方法名说明
void replaceAll(UnaryOperator)用函数接口的返回结果替代原list中的值 [2]
sort(Comparator<? super E>)根据自定义Comparator规则对元素进行排列 [3]
E get(int)根据index获取元素
E set(int, E)将元素E分配给索引指定的位置
void add(int, E)将元素插入到索引位置。插入点以及之后的元素依次向后移动,不会覆盖任何元素
boolean addAll(int, Collection<? extends E>)在int索引处插入集合Collection中所有元素,插入点以及之后的元素依次向后移动,不会覆盖任何元素。如果调用列表更改则返回true,否则返回false
E remove(int)删除位置索引处的元素,并返回已删除的元素。结果列表是压缩的,也就是说,后续元素的索引减1
int indexOf(Object)返回第一个object的索引。如果object不是列表的元素,则返回-1
int lastIndexOf(Object)返回调用列表中最后一个object的索引。如果object不是列表的元素,则返回-1
ListIterator listIterator()返回调用列表开头的迭代器
ListIterator listIterator(int)返回从指定索引开始的迭代器
List subList(int, int)返回从int_1到int_2 -1的元素列表

1.1 ArrayList

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,ArrayList适合随机查找和遍历,不适合插入和删除。

部分源码

	transient Object[] elementData; //数组实现

	//默认数组大小为10
	public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

	//填入initialCapacity改变数组默认大小
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

1.2 LinkedList

LinkedList 是用链表结构存储数据的,适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
   //不仅实现了List接口,还实现了Deque接口(Deque继承了Queue接口),所以可以用来实现队列等
{
	...

    transient Node<E> first; //链表实现
    transient Node<E> last;
    
    ...
}

1.3 Vector

Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要高花费,因此,访问它比访问 ArrayList 慢。以及Vector 包含了许多传统的方法,这些方法不属于集合框架。
Vector是通过用synchronized修饰方法来支持线程同步的。
对比ArrayList和Vector的源码中的add方法:

//ArrayList
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
//Vector
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

2. Set

和 List 不同,Set是一个无序、且元素不能重复的集合。假设两个元素 e1 和 e2,如果 e1.equals(e2),它们是不能在 Set中同时存在的。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。

2.1 HashSet

HashSet按照哈希值来存取元素。HashSet首先判断两个元素的哈希值,如果一样则比较equals,如果都一样则视为同一个元素;如果equals为false则视为不同元素。但HashSet仅仅按照哈希值存取元素,这就意味着一个哈希值的位置上可以存取多个元素。(哈希值相同,但equals为false)
通过HashSet的构造器可以发现,它是由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<>();
    }

但是我们可以想象得到,Set中不允许元素的重复,Map中不允许元素的key重复,这是不是意味着Set其实可以理解为没有value的Map呢? 这样想想,HashSet底层是由HashMap实现也就可以理解了。
看一段源码:

    private static final Object PRESENT = new Object();
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

这是HashSet中的add(E e)方法,可以预见的,它调用了map的put(key,value)方法,key就是我们要add的元素,value不是null,而是一个通用的白板Object对象PRESENT

2.2 TreeSet

TreeSet是一个树形结构的集合,底层实现同样是用了Map家族中的TreeMap,先看后面的TreeMap再回来看TreeSet就比较容易理解了。
TreeSet会按照一定的规则对插入进来的元素进行排序,来维护树的有序性。(红黑树——一种自平衡二叉搜索树)

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

2.3 LinkedHashSet

同样可以预见的,LinkedHashSet的底层使用了LinkedHashMap。( ̄Д ̄)ノ
LinkedHashSet继承于HashSet(同LinkedHashMap于HashMap关系类似)。具体内容直接往下拉看LinkedHashMap就好,这里就不重复赘述了。

3. Queue

通常来说队列就是一个先入先出(FIFO)的数据结构。

方法名说明
boolean add(E e)增加一个元索 成功返回true 如果队列已满,则抛出一个IllegalStateException异常
boolean offer(E e)添加一个元素并返回true 如果队列已满,则返回false
E remove()检索并删除此队列的head。此方法与poll不同之处在于,如果此队列为空,它将引发异常。
E poll()同样为删除head,如果队列为空,则返回null
E element()检索但不删除此队列的head。此方法与peek不同之处在于,如果此队列为空,它将引发异常。
E peek()返回队列头部的元素。如果队列为空,则返回null

Queue框架
我们发现Queue接口下面也是很庞大了,由于篇幅已经很长了,这里只简单介绍一下队列的普通实现Deque接口,其他例如PriorityQueue以及一些阻塞队列放到下一篇来写。

3.1 Deque

Deque对Queue的方法进行了扩展,实现了一个双向队列,意味着它也可以当作栈来使用。

方法名说明
void push(E)向队列头部插入一个元素,失败时抛出异常
void addFirst(E)向队列头部插入一个元素,失败时抛出异常
void addLast(E)向队列尾部插入一个元素,失败时抛出异常
boolean offerFirst(E)向队列头部加入一个元素,失败时返回false
boolean offerLast(E)向队列尾部加入一个元素,失败时返回false
E getFirst()获取队列头部元素,队列为空时抛出异常
E getLast()获取队列尾部元素,队列为空时抛出异常
E peekFirst()获取队列头部元素,队列为空时返回null
E peekLast()获取队列尾部元素,队列为空时返回null
boolean removeFirstOccurrence(Object)删除第一次出现的指定元素,不存在时返回false
boolean removeLastOccurrence(Object)删除最后一次出现的指定元素,不存在时返回false
E pop()弹出队列头部元素,队列为空时抛出异常
E removeFirst()弹出队列头部元素,队列为空时抛出异常
E removeLast()弹出队列尾部元素,队列为空时抛出异常
E pollFirst()弹出队列头部元素,队列为空时返回null
E pollLast()弹出队列尾部元素,队列为空时返回null

Deque两个实现类ArrayDeque和LinkedList,LinkedList是链表实现,在上文已经有提到。这里主要看一下ArrayDeque的实现。

3.1.1 ArrayDeque

与LinkedList不同,ArrayDeque的底层是数组实现,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,通常来说,性能比LinkedList要好。

	//数组实现
	transient Object[] elements;
	//默认数组初始长度16
	public ArrayDeque() {
        elements = new Object[16];
    }
    //构造一个能容纳指定大小的空队列
    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }

ArrayDeque分别用两个变量head和tail作为指针指向数组的头和尾。

    transient int head;
    transient int tail;

因为是双向队列,所以可以在头和尾In或Out,看一段源码:

   public void addLast(E e) {
       if (e == null)
           throw new NullPointerException();
       elements[tail] = e;
       if ( (tail = (tail + 1) & (elements.length - 1)) == head)
           doubleCapacity();
   }

在尾部添加一个元素e:elements[tail] = e;
主要看这句:
if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity();
这是一个类似于HashMap中的取模的操作,因为长度规定是2的幂,是二进制整数,所以length-1则是全是1的掩码,那么如果tail+1溢出了(等于length),这样计算的结果是0,变成了一个循环数组。当tail和head重合的时候,说明数组已经满了,则会对数组扩容:doubleCapacity()。其他的方法也是类似。
ArrayDeque通常来说比LinkedList更高效,因为可以在常量时间通过index对元素进行定位,并且省去了对元素进行包装的空间和时间成本。

二. Map框架

在这里插入图片描述

1. Map

方法名说明 [5]
int size()返回此映射中的键-值映射关系数
boolean isEmpty()如果此映射未包含任何键-值映射关系,则返回 true
boolean containsKey(Object)如果此映射包含指定键的映射关系,则返回 true
boolean containsValue(Object)如果此映射将一个或多个键映射到指定值,则返回 true
V get(Object)返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null
V put(K,V)将指定的值与此映射中的指定键关联
V remove(Object)如果存在一个键的映射关系,则将其从此映射中移除
void putAll(Map<? extends K, ? extends V>)从指定映射中将所有映射关系复制到此映射中
void clear()从此映射中移除所有映射关系
Set keySet()返回此映射中包含的键的Set表
Collection values()返回此映射中包含的值的 Collection 集合
Set<Entry<K,V>> entrySet()返回此映射中的键值对集合
boolean equals(Object)判断两个Map是否相等,实际上是比较m1.entrySet().equals(m2.entrySet())是否为true
int hashCode()返回map的哈希值,map的哈希值是由map中的所有entry的哈希值的加和而得的,这样确保了如果m1.equals(m2)那么m1.hashCode()==m2.hashCode()

1.1 HashMap

基于Map接口实现。允许key和value的空值。不支持线程同步,不保证元素顺序排列,同样不保证元素顺序随时间不变。
HashMap有两个影响其性能的参数:初始容量(initial capacity)和负载因子(load factor)。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

初始容量(table的初始长度)默认值是默认是16
负载因子默认是0.75
当阈值超过当前容量*负载因子的时候,容器将自动扩容并且重新哈希,扩容后的容量是之前的两倍。
初始容量和负载因子也允许修改,但默认的负载因子值0.75是对空间和时间效率的一个平衡选择。对于插入元素较多的场景,可以把初始容量适当增大以减少重新哈希的次数。

Node<K,V>类型的变量table是HashMap中存储数据的变量。

	 transient Node<K,V>[] table;

Node是HashMap的内部类,它实现了Map.Entry接口:

    static class Node<K,V> implements Map.Entry<K,V> {
       final int hash;
       final K key;
       V value;
       Node<K,V> next;
     }

那么HashMap如何把一个Node存进table里面呢?或者说如何通过K寻找到table对应的index呢?[6]
在这里插入图片描述
1)首先获取Key的哈希值。
2)对哈希值再次进行哈希运算,避免出现一个很差的哈希算法,把所有的数据放到内部数组的同一个链表里。
3)对再次哈希的哈希值进行数组长度(最小为1)的位掩码运算,这个运算保证生成的索引不会比数组的长度大。

经过这三步就确定了一个Node的index,那么如果有几个不同的Node经过计算获得的index值相同,也就是说不同的Node需要放在table数组中的同一个位置,这个时候怎么办?

在JDK 1.8之前,table的数据结构类型为“链表散列”(数组中每一个元素为一个链表),也就是说table中的每一个位置都储存一个链表的head,我们可以看到之前Node类源码中有一个属性Node<K,V> next;可以使Node实现链表功能。
在这里插入图片描述
当向HashMap中put(key,value)时,通过上面三步计算出位置索引为i,将其放入到Node[i]中,如果这个位置上面已经有元素了,那么就将新加入的元素放在链表的头上,最先加入的元素在链表尾。显而易见,新加入的节点放在链表的head而不是尾部可以避免多余的搜索操作。

在JDK 1.8之后,Node类可以被TreeNode继承,这样的数据类型使得在table中存储的元素既可以是链表也可以是树
对于HashMap来说:

  • 如果一个内部表的索引超过8个节点,链表会转化为红黑树
  • 如果内部表的索引少于6个节点,树会变回链表
    这种变化避免了当链表过长而导致的巨大开销,因为对于链表的搜索时间复杂度为O(n),而红黑树的搜索复杂度为O(LogN)。红黑树相关原理就不在这里阐述了。
    在这里插入图片描述

1.2 HashTable

HashTable使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低。因为当一个线程访问HashTable的同步方法,其它线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法获取元素,所以竞争越激烈效率越低。因此在新的工程中不推荐使用HashTable
HashTable在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么多线程访问容器里不同的数据段时,线程间不会存在竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术(JDK 1.8之前)。首先将数据分成一段一段地储存,然后给每一段配一把锁,当一个线程占用锁访问其中一段数据时,其它段的数据也能被其它线程访问。下面讲解ConcurrentHashMap。

1.3 ConcurrentHashMap

在JDK1.8之前,ConcurrentHashMap使用分段锁的方式存储数据,每一段叫做一个Segment,而每一个Segment的结构与JDK1.7 中HashMap的存储结构是一样的——链表散列。
ConcurrentHashMap JDK1.7结构
对于ConcurrentHashMap,它的主要的构造器需要传入三个参数:initialCapacity、loadFactor、concurrencyLevel。前两个参数与HashMap中类似,concurrencyLevel代表Segment的个数。
默认情况下,initialCapacity等于16,loadFactor等于0.75,concurrencyLevel等于16。
由于存在Segment,ConcurrentHashMap每次进行插入或者查找的时候就需要先定位到key所在的Segment段,然后再定位到对应的Entry。

对于JDK 1.8之后的ConcurrentHashMap,同样引入了红黑树的结构。但是并没有继续使用1.8之前的Segment+分段锁的模式,而是类似JDK 1.8及以后的HashMap的数据结构——数组+链表+红黑树。而锁则采用CAS和synchronized实现。这种方法免除了最大并发数受到Segment个数限制。

CAS(Compare And Swap,比较交换):
CAS有三个操作数,内存值V、预期值A、要修改的新值B,当且仅当A和V相等时才会将V修改为B,否则什么都不做。

1.4 TreeMap

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序, 也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。
在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator。
TreeMap 的底层数据结构实现是通过红黑树实现的,每个 Entry 都被当成“红黑树”的一个节点对待。当 TreeMap 添加或者查找元素时,需要通过循环找到对应的 Entry 的位置,消耗较高,但好处是TreeMap 中的所有 Entry 总是按 key 根据指定排序规则保持有序状态。

1.5 LinkedHashMap

LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
LinkedHashMap中的Entry结构继承于HashMap中Node结构,区别是定义了两个变量:before,after。显而易见这种结构使得LinkedHashMap可以再HashMap原有的结构上再将所有的Entry维护成一个双向链表,链表的顺序就是插入的顺序。

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

在这里插入图片描述
在构造LinkedHashMap的时候,可以在构造器中配置参数决定是否按照访问次序排序。
默认情况按照插入次序排序,比如依次插入Entry1,Entry2,Entry3,那么插入完成之后,链表的情况是:(双向链表)
EntryHead < — > Entry1 < — > Entry2 < — > Entry3
那么使用迭代器遍历得到的顺序也就是Entry1->Entry2->Entry3

但是如果按照访问次序排序,所谓按照访问次序排序就是按照调用get方法的次序排序。比如在上面的链表建立完成之后,调用了map.get(entry1.key),在调用过后Entry1就会被挪到链表的末尾:
EntryHead < — > Entry2 < — > Entry3 < — > Entry1
这时候再使用迭代器遍历的结果就是Entry2->Entry3->Entry1

终于写完鸟QAQ
Reference:

[1] https://blog.csdn.net/vking_wang/article/details/16965853
[2] https://blog.csdn.net/Atimynyc/article/details/82851004
[3] https://blog.csdn.net/Atimynyc/article/details/82853952
[4] https://www.yiibai.com/java/java_list_interface.html
[5] https://www.runoob.com/java/java-map-interface.html
[6] https://mp.weixin.qq.com/s/SiHedmstpeA8BwCyCW9m7w
[7] https://blog.csdn.net/zzt46245/article/details/59575478
[8] https://blog.51cto.com/14220760/2364724
[9] https://blog.csdn.net/bill_xiang_/article/details/81122044
[10] https://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html
[11] https://www.jianshu.com/p/8f4f58b4b8ab
[12] https://blog.csdn.net/qq_42914528/article/details/83819292
[13] https://www.cnblogs.com/bushi/p/6681543.html
[14] https://www.cnblogs.com/mfrank/p/9600137.html

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值