集合知识点
什么是集合
- 集合就是一个放数据的容器,准确的说是放数据对象引用的容器
- 集合类存放的都是对象的引用,而不是对象本身
- 集合的类型主要有三种:list,set,map
集合的特点
- 集合用于存储对象的容器,对象是来封装数据的,对象多了也需要存储集中式管理
- 和数组相比较,集合的长度是可变的,大小不确定
集合和数组的区别
- 集合容量是自增的,数组是固定的
- 集合底层是高性能的数据结构和算法,提高了程序速度和质量
- 集合方便扩展,提高代码的复用性
- 集合存储是引用数据类型,而数组可以为基本数据类型和引用数据类型
常用的集合类有哪些
Map和Collection接口是所有集合框架的父接口
- collection接口的子接口是List和set
- Map接口的实现类主要有HashMap.TreeMap,HashTable,ConcurrentHashMap
- set接口的实现类主要有TreeSet,HashSet,LinkedHashSet
- List接口的实现类主要有ArrayList,LinkedList,vector
List,Map,Set三者区别
集合底层的数据结构
- collection接口
- List
- ArrayList:object数组
- LinkedList:双向链表
- Vector:Object数组
- Set
- HashSet(无序,唯一):基于hashmap实现,底层采用hashMap保存元素
- linkedHashSet:LinkedHashSet继承于HashSet,并且内部是通过LinkedHashMap来实现的
- TreeSet(有序,唯一):红黑树(自平衡的二叉搜索树)
- List
- Map
- HashMap:JDK1.8之前HashMap有数组和链表组成,数组是HashMap的主体,链表则是为解决hash冲突而存在的(拉链法),JDK1.8之后在解决hash冲突时有了比较大的变化,当链表长度大于阈值(默认为8),将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:继承Hashmap,所以底层也是拉链式散列结构
- HashTable:数组+链表实现
- TreeMap:红黑树实现
哪些集合是线程安全的
- vector:就比ArrayList多了一个synchronize(线程安全)
- hashTable:就比HashMap多了个synchronize(线程安全)
- concurrentHashMap:高并发,高吞吐量的线程线程安全.由segment数组结构和hashEntry数组结构组成.segment在concurrentHashMap中扮演锁的角色,HashEntry则用于存储键值对数据.一个concurrentHashMap里面包含一个segment数组,segment结构和hashmap类似,是一种数组+链表结构;一个segment里面包含一个HashEntry数组,每一个HashEntry有链表组成;每个segment守护hashentry数组里面的元素,当HashEntry数组里面的数据进行修改时,必须先获得对应的segment锁
快速失败机制Fail-Fast
fail-fast是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast
案例:假设有两个线程(线程1,线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上的修改,不是简单的修改集合元素的内容),那么这时候就会抛出ConcurrentModificationException异常,从而产生fail-fast机制
原因:迭代器在遍历集合时直接访问集合中的内容,并且遍历过程中使用一个modCount变量.集合在被遍历期间如果内容发生改变,就会改变MODCount的值.每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测MODCount是否改变
解决办法:在遍历过程中,所有涉及到改变MODCount值的地方全部加上synchronized
使用copyonwriteArrayList来替换ArrayList
怎么保证一个集合不被改变
可以使用Collections.unmodifiableCollection(Collection c)创建一个可读集合
ArrayList<String> arrayList = new ArrayList<>();
Collection<String> strings = Collections.unmodifiableCollection(arrayList);
strings.add("lsy");
System.out.println(strings);
运行结果:
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableCollection.add(Collections.java:1057)
at com.lsy.collection.List.main(List.java:14)
在多线程场景下如何使用ArrayList
使用Collections.synchronizedList()方法将其转换成线程安全后在使用
ArrayList<String> arrayList = new ArrayList<>();
java.util.List<String> strings = Collections.synchronizedList(arrayList);
strings.add("aaa");
Iterator
Iterator是单向遍历集合,但是相对于更加安全,因为他可以确保集合元素被更改的时候,就会抛出ConcurrentModificationException异常
ListIterator和Iterator区别
- Iterator可以遍历set和list集合,但是ListIterator只可以遍历list
- ListIterator可以双向遍历,Iterator只可以单向遍历
- ListIterator里面的方法更多
Iterator的使用
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arrayList.add(""+i);
}
arrayList.toArray();
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
HashSet原理
- HashSet其实就是HashMap用来实现的,HashSet的值放到HashMap的key上,HashMap的值为Present
- HashSet的相关操作基本上都是调用HashMap的相关方法来实现
hashSet构造方法
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
HashSet如何检查重复?如何保证数据不重复
- 向HashSet中add()元素时,首先会判断元素是否存在,不仅要比较hash值,同时还要结合equals方法比较
- HashSet中的add方法会使用HashMap的put()方法(HashMap比较Key是否相等是先比较hashcode再比较)
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
什么是hash算法
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值
HashMap的实现原理
HashMap是基于哈希表的map接口的非同步实现,此实现提供所有可选的映射操作,并允许使用null值和null键
底层数据结构:在jdk1.7底层结构为数组+链表,数组为主体,链表为解决hash冲突,也就是说一个数组的每一格就是一个链表,若遇到hash冲突,就将冲突的值添加到链表中
在jdk1.8之后,为解决hash冲突有了比较大的变化,当链表大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
不同 | jdk1.7 | jdk1.8 |
---|---|---|
存储结构 | 数组+链表 | 数组+链表+红黑树 |
初始化方式 | 单独函数:inflateTable | 继承到resize函数中 |
hash值计算方法 | 扰动处理:9次扰动=4次位运算+5次异或运算 | 扰动处理:2次扰动=1次位运算+1次异或运算 |
存放规则 | 无冲突时存放数组;冲突时,存放到链表 | 无冲突时,存放数组;冲突时:链表长度<8存放单链表,链表长度>8存放红黑树 |
插入数据方式 | 头插法(先将原位置的元素后移一位,再将数据插入该位置) | 尾插法 |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算 | 按照扩容后的规律计算(扩容后的位置=原位置or(原位置+就容量)) |
HashMap的长度为什么是2的幂次方
- 为了能让HashMap存取高效,尽量减少碰撞,也就是把数据均匀分配,每个链表/红黑树长度大致一样
这个算法如何设计:首先我们想到的是取余操作,但是取余%操作如果除数是2的幂次方则等价于其除数减一的与(&)操作(hash%length==hash&(length-1))
为什么是两次扰动:这样加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性和均匀性
HashMap与HashTable有什么区别
区别 | HashMap | HashTable |
---|---|---|
线程安全 | 非线性安全 | 线程安全(内部方法都经过synchronize修饰) |
效率 | 高 | 低 |
对null的支持 | null可以作为键,但是只可以有一个null键,可以有多个null值 | 只有put一个null值,就会报错 |
初始化容量 | 初始化大小16,之后每次扩容,容量变为2倍 | 初始化大小11,容量变为原来的2n+1 |
TreeMap简介
- Tree是一个有序的Key-value集合,通过红黑树实现
- 线程同步
如何决定使用HashMap还是TreeMap
在Map中插入,删除,查找元素这类操作时,HashMap最好,但是如果需要对一个有序的key集合进行遍历,可以选择TreeMap
说一下ConcurrentHashMap
ConcurrentHashMap用分段锁实现,HashTable用一把锁实现
JDK1.7:
-
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个数据时,其他段的数据也可能被其他线程访问
-
采用segment和HashEntry的方式进行实现,一个concurrentHashMap包括一个segment数组,一个segment数组包括一个HashEntry数组
JDK1.8
放弃了臃肿的Segment设计,采取的是采用Node+CAS+Synchronized来保证并发安全进行实现,synchronize只锁定当前链表或者红黑树的首节点,这样只要hash不冲突,就不会产生并发