Yolofyi's Guide
首页
  • 前端文章

    • JavaScript
    • HTML
    • CSS
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • Mysql

    • Mysql
  • Java

    • Java基础
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 助手
收藏
  • 分类
  • 标签
  • 归档

Yolofyi

船是自己,灯塔是自己,岸也是自己
首页
  • 前端文章

    • JavaScript
    • HTML
    • CSS
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • Mysql

    • Mysql
  • Java

    • Java基础
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 助手
收藏
  • 分类
  • 标签
  • 归档
  • Mysql

  • Java

    • Java基础
    • Java并发
    • Spring源码解析
    • 设计模式
    • Java 集合
    • JVM
    • Spring使用与实现总结
    • Java集合面试题及答案
      • 1. hashMap 的原理
      • 2. hashcode 的计算
      • 3. hashMap 参数以及扩容机制
      • 4. get()方法
      • 5. put()方法
      • 6. HashMap 问题 jdk1.8 优化
      • 7. 一些面试题
        • 7.1 HashMap 和 TreeMap 的区别
        • 7.2 hashmap 为什么可以插入空值?
        • 7.3 Hashmap 为什么线程不安全:(hash 碰撞和扩容导致)
        • 7.4 Hashmap 碰撞严重
        • 7.4 HashMap 高并发情况下会出现什么问题?
        • 7.5 HashMap 的存放自定义类时,需要实现自定义类的什么方法?
        • 7.6 Hashmap 为什么线程不安全
        • 7.7 Hashmap 中的 key 可以为任意对象或数据类型吗?
      • 1. 概述
      • 2. JDK1.7 ConCurrentHashMap 原理
      • 3. JDK1.7 Get
        • 3.1 在 get 代码的 ① 和 ② 之间,另一个线程新增了一个 entry
        • 3.2 在 get 代码的 ① 和 ② 之间,另一个线程修改了一个 entry 的 value
        • 3.3 在 get 代码的 ① 之后,另一个线程删除了一个 entry
        • 4. JDK1.7 PUT
        • 5. JDK1.7 Remove
        • 6. JDK1.7 & JDK1.8 size()
        • 5.JDK 1.8 CurrentHashMap 概述
        • 6. JDK1.8 put
        • 7. JDK1.8 get 方法
        • 8.rehash 过程
      • 1.参数
      • 1.put
      • 2.get
      • 3.Remove
      • 4.扩容
      • 4.1 hashtable 和 hashmap 的区别
      • 4.2 HashMap 和 ConCurrentHashMap 区别
      • 4.3 ConcurrentHashMap 和 HashTable 区别
      • 4.4 linkedHashMap
      • 4.5 Linkedhashmap 与 hashmap 的区别
      • 4.6 HashSet
      • 4.7 hashmap 与 hashset 区别
      • 4.8 Collections.sort 内部原理
      • 4.9 hash 算法
      • 1.ArrayList 与 LinkedList 区别
      • 2. Vetor arraylist Linkedlist 区别
        • ArrayList#
        • Vector
        • LinkedList
      • 3.使用 ArrayList 的迭代器会出现什么问题?
      • 4. 数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用 Array 而不是 ArrayList?
      • 5.ArrayList 和 Vector 有何异同点?
        • 相同点:
        • 不同点:
      • 6. 快速失败(fail-fast)和安全失败(fail-safe)
        • 快速失败(fail—fast)
        • 安全失败(fail—safe)
        • Iterator 和 ListIterator 的区别是什么?
        • 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
        • Enumeration 接口和 Iterator 接口的区别有哪些?
  • Tomcat

  • Redis

  • 分布式

  • Linux

  • Docker

  • 后端
  • Java
yolofyi
2020-07-20
目录

Java集合面试题及答案

# 一. HashMap

# 1. hashMap 的原理

hashmap 是数组和链表的结合体,数组每个元素存的是链表的头结点 往 hashmap 里面放键值对的时候先得到 key 的 hashcode,然后重新计算 hashcode, (让 1 分布均匀因为如果分布不均匀,低位全是 0,则后来计算数组下标的时候会 冲突),然后与 length-1 按位与,计算数组出数组下标 如果该下标对应的链表为空,则直接把键值对作为链表头结点,如果不为空,则 遍历链表看是否有 key 值相同的,有就把 value 替换, 没有就把该对象最为链表的第 一个节点,原有的节点最为他的后续节点

# 2. hashcode 的计算

https://www.zhihu.com/question/20733617/answer/111577937 https://blog.csdn.net/justloveyou_/article/details/62893086 Key.hashcode 是 key 的自带的 hascode 函数是一个 int 值 32 位

hashmap jdk1.8 中 hashmap 重计算 hashcode 方法改动: 高 16 位异或低 16 位

return (key == null) ? 0 : h = key.hashCode() ^ (h >>> 16); 首先确认:当 length 总是 2 的 n 次方时, h & (length - 1) 等价于 hash 对 length 取模 , 但是&比%具有更高的效率; Jdk1.7 之前:h & (length - 1);//第三步,取模运算

# 3. hashMap 参数以及扩容机制

初始容量 16,达到阀值扩容,阀值等于最大容量*负载因子,扩容每次 2 倍,总是 2 的 n 次方 扩容机制: 使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里,Java1.重新计算每个元素在数组中的位置。Java1.8 中不是重新计算,而是用了一种更巧妙的方式。

# 4. get()方法

整个过程都不需要加锁

# 5. put()方法

这里 HashMap 里面用到链式数据结构的一个概念。上面我们提到过 Entry 类里面有一个 next 属性,作用是指向下一个 Entry。打个比方, 第一个键值对 A 进来,通过计算其 key 的 hash 得到的 index=0,记做:Entry[0] = A。一会后又进来一个键值对 B,通过计算其 index 也等于 0,现在怎么办?HashMap 会这样做:B.next = A,Entry[0] = B,如果又进来 C,index 也等于 0,那么 C.next = B,Entry[0] = C;这样我们发现 index=0 的地方其实存取了 A,B,C 三个键值对,他们通过 next 这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap 的大致实现

# 6. HashMap 问题 jdk1.8 优化

(1)HashMap 如果有很多相同 key,后面的链很长的话,你会怎么优化?或者你会用什么数据结构来存储?针对 HashMap 中某个 Entry 链太长,查找的时间复杂度可能达到 O(n),怎么优化? Java8 做的改变:

  • HashMap 是数组+链表+红黑树(JDK1.8 增加了红黑树部分),当链表长度>=8 时转化为红黑树 在 JDK1.8 版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap 的性能,其中会用到红黑树的插入、删除、查找等算法。
  • java8 中对 hashmap 扩容不是重新计算所有元素在数组的位置,而是我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash, 只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引+oldCap”。

# 7. 一些面试题

# 7.1 HashMap 和 TreeMap 的区别

Hashmap 使用的是数组+链表,treemap 是红黑树

# 7.2 hashmap 为什么可以插入空值?

HashMap 中添加 key==null 的 Entry 时会调用 putForNullKey 方法直接去遍历 table[0]Entry 链表,寻找 e.key==null 的 Entry 或者没有找到遍历结束

如果找到了 e.key==null,就保存 null 值对应的原值 oldValue,然后覆盖原值,并返回 oldValue 如果在 table[0]Entry 链表中没有找到就调用 addEntry 方法添加一个 key 为 null 的 Entry

# 7.3 Hashmap 为什么线程不安全:(hash 碰撞和扩容导致)

  • HashMap 底层是一个 Entry 数组,当发生 hash 冲突的时候,hashmap 是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。假如 A 线程和 B 线程同时对同一个数组位置调用 addEntry,两个线程会同时得到现在的头结点,然后 A 写入新的头结点之后,B 也写入新的头结点,那 B 的写入操作就会覆盖 A 的写入操作造成 A 的写入操作丢失
  • 删除键值对的代码如上:当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去, 其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改
  • 当多个线程同时检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组 table,结果最终只有最后一个线程生成的新数组被赋给 table 变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的 table 作为原始数组,这样也会有问题。

# 7.4 Hashmap 碰撞严重

可以自定义重写 hash——多 hash 函数

# 7.4 HashMap 高并发情况下会出现什么问题?

扩容问题

# 7.5 HashMap 的存放自定义类时,需要实现自定义类的什么方法?

答:hashCode 和 equals。通过 hash(hashCode)然后模运算(其实是与的位操作)定位在 Entry 数组中的下标,然后遍历这之后的链表,通过 equals 比较有没有相同的 key,如果有直接覆盖 value,如果没有就重新创建一个 Entry。

# 7.6 Hashmap 为什么线程不安全

hash 碰撞和扩容导致,HashMap 扩容的的时候可能会形成环形链表,造成死循环。

# 7.7 Hashmap 中的 key 可以为任意对象或数据类型吗?

可以为 null 但不能是可变对象,如果是可变对象的话,对象中的属性改变,则对象 HashCode 也进行相应的改变,导致下次无法查找到己存在 Map 中的效据 如果可变对象在 HashMap 中被用作键,时就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可.

# 二. CurrentHashMap

http://www.importnew.com/21781.html https://blog.csdn.net/dingji_ping/article/details/51005799 https://www.cnblogs.com/chengxiao/p/6842045.html http://ifeve.com/hashmap-concurrenthashmap-%E7%9B%B8%E4%BF%A1%E7%9C%8B%E5%AE%8C%E8%BF%99%E7%AF%87%E6%B2%A1%E4%BA%BA%E8%83%BD%E9%9A%BE%E4%BD%8F%E4%BD%A0%EF%BC%81/

# 1. 概述

一个 ConcurrentHashMap 维护一个 Segment 数组,一个 Segment 维护一个 HashEntry 数组。

# 2. JDK1.7 ConCurrentHashMap 原理

其中 Segment 继承于 ReentrantLock ConcurrentHashMap 使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问, 能够实现真正的并发访问。

Segment 继承了 ReentrantLock,表明每个 segment 都可以当做一个锁。这样对每个 segment 中的数据需要同步操作的话都是使用每个 segment 容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的 segment。

# 3. JDK1.7 Get

  • CurrentHashMap 是否使用了锁???

它也没有使用锁来同步,只是判断获取的 entry 的 value 是否为 null,为 null 时才使用加锁的方式再次去获取。 这里可以看出并没有使用锁,但是 value 的值为 null 时候才是使用了加锁!!!

  • Get 原理: 第一步,先判断一下 count != 0;count 变量表示 segment 中存在 entry 的个数。如果为 0 就不用找了。假设这个时候恰好另一个线程 put 或者 remove 了这个 segment 中的一个 entry,会不会导致两个线程看到的 count 值不一致呢?看一下 count 变量的定义: transient volatile int count; 它使用了 volatile 来修改。我们前文说过,Java5 之后,JMM 实现了对 volatile 的保证:对 volatile 域的写入操作 happens-before 于每一个后续对同一个域的读写操作。所以,每次判断 count 变量的时候,即使恰好其他线程改变了 segment 也会体现出来

# 3.1 在 get 代码的 ① 和 ② 之间,另一个线程新增了一个 entry

如果另一个线程新增的这个 entry 又恰好是我们要 get 的,这事儿就比较微妙了。下图大致描述了 put 一个新的 entry 的过程。

因为每个 HashEntry 中的 next 也是 final 的,没法对链表最后一个元素增加一个后续 entry 所以新增一个 entry 的实现方式只能通过头结点来插入了。newEntry 对象是通过  new HashEntry(K k , V v, HashEntry next)  来创建的。如果另一个线程刚好 new 这个对象时,当前线程来 get 它。因为没有同步,就可能会出现当前线程得到的 newEntry 对象是一个没有完全构造好的对象引用。 如果在这个 new 的对象的后面,则完全不影响,如果刚好是这个 new 的对象,那么当刚好这个对象没有完全构造好,也就是说这个对象的 value 值为 null,就出现了如下所示的代码,需要重新加锁再次读取这个值!

# 3.2 在 get 代码的 ① 和 ② 之间,另一个线程修改了一个 entry 的 value

value 是用 volitale 修饰的,可以保证读取时获取到的是修改后的值。

# 3.3 在 get 代码的 ① 之后,另一个线程删除了一个 entry

假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3 这个 entry,因为 HashEntry 中 next 的不可变,所以我们无法直接把 e2 的 next 指向 e4,而是将要删除的节点之前的节点复制一份,形成新的链表。它的实现大致如下图所示: 如果我们 get 的也恰巧是 e3,可能我们顺着链表刚找到 e1,这时另一个线程就执行了删除 e3 的操作,而我们线程还会继续沿着旧的链表找到 e3 返回。这里没有办法实时保证了,也就是说没办法看到最新的。 我们第 ① 处就判断了 count 变量,它保障了在 ① 处能看到其他线程修改后的。① 之后到 ② 之间,如果再次发生了其他线程再删除了 entry 节点,就没法保证看到最新的了,这时候的 get 的实际上是未更新过的!!!。 不过这也没什么关系,即使我们返回 e3 的时候,它被其他线程删除了,暴漏出去的 e3 也不会对我们新的链表造成影响。

# 4. JDK1.7 PUT

  • 1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  • 2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  • 3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  • 4.最后会解除在 1 中所获取当前 Segment 的锁。
  • 5.可以说是首先找到 segment,确定是哪一个 segment,然后在这个 segment 中遍历查找 key 值是要查找的 key 值得 entry,如果找到,那么就修改该 key,如果没找到,那么就在头部新加一个 entry.

# 5. JDK1.7 Remove

# 6. JDK1.7 & JDK1.8 size()

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
1
2
3
4
5
6

volatile 保证内存可见,最大是 65535.

# 5.JDK 1.8 CurrentHashMap 概述

1.其中抛弃了原有的 Segment 分段锁,而采用了  CAS + synchronized  来保证并发安全性。 2.大于 8 的时候才去红黑树链表转红黑树的阀值,当 table[i]下面的链表长度大于 8 时就转化为红黑树结构。

# 6. JDK1.8 put

  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

# 7. JDK1.8 get 方法

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。

# 8.rehash 过程

  • Redis rehash :dictRehash 每次增量 rehash n 个元素,由于在自动调整大小时已设置好了 ht[1]的大小,因此 rehash 的主要过程就是遍历 ht[0],取得 key,然后将该 key 按 ht[1]的 桶的大小重新 rehash,并在 rehash 完后将 ht[0]指向 ht[1],然后将 ht[0]清空。在这个过程中 rehashidx 非常重要,它表示上次 rehash 时在 ht[0]的下标位置。
  • 可以看到,redis 对 dict 的 rehash 是分批进行的,这样不会阻塞请求,设计的比较优雅。
  • 但是在调用 dictFind 的时候,可能需要对两张 dict 表做查询。唯一的优化判断是,当 key 在 ht[0]不存在且不在 rehashing 状态时,可以速度返回空。如果在 rehashing 状态,当在 ht[0]没值的时候,还需要在 ht[1]里查找。
  • dictAdd 的时候,如果状态是 rehashing,则把值插入到 ht[1],否则 ht[0]

# 三 Hashtable

https://blog.csdn.net/ns_code/article/details/36191279

# 1.参数

-(1)table 是一个 Entry[]数组类型,而 Entry 实际上就是一个单向链表。哈希表的"key-value 键值对"都是存储在 Entry 数组中的。-(2)count 是 Hashtable 的大小,它是 Hashtable 保存的键值对的数量。

  • (3)threshold 是 Hashtable 的阈值,用于判断是否需要调整 Hashtable 的容量。threshold 的值="容量*加载因子"。
  • (4)loadFactor 就是加载因子。
  • (5)modCount 是用来实现 fail-fast 机制的

# 1.put

从下面的代码中我们可以看出,Hashtable 中的 key 和 value 是不允许为空的,当我们想要想 Hashtable 中添加元素的时候,首先计算 key 的 hash 值,然 后通过 hash 值确定在 table 数组中的索引位置,最后将 value 值替换或者插入新的元素,如果容器的数量达到阈值,就会进行扩充。

# 2.get

# 3.Remove

在下面代码中,如果prev为null了,那么说明第一个元素就是要删除的元素,那么就直接指向第一个元素的下一个即可。![](https://github.com/gzc426/picts/blob/master/%E5%9B%BE%E7%89%8723.png)

# 4.扩容

  • 默认初始容量为 11
  • 线程安全,但是速度慢,不允许 key/value 为 null
  • 加载因子为 0.75:即当 元素个数 超过 容量长度的 0.75 倍 时,进行扩容
  • 扩容增量:2*原数组长度+1 如 HashTable 的容量为 11,一次扩容后是容量为 23

# 四. 一些面试题

# 4.1 hashtable 和 hashmap 的区别

# 4.2 HashMap 和 ConCurrentHashMap 区别

# 4.3 ConcurrentHashMap 和 HashTable 区别

  • ConcurrentHashMap 仅仅锁定 map 的某个部分,而 Hashtable 则会锁定整个 map。
  • hashtable(同一把锁):使用 synchronized 来保证线程安全,但效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
  • concurrenthashmap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
  • concurrenthashmap 是由 Segment 数组结构和 HahEntry 数组结构组成。Segment 是一种可重入锁 ReentrantLock,扮演锁的角色。HashEntry 用于存储键值对数据。一个 concurrenthashmap 里包含一个 Segment 数组。Segment 的结构和 Hashmap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment。

# 4.4 linkedHashMap

https://blog.csdn.net/justloveyou_/article/details/71713781

# 4.5 Linkedhashmap 与 hashmap 的区别

  • 1.LinkedHashMap 是 HashMap 的子类 -2.LinkedHashMap 中的 Entry 增加了两个指针 before 和 after,它们分别用于维护双向链接列表。
  • 3.在 put 操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 put 操作,但是在细节上还是做了一定的调整,比如,在 LinkedHashMap 中向哈希表中插入新 Entry 的同时,还会通过 Entry 的 addBefore 方法将其链入到双向链表中。
  • 4.在扩容操作上,虽然 LinkedHashMap 完全继承了 HashMap 的 resize 操作,但是鉴于性能和 LinkedHashMap 自身特点的考量,LinkedHashMap 对其中的重哈希过程(transfer 方法)进行了重写
  • 5.在读取操作上,LinkedHashMap 中重写了 HashMap 中的 get 方法,通过 HashMap 中的 getEntry 方法获取 Entry 对象。在此基础上,进一步获取指定键对应的值。

# 4.6 HashSet

对于 HashSet 而言,它是基于 HashMap 实现的 Hashset 源码 http://zhangshixi.iteye.com/blog/673143

Hashset 如何保证集合的没有重复元素? 可以看出 hashset 底层是 hashmap 但是存储的是一个对象,hashset 实际将该元素 e 作为 key 放入 hashmap,当 key 值(该元素 e)相同时,只是进行更新 value,并不会新增加,所以 set 中的元素不会进行改变。

# 4.7 hashmap 与 hashset 区别

# 4.8 Collections.sort 内部原理

重写 Collections.sort()

import java.util.*;
class xd{
    int a;
    int b;
    xd(int a,int b){
        this.a = a;
        this.b = b;
    }
}
public class Main {
    public static void main(String[] arg) {
        xd a = new xd(2,3);
        xd b = new xd(4,1);
        xd c = new xd(1,2);
        ArrayList<xd> array = new ArrayList<>();
        array.add(a);
        array.add(b);
        array.add(c);
        Collections.sort(array, new Comparator<xd>() {
            @Override
            public int compare(xd o1, xd o2) {
                if(o1.a > o2.a)
                    return 1;
                else if(o1.a < o2.a)
                    return -1;
                return 0;
            }
        });
        for(int i=0;i<array.size();i++)
            System.out.println(array.get(i).a);
        for(int i=0;i<array.size();i++)
            System.out.println(array.get(i).b);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 4.9 hash 算法

# 五.ArrayList,LinkedList 和 Vector 的区别和实现原理

Vector :
https://blog.csdn.net/chenssy/article/details/37520981

# 1.ArrayList 与 LinkedList 区别

ArrayList 和 LinkedList 都实现了 List 接口,他们有以下的不同点: ArrayList 是基于索引的数据接口,它的底层是数组。它可以以 O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList 是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是 O(n)。 相对于 ArrayList,LinkedList 的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。 LinkedList 比 ArrayList 更占内存,因为 LinkedList 为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

# 2. Vetor arraylist Linkedlist 区别

# ArrayList#

ArrayList## 就是动态数组,是 Array 的复杂版本,动态的增加和减少元素.当更多的元素加入到 ArrayList 中时,其大小将会动态地增长。它的元素可以通过 get/set 方法直接访问,因为 ArrayList 本质上是一个数组。初始容量为 10。

  • 1.插入元素的时候可能扩容,删除元素时不会缩小容量。
  • 2.扩容增长为 Arraylist 增长原来的 0.5 倍
    1. 而 Arraylist 没有设置增长空间的方法。
  • 4.线程不同步

# Vector

Vector 和 ArrayList 类似, 区别在于 Vector 是同步类(synchronized).因此,开销就比 ArrayList 要大。初始容量为 10。实现了随机访问接口,可以随机访问。Vector 是内部是以动态数组的形式来存储数据的。

  • 1.Vector 还可以设置增长的空间大小,
  • 2.  及 Vector 增长原来的 1 倍 3.vector 线程同步

# LinkedList

LinkedList 是一个双链表,在添加和删除元素时具有比 ArrayList 更好的性能.但在 get 与 set 方面弱于 ArrayList.当然,这些对比都是指数据量很大或者操作很频繁的情况下的对比。它还实现了 Queue 接口,该接口比 List 提供了更多的方法,包括 offer(),peek(),poll()等.

ArrayList 和 LinkedList 的使用场景,其中 add 方法的实现 ArrayList,LinkedList 的实现以及插入,查找,删除的过程

# 3.使用 ArrayList 的迭代器会出现什么问题?

单线程和多线程环境下; 常用的迭代器设计模式,iterator 方法返回一个父类实现的迭代器。

  • 1、迭代器的 hasNext 方法的作用是判断当前位置是否是数组最后一个位置,相等为 false,否则为 true。
  • 2、迭代器 next 方法用于返回当前的元素,并把指针指向下一个元素,值得注意的是,每次使用 next 方法的时候,都会判断创建迭代器获取的这个容器的计数器 modCount 是否与此时的不相等,不相等说明集合的大小被修改过,如果是会抛出 ConcurrentModificationException 异常,如果相等调用 get 方法返回元素即可。

# 4. 数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用 Array 而不是 ArrayList?

答:不同点:定义上:Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。容量上:Array 大小固定,ArrayList 的大小是动态变化的。操作上:ArrayList 提供更多的方法和特性,如:addAll(),removeAll(),iterator()等等。使用基本数据类型或者知道数据元素数量的时候可以考虑 Array;ArrayList 处理固定数量的基本类型数据类型时会自动装箱来减少编码工作量,但是相对较慢。

# 5.ArrayList 和 Vector 有何异同点?

# 相同点:

  • (1)两者都是基于索引的,都是基于数组的。
  • (2)两者都维护插入顺序,我们可以根据插入顺序来获取元素。
  • (3)ArrayList 和 Vector 的迭代器实现都是 fail-fast 的。
  • (4)ArrayList 和 Vector 两者允许 null 值,也可以使用索引值对元素进行随机访问。

# 不同点:

  • (1)Vector 是同步,线程安全,而 ArrayList 非同步,线程不安全。对于 ArrayList,如果迭代时改变列表,应该使用 CopyOnWriteArrayList。
  • (2)但是,ArrayList 比 Vector 要快,它因为有同步,不会过载。
  • (3)在使用上,ArrayList 更加通用,因为 Collections 工具类容易获取同步列表和只读列表。

# 6. 快速失败(fail-fast)和安全失败(fail-safe)

# 快速失败(fail—fast)

  • 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。
  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
  • 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。
  • 场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

# 安全失败(fail—safe)

  • 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
  • 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
  • 缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
  • 场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出 ConcurrentModification 异常,java.util 下都是快速失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在 java.util.concurrent 下都是安全失败

# Iterator 和 ListIterator 的区别是什么?

答:Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历 List。Iterator 对集合只能是前向遍历,ListIterator 既可以前向也可以后向。 ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。

# 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

答:Iterator 的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util 包下面的所有的集合类都是快速失败的,而 java.util.concurrent 包下面的所有的类都是安全失败的。快速失败的迭代器会抛出 ConcurrentModificationException 异常,而安全失败的迭代器永远不会抛出这样的异常。

# Enumeration 接口和 Iterator 接口的区别有哪些?

答:Enumeration 速度是 Iterator 的 2 倍,同时占用更少的内存。但是,Iterator 远远比 Enumeration 安全,因为其他线程不能够修改正在被 iterator 遍历的集合里面的对象。同时,Iterator 允许调用者删除底层集合里面的元素,这对 Enumeration 来说是不可能的。

上次更新: 2023/08/06, 22:51:57
Spring使用与实现总结
Tomcat源码解析

← Spring使用与实现总结 Tomcat源码解析→

最近更新
01
MySQL开发规范及慢查询优化
08-25
02
linux增加swap交换空间
08-16
03
uni-app云打包Android Apk
08-13
更多文章>
| Copyright © 2022-2023 yolofyi.com - All rights reserved | 鄂ICP备2022003053号 |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式