Java集合总结
2021/5/8 1:25:13
本文主要是介绍Java集合总结,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
最近复习了一下数据结构,对数据结构有了更深了解,回头再来看一下集合相关知识就感觉豁然开朗,面试中集合也是必考题,便有了这篇集合总结,其中HashMap(包括部分源码分析)篇幅大概有6000+字,希望大家能耐心看完,看完后多少都会有一些收获。
数据结构
先来简单复习一下集合相关的数据结构。
一、数据结构的分类:
1.数据结构包括:逻辑结构和物理结构,其中逻辑结构稍复杂一些;
2.逻辑结构包括:线性结构(如 顺序表、栈、队列)和非线性结构(如 树、图);
3.物理结构包括:顺序存储结构(如 数组)、链式存储结构(如 链表)。
二、集合数据结构:
1. 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n);
2.链表:对于链表的新增,删除等操作,仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n);
3.二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn);
4.哈希表:在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。
数据结构本篇不做详细说明,后续会有专题写相关的数据结构及算法。
集合框架图
Java 集合,也称作容器就是用来存放数据的,主要是由两大接口 派生出来的Collection 和 Map,具体请看下图:
Collection
collection主要存储单值类型的数据,主要的子接口有 list、set和queue(队列本篇不做介绍)。
操作集合,无非就是「增删改查」四大类,也叫 CRUD,这里对集合API就不做介绍了。
List
主要特点是 元素有序且元素可重复。
面试题:ArrayList、LinkList和Vector的区别?
ArrayList 底层是数组,查询效率是O(1) ,新增和删除效率是O(n),查询效率高,线程不安全;
LinkList 底层是双向链表,查询效率是O(n),新增和删除效率是O(1),新增和删除效率高,线程不安全;
Vector 底层是数组结构,属于线程安全集合,使用频率较低。
Set
主要特点是 元素无序且元素不可重复。
面试题:HashSet和TreeSet的区别?
HashSet :
1. 数据结构 底层是HashMap实现;
2. 顺序性 不能保证元素的排列顺;
3. null元素 元素可以为null,但只能存放一个null元素;
4. 时间复杂度 add(),remove(),contains()方法的时间复杂度是O(1)。
TreeSet:
1. 数据结构 底层是treeMap(红黑树)实现;
2. 顺序性 元素是自动排好序的;
3. null元素 不能存放null元素;
4. 时间复杂度 add(),remove(),contains()方法的时间复杂度是O(logn)。
Map
主要存放键值对数据,本篇会重点介绍面试率极高的hashmap。
HashMap
JDK1.7 Hashmap由数组和链表组成,JDK1.8做了主要做了2处有优化: 1. 数据结构变成了数组、链表和红黑树,解决是链表太长查询效率低问题;2. 在哈希冲突时头插法变成了尾插法,解决是在hashmap扩容时形成环形链表问题。
以下篇幅介绍的都是JDK1.8的hashmap。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ... }
HashMap数据结构
JDK 1.8 hashMap的数据结构图:
扩容条件
先看一下hashMap源码中几个关键的字段:
/**实际存储的key-value键值对的个数*/ transient int size; /**阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后, threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到*/ int threshold; /**负载因子,代表了table的填充度有多少,默认是0.75 加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。 所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。 */ final float loadFactor; /**HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时, 如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作), 需要抛出异常ConcurrentModificationException*/ transient int modCount;
hashMap条件:
- 当集合容量超过了阈值(threshold)就会进行扩容;
- 当链表长度>=8且数组长度小于64时,也会扩容。
/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. * 新增数据时链表长度大于8时会进行树化 */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //树化时 判断 数组为空 或 数组长度 < MIN_TREEIFY_CAPACITY =64 则直接扩容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
扩容机制
- 扩容:创建一个新的Entry空数组,长度是原数组的2倍;
//hashmap扩容方法 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //原数组不为空 则原数组容量为 table.length int oldCap = (oldTab == null) ? 0 : oldTab.length; //原数组阈值 int oldThr = threshold; //新数组 容量和阈值 都默认为 0 int newCap, newThr = 0; //原数组容量大于0 if (oldCap > 0) { //原数组容量 大于等于 最大容量 MAXIMUM_CAPACITY = 1 << 30 则直接返回 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 } ...
- 计算hash:index = HashCode(Key) & (Length - 1);
- ReHash: 由于新数组的长度变了,则需要遍历原Entry数组,重新计算hash把所有的Entry重新Hash到新数组。
小结: 可见在用hashMap时最好先计算好hashMap的容量,初始化带上容量大小,毕竟扩容是非常消耗性能的。
树化条件
树化必须同时满足2个条件:
- 链表长度>=8 (binCount >=7 因为binCount 是从0开始算) ;
//binCount 从0开始自增 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //当 binCount == 7时 则树化 并跳出循环 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; }
- 数组长度>=64(具体看以上treeifyBin源码分析);
数据查询
/** * * @param hash 需要被获取元素的hash值 * @param key 需要被获取的元素 * @return 返回被需要到的元素,没有获取到则返回null */ final HashMap.Node<K, V> getNode(int hash, Object key) { //临时变量储存table数组 HashMap.Node[] tab; //临时变量获取第一个元素 HashMap.Node first; //n为table的长度 int n; // first = tab[n - 1 & hash]) 计算数组下标 获取到数组上第一个node 且 node的key就是要查询key 则直接返回 node数组 if ((tab = this.table) != null && (n = tab.length) > 0 && (first = tab[n - 1 & hash]) != null) { Object k; //first元素存在,切first元素即锁需要查找的元素,直接返回first. if (first.hash == hash && ((k = first.key) == key || key != null && key.equals(k))) { return first; } //数组上的node不是查询目标且指向下一个node不为空 HashMap.Node e; if ((e = first.next) != null) { //判断node是否为红黑树 if (first instanceof HashMap.TreeNode) { //按照红黑树方式去遍历 return ((HashMap.TreeNode)first).getTreeNode(hash, key); } //如果链表没有被树化,则使用链表的方式查询. do { //循环判断当前的临时变量e是否与所需元素相同 if (e.hash == hash && ((k = e.key) == key || key != null && key.equals(k))) { return e; } } while((e = e.next) != null); } } return null; }
通过分析源码查询过程可以分为3部分:
- 计算hashCode 获取到数组上的node,且node.key==key则数组上的就是就是查询目标;
- 是链表结构则按照链表方式遍历查询目标;
- 是红黑树结构则按照红黑树方式遍历查询目标。
数据存储
先上源码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 如果存储元素的table为空,则进行必要字段的初始化 默认为16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果根据hash值获取的node为空,则直接新增 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 如果新插入的结点和table中p结点的hash值,key值相同的话 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 { //如果不是红黑树则为链表按照链表方式插入 binCount从0开始自增 for (int binCount = 0; ; ++binCount) { // 代表这个单链表只有一个头部结点,则直接新建一个结点即可 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 当binCount>=7 即 链表长度>=8时,将链表转红黑树 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 p = e; } } // 如果存在这个映射就覆盖 if (e != null) { // existing mapping for key V oldValue = e.value; // 判断是否允许覆盖,并且value是否为空 if (!onlyIfAbsent || oldValue == null) e.value = value; // 回调以允许LinkedHashMap后置操作 afterNodeAccess(e); return oldValue; } } ++modCount; // 如果容量超过阈值则进行扩容 if (++size > threshold) resize(); // 回调以允许LinkedHashMap后置操作 afterNodeInsertion(evict); return null; }
从以上源码分析hashMap数据存储也可简单分为4部分:
- 在数组上计算hashCode的node为空则直接新增;
- 获取的node不为空,如果是红黑树就按照红黑树方式插入;
- 获取的node不为空,如果是链表则按照链表方式插入(会触发树化或扩容操作);
- 新增成功后,还需要判断数组容量是否有超过阈值,超过则需要扩容。
面试题:HashMap负载因子是多少?为什是这么多?
默认为0.75,这个是在时间和空间之间平衡的一个数值,负载因子是可以自定义的(不推荐)
面试题:HashMap和HashTable区别
- 初始化容量 HashMap初始容量为16,HashTable初始容量为11(负载因子都相同);
- 线程安全 HashMap为非线程安全,HashTable为线程安全(get和put方法都用synchronized修饰,效率较差);
- 扩容容量 HashMap扩容时容量:capacity2,HashTable扩容时容量:capacity2+1
- 遍历方式 HashMap仅支持Iterator的遍历方式,Hashtable支持Iterator和Enumeration两种遍历方式;
- null值存储 HashMap中key和value都允许为null,HashTable在遇到null时,会抛出NullPointerException异常。
ps:一般问到hashMap就一定会问到currentHashMap,由于篇幅较长了下次会单独写一篇currentHashMap。
总结思维导图
最后奉上自己总结集合的思维导图,希望能帮助大家。
既然都看到最后了请帮忙一键三连哦!
这篇关于Java集合总结的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27本地多文件上传的简单教程
- 2024-11-27低代码开发:初学者的简单教程
- 2024-11-27如何轻松掌握拖动排序功能
- 2024-11-27JWT入门教程:从零开始理解与实现
- 2024-11-27安能物流 All in TiDB 背后的故事与成果
- 2024-11-27低代码开发入门教程:轻松上手指南
- 2024-11-27如何轻松入门低代码应用开发
- 2024-11-27ESLint开发入门教程:从零开始使用ESLint
- 2024-11-27Npm 发布和配置入门指南
- 2024-11-27低代码应用课程:新手入门指南