Java集合框架

集合框架图

image.png

常用的数据结构

List

List是Java集合框架中的一个接口,它表示一个有序的、可重复的数据集合。List接口定义了许多操作方法,可以用于添加、删除、获取和修改列表中的元素。它提供了灵活的数据访问和操作方式,适用于许多常见的编程场景 。常用的实现类有ArrayList和LinkedList。

ArrayList

ArrayList是Java集合框架中List接口的一个常用实现类。它基于动态数组实现,可以自动调整大小,适用于频繁的随机访问和遍历操作。ArrayList提供了一系列方法,使得对列表的添加、删除、获取和修改操作变得简单和高效,作用如下

  1. 高效的随机访问:由于ArrayList基于数组实现,它通过索引来快速访问元素。相比链表实现的LinkedList,ArrayList在随机访问方面具有更高的效率,时间复杂度为O(1)。
  2. 动态调整大小:ArrayList在内部使用数组来存储元素,当元素数量增加或减少时,ArrayList会自动进行数组的扩容和缩容操作。这使得ArrayList能够根据需要动态调整大小,避免了手动管理数组容量的麻烦。
  3. 与数组的相互转换:ArrayList可以方便地与数组之间进行转换。通过ArrayList的toArray()方法,可以将ArrayList转换为数组;而通过Arrays.asList()方法,可以将数组转换为ArrayList。

ArrayList增删改查的实现逻辑
ArrayList的底层代码实现逻辑主要依赖于数组
数组获取下标快的原因:

  1. 连续内存存储:Java数组中的元素在内存中是连续存储的。这意味着元素之间没有间隔,通过计算数组的起始地址和下标的偏移量,可以直接访问到所需的元素。这样的存储方式使得获取下标的值具有很高的效率,因为不需要进行额外的寻址操作。
  2. 数组下标计算:数组元素的下标是通过偏移量计算得到的。Java虚拟机通过将数组的起始地址与下标乘以每个元素的大小相加,来定位特定下标的元素。这个计算过程是非常简单且高效的,因为乘法和加法运算是计算机硬件支持的基本操作。
  3. 编译器优化:Java编译器对数组的访问进行了优化。在编译阶段,编译器可以进行一些优化措施,例如循环展开、寄存器分配等,以提高数组访问的性能。这些优化可以进一步加快获取下标的值的速度。

以下是ArrayList增删改查基本操作说明

  1. 增加元素(add):
  • 当调用add()方法向ArrayList添加元素时,ArrayList会先检查当前元素数量(size)是否达到了内部数组的容量阈值(内部数组的容量阈值是根据ArrayList的实现策略和负载因子(load factor)决定的,默认情况下,当元素数量超过当前容量的75%时,就会触发扩容操作)。如果达到或者超过阈值,则会进行扩容操作。否则新元素会被添加到数组的末尾,更新元素的数量。
  • 扩容时,ArrayList会创建一个新的更大的数组,一般是原数组容量的1.5倍(具体的扩容策略可能有所不同,但通常是以一定比例扩容),并将原数组中的元素复制到新数组中。新元素会被添加到数组的末尾,更新ArrayList的内部数组引用为新数组,以及更新容量和元素数量
  • 需要注意的是,ArrayList的底层数组是动态调整大小的。当需要扩容时,ArrayList会创建一个更大的数组,并将原数组中的元素复制到新数组中。这涉及到系统开销,因此在预知大量元素添加的情况下,可以通过初始化ArrayList时指定初始容量,避免频繁的扩容操作,提高性能。

扩容判断伪代码

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = 10;
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 如果当前元素数量超过了内部数组的容量,则进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // 计算新的容量,一般是当前容量的1.5倍
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 如果新容量小于需要的最小容量,则使用需要的最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    // 如果新容量超过了最大容量的限制,则进行特殊处理
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 创建新的数组,并将原数组中的元素复制到新数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

  1. 删除元素(remove):
  • 当调用remove()方法从ArrayList删除元素时,ArrayList会先检查指定索引是否在有效范围内。
  • 如果索引有效,ArrayList会将要删除的元素从数组中移除。
  • 移除后,ArrayList会将数组中的元素重新排列,确保没有空洞。
  1. 修改元素(set):
  • 当调用set()方法修改ArrayList中的元素时,ArrayList会检查指定索引是否在有效范围内。
  • 如果索引有效,ArrayList会直接替换数组中相应位置的元素。
  1. 查找元素(get、indexOf):
  • 当调用get()方法根据索引获取ArrayList中的元素时,ArrayList会检查索引是否在有效范围内,并直接返回对应位置的元素。
  • 当调用indexOf()方法根据元素值查找索引时,ArrayList会从数组的头部开始遍历,逐个比较元素的值,直到找到匹配的元素或遍历完所有元素。

LinkedList

LinkedList是基于链表的数据结构实现的,它的底层代码实现逻辑与ArrayList有所不同。适用于插入和删除操作,下面是LinkedList的增删改查操作的底层实现逻辑的简要描述

  1. 增加元素(add):
  • 当调用add()方法向LinkedList添加元素时,LinkedList会创建一个新的节点,并将新节点添加到链表的尾部。
  • 如果链表为空,则新节点成为链表的头节点。
  • 如果链表非空,则通过修改原尾节点的next指针将新节点链接到链表尾部,然后更新尾节点的引用。
  1. 删除元素(remove):
  • 当调用remove()方法从LinkedList删除元素时,LinkedList会先在链表中找到要删除的节点。
  • 删除操作需要修改前一个节点的next指针,将其指向要删除节点的下一个节点,从而维持链表的连接。
  • 如果要删除的节点是头节点,则更新头节点的引用为下一个节点。
  1. 修改元素(set):
  • 当调用set()方法修改LinkedList中的元素时,LinkedList会在链表中找到指定索引的节点,并将节点的值进行更新。
  1. 查找元素(get、indexOf):
  • 当调用get()方法根据索引获取LinkedList中的元素时,LinkedList会在链表中顺序查找指定索引的节点,并返回节点的值。

  • 当调用indexOf()方法根据元素值查找索引时,LinkedList会从链表的头节点开始遍历,逐个比较节点的值,直到找到匹配的节点或遍历完整个链表。

    由于LinkedList是基于链表实现的,它的增删操作不需要像ArrayList那样进行数组的扩容和元素的复制。相对于ArrayList,LinkedList在插入和删除元素时更加高效。但是,LinkedList在访问和查找元素时需要遍历链表,效率较低。

Set

Set是一个不包含重复元素的数据集合。常用的实现类有HashSet和TreeSet。

HashSet

 HashSet是基于哈希表的数据结构,它使用HashMap来实现。在HashSet内部,实际上维护了一个私有的HashMap实例,该HashMap的键就是HashSet中的元素,而值则是一个固定的对象(在实际的实现中,可以看作是一个常量对象)<br />    哈希表是一种通过将键映射到特定位置来实现快速查找的数据结构。它使用哈希函数将键转换为数组索引,并在该索引处存储对应的值。当需要查找或插入值时,哈希表使用相同的哈希函数来确定键的索引,并在该位置执行操作。这样,可以在平均情况下以常数时间复杂度(O(1))执行查找、插入和删除操作<br />**HashSet内部实现**
    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

Hash值计算

/**
来判断 key 是否为 null。
如果 key 为 null,则返回 0,表示哈希值为 0。
如果 key 不为 null,则使用 key.hashCode() 方法计算 key 的哈希码,并将结果赋值给变量 h。
这里使用了位运算符 ^(异或)来对 h 进行操作,h >>> 16 表示将 h 无符号右移 16 位
 */
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

下面是一些基本方法的说明

  1. 添加元素(add): 当向HashSet中添加一个元素时,HashSet会首先计算元素的哈希码(通过调用元素的hashCode()方法)。然后,它使用哈希码和位运算来确定元素在内部数组中的索引位置。如果该位置上已经存在元素,则使用链表或红黑树等数据结构来处理哈希碰撞(两个不同的元素具有相同的哈希码)。最终,HashSet将元素存储在对应的位置上。实际上是将该元素作为HashMap的键添加到HashMap中,并将该元素映射到一个固定的值(PRESENT)
 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
  1. 删除元素(remove): 当从HashSet中删除一个元素时,HashSet会首先计算该元素的哈希码,然后确定它在内部数组中的索引位置。然后,HashSet会遍历该位置上的链表或红黑树(如果存在哈希碰撞),找到要删除的元素,并进行删除操作。实际上是对HashMap的删除操作
public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
  1. 不可修改元素: HashSet中的元素是不可变的,因此它没有直接提供修改元素的方法。要修改一个元素,需要先从HashSet中删除该元素,然后再将修改后的元素添加回去。

  2. 查找元素是否在Set中(contains): 在HashSet中,我们通常使用contains()方法来判断一个元素是否存在。contains()方法会根据元素的哈希值和equals()方法来确定元素是否存在于HashSet中。如果存在,返回true。否则返回false

    HashSet中的元素是无序的,即元素的存储顺序不一定与添加顺序相同。此外,HashSet允许存储null元素,但只能存储一个null元素,因为HashSet不允许重复元素

TreeSet

HashSet是基于红黑树数据结构实现,提供有序的元素遍历。它使用TreeMap来实现。在HashSet内部,实际上维护了一个私有的TreeMap实例,该TreeMap的键就是HashSet中的元素,而值则是一个固定的对象(在实际的实现中,可以看作是一个常量对象)
TreeSet的无参构造

 public TreeSet() {
        this(new TreeMap<E,Object>());
    }

TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

红黑树
红黑树是一种自平衡的二叉搜索树,具有以下特性

  1. 红黑树的节点是红色或黑色。

  2. 根节点是黑色的。

  3. 叶子节点(NIL节点,表示空节点)是黑色的。

  4. 如果一个节点是红色的,则其子节点必须是黑色的。

  5. 从根节点到叶子节点的任意路径上,黑色节点的数量相同。

    TreeSet的增删改查操作涉及到红黑树的插入、删除和查找操作。下面是TreeSet的增删改查操作的底层代码实现逻辑的简要描述

  6. 添加元素(增):

  • 当向TreeSet中添加元素时,实际上是将元素作为TreeMap中的键添加到TreeMap中。
  • 元素的值可以是一个固定的常量对象(TreeSet中不关心值),因为TreeSet只关心元素的唯一性和顺序。
  • 添加元素的过程类似于向TreeMap插入键值对的过程,将元素作为键,值为一个固定的常量对象。
  • 插入过程中,根据元素的比较规则(自然顺序或自定义比较器)将元素插入到红黑树的合适位置。
  • 添加元素后,红黑树会自动进行调整以保持平衡性质。
public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
  1. 删除元素(删):
  • 当从TreeSet中删除元素时,实际上是从TreeMap中删除对应的键值对。
  • 根据元素的比较规则找到待删除的键值对,并将其从红黑树中移除。
  • 删除键值对后,红黑树会自动进行调整以保持平衡性质。
  public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
  1. 修改元素(改):
  • TreeSet中的元素是不可修改的,因为修改元素可能导致元素的顺序发生变化。如果需要修改元素,需要先删除原有元素,然后添加修改后的元素。
  1. 查找元素(查):
  • 当在TreeSet中查找元素时,实际上是在TreeMap中查找对应的键。
  • 根据元素的比较规则进行二叉搜索树的查找操作,沿着树的路径逐个比较节点的键值,直到找到目标元素或遍历完整个树。
  • 查找操作的时间复杂度为O(log n),其中n是TreeSet中元素的数量。
  • TreeSet提供查询的方法有以下几种
first(): 返回TreeSet中的第一个(最低)元素。
last(): 返回TreeSet中的最后一个(最高)元素。
lower(element): 返回TreeSet中小于给定元素的最大元素。
higher(element): 返回TreeSet中大于给定元素的最小元素。
floor(element): 返回TreeSet中小于或等于给定元素的最大元素。
ceiling(element): 返回TreeSet中大于或等于给定元素的最小元素。
pollFirst(): 移除并返回TreeSet中的第一个元素。
pollLast(): 移除并返回TreeSet中的最后一个元素。

Map

Map是一种键值对的数据结构,存储着唯一的键和对应的值。常用的实现类有HashMap和TreeMap。

HashMap

HashMap是Java集合框架中的键值对(Key-Value)映射实现类,它使用哈希表(Hash Table)数据结构来存储和管理键值对。HashMap的增删改查操作涉及到哈希表的插入、删除和查找操作。下面是HashMap的增删改查操作的底层代码实现逻辑的简要描述:

  1. 添加元素(put):
  • 当向HashMap中添加键值对时,首先根据键的哈希值计算出数组索引位置。
  • 如果该索引位置为空(即没有发生哈希冲突),直接将键值对存储在该位置。
  • 如果该索引位置已经存在其他键值对(即发生了哈希冲突),则通过链表或红黑树解决冲突,将键值对插入到链表或红黑树中。
  • 在插入键值对后,如果链表或红黑树的长度超过一定阈值(默认为8),HashMap会根据具体情况将链表转换为红黑树,以提高查找的效率。
  1. 删除元素(remove):
  • 当从HashMap中删除键值对时,首先根据键的哈希值计算出数组索引位置。
  • 如果该索引位置为空,表示没有要删除的键值对。
  • 如果该索引位置存在一个键值对,可以直接删除该键值对。
  • 如果该索引位置存在多个键值对(可能是链表或红黑树),需要遍历链表或红黑树来找到要删除的键值对,并将其删除。
  1. 修改元素(put):
  • HashMap中的元素是可修改的,可以直接通过键来修改对应的值。
  • 首先根据键的哈希值计算出数组索引位置。
  • 如果该索引位置为空,表示没有要修改的键值对。
  • 如果该索引位置存在一个键值对,可以直接修改该键值对的值。
  • 如果该索引位置存在多个键值对(可能是链表或红黑树),需要遍历链表或红黑树来找到要修改的键值对,并修改其值。
  1. 查找元素(get):
  • 当在HashMap中查找元素时,首先根据键的哈希值计算出数组索引位置。
  • 如果该索引位置为空,表示没有找到对应的键值对。
  • 如果该索引位置存在一个键值对,可以直接返回该键值对的值。
  • 如果该索引位置存在多个键值对(可能是链表或红黑树),需要遍历链表或红黑树来找到对应的键值对,并返回其值。

HashMap的底层实现利用了哈希表的特性,通过哈希函数将键映射到数组索引位置,并使用链表或红黑树来解决哈希冲突。这样可以在常数时间内执行插入、删除和查找操作,使得HashMap具有高效的性能。

TreeMap

  1. 添加元素(put):
  • 当向TreeMap中添加键值对时,首先根据键的比较规则将键值对插入红黑树中的合适位置。
  • 如果红黑树为空,直接将键值对作为根节点插入红黑树。
  • 如果红黑树不为空,则从根节点开始,根据键的比较结果依次向左子树或右子树遍历,找到插入位置。
  • 插入键值对后,红黑树会自动进行平衡调整以保持红黑树的性质。
  1. 删除元素(remove):
  • 当从TreeMap中删除键值对时,根据键的比较规则在红黑树中找到对应的节点。
  • 如果找到节点,则根据节点的情况进行删除操作:
    • 如果待删除节点没有子节点,可以直接删除该节点。
    • 如果待删除节点有一个子节点,可以将子节点代替待删除节点。
    • 如果待删除节点有两个子节点,需要找到其后继节点或前驱节点作为替代,并删除后继节点或前驱节点。
  • 删除节点后,红黑树会自动进行平衡调整以保持红黑树的性质。
  1. 修改元素(put):
  • TreeMap中的元素是可修改的,可以直接通过键来修改对应的值。
  • 根据键的比较规则在红黑树中找到对应的节点,并更新节点的值。
  1. 查找元素(get):
  • 当在TreeMap中查找元素时,根据键的比较规则在红黑树中进行二叉搜索树的查找操作。
  • 从根节点开始,根据键的比较结果依次向左子树或右子树遍历,直到找到目标节点或遍历完整个树。
  • 查找操作的时间复杂度为O(log n),其中n是TreeMap中键值对的数量。

TreeMap的底层实现利用了红黑树的特性,通过自平衡的红黑树结构实现了高效的插入、删除和查找操作,同时保持了键的有序性。这使得TreeMap适用于需要有序遍历、范围查找和基于键的排序操作的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值