说说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); // 输出最大元素

posted @ 2020-06-10 18:56  微辰  阅读(1370)  评论(0编辑  收藏  举报