Java集合详解(List、Map、Set)

集合

单列集合双列集合

集合分为单列集合和双列集合

  • 单列集合分为list,set;
  • 双列集合就是map;

我们常用的是ArrayList和HashMap

  • list分为ArrayList和LinkedList;
  • set分为HashSet和TreeSet;
  • map分为hashmap和treemap;

ArrayList

ArrayList底层是数组,默认长度为0;当添加第一个元素时,长度变为10,扩容机制是当数组存满时,还要继续添加元素,就会创建一个新的数组,容量是之前数组的1.5倍,并把之前元素复制进新数组。

HashMap

HashMap1.7之前底层是数组+链表,1.8之后是数组+链表+红黑树,底层重写了hashcode和equals方法,先计算hash值,hash值会根据hash函数计算出索引值,存入指定位置,如果位置上没有,存入,如果有比较equals,如果相同,新值替换旧值,如果不同,就挂下面,链表长度大于等于8,转为红黑树,小于等于6,转成链表。扩容机制,默认是16,加载因子0.75,每次长度乘2。

HashMap的主干是一个Entry数组。
Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
Entry是hashmap中的静态内部类

HashMap中关键属性
transient Entry[] table; // 存储元素的实体数组
transient int size;// 存放元素的个数
int threshold; // 临界值   当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
final float loadFactor; // 加载因子
transient int modCount;// 被修改的次数

HashTable

HashTable和concurrenthashmap线程安全,hashtable所有的方法都是synchronized修饰的
concurrenthashmap1.7之后是CAS+synchronized,所以是线程安全的。

解决hash冲突的方法

hash碰撞冲突:hashCode()的方法是为了产生不同的hash值,但是当两个对象的hash一样时,就发生了碰撞冲突

我们常用的解决方法有四种:

  • 开放地址法
  • 再hash法
  • 拉链法
  • 建立公共溢出区

开放地址法

开放地址法:
基本思想:当发生地址冲突的时候,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止
所用公式 Hi(key) = [H(key) + di]mod m;其中i = 1、2、3…k(k<m-1),H(key)为关键字key的直接hash地址;M为hash表的长度;
di为再次探测时的地址增量;根据di的不同取法,有不同的称呼;
线性探测再散列:di = 1、2、3、4…k (k<m-1)
二次探测再散列:di = 12,-12,22,-22…k2,-k2 (k<=m/2)
伪随机再散列:di = 伪随机数

再hash法

再hash法:
当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。
缺点:计算时间增加。
比如上面第一次按照姓首字母进行哈希,如果产生冲突可以按照姓字母首字母第二位进行哈希,再冲突,第三位,直到不冲突为止;

拉链法

拉链法:
在HashMap中 就是使用拉链法 来解决hash冲突的问题的;
将所有关键字为同义词的记录存储在同一线性链表中。
优点:

  1. 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  2. 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  3. 开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
  4. 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

缺点:
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

建立公共溢出区

建立公共溢出区:
建立公共溢出区的基本思想是:假设哈希函数的值域是[1,m-1],则设向量HashTable[0…m-1]为基本表,每个分量存放一个记录,另外设向量OverTable[0…v]为溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

常用的容器要点总结(list、map、set)

  • ArrayList
    - 基于动态数组的数据结构
    - 随机访问快,增删慢
    - 占用内存少,每个索引的位置是实际的数据
    - 效率高,线程不安全
  • LinkedList
    - 链表结构
    - 增删快,访问慢(数据多的情况下增删也不一定快)
    - 占用内存较多,每个节点中存储的是实际的数据和前后节点的位置
    - 线程不安全
  • Vector
    - 效率低
    - 线程安全
  • HashMap
    - HashMap是无序的
    - 方法不是同步的
    - 线程不安全
    - 效率较高
    - key value允许null值
    - HashMap的父类是AbstractMap
  • HashTable
    - HashTable是无序的
    - 方法是同步的
    - 线程安全
    - 效率较低(Hashtable的所有 public 方法声明中都有 synchronized关键字,HashMap的源码中则没有)
    - key value不允许null值
    - Hashtable的父类是Dictionary
  • TreeMap
    - TreeMap是有序的
    - 不能重复
    - 当未实现 Comparator 接口时,value可以为null,key 不可以为null,否则抛 NullPointerException 异常;
    - 当实现 Comparator 接口时,若未对 null 情况进行判断,则可能抛 NullPointerException 异常。如果针对null情况实现了,可以存入,但是却不能正常使用get()访问,只能通过遍历去访问
  • HashSet
    - 底层数据结构是哈希表
    - 唯一、无序
    - 两个方法:hashCode()和equals()
  • LinkedHashSet
    - 底层数据结构是链表和哈希表
    - 由链表保证元素有序、哈希表保证元素唯一
  • TreeSet
    - 底层数据结构是红黑树
    - 自然排序、比较器排序
    - 根据比较的返回值是否是0来决定是否唯一
    - 唯一、有序

HashMap的put存储过程

1、hash(key),取key的hashcode进行高位运算,返回hash值
2、如果hash数组为空,直接resize()
3、对hash进行取模运算计算,得到key-value在数组中的存储位置i
(1)如果table[i] == null,直接插入Node<key,value>
(2)如果table[i] != null,判断是否为红黑树p instanceof TreeNode。
(3)如果是红黑树,则判断TreeNode是否已存在,如果存在则直接返回oldnode并更新;不存在则直接插入红黑树,++size,超出threshold容量就扩容
(4)如果是链表,则判断Node是否已存在,如果存在则直接返回oldnode并更新;不存在则直接插入链表尾部,判断链表长度,如果大于8则转为红黑树存储,++size,超出threshold容量就扩容

1.8之前是头插法,1.8之后是尾插法

public V put(K key, V value) {
    
    
        // 如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,
        // 此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
    
    
            inflateTable(threshold);
        }
       // 如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);// 对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);// 获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    
    
        // 如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    
    
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;// 保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);// 新增一个entry
        return null;
    }

List、Map、Set的区别

  • list有序,顺序是添加的顺序
  • set无序指的是打乱了插入的顺序,不能重复。HashSet底层是HashMap是真正的无序;TreeSet有序,但这个顺序是根据排序规则排序的(二叉树排序)
  • map是键值对

ArrayList和LinkedList的区别

  • LinkedList首部插入数据很快,因为只需要修改插入元素前后节点的prev值和next值即可
  • ArrayList首部插入数据慢,因为数组复制的方式移位耗时多
  • LinkedList中间插入数据慢,因为遍历链表指针(二分查找)耗时多
  • ArrayList中间插入数据快,因为定位插入元素位置快,移位操作的元素没那么多
  • LinkedList尾部插入数据慢,因为遍历链表指针(二分查找)耗时多
  • ArrayList尾部插入数据快,因为定位速度快,插入后移位操作的数据量少

HashMap、TreeMap和HashTable的区别

  • HashMap和HashTable是无序的,TreeMap是有序的(二叉树排序)
  • HashMap的方法不是同步的、线程不安全。Hashtable的方法是同步的、线程安全的;
  • HashMap效率较高,Hashtable效率较低
  • HashMap允许null值(key和value都允许),HashTable不允许null值
  • 父类不同:HashMap的父类是AbstractMap,Hashtable的父类是Dictionary

HashMap

  • JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个数组中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
  • JDK1.8及之后,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间

HashMap的实现原理:
首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中

猜你喜欢

转载自blog.csdn.net/Mr_Gaojinchao/article/details/120627914