2.面试题--java集合

java集合

Java集合类主要由两个接口CollectionMap派生出来的,Collection有三个子接口:List、Set、Queue。
请添加图片描述

请添加图片描述

ArrayList和Vector的区别

两个类都实现了 List 接口(List接口有继承Collection接口),他们都是有序集合,相当于一种动态的数组,可以按位置索引号查询某个元素,存放的元素允许重复的。

同步性
Vector 是线程安全的,它的方法内部使用了synchronized关键字来确保在多线程环境下操作的原子性。
ArrayList 是线程序不安全的,它在单线程环境中的性能通常优于Vector。

单线程建议使用 ArrayList,不考虑线程安全,效率会高些;多线程可以使用 Vector,本身线程安全的。

如果需要在多线程中使用类似ArrayList的数据结构,可以考虑使用Collections.synchronizedList()来包装一个ArrayList,或者使用CopyOnWriteArrayList,它提供了线程安全的“写入时复制”语义。

数据增长
ArrayList和Vector都有默认的初始容量(通常是10)。
ArrayList的容量增长策略是当前容量的1.5倍,允许通过构造函数来指定初始容量。
Vector的容量增长策略是当前容量的2倍,允许通过构造函数来指定初始容量和增长因子。

ArrayList的扩容机制

  1. 初始化一个ArrayList而不指定初始容量时,数组容量为零。第一次添加元素时,扩容为默认的初始容量10。初始化时可以指定数组的容量ArrayList(int n)。

  2. ArrayList的add(Objecto)方法添加元素,如果需要扩容时,扩容为原来容量的1.5倍,不是原来的容量直接乘以1.5,而是原来容量值右移一位加原来容量值等于新容量值。
    例如:15>>1+15= 7+15=22

  3. ArrayList的addAll(Collection c)添加元素时,数组中没有元素时,通常会扩容到一个默认的初始容量10,数组有元素时,数组容量为Math.max(原容量1.5倍,实际元素个数)最大值

ArrayList<Integer> list=new ArrayList<>();
   for(int i=0;i<10;i++){
    list.add(i);
   }
list.addAll(list.of(1,2,3,4,5,6));
//下次扩容值为15,实际存放容量了6个整数,则实际扩容后容量值为16.
System.out.println(length(list));

ArrayList的常用方法

boolean add(Object 0) 在列表的末尾顺序添加元素,起始索引位置从0开始;

void add(int index,Object o) 在指定的索引位置添加元素。索引位置必须介于0和列表中元素个数之间

int size() 返回列表中的元素个数

Object get(int index) 返回指定索引位置处的元素。取出的元素是Object类型,使用前需要进行强制类型转换

boolean contains(Object o) 判断列表中是否存在指定元素

boolean remove(Object o) 从列表中删除元素

Object remove(int index) 从列表中删除指定位置元素,起始索引位置从0开始

Object set(int index,Object o) 指定下标进行修改其中的元素,返回的是修改前的对象

数组 (Array) 和列表 (ArrayList)区别

Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
Array 大小是固定的,ArrayList 的大小是动态变化的。

假如元素的大小是固定的,我们就应该用 Array 而不是ArrayList

LinkedList

①基于双向链表, 无需连续内存,内部使用Node节点来存储元素。

②随机访问慢 ,要沿着链表遍历。

③头尾插入删除性能高 ,中间插入和删除只需要修改头尾节点的引用即可。

④占用内存多,LinkedList的每个元素都需要一个额外指针来指向下一个节点的引用地址。

ArrayList,Vector, LinkedList 的存储性能和特性

ArrayList 和 Vector 都是使用数组方式存储数据,都允许直接按索引查询元素,但是插入删除元素需要移动元素等内存操作,Vector 中的方法使用了synchronized 关键字,通常性能上较 ArrayList 差。
LinkedList 使用双向链表实现存储,按索引查询元素时需要进行前向或后向遍历,查询较慢。
插入删除元素只需要修改头尾节点的引用即可,所以插入删除速度较快 。

迭代器 (Iterator)

迭代器(Iterator)是一个设计模式,它使得我们能够顺序地访问一个集合各个元素,而又不需要暴露该对象的内部表示。迭代器模式为遍历不同的聚合结构提供了一个统一的接口,从而支持以相同的方式遍历不同的集合结构。

迭代器可以在迭代的过程中删除底层集合的元素, 但是不可以直接调用集合的 remove(Object Obj) 删除,可以通过迭代器的 remove() 方法删除。

Iterator 和 ListIterator 的区别

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

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

fail-fast迭代器,遍历集合的同时不能修改集合结构(除了迭代器自身的 remove 方法),一旦修改迭代器会立即抛出ConcurrentModificationException。

不是所有的 java.util 包下的集合类都是 fail-fast 的。但是,大多数如 ArrayList、HashSet 等标准集合类都实现了 fail-fast 迭代器。

fail-safe迭代器,不会直接访问集合的底层数据,而是基于集合的一个拷贝(co进行遍历。因此集合在遍历过程中被修改,迭代器仍然能够安全地遍历。

不是所有的 java.util.concurrent 包下的类都是 fail-safe 的。但是,CopyOnWriteArrayList 和 CopyOnWriteArraySet 是 fail-safe 的典型例子

List,Set和Map 的区别

List

List 是有序集合,继承Collection接口,可以包含重复元素。通过索引(位置)来访问和操作元素,例如可以使用 list.get(i) 方法获取指定位置的元素。
List 是支持按索引访问的,因此查询速度较快。但在插入或删除元素时,需要移动其他元素以保持顺序,因此插入和删除的效率相对较低。

Set

Set 是无序集合,继承Collection接口,不允许包含重复元素,不能通过索引来访问元素。
Set 的特点是快速查找,因为它使用了哈希表等数据结构来存储元素,但是不保证元素的顺序。

Map

Map 是键值对的集合,其中每个元素都包含一个键对象和对应的值对象。Map 不属于 Collection 接口的继承体系,它是独立的接口。
Map 中的键对象是唯一的,但值对象可以重复。可以通过键来获取对应的值,不能通过索引来访问。

HashMap底层原理

HashMap 是以键值对(key-value)的形式存储元素的。

哈希函数与哈希值
当向HashMap中插入一个键值对时,首先会对键调用hashCode()方法计算一个原始哈希值h,然后 h 和 h右移十六位 做异或运算得到实际哈希值。

确定数组索引
计算得到的哈希值并不能直接作为数组的下标,因为哈希值可能是一个很大的整数,而数组的长度是有限的。因此哈希值与数组长度减一的做按位与运算(hash & (capacity - 1)),就可以得到元素存放在数组中的下标。原因是HashMap的容量(即数组长度)总是2的幂,这样可以确保哈希值的低位信息被充分利用,同时减少哈希冲突的可能性。

解决哈希冲突

哈希冲突:不同的键可能计算出相同的哈希值。
解决方案:HashMap采用了链表或红黑树(jdk8之后)来存储具有相同哈希值的键值对。当发生哈希冲突时,新插入的键值对会被添加到链表或红黑树的末尾。

自动扩容

当HashMap中的元素数量超过某个阈值时,HashMap会进行自动扩容。扩容操作会创建一个新的桶数组,其容量通常是原数组的两倍,然后将原有的键值对根据新的容量重新计算哈希值并放入新的数组中。这样做可以减少哈希冲突的概率,提高HashMap的性能。

注意:默认情况下负载因子的默认值为 0.75,数组大小为 16,那么当HashMap中元素个数超过 160.75=12 的时候,就把数组的大小扩展为 216=32。

HashMap在多线程是不安全的,多线程环境下可以使用ConcurrentHashMap。

HashMap 实现了Serializable接口,支持序列化,实现了Cloneable接口,支持克隆。

hashCode()的作用

在Java中,hashCode()方法用于返回对象的哈希码值,这个值通常是一个整数。这个哈希码的主要用途是在哈希表(如HashMap、HashSet、Hashtable和ConcurrentHashMap等)中确定对象应该存储在哪个存储桶中。

如果两个对象通过equals(Object obj)方法比较是相等的,那么它们的hashCode()方法必须返回相同的整数结果。反过来不成立,两个hashCode()结果相同的对象并不一定在equals(Object obj)方法中也被认为是相等的。

重写equals(Object obj)方法时,通常也需要重写hashCode()方法。

HashSet或HashMap添加对象流程

添加对象时,集合首先调用该对象的hashCode()方法来获取其哈希码。根据这个哈希码,集合可以确定该对象应该存储在哪个存储桶中。如果存储桶中已经有了其他对象,那么集合会遍历该桶中的所有对象,调用它们的equals()方法与新添加的对象进行比较。

如果equals()方法返回true,则说明新对象与桶中的某个对象相等,因此不会添加新对象(对于HashSet)或会更新旧对象(对于HashMap的键)。
如果equals()方法返回false,则说明新对象与桶中的所有对象都不相等,因此会将新对象添加到桶中。
当桶中的元素数量超过某个阈值,Java 8及以后的HashMap会将桶中的元素从链表转换为红黑树来提高性能。

JDK 1.7 与1.8下HashMap的不同

Java 1.7中:
HashMap的底层数据结构是数组+单向链表,所有的键值对存储在一个Entry数组中。每个Entry对象包含一个key-value键值对,以及一个指向下一个Entry的指针,从而形成了链表结构。当哈希冲突发生时(即两个或多个键的哈希值相同时),新的Entry会插入到对应链表的末尾。

Java 1.8中:
HashMap的底层数据结构变为数组+链表/红黑树。当链表长度达到一定的阈值(默认为8)时,链表会转换为红黑树,为了提高HashMap的性能。

HashMap何时会树化

链表长度大于阈值(默认8);HashMap的容量(即桶数组的长度)大于等于阈值(默认为64)HashMap就会进行树化

HashMap何时会退化为链表

扩容时退化

在扩容时如果拆分树时,如果某个桶(bucket)中的红黑树在拆分后元素个数小于或等于6,那么这个红黑树会被退化为链表。

删除节点时退化

HashMap中删除节点时,若根节点(root)或根节点左子树(root.left)或 根节点右子树(root.right)或根节点的左子树的左子树(root.left.left)中有一一个为null ,会退化为链表。

HashMap的索引如何计算? hashCode 都有了,为何还要提供hash()方法?数组容量为何是2的n次幂?

①使用hashCode()计算key的哈希值h,然后 h 和 h右移十六位 做异或运算,得到最终的哈希值。最后哈希值与数组容量减1的结果(hash & (capacity - 1) )进行按位与运算得到索引。

②通过hash()方法对原始的哈希值进行二次处理,可以使得最终的哈希值分布更加均匀,从而减少哈希冲突,提高HashMap的性能

③计算索引时,如果数组容量是2的n次幂,可以使用位与运算代替取模运算,效率更高;确保哈希值的低位信息能够被充分利用,进一步减少哈希冲突的可能性

hash方法底层源码

   //源码:计算哈希值的方法
   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

首先判断key是否为null,是返回结果0;
否使用hashCode()计算key的哈希值h,然后 h 和 h右移十六位 做异或运算,得到最终的哈希值。

   //底层get方法源码
   public V get(Object key) {
        Node<K,V> e;
        //通过getNode()获取key-value
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    //getNode方法中使用(n - 1) & hash 进行按位与运算得到元素存储的数组下标
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

JDK 1.7与1.8下HashMap的put方法流程有何不同

①懒惰初始化
HashMap是懒惰创建数组的,即首次使用时才创建数组(桶数组)。

②计算索引
根据key的哈希值计算桶的索引

③检查桶是否为空
如果桶的下标对应的位置为空,则在该位置创建一个新的Node并返回

④处理已存在的元素
如果桶的下标已经有人占用,则根据当前桶中元素的类型执行不同的逻辑:

  1. 如果是红黑树节点,按照红黑树的添加或更新逻辑处理.
  2. 如果是链表节点,按照链表的添加或更新逻辑处理。如果链表长度超过了树化阈值,则进行树化操作。

⑤检查是否需要扩容

在添加元素之后,检查HashMap的当前容量是否超过了阈值(通常是容量的0.75倍)。如果超过了阈值,则进行扩容操作。

不同点

①链表插入节点的方式
1.7 是头插法,多线程下可能引发循环链表
1.8是尾插法 ,可以避免循环链表

②扩容的时机
1.7 是元素数量大于阈值(容量*加载因子)并且没有空位时,才会进行扩容。
1.8是元素数量大于阈值时,就会进行扩容。

③扩容时计算索引的优化
1.8在扩容计算Node索引时,使用了更优化的算法
(n - 1) & hash

HashMap加载因子为何默认是0.75

①在空间占用与查询时间之间取得较好的权衡

②大于这个值,空间节省了,但链表就会比较长影响性能

③小于这个值,冲突减少了,但扩容就会更频繁,空间占用多

HashMap在多线程下会有什么问题

①数据不一致性
当多个线程同时对HashMap进行写操作时(例如put或remove),可能会引发数据的不一致
②死链问题
多个线程同时对同一个桶进行操作,可能会导致链表或树形结构出现死循环
③线程阻塞
当一个线程在遍历HashMap的同时,另一个线程对其进行修改操作时,可能会抛出ConcurrentModificationException异常,从而导致线程阻塞

HashMap的key能否为null,作为key的对象有什么要求?

①HashMap 的key可以为null,但是最多只能有一个null键。

②作为key的对象,必须实现hashCode和equals。

③保证在HashMap使用期间其hashCode值不变。

Hashtable基本原理

Hashtable底层原理主要基于哈希表实现, 使用哈希函数将键(key)转换为数组索引。

Hashtable是线程安全的,能用于多线程环境中。

Hashtable实现了Serializable接口,支持序列化,实现Cloneable接口,能被克隆。

HashMap和Hashtable的区别

二者父类不同
HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了Map、Cloneable、Serializable这三个接口

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {}

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {}

线程安全
Hashtable是线程安全的,它的所有方法都是同步的(所有方法都用synchronized修饰),即对于多个线程同时访问一个Hashtable实例时,可以保证数据的唯一性。而HashMap不是线程安全的,如果多个线程同时访问一个HashMap实例,可能会出现数据不一致的情况。在单线程环境下使用时,HashMap性能要比Hashtable高。

null键和null值的支持
Hashtable不允许键或值为null,否则会抛出NullPointerException异常;而HashMap可以允许null键和null值

计算hash值方式不同
HashMap通过计算key的hashCode(),在调用HashMap的hash()进行二次哈希,得到hash值。
Hashtable通过计算key的hashCode() ,得到hash值就为最终hash值。

初始容量和负载因子
Hashtable的初始容量和负载因子是固定的,在创建Hashtable实例时必须指定;而HashMap可以在创建时指定初始容量和负载因子,也可以在运行时动态调整。

ConcurrentHashMap的实现原理

数据结构
ConcurrentHashMa同样采用“数组+链表+红黑树”的混合结构。当链表长度超过一定阈值(默认为 8)时,链表会转化为红黑树,以加快查找速度;当树的大小小于一定阈值(默认为 6)时,又会退化为链表.

关于线程安全
在JDK 1.7及之前,ConcurrentHashMap采用了分段锁。整个哈希表被分为多个段(Segment),每个段维护着一部分桶(bucket)。每个段都有自己的锁,因此多个线程可以并发地修改不同段的数据,从而提高了并发性能。

在JDK 1.8及之后,ConcurrentHashMap使用更加精细化的锁机制(CAS操作和同步块),以减小锁粒度,从而提高并发性能。每个桶(Node)在更新时,都会尝试使用 CAS 操作进行无锁更新。如果 CAS 失败(其他线程正在修改该桶),则进入同步块,并使用 Synchronized 锁来确保操作的原子性。

扩容机制
采用渐进式扩容的方式。

Hashtable和ConcurrentHashMap的区别

线程安全性
Hashtable使用同步方法来实现线程安全,整个Hashtable在任一时刻只能被一个线程访问。在高并发场景下,Hashtable的性能可能会受到严重影响。
ConcurrentHashMap使用更加精细化的锁机制(CAS操作和同步块),以减小锁粒度,从而提高并发性能。它允许多个线程同时访问不同的段或桶,因此更适合高并发场景。

数据结构
Hashtable的底层结构是数组+链表,与早期的HashMap类似。
ConcurrentHashMap在JDK 1.8中的底层结构与HashMap相同,即“数组+链表+红黑树”。

扩容机制

Hashtable在扩容时,会创建一个新的数组,其大小是原数组的两倍再加一,然后重新计算所有元素的哈希值并将它们放入新的数组中。这个过程是同步的,因此在扩容期间,Hashtable的性能可能会受到影响。
ConcurrentHashMap的扩容机制更加复杂,它采用了渐进式扩容的方式,将扩容操作分摊到多个步骤中完成。

键值对限制

Hashtable不允许使用null作为键或值,如果尝试插入null键或值,会抛出NullPointerException。而ConcurrentHashMap则允许使用null作为键或值(jdk1.8之后)

初始容量和扩容因子
Hashtable的初始容量默认是11,扩容因子默认是0.75。当元素数量超过阈值(容量*扩容因子)时,会进行扩容。
ConcurrentHashMap默认初始容量是16,扩容因子默认是0.75,插入第12个元素扩容为32。

HashSet 的实现原理

Set 里的元素是不能重复的,元素重复与否是使用 equals() 方法进行判断的

HashSet实现了 Set 接口,这意味着它不允许集合中出现重复的元素。
HashSet 的实现原理是基于 HashMap 的,它利用 HashMap 的 key 的唯一性来保证元素的唯一性。
当我们向 HashSet 中添加对象时,需要重写对象的 hashCode 和 equals 方法,保证 HashSet 中的元素唯一性。

Collection和Collections的区别

定义与功能

Collection:是Java集合框架的根接口,它不直接提供具体实现,而是为其子接口(如List、Set等)提供统一的集合操作规范。Collection接口定义了如添加、删除、遍历等基本操作。

Collections:是针对集合类的一个帮助类. 它提供一系列的静态方法对各种集合的搜索, 排序, 线程安全化等操作。

实现方式

Collection:作为一个接口,它不能被直接实例化。要使用它,你需要使用其实现类(如ArrayList、LinkedList、HashSet等)。

Collections:是一个类,提供了大量的静态方法,可以直接调用这些方法来对集合进行操作。

使用场景

Collection:当你需要创建一个具体的集合对象时,你会使用它的实现类。例如,如果你需要一个有序的集合,可以使用ArrayList;如果你需要一个不包含重复元素的集合,可以使用HashSet。

Collections:当你需要对已有的集合对象进行操作,但又不希望修改原有集合的类时,你会使用Collections的静态方法。例如,你可以使用Collections.sort()方法对List进行排序,或者使用Collections.synchronizedList()方法创建一个线程安全的List。

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

线性安全的集合类:

Vector
Hashtable
ConcurrentHashMap
Stack:Stack 是 Vector 的一个子类,实现了一个后进先出的堆栈。

v线性不安全的集合类v:

HashMap 哈希表
ArrayList 动态数组
LinkedList 双向链表
HashSet 哈希集合
TreeSet
TreeMap

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值