【Java集合】List,Map,Set-详细讲解_map,hasmap,list
文章目录
- Java 集合框架核心知识点总结
 - 
- 概念
 - 
- 数组和集合的区别
 - Collection 和 Collections 有什么区别
 
 - List
 - Map
 - 
- Map 的遍历方式
 - HashMap 的实现原理
 - 解决哈希冲突的方法有哪些
 - HashMap 是线程安全的吗,可能会出现哪些问题?
 - HashMap 的 put 方法的过程
 - HashMap 一般使用什么作为 key?为什么
 - 为什么 HashMap 使用红黑树而不是直接使用平衡二叉树
 - HashMap 的扩容机制
 - 说一说 HashMap 的负载因子
 - ConcurrentHashMap 是怎么实现的
 - HashTable 的实现原理
 - HashMap, HashTable, ConcurrentHashMap 的区别
 
 - Set
 - 
- Set 集合有什么特点,怎么实现的
 - 有序的 set 集合是什么?
 
 
 
Java 集合框架核心知识点总结
概念
数组和集合的区别
- 数组的长度是不可变的,一旦被创建,不能被修改。而集合的长度是可变的。
 - 数组中存储的元素可以是基本数据类型,也可以是对象。而集合中存储的元素只能是对象。
 - 数组可以通过下标直接访问到元素,而集合需要通过迭代器等方式来获取元素。
 
Collection 和 Collections 有什么区别
- Collection 是集合类的总接口,其中定义了一些通用的方法比如增加删除元素,
List,Set都是它的实现类。 - Collections 是 Java 提供的用来处理集合的工具包,位于 
java.util包中。 
List
List 中的几种实现,以及他们的不同
- ArrayList:它是由可变数组实现的,是线程不安全的。它的元素访问速度比较快,但是对元素的增加和删除速度比较慢。
 - LinkedList:基于双向链表实现的,也是线程不安全的。它的元素访问速度比较慢但是元素的增加和删除速度比较快。
 - Vector:类似与 
ArrayList也是可变数组,它是线程安全的。 - CopyOnWriteArrayList:它也是线程安全的,它通过对写操作进行加锁,保证了线程安全。对读操作没有加锁,这使得读写分离,可以在修改的同时不影响读操作。
 
List 可以一边遍历一边修改元素吗
这与遍历的方式有关:
- 
使用普通 for 循环遍历:可以直接修改。
for (int i = 0; i < list.size(); i++) { list.set(i, \"newValue\"); // 可以直接修改} - 
使用增强型 for 循环 (foreach):不可以直接修改,因为它的底层是基于迭代器实现的,强行修改可能会导致
ConcurrentModificationException。for (String item : list) { list.remove(item); // 可能抛出 ConcurrentModificationException} - 
使用迭代器遍历:可以使用迭代器的
remove(),set()方法来进行删除和修改操作。Iterator<String> iterator = list.iterator();while (iterator.hasNext()) { String item = iterator.next(); iterator.remove(); // 安全删除 // iterator.set(\"newValue\"); // 安全修改} 
ArrayList 线程不安全体现在哪里?
- 可能会出现 null 值。
ArrayList底层实现add方法是先判断当前位置是否超过容量大小,如果没有将值放入进去,然后将当前位置对应的变量size++。如果此时两个线程,线程1先判断当前size为9,未超过容量,则执行set操作,还没进行size++时就转换到线程二进行操作,同样判断当前位置为9,进行set操作。这时候两个线程再分别进行size++操作,这时候size=11,导致size=10这个位置为null。 - 可能越界:同样在执行 
add操作时,线程一执行完set操作后还没有进行size++,线程二就去判断是否超过容量,结果发现没有超过容量,但是还没有执行set操作就进入线程1进行size++,这时候线程二执行set操作就会发生越界。 - size 与 add 的数量不一致:这是由于 
size++这个操作本身就不是原子性的,它分为获取size值,将其进行+1,将新值赋值给size。可能线程1和线程2拿到一样的size然后互相覆盖掉了。 
ArrayList 的扩容机制
当内部数组容量不够时,会触发扩容机制。
- 计算扩容后数组的大小:新容量一般为旧容量的 1.5 倍。
 - 创建新数组:根据计算得到的大小创建新数组。
 - 数据复制:将原数组当中的数据复制到新数组当中。
 - 更新引用:将指向原数组的引用改为指向新数组。
 
Map
Map 的遍历方式
- 通过 
for-each和entrySet来进行遍历for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + \": \" + entry.getValue());} - 通过 
for-each和keySet来进行遍历for (String key : map.keySet()) { System.out.println(key + \": \" + map.get(key));} - 通过 
entrySet和迭代器来进行遍历Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); System.out.println(entry.getKey() + \": \" + entry.getValue());} - 通过 
forEach方法和Lambda表达式进行遍历map.forEach((k, v) -> System.out.println(k + \": \" + v)); - 通过 
Stream API进行遍历map.entrySet().stream().forEach(entry -> { System.out.println(entry.getKey() + \": \" + entry.getValue());}); 
HashMap 的实现原理
HashMap 在 jdk1.7 以前是由数组+链表实现的,通过计算 key 的 hashcode 来将其映射到数组当中的插槽位置。如果多个 key 计算得到的插槽位置相同,那么使用链表将其连接起来存放在插槽中。
 在 jdk1.8 以后是由数组+链表+红黑树实现的。当链表长度大于 8,数组长度大于 64 时,会将链表转化为红黑树,提高查询速率。
解决哈希冲突的方法有哪些
- 再哈希法:当发生哈希冲突时,我们可以使用其他 
hash函数来再次计算。 - 连接法:通过使用链表等数据结构将其连接在一起,放在同一个哈希桶中(
HashMap采用的方法)。 - 开放定址法:通过使用线性探测等算法重新找到位置存放。
 - 哈希桶扩容。
 
HashMap 是线程安全的吗,可能会出现哪些问题?
不是,在 jdk1.7 以前,多线程环境下可能在增加元素时产生循环链表,导致 entrykey 死循环,还可能发生数据丢失的问题。在 jdk1.8 以后,使用数组+链表+红黑树的结构,解决了 entrykey 链死循环的问题。但是还是有 put 方法的覆盖问题。
HashMap 的 put 方法的过程
- 根据要存放元素的 
key计算hashcode找到插槽位置。 - 判断该位置是否存在元素,如果不存在,直接创建一个 
entry对象来存储该键值对,并将修改次数+1。 - 如果存在元素,进一步通过 
equals方法判断是否相同。如果相同直接将其value进行更新。如果不相同则遍历红黑树或者链表,查看是否有相同的元素,从而进行更新或者插入操作。 - 检查链表长度是否大于阈值(是否为 
8)。 - 检查负载因子是否大于 
0.75,如果大于进行扩容。 
HashMap 一般使用什么作为 key?为什么
String,因为 String 是不可变的,一旦被创建不能被修改,它的 hashcode 和 equals 方法都是固定的,较为稳定。
为什么 HashMap 使用红黑树而不是直接使用平衡二叉树
- 平衡二叉树它要求每个子树的高度差不能超过 
1,这个要求太严格了。会频繁修改树的结构。 - 红黑树要求最长结点长度不能超过最短节点的 
2倍,虽然牺牲了一部分查询效率,但是不会频繁触发修改树的结构。 
O(log n),稍弱于 AVLO(log n),查询性能极致HashMap 的扩容机制
当负载因子大于 0.75,即元素数量与数组大小的比值为 0.75 时会触发扩容。扩容一般分为两步:
- 计算扩容后的长度并创建数组:新容量为旧容量的 2 倍。
 - 将旧数组中的数据转移到新数组:
- 在 
jdk1.7以前,转移操作是直接将数组重新进行一个一个的哈希运算,重新分配位置。 - 在 
jdk1.8以后,通过将该元素当前位置&扩容后数组大小得到0或者1,如果为0则还在原位置,如果为1则在该位置+原数组大小的位置。这样可以不用通过哈希运算快速找到新位置。极大提高效率。 
 - 在 
 
说一说 HashMap 的负载因子
负载因子是指元素的数量与数组大小的比值。当大于 0.75 时会触发扩容。设置为 0.75 的原因是平衡了时间和空间复杂度。负载因子过大,会导致大量碰撞,导致性能降低。过小会导致内存利用率不高。
ConcurrentHashMap 是怎么实现的
- 在 
jdk1.7以前,是由数组+链表实现的。并且通过Segment(可重入锁)将数据分为一段一段的,并且为每一段数据分配一把锁。当一个线程访问一段数据时,其他段的数据也能被其他线程所访问。 - 在 
jdk1.8以后采用数组+链表+红黑树实现。并且通过volatile+synchronized或CAS实现的。当向其中添加一个元素时,会先判断容器是否被创建,如果没有,就使用volatile和CAS(乐观锁)来初始化容器。然后判断容器内该位置是否有元素,如果没有,直接使用CAS创建元素(乐观锁),如果有,使用synchronized关键字,然后遍历链表或者红黑树,进行修改或者创建元素。 
HashTable 的实现原理
HashTable底层是通过数组+链表实现的,数组是本体,链表是为了解决哈希冲突的。HashTable通过将内部方法都用synchronized关键字进行修饰,来实现线程安全的。
HashMap, HashTable, ConcurrentHashMap 的区别
nullnullnull- HashMap 是非线程安全的,它的 
key和value允许为null。默认初始容量为16,扩容时一般扩容为原来的2倍。如果设置了初始值,扩容为2的幂次倍。它的底层是由链表+数组+红黑树实现的。 - HashTable 是线程安全的,它的 
key和value不允许为null。默认初始容量为11,扩容时一般扩容为原来的2n+1。如果设置了初始值,直接使用初始值。它的底层是数组+链表实现的。 - ConcurrentHashMap 是线程安全的,它的 
key和value不允许为null。它可以在多线程环境下进行并发读写。它的底层是由数组+链表+红黑树实现的。 
Set
Set 集合有什么特点,怎么实现的
特点:无重复。
 通过内部的数据结构来实现 key 的无重复,首先它会根据 hash 值来判断该元素在 set 中是否存在,如果存在,进一步使用 equals 方法进行判断。
有序的 set 集合是什么?
TreeSet:通过比较器中的比较规则来进行有序排序。LinkedHashSet:通过链表来记录插入的顺序。


