zoukankan      html  css  js  c++  java
  • Java 集合总结

    一、List、Set 和 Map 的区别?

    首先 List 和 Set 是 Collection 接口的子接口,而 Map 是独立的一个接口,与 Collection 无关

    • List:有序,可重复(有序是指存储顺序跟输入的一样,而不是说按某种排序方法排序的)。实现的类有:ArrayList、LinkedList、Vector。
    • Set:无序,唯一(无序是指存储顺序跟输入的不一样,而不是说按某种排序方法排序的)。实现的类有:HashSet、TreeSet。
    • Map:使用键值对存储。Map 会维护与 Key 有关联的值。两个 Key 可以引用相同的对象,但是 Key 不能重复。

    二、List

    (一)、ArrayList 与 LinkedList 的区别?

    • 线程安全:都是不同步的,所有都是线程不安全的;
    • 底层数据结构
      • ArrayList:数组。因此,支持高效的随机访问,且随机访问的时间复杂度为 O(1)。但是插入和删除的时间复杂度为 O(n)。
      • LinkedList:双向链表数据结构 (JDK1.6 之前为循环链表,JDK1.7 取消了循环,注意双向链表和双向循环列表的区别)。因此,插入和删除的时间复杂度为 O(1)。随机访问的时间复杂度为 O(n)。
    • 内存占用空间:ArrayList 主要体现在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费主要体现在它的每一个元素都有节点,会占用空间。

     (二)、ArrayList 与 Vector 的区别?为什么要用 ArrayList 取代 Vector?

     区别:Vector 类中的所有方法都是同步的,所以是线程安全的。但一个线程访问 Vector 的话,代码要在同步操作上耗费大量的时间,因此效率低。

        ArrayList 是不同步的,所以线程不安全,但是效率高。所以如果在不需要保证线程安全时,建议考虑使用 ArrayList。

    (三) 、CopyOnWriteArryList

    当迭代次数远大于修改次数时使用该容器。没次修改其都会复制底层的数组。

    CopyOnWriteArrayList 是 ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等) 都是通过对底层数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作数量大大超过了可变操作的数量时,这种方法可能比其他替代方法更有效。

    三、Map

     (一)HashMap 和 Hashtable 的区别?

    • 继承不同:HashMap 继承 AbstractMap,而 Hashtable 继承 Dictionary。
    • 线程是否安全:HashMap 是非线程安全的,Hashtable 是线程安全的;Hashtable 的内部方法基本都被 synchronized 修饰过。如果你要保证线程安全的话就得使用 ConcurrentHashMap。因为线程安全的问题,所以 HashMap 会比 Hashtable 效率高。基本 Hashtable 要被淘汰了,尽量不要用 Hashtable。
    • 对 Null 的支持:HashMap 中,Null 即可以作为键,又可以作为值,但是 Hashtable 里都不能使用 Null。所以 HashMap 不能由 get() 方法来判断是否存在某个键,而应该用 containsKey() 方法来判断。
    • 初始容量大小和每次扩充容量大小的不同:
      • 创建时如果不指定初始容量的初始值,Hashtable 默认的初始大小为 11,每次扩充,容量变为原来的 2n + 1。HashMap 默认的初始大小为 16,之后每次扩容都变为原来的 2 倍。
      • 创建时如果给定了容量的初始值,那么Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的 tableSizeFor() 方法保证),也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。
    • 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
    • 哈希的使用值不同:Hashtable 直接使对象 hashCode() 的值;而 HashMap 会重新计算 hash 值。

    HashMap 部分源码(JDK 1.8):

    • 不带初始容量的:
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    • 带初始容量的:
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
        }
    public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);

    Hashtable 部分源码 (JDK 1.8):

    • 不带初始容量的:
    public Hashtable() {
        this(11, 0.75f);
    }
    • 带初始容量的:
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, Max_ARRAY_SIZE + 1);
    }

    (二)、HashMap 和 HashSet 的区别?

    其实 HashSet 底层是基于 HashMap 实现的。

    HashMap HashSet
    实现了 Map 接口 实现 Set 接口
    调用 put() 方法向集合 Map 添加元素 调用 add() 方法向 Set 中添加元素
    HashMap 使用键 (Key) 计算hashCode HashSet 使用成员对象来计算 hashCode 值,对于两个对象来说 hashCode 值可能相等,所以 equals() 方法用来判断对象的相等性

    (三)、HashSet怎样检查重复 (属于 Set 集合) 

    当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他加入对象的 hashCode 值作比较,如果没有相符合的 hashCode 值,HashSet 会假设对象没有重复出现。但是如果发现有相同值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否是真的相同。如果两者相同,HashSet 就不会允许其加入。

    (四)、HashMap 的底层实现

    在 JDK1.8 之前 HashMap 底层是数组和链表结合在一起使用,也就是链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n-1) & hash 判断当前元素存放的位置(这里的 n 为数组长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同,直接覆盖,不相同则通过拉链法解决冲突(哈希冲突)。

    JDK 1.8 HashMap 的 hash 方法源码:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    对比 JDK 1.7 HashMap 的 hash 方法的源码

    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
    
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    相比 JDK 1.8 的 hash 方法,JDK 1.7 的 hash 方法的性能会差一点点,毕竟扰动了 4 次。

    所谓 “拉链法” 就是:将链表和数组结合起来。也就是说创建一个链表数组,数组中的每一个位置就是一个链表。如果遇到哈希冲突,就将冲突的值加入到链表中。如下图:

    JDK 1.8 之前的内部结构

    JDK 1.8 之后

    相较于之前的版本,JDK 1.8 之后再解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

    JDK 1.8 之后的 HashMap 底层数据结构

    TreeMap、TreeSet 以及 JDK1.8之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

    (五)、HashMap 的长度为什么是 2 的幂次方?

    首先,为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值 -2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是 "h & (length-1)" 。(h 代表实际的 hash 值, n 代表数组长度)。

    其次,length 为 2 的整数次幂的话能够保证 length 为偶数,这样 length-1 就为奇数,奇数的二进制的嘴都以为都是 1,这样就能保证 h & (length-1) 的最后一位可能为 0 也可能为 1 (二进制表示),换成十进制后最后的结果就有可能是偶数也有可能是奇数,这样更能保证散列的均匀性。而如果 length 为奇数的话,很明显 length-1 就为偶数,偶数的二进制表示最后一位为 0,这样 h & (length-1) 的最后一位肯定也为 0 ,换成十进制就为偶数,这样会造成极大的浪费。

    (六)、HashMap 多线程操作导致死循环的问题

    主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。

    (七)、ConcurrentHashMap 是如何实现的?采用的是什么锁?

    首先来看看 ConcurrentHashMap 的结构:ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。而 Segment 主要是用于存放一种重入锁 ReentrantLock,Segment 的数据结构与 HashMap 类似,是一种数组+链表结构;HashEntry 用来存放键值对数据,是一种链表结构。当要修改 HashEntry 的时候,就必须先通过 Segment 才能进行修改,不能获得则休眠。ConcurrentHashMap 所使用的的所分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,每个 Segment 利用 ReentrantLock 实现,当一个线程被占用锁访问其中一个数据的时候,其他段的数据也能被其他线程访问

    从下图可以看出 ConcurrentHashMap 主要有三大结构:整个 Hash 表,Segment(段),HashEntry (节点)。每个 Segment 就相当于一个 Hashtable。每个 HashEntry 代表 Hash 表中的一个节点,在其定义的结构中可以看到,出来 value 值与 next 没有定义 final (value 和 next 定义为 volatile),其余的都为 final 类型。ConcurrentHashMap 读不需要加锁

    根据 hash 值定位 Segment 时会调用 Segment 方法,并返回相应的 Segment 在数组中的下标。Segment 的 get 操作实现非常简单和高效。先经过一次哈希,然后使用这个哈希值通过运算定位到 Segment,再通过哈希算法定位到元素。get 操作的高效之处在于整个 get 过程中不需要加锁,除非读到的值是空的才会加锁重读,我们知道 Hashtable 容器的 get 方法是需要加锁的,那么 ConcurrentHashMap 的 get 操作是如何做到不加锁的?原因在于它的 get 方法里将要使用的共享变量都定义成了 volatile,如用于统计当前 Segment 大小的 count 字段和用于存储值的 HashEntry 的 value。定义成 volatile 的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在 get 操作里只需要读不需要写共享变量 count 和 value,所以可以不用加锁。

    之所以不会读到过期值,是根据 Java 模型的 happen-before 原则,对 volatile 字段的写入操作先于读操作,即使两个线程同时修改和获取 volatile 变量,get 操作也能拿到最新的值,这就是用 volatile 替换所的景点应用场景。

     

    Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能够充当锁的角色。每个 Segment 对象用来守护其(成员变量 table 中)包含的若干个桶。count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成链表) 包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所有在每个 Segment 对象中都包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,视为了避免出现 “热点域” 而影响 ConcurrentHashMap 的并发性。

    默认情况下,每个 ConcurrentHashMap 类会创建 16 个并发的 Segment,每个 Hash链都是有 HashEntry 节点组成的。ConcurrentHashMap 中的读方法不许需要加锁,所有的修改操作在进行结构修改时都会在最后一步写 count 变量,通过这种机制保证 get 操作能够得到几乎最新的结构更新。

    参考:https://blog.csdn.net/yansong_8686/article/details/50664351

     (八)、CurrentHashMap 和 HashMap 的区别?

    • 底层数据结构:JDK 1.7 的 ConcurrentHash 底层采用 分段的数组+链表 实现,JDK 1.8 采用的数据结构跟 HashMap 1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
    • 实现线程安全的方式(重要):
      • 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分段(Segment),每一把锁只所容器的一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了 JDK1.8 的时候,已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后对 synchronized 锁做了很多的优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 是数据结构,但是已经简化了属性,只是为了兼容旧版本;
      • Hashtable(通一把锁):使用 sunchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法的时,其他线程也访问同步方法,可能会进入阻塞状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈而效率也会越低。

     

    Hashtable 

    JDK1.7 的 ConcurrentHashMap

     

    JDK1.8 的 ConcurrentHashMap(TreeBin:红黑树节点;Node:链表节点)

     (九)、ConcurrentHashMap 线程安全的具体实现方式/底层具体实现

    JDK1.7:

    首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段数据也能被其他线程访问。

    ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

    Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

    static class Segment<K,V> extends ReentrantLock implements Serializable {
    }

    一个 ConcurrentHashMap 里包含了一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组+链表结构,一个 Segment 包含多个 HashEntry 组成的数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护者一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。

    JDK1.8

    ConcurrentHashMap 取消了 Segment 分段,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 相似,数组+链表/红黑树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(n))转换为红黑树(寻址时间复杂度为 O(log n))

    synchronized 只锁定当前链表或者红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率得到提升。

    参考:https://www.cnblogs.com/chengxiao/p/6059914.htmlhttps://www.cnblogs.com/chengxiao/p/6842045.html

    作者:意无尽 公众号:意无尽 关于作者:本人目前传统专业,现自学 Java,后续会有向大数据方向转型。希望自己能一步一个脚印的走下去,以此博客来见证我技术的成长轨迹!
  • 相关阅读:
    P1012 拼数(水题)
    oracle 存储过程中调用同义词报错“表和视图不存在”
    C#文件相对路径
    C# WebAPi接收和发送图片
    EFCore学习笔记一:(安装EFCore并根据Code First生成数据库)
    Winform切换登录用户
    Winform中子控件Dock排列顺序问题
    ORA-28001: the password has expired解决方法
    C#实体类生成XML(注意<![CDATA]>标签的不解析)
    一次完整的HTTP请求过程
  • 原文地址:https://www.cnblogs.com/reformdai/p/11019530.html
Copyright © 2011-2022 走看看