• Java 相关高频面试解析


    目录

    1. HashMap

    (1)问:HashMap 有用过吗?您能给我说说他的主要用途吗?

    (2)问:您能说说 HashMap 常用操作的底层实现原理吗?如存储 put(K key, V value),查找 get(Object key),删除 remove(Object key),修改 replace(K key, V value)等操作。

    (3).问:您能说说 HashMap 和 HashTable 的区别吗?

    (4)问:您说 HashMap 不是线程安全的,那如果多线程下,它是如何处理的?并且什么情况下会发生线程不安全的情况?

    (5)问:我们在使用 HashMap 时,选取什么对象作为 key 键比较好,为什么?

    总结

    2. ArrayList

    定义

    ArrayList 的构造器

    add 方法源码分析

    get 方法源码分析

    set 方法源码分析

    3.LinkedList

    定义

    4.Hashset

    5、List、Set、Map区别



    1. HashMap

    (1)问:HashMap 有用过吗?您能给我说说他的主要用途吗?

    答:有用过,我在平常工作中经常会用到 HashMap 这种数据结构, HashMap 是基
    Map 接口实现的一种键 - 值对 的存储结构,允许 null 值,同时非
    有序,非同步 ( 即线程不安全 ) HashMap 的底层实现是数组 + 链表 + 红黑树
    JDK1.8 增加了红黑树部分)。它存储和查找数据时,是根据键 key hashCode
    的值计算出具体的存储位置。 HashMap 最多只允许一条记录的键 key null
    HashMap 增删改查等常规操作都有不错的执行效率,是 ArrayList LinkedList
    等数据结构的一种折中实现。
    示例代码:
    // 创建一个 HashMap,如果没有指定初始大小,默认底层 hash 表数组的
    大小为 16
    HashMap < String , String > hashMap = new HashMap();
    // 往容器里面添加元素
    hashMap .put("小明", "好帅");
    hashMap .put("老王", "坑爹货");
    hashMap .put("老铁", "没毛病");
    hashMap .put("掘金", "好地方");
    hashMap .put("王五", "别搞事");
    // 获取 key 为小明的元素 好帅
    String element = hashMap .get("小明");
    // value : 好帅
    System .out.println( element );
    // 移除 key 为王五的元素
    String removeElement = hashMap .remove("王五");
    // value : 别搞事
    System .out.println( removeElement );
    // 修改 key 为小明的元素的值 value 为 其实有点丑
    hashMap .replace("小明", "其实有点丑");
    // {老铁=没毛病, 小明=其实有点丑, 老王=坑爹货, 掘金=好地方}
    System .out.println( hashMap );
    // 通过 put 方法也可以达到修改对应元素的值的效果
    hashMap .put("小明", "其实还可以啦,开玩笑的");
    // {老铁=没毛病, 小明=其实还可以啦,开玩笑的, 老王=坑爹货, 掘金=
    好地方}
    System .out.println( hashMap );
    // 判断 key 为老王的元素是否存在(捉奸老王)
    boolean isExist = hashMap .containsKey("老王");
    // true , 老王竟然来搞事
    System .out.println( isExist );
    // 判断是否有 value = "坑爹货" 的人
    boolean isHasSomeOne = hashMap .containsValue("坑爹货");
    // true 老王是坑爹货
    System .out.println( isHasSomeOne );
    // 查看这个容器里面还有几个家伙 value : 4
    System .out.println( hashMap .size());

    (2)问:您能说说 HashMap 常用操作的底层实现原理吗?如存储 put(K key, V value),查找 get(Object key),删除 remove(Object key),修改 replace(K key, V value)等操作。

    2.1):调用 put(K key, V value) 操作添加 key-value 键值对时,进行了如下操作:
    判断哈希表 Node[] table 是否为空或者 null ,是则执行 resize()
    方法进行扩容。

    根据插入的键值 key hash 值,通过(n - 1) & hash 当前元素的 hash

    & hash 表长度 - 1 (实际就是 hash % hash 表长度)
    计算出存储位
    table[i] 。如果存储位置没有元素存放,则将新增结点存储在此位置
    table[i]
    如果存储位置已经有键值对元素存在,则判断该位置元素的 hash 值和 key
    值是否和当前操作元素一致,一致则证明是修改 value 操作,覆盖 value
    即可。
    当前存储位置即有元素,又不和当前操作元素一致,则证明此位置
    table[i] 已经发生了 hash 冲突,则通过判断头结点是否是 treeNode ,如
    果是 treeNode 则证明此位置的结构是红黑树,已红黑树的方式新增结点。
    如果不是红黑树,则证明是单链表,将新增结点插入至链表的最后位置,
    随后判断当前链表长度是否 大于等于 8 ,是则将当前存储位置的链表转
    化为红黑树。遍历过程中如果发现 key 已经存在,则直接覆盖 value
    插入成功后,判断当前存储键值对的数量 大于 阈值 threshold 是则扩
    容。
    2.2):调用 get(Object key) 操作根据键 key 查找对应的 key-value 键值对时,进行
    了如下操作:
    先调用 hash(key) 方法计算出 key hash
    根据查找的键值 key hash 值,通过 (n - 1) & hash 当前元素的 hash
    & hash 表长度 - 1 (实际就是 hash % hash 表长度)
    计算出存储位
    table[i] ,判断存储位置是否有元素存在 。
    如果存储位置有元素存放,则首先比较头结点元素,如果头结点的 key
    hash 值 和 要获取的 key hash 值相等,并且 头结点的 key 本身 和要
    获取的 key 相等,则返回该位置的头结点。
    如果存储位置没有元素存放,则返回 null
    如果存储位置有元素存放,但是头结点元素不是要查找的元素,则需要遍
    历该位置进行查找。
    先判断头结点是否是 treeNode ,如果是 treeNode 则证明此位置的结构是
    红黑树,以红色树的方式遍历查找该结点,没有则返回 null
    如果不是红黑树,则证明是单链表。遍历单链表,逐一比较链表结点,链
    表结点的 key hash 值 和 要获取的 key hash 值相等,并且 链表结
    点的 key 本身 和要获取的 key 相等,则返回该结点,遍历结束仍未找到
    对应 key 的结点,则返回 null
    2.3):调用 remove(Object key)操作根据键 key 删除对应的 key-value 键值对时,进
    行了如下操作:
    先调用 hash(key) 方法计算出 key hash
    根据查找的键值 key hash 值,通过 (n - 1) & hash 当前元素的 hash
    & hash 表长度 - 1 (实际就是 hash % hash 表长度)
    计算出存储位
    table[i] ,判断存储位置是否有元素存在 。
    如果存储位置有元素存放,则首先比较头结点元素,如果头结点的 key
    hash 值 和 要获取的 key hash 值相等,并且 头结点的 key 本身 和要
    获取的 key 相等,则该位置的头结点即为要删除的结点,记录此结点至
    变量 node 中。
    如果存储位置没有元素存放,则没有找到对应要删除的结点,则返回 null
    如果存储位置有元素存放,但是头结点元素不是要删除的元素,则需要遍
    历该位置进行查找。
    先判断头结点是否是 treeNode ,如果是 treeNode 则证明此位置的结构是
    红黑树,以红色树的方式遍历查找并删除该结点,没有则返回 null
    如果不是红黑树,则证明是单链表。遍历单链表,逐一比较链表结点,链
    表结点的 key hash 值 和 要获取的 key hash 值相等,并且 链表结
    点的 key 本身 和要获取的 key 相等,则此为要删除的结点,记录此结点
    至变量 node 中,遍历结束仍未找到对应 key 的结点,则返回 null
    如果找到要删除的结点 node ,则判断是否需要比较 value 也是否一致,
    如果 value 值一致或者不需要比较 value 值,则执行删除结点操作,删除
    操作根据不同的情况与结构进行不同的处理。
    如果当前结点是树结点,则证明当前位置的链表已变成红黑树结构,通过
    红黑树结点的方式删除对应结点。
    如果不是红黑树,则证明是单链表。如果要删除的是头结点,则当前存储
    位置 table[i] 的头结点指向删除结点的下一个结点。
    如果要删除的结点不是头结点,则将要删除的结点的后继结点 node.next
    赋值给要删除结点的前驱结点的 next 域,即 p.next = node.next;
    2.4):调用 replace(K key, V value)操作根据键 key 查找对应的 key-value 键值对,
    随后替换对应的值 value ,进行了如下操作:
    先调用 hash(key) 方法计算出 key hash
    随后调用 getNode 方法获取对应 key 所映射的 value 值 。
    记录元素旧值,将新值赋值给元素,返回元素旧值,如果没有找到元素,
    则返回 null

    (3).问:您能说说 HashMap HashTable 的区别吗?

    答: HashMap HashTable 有如下区别:
    1 )容器整体结构:
    HashMap key value 都允许为 null HashMap 遇到 key null 的时候,
    调用 putForNullKey 方法进行处理,而对 value 没有处理。
    Hashtable key value 都不允许为 null Hashtable 遇到 null ,直接
    返回 NullPointerException
    2 ) 容量设定与扩容机制:
    HashMap 默认初始化容量为 16 ,并且容器容量一定是 2 n 次方,扩容
    时,是以原容量 2 倍 的方式 进行扩容。
    Hashtable 默认初始化容量为 11 ,扩容时,是以原容量 2 倍 再加 1
    方式进行扩容。即 int newCapacity = (oldCapacity << 1) + 1;
    3 ) 散列分布方式(计算存储位置):
    HashMap 是先将 key 键的 hashCode 经过扰动函数扰动后得到 hash 值,然
    后再利用 hash & (length - 1) 的方式代替取模,得到元素的存储位置。
    Hashtable 则是除留余数法进行计算存储位置的(因为其默认容量也不是
    2 n 次方。所以也无法用位运算替代模运算), int index = (hash &
    0x7FFFFFFF) % tab.length;
    由于 HashMap 的容器容量一定是 2 n 次方,所以能使用 hash & (length
    - 1) 的方式代替取模的方式计算元素的位置提高运算效率,但 Hashtable
    的容器容量不一定是 2 n 次方,所以不能使用此运算方式代替。
    4 )线程安全(最重要):
    HashMap 不是线程安全,如果想线程安全,可以通过调用
    synchronizedMap(Map m) 使其线程安全。但是使用时的运行效率会
    下降,所以建议使用 ConcurrentHashMap 容器以此达到线程安全。
    Hashtable 则是线程安全的,每个操作方法前都有 synchronized 修饰使
    其同步,但运行效率也不高,所以还是建议使用 ConcurrentHashMap 容器
    以此达到线程安全。
    因此, Hashtable 是一个遗留容器,如果我们不需要线程同步,则建议使用
    HashMap ,如果需要线程同步,则建议使用 ConcurrentHashMap

    (4)问:您说 HashMap 不是线程安全的,那如果多线程下,它是如何处理的?并且什么情况下会发生线程不安全的情况?

    答: HashMap 不是线程安全的,如果多个线程同时对同一个 HashMap 更改数据的
    话,会导致数据不一致或者数据污染。如果出现线程不安全的操作时, HashMap
    会尽可能的抛出 ConcurrentModificationException 防止数据异常,当我们在对
    一个 HashMap 进行遍历时,在遍历期间,我们是不能对 HashMap 进行添加,删除
    等更改数据的操作的,否则也会抛出 ConcurrentModificationException 异常,
    此为 fail-fast (快速失败)机制。从源码上分析,我们在 put,remove 等更改 HashMap
    数据时,都会导致 modCount 的改变,当 expectedModCount != modCount 时,
    则抛出 ConcurrentModificationException 。如果想要线程安全,可以考虑使用
    ConcurrentHashMap
    而且,在多线程下操作 HashMap ,由于存在扩容机制,当 HashMap 调用
    resize() 进行自动扩容时,可能会导致死循环的发生。
    由于时间关系,我暂不带着大家一起去分析 resize() 方法导致死循环发生的现
    象造成原因了,迟点有空我会再补充上去,请见谅,大家可以参考如下文章:
    Java 8 系列之重新认识 HashMap
    谈谈 HashMap 线程不安全的体现

    (5)问:我们在使用 HashMap 时,选取什么对象作为 key 键比较好,为什么?

    答:可变对象:指创建后自身状态能改变的对象。换句话说,可变对象是该对象
    在创建后它的哈希值可能被改变。
    我们在使用 HashMap 时,最好选择不可变对象作为 key 。例如 String
    Integer 等不可变类型作为 key 是非常明智的。
    如果 key 对象是可变的,那么 key 的哈希值就可能改变。在 HashMap 中可
    变对象作为 Key 会造成数据丢失。因为我们再进行 hash & (length - 1)
    取模运算计算位置查找对应元素时,位置可能已经发生改变,导致数据丢
    失。
    详细例子说明请参考: 危险!在 HashMap 中将可变对象用作 Key

    总结

    HashMap 是基于 Map 接口实现的一种键 - 值对 的存储结构,允
    null 值,同时非有序,非同步 ( 即线程不安全 ) HashMap 的底层实现是
    数组 + 链表 + 红黑树( JDK1.8 增加了红黑树部分)。
    HashMap 定位元素位置是通过键 key 经过扰动函数扰动后得到 hash 值,
    然后再通过 hash & (length - 1) 代替取模的方式进行元素定位的。
    HashMap 是使用链地址法解决 hash 冲突的,当有冲突元素放进来时,会
    将此元素插入至此位置链表的最后一位,形成单链表。当存在位置的链表
    长度 大于等于 8 时, HashMap 会将链表 转变为 红黑树,以此提高查找
    效率。
    HashMap 的容量是 2 n 次方,有利于提高计算元素存放位置时的效率,
    也降低了 hash 冲突的几率。因此,我们使用 HashMap 存储大量数据的时
    候,最好先预先指定容器的大小为 2 n 次方,即使我们不指定为 2 n
    次方, HashMap 也会把容器的大小设置成最接近设置数的 2 n 次方,如,
    设置 HashMap 的大小为 7 ,则 HashMap 会将容器大小设置成最接近 7
    一个 2 n 次方数,此值为 8
    HashMap 的负载因子表示哈希表空间的使用程度(或者说是哈希表空间的
    利用率)。当负载因子越大,则 HashMap 的装载程度就越高。也就是能容
    纳更多的元素,元素多了,发生 hash 碰撞的几率就会加大,从而链表就
    会拉长,此时的查询效率就会降低。当负载因子越小,则链表中的数据量
    就越稀疏,此时会对空间造成浪费,但是此时查询效率高。
    HashMap 不是线程安全的, Hashtable 则是线程安全的。但 Hashtable
    一个遗留容器,如果我们不需要线程同步,则建议使用 HashMap ,如果需
    要线程同步,则建议使用 ConcurrentHashMap
    在多线程下操作 HashMap ,由于存在扩容机制,当 HashMap 调用 resize()
    进行自动扩容时,可能会导致死循环的发生。
    我们在使用 HashMap 时,最好选择不可变对象作为 key 。例如 String
    Integer 等不可变类型作为 key 是非常明智的。

    2. ArrayList

    定义

    快速了解 ArrayList 究竟是什么的一个好方法就是看 JDK 源码中对 ArrayList 类的
    注释,大致翻译如下:
    /**
    * 实现了 List 的接口的可调整大小的数组。实现了所有可选列表操作,并且
    允许所有类型的元素,
    * 包括 null。除了实现了 List 接口,这个类还提供了去动态改变内部用于存
    储集合元素的数组尺寸
    * 的方法。(这个类与 Vector 类大致相同,除了 ArrayList 是非线程安全外。)
    size,isEmpty,
    * get,set,iterator,和 listIterator 方法均为常数时间复杂度。add 方
    法的摊还时间复杂度为
    * 常数级别,这意味着,添加 n 个元素需要的时间为 O(n)。所有其他方法的
    时间复杂度都是线性级别的。
    * 常数因子要比 LinkedList 低。
    * 每个 ArrayList 实例都有一个 capacity。capacity 是用于存储 ArrayList
    的元素的内部数组的大小。
    * 它通常至少和 ArrayList 的大小一样大。当元素被添加到 ArrayList 时,它
    的 capacity 会自动增长。
    * 在向一个 ArrayList 中添加大量元素前,可以使用 ensureCapacity 方法来
    增加 ArrayList 的容量。
    * 使用这个方法来一次性地使 ArrayList 内部数组的尺寸增长到我们需要的
    大小提升性能。需要注意的
    * 是,这个 ArrayList 实现是未经同步的。若在多线程环境下并发访问一个
    ArrayList 实例,并且至少
    * 一个线程对其作了结构型修改,那么必须在外部做同步。(结构性修改指的
    是任何添加或删除了一个或
    * 多个元素的操作,以及显式改变内部数组尺寸的操作。set 操作不是结构性
    修改)在外部做同步通常通
    * 过在一些自然地封装了 ArrayList 的对象上做同步来实现。如果不存在这样
    的对象,ArrayList 应
    * 使用 Collections.synchronizedList 方法来包装。最好在创建时就这么做,
    以防止对 ArrayList
    * 无意的未同步访问。(List list = Collections.synchronizedList(new
    ArrayList(...));)
    * ArrayList 类的 iterator()方法以及 listIterator()方法返回的迭代器是
    fail-fast 的:
    * 在 iterator 被创建后的任何时候,若对 list 进行了结构性修改(以任何除
    了通过迭代器自己的
    * remove 方法或 add 方法的方式),迭代器会抛出一个
    ConcurrentModificationException 异常。
    * 因此,在遇到并发修改时,迭代器马上抛出异常,而不是冒着以后可能在
    不确定的时间发生不确定行为
    * 的风险继续。需要注意的是,迭代器的 fail-fast 行为是不能得到保证的,
    因为通常来说在未同步并发
    * 修改面前无法做任何保证。fail-fast 迭代器会尽力抛出
    ConcurrentModificationException 异常。
    * 因此,编写正确性依赖于这个异常的程序是不对的:fail-fast 行为应该仅
    仅在检测 bugs 时被使用。
    * ArrayList 类是 Java 集合框架中的一员。
    根据源码中的注释,我们了解了 ArrayList 用来组织一系列同类型的数据对象,支
    持对数据对象的顺序迭代与随机访问。我们还了解了 ArrayList 所支持的操作以及
    各项操作的时间复杂度。接下来我们来看看这个类实现了哪些接口。
    public class ArrayList extends AbstractList
    implements List, RandomAccess, Cloneable, java . io .Serializable
    我们可以看到,它实现了 4 个接口: List RandomAccess Cloneable Serializable
    官方文档对 List 接口的说明如下:List 是一个有序的集合类型(也被称作序列)。
    使用 List 接口可以精确控制每个元素被插入的位置,并且可以通过元素在列表中
    的索引来访问它。列表允许重复的元素,并且在允许 null 元素的情况下也允许多
    个 null 元素。
    List 接口定义了以下方法:
    ListIterator < E > listIterator();void add(int i , E element );E remove(int
    i );E get(int i );E set(int i , E element );int indexOf(Object element );
    我们可以看到, add get 等方法都是我们在使用 ArrayList 时经常用到的。
    ArrayList 的源码注释中提到了, ArrayList 使用 Object 数组来存储集合元素。我
    们来一起看下它的源码中定义的如下几个字段:
    /** * 默认初始 capacity. */
    private static final int DEFAULT_CAPACITY = 10;/** * 供空的 ArrayList
    实例使用的空的数组实例 */
    private static final Object [] EMPTY_ELEMENTDATA = {};/** * 供默认大小
    的空的 ArrayList 实例使用的空的数组实例。
    * 我们把它和 EMPTY_ELEMENTDATA 区分开来,一边指导当地一个元素被添
    加时把内部数组尺寸设为
    * 多少
    */
    private static final Object [] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/**
    * 存放 ArrayList 中的元素的内部数组。
    * ArrayList 的 capacity 就是这个内部数组的大小。
    * 任何 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空
    ArrayList 在第一个元素
    * 被添加进来时,其 capacity 都会被扩大至 DEFAULT_CAPACITYhe
    */
    transient Object [] elementData ; // non-private to simplify nested class
    access/** *ArrayList 所包含的元素数 */
    private int size ;
    通过以上字段,我们验证了 ArrayList 内部确实使用一个 Object 数组来存储集合
    元素。

    ArrayList 的构造器

    首先我们来看一下我们平时经常使用的 ArrayList 的无参构造器的源码:
    /** * Constructs an empty list with an initial capacity of ten. */public
    ArrayList() {
    this. elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA ;}
    我们可以看到,无参构造器仅仅是把 ArrayList 实例的 elementData 字段赋值为
    DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    接下来,我们再来看一下 ArrayList 的其他构造器:
    1. /** * Constructs an empty list with the specified initial capacity.
    2. * * @param initialCapacity the initial capacity of the list
    3. * @throws IllegalArgumentException if the specified initial
    4. capacity
    5. * is negative
    6. */
    7. public ArrayList(int initialCapacity) {
    8. if (initialCapacity > 0) {
    9. this.elementData = new Object[initialCapacity];
    10. } else if (initialCapacity == 0) {
    11. this.elementData = EMPTY_ELEMENTDATA;
    12. } else {
    13. throw new IllegalArgumentException("Illegal Capacity: "+
    14. initialCapacity);
    15. }}
    16. /** * Constructs a list containing the elements of the specified
    17. * collection, in the order they are returned by the collection's *
    18. iterator.
    19. * * @param c the collection whose elements are to be placed into this
    20. list
    21. * @throws NullPointerException if the specified collection is null
    22. */
    23. public ArrayList(Collection extends E> c) {
    24. elementData = c.toArray();
    25. if ((size = elementData.length) != 0) {
    26. // c.toArray might (incorrectly) not return Object[] (see 6260652)
    27. if (elementData.getClass() != Object[].class)
    28. elementData = Arrays.copyOf(elementData, size, Object[].class);
    29. } else {
    30. // replace with empty array.
    31. this.elementData = EMPTY_ELEMENTDATA;
    32. }}
    通过源码我们可以看到,第一个构造器指定了 ArrayList 的初始 capacity ,然后根
    据这个初始 capacity 创建一个相应大小的 Object 数组。若 initialCapacity 0 ,则
    elementData 赋值为 EMPTY_ELEMENTDATA ;若 initialCapacity 为负数,则抛出
    一个 IllegalArgumentException 异常。
    第二个构造器则指定一个 Collection 对象作为参数,从而构造一个含有指定集合
    对象元素的 ArrayList 对象。这个构造器首先把 elementData 实例域赋值为集合对
    象转为的数组,而后再判断传入的集合对象是否不含有任何元素,若是的话,则
    elementData 赋值为 EMPTY_ELEMENTDATA ;若传入的集合对象至少包含一个
    元素,则进一步判断 c.toArray 方法是否正确返回了 Object 数组,若不是的话,
    则需要用 Arrays.copyOf 方法把 elementData 的元素类型改变为 Object
    现在,我们又了解了 ArrayList 实例的构建过程,那么接下来我们来通过 ArrayList
    get set 等方法的源码来进一步了解它的实现原理

    add 方法源码分析

    /** * Appends the specified element to the end of this list.
    * * @param e element to be appended to this list
    * @return true (as specified by {@link Collection#add})
    */public boolean add(E e ) {
    ensureCapacityInternal( size + 1); // Increments modCount!!
    elementData [ size ++] = e ;
    return true;}
    我们可以看到,在 add 方法内部,首先调用了 ensureCapacityInternal(size+1) ,这
    句的作用有两个:
    保证当前 ArrayList 实例的 capacity 足够大;
    增加 modCount modCount 的作用是判断在迭代时是否对 ArrayList 进行了结构性修
    改。
    然后通过将内部数组下一个索引处的元素设置为给定参数来完成了向 ArrayList
    中添加元素,返回 true 表示添加成功。

    get 方法源码分析

    /** * Returns the element at the specified position in this list.
    * * @param index index of the element to return
    * @return the element at the specified position in this list
    * @throws IndexOutOfBoundsException {@inheritDoc}
    */public E get(int index ) {
    rangeCheck( index );
    return elementData( index );}
    首先调用了 rangeCheck 方法来检查我们传入的 index 是否在合法范围内,然后调
    用了 elementData 方法,这个方法的源码如下:
    E elementData(int index ) {
    return ( E ) elementData [ index ];}

    set 方法源码分析

    /** * Replaces the element at the specified position in this list with
    * the specified element.
    * * @param index index of the element to replace
    * @param element element to be stored at the specified position
    * @return the element previously at the specified position
    * @throws IndexOutOfBoundsException {@inheritDoc}
    */
    public E set( int index , E element ) {
    rangeCheck( index );
    E oldValue = elementData( index );
    elementData [ index ] = element ;
    return oldValue ;}
    我们可以看到, set 方法的实现也很简单,首先检查给定的索引是否在合法范围
    内,若在,则先把该索引处原来的元素存储在 oldValue 中,然后把新元素放到该
    索引处并返回 oldValue 即可。

    3.LinkedList

    定义

    LinkedList 类源码中的注释如下:
    /** * 实现了 List 接口的双向链表。实现了所有可选列表操作,并且可以存储
    所有类型的元素,包括 null。
    * 对 LinkedList 指定索引处的访问需要顺序遍历整个链表,直到到达指定元
    素。
    * 注意 LinkedList 是非同步的。若多线程并发访问 LinkedList 对象,并且至
    少一个线程对其做
    * 结构性修改,则必须在外部对它进行同步。这通常通过在一些自然封装了
    LinkedList 的对象上
    * 同步来实现。若不存在这样的对象,这个 list 应使用
    Collections.synchronizedList 来包装。
    * 这最好在创建时完成,以避免意外的非同步访问。
    * LinkedList 类的 iterator()方法以及 listIterator()方法返回的迭代器是
    fail-fast 的:
    * 在 iterator 被创建后的任何时候,若对 list 进行了结构性修改(以任何除
    了通过迭代器自己的
    * remove 方法或 add 方法的方式),迭代器会抛出一个
    ConcurrentModificationException 异常。
    * 因此,在遇到并发修改时,迭代器马上抛出异常,而不是冒着以后可能在不
    确定的时间发生不确定行为
    * 的风险继续。需要注意的是,迭代器的 fail-fast 行为是不能得到保证的,
    因为通常来说在未同步并发
    * 修改面前无法做任何保证。fail-fast 迭代器会尽力抛出
    ConcurrentModificationException 异常。
    * 因此,编写正确性依赖于这个异常的程序是不对的:fail-fast 行为应该仅
    仅在检测 bugs 时被使用。
    * LinkedList 类是 Java 集合框架中的一员。
    */
    LinkedList 是对链表这种数据结构的实现(对链表还不太熟悉的小伙伴可以参考
    深入理解数据结构之链表 ),当我们需要一种支持高效删除 / 添加元素的数据结
    构时,可以考虑使用链表。
    总的来说,链表具有以下两个优点:
    插入及删除操作的时间复杂度为 O(1)
    可以动态改变大小
    链表主要的缺点是:由于其链式存储的特性,链表不具备良好的空间局部性,也
    就是说,链表是一种缓存不友好的数据结构。

    4.Hashset

    HashSet Set 的一种实现方式,底层主要使用 HashMap 来确保元素不重复。
    总结
    1 HashSet 内部使用 HashMap key 存储元素,以此来保证元素不重复;
    2 HashSet 是无序的,因为 HashMap key 是无序的;
    3 HashSet 中允许有一个 null 元素,因为 HashMap 允许 key null
    4 HashSet 是非线程安全的;
    5 HashSet 是没有 get() 方法的;

    5、List、Set、Map区别

    在说三者区别的时候先普及一下三个概念:数组,链表和哈希表

    数组:存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;


    链表:存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。


    哈希表:那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

    1.List列表:元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许加入重复元素,因为它可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引

    2.Set集合:没有顺序,不能重复。Set集合类似于一个罐子,"丢进"Set集合里的多个对象之间没有明显的顺序。Set继承自Collection接口,不能包含有重复元素(记住,这是整个Set类层次的共有属性)。

    3.Map:用于保存具有"映射关系"的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value。key和value都可以是任何引用类型的数据。Map的key不允许重复

    ArrayList -是线程不安全 底层是由数组实现 他的构造主要从AbstractList实现,主要是判断下初始元素的容量,ArrayList最大的特点就是提供了Add、Get操作,当然可以通过迭代器来遍历,对于元素的存在可以通过contains方法判断。

    LinkedList - 线程不安全的 作为一种双向链表结构,对于元素的插入、删除效率比较高,只需要调整节点指向即可,但是对于随机查找而言性能主要看这个链表长度和运气了。 LinkedList也提供了ArrayList的get方法,但是要复杂的多,主要通过next或previous方法遍历得到,LinkedList要移动指针。

    Vector -线程安全的,这两个类底层都是由数组实现的,效率低  比较简单和ArrayList差不多,主要是内部实现了synchronized关键字,实现了线程安全访问但性能有些降低,同时对于元素的扩充在算法上和ArrayList稍有不同,通过构造的容量增量系数来决定。

  • 相关阅读:
    加密行业焦点:本周五,关注灰度GBTC转型是否有解?
    【网络安全】「漏洞原理」(二)SQL 注入漏洞之理论讲解
    已成功见刊检索的国际学术会议论文海报展示(2)
    IP子网的划分
    力扣 2. 两数相加
    Java中级面试题及答案(120道Java中级面试题大汇总)
    我是这样保持精力充沛的
    代码仓库操作
    Linux入门与进阶(完结篇)
    万字总结随机森林原理、核心参数以及调优思路
  • 原文地址:https://blog.csdn.net/qq_14931305/article/details/125994706