集合源码解读笔记

ArrayList:

Arraylist底层是基于数组实现

add方法如何实现?

1.判断集合容量是否装的下
2.如果装不下则扩容是以1.5倍将原来数组的容量拷贝到新的数组中。 

Get方法如何实现?

1.直接提供了根据index下标查询效率非常高

Remove方法如何实现的呢?

1.查找到删除对应的index下标位置+1到最后index元素值向前移动一位。 

Vector 与 ArrayList集合区别相同点:

相同点:

1. ArrayList和 Vector 默认初始化容量=10

2.底层都是基于数组实现
3. List接口下子类

不同点:

1. ArrayList线程不安全Vector线程是安全的

2. ArrayList每次扩容是原来容量的1.5倍,Vector每次扩容是原来容量的2倍;Vector可以设置每次扩容的容量。
4. ArrayList是以懒加载的形式初始化容量,Vector直接通过构造函数初始化数组容量=10

HashMap 与 HashTable 的区别  

备注:让面试官引入 多线程、锁、 ConcurrentHashMap
1.HashMap 线程不安全 HashTable 线程是安全的采用 synchronized
2.HashMap 允许存放 key null HashTable 不允许存放 key null
3. 在多线程的情况下,推荐使用 ConcurrentHashMap 线程安全 且效率非常高
面试官下一个问题 就会问题说“ ConcurrentHashMap 原理”

重写了 equals 方法 为什么 也需要重写 HashCode 方法?

与 equals 区别  

== 比较两个对象内存地址
Equals 方法属于 Object 父类中,默认的情况下两个对象内存地址是否相等,只是我们重写 Object 父类中 Equals 方法来实现 比较 对象属性值是否相等
两个对象值如果相等的话, HashCode 相等
两个对象值如果不相等的话, HashCode 不一定相等
两个对象 hashCode 值 如果相等, 值是否相等 不一定
两个对象值如果是相等的话, hashcode 是相等。

什么是 Hash 冲突

 Key 值不同 但是 hashcode 值相等

谈谈 HashMap 的 Hash 冲突如何解决的  

JDK1.7 数组 + 链表 链表缺陷:如果链表过长的情况下查询的时间复杂度就是为 o(n) 需要从头查询尾部 效率非常低;
JDK1.8 数组 + 链表 + 红黑树

HashMap 底层是如何实现的 

HashMap1.7 版本中底层是基于数组 + 链表实现的,如果发生 Hash 冲突概率问题,会存放到同一个链表中,链表如果过长 会从头查询到尾部 效率非常低。
所以在 HashMap1.8 版本 (数组容量 >=64& 链表长度大于 8 ) 就会将该链表转化红黑树。
public class DemoHashMap<K, V> {
    private Entry[] objects = new Entry[10000];

    class Entry<K, V> {
        K k;
        V v;
        Entry<K, V> next;

        public Entry(K k, V v) {
            this.k = k;
            this.v = v;
        }
    }

    public void put(K k, V v) {
        int index = k.hashCode() % objects.length;
        Entry<K, V> oldEntry = objects[index];
        if (oldEntry == null) {
            objects[index] = new Entry<K, V>(k, v);
        } else {
            oldEntry.next = new Entry<K, V>(k, v);
        }
    }

    public V get(K k) {
        int index = k.hashCode() % objects.length;
        for (Entry<K, V> entry = objects[index]; entry != null; entry = entry.next) {
            if (entry.k.equals(k) || entry.k == k) {
                return entry.v;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        DemoHashMap demoHashMap = new DemoHashMap();
        demoHashMap.put("a", "a");
        demoHashMap.put(97, 97);
        System.out.println(demoHashMap.get("a"));
        System.out.println(demoHashMap.get(97));
    }
}

HashMap 根据 Key 查询时间复杂度?

1.Key 没有产生 hash 冲突 时间复杂度是为 o(1); 只需要查询一次

2.Key 产生 hash 冲突 采用链表存放则为 O(N) 从头查询到尾部
3.key 产生 hash 冲突采用红黑树存放则为 O (LogN)

HashMap 底层是有序存放的吗?

是无序的,因为 Hash 算法是散列计算的 没有顺序,如果需要顺序可以使用LinkedHashMap集合采用双向链表存放。

Put(1,1) --index=6
Put(2,2)---index=0
Put(3,3)---index=7
2,1,3
遍历是根据数组 index=0

HashMap7 扩容产生死循环问题有了解过吗?

其实这个 JDK 官方不承认这个 bug,因为 HashMap 本身是线程不安全的,不推荐在多线程的情况下使用,是早期阿里一名员工发生在多线程的情况下使用

HashMap1.7 扩容会发生死循环问题,因为 HashMap1.7 采用头插入法 
HashMap1.8 改为尾插法 。
如果是在多线程的情况下 推荐使用 ConcurrentHashMap

HashMap Key 为 null 存放在 什么位置

存放在数组 index 0 的位置。

ConcurrentHashMap 底层是如何实现?  

1.传统方式 使用 HashTable 保证线程问题,是采用 synchronized 锁将整个 HashTable 中的数组锁住,在多个线程中只允许一个线程访问 Put 或者 Get,效率非常低,但是能够保证线

程安全问题。
2. 多线程的情况下 JDK 官方推荐使用 ConcurrentHashMap:
ConcurrentHashMap 1.7 采用分段锁设计底层实现原理:数组 +Segments 分段锁+HashEntry 链表实现
大致原理就是将一个大的 HashMap 分成 n 多个不同的小的 HashTable 不同的 key 计算 index 如果没有发生冲突 则存放到不同的小的 HashTable 中,从而可以实现多线程,同时做 put 操作,但是如果多个线程同时 put 操作 key 发生了 index 冲突落到同一个小的 HashTable 中还是会发生竞争锁。
3.ConcurrentHashMap 1.7 采用 Lock +CAS 乐观锁 +UNSAFE 类 里面有实现 类
似于 synchronized 锁的升级过程。
4.ConcurrentHashMap 1.8 版本 put 操作 取消 segment 分段设计 直接使用 Node
数组来保存数据 index 没有发生冲突使用 cas index 如果发生冲突则 使用 synchronized

 HashMap 加载因子为什么是0.75而0.5 或者 1.0 呢?

加载因子也叫扩容因子或负载因子,用来判断什么时候进行扩容的。

关于loadFactor在JDK官方文档里有说明:一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。

那加载因子为什么是 0.75 而不是 0.5 或者 1.0 呢?

假如加载因子是 0.5,HashMap 的初始化容量是 16,那么当 HashMap 中有 16*0.5=8 个元素时,HashMap 就会频繁的扩容,浪费空间。

如果加载因子是1,容量使用默认初始值16,那么表示一个HashMap需要在"满了"之后才会进行扩容。那么在HashMap中,最好的情况是这16个元素通过hash算法之后分别落到了16个不同的桶中,否则就必然发生哈希碰撞。而且随着元素越多,哈希碰撞的概率越大,查找速度也会越低。

这其实是出于容量和性能之间平衡的结果:

当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子

 loadFactor=0.75科学依据:

 a .根据数学公式推算。负载因子为log(2)的时候,可以既减少哈希冲突,又浪费空间,是时间和空间的权衡。

    log(2)大约为0.7。

 b.根据HashMap的扩容机制,应该保证capacity的值永远都是2的幂。

    为了保证负载因子(loadFactor) * 容量(capacity)的结果是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的幂乘积结果都是整数。

 HashMap底层是怎么做扩容的?

JDK 1.7:

创建一个新的table,并调用transfer方法把旧数组中的数据迁移到新数组中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致死链现象。

JDK1.8(resize做了很多优化):

  1. 在resize()方法中,定义了oldCap参数,记录了原table的长度,定义了newCap参数,记录新table长度,newCap是oldCap长度的2倍,然后循环原table,把原table中的每个链表中的每个元素放入新table
  2. 计算索引做了优化:hash(原始hash) & oldCap(原始容量) == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap(原始容量)
  3. 在JDK1.8中发生hashmap扩容时,遍历hashmap每个bucket里的链表,每个链表可能会被拆分成两个链表,不需要移动的元素置入loHead为首的链表,需要移动的元素置入hiHead为首的链表,然后分别分配给老的buket和新的buket

注意:

  • 1.7 是大于阈值(threshold = factor * capacity )且没有空位时才扩容,而 1.8 是大于阈值就扩容;

  • 1.7是先扩容再插入数据,1.8是先插入数据再扩容;

  • 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容;

  • HashMap的容量达到2的30次方,就不会在进行扩容了;

    Hashmap采用int类型,这是结合性能考虑做出的选择

    由于int类型限制了该变量的长度为4个字节共32个二进制位,按理说可以向左移动31位即2的31次幂。但是事实上由于二进制数字中最高的一位也就是最左边的一位是符号位,用来表示正负之分(0为正,1为负),所以只能向左移动30位,而不能移动到处在最高位的符号位!

 HashMap 的Key如何减少index冲突(hash冲突)?

比较出名的有四种(1)开放定址法(2)链地址法(3)再哈希法(4)公共溢出区域法

  1. 开放地址法,当前key被占领了就找下一个位置,下一个位置的定位方案有线性探测再散列、平方探测再散列、随机探测再散列。
  2. 链地址法,hash相同的放在一个新的叫同义词链的链表里,链表头指针放到value位置。
  3. 再哈希,运用多个哈希函数,一个的哈希值冲突就用下一个。
  4. 建立公共溢出区,将哈希表分为基本表和溢出表,凡是是基本表发生冲突的,放入溢出表中。
    hashmap采用的是链地址法。

  HashMap中put操作

首次put:map.put("haha","haha的值");

将"haha的值"插入到"haha"的键的node中。

1)计算出"haha"的hash值

int hash = hash("haha");

2)判断数组是否为空

判断Node[]数组是否为空,为空的话按照初始长度开辟数组空间

3)对hash值作减一与运算得到数组下标

直接算出来的hash值可能非常大,不可能直接当作数组下标的。对此hashmap的设计者有自己的解决方案:求余

也就是:index = hash值 % 数组长度

这样的话index的值永远都在数组长度之内,也就可以作为数组下标了

但是这样做有一个缺点:效率低,于是hashmap的设计者把求余改成了一个效率更高的运算:减一与运算

也就是:index = hash值 & (数组长度-1)

为什么这样得出来的index也在数组长度之内呢?可以看下例子(由于是位运算,需要把hash值和数组长度分解成二进制,这样看的更清楚,假设它两二进制只有八位):

数组长度:    0001 0000

数组长度-1: 0000 1111

hash值:       1101 0101

与操作:        0000 0101

可以看到,数组长度-1后,前四位变成了0,跟hash值作与操作之后,hash值前四位不管是什么,都会变成0,而后四位由于对方都是1,因此得以保留下来。这样得到最后的结果永远都不会超过数组长度。

这里必须要满足一个前提条件:数组长度必须要是2的n次方。因为这样才能保证数组长度-1之后,前面为0,后面为1。

4)把值赋给对应的node

数组下标拿到了,要插入的位置也就基本确定了。在插入之前,hashmap会去判断当前数组位置上有没有元素,由于我们这是第一次插入,因此这里就是直接插入元素。

这里插入的方式很简单,就是把node的四大参数(hash值、key、value、next)赋给当前数组位置上的node。由于是位置上第一个元素,后继没有链表元素,next的值就是null。

5)插入后操作

插入之后,hashmap的全局变量:size,也就是已有元素数量,加一,然后看下有没有大于扩容阈值,如果大的话就要扩容。

6)最终效果(这里设"haha"算出的index为2)

haha put结果

haha put结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值