【面试题-004】ArrayList 和 LinkList区别


在这里插入图片描述

ArrayListLinkedList 都是 Java 中常用的动态数组实现,都实现了 List 接口,但它们在内部数据结构和性能方面有所不同:

  1. 内部数据结构:
    • ArrayList 是基于动态数组的数据结构,它允许快速随机访问。数组的大小在创建时是固定的,当数组满时,ArrayList 会自动扩容,创建一个新的更大的数组,并将原数组的内容复制到新数组中。
    • LinkedList 是基于双向链表的数据结构,每个元素都是一个节点,包含数据和两个指针,分别指向前一个节点和后一个节点。链表的特点是元素可以灵活地插入和删除,不需要移动其他元素。
  2. 性能:
    • ArrayList 提供了更快的随机访问和顺序访问速度,时间复杂度为 O(1)。但是,在数组中间插入或删除元素时,需要移动目标位置后的所有元素,时间复杂度为 O(n)。
    • LinkedList 在链表中间插入和删除元素非常快,时间复杂度为 O(1)。但是,随机访问较慢,时间复杂度为 O(n),因为需要从头节点或尾节点开始遍历链表。
  3. 内存占用:
    • ArrayList 由于是基于数组的,需要连续的内存空间,而且在扩容时可能会浪费一些内存(因为新数组可能会留有未使用的空间)。
    • LinkedList 每个元素都需要额外的内存来存储指向前一个和后一个节点的指针,因此在存储大量元素时,可能会比 ArrayList 使用更多的内存。
  4. 适用场景:
    • 当需要频繁进行随机访问操作时,ArrayList 是更好的选择。
    • 当需要频繁在列表中间进行插入和删除操作时,LinkedList 更适合。
      选择 ArrayList 还是 LinkedList 取决于具体的应用场景和对性能的需求。在实际开发中,ArrayList 由于其优异的随机访问性能,通常是最常用的列表实现。只有在特定的场景下,当链表的特性(如频繁的插入和删除操作)能带来明显的性能优势时,才会考虑使用 LinkedList

List和set

Java 提供了丰富的集合框架(Collection Framework),用于存储和管理对象集合。这些集合可以分为几个主要类别:

  1. List(列表):列表是一个有序的集合,可以包含重复的元素。实现 List 接口的类包括:
    • ArrayList:基于动态数组的数据结构,提供快速的随机访问和顺序访问。
    • LinkedList:基于双向链表的数据结构,提供快速的插入和删除操作。
    • Vector:和 ArrayList 类似,但是是同步的,适用于多线程环境。不过,由于同步带来的性能开销,通常建议使用 ArrayList 并自行同步,或者使用并发集合。
  2. Set(集):集是一个无序的集合,不包含重复的元素。实现 Set 接口的类包括:
    • HashSet:基于哈希表实现,提供快速的插入、删除和查找操作。
    • LinkedHashSet:具有 HashSet 的查找效率,并且维护了插入顺序。
    • TreeSet:基于红黑树实现,可以确保元素处于排序状态。
    • EnumSet:用于存放枚举类型,内部使用位向量实现,非常高效。
  3. Queue(队列):队列是一个先进先出(FIFO)的数据结构。实现 Queue 接口的类包括:
    • PriorityQueue:基于优先级堆的无界优先队列。
    • LinkedList:也可以用作队列,因为实现了 Queue 接口。
    • ArrayDeque:是一个可扩容的双端队列,可以作为栈或队列使用。
  4. Deque(双端队列):双端队列允许在队列的两端进行元素的插入和删除。实现 Deque 接口的类包括:
    • ArrayDeque:基于数组的双端队列实现。
    • LinkedList:也可以用作双端队列。
  5. Map(映射):映射是一个键值对集合,键不包含重复的元素。实现 Map 接口的类包括:
    • HashMap:基于哈希表实现,提供快速的查找、插入和删除操作。
    • LinkedHashMap:维护了插入顺序的 HashMap
    • TreeMap:基于红黑树实现,可以确保键处于排序状态。
    • Hashtable:和 HashMap 类似,但是是同步的,适用于多线程环境。同样,由于同步带来的性能开销,通常建议使用 HashMap 并自行同步,或者使用并发集合。
    • EnumMap:键为枚举类型的特殊映射,内部使用数组实现,非常高效。
  6. 并发集合:Java 提供了一些线程安全的集合,用于多线程环境:
    • ConcurrentHashMap:线程安全的 HashMap
    • CopyOnWriteArrayList:线程安全的 ArrayList,适用于读多写少的场景。
    • CopyOnWriteArraySet:线程安全的 Set,适用于读多写少的场景。
    • BlockingQueue:线程安全的队列,用于生产者-消费者模式。
    • ConcurrentLinkedQueue:线程安全的非阻塞队列。
  7. 其他集合
    • Stack:栈是一个后进先出(LIFO)的数据结构,Java 中没有单独的 Stack 类,而是使用 Deque 接口的实现类,如 ArrayDeque,来实现栈的功能。
      这些集合类和接口都在 java.util 包中,除了并发集合,它们大多在 java.util.concurrent 包中。Java 的集合框架提供了丰富的API,使得操作集合变得非常方便和灵活。

ArrayList扩容机制

ArrayList 是 Java 中使用最广泛的动态数组实现之一。它允许我们动态地添加和删除元素,而不需要担心数组的固定大小。ArrayList 的扩容机制是其核心特性之一,下面是它的工作原理:

  1. 初始容量
    • 当我们创建一个 ArrayList 对象时,可以指定一个初始容量,如果没有指定,默认容量为 10。
  2. 扩容时机
    • 当我们尝试添加元素到 ArrayList 中,并且数组的当前大小不足以容纳新元素时,ArrayList 需要进行扩容。
  3. 扩容过程
    • ArrayList 通过一个内部数组来存储元素。当需要扩容时,它会创建一个新的更大的数组(通常是原数组大小的 1.5 倍),然后将原数组中的所有元素复制到新数组中。
    • 这个过程是通过 System.arraycopy() 方法实现的,它是一个本地方法,可以高效地复制数组。
  4. 内存复制
    • 扩容涉及到内存复制,这是一个相对昂贵的操作,因为它需要将所有现有元素从一个数组复制到另一个数组。
    • 因此,虽然 ArrayList 提供了动态添加元素的便利,但在大量添加元素的场景下,频繁的扩容可能会影响性能。
  5. 预分配
    • 为了避免频繁的扩容操作,如果预先知道将要存储的元素数量,可以在创建 ArrayList 时指定一个足够大的初始容量,这样可以减少扩容的次数。
  6. 缩容
    • ArrayList 没有提供自动缩容的功能。如果需要减少存储空间的使用,可以通过调用 trimToSize() 方法来缩小数组的大小以匹配当前元素数量。
      扩容机制是 ArrayList 能够灵活地处理元素数量的变化的关键,但它也带来了性能上的考虑。在实际使用中,根据应用场景和性能要求,合理地管理 ArrayList 的容量是非常重要的。

HashMap扩容机制

HashMap 是 Java 中使用哈希表实现的映射接口,它存储键值对(key-value pairs)。HashMap 的扩容机制是其核心特性之一,用于处理哈希表中的哈希冲突和提高性能。下面是 HashMap 的扩容机制的工作原理:

  1. 初始容量和负载因子
    • 创建 HashMap 时,可以指定初始容量和负载因子。初始容量是哈希表中的桶数,负载因子是哈希表填充程度的度量标准。
  2. 扩容时机
    • HashMap 中的元素数量达到容量和负载因子的乘积时,即 HashMap 的实际大小超过了负载因子与当前容量的乘积,HashMap 就会进行扩容。
  3. 扩容过程
    • 扩容过程涉及创建一个新的更大的数组(通常是原数组大小的两倍),然后将原数组中的所有元素重新哈希并复制到新数组中。
    • 重新哈希是因为哈希表的容量改变了,每个键的哈希值与新容量之间的关系可能会改变,因此需要重新计算每个键的索引位置。
  4. 内存复制
    • 扩容涉及到内存复制,这是一个相对昂贵的操作,因为它需要将所有现有元素从一个数组复制到另一个数组,并重新计算每个元素的哈希值。
    • 这个过程是通过数组的复制和链表的遍历来实现的。
  5. 链表和红黑树
    • HashMap 中,哈希表的每个桶可能包含一个链表或一棵红黑树。当桶中的元素数量超过一定阈值时,链表会转换为红黑树,以提高搜索效率。
    • 扩容时,链表和红黑树中的元素都需要重新哈希和重新组织。
  6. 性能考虑
    • 频繁的扩容可能会影响 HashMap 的性能,因为每次扩容都需要重新哈希和复制所有元素。
    • 为了避免性能问题,如果预先知道将要存储的键值对数量,可以在创建 HashMap 时指定一个足够大的初始容量。
      HashMap 的扩容机制是为了保持哈希表的性能和效率,同时处理哈希冲突。在实际使用中,根据应用场景和性能要求,合理地管理 HashMap 的容量是非常重要的。

HashMap初始容量(Initial Capacity)和负载因子(Load Factor)

在 Java 的 HashMap 中,初始容量(Initial Capacity)和负载因子(Load Factor)是两个重要的参数,它们在创建 HashMap 时可以进行调整,以优化性能和内存使用。

  1. 初始容量
    • 初始容量是指 HashMap 创建时的桶数,即内部数组的大小。默认的初始容量是 16。
    • 设置一个合适的初始容量可以减少扩容操作的次数,从而提高性能。如果预先知道将要存储的键值对数量,可以选择一个接近于预期数量的初始容量,但最好保持为 2 的幂,因为 HashMap 使用哈希值与数组长度的模运算来定位元素,2 的幂可以使得这个运算更高效。
  2. 负载因子
    • 负载因子是衡量 HashMap 填充程度的一个指标,它决定了 HashMap 何时进行扩容。负载因子的默认值是 0.75。
    • 负载因子等于当前元素数量(即键值对的数量)与内部数组大小的比值。当 HashMap 中的元素数量达到负载因子与内部数组大小的乘积时,HashMap 就会进行扩容,通常是容量翻倍。
    • 设置一个较低的负载因子可以减少哈希冲突的概率,但会增加内存的使用和扩容操作的频率。设置一个较高的负载因子可以节省内存,但可能会增加哈希冲突的概率和链表的长度,从而降低性能。
      在实际应用中,选择合适的初始容量和负载因子取决于具体的使用场景。如果对内存使用非常敏感,可以选择一个较高的负载因子。如果对性能要求较高,尤其是在插入和查找操作非常频繁的情况下,可以选择一个较低的负载因子,并设置一个足够大的初始容量以减少扩容操作的次数。

HashMap与HashTable区别 ?HashMap底层数据结构?

HashMapHashtable 都是 Java 中用于存储键值对的数据结构,但它们之间有一些关键的区别:

  1. 同步性
    • HashMap 不是同步的,如果多个线程同时访问并修改 HashMap,必须外部同步。
    • Hashtable 是同步的,它所有的公共方法都是同步的,适用于多线程环境。但是,这会带来性能开销,因为它需要锁定整个表来防止并发修改。
  2. null值和null键
    • HashMap 允许使用一个 null 键和多个 null 值。
    • Hashtable 不允许使用 null 键或 null 值。
  3. 迭代顺序
    • HashMap 提供了更快的迭代速度,并且迭代顺序是不确定的。
    • Hashtable 的迭代速度较慢,并且迭代顺序也是不确定的。
  4. 继承
    • HashMap 继承自 AbstractMap 类。
    • Hashtable 继承自 Dictionary 类,这是一个已经被废弃的类。
  5. 性能
    • HashMap 通常提供比 Hashtable 更好的性能,因为 HashMap 的实现更加优化。
  6. 历史
    • Hashtable 是早期 Java 版本中的实现,而 HashMap 是在 Java 2(JDK 1.2)中引入的。
      HashMap 的底层数据结构是一个数组,数组的每个元素是一个链表(在 Java 8 及更高版本中,链表在达到一定长度后会转换为红黑树以提高性能)。这个数组被称为桶(bucket)数组,每个桶对应一个哈希值。当插入一个键值对时,首先会根据键的哈希值计算出桶的索引,然后将键值对存储在相应的桶中的链表(或红黑树)中。如果两个不同的键产生了相同的哈希值,会发生哈希冲突,这时会在同一个桶中的链表(或红黑树)中存储这两个键值对。
      由于 Hashtable 的许多特性已经被 HashMap 替代,并且 Hashtable 的同步性能较差,通常建议在不需要线程安全的场景下使用 HashMap,在需要线程安全的场景下使用 ConcurrentHashMap

ConcurrentHashMap底层数据结构?

ConcurrentHashMap 是 Java 中的一个线程安全的映射实现,它位于 java.util.concurrent 包中。ConcurrentHashMap 的底层数据结构在 Java 8 及其之后的版本中经历了一些变化,下面是主要的组成:

  1. 节点(Node)
    • ConcurrentHashMap 中的元素以节点(Node)的形式存储,每个节点包含键、值、哈希值和指向下一个节点的指针。
  2. 数组(Segment)(Java 8 之前):
    • 在 Java 8 之前的版本中,ConcurrentHashMap 使用了一个分段锁(Segment)的数据结构,其中内部数组被分割成多个段,每个段是一个独立的锁结构,用于减少锁竞争。
    • 每个段包含一个小的哈希表,用于存储节点。
  3. 桶(Bucket)数组(Java 8 及之后):
    • 从 Java 8 开始,ConcurrentHashMap 的底层数据结构被重新设计,去掉了分段锁,转而使用一个大的桶数组(也称为哈希桶数组或哈希表),类似于 HashMap 的结构。
    • 桶数组中的每个桶可能包含一个链表或一棵树(红黑树),用于解决哈希冲突。
  4. 链表和红黑树
    • 当多个键映射到同一个桶时,这些键值对以链表的形式存储。
    • 在链表长度超过一定阈值后,链表会被转换成红黑树,以提高搜索效率。
  5. CAS(Compare-And-Swap)操作
    • ConcurrentHashMap 使用了无锁算法和 CAS 操作来实现并发安全,这是一种乐观锁策略,它允许在不加锁的情况下对数据进行修改,只有当预期值与实际值相同时才进行更新。
  6. 同步机制
    • ConcurrentHashMap 使用了细粒度的同步机制,只对哈希桶数组中的特定桶进行锁定,而不是整个映射,这大大减少了锁竞争,提高了并发性能。
      ConcurrentHashMap 的设计目的是提供一种高效的线程安全映射,它在多线程环境中提供了良好的并发性能,同时避免了 Hashtable 的全局锁带来的性能瓶颈。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值