《Java面试 集合篇》

Java 集合, 也叫作容器,一般用来存储和操作数据,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素有三个子接口,分别是set、List、Queue;另一个是 Map 接口,主要用于存放键值对。其中比较常用的是arraylist、hashset、hashmap。arraylist本质是一个可变长度的数组,容量可以动态增长,并且允许存储重复元素,保证元素存放的顺序,支持高效的随机访问,但是线程不安全。hashset保证元素的顺序,不允许存储重复的元素。hashmap则主要存储键值对,其中,键是唯一的,不允许重复,不是线程安全的。如果要求线程安全,可以使用concurrenthashmap吧。

 

ce2b3c1af472494d9c84eb5fd511ed46.png

 (图源自JavaGuide)

1、集合和数组的区别:

af608e110f324e70a600bdfbb3cd1fe2.png

2、List, Set, Queue, Map 四者的区别 

  • List: 存储的元素是有序的、可重复的。
  • Set: 存储的元素是无序的、不可重复的。
  • Queue: 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
  • Map: 使用键值对(key-value)存储,key 是无序的、不可重复的,value 是无序的、可重复的。

3、集合框架底层数据结构总结

Collection 接口下面的集合。

List

  • ArrayListObject[] 数组
  • VectorObject[] 数组
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

Set

  • HashSet(无序,唯一): 底层数据结构是 HashMap 
  • LinkedHashSet: LinkedHashSetHashSet 的子类,底层数据结构通过 LinkedHashMap 。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

Queue

  • PriorityQueue: Object[] 数组来实现二叉堆
  • ArrayQueue: Object[] 数组 + 双指针

 Map 接口下面的集合

  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 HashMap的是由数组、链表+红黑树组成,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMapLinkedHashMap 继承自 HashMap,因此他的底部仍然采用数组+链表或红黑树的方式。此外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序,同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • Hashtable: 数组+链表组成。
  • TreeMap 红黑树(自平衡的排序二叉树)

如何选择集合

要根据具体的需求决定,如果只需要存放元素值时,则选择选择实现Collection 接口的集合,如果还要保证元素的唯一性,则选择set接口的集合;如果存放键值对的方式,则选择Map接口的集合,并按照相应的要求进行进一步的选择。

解决hash冲突的方法

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

二次探测:发生冲突时通过平方加减来找到第二次插入的位置。

链地址法:如果发生冲突,将元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

4、List:

ArrayList 和 Vector 的区别

  1. ArrayListList 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程不安全 ;
  2. Vector 也是 List 的实现类,底层使用Object[] 存储,线程安全的。

ArrayList 与 LinkedList 区别

1、两者都是线程不安全的。

2、ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

3、ArrayList 支持高效的随机访问,LinkedList 不支持。

4、ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间

5、ArrayList的扩容机制(重要)

底层结构是动态数组。它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。

(JDK8) 以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,初始数组容量扩为 10。然后继续向其中添加元素,直到添加完第10个元素,此时还没有扩容。如果要添加第11个元素,则会进行扩容,主要的扩容方法是老的容量右移一位,即/2,新容量为老容量+/2之后的容量之和,即老容量的1.5倍,以此类推。

6、CompareTo 和 Comparator的联系和区别

  1. 两者都常用于比较
  2. Comparable接口位于java.lang包下;Comparator位于java.util包下。
  3. Comparable接口只提供了一个compareTo()方法;Comparator接口不仅提供了compara()方法,还提供了其他默认方法,如reversed()、thenComparing()。
  4. 如果要用Comparable接口,则必须实现这个接口,并重写comparaTo()方法;但是Comparator接口可以在类外部使用,通过将该接口的一个匿名类对象当做参数传递给Collections.sort()方法或者Arrays.sort()方法实现排序。这样我们就可有不用改变类本身的代码而实现对类对象排序。

类中继承Comparable接口,并实现compareTo方法,完成自定义排序

public class User implements Comparable<User>{
    private Integer id;
    private Integer age;

    public User() {
    }

    public User(Integer id, Integer age) {
        this.id = id;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                '}';
    }

    //升序排列效果
    public int compareTo(User o) {
        if(this.age > o.getAge()) {
            return 1; //如果此对象大于目标对象,返回正整数1
        }else if(this.age < o.getAge()) {
            return -1; //如果此对象小于目标对象,返回负整数-1
        }else{
            return 0; //相等
        }
    }
}

类中继承Comparator接口,并实现compare方法,完成自定义排序

import java.util.Comparator;

public class Child implements Comparator<Child> {
    private Integer id;
    private Integer age;

    public Child() {
    }

    public Child(Integer id, Integer age) {
        this.id = id;
        this.age = age;
    }

    //升序效果
    @Override
    public int compare(Child o1, Child o2) {
        return o1.getAge() > o2.getAge() ? 1 : (o1.getAge() == o2.getAge() ? 0 : -1);
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Child{" +
                "id=" + id +
                ", age=" + age +
                '}';
    }
}

也可以通过匿名内部类的方式,将该接口的一个匿名类对象当做参数传递给Collections.sort()方法或者Arrays.sort()方法实现排序

public class Test {
    public static void main(String[] args) {
        Child child1 = new Child(1, 14);
        Child child2 = new Child(2, 12);
        Child child3 = new Child(3, 10);

        List<Child> list = new ArrayList<>();
        list.add(child1);
        list.add(child2);
        list.add(child3);

        //匿名内部类
        Collections.sort(list, new Comparator<Child>() {
            @Override
            public int compare(Child o1, Child o2) {
                return  o1.getAge() > o2.getAge() ? 1 : (o1.getAge() == o2.getAge() ? 0 : -1);
            }
        });

        // 或者使用JDK8中的Lambda表达式
        //Collections.sort(list, (o1, o2) -> (o1.getAge()-o2.getAge()));

        list.stream().forEach(System.out::println);
    }
}

多字段自定义排序

public class TestSort {

    public static void main(String[] args) {
        List<Student> studentList = Arrays.asList(
                new Student(1L, "张三", 95.5d),
                new Student(2L, "李四", 97.5d),
                new Student(3L, "王五", 95.5d),
                new Student(4L, "赵六", 96.5d),
                new Student(5L, "钱七", 98.5d),
                new Student(6L, "小二", 97d)
        );
        
        Comparator<Student> byScore = Comparator.comparing(Student::getScore, (s1, s2) -> {
            return s2.compareTo(s1);
        });//按照分数降序
        
        Comparator<Student> byName = Comparator.comparing(Student::getName);//按照名字字典升序

        Collections.sort(studentList,byScore.thenComparing(byName));//先按照分数降序,再按照名字升序
        
        System.out.println(studentList);
    }

}

 

7、Set

  • 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
  • 不可重复性是指添加的元素按照 equals() 判断时 ,返回 false,需要同时重写 equals() 方法和 hashCode() 方法
  1. hashSetLinkedHashSet 和 TreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的
  2. 三者的底层数据结构不同,HashSet 的底层数据结构是哈希表,LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。三者适用于不同的场景。

 

8、Queue

Queue是单端队列,而Deque是双端队列

ArrayDeque 与 LinkedList 的区别

  • ArrayDeque 是基于可变长的数组和双指针来实现,而 LinkedList 则通过链表来实现。

  • ArrayDeque 不支持存储 NULL 数据,但 LinkedList 支持。

  • ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。

从性能的角度上,选用 ArrayDeque 来实现队列要比 LinkedList 更好。此外,ArrayDeque 也可以用于实现栈

PriorityQueue

元素出队顺序是与优先级相关的,优先级最高的元素先出队。

  • PriorityQueue 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据
  • PriorityQueue 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。
  • PriorityQueue 是非线程安全的,且不支持存储 NULLnon-comparable 的对象。
  • PriorityQueue 默认是小顶堆,但可以接收一个 Comparator 作为构造参数,从而来自定义元素优先级的先后。

Map:

HashMap 和 Hashtable 的区别:

  1. 线程安全方面:HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!)
  2. 因为线程安全的问题,HashMap 要比 Hashtable 效率高
  3. HashMap 可以存储 null 的 key 和 value,hashtable不允许
  4. 初始容量大小和每次扩充容量大小的不同 :① 创建时如果不指定容量初始值,Hashtable 默认为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。
  5. JDK1.8 以后的 HashMap 引入了红黑树,当链表的长度超过阈值时,默认为8,会将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。hashtable则是数组+链表

HashMap 和 HashSet 区别

HashSet 底层就是基于 HashMap 实现的

36cabbec004d4191a29a73d67bc97207.png

( 表源自JavaGuide)

HashMap 和 TreeMap 区别

TreeMap 和HashMap 都继承自AbstractMap ,但是TreeMap它还实现了NavigableMap接口和SortedMap 接口

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

实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。

HashMap底层实现:

JDK1.7之前,采用数组+链表的结构,在JDK1.8之后,引入了红黑树。当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间,提高查询效率。

95252e22ef7342a788198a0de19787e3.png

 当我们向HsahMap中存放元素时,它会根据Key的 hashcode 经过扰动函数(hash())处理

(拿着key的哈希值,先“>>>”无符号右移16位,然后“^”异或上key的哈希值,得到一个值,再拿着这个值去“&”上数组长度减一)这样做的目的是让hash值的散列度更高,减少hash冲突,提高查询性能。

过后得到 hash 值,然后进行取模运算,判断当前元素存放的位置。但是,这样会遇到哈希冲突的情况。两个不同哈希值的Key经过取模运算((n - 1) & hash == hash%n位运算效率更高后,得到的数可能相同。因此,借助拉链法来解决hash冲突,将冲突的值添加到链表中(采用尾插法)。同时,为了防止链表过长而导致的查询效率下降,当链表长度大于8并且数组长度大于等于64时,将链表转化为红黑树,提高查询效率。

红黑树简单介绍:

1.结点是红色或黑色。

2.根结点是黑色。

3.每个叶子结点都是黑色的空结点(NIL结点)。

4 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)

5.从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。

aab76cd1672a48a8a0d7d8b1bb7f0bab.png

 红黑树是一种自平衡的二叉查找树,红黑树的时间复杂度为O(logN),链表的为O(N)

HashMap 的长度为什么是 2 的幂次方

1、可以让数据更散列更均匀的分布,更充分的利用数组的空间

2、在扩容迁移的时候不需要再重新通过哈希定位新的位置了。扩容后,元素新的位置,要么在原脚标位,要么在原脚标位+扩容长度这么一个位置

HashMap的扩容机制:

默认长度为16,当元素个数达到临界值时,会触发扩容机制。(图片来自B站跟着Mic学架构https://space.bilibili.com/1031543543

e30460b3c77f49d0acfe2298d4198503.png

 扩容为原来的2倍。

为什么扩容因子是0.75.

因为扩容因子表示的是hash表中的元素的填充程度,扩容因子越大,则触发扩容的元素的个数会越多,空间利用率高,但是发生hash冲突的概率也会增大,设置为0.75很好的达到了空间成本和时间成本的平衡。

ConcurrentHashMap

是在hashmap基础上实现的,但是他是线程安全的。

在JDK1.8之前,ConcurrentHashMap采用分段的数组+链表实现,使用分段锁,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。

在JDK1.8之后,ConcurrentHashMap采Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。

Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

 

JDK1.7:

首先将数据分为一段一段(每一段就是一个Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

6abc098ba35f41198dbc5f7f31a7d9f4.png

 

 JDK1.8之后:

523e97330dec4fe68d1898505bdf48b4.png

 Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。当冲突链表达到一定长度时,链表会转换成红黑树。

Java 8 中,锁粒度更细,是数组中的某一个节点(只锁定当前链表或红黑二叉树的首节点),在1.7中,锁定的是一个片段,性能更高。

扩容:ConcurrentHashMap采用多线程并发扩容,多个线程对原始数组进行分片,然后每个线程去负责每一个分片后的数据迁移,从而提升了数据迁移效率。

获取元素总个数。当线程竞争不激烈的时候,直接采用CAS方式,实现元素个数的原子递增;当比较激烈时,使用数组维护元素个数,当需要增加元素个数时,在数组中随机选择一个,再通过CAS算法来实现原子递增。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值