⾯试最常⻅问题之 Java 集合框架

面试最常⻅问题之 Java 集合框架

chatGPT免费体验!

Java 集合概览

从下图可以看出,在 Java 中除了以 Map 结尾的类之外, 其他类都实现了 Collection 接⼝。 并且,以 Map 结尾的类都实现了 Map 接⼝。

在这里插入图片描述

说说 List,Set,Map 三者的区别?
  • List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。
  • Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
  • Map (⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数 y=f(x), “x”代表 key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个 键最多映射到⼀个值。
集合框架底层数据结构总结

先来看⼀下 Collection 接⼝下⾯的集合。

List
  • Arraylist : Object[] 数组
  • Vector : Object[] 数组
  • LinkedList : 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
  • HashSet (⽆序,唯⼀): 基于 HashMap 实现的,底层采⽤ HashMap 来保存元素
  • LinkedHashSet : LinkedHashSet 是 HashSet 的⼦类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现⼀样,不过还是有⼀点点区别的
  • TreeSet (有序,唯⼀): 红⿊树(⾃平衡的排序⼆叉树) 再来看看 Map 接⼝下⾯的集合。
Map
  • HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主 要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较⼤ 的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓ 度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以 减少搜索时间
  • LinkedHashMap : LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散 列结构即由数组和链表或红⿊树组成。另外, LinkedHashMap 在上⾯结构的基础上,增加了 ⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作, 实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
  • Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突 ⽽存在的
  • TreeMap : 红⿊树(⾃平衡的排序⼆叉树)
如何选⽤集合?

主要根据集合的特点来选⽤,⽐如我们需要根据键值获取到元素值时就选⽤ Map 接⼝下的集合,需 要排序时选择 TreeMap ,不需要排序时就选择 HashMap ,需要保证线程安全就选⽤ ConcurrentHashMap 。 当我们只需要存放元素值时,就选择实现 Collection 接⼝的集合,需要保证元素唯⼀时选择实现 Set 接⼝的集合⽐如 TreeSet 或 HashSet ,不需要就选择实现 List 接⼝的⽐如 ArrayList 或 LinkedList ,然后再根据实现这些接⼝的集合的特点来选⽤。

为什么要使⽤集合?

当我们需要保存⼀组类型相同的数据的时候,我们应该是⽤⼀个容器来保存,这个容器就是数组,但 是,使⽤数组存储对象具有⼀定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的, 于是,就出现了“集合”,集合同样也是⽤来存储多个数据的。 数组的缺点是⼀旦声明之后,⻓度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数 据的类型;⽽且,数组存储的数据是有序的、可重复的,特点单⼀。 但是集合提⾼了数据存储的灵活 性,Java 集合不仅可以⽤来存储不同类型不同数量的对象,还可以保存具有映射关系的数据

Iterator 迭代器

迭代器 Iterator 是什么?
public interface Iterator<E> {
 //集合中是否还有元素
 boolean hasNext();
 //获得集合中的下⼀个元素
 E next();
 ......
}

Iterator 对象称为迭代器(设计模式的⼀种),迭代器可以对集合进⾏遍历,但每⼀个集合内部的 数据结构可能是不尽相同的,所以每⼀个集合存和取都很可能是不⼀样的,虽然我们可以⼈为地在每⼀ 个类中定义 hasNext() 和 next() ⽅法,但这样做会让整个集合体系过于臃肿。于是就有了迭代 器。

迭代器是将这样的⽅法抽取出接⼝,然后在每个类的内部,定义⾃⼰迭代⽅式,这样做就规定了整个集 合体系的遍历⽅式都是 hasNext() 和 next() ⽅法,使⽤者不⽤管怎么实现的,会⽤即可。迭代器的 定义为:提供⼀种⽅法访问⼀个容器对象中各个元素,⽽⼜不需要暴露该对象的内部细节。

迭代器 Iterator 有啥⽤?

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

如何使⽤?

我们通过使⽤迭代器来遍历 HashMap ,演示⼀下 迭代器 Iterator 的使⽤。

Map<Integer, String> map = new HashMap();
map.put(1, "Java");
map.put(2, "C++");
map.put(3, "PHP");
Iterator<Map.Entry<Integer, Stringef iterator =
map.entrySet().iterator();
while (iterator.hasNext()) {
 Map.Entry<Integer, String> entry = iterator.next();
 System.out.println(entry.getKey() + entry.getValue());
}

有哪些集合是线程不安全的?怎么解决呢?

我们常⽤的 Arraylist , LinkedList , Hashmap , HashSet , TreeSet , TreeMap , PriorityQueue 都不是线程安全的。

解决办法很简单,可以使⽤线程安全的集合来代替。

如果你要使⽤线程安全的集合的话, java.util.concurrent 包中提供了很多并发容器供你使⽤:

  1. ConcurrentHashMap : 可以看作是线程安全的 HashMap
  2. CopyOnWriteArrayList :可以看作是线程安全的 ArrayList ,在读多写少的场合性能⾮常 好,远远好于 Vector .
  3. ConcurrentLinkedQueue :⾼效的并发队列,使⽤链表实现。可以看做⼀个线程安全的 LinkedList ,这是⼀个⾮阻塞队列。
  4. BlockingQueue : 这是⼀个接⼝,JDK 内部通过链表、数组等⽅式实现了这个接⼝。表示阻塞 队列,⾮常适合⽤于作为数据共享的通道。
  5. ConcurrentSkipListMap :跳表的实现。这是⼀个 Map ,使⽤跳表的数据结构进⾏快速查 找。

Collection ⼦接⼝之 List

Arraylist 和 Vector 的区别?
  1. ArrayList 是 List 的主要实现类,底层使⽤ Object[ ]存储,适⽤于频繁的查找⼯作,线程不 安全 ; 2
  2. Vector 是 List 的古⽼实现类,底层使⽤ Object[ ]存储,线程安全的。
Arraylist 与 LinkedList 区别?
  1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层使⽤的是 Object 数组; LinkedList 底层使⽤的是 双 向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链 表的区别,下⾯有介绍到!)
  3. 插⼊和删除是否受元素位置的影响: ① ArrayList 采⽤数组存储,所以插⼊和删除元素的 时间复杂度受元素位置的影响。 ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在 将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进 ⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的 操作。 ② LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂 度不受元素位置的影响,近似 O(1),如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, E element) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
  4. 是否⽀持快速随机访问: LinkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。 快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。
  5. 内存空间占⽤: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空 间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间 (因为要存放直接后继和直接前驱以及数据)。

补充内容:双向链表和双向循环链表

双向链表: 包含两个指针,⼀个 prev 指向前⼀个节点,⼀个 next 指向后⼀个节点。

另外推荐⼀篇把双向链表讲清楚的⽂章:https://juejin.im/post/5b5d1a9af265da0f47352f14

在这里插入图片描述

双向循环链表: 最后⼀个节点的 next 指向 head,⽽ head 的 prev 指向最后⼀个节点,构成⼀个 环。

在这里插入图片描述

补充内容:RandomAccess 接⼝

查看源码我们发现实际上 RandomAccess 接⼝中什么都没有定义。所以,在我看来 RandomAccess 接⼝不过是⼀个标识罢了。标识什么? 标识实现这个接⼝的类具有随机访问功能。 在 binarySearch() ⽅法中,它要判断传⼊的 list 是否 RamdomAccess 的实例,如果是,调 ⽤ indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法

public interface RandomAccess { } 

ArrayList 实现了 RandomAccess 接⼝, ⽽ LinkedList 没有实现。为什么呢?我觉得还是 和底层数据结构有关! ArrayList 底层是数组,⽽ LinkedList 底层是链表。数组天然⽀持随机 访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元 素,时间复杂度为 O(n),所以不⽀持快速随机访问。, ArrayList 实现了 RandomAccess 接⼝, 就表明了他具有快速随机访问功能。 RandomAccess 接⼝只是标识,并不是说 ArrayList 实现 RandomAccess 接⼝才具有快速随机访问功能的!

说⼀说 ArrayList 的扩容机制吧

详⻅笔主的这篇⽂章:通过源码⼀步⼀步分析 ArrayList 扩容机制

Collection ⼦接⼝之 Set

comparable 和 Comparator 的区别
  • comparable 接⼝实际上是出⾃ java.lang 包 它有⼀个 compareTo(Object obj) ⽅法⽤ 来排序
  • comparator 接⼝实际上是出⾃ java.util 包它有⼀个 compare(Object obj1, Object obj2) ⽅法⽤来排序

⼀般我们需要对⼀个集合使⽤⾃定义排序时,我们就要重写 compareTo() ⽅法或 compare() ⽅法, 当我们需要对某⼀个集合实现两种排序⽅式,⽐如⼀个 song 对象中的歌名和歌⼿名分别采⽤⼀种排序 ⽅法的话,我们可以重写 compareTo() ⽅法和使⽤⾃制的 Comparator ⽅法或者以两个 Comparator 来实现歌名排序和歌星名排序,第⼆种代表我们只能使⽤两个参数版的 Collections.sort() .

Comparator 定制排序
 ArrayList<Integer> arrayList = new ArrayList<Integer>();
 arrayList.add(-1);
 arrayList.add(3);
 arrayList.add(3);
 arrayList.add(-5);
 arrayList.add(7);
 arrayList.add(4);
 arrayList.add(-9);
 arrayList.add(-7);
 System.out.println("原始数组:");
 System.out.println(arrayList);
 // void reverse(List list):反转
 Collections.reverse(arrayList);
 System.out.println("Collections.reverse(arrayList):");
 System.out.println(arrayList);
 // void sort(List list),按⾃然排序的升序排序
 Collections.sort(arrayList);
 System.out.println("Collections.sort(arrayList):");
 System.out.println(arrayList);
 // 定制排序的⽤法
 Collections.sort(arrayList, new Comparator<Integer>() {
 @Override
 public int compare(Integer o1, Integer o2) {
 return o2.compareTo(o1);
 }
 });
 System.out.println("定制排序后:");
 System.out.println(arrayList);

Output:

原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]
重写 compareTo ⽅法实现按年龄来排序
// person对象没有实现Comparable接⼝,所以必须实现,这样才不会出错,才可
以使treemap中的数据按顺序排列
// 前⾯⼀个例⼦的String类已经默认实现了Comparable接⼝,详细可以查看
String类的API⽂档,另外其他
// 像Integer类等都已经实现了Comparable接⼝,所以不需要另外实现了
public class Person implements Comparable<Person> {
 private String name;
 private int age;
 public Person(String name, int age) {
 super();
 this.name = name;
 this.age = age;
 }
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
 public int getAge() {
 return age;
 }
 public void setAge(int age) {
 this.age = age;
 }
 /**
 * T重写compareTo⽅法实现按年龄来排序
 */
 @Override
 public int compareTo(Person o) {
 if (this.age > o.getAge()) {
 return 1;
 }
 if (this.age < o.getAge()) {
 return -1;
 }
 return 0;
 }
}
 

public static void main(String[] args) {
 TreeMap<Person, String> pdata = new TreeMap<Person, String>();
 pdata.put(new Person("张三", 30), "zhangsan");
 pdata.put(new Person("李四", 20), "lisi");
 pdata.put(new Person("王五", 10), "wangwu");
 pdata.put(new Person("⼩红", 5), "xiaohong");
 // 得到key的值的同时得到key所对应的值
 Set<Person> keys = pdata.keySet();
 for (Person key : keys) {
 System.out.println(key.getAge() + "-" + key.getName());
 }
 }

Output:

5-⼩红
10-王五
20-李四
30-张三
⽆序性和不可重复性的含义是什么
  1. 什么是⽆序性?⽆序性不等于随机性 ,⽆序性是指存储的数据在底层数组中并⾮按照数组索引的顺 序添加 ,⽽是根据数据的哈希值决定的。
  2. 什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重 写 equals()⽅法和 HashCode()⽅法。
⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
  • HashSet 是 Set 接⼝的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;
  • LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
  • TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排序。

Map 接⼝

HashMap 和 Hashtable 的区别
  • 线程是否安全: HashMap 是⾮线程安全的,HashTable 是线程安全的,因为 HashTable 内部的 ⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap 吧!);
  • 效率: 因为线程安全的问题,HashMap 要⽐ HashTable 效率⾼⼀点。另外,HashTable 基本被 淘汰,不要在代码中使⽤它;
  • 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会 抛出 NullPointerException。
  • 初始容量⼤⼩和每次扩充容量⼤⼩的不同 : ① 创建时如果不指定容量初始值,Hashtable 默 认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩(HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤ 2 的幂作为 哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于 阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择 先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。 Hashtable 没有这样的机制
HashMap 中带有初始容量的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

下⾯这个⽅法保证了 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩。

/**
 * Returns a power of two size for the given target capacity.
 */
 static final int tableSizeFor(int cap) {
 int n = cap - 1;
 n |= n e>f 1;
 n |= n e>f 2;
 n |= n e>f 4;
 n |= n e>f 8;
 n |= n e>f 16;
 return (n < 0) ? 1 : (n |{ MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY
: n + 1;
 }

HashMap 和 HashSet 区别

如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的 源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet ⾃⼰不得不 实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。

HashMapHashSet
实现了Map接口实现Set接口
存储键值对仅存储对象
调用put()向map中添加元素调用add()方法向Set中添加元素
HashMap使用键值对(key)计算HashCodeHashSet使用成员对象计算hashcode值。对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性
HashMap 和 TreeMap 区别

TreeMap 和 HashMap 都继承⾃

AbstractMap ,但是需要注意的是 TreeMap 它还实现了 NavigableMap 接⼝和 SortedMap 接⼝。

在这里插入图片描述

实现 NavigableMap 接⼝让 TreeMap 有了对集合内元素的搜索的能⼒。

实现 SortMap 接⼝让 TreeMap 有了对集合中的元素根据键排序的能⼒。默认是按 key 的升序排 序,不过我们也可以指定排序的⽐较器。示例代码如下:

  /**
         * @author shuang.kou
         * @createTime 2020年06⽉15⽇ 17:02:00
         */
        public class Person {
            private Integer age;

            public Person(Integer age) {
                this.age = age;
            }

            public Integer getAge() {
                return age;
            }

            public static void main(String[] args) {
                TreeMap<Person, String> treeMap = new TreeMap<>(new
                     Comparator<Person>() {
                         @Override
                         public int compare(Person person1, Person person2) {
                             int num = person1.getAge() - person2.getAge();
                             return Integer.compare(num, 0);
                         }
                     });
                treeMap.put(new Person(3), "person1");
                treeMap.put(new Person(18), "person2");
                treeMap.put(new Person(35), "person3");
                treeMap.put(new Person(16), "person4");
                treeMap.entrySet().stream().forEach(personStringEntry -> {
                    System.out.println(personStringEntry.getValue());
                });
            }
        }

输出:

person1
person4
person2
person3

可以看出, TreeMap 中的元素已经是按照 Person 的 age 字段的升序来排列了。 上⾯,我们是通过传⼊匿名内部类的⽅式实现的,你可以将代码替换成 Lambda 表达式实现的⽅式:

TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) > {
 int num = person1.getAge() - person2.getAge();
 return Integer.compare(num, 0);
});

综上,相⽐于 HashMap 来说 TreeMap 主要多了对集合中的元素根据键排序的能⼒以及对集合内元素 的搜索的能⼒。

HashSet 如何检查重复

当你把对象加⼊ HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也 会与其他加⼊的对象的 hashcode 值作⽐较,如果没有相符的 hashcode,HashSet 会假设对象没有重 复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等 的对象是否真的相同。如果两者相同,HashSet 就不会让加⼊操作成功。(摘⾃我的 Java 启蒙书 《Head fist java》第⼆版)

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

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

对于基本类型来说,== ⽐较的是值是否相等;

对于引⽤类型来说,== ⽐较的是两个引⽤是否指向同⼀个对象地址(两者在内存中存放的地址(堆内 存地址)是否指向同⼀个地⽅);

对于引⽤类型(包括包装类型)来说,equals 如果没有被重写,对⽐它们的地址是否相等;如果 equals()⽅法被重写(例如 String),则⽐较的是地址⾥的内容。

HashMap 的底层实现
JDK1.8 之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置 (这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash ⽅法。使⽤ hash ⽅法也就是扰动函数是为了防⽌⼀些实现 ⽐较差的 hashCode() ⽅法 换句话说使⽤扰动函数之后可以减少碰撞。

JDK 1.8 HashMap 的 hash ⽅法源码:

JDK 1.8 的 hash ⽅法 相⽐于 JDK 1.7 hash ⽅法更加简化,但是原理不变。

static final int hash(Object key) {
 int h;
 // key.hashCode():返回散列值也就是hashcode
 // ^ :按位异或
 // e>fÅ⽆符号右移,忽略符号位,空位都以0补⻬
 return (key }~ null) ? 0 : (h = key.hashCode()) ^ (h e>f 16);
 }

对⽐⼀下 JDK1.7 的 HashMap 的 hash ⽅法源码.

static int hash(int h) {
 // This function ensures that hashCodes that differ only by
 // constant multiples at each bit position have a bounded
 // number of collisions (approximately 8 at default load factor).
 h ^= (h e>f 20) ^ (h e>f 12);
 return h ^ (h e>f 7) ^ (h e>f 4);
}

相⽐于 JDK1.8 的 hash ⽅法 ,JDK 1.7 的 hash ⽅法的性能会稍差⼀点点,因为毕竟扰动了 4 次。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链 表。若遇到哈希冲突,则将冲突的值加到链表中即可。

在这里插入图片描述

JDK1.8 之后

相⽐于之前的版本, JDK1.8 之后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不 是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。

在这里插入图片描述

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都⽤到了红⿊树。红⿊树就是为了解决⼆叉 查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构。

HashMap 的⻓度为什么是 2 的幂次⽅

为了能让 HashMap 存取⾼效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到了过 了,Hash 值的范围值-2147483648 到 2147483647,前后加起来⼤概 40 亿的映射空间,只要哈希函数 映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个 40 亿⻓度的数组,内存是放不下 的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤ 来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n 代表 数组⻓度)。这也就解释了 HashMap 的⻓度为什么是 2 的幂次⽅。

这个算法应该如何设计呢?

我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次 则等价于与其除数减⼀的与(&)操作(也就是说 hash%lengthFGhash&(length-1)的前提是 length 是 2 的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓ 度为什么是 2 的幂次⽅。

HashMap 多线程操作导致死循环问题

主要原因在于并发下的 Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这个问 题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数 据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。

详情请查看:https://coolshell.cn/articles/9606.html

HashMap 有哪⼏种常⻅的遍历⽅式?

HashMap 的 7 种遍历⽅式与性能分析!

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现,JDK1.8 采 ⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红⿊⼆叉树。Hashtable 和 JDK1.8 之前 的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是 HashMap 的主体,链表 则是主要为了解决哈希冲突⽽存在的;
  • 实现线程安全的⽅式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个 桶数组进⾏了分割分段(Segment),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同 数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起 来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但 是已经简化了属性,只是为了兼容旧版本;② Hashtable(同⼀把锁) :使⽤ synchronized 来保 证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进 ⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。

两者的对⽐图:

HashTable:

在这里插入图片描述

JDK1.7 的 ConcurrentHashMap:

在这里插入图片描述

JDK1.8 的 ConcurrentHashMap:

在这里插入图片描述

JDK1.8 的 ConcurrentHashMap 不在是 Segment 数组 + HashEntry 数组 + 链表,⽽是 Node 数 组 + 链表 / 红⿊树。不过,Node 只能⽤于链表的情况,红⿊树的情况需要使⽤ TreeNode 。当冲突 链表达到⼀定⻓度时,链表会转换成红⿊树。

ConcurrentHashMap 线程安全的具体实现⽅式/底层具体实现
JDK1.7(上⾯有示意图)

⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据 时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。HashEntry ⽤于存储 键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable
{
}

⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组。Segment 的结构和 HashMap 类似,是⼀种数组 和链表结构,⼀个 Segment 包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素,每 个 Segment 守护着⼀个 HashEntry 数组⾥的元素,当对 HashEntry 数组的数据进⾏修改时,必须⾸ 先获得对应的 Segment 的锁。

JDK1.8 (上⾯有示意图)

ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS 和 synchronized 来保证并发安全。数据结构 跟 HashMap1.8 的结构类似,数组+链表/红⿊⼆叉树。Java 8 在链表⻓度超过⼀定阈值(8)时将链表 (寻址时间复杂度为 O(N))转换为红⿊树(寻址时间复杂度为 O(log(N)))

synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效 率⼜提升 N 倍。

Collections ⼯具类

Collections ⼯具类常⽤⽅法:

  1. 排序
  2. 查找,替换操作
  3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使⽤ JUC 包下的并发集合)
排序操作
void reverse(List list)//反转 
void shuffle(List list)//随机排序 
void sort(List list)//按⾃然排序的升序排序 
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 
void swap(List list, int i , int j)//交换两个索引位置的元素 
void rotate(List list, int distance)//旋转。当distance为正数时,将list后 distance个元素整体移到前⾯。当distance为负数时,将 list的前distance个元 素整体移到后⾯

查找,替换操作

int binarySearch(List list, Object key)//对List进⾏⼆分查找,返回索引,
注意List必须是有序的
int max(Collection coll)//根据元素的⾃然顺序,返回最⼤的元素。 类⽐int
min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最⼤元素,排序
规则由Comparatator类控制。类⽐int min(Collection coll, Comparator c)
void fill(List list, Object obj)//⽤指定的元素代替指定list中的所有元素。
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第⼀次出现
的索引,找不到则返回-1,类⽐int lastIndexOfSubList(List source, list
target).
boolean replaceAll(List list, Object oldVal, Object newVal), ⽤新元素替
换旧元素
同步控制

Collections 提供了多个 synchronizedXxx() ⽅法·,该⽅法可以将指定集合包装成线程同步的集 合,从⽽解决多线程并发访问集合时的线程安全问题。

我们知道 HashSet , TreeSet , ArrayList , LinkedList , HashMap , TreeMap 都是线程不安全 的。 Collections 提供了多个静态⽅法可以把他们包装成线程同步的集合。

最好不要⽤下⾯这些⽅法,效率⾮常低,需要线程安全的集合类型时请考虑使⽤ JUC 包下的并发集 合。

⽅法如下:

synchronizedCollection(Collection<T> c) //返回指定 collection ⽀持的同步(线程安全的)collection
synchronizedList(List<T> list)//返回指定列表⽀持的同步(线程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射⽀持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set ⽀持的同步(线程安全的)set

其他重要问题

什么是快速失败(fail-fast)?

快速失败(fail-fast) 是 Java 集合的⼀种错误检测机制。在使⽤迭代器对集合进⾏遍历的时候,我们 在多线程下操作⾮安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 ConcurrentModificationException 异常。 另外,在单线程下,如果在遍历过程中对集合对象 的内容进⾏了修改的话也会触发 fail-fast 机制。

注:增强 for 循环也是借助迭代器进⾏遍历。

举个例⼦:多线程下,如果线程 1 正在对集合进⾏遍历,此时线程 2 对集合进⾏修改(增加、删除、 修改),或者线程 1 在遍历过程中对集合进⾏修改,都会导致线程 1 抛出 ConcurrentModificationException 异常。

为什么呢?

每当迭代器使⽤ hashNext() / next() 遍历下⼀个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;否则抛出异常,终⽌遍历。

如果我们在集合被遍历期间对其进⾏修改的话,就会改变 modCount 的值,进⽽导致 modCount Ö~ expectedModCount ,进⽽抛出 ConcurrentModificationException 异常。

注:通过 Iterator 的⽅法修改集合的话会修改到 expectedModCount 的值,所以不会抛出异 常。

final void checkForComodification() {
 if (modCount Ö~ expectedModCount)
 throw new ConcurrentModificationException();
}

好吧!相信⼤家已经搞懂了快速失败(fail-fast)机制以及它的原理。

我们再来趁热打铁,看⼀个阿⾥巴巴⼿册相关的规定:

在这里插入图片描述

有了前⾯讲的基础,我们应该知道:使⽤ Iterator 提供的 remove ⽅法,可以修改到 expectedModCount 的值。所以,才不会再抛出 ConcurrentModificationException 异常。 final void checkForComodification() { if (modCount Ö~ expectedModCount) throw new ConcurrentModificationException(); }

什么是安全失败(fail-safe)呢?

明⽩了快速失败(fail-fast)之后,安全失败(fail-safe)我们就很好理解了。

采⽤安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,⽽是先复制原有集合内容,在 拷⻉的集合上进⾏遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常。

Arrays.asList()避坑指南

最近使⽤ Arrays.asList() 遇到了⼀些坑,然后在⽹上看到这篇⽂章:Java Array to List Examples 感觉挺不错的,但是还不是特别全⾯。所以,⾃⼰对于这块⼩知识点进⾏了简单的总结。

简介

Arrays.asList() 在平时开发中还是⽐较常⻅的,我们可以使⽤它将⼀个数组转换为⼀个 List 集 合。

String[] myArray = { "Apple", "Banana", "Orange" }List<String> myList = Arrays.asList(myArray);
//上⾯两个语句等价于下⾯⼀条语句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");

JDK 源码对于这个⽅法的说明:

/**
*返回由指定数组⽀持的固定⼤⼩的列表。此⽅法作为基于数组和基于集合的API
之间的桥梁,与 Collection.toArray()结合使⽤。返回的List是可序
列化并实现RandomAccess接⼝。
*/
public static <T> List<T> asList(T... a) {
 return new ArrayList<>(a);
}

《阿⾥巴巴 Java 开发⼿册》对其的描述

Arrays.asList() 将数组转换为集合后,底层其实还是数组,《阿⾥巴巴 Java 开发⼿册》对于这个 ⽅法有如下描述:

在这里插入图片描述

使⽤时的注意事项总结

传递的数组必须是对象数组,⽽不是基本类型。

Arrays.asList() 是泛型⽅法,传⼊的对象必须是对象数组。

int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//数组地址值
System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[0]);//1

当传⼊⼀个原⽣数据类型数组时, Arrays.asList() 的真正得到的参数就不是数组中的元素,⽽是 数组对象本身!此时 List 的唯⼀元素就是这个数组,这也就解释了上⾯的代码。

我们使⽤包装类型数组就可以解决这个问题。

Integer[] myArray = { 1, 2, 3 };

使⽤集合的修改⽅法: add() 、 remove() 、 clear() 会抛出异常。

List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运⾏时报错:UnsupportedOperationException
myList.remove(1);//运⾏时报错:UnsupportedOperationException
myList.clear();//运⾏时报错:UnsupportedOperationException

Arrays.asList() ⽅法返回的并不是 java.util.ArrayList ,⽽是 java.util.Arrays 的 ⼀个内部类,这个内部类并没有实现集合的修改⽅法或者说并没有重写这些⽅法。

List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList

下图是 java.util.Arrays$ArrayList 的简易源码,我们可以看到这个类重写的⽅法有哪些。

 private static class ArrayList<E> extends AbstractList<E>
 implements RandomAccess, java.io.Serializable
 {
 ...
 @Override
 public E get(int index) {
 ...
 }
 @Override
 public E set(int index, E element) {
 ...
 }
 @Override
 public int indexOf(Object o) {
 ...
 }
 @Override
 public boolean contains(Object o) {
 ...
 }
 @Override
 public void forEach(Consumer<? super E> action) {
 ...
 }
 @Override
 public void replaceAll(UnaryOperator<E> operator) {
 ...
 }
 @Override
 public void sort(Comparator<? super E> c) {
 ...
 }
 }

我们再看⼀下 java.util.AbstractList 的 remove() ⽅法,这样我们就明⽩为啥会抛出 UnsupportedOperationException 。

public E remove(int index) {
 throw new UnsupportedOperationException();
}

ivate static class ArrayList extends AbstractList
implements RandomAccess, java.io.Serializable
{

@Override
public E get(int index) {

}
@Override
public E set(int index, E element) {

}
@Override
public int indexOf(Object o) {

}
@Override
public boolean contains(Object o) {

}
@Override
public void forEach(Consumer<? super E> action) {

}
@Override
public void replaceAll(UnaryOperator operator) {

}
@Override
public void sort(Comparator<? super E> c) {

}
}


我们再看⼀下 java.util.AbstractList 的 remove() ⽅法,这样我们就明⽩为啥会抛出 UnsupportedOperationException 。

```java
public E remove(int index) {
 throw new UnsupportedOperationException();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值