文章目录

  • 1 Collection
  • 1.1 List 有序,可重复
  • 1.1.1 ArrayList
  • 1.1.1.1 线程不安全:
  • 1.1.1.2 报错并发修改异常(java.util.ConcurrentModificationException)
  • 1.1.1.3 解决
  • 1.1.2 Vector
  • 1.1.3 LinkedList
  • 1.2 Set 无序,唯一
  • 1.2.1 HashSet(无序,唯一)
  • 1.2.3 LinkedHashSet(FIFO插入有序,唯一)
  • 1.2.4 TreeSet(有序,唯一)
  • 2 Map
  • 2.1 HashMap
  • 2.1.1 hashcode
  • 2.1.2 哈希冲突
  • 2.1.3 底层数据结构
  • 2.1.4 默认容量,负载因子
  • 2.1.5 hashMap数组下标的计算规则
  • 2.1.6 为什么要2的幂以及扩容也要为2的幂?
  • 2.1.7 hashMap的put过程
  • 2.1.8 hashMap的resize()操作对之前数据的处理


java hashmap的参数怎么传 java hashmap contains_链表

1 Collection

1.1 List 有序,可重复

1.1.1 ArrayList

线程不安全,效率高
底层数据结构:数组
优点:查询快
缺点:增删慢
默认大小:10
扩容:1.5倍(10->15)(15->22)

1.1.1.1 线程不安全:
public static void main(String[] args) {
    List<String> list = new ArrayList<>();//替换行
    for (int i = 1; i <= 5; i++){
        new Thread(()->{
            list.add(UUID.randomUUID().toString().substring(0,8));
            System.out.println(list);
        },String.valueOf(i)).start();
    }
}
1.1.1.2 报错并发修改异常(java.util.ConcurrentModificationException)

java hashmap的参数怎么传 java hashmap contains_数组_02

1.1.1.3 解决
1. vector
2. List list = Collections.synchronizedList(new ArrayList<>());
3. List list = new CopyOnWriteArrayList<>(); //写时复制 先复制再修改

1.1.2 Vector

线程安全,效率低
底层数据结构:数组
优点:查询快
缺点:增删慢
默认大小:10
扩容:2倍(10->20)

1.1.3 LinkedList

线程不安全
底层数据结构:双向链表
优点:增删快
缺点:查询慢

1.2 Set 无序,唯一

Set存储是借助Map实现,add代码

public boolean add(E e) {
 return map.put(e, PRESENT)==null;
 }

其中PRESENT为new Object
Set把值存进map的key中实现值的唯一性
Mapkey唯一性用到了hashcode(),equals()

1.2.1 HashSet(无序,唯一)

线程不安全
底层数据结构:哈希表

1.2.3 LinkedHashSet(FIFO插入有序,唯一)

底层数据结构:链表和哈希表
1.由链表保证元素有序
2.由哈希表保证元素唯一

1.2.4 TreeSet(有序,唯一)

底层数据结构:红黑树
1.自然排序比较器排序保证元素有序
2.根据比较的返回值是否是0来决定,保证元素唯一性

2 Map

由于Map的key的唯一性需要hashcode(),equals()保证,
所有对象要放进Map时要重写这两个方法

2.1 HashMap

2.1.1 hashcode

Java中的hashCode方法就是根据一定的规则将与对象相关的信息映射成一个数值,这个数值称作为散列值

2.1.2 哈希冲突

计算出的哈希值是一样的时候

2.1.3 底层数据结构

hashMap在
jdk1.7的实现为数组+链表
jdk1.8的实现为数组+链表+红黑树
哈希冲突的时候用链表解决
但是哈希冲突剧烈的时候链表可能较长,所以引入了红黑树
形成红黑树的条件数组容量到达64并且链表的长度到达8

2.1.4 默认容量,负载因子

初始容量16负载因子默认0.75,最大容量2^30
当数组容量到达12(16*0.75=12)的时候触发扩容

2.1.5 hashMap数组下标的计算规则

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }
  1. 第一步
    key.hashCode():先用jdk里面的hashcode算出对象本身的哈希值
    h >>> 16:无符号右移16位 ,>>> 跟>>很像,只是无符号右移动之后二进制前面的高位被0补充

    然后对算出的两个值 异或(^)
    右移16位可以保留下高16位与低16位的特征
  2. 第二步
if ((p = tab[i = (n - 1) & hash]) == null)
   		tab[i] = newNode(hash, key, value, null);

其中n=当前数组的长度,数组的下标为0~n-1,。
索引= (数组的长度-1)& hash
上面的索引公式相当于 hash%数组的长度(当数组的长度为2的n次幂

为什么要%数组的长度呢?
因为hash的值可能超过了当前数组的长度,%之后hash就在数组的范围之内了

为什么不用hash%数组的长度呢?
按位与是直接二进制计算,所以计算的速度比%快

2.1.6 为什么要2的幂以及扩容也要为2的幂?

1.1 n为2的幂次方时 二进制1较多
如16(10000)- 1 = 15(1111)
如 8(1000) - 1 = 7(111)
1.2 n不为2的幂次方时二进制0较多
如17(1 0001) -1 = 16(10000)
如19(1 0011) -1 = 18(1 0010)
1.3 当再进行按位与(&)运算的时候(&运算二进制对应位位都为1才为1n为2的幂次方时由于1较多,与hash运算之后可以有效的避免hash冲突(1和0应该分布较均匀),而n不为2的幂次方时,由于0的位数较多,计算出来的数组下标大概率0的位数也较多(&的特性),这可能增大hash冲突

取余(%)操作中如果除数是2的幂次则等价于与其除数减⼀的与(&)操作
(也就是说 hash%lengthdehash&(length-1)的前提是 length 是2的n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次⽅。

2.1.7 hashMap的put过程

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  1. 第一次put会扩容(数组为16)
if ((tab = table) == null || (n = tab.length) == 0)
     n = (tab = resize()).length;
  1. 经过计算得到数组下标如果当前下标为null(即当前数组还没有存放元素)就放元素到当前下标的位置
if ((p = tab[i = (n - 1) & hash]) == null)
    ab[i] = newNode(hash, key, value, null);

下面是发生的hash碰撞的的情况

  1. 当put的key于之前的key相同时时会把之前的值覆盖
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;

java hashmap的参数怎么传 java hashmap contains_数组_03

  1. 当阈值大于8并且数组大于64时,链表已经转换为红黑树,把元素添加到红黑树中
else if (p instanceof TreeNode) //判断当前节点是否为红黑树的节点
  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  1. 遍历链表进行值的覆盖或转化为红黑树
for (int binCount = 0; ; ++binCount) { //遍历链表
     if ((e = p.next) == null) { //当节点到达末尾时
         p.next = newNode(hash, key, value, null); //添加一个节点
         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //当节点大于阈值8时,转换为红黑树
             treeifyBin(tab, hash);
         break;
     }
     if (e.hash == hash &&  //与之前的key重复,值覆盖
         ((k = e.key) == key || (key != null && key.equals(k))))
         break;
     p = e;
 }
  1. 当超过数组的扩容边界时,进行扩容
++modCount;//节点数量增加1
 if (++size > threshold) //当超过数组的扩容边界时,进行扩容
     resize();

2.1.8 hashMap的resize()操作对之前数据的处理

它会重新计算一次数组中的哈希值
计算公式:之前计算好的哈希值&旧数组的长度
如果e.hash & oldCap == 0还是在原来的下标处
如果e.hash & oldCap == 1 则在原来下标的基础上再加上旧数组的长度