Java集合面试题

集合容器概述

什么是集合

集合框架:用于存储数据的容器。

集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。
任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。

实现:集合接口的具体实现,是重用性很高的数据结构。

算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。

它减少了程序设计的辛劳。

集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。
通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。 它提高了程序速度和质量。

集合的特点

集合的特点主要有如下两点:

  • 对象封装数据,对象多了也需要存储。集合用于存储对象。

  • 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因为集合是可变长度的。

    集合和数组的区别

    • 数组是固定长度的;集合可变长度的。
    • 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
    • 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

    一、数组声明了它容纳的元素的类型,而集合不声明。

    二、数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。而集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。

    三、数组的存放的类型只能是一种(基本类型/引用类型),集合存放的类型可以不是一种(不加泛型时添加的类型是Object)。

    四、数组是java语言中内置的数据类型,是线性排列的,执行效率或者类型检查都是最快的。

    数据结构:就是容器中存储数据的方式。

    对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。

    集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参阅顶层内容。建立底层对象。

    使用集合框架的好处

    1. 容量自增长;
    2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
  1. 允许不同 API 之间的互操作,API之间可以来回传递集合;
  2. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
  3. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。

常用的集合类有哪些?

Map接口和Collection接口是所有集合框架的父接口Collection父类是Iterable

  1. Collection接口的子接口包括:Set接口和List接口
  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各有什么特点?

image-20200803221452552

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。

Collection集合主要有List和Set两大接口

  • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。(有序可重复)

  • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。(无序不可重复)

    Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 Key相同直接覆盖

    Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

    集合框架底层数据结构

    Collection

    1. List
    • Arraylist: Object数组
    • Vector: Object数组
    • LinkedList: 双向循环链表
    1. Set
    • HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
    • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
    • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

    Map

    • HashMap: JDK1.8之前HashMap由 数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)并且数组长度大于64时,将链表转化为红黑树,以减少搜索时间
    • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的**底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。**另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
    • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
    • TreeMap: 红黑树**(自平衡的排序二叉树)**

    LinkedList 的 element()方法 底层其实是 getFirst()和 peek()方法的区别?

    两者都是 查看链表的第一个元素,不删除,但是 peek 当查找时,为空链表 返回null getFirst 则抛出NoSuchElementException异常

    poll()与remove() 底层调用 removeFirst 也是 如此

    哪些集合类是线程安全的?

    • vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
    • stack:堆栈类,先进后出。
    • hashtable:就比hashmap多了个线程安全。
    • enumeration:枚举,相当于迭代器。

    Java集合的快速失败机制 “fail-fast”?

    是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

    例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

    原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

    1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
    2. 使用CopyOnWriteArrayList来替换ArrayList
    1.1 fail-fast 机制简介

    **fail-fast 机制是java集合(Collection)中的一种错误机制。**多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

    例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

    import java.util.*;
    import java.util.concurrent.*;
    
    /*
     * @desc java集合中Fast-Fail的测试程序。
     *
     *   fast-fail事件产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException异常。
     *   fast-fail解决办法:通过util.concurrent集合包下的相应类去处理,则不会产生fast-fail事件。
     *
     *   本例中,分别测试ArrayList和CopyOnWriteArrayList这两种情况。ArrayList会产生fast-fail事件,而CopyOnWriteArrayList不会产生fast-fail事件。
     *   (01) 使用ArrayList时,会产生fast-fail事件,抛出ConcurrentModificationException异常;定义如下:
     *            private static List<String> list = new ArrayList<String>();
     *   (02) 使用时CopyOnWriteArrayList,不会产生fast-fail事件;定义如下:
     *            private static List<String> list = new CopyOnWriteArrayList<String>();
     *
     * @author Zeng
     */
    public class FastFailTest {
         
    
        private static List<String> list = new ArrayList<String>();
        //private static List<String> list = new CopyOnWriteArrayList<String>();
        public static void main(String[] args) {
         
        
            // 同时启动两个线程对list进行操作!
            new ThreadOne().start();
            new ThreadTwo().start();
        }
    
        private static void printAll() {
         
            System.out.println("");
    
            String value = null;
            Iterator iter = list.iterator();
            while(iter.hasNext()) {
         
                value = (String)iter.next();
                System.out.print(value+", ");
            }
        }
    
        /**
         * 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list
         */
        private static class ThreadOne extends Thread {
         
            public void run() {
         
                int i = 0;
                while (i<6) {
         
                    list.add(String.valueOf(i));
                    printAll();
                    i++;
                }
            }
        }
    
        /**
         * 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list
         */
        private static class ThreadTwo extends Thread {
         
            public void run() {
         
                int i = 10;
                while (i<16) {
         
                    list.add(String.valueOf(i));
                    printAll();
                    i++;
                }
            }
        }
    
    }
    

    运行结果

    运行该代码,抛出异常java.util.ConcurrentModificationException!产生fail-fast事件!

    结果说明

    • FastFailTest中通过 new ThreadOne().start() 和 new ThreadTwo().start() 同时启动两个线程去操作list
      ThreadOne线程:向list中依次添加0,1,2,3,4,5。每添加一个数之后,就通过printAll()遍历整个list
      ThreadTwo线程:向list中依次添加10,11,12,13,14,15。每添加一个数之后,就通过printAll()遍历整个list
    • 当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出ConcurrentModificationException异常,产生fail-fast事件
    1.3 fail-fast解决办法

    fail-fast机制,是一种错误检测机制。**它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。**若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
    所以,本例中只需要将ArrayList替换成java.util.concurrent包下对应的类即可。
      将代码

    private static List<String> list = new ArrayList<String>();
    

    替换为

    private static List<String> list = new CopyOnWriteArrayList<String>();
    

    怎么确保一个集合不能被修改?

    可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

    List<String> list = new ArrayList<>();
    list. add("x");
    Collection<String> clist = Collections. unmodifiableCollection(list);
    clist. add("y"); // 运行时此行报错
    System. out. println(list. size());
    
    
* 为什么会报ConcurrentModificationException异常?
* 1. Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 互斥锁。
* 2. Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,
* 这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,
* 3. 所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
* 4. 所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。

Collection接口

List接口

迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

Iterator 怎么使用?有什么特点?
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
   
  String obj = it. next();
  System. out. println(obj);
}

Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

如何边遍历边移除 Collection 中的元素?
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   
   *// do something*
   it.remove();
}

一种最常见的错误代码如下:

for(Integer i : list){
   
   list.remove(i)
}

运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

主要是集合遍历是使用Iterator, Iterator是工作在一个独立的线程中,并且拥有一个互斥锁。Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast原则 Iterator 会马上抛出java.util.ConcurrentModificationException 异常。所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。

ArrayList中MAX_ARRAY_SIZE为什么是 Integer.MAX_VALUE - 8 以及数组在java中到底是什么数据类型

数组在java里是一种特殊类型,既不是基本数据类型(开玩笑,当然不是)也不是引用数据类型。
有别于普通的“类的实例”对象,java里数组不是类,所以也就没有对应的class文件,数组类型是由jvm从元素类型合成出来的;在jvm中获取数组的长度是用arraylength这个专门的字节码指令的;
在数组的对象头里有一个_length字段,记录数组长度,只需要去读_length字段就可以了。
image-20200903172549250
所以ArrayList中定义的最大长度为Integer最大值减8,这个8就是就是存了数组_length字段。

Iterator 和 ListIterator 有什么区别?
  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

遍历方式有以下几种:

  1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
  2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

  • 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
  • 如果没有实现该接口,表示不支持 Random Access,如LinkedList。

推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。

说一下 ArrayList 的优缺点

ArrayList的优点如下:

image-20200804095947329

  • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
  • ArrayList 在顺序添加一个元素的时候非常方便。

ArrayList 的缺点如下:

  • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
  • 插入元素的时候,也需要做一次元素复制操作,缺点同上。

ArrayList 比较适合顺序添加、随机访问的场景。

如何实现数组和 List 之间的转换?
  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。
// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

// array to list
String[] array = new String[]{
   "123","456"};
Arrays.asList(array);

ArrayList 和 LinkedList 的区别是什么?

ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
本文主要看一下,两种List集合,插入删除效率情况,为什么使用ArrayList的情况更多些?

LinkedList首部插入数据很快,因为只需要修改插入元素前后节点的prev值和next值即可。
ArrayList首部插入数据慢,因为数组复制的方式移位耗时多。

LinkedList中间插入数据慢,因为遍历链表指针(二分查找)耗时多;
ArrayList中间插入数据快,因为定位插入元素位置快,移位操作的元素没那么多。

LinkedList尾部插入数据慢,因为遍历链表指针(二分查找)耗时多;
ArrayList尾部插入数据快,因为定位速度快,插入后移位操作的数据量少;

总结:在集合里面插入元素速度比对结果是,首部插入,LinkedList更快;中间和尾部插入,ArrayList更快;
在集合里面删除元素类似,首部删除,LinkedList更快;中间删除和尾部删除,ArrayList更快;
由此建议,数据量不大的集合,主要进行插入、删除操作,建议使用LinkedList;
数据量大的集合,使用ArrayList就可以了,不仅查询速度快,并且插入和删除效率也相对较高

  • 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
  • 主要控件开销不同ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkedList主要控件开销在于需要存储结点信息以及结点指针信息。
  • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全
  • 自由性不同ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

补充:数据结构基础之双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

什么时候用LinkList,什么时候用ArrayList

当操作是在一列 数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;

当你的操作是在一列数据的前面或中 间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

ArrayList的JDK1.8之前与之后的实现区别?

 JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组

 JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组

概括的说,LinkedList线程不安全的,允许元素为null双向链表
其底层数据结构是链表,它实现List<E>, Deque<E>, Cloneable, java.io.Serializable接口,它实现了Deque<E>,所以它也可以作为一个双端队列。和ArrayList比,没有实现RandomAccess所以其以下标,随机访问元素速度较慢

因其底层数据结构是链表,所以可想而知,它的增删只需要移动指针即可,故时间效率较高不需要批量扩容也不需要预留空间,所以空间效率ArrayList

缺点就是需要随机访问元素时,时间效率很低,虽然底层在根据下标查询Node的时候,会根据index判断目标Node在前半段还是后半段,然后决定是顺序还是逆序查询以提升时间效率。不过随着n的增大,总体时间效率依然很低。

当每次增、删时,都会修改modCount。

总结:

LinkedList 是双向列表。

  • 链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作。对比ArrayList是通过System.arraycopy完成批量增加的。增加一定会修改modCount。
  • 通过下标获取某个node 的时候,(add select),会根据index处于前半段还是后半段 进行一个折半,以提升查询效率
  • 删也一定会修改modCount。 按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。 按元素删,会先去遍历链表寻找是否有该Node,如果有,去链表上unlink掉这个Node。
  • 改也是先根据index找到Node,然后替换值。改不修改modCount。
  • 查本身就是根据index找到Node。
  • 所以它的CRUD操作里,都涉及到根据index去找到Node的操作。
ArrayList 和 Vector 的区别是什么?

image-20200804100553321

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合

  • 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
  • 性能:ArrayList 在性能方面要优于 Vector。
  • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍如果有增长系数 则变为 原来的值 + 增长系数而 ArrayList 只会增加 50%。
  • 初始化 vector 容量 10 增长系数 0 也就是 initialCapacity = 10,capacityIncrement = 0; ArrayList初始化 10 DEFAULT_CAPACITY= 10

Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。

Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?

ArrayList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。

Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差

LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快

多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
   
    System.out.println(synchronizedList.get(i));
}

为什么 ArrayList 的 elementData 加上 transient 修饰?

ArrayList 中的数组定义如下:

private transient Object[] elementData;

再看一下 ArrayList 的定义:

public class ArrayList<E> extends AbstractList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable

可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
   
    *// Write out element count, and any hidden stuff*
        int expectedModCount = modCount;
    s.defaultWriteObject();
    *// Write out array length*
        s.writeInt(elementData.length);
    *// Write out all elements in the proper order.*
        for (int i=0; i<size; i++)
            s.writeObject(elementData[i]);
    if (modCount != expectedModCount) {
   
        throw new ConcurrentModificationException();
}

每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

List 和 Set 的区别

List , Set 都是继承自Collection 接口

List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

Set和List对比

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

Set接口

说一下 HashSet 的实现原理?

HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

以下是HashSet 部分源码:

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
   
    map = new HashMap<>();
}

public boolean add(E e) {
   
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
	return map.put(e, PRESENT)==null;
}

hashCode()与equals()的相关规定

  1. 如果两个对象相等,则hashcode一定也是相同的
  2. 两个对象相等,对两个equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的
  4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

==与equals的区别

  1. == 是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
  2. == 是指对内存地址进行比较 equals()是对字符串的内容进行比较
  3. == 指引用是否相同 equals()指的是值是否相同

== 基本数据类型比较值,引用类型比较地址是否相等

HashSet与HashMap的区别

image-20200804102316163

Queue

BlockingQueue是什么?

Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;

当在添加一个元素时,它会等待队列中的可用空间。

BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。

我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。

Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

在 Queue 中 poll()和 remove()有什么区别?
  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
  • 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
Queue<String> queue = new LinkedList<String>();
queue. offer("string"); // add
System. out. println(queue. poll());
System. out. println(queue. remove());
System. out. println(queue. size());

Map接口

HahsMap

HashMap。HashMap 最早出现在JDK 1.2中,底层基于散列算法实现。

HashMap 允许null键和null值,

在计算哈键的哈希值时,null 键哈希值为0。

HashMap并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化。

另外,需要注意的是,HashMap 是非线程安全类,在多线程环境下可能会存在问题。

注意:不是说变成了红黑树效率就一定提高了,只有在链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树,

hashMap初始容量赋值为1w,存入 1w 条数据,会进行扩容吗,1000条?

当指定初始容量,且table未初始化时,首先使用初始容量的HashMap构造器,会调用tableSizeFor() 方法进行处理,再把结果赋值给threshold (阈值)。当第一次添加元素的时候,会进行初始化,新的容量变为原来的阈值(既tableSizeFor()方法处理后的值),再,新的阈值为新的容量乘以负载因子1w经过 tableSizeFor() 方法处理之后,阈值变成 2 的 14 次幂 16384,初始化后,新的容量为原来的阈值16384,新的阈值为12288(16384 * 0.75)大于一万,所以不会扩容

1000条经过 tableSizeFor() 方法处理之后,阈值变成1024,初始化后,新的容量为原来的阈值1024,新的阈值为768(1024 * 0.75)小于1000,所以会扩容

https://www.cnblogs.com/xyy2019/p/11765941.html

为什么1.8中扩容后的元素新位置为原位置加数组长度?

https://blog.csdn.net/qq32933432/article/details/86668385

可以看到,由于每次扩容会把原数组的长度*2,那么再二进制上的表现就是多出来一个1,比如元数组16-1二进制为1111,那么扩容后的32-1的二进制就变成了1 1111
而扩容前和扩容后的位置是否一样完全取决于多出来的那一位与key值的hash做按位与运算之后的值值是为0还是1。为0则新位置与原位置相同,不需要换位置,不为零则需要换位置。

而为什么新的位置是原位置+原数组长度,是因为==每次换的位置只是前面多了一个1而已==。那么新位置的变化的高位进1位。而每一次高位进1都是在加上原数组长度的过程。

image-20200906112807630

正好1+2=3 3+4=7 7+8=15 。也就验证了新的位置为原位置+原数组长度。

总结

jdk1.8中在计算新位置的时候并没有跟1.7中一样重新进行hash运算,而是用了原位置+原数组长度这样一种很巧妙的方式,而这个结果与hash运算得到的结果是一致的,只是会更块。

HahsMap中containsKey和get的区别

Map集合允许值对象为null,并且没有个数限制,所以当get()方法的返回值为null时,可能有两种情况,一种是在集合中没有该键对象,另一种是该键对象没有映射任何值对象,即值对象为null。因此,在Map集合中不应该利用get()方法来判断是否存在某个键,而应该利用containsKey()方法来判断

https://blog.csdn.net/u012903926/article/details/47293521

为什么HashMap是线程不安全的?

主要体现

· jdk1.7中,当多线程操作同一map时,在扩容的时候会因链表反转发生循环链表或丢失数据的情况

· jdk1.8中,当多线程操作同一map时,会发生数据覆盖的情况

在put的时候,由于put的动作不是原子性的,线程A在计算好链表位置后,挂起,线程B正常执行put操作,之后线程A恢复,会直接替换掉线程b put的值 所以依然不是线程安全的

jdk1.7中HashMap的transfer函数如下:

 1    void transfer(Entry[] newTable, boolean rehash) {
   
 2         int newCapacity = newTable.length;
 3         for (Entry<K,V> e : table) {
   
 4             while(null != e) {
   
 5                 Entry<K,V> next = e.next;
 6                 if (rehash) {
   
 7                     e.hash = null == e.key ? 0 : hash(e.key);
 8                 }
 9                 int i = indexFor(e.hash, newCapacity);
10                 e.next = newTable[i];
11                 newTable[i] = e;
12                 e = next;
13             }
14         }
15     }

总结下该函数的主要作用:

在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。下面进行详细分析。

前提条件:

这里假设

#1.hash算法为简单的用key mod链表的大小。

#2.最开始hash表size=2,key=3,7,5,则都在table[1]中。

#3.然后进行resize,使size变成4。

未resize前的数据结构如下:

image-20200911174329739

如果在单线程环境下,最后的结果如下:

image-20200911174345954

这里的转移过程,不再进行详述,只要理解transfer函数在做什么,其转移过程以及如何对链表进行反转应该不难。

然后在多线程环境下,假设有两个线程A和B都在进行put操作。线程A在执行到transfer函数中第11行代码处挂起,因为该函数在这里分析的地位非常重要,因此再次贴出来。

image-20200911174417566

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

image-20200911174447412

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

image-20200911174514573

这里需要特别注意的点:由于线程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

此时结果如下:

image-20200911174622517

继续循环:

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

结果如下:

image-20200911174713452

再次进行循环:

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循环结束。

结果如下:

image-20200911174804854

在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环,造成悲剧。

在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,

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) // 如果没有hash碰撞则直接插入元素
            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;
    }

这是jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。

这里只是简要分析下jdk1.8中HashMap出现的线程不安全问题的体现,后续将会对java的集合框架进行总结,到时再进行具体分析。

首先HashMap是线程不安全的,其主要体现:

1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。

2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

HashMap中元素在数组中的下标

Index=(key的hashCode与其高16位进行异或运算)再与(数组长度-1)进行与的运算

HahsMap转变为红黑树

链表长度大于或等于8,数组长度大于或等于64转变为红黑树

小于或等于6退化为链表

问题一:什么是红黑树呢?

红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。如果之前没有了解过红黑树的话,也没关系,你就记住红黑树的查找效率很高就OK了

问题二:为什么不一下子把整个链表变为红黑树呢?

这个问题的意思是这样的,就是说我们为什么非要等到链表的长度大于等于8的时候,才转变成红黑树?在这里可以从两方面来解释

(1)构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。

(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

OK,到这里相信我们对hashMap的底层数据结构有了一个认识。现在带着上面的结构图,看一下如何存储一个元素。

image-20200805144253879

上面这个流程,不知道你能否看到,红色字迹的是三个判断框,也是转折点,我们使用文字来梳理一下这个流程:

(1)第一步:调用put方法传入键值对

(2)第二步:使用hash算法计算hash值

(3)第三步:根据hash值确定存放的位置,判断是否和其他键值对位置发生了冲突

(4)第四步:若没有发生冲突,直接存放在数组中即可

(5)第五步:若发生了冲突,还要判断此时的数据结构是什么?

(6)第六步:若此时的数据结构是红黑树,那就直接插入红黑树中

(7)第七步:若此时的数据结构是链表,判断插入之后是否大于等于8

(8)第八步:插入之后大于8了,就要先调整为红黑树,在插入

(9)第九步:插入之后不大于8,那么就直接插入到链表尾部即可。

image-20200805145109516

image-20200805145130861

对于拉链式的散列算法,其数据结构是由数组和链表(或树形结构)组成。在进行增删查等操作时,首先要定位到元素的所在桶的位置,之后再从链表中定位该元素。比如我们要查询上图结构中是否包含元素 35,步骤如下:

  1. 定位元素35所处桶的位置,index = 35 % 16 = 3
  2. 3号桶所指向的链表中继续查找,发现35在链表中。

考虑一个问题:桶数组 table 是 HashMap 底层重要的数据结构,不序列化的话,别人还怎么还原呢?

HashMap 并没有使用默认的序列化机制,而是通过实现 readObject/writeObject 两个方法自定义了序列化的内容。HashMap 中存储的内容是键值对,只要把键值对序列化了,就可以根据键值对数据重建 HashMap。

也有的人可能会想,序列化 table 不是可以一步到位,后面直接还原不就行了吗?但序列化 table 存在着两个问题:

  1. table 多数情况下是无法被存满的,序列化未使用的部分**,浪费空间**
  2. 同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。

以上两个问题中,第一个问题比较好理解,第二个问题解释一下。

HashMap 的 get/put/remove 等方法第一步就是根据 hash 找到键所在的桶位置,但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,**不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。**也就是说==同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。==

loadFactor 负载因子

loadFactor 指的是负载因子 HashMap 能够承受住自身负载(大小或容量)的因子,loadFactor 的默认值为 0.75 认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16 * 0.75=12 的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能

**负载因子越大表示散列表的装填程度越高,反之愈小。**对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费

需要指出的一点是:HashMap 要求容量必须是 2 的幂

先看看阈值的计算方法,需要指出的一点是:HashMap 要求容量必须是 2 的****幂 。阈值具体计算方式如下:

static final int tableSizeFor(int cap) {
   
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
下面分析这个算法:

首先,要注意的是这个操作是无符号右移后,再或上原来的值。

为什么要对 cap 做减 1 操作:int n = cap - 1 ?

这是为了防止,cap 已经是 2 的幂。如果 cap 已经是 2 的幂, 又没有执行这个减 1 操作,则执行完后面的几条无符号右移操作之后,返回的 capacity 将是这个 cap 的 2 倍。如果不懂,要看完后面的几个无符号右移之后再回来看看。

下面看看这几个无符号右移操作:

如果 n 这时为 0 了(经过了 cap-1 之后),则经过后面的几次无符号右移依然是 0,最后返回的 capacity 是 1(最后有个 n+1 的操作)。

这里只讨论 n 不等于 0 的情况。

第一次右移

n |= n >>> 1;

由于 n 不等于 0,则 n 的二进制表示中总会有一bit为 1,这时考虑最高位的 1。通过无符号右移 1 位,则将最高位的 1 右移了 1 位,再做或操作,使得 n 的二进制表示中与最高位的 1 紧邻的右边一位也为 1,如 000011xxxxxx。

第二次右移

n |= n >>> 2;

注意,这个 n 已经经过了 n |= n >>> 1; 操作。假设此时 n 为 000011xxxxxx ,则 n 无符号右移两位,会将最高位两个连续的 1 右移两位,然后再与原来的 n 做或操作,这样 n 的二进制表示的高位中会有 4 个连续的 1。如 00001111xxxxxx 。

第三次右移

n |= n >>> 4;

这次把已经有的高位中的连续的 4 个 1,右移 4 位,再做或操作,这样 n 的二进制表示的高位中会有8个连续的 1。如 00001111 1111xxxxxx 。

以此类推

image-20200805211559613

注意,容量最大也就是 32bit 的正数,因此最后 n |= n >>> 16; ,最多也就 32 个 1,但是这时已经大于了 MAXIMUM_CAPACITY ,所以取值到 MAXIMUM_CAPACITY 。

注意,得到的这个 capacity 赋值给了 threshold,因此 threshold 就是所说的容量。当 HashMap 的 size 到达 threshold 这个阈值时会扩容。

但是,请注意,在构造方法中,并没有对 table 这个成员变量进行初始化,table 的初始化被推迟到了 put 方法中,在 put 方法中会对 threshold 重新计算。

image-20200805214757551

这里的 hash 值是 key.hashCode() 得到的。但是在 HashMap 这里,通过位运算重新计算了 hash 值的值。为什么要重新计算?

    static final int hash(Object key) {
   
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

主要是因为 n (HashMap 的容量) 值比较小,hash 只参与了低位运算,高位运算没有用上。这就增大了 hash 值的碰撞概率。而通过这种位运算的计算方式,使得高位运算参与其中,减小了 hash 的碰撞概率,使 hash 值尽可能散开。如何理解呢?把前面举的例子 hash = 185,n = 16,按照 HashMap 的计算方法咱们再来走一遍。

图中的 hash 是由键的 hashCode 产生。计算余数时,由于 n 比较小,hash 只有低 4 位参与了计算,高位的计算可以认为是无效的。这样导致了计算结果只与低位信息有关,高位数据没发挥作用。为了处理这个缺陷,我们可以上图中的 hash 高 4 位数据与低 4 位数据进行异或运算,即 hash ^ (hash >>> 4)。通过这种方式,让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。此时的计算过程如下:

image-20200805171519556

这次计算以后,发现最后的结果已经不一样了,hash 的高位值对结果产生了影响。这里为了举例子,使用了 8 位数据做讲解。在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前 16 位为高位,后16位为低位,所以要右移 16 位。

扩容:

image-20200805174840193

HashMap 在设计之初,并没有考虑到以后会引入红黑树进行优化。所以并没有像 TreeMap 那样,要求键类实现 comparable 接口或提供相应的比较器。但由于树化过程需要比较两个键对象的大小,在键类没有实现 comparable 接口的情况下,怎么比较键与键之间的大小了就成了一个棘手的问题。为了解决这个问题,HashMap 是做了三步处理,确保可以比较出两个键的大小,如下:

  1. 比较键与键之间 hash 的大小,如果 hash 相同,继续往下比较
  2. 检测键类是否实现了 Comparable 接口,如果实现调用 compareTo 方法进行比较
  3. 如果仍未比较出大小,就需要进行仲裁了,仲裁方法为 tieBreakOrder(大家自己看源码吧)

通过上面三次比较,最终就可以比较出孰大孰小。比较出大小后就可以构造红黑树了,最终构造出的红黑树如下:

image-20200805190022414

橙色的箭头表示 TreeNode 的 next 引用。由于空间有限,prev 引用未画出。可以看出,链表转成红黑树后,原链表的顺序仍然会被引用仍被保留了(红黑树的根节点会被移动到链表的第一位),我们仍然可以按遍历链表的方式去遍历上面的红黑树。这样的结构为后面红黑树的切分以及红黑树转成链表做好了铺垫,我们继续往下分析。

image-20200805192804243

梳理以下 get 函数的执行过程

  1. 判定三个条件 table 不为 Null & table 的长度大于 0 & table 指定的索引值不为 Null,否则直接返回 null,这也是可以存储 null
  2. 判定匹配 hash 值 & 匹配 key 值,成功则返回该值,这里用了 == 和 equals 两种方式,对于 int,string,同一个实例对象等可以适用。
  3. 若 first 节点的下一个节点不为 Null
    1. 若下一个节点类型为 TreeNode 红黑树,通过红黑树查找匹配值,并返回查询值
    2. 否则就是单链表,还是通过匹配 hash 值 & 匹配 key 值来获取数据。

HashMap 的删除操作并不复杂,仅需三个步骤即可完成。

第一步是定位桶位置,

第二步遍历链表并找到键值相等的节点,

第三步删除节点。

说一下 HashMap 的实现原理?

HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变

HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap 基于 Hash 算法实现的

  1. 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标

  2. 存储时,如果出现hash值相同的key,此时有两种情况。

    (1)如果key相同,则覆盖原始值;

    (2)如果key不同(出现冲突),则将当前的key-value放入链表中

  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。

  4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后并且数组的长度大于64,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树。

JDK1.8之前

JDK1.8之前采用的是拉链法。

拉链法:将链表和数组相结合。也就是说创建

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值