集合

为什么使用集合

集合就是用来保存对象的,他的长度可变,数组的长度每次定义好之后就不可变了,但是我们不知道要存储多少个元素,所以我们就要使用集合。

集合中的数据都是存放在内存中的。当程序关闭或重启后集合中的数据丢失,所以集合是一种临时存储数据的容器

集合分类

迭代器

迭代器是一种设计模式,接口,专门用来遍历循环集合,它只使用容器Collection,并不关心容器类型。

应用场景:我们使用容器时必须指定类型,但是我想把list的操作给set。就会使用到Iterator.

每个实现类都提供了.iterator();返回值就是迭代器对象。

public class TestIterator {
  public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //获取集合的迭代器
    Iterator<Integer> iterator = list.iterator();
    //.hasNext() 判断是否有下一个元素
    while (iterator.hasNext()) {
      //获取下一个元素
      Integer next = iterator.next();
      System.out.println(next);
    }

    //lterable用来返回迭代器,实现了该接口的类就算是可迭代对象了,可以用for-each循环访问数据。省去了next操作
   Collection  list1 = new ArrayList<>();
    for(Object o : list1){
        System.out.println(o);
    }
  }
}
1
2
3

注意

java中的迭代器是一个接口,名为Iterator,将通用性做到了极致,可以访问不同特性的集合数据而无需关心他们的内部实现。集合并不是直接去实现Iterator接口,而是实现的Iterable接口,用这个Iterable定义的方法区返回当前集合的迭代器,Collection就继承了这个接口,所以Collection体系内的集合都的按照这种方式返回迭代器collection.iterator(),以供大家访问数据。

why?

如果集合直接实现迭代器的话,那别人调用了当前集合的next方法,就会影响到你遍历数据,比如你希望从头开始遍历数据,但是别人已经遍历完了,你就拿不到数据了,而通过实现Iterable这种方式,就可以每次返回新的迭代器,不同迭代器遍历数据时互不影响。所以这就可以看出迭代器是具有独立性和隔离性的。但如果我们正确的返回了迭代器,那么我们就可以用for-each来循环数据。

总结

Iterable用来返回迭代器,实现了该接口的类就算是可迭代对象了,可以用for-each循环访问数据。省去了next操作

Iterator就是迭代器,用来遍历集合的数据,并无需关心集合的内部实现。for-each底层用的就是迭代器

Iterator

只能单向遍历

.hasNext(); 检查集合中是否还存在元素

.next(); //获取集合的下一个元素

.remove(); 将迭代器返回的元素删除

ListIterator

双向遍历

.hasNext(); 检查集合中是否还存在元素

.next(); //获取集合的下一个元素

.remove(); 将迭代器返回的元素删除

.add(); 添加元素

public class TestListIterator {
    public static void main(String[] args) {
        //创建一个集合对象
        List<Integer> list = new ArrayList<Integer>();
        //向集合中添加分数
        list.add(78);
        list.add(80);
        list.add(89);        
        ListIterator<Integer> lit = list.listIterator();    
        //从前向后遍历
        while(lit.hasNext()){
            lit.next();
        }    
       //从后向前遍历 ----   前提是必须正着来一遍
        while(lit.hasPrevious()){
            int elem = lit.previous();
            System.out.println(elem +"  "+lit.nextIndex()+"  "+lit.previousIndex());
        }
    }
}
89  2  1
80  1  0
78  0  -1

区别

1.适用范围不同: iterator : 适用于Collection集合 ---- list和set 。Listiterator :适用于list

2.遍历顺序不一样: Listiterator 双向遍历。iterator 单向遍历(向后)

3.操作不一样:Listiterator 在循环元素时可以可以add、remove,set ,Iterator:remove

​ Listiterator 可以定位当前索引位置,nextIndex(),previousIndex(),Interator不可以。

foreach和Iterator

Iterator只能用来遍历集合。foreach可以遍历集合和数组,但是遍历集合时不能操作数据,对数据进行添加删除等操作,Iterator可以。
JDK1.8之后添加了removelf方法,可以条件删除数据

源码

以ArrayList为例

public class TestIterator {
  public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //获取集合的迭代器
    Iterator<Integer> iterator = list.iterator();
    //.hasNext() 判断是否有下一个元素
    while (iterator.hasNext()) {
      //获取下一个元素
      Integer next = iterator.next();
      System.out.println(next);
    }
  }
}
 /**
     * The size of the ArrayList (the number of elements it contains).
     *ArrayList的大小(它包含的元素数)。
*/
private int size;   // 数组容量 = 3

  /**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     这个列表被结构修改的的次数。结构修改是指改变列表的大小,或以其他方式干扰它,
     使进行中的迭代可能产生不正确的结果
     *
     *This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
      此字段由迭代器和列表迭代器实现使用 由迭代器和方法返回。如果此字段的值发生意外变化,则迭代器(或列表
      将抛出一个代码并发修改异常 .对于{next}、{remove}、{previor},{set}或{add}操作。。提供       了fail-fast(快速失败)行为,不是非确定性行为
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     子类使用此字段是可选的,如果子类希望提供快速失败迭代器(和列表迭代器)只需在其{add(int,E)}和
     {remove(int)}方法(及其重写的任何其他方法导致列表的结构修改)。
     一次呼叫{add(int,E)}或{remove(int)}添加的数量不得超过或者迭代器(和列表迭代器)将抛伪  		ConcurrentModificationExceptions。如果实施者不希望提供快速失败迭代器,此字段可忽略
*/     
//在 AbstractList 定义的全局变量,他在add,remove。set等修改数据的时候会+1
protected transient int modCount = 0; 

private class Itr implements Iterator<E> {
    	 // 调用next方法返回的元素索引   
        int cursor;        // 0 1 2 3
    	// 最近一次调用next方法返回的元v素索引-如果没有,则为-1
        int lastRet = -1;  //-1 0 1 2 
    	// expectedModCount 修改次数
        int expectedModCount = modCount;  // 3 -- 循环调用next方法 3次
    
		//判断集合中是否还有元素
        public boolean hasNext() {
            //第一次调用的元素索引是否等于集合长度
            return cursor != size;  // 0  != 3 ;  true
        }
    
		// 返回下一个元素
        @SuppressWarnings("unchecked")
        public E next() {
            // 判断是否进行了修改操作 
            checkForComodification();
            // i = 0  第一次调用的元素下标
            int i = cursor;
            // next返回的索引 大于 数组
            if (i >= size)  // 0 >=3  flase
                //无搜索元素异常
                throw new NoSuchElementException();
            // 转换为数组
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                //并发修改异常
                throw new ConcurrentModificationException();
            //next返回的下标 +1
            cursor = i + 1;  // 0+1 = 1
            // 返回元素
            return (E) elementData[lastRet = i];  // elementData【0】
        }
		// 删除迭代器返回的元素
        public void remove() {
            //判断 最近一次访问的元素下标 是否正确
            if (lastRet < 0) // 0< 0  flase
                //非法状态异常 --- 集合不存在
                throw new IllegalStateException();
          
            checkForComodification();

            try {
                //删除元素
                ArrayList.this.remove(lastRet); // 0  modCount +1 = 4
                // 1 = 0
                cursor = lastRet;
                //最近一次调用的下标赋值为-1   
                // 0 = -1
                lastRet = -1;
                // 3  =  4  
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

		// fail-fast(快速失败机制)单线程和多线程中会出现
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

fail-fast 快速失败机制

java集合中的一种错误检查机制,是不确定性。主要用于线程操作(单线程也会发生)

触发条件

1.在循环遍历集合时,向集合中插入值或删除集合中值时会出现这个异常。

public class Test {
  public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    for (Integer integer : list) {
      list.remove(integer);
    }
  }
}

2.当多个线程对同一个Collection集合进行修改数据的操作。其中一个线程用迭代器进行循环,但是当前集合的数据被其他线程所改变。单线程也会。

why

迭代器在调用add,next、等方法时中会调用checkForComodification() 方法,他主要用来检测modCount 是否 expectedModCount相同,不相同就会触发异常。

解决

1.使用线程安全的集合 ConcurrentHashMap,CopyOnWriteArrayList ,CopyOnWriteArraySet

2.添加锁,在所有涉及改变数据结构即modcout值的操作加上Synchronized。

List

有序可重复,允许多个null值,按照插入顺序添加数据,获取元素时可以根据迭代器和获取元素下标

ArrayList

底层是一个数组,增删慢,查询快,最常使用
数组长度固定,超出以后需要扩容,也就是创建一个新的数组,将旧的内容放到新的里面,使用尾插法并指定初始容量可以极大提高性能,甚至超过Liked list(需要创建大量的node对象)

LinkedList

底层是一个双向数组,保存下一个元素的下标引用,查询慢,增删快
循环数据时只能使用迭代器,不能用for,因为数据是零散的存储在内存中,当我们用for循环来get某个元素时,都要对List重新进行遍历,性能消耗极大,并且indexof(获取内容第一次出现的下标),结果为空时会遍历整个集合

Set

无序不重复,最多允许一个null值,通过迭代器或者转变为数组来获取元素

HaseSet

底层是哈希表,无序,查询速度最快,TreeSet其次(按内容查询,如果按索引查询,Arraylist最快)

TreeSet

底层是TreeMap(红黑树),有序(可以自定义)

LinkedHaseSet

底层是哈希表+链表,有序(插入顺序)

注意-----唯一性

1.存储自定义类在一个底层实现为哈希表的时候,我们要重写hasecode 和 equals 方法。

java定义的类比如String,本身就已经重写了这两个方法,如果我们自定的类没有重写着两种方法,那么会存入相同的数据,破坏了唯一性。

2.存储元素在一个底层为红黑树的集合时想要自定义排序,我们要实现comparable(内部比较器)或Comparator(外部比较器)接口,内部比较器只有一个,外部比较器有多个。

Comparable 外部比较器 <0 倒序 , = 0 相同 > 0 正序

被abstract修饰,不能作为内部类使用

public class Student implements Comparable<Student>{   
    private  Integer age;   
    private  String  name;
    //重写compareTo方法 
 	@Override                                      
    	public int compareTo(Student o) {                
            return this.age - o.age; // 按年龄正序排列                                       
        	//return o.age - this.age;  按年龄倒序排列 
    	}                                              
}

Comparator 内部比较器 有多个参数

public class iterator1 {
    public static void main(String[] args) {
        test1 test1 = new test1(1,"A");
        test1 test2 = new test1(2,"Q");
        test1 test3 = new test1(2,"C");
        // 匿名内部类  --- 当这个比较方法只使用一次,我们淡出创建一个类有点浪费
        Set<test1> tests = new TreeSet<test1>(new Comparator<test1>() {
            @Override
            public int compare(test1 o1, test1 o2) {
                double age = o1.age -o2.age;
                if (age> 0) return 1;
                else if (age< 0) return  0;
                // 如果年龄相同,按照姓名倒序
                else  return o1.name.compareTo(o2.name);
            }
        });
}

Map

key - value 形式存储,key不重复

HaseMap:无序,key可以为null

TreeMap: 有序 key 不为null

LinkedHaseMap: key不为null,有序(按照插入顺序,get的元素放在最后面)

key相同时,后者会覆盖前者,返回前者

Hase

1.为什么使用

无序数组的查询时间复杂度为O(n) , 全部遍历一遍

有序数组 用二分查找,时间复杂度0(long2^n)

二叉平衡树 用二分查找,时间复杂度0(long2^n)

hase表 通过hase算法得到存储地址 时间复杂度 O(1)

2.hase冲突

hase算法计算的数据是无限,但是计算后的范围是有限的,所以会出现不同的数据,他们的hasecode是相同的

解决

1.链地址法:HaseMap常用(JDK1.7之前链表+数组,JDK1.8之后链表+数组+红黑树)

​ Hash Map采用的是单向链表法,它具有Next指针,可以连接下一个实体,以此来解决Hash冲突的问题。
image

2,再哈希法:提供多个hase函数,如果第一冲突了,就用第二个hase值去计算

3,公共溢出法:创建一个公共区域。将这些溢出的元素都放在这里

4,开发地址法:当key的哈希地址出现冲突时,从冲突的位置向下找一个为空的数组下标进行存储,如果到最后一个依旧没有找到,那么就从头开始查找(注:该方法有三种分类,这只是其中一个)

3.HaseMap

https://www.bilibili.com/video/av60318686

https://blog.csdn.net/wangzhipeng47/article/details/106751172#comments

JDK1.7之前链表+数组(头插),JDK1.8之后链表+数组+红黑树(尾插)

1.7:在构造方法中创建一个长度16的 Entry【】table数组对象用来存储数据
1.8:在第一次调用put方法创建数组 Node【】table用来存储数据

底层实现

从Java8开始HashMap底层由数组+链表变成数组+链表+红黑树。

​ 使用HashMap时,当使用无参构造方法实例化时,设置扩容因子为默认扩容因子0.75。

​ 当向HashMap添加内容时,会对Key做Hash计算,把得到的Hash值和数组长度-1按位与,计算出存储的位置。

​ 如果数组中该没有内容, 直接存入数组中(Node节点对象), 该下标中有Node对象了, 把内容添加到对应的链表或红黑树中。

​ 如果添加后链表长度大于等于8,会判断数组的长度是否大于等于64,如果小于64对数组扩容,扩容长度为原长度的2倍,扩容后把原Hash表内容重新放入到新的Hash表中。如果Hash长度大于等于64会把链表转换红黑树。

​ 最终判断HashMap中元素个数是否已经达到扩容值(threshold),如果达到扩容值,需要进行扩容,扩容一倍。

​ 反之,如果删除元素后,红黑树的元素个数小于等于6,由红黑树转换为链表。

问题

1. key值怎么选择,用自定义类需要注意什么

一般选择Interager和String类型,因为他们是被final修饰的,不可改变,保证hase值具有不可更改性和计算准确性,减少hase碰撞的几率
1.被final修饰,保证了其不可变性,也就是key不可更改,不会存在获取到的hase值不同的情情
2.重写了equals和hasecode方法,不会出出现hase值计算错误
使用自定义类时,我们要重写equals和hasecode方法.

2. 为什么使用红黑树而不用二叉树

二叉树在特殊情况下会生成一条线性结构,和链表相似,查询速度太慢。红黑树是二叉平衡树,会通过左旋优璇等操作保持平衡,虽然会付出一些代价,但是比起遍历链表而言,代价比较小,如果链表长度过小,就没必要转换为红核数,转换了反而浪费性能,查询速度慢。

3. 什么时候需要把链表转为红黑树

链化:红黑树节点<6
树化:链表长度大于等于8,数组长度大于等于64

4. 加载因子为什么是0.75

加载因子就是元素个数/数组容量
如果加载因子为1,空间利用率大大提升,但是产生hase冲突的机会就会大大增加,链表过长,查询效率慢
如果加载因子为0.5,hase冲突大大减少,但是空间利用率不高,频繁扩容,代价太大。

5. 为什么扩容

当我们添加的数据过多,产生的链表也就越长,当我们查询元素时,就需要遍历整个列表,速度太慢,哈希表的意义也就没有了。为了避免这种情况发生,我们就需要减少链表的长度-----扩容也就是增加数组的长度。

6.什么时候扩容

1.集合初始化,也就是第一次put的时候,数组长度默认为16
2.数组长度<64,链表长度>8
3.达到了阀值 = 加载因子*数组容量

7.HaseMap扩容机制

1.7之前: 数组+链表
1.先生成一个新的数组,之前的2倍
2.遍历老数组中所有的元素
3.获取每个元素的key值,并基于新数组的长度重新进行哈希运算,得到新的下标
4.将元素添加新的数组中
5.所有元素添加完成之后,将新数组赋值给对HaseMap象的table

1.8之后: 数组+链表+红黑树
1.先生成一个新的数组,之前的2倍
2.遍历老数组的每一个元素
	链表:直接获取key值,重新计算下标,添加
	红黑树:先遍历红黑树,计算出每个元素的下标,统计每个下标位置的元素个数,如果这个位置的元素大于8,则生成一个新的红黑树,并将红黑树的根节点添加到新数组的对应的位置,小于8,生成链表,将头节点添加到新数组对应的下标位置
3.所有元素添加完成之后,将新数组赋值给对HaseMap象的table

8. 存储在Node中的hash值, 是否就是key的Hasecode()?

不是,存储的是对Key先做(hasecode)计算, 然后再无符号右位移16, 再按位异或,为了减少hase冲突。`

9. 怎么获取一个线程安全的HaseMap

1.加锁  ---  使用Collections中的`Collections.synchronizedMap(map);`方法返回一个线程安全的HaseMap但是返回的并HaseMap不是真正的。只是一个引用
2.使用ConcurrentHashMapMap,HaseTable(已经废弃)

10. HaseMap和HaseTable的区别

1.线程是否安全: HaseMap是非线程安全的,HaseTable是线程安全的,内部使用的方法基本都经过synchronized修饰(如果想保证线程安全建议使用ConcurrentHashMap,HaseTable已经被淘汰了)

2.效率: 因为线程问题,HaseMap要比HaseTable效率要高一些

3.null值: HaseMap中可以支持key和value为null,key只有一个,value可以为多个。HaseTable key不能为null

4.初始容量和扩容: HaseMap默认初始容量为16,每次扩容,数组为之前的两倍,如果指定了初始容量,将其扩容为2的幂次方。HaseTable默认初始容量为11,每次扩容,数组为之前的两倍+1,如果指定了初始容量,将其扩容为2的幂次方

5.底层结构: HaseMap在JDK1.8之后,使用链表+数组+红黑树来提高查询效率

6.hase计算:   HaseMap的对key的Hasecode进行了二次hash,以获得更好的散列值,然后对table数组长度取模,Hashtable直接使用key的hashcode对table数组的长度直接进行取模

11.哈希表底层采用什么算法计算hase值,还有什么算法可以用
底层采用key的hasecode方法值结合数组长度进行无符号右移,按位异或(^),按位与(&)计算出索引。
换可以用:平方取中法,取余数,伪随机数法,位运算效率最高,其他较低

HaseMap为什么线程不安全
put

有两个线程A和B

首先A插入一个key-value值到HashMap中,计算得到桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了

此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面

假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的

那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,

如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

image

resize扩容

JDK1.7时会发生,因为他使用的头插法

单向链表对表头操作效率最高,因此我们要保证先加入的数据在表头,然后将链表整体下移,因此新添加得数据放在链头,最先加入的数值放在链尾,但如果这个位置没有数据,那就直接放就完了。

【大厂经典面试题,CTO带你深入解读:为什么HashMap会产生死循环?【Java面试】-哔哩哔哩】 https://b23.tv/0fGpcmU

两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。

在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,

转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。

image

我们假设有两个线程同时需要执行resize操作

此时线程A中运行结果如下:

img

线程A挂起后,此时线程B正常执行,并完成resize操作,结果如下:

img

这里需要特别注意的点:由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry

都是主存中最新值:7.next=3,3.next=null。

此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,代码执行过程如下:

newTable[3]=e ----> newTable[3]=3  e=next ----> e=7

此时结果如下:

img

继续循环:

e=7 next=e.next ----> next=3【从主存中取值】
e.next=newTable[3] ----> e.next=3【从主存中取值】
newTable[3]=e ----> newTable[3]=7
e=next ----> e=3

结果如下:

img

再次进行循环:

e=3  next=e.next ----> next=null
e.next=newTable[3] ----> e.next=7 
即:3.next=7 
newTable[3]=e ----> newTable[3]=3 
e=next ----> e=null

注意此次循环:e.next=7,而在上次循环中7.next=3,出现环形链表,并且此时e=null循环结束。

结果如下:

img

源码JDK1.8之后

1.基本属性

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
  //序列化和反序列化时使用相同的id
  private static final long serialVersionUID = 362498820763181265L;
  //初始化容量
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  //最大容量
  static final int MAXIMUM_CAPACITY = 1 << 30;
  //默认负载因子
  static final float DEFAULT_LOAD_FACTOR = 0.75f;
  //树形阈值
  static final int TREEIFY_THRESHOLD = 8;
  //链形阀值
  static final int UNTREEIFY_THRESHOLD = 6;
  //最小树形容量
  static final int MIN_TREEIFY_CAPACITY = 64;
  //节点
  transient Node<K,V>[] table;
  //存储键值对的个数
  transient int size;
  //散列表被修改的次数
  transient int modCount; 
  //扩容之后的数组大小 容量*加载因子
  int threshold;
  //负载因子
  final float loadFactor;
}

2. 构造方法

    //无参构造器
	public HashMap() {
           //加载因子默认为0.75
           this.loadFactor = DEFAULT_LOAD_FACTOR;
       }

    //指定容量大小的构造器,调用的是全参构造器
	public HashMap(int initialCapacity) {
        	//     指定的容器长度 , 加载因子 0.75
           this(initialCapacity, DEFAULT_LOAD_FACTOR);
       }

       //全参构造器       指定的容器长度       加载因子
	public HashMap(int initialCapacity, float loadFactor) {
        	// 如果指定的容器长度 小于 0
           if (initialCapacity < 0)
                // 抛出异常
               throw new IllegalArgumentException("Illegal initial capacity: " +
                       initialCapacity);
        
        //HashMap 的最大容量只能是 MAXIMUM_CAPACITY,哪怕传入的数值大于最大容量,也按照最大容量赋值
           // 如果容器大小大于 最大容量
           if (initialCapacity > MAXIMUM_CAPACITY)
               // 容器长度 设置为  最大容量
               initialCapacity = MAXIMUM_CAPACITY;
        
           //   加载因子小于0   或者    不是一个数字 
           if (loadFactor <= 0 || Float.isNaN(loadFactor))
               throw new IllegalArgumentException("Illegal load factor: " +
                       loadFactor);
        	//加载因子为0.75
           this.loadFactor = loadFactor;
        
           //扩容之后的数组大小
           this.threshold = tableSizeFor(initialCapacity);
       }


        //返回给定目标容量的 2 大小的幂。
        static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
       }


       //创建一个新的HaseMap将传入的子Map中的全部元素逐个添加到HashMap中
	public HashMap(Map<? extends K, ? extends V> m) {
           //设置加载因子
           this.loadFactor = DEFAULT_LOAD_FACTOR;
           putMapEntries(m, false);
       }

3.Node 结点

1.7 之前是Entry 结点,1.8之后则是 Node 结点,其实相差不大,因为都是实现了 Map.Entry (Map 接口中的 Entry 接口)接口,即,实现了 getKey() , getValue() , equals(Object o )和 hashCode() 等方法;

static class Node<K,V> implements Map.Entry<K,V> {
    //hash 值
    final int hash;
    //键
    final K key;
    //值
    V value;
    //后继,链表下一个结点
    Node<K,V> next;
    //全参构造器
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    //返回与此项对应的键
    public final K getKey()        { return key; }
    //返回与此项对应的值
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }
    //hash 值
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    //判断2个Entry是否相等,必须key和value都相等,才返回true  
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

4. 添加键值对

4.1 put()方法

//添加键值对
public V put(K key, V value) {
  /*
   *参数一: 调用hash()方法
   *参数二: 键
   *参数三: 值
   **/
  return putVal(hash(key), key, value, false, true);
}

4.2 hash()方法

static final int hash(Object key) {
  int h;
  //key为空,hase值就是 0 ,否则hasecode移位右移16位进行按位异或运算
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

4.3 putVal()方法

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
             onlyIfAbsent如果为真,则不更改现有值
     *@param onlyIfAbsent if true, don't change existing value
               exist如果为false,则表处于创建模式。
     * @param evict if false, the table is in creation mode.
               return上一个值,如果没有,则返回null
     * @return previous value, or null if none
     */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    //申明tab 和 p 用于操作原数组和结点
    Node<K,V>[] tab; Node<K,V> p;
    int n, i;
    //如果原数组是空或者原数组的长度等于0,那么通过resize()方法进行创建初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        //获取到创建后数组的长度n
        n = (tab = resize()).length;

    //通过key的hash值和 数组长度-1 计算出存储元素结点的数组中位置(和1.7一样)
    //并且,如果该位置为空时,则直接创建元素结点赋值给该位置,后继元素结点为null
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //否则,说明该位置存在元素
        Node<K,V> e; K k;
        //判断table[i]的元素的key是否与添加的key相同,若相同则直接用新value覆盖旧value
        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 {
            //遍历table[i],并判断添加的key是否已经存在,和之前判断一样,hash和equals
            //遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
            for (int binCount = 0; ; ++binCount) {
                //如果遍历的下一个结点为空,那么直接插入
                //该方法是尾插法(与1.7不同)
                //将p的next赋值给e进行以下判断
                if ((e = p.next) == null) {
                    //直接创建新结点连接在上一个结点的后继上
                    p.next = newNode(hash, key, value, null);
				//如果插入结点后,链表的结点数大于等7(8-1,即大于8)时,则进行红黑树的转换
				//注意:不仅仅是链表大于8,并且会在treeifyBin方法中判断数组是否为空或数组长度是否小于64
				//如果小于64则进行扩容,并且不是直接转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    //完成后直接退出循环
                    break;
                }
                //不退出循环时,则判断两个元素的key是否相同
                //若相同,则直接退出循环,进行下面替换的操作
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //否则,让p指向下一个元素结点
                p = e;
            }
        }
        //接着上面的第二个break,如果e不为空,直接用新value覆盖旧value并且返回旧value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //添加成功后,判断实际存在的键值对数量size是否大于扩容阈值threshold(第一次时为12)
    if (++size > threshold)
        //若大于,扩容
        resize();
    //添加成功时会调用的方法(默认实现为空)
    afterNodeInsertion(evict);
    return null;
}

4.4 resize()方法

   //该函数有两种使用情况:初始化哈希表    前数组容量过小,需要扩容
        final Node<K,V>[] resize() {
            //获取原数组
            Node<K,V>[] oldTab = table;
            //获取到原数组的容量oldCap
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            //获取原扩容之后的大小
            int oldThr = threshold;
            //新的容量和阈值目前都为0
            int newCap, newThr = 0;
            if (oldCap > 0) {
                //如果原数组容量大于等于最大容量,那么不再扩容
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //而没有超过最大容量,并且大于初始容量 16,那么扩容为原来的2倍
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                        oldCap >= DEFAULT_INITIAL_CAPACITY)  
                    //扩容为原2倍
                    newThr = oldThr << 1; // double threshold
            }
            //经过上面的if,那么这步为初始化容量(使用有参构造器的初始化)
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                //否则,使用的无参构造器
                //那么,容量为16,阈值为12(0.75*16)
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            //计算新的resize的上限
            if (newThr == 0) {
     float ft = (float)newCap * loadFactor;
 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            //使用新的容量床架一个新的数组
             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            //将新的数组引用赋值给table
            table = newTab;
            //如果原数组不为空,那么就进行元素的移动
            if (oldTab != null) {
                //遍历原数组中的每个位置的元素
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        //如果该位置元素不为空,那么上一步获取元素接着置为空
                        oldTab[j] = null;
                        //判断该元素上是否有链表
                        if (e.next == null)
        //如果无链表,确定元素存放位置,
//扩容前的元素位置为 (oldCap - 1) & e.hash ,所以这里的新的位置只有两种可能:1.位置不变,
//2.变为 原来的位置+oldCap,下面会详细介绍
                            newTab[e.hash & (newCap - 1)] = e;
                            //判断是否是树结点,如果是则执行树的操作
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            //否则,说明该元素上存在链表,那么进行元素的移动
                            //根据变化的最高位的不同,也就是0或者1,将链表拆分开
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                //最高位为0时,则将节点加入 loTail.next
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                //最高位为1,则将节点加入 hiTail.next
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
//通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与newTab[j+oldCap]上面去
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }

ConcurrentHashMap

https://blog.csdn.net/qq_41737716/article/details/90549847

JDK1.7之前

首先将数据分为一段一段的进行存储,然后每一段数据配上一把锁(lock锁),当一个线程占用锁访问其中一个数据时,其他的数据段也可以被其他的线程进行访问。

JDK1.8 之后

锁的的是节点,volatile+CAS实现无锁化,底层使用数组+链表+红黑树

CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

posted @ 2023-01-13 23:22  早日月薪五万  阅读(101)  评论(0编辑  收藏  举报