说说List,Set,Map三者的区别
说说List,Set,Map三者的区别
- List 是一个有序、可重复的集合,
- Set 集合类似于一个罐子,程序可以依次把多个对象“丢进”Set 集合,而 Set 集合通常不能记住元素的添加顺序。Set 集合中的对象不按特定的方式排序,Set 集合中不能包含重复的对象,Set 集合中不能包含重复的对象
- Map 是一种键-值对(key-value)集合,Map 的 key 不允许重复,value 可以重复,Map 接口主要有两个实现类:HashMap 类和 TreeMap 类。其中,HashMap 类按哈希算法来存取键对象,而 TreeMap 类可以对键对象进行排序。
List
ArrayList 类实现了可变数组的大小,它还提供了快速基于索引访问元素的方式,对尾部成员的增加和删除支持较好。允许对集合中的元素进行快速的随机访问,不过向 ArrayList 中插入与删除元素的速度相对较慢。
LinkedList 类采用链表结构保存对象,LinkedList 类采用链表结构保存对象,LinkedList 类采用链表结构保存对象,但是 LinkedList 类随机访问元素的速度则相对较慢
Vector: 底层是数组实现,线程安全的,操作的时候使⽤synchronized进⾏加锁
集合存储位置怎么获取
根据key 生成的hash值32位数,用高16位,和低16位做异或运算,相同是0,不同是1,
Vector和ArrayList、LinkedList联系和区别
ArrayList:底层是数组实现,线程不安全,查询和修改⾮常快,但是增加和删除慢
LinkedList: 底层是双向链表,线程不安全,查询和修改速度慢,但是增加和删除速度快
Vector: 底层是数组实现,线程安全的,操作的时候使⽤synchronized进⾏加锁
ArrayList 类和 LinkedList 类的区别
ArrayList 是基于动态数组数据结构的实现,访问元素速度优于 LinkedList。
LinkedList 是基于链表数据结构的实现,占用的内存空间比较大,但在批量插入或删除数据时优于 ArrayList。
对于快速访问对象的需求,使用 ArrayList 实现执行效率上会比较好。需要频繁向集合中插入和删除元素时,LinkedList 类比 ArrayList 类效果高。
如果需要保证线程安全,ArrayList应该怎么做有几种⽅式
⽅式⼀:⾃⼰写个包装类,根据业务⼀般是add/update/remove加锁
⽅式⼆:Collections.synchronizedList(new ArrayList<>()); 使⽤synchronized加锁
⽅式三:CopyOnWriteArrayList<>() 使⽤ReentrantLock加锁
CopyOnWriteArrayList吗?和 Collections.synchronizedList实现线程安全有什么区别
-
CopyOnWriteArrayList:执⾏修改操作时,会拷⻉⼀份新的数组进⾏操作(add、set、 remove 等),代价⼗分昂贵,在执⾏完修改后将原来集合指向新的集合来完成修改操作,源码⾥⾯⽤ReentrantLock可重⼊锁来保证不会有多个线程同时拷⻉⼀份数组
-
Collections.synchronizedList:线程安全的原因是因为它⼏乎在每个⽅法中都使⽤了 synchronized同步锁
CopyOnWriteArrayList的设计思想是怎样的,有什么缺点
答案:设计思想:读写分离+最终⼀致
缺点:内存占⽤问题,写时复制机制,内存⾥会同时驻扎两个对象的内存,旧的对象和新写⼊的对象, 如果对象⼤则容易发⽣Yong GC和Full GC
说下ArrayList的扩容机制是怎样的
-
JDK1.7之前ArrayList默认⼤⼩是10,JDk1.7之后是0
-
未指定集合容量,默认是0,若已经指定⼤⼩则集合⼤⼩为指定的
-
当集合第⼀次添加元素的时候,集合⼤⼩扩容为10
-
ArrayList的元素个数⼤于其容量,扩容的⼤⼩= 原始⼤⼩+原始⼤⼩/2
Set
HashSet 是 Set 接口的典型实现,HashSet 是按照 Hash 算法来存储集合中的元素。因此具有很好的存取和查找性能。
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals() 方法比较返回的结果为 true,但它们的 hashCode 不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功。
HashSet hs = new HashSet(); // 调用无参的构造函数创建HashSet对象
HashSet<String> hss = new HashSet<String>(); // 创建泛型的 HashSet 集合对象
public static void main(String[] args) {
HashSet<String> courseSet = new HashSet<String>(); // 创建一个空的 Set 集合
String course1 = new String("Java");
String course2 = new String("Python");
String course3 = new String("C语言");
String course4 = new String("Golang");
courseSet.add(course1); // 将 course1 存储到 Set 集合中
courseSet.add(course2); // 将 course2 存储到 Set 集合中
courseSet.add(course3); // 将 course3 存储到 Set 集合中
courseSet.add(course4); // 将 course4 存储到 Set 集合中
Iterator<String> it = courseSet.iterator();
while (it.hasNext()) {
System.out.println( (String) it.next() ); // 输出 Set 集合中的元素
}
System.out.println( courseSet.size() + "教程!");
}
TreeSet 类同时实现了 Set 接口和 SortedSet 接口。SortedSet 接口是 Set 接口的子接口,可以实现对集合进行自然排序,因此使用 TreeSet 类实现的 Set 接口默认情况下是自然排序的,这里的自然排序指的是升序排序。
注意:在使用自然排序时只能向 TreeSet 集合中添加相同数据类型的对象,否则会抛出 ClassCastException 异常。
Map
Map 是一种键-值对(key-value)集合,用于保存具有映射关系的数据。
Map 接口主要有两个实现类:HashMap 类和 TreeMap 类。其中,HashMap 类按哈希算法来存取键对象,而 TreeMap 类可以对键对象进行排序。
HashMap users = new HashMap();
users.put("11", "张浩太"); // 将学生信息键值对存储到Map中
users.put("22", "刘思诚");
users.put("33", "王强文");
users.put("44", "李国量");
users.put("55", "王路路");
System.out.println("******** 学生列表 ********");
Iterator it = users.keySet().iterator();
while (it.hasNext()) {
// 遍历 Map
Object key = it.next();
Object val = users.get(key);
System.out.println("学号:" + key + ",姓名:" + val);
}
TreeMap 类的使用方法与 HashMap 类相同,唯一不同的是 TreeMap 类可以对键对象进行排序
遍历map
使用 for-each 循环遍历 key 或者 values,
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
// 打印键集合
for (String key : map.keySet()) {
System.out.println(key);
}
// 打印值集合
for (String value : map.values()) {
System.out.println(value);
}
使用迭代器(Iterator)遍历
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
Iterator<Entry<String, String>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, String> entry = entries.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + ":" + value);
}
通过键找值遍历,这种方式的效率比较低,因为本身从键取值是耗时的操作。
for(String key : map.keySet()){
String value = map.get(key);
System.out.println(key+":"+value);
}
说下 HashMap和Hashtable 的区别
- HashMap:底层是基于数组+链表,⾮线程安全的,默认容量是16、允许有空的健和值
- Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有 null的健和值
HashMap线程不安全,HashTable 线程安全,应为用synchronized 修饰。要保证线程安全的话就使⽤ ConcurrentHashMap。
Hashtable 默认 的初始⼤⼩为11,之后每次扩充,容量变为原来的2n+1。
HashMap 默认的初始化⼤⼩为16。之后 每次扩充,容量变为原来的2倍。
HashMap 在解决哈希冲突时,链表长度大于8,会转成红黑树,减少搜索时间。
hashMap: 散列桶(数组+链表),可以实现快速的存储和检索,但是确实包含⽆序的元素,适⽤于在 map中插⼊删除和定位元素
treeMap:使⽤存储结构是⼀个平衡⼆叉树->红⿊树,可以⾃定义排序规则,要实现Comparator接⼝
Set和Map的关系
核⼼就是不保存重复的元素,存储⼀组唯⼀的对象 set的每⼀种实现都是对应Map⾥⾯的⼀种封装,
HashSet对应的就是HashMap,treeSet 对应的就是 treeMap
如果需要线程安全,且效率⾼的Map
- 多线程环境下可以⽤ concurrent 包下的 ConcurrentHashMap,
- Collections.synchronizedMap() 使⽤Collections.synchronizedMap包装后返回的map是加锁的
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
HashMap底层是 数组+链表+红⿊树,为什么要⽤这⼏类结构呢?
-
数组 Node<K,V>[] table ,根据对象的key的hash值进⾏在数组⾥⾯是哪个节点
-
链表的作⽤是解决hash冲突,将hash值⼀样的对象存在⼀个链表放在hash值对应的槽位
-
红⿊树 JDK8使⽤红⿊树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn),
-
通过hash碰撞,让HashMap不断产⽣碰撞,那么相同的key的位置的链表就会不断增⻓,当对这个Hashmap的相应位置进⾏查询的时候,就会循环遍历这个超级⼤的链表,性能就会下降,所以改⽤红⿊树
为啥选择红⿊树⽽不⽤其他树,⽐如⼆叉查找树,为啥不⼀直开始就⽤红⿊树,⽽是到8的⻓度后 才变换
- ⼆叉查找树在特殊情况下也会变成⼀条线性结构,和原先的链表存在⼀样的深度遍历问题,查找性能 就会慢,
- 使⽤红⿊树主要是提升查找数据的速度,红⿊树是平衡⼆叉树的⼀种,插⼊新数据后会通过左旋,右旋、变⾊等操作来保持平衡,解决单链表查询深度的问题
- 数据量少的时候操作数据,遍历线性表⽐红⿊树所消耗的资源少,且前期数据少平衡⼆叉树保持平衡是需要消耗资源的,所以前期采⽤线性表,等到⼀定数之后变换到红⿊树
说下hashmap的put和get的核⼼逻辑(JDK8以上版本)
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//获取首节点,hash碰撞概览小,通常链表第一个节点就是值,没必要去循环遍历,处于效率
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不止一个节点,就需要循环遍历,存在多个hash碰撞
if ((e = first.next) != null) {
//判断是否是红黑树,如果是则调用树的查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//链表结构,则循环遍历获取节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
ConcurrentHashMap吗?为什么性能⽐ hashtable⾼,说下原理
hashtable类基本上所有的⽅法都是采⽤synchronized 进⾏线程安全控制⾼并发情况下效率就降低。ConcurrentHashMap是采⽤了 分段锁的思想提⾼性能,锁粒度更细化
ConcurrentHashMap实现的区别有没了解 JDK8之前 和之后的优化
-
JDK8之前,ConcurrentHashMap使⽤锁分段技术,将数据分成⼀段段存储,每个数据段配置⼀把锁,即segment类,这个类继承ReentrantLock来保证线程安全。技术点:Segment+HashEntry
-
JKD8的版本取消Segment这个分段锁数据结构,底层也是使⽤Node数组+链表+红⿊树,从⽽实现对 每⼀段数据就⾏加锁,也减少了并发冲突的概率,CAS(读)+Synchronized(写) 。技术点:Node+Cas+Synchronized
Collections 类操作集合
Collections.sort(prices); // 调用sort()方法对集合进行排序
Collections.reverse(students); // 调用reverse()方法对集合元素进行反转排序
Collections.max(nums); // 输出最大元素
Collections.min(nums); // 输出最大元素