java容器,面试必备知识点整理!(源码解读)

在码代码的过程中,我们常常需要对大量对象引用管理,为了有效的归类管理,将同类的引用放在一个数据容器中。

(如果对您的学习有所帮助记得点个赞喔)

容器主要由:Collection与Map两种构成。

一.概述

1.Collection

    包含三大类,set、list、queue。思维导图如下所示:

1.1 Set

TreeeSet:使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

HashSet: HashSet 通过 hashCode 值来确定元素在内存中的位置。一个 hashCode 位置上可以存放多个元素。

LinkedHashSet:是HashSet与LinkedHashMap的结合。底层使用 LinkedHashMap 来保存所有元素,其所有的方法操作上又与 HashSet 相同。

1.2 List

ArrayList:是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。

因此,它适合随机查找和遍历,不适合插入和删除。

Vector:也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList慢。

LinkedList:用链表结构存储数据的,适合动态插入与删除,随机访问、遍历较慢,此外提供了用于操作表头表尾的方法,可当作栈、队列、双向队列使用。

2.Map

    相关知识思维导图如下所示:

2.1 HashMap(数组+链表+红黑树)

    根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。

2.2 HashTable

很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分锁。

2.3TreeMap

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。

如果使用排序的映射,建议使用 TreeMap。

2.4 LinkedHashMap(记录插入顺序)

LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

二.深入理解

下面就几个重要的知识点深入讲解:

1、ArrayList

ArrayList是基于数组实现的,其默认大小为10,但存储的内容超出十个就将考虑扩容。

(1)、扩容

      扩容一般发生在添加元素时,有一个ensureCapacityInternal()方法保证容量足够,如果不够,就使用grow()方法进行扩容,扩容后变为原有容量的1.5倍。

public boolean add(E e) {    //元素添加方法

    ensureCapacityInternal(size + 1);  // 保证有足够的空间

    elementData[size++] = e;  //直接添加

    return true;

}

      在初次添加元素的时候需要做特别的处理,然后判断是否超过最大容量,判断在现有的size+1的基础上是否需要扩容,然后扩容,拷贝数组添加元素。

private void ensureCapacityInternal(int minCapacity) {

/*如果使用默认的构造参数的话,则选择minCapacity和DEFAULT_CAPACITY中较大的一个来作为最小的初始

容量, 大家可能奇怪,既然判断了初始化这一说,为何还会有传入的minCapacity 和DEFAULT_CAPACITY

比较这一说,是因为ensureCapacityInternal也会被addAll调用

*/

    if (elementData == DEFAULTCAPACITY_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);

    }

grow()代码实现扩容

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);//copy the data to new array

    }

(2)元素删除

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),所以这就是为什么说ArrayList 删除元素的代价是非常高的。代码就忽略了,较为简单的向前移位操作。

(3)序列化

        所谓的JAVA序列化与反序列化,序列化就是将JAVA 对象以一种的形式保持,比如存放到硬盘,或是用于传输。反序列化是序列化的一个逆过程。规定被序列化的对象必须实现java.io.Serializable这个接口,而我们分析的目标ArrayList同样实现了该接口。

        但在实际开发工作中我们并不需要所有元素都需要序列化,比如银行卡、密码等信息,同时数组是由transient修饰,默认不序列化,此时就需要对部分元素进行序列化。ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。(此处不展开讲、有兴趣可以自行查询相关知识)

(4).Fail-Fast

      modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

      在进行序列化或者迭代遍历等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

出现的场景:

          在单线程中:遍历的过程中进行修改(使用Iterator遍历)

        多线程中:一个线程修改另外一个线程的遍历的Collection

解决的方法:对对象加锁、使用java.util.concurrent包中的类来代替ArrayList和Hashmap来表达。

(5).线程安全问题

    ArrayList没有同步机制,所以是线程不安全的,有以下两种方式对其进行操作使其安全。

          a.使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

List<String> list = new ArrayList<>();

List<String> synList = Collections.synchronizedList(list);

b.可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

List<String>list=newCopyOnWriteArrayList<>();

注:CopyOnWriteArrayList()

核心思想:读写分离,写操作在复制数组进行、读操作在原数组进行,互相分离,互不干扰。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

public boolean add(E e) {

    final ReentrantLock lock = this.lock;

    lock.lock();

    try {

        Object[] elements = getArray();

        int len = elements.length;

        Object[] newElements = Arrays.copyOf(elements, len + 1);

        newElements[len] = e;

        setArray(newElements);

        return true;

    } finally {

        lock.unlock();

    }

}

final void setArray(Object[] a) {

    array = a;

}

优点:大大提高了读操作的性能,因此很适合读多写少的应用场景。

缺点:内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;

数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

结论:CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

2、Vector

(1)线程安全

实现与ArrayList类似,不过使用Synchronized进行同步。

public synchronized boolean add(E e) {

    modCount++;

    ensureCapacityHelper(elementCount + 1);

    elementData[elementCount++] = e;

    return true;

}

public synchronized E get(int index) {

    if (index >= elementCount)

        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);

}

(2)扩容

      Vector的构造函数与ArrayList有所不同,Vector 的构造函数可以传入 capacityIncrement 参数,它的作用是在扩容时使容量 capacity 增长 capacityIncrement。如果这个参数的值小于等于0,扩容时每次都令 capacity 为原来的两倍。默认情况下,capacityIncrement的值为0,也就是说默认情况下 Vector 每次扩容时容量都会翻倍。

public Vector(int initialCapacity, int capacityIncrement) {

    super();

    if (initialCapacity < 0)

        throw new IllegalArgumentException("Illegal Capacity: "+

                                          initialCapacity);

    this.elementData = new Object[initialCapacity];

    this.capacityIncrement = capacityIncrement;

}

扩容方法如下

private void grow(int minCapacity) {

    // overflow-conscious code

    int oldCapacity = elementData.length;

    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?

                                    capacityIncrement : oldCapacity);

    if (newCapacity - minCapacity < 0)

        newCapacity = minCapacity;

    if (newCapacity - MAX_ARRAY_SIZE > 0)

        newCapacity = hugeCapacity(minCapacity);

    elementData = Arrays.copyOf(elementData, newCapacity);

}

(3).与ArrayList对比

1.Vector是同步的,线程安全,ArrayList不安全。

2.同样是由于同步原因,开销就比 ArrayList 要大,访问速度更慢。

3.Vector 每次扩容请求其大小的 2 倍,ArrayList是1.5倍。

3、LinkedList

基于双向链表实现,使用 Node 存储链表节点信息。

private static class Node<E> {

    E item;

    Node<E> next;

    Node<E> prev;

}

transient Node<E> first;

transient Node<E> last;//每个链表存储了 first 和 last 指针

与ArrayList对比,可以归结为数组和链表的区别:

数组支持随机访问,但插入删除的代价很高,需要移动大量元素;

链表不支持随机访问,但插入删除只需要改变指针。

4、HashMap

数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。

数组:数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。

数组是静态分配内存,并且在内存中连续。

数组利用下标定位,时间复杂度为O(1)

数组插入或删除元素的时间复杂度O(n)

数组的特点是:寻址容易,插入和删除困难

链表:链表存储区间离散,占用内存比较宽松。

链表是动态分配内存,并不连续。

链表定位元素时间复杂度O(n)

链表插入或删除元素的时间复杂度O(1)

链表的特点是:寻址困难,插入和删除容易。

结合二者,哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间。

(1)HashMap的存储结构:

      包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。

transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> {

    final K key;

    V value;

    Entry<K,V> next;

    int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {

        value = v;

        next = n;

        key = k;

        hash = h;

    }

    public final K getKey() {

        return key;

    }

    public final V getValue() {

        return value;

    }

    public final V setValue(V newValue) {

        V oldValue = value;

        value = newValue;

        return oldValue;

    }

  ......

}

(2)拉链法解决哈希冲突

HashMap<String, String> map = new HashMap<>();

map.put("K1", "V1");

map.put("K2", "V2");

map.put("K3", "V3");

      上面代码为新建hashMap并插入过程,如下图所示;插入到同一个桶,使用头插法进行插入。

确定桶下标:

int hash = hash(key);

int i = indexFor(hash, table.length);

计算哈希值

final int hash(Object k) {

    int h = hashSeed;

    if (0 != h && k instanceof String) {

        return sun.misc.Hashing.stringHash32((String) k);

    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by

    // constant multiples at each bit position have a bounded

    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);

    return h ^ (h >>> 7) ^ (h >>> 4);

}

public final int hashCode() {

    return Objects.hashCode(key) ^ Objects.hashCode(value);

}

            确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方(这也就是为什么HashMap扩容每次都是2倍的原因),那么就可以将这个操作转换为位运算。

static int indexFor(int h, int length) {

    return h & (length-1);//位运算效果与取模操作一致,但性能更高。

}

/*位运算

y      : 10110010

x-1    : 00001111

y&(x-1) : 00000010

*/

//取模: y%x : 00000010

(3)扩容

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此查找的复杂度为 O(N/M)。

为了让查找的成本降低,应该使 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

void addEntry(int hash, K key, V value, int bucketIndex) {

    Entry<K,V> e = table[bucketIndex];

    table[bucketIndex] = new Entry<>(hash, key, value, e);

    if (size++ >= threshold)

        resize(2 * table.length);

}

扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时。

void resize(int newCapacity) {

    Entry[] oldTable = table;

    int oldCapacity = oldTable.length;

    if (oldCapacity == MAXIMUM_CAPACITY) {

        threshold = Integer.MAX_VALUE;

        return;

    }

    Entry[] newTable = new Entry[newCapacity];

    transfer(newTable);

    table = newTable;

    threshold = (int)(newCapacity * loadFactor);

}

void transfer(Entry[] newTable) {

    Entry[] src = table;

    int newCapacity = newTable.length;

    for (int j = 0; j < src.length; j++) {

        Entry<K,V> e = src[j];

        if (e != null) {

            src[j] = null;

            do {

                Entry<K,V> next = e.next;

                int i = indexFor(e.hash, newCapacity);

                e.next = newTable[i];

                newTable[i] = e;

                e = next;

            } while (e != null);

        }

    }

}

进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上,使用 hash%capacity 来确定桶下标,而每次扩容都是2的n次方,简化了重新计算的。

(4)比较:

A.HashMap和HashTable的区别

1.HashTable的方法是同步的,在方法的前面都有synchronized来同步,HashMap未经同步,所以在多线程场合要手动同步。

2.HashTable不允许null值(key和value都不可以) ,HashMap允许null值(key和value都可以)。

        3.HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。

4.HashTable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。

        5.HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

B.HashMap与HashSet的关系:

    1、HashSet底层是采用HashMap实现的:

public HashSet() {

map = new HashMap<E,Object>();

}

    2、调用HashSet的add方法时,实际上是向HashMap中增加了一行(key-value对),该行的key就是向HashSet增加的那个对象,该行的value就是一个Object类型的常量。

C.Hashtable 和 ConcurrentHashMap 的关系

主要区别就是加锁的粒度以及如何加锁,ConcurrentHashMap 的加锁粒度要比HashTable更细一点。将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

5.ConcurrentHashMap

ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。Segment 继承自 ReentrantLock。具体信息可以参考链接[https://zhuanlan.zhihu.com/p/31614308]讲的很生动,当然,有耐心可以去看看JUC源码。

hello.world:JVM基础知识大整合,面试看这一篇就够了!​zhuanlan.zhihu.com

如果对您有所帮助就点赞吧!最近找工作,欢迎大家一起交流。欢迎收藏点赞!

(文中部分图片源自书本、某大佬博客,侵删)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341