Java 学习 - Day 14

1、集合

1.1、定义

        集合(Collection)在Java中是一种容器,用于存储、检索、管理和操作一组对象,与之类似的是之前提到的数组,数组的不足在于:

        1、长度固定:
                数组一旦被创建,其大小(长度)就是固定的,这意味着无法动态地改变数组的大小。如果需要添加或删除元素,可能需要创建新的数组并将原有数组中的元素复制过去,这在处理大量数据时效率低下且耗费资源。
        2、类型单一:
                数组只能存储同一种类型的元素,这限制了它的灵活性。虽然可以通过Object类型的数组来存储不同类型的数据,但这并不是最佳实践,因为这样可能会导致类型安全问题。
        3、内存连续性:
                数组要求在内存中分配一块连续的空间,对于非常大的数组,可能难以找到足够大的连续内存块,特别是在内存碎片较多的情况下。
        4、索引越界检查:
                Java中数组访问时会有索引越界的检查,这虽然保证了安全性,但也可能带来一定的性能开销。

        而与数组相比,集合的优点在于:

        1、长度可变:集合可以动态地扩展或缩小,不需要预先指定大小。
        2、类型多样性:集合可以存储任何类型的对象,并且可以混合存储不同类型的对象。
        3、灵活的操作:集合提供了丰富的API来支持各种操作,如添加、删除、迭代等。
        4、类型安全:通过泛型的支持,集合可以在编译时确保类型安全,避免了运行时的ClassCastException。

        我们所说的集合通常是Collection 或者Map 的实现类,其中Collection 还有两个子接口List、Set 都表示单列集合;而Map 则表示双列集合

1.2、Collection

        Collection接口是Java集合框架的基础接口之一,它定义了所有单列集合类(即只存储单个对象的集合)应具有的基本操作和行为。Collection接口本身并不提供具体的实现,而是作为所有具体集合类的共同父接口,规定了集合类必须支持的方法。

        Collection 集合的特点如下:

                1、通用性:Collection接口提供了一系列通用方法,如添加元素(add)、移除元素(remove)、判断是否包含某个元素(contains)、获取元素数量(size)等,这些方法适用于所有实现了该接口的集合类。
                2、迭代支持:Collection接口继承自Iterable接口,因此实现了Collection接口的集合类都支持迭代操作。通过调用iterator()方法可以获得一个Iterator对象,用于遍历集合中的元素。
                3、子接口:Collection有几个重要的子接口,如List和Set,它们各自定义了更具体的行为。List接口保持元素的插入顺序,并允许重复元素;Set接口不允许重复元素,并且通常不保持元素的插入顺序(除了LinkedHashSet)。
                4、实现类:Collection接口有许多具体的实现类,例如ArrayList、LinkedList、HashSet、TreeSet等,每种实现类都有其特定的内部结构和性能特征,适合不同的应用场景。
                5、扩展性:Collection接口设计得非常灵活,允许开发人员根据需要创建自己的集合实现,只需遵循接口定义的方法即可。
                6、类型安全:从Java 5开始,通过引入泛型,Collection接口支持类型安全的集合,可以指定集合中元素的具体类型,从而避免了运行时的类型转换错误。

        Collection 集合中常用的方法如下:

                1、添加与删除
                        boolean add(E e):将指定的元素添加到此集合中(如果该集合不支持此操作,则抛出UnsupportedOperationException)。
                        boolean remove(Object o):从此集合中移除指定的元素(如果该集合不支持此操作,则抛出UnsupportedOperationException)。
                        boolean addAll(Collection<? extends E> c):将指定集合中的所有元素添加到此集合中。
                        boolean removeAll(Collection<?> c):从此集合中移除指定集合包含的所有元素。
                        boolean retainAll(Collection<?> c):仅保留此集合中包含于指定集合中的元素。
                2、查询
                        int size():返回集合中元素的数量。
                        boolean isEmpty():如果此集合不包含任何元素,则返回true。
                        boolean contains(Object o):如果此集合包含指定元素,则返回true。
                        boolean containsAll(Collection<?> c):如果此集合包含指定集合中的所有元素,则返回true。
                3、迭代
                        Iterator<E> iterator():返回在此集合的元素上进行迭代的迭代器。
                        Object[] toArray():返回包含此集合中所有元素的数组。
                        <T> T[] toArray(T[] a):返回包含此集合中所有元素的数组;如果该数组足以容纳集合,则返回该数组,否则返回一个新的数组。
                4、其他
                        void clear():移除此集合中的所有元素。
                        boolean equals(Object o):比较指定的对象与此集合是否相等。
                        int hashCode():返回集合的哈希码值。

1.2.1、Iterable

        Iterable接口是一个泛型接口,它定义了一个iterator()方法,该方法返回一个实现了Iterator接口的对象。任何实现了Iterable接口的类都可以被用来迭代其中的元素。在Java中,Iterable接口主要用于集合类,使得集合可以被遍历,其本身并不存放对象。

        Iterator接口提供了几个核心方法来遍历集合中的元素:
                1、boolean hasNext():如果仍有更多的元素可以迭代,则返回true。
                2、E next():返回迭代中的下一个元素。
                3、void remove():从产生此迭代器的集合中移除迭代器返回的最后一个元素(可选操作)。

        迭代器的执行原理如下:

                1、获取迭代器:
                        当需要遍历一个实现了Iterable接口的对象时,首先调用该对象的iterator()方法来获取一个Iterator实例。
                2、检查是否有下一个元素:
                        在遍历过程中,使用hasNext()方法来检查集合中是否还有未被访问的元素。如果返回true,则表示还有元素可以访问。
                3、获取下一个元素:
                        如果hasNext()返回true,则调用next()方法来获取下一个元素。每次调用next()方法都会返回序列中的下一个元素。
                4、移除元素(可选):
                        可以选择性地调用remove()方法来移除迭代器返回的最后一个元素。这通常用于在遍历过程中修改集合。


        迭代器通常维护一个内部状态,比如一个索引或指针,用于跟踪当前迭代的位置。每次调用next()方法时,迭代器会更新这个状态,指向集合中的下一个元素

        比如使用Iterable 遍历MyCollection 集合如下:

MyCollection<String> myCollection = new MyCollection<>();
Iterator<String> iterator = myCollection.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    System.out.println(item);
}

        

        除了通过Iterable 对象遍历集合之外,也可以使用增强for循环,也被称为for-each循环,是在Java 5(JDK 1.5)中引入的一种新的循环结构。它的主要目的是简化对数组和集合的遍历过程,提高代码的可读性和简洁性。增强for循环特别适用于只需要读取集合或数组中的元素而不需关心索引的情况。

        在使用增强for 时,实际上是对标准迭代器(Iterator)的封装。当编译器遇到增强for循环时,它会自动转化为使用迭代器的循环。这意味着,对于每一个元素,编译器会调用Iterator的next()方法来获取下一个元素,并在每次循环结束时调用hasNext()方法来检查是否还有更多的元素。

MyCollection<String> myCollection = new MyCollection<>();
for (String item : myCollection) {
    System.out.println(item);
}

        使用增强for 时需要注意以下问题:

                1、不可修改集合:使用增强for循环时,不能直接修改正在遍历的集合或数组的内容,因为这会导致不确定的行为。如果需要修改集合中的元素,应该使用传统的for循环或迭代器。
                2、空指针异常:在使用增强for循环之前,应当确保遍历的对象不是null,否则会导致NullPointerException。
                3、性能考虑:尽管增强for循环提供了更好的可读性,但在某些需要频繁调用hasNext()和next()方法的情况下,性能可能不如手动使用迭代器优化。

1.2.2、List

        List接口是Java集合框架的一部分,它继承自Collection接口,并提供了额外的方法来操作列表中的元素。其特点如下:
                1、有序性:List中的元素是有顺序的,元素按照插入顺序排列,并且可以通过索引(基于零的索引)来访问。
                2、可重复性:List允许存储重复的元素。
                3、索引访问:List提供了许多基于索引的方法来插入、访问、替换和删除元素。
                4、线程不安全:大多数List实现类(如ArrayList和LinkedList)默认情况下不是线程安全的,如果多个线程并发访问,需要外部同步。

        List接口定义了许多方法,以下是其中一些常用的方法:
                1、添加元素
                        boolean add(E e):向列表末尾添加一个元素。
                        void add(int index, E element):在列表中的指定位置插入一个元素。
                2、获取元素
                        E get(int index):返回列表中指定位置的元素。
                        E set(int index, E element):用指定元素替换列表中指定位置上的元素,并返回旧值。
                3、删除元素
                        E remove(int index):删除列表中指定位置的元素,并返回该元素。
                        boolean remove(Object o):从列表中删除第一次出现的指定元素(如果它是列表的元素)。
                4、查找元素
                        int indexOf(Object o):返回指定元素在列表中首次出现的位置(索引),如果列表不包含此元素,则返回-1。
                        int lastIndexOf(Object o):返回指定元素在列表中最后一次出现的位置(索引),如果列表不包含此元素,则返回-1。
                5、其他
                        ListIterator<E> listIterator():返回列表元素的列表迭代器。
                        List<E> subList(int fromIndex, int toIndex):返回从fromIndex(含)到toIndex(不含)的列表视图。
                        void sort(Comparator<? super E> c):根据指定比较器提供的顺序对列表中的元素进行排序。

        List接口有多个实现类,常见的实现包括:
                1、ArrayList:基于动态数组实现,提供了对元素的快速随机访问,但插入和删除操作较慢。
                2、LinkedList:基于双向链表实现,插入和删除操作较快,但随机访问较慢。
                3、Vector:类似于ArrayList,但它是线程安全的,因此在多线程环境中使用时不需要额外的同步措施。
                4、Stack:继承自Vector,实现了一个后进先出(LIFO)栈。

1.2.2.1、ArrayList

        ArrayList是Java中List接口的一个实现类,它提供了动态数组的功能

        ArrayList 的底层实现如下:

                1、数组存储:
                        ArrayList内部使用一个Object类型的数组elementData来存储元素。这个数组随着元素的增加可以自动扩展。
                        数组的初始容量默认为10(如果没有指定初始容量的话),并且当数组容量不足以容纳新添加的元素时,ArrayList会自动调整其容量。
                2、容量调整(扩容):
                        当尝试向ArrayList中添加元素,而当前容量不足以容纳新元素时,ArrayList会创建一个新的更大的数组,并将原有数组中的所有元素复制到新数组中。
                        扩容通常是将当前容量增加50%,即新的容量为旧容量的1.5倍。例如,如果当前容量为10,那么扩容后的容量将是15。
                3、初始化与构造函数:
                        可以通过构造函数指定初始容量,例如new ArrayList<>(initialCapacity)。
                        如果没有指定初始容量,ArrayList会使用默认的初始容量(通常是10)。        


        ArrayList 的特点为:

                1、随机访问:由于ArrayList基于数组实现,所以随机访问元素的时间复杂度为O(1),非常高效。
                2、插入与删除:在列表中间插入或删除元素的时间复杂度为O(n),因为需要移动插入或删除位置之后的所有元素。
                3、扩容操作:扩容操作涉及数组复制,时间复杂度为O(n),但这是不频繁的操作,平均而言,每次添加元素的时间复杂度接近O(1)。

                4、序列化:ArrayList中的elementData字段被标记为transient,这意味着它不会参与序列化过程。序列化时,ArrayList会保存其元素的列表视图,而不是直接保存elementData数组。


        自动扩容流程:

                1、add(E e) 方法
                        当调用 add 方法添加一个新元素到 ArrayList 时,首先会检查当前数组是否还有足够的空间来存放新的元素。
如果没有足够的空间,ArrayList 将会调用 ensureCapacityInternal 方法来确保有足够的容量。
                2、ensureCapacityInternal(int minCapacity) 方法
                        这个方法用于确保内部数组 elementData 的容量至少为 minCapacity。
如果当前容量小于 minCapacity,则会调用 ensureExplicitCapacity 方法。
                3、ensureExplicitCapacity(int minCapacity) 方法
                        这个方法会检查是否需要增加 ArrayList 的容量。如果需要,则调用 grow 方法。
                4、grow(int minCapacity) 方法
                        grow 方法负责实际的扩容操作。通常情况下,ArrayList 会将当前容量增加到原来的 1.5 倍,然后创建一个新的数组,并将旧数组中的所有元素复制到新数组中。完成后,elementData 字段会被新数组替换,从而完成扩容。


        ArrayList 适用于需要频繁进行随机访问,且插入顺序和取出顺序一致的情况   

1.2.2.2、Vector

        和ArrayList 一样,Vector 也是用于存储动态数组的集合类,实现了List 接口。而与ArrayList 相比,二者的区别如下:

        1、线程安全性:
                Vector 是线程安全的,这意味着它的所有公共方法都被 synchronized 修饰符所修饰,因此可以在多线程环境中安全地使用,无需额外的同步措施。
                相比之下,ArrayList 不是线程安全的,如果多个线程同时访问一个 ArrayList 实例,必须通过外部同步机制来确保线程安全。
        2、性能:
                由于 Vector 提供了同步功能,它在多线程环境中的性能通常低于 ArrayList,特别是在不需要同步的情况下。
                ArrayList 在单线程环境中通常具有更好的性能,因为它没有同步开销。
        3、增长策略:
                当 Vector 的容量不足以容纳更多的元素时,默认情况下它会将容量翻倍来适应更多的元素(默认初始容量为10)。
                ArrayList 在需要更多空间时,通常会增加原有容量的 50%。
        4、历史背景:
                Vector 是 jdk 1.0的一部分,而 ArrayList 是jdk 1.2 引入的。
        5、使用场景:
                如果需要一个线程安全的列表,可以选择 Vector,但在现代 Java 编程实践中,更推荐使用 Collections.synchronizedList(new ArrayList(...)) 来包装一个 ArrayList,这样可以获得更好的性能。
                对于大多数情况,特别是单线程应用,推荐使用 ArrayList 因为其提供了更好的性能。

1.2.2.3、LinkedList

        LinkedList 是 Java 中的一个集合类,它实现了 List 接口,并且是以双向链表的形式来存储元素。其特点如下:

        1、双向链表结构:
                每个元素(节点)都有一个指向其前一个节点 (prev) 和后一个节点 (next) 的引用。这种结构使得 LinkedList 能够在任意位置插入或删除元素,而无需移动其他元素。
        2、插入和删除操作高效:
                由于 LinkedList 使用链表结构,插入和删除操作只需要更新相关节点的指针即可完成,因此这些操作的时间复杂度为 O(1),即常数时间。
        3、查询效率较低:
                为了访问链表中的某个特定元素,LinkedList 必须从头节点或尾节点开始遍历,直到找到指定位置的元素。因此,随机访问的时间复杂度为 O(n),其中 n 是链表的长度。
        4、元素可重复:
                LinkedList 允许存储重复的元素,并且可以包含 null 值。
        5、内存消耗较大:
                每个节点除了存储实际的数据外,还需要额外的内存来保存前后节点的引用,这使得 LinkedList 在内存使用上比数组实现的集合(如 ArrayList)更加昂贵。
        6、线程不安全:
                LinkedList 类似于 ArrayList,其基本操作不是线程安全的。如果多个线程同时访问一个 LinkedList 实例,则必须保证外部同步。
        7、内部类 Node:
                LinkedList 使用了一个内部类 Node 来表示链表中的节点。这个内部类持有实际的数据项和对前后节点的引用。

        其底层操作机制如下:

                1、双向链表结构:
                        LinkedList 维护了一个双向链表,每个节点(Node 对象)包含三个属性:prev、next 和 item。prev 指向前一个节点,next 指向后一个节点,而 item 存储实际的数据值。
                2、首尾节点引用:
                        LinkedList 类中维护了两个引用 first 和 last,分别指向链表的第一个节点和最后一个节点。这使得在空链表的情况下,first 和 last 都为 null;对于非空链表,first.prev 和 last.next 都为 null。
                3、插入操作:
                        插入操作可以通过修改节点之间的 prev 和 next 引用来完成。例如,向链表的中间插入一个新节点时,需要更新新节点的前后节点的指针,使其指向新节点,并更新新节点的前后指针。
                        在链表头部插入新节点时,新节点的 next 指向当前的 first 节点,并且 first.prev 指向新节点,然后更新 first 指向新节点。
                        在链表尾部插入新节点时,新节点的 prev 指向当前的 last 节点,并且 last.next 指向新节点,然后更新 last 指向新节点。
                4、删除操作:
                        删除操作同样涉及更新节点之间的指针。删除一个节点时,需要修改该节点的前一个节点的 next 指针和后一个节点的 prev 指针,使其不再指向被删除的节点。
                        删除链表的第一个节点时,需要更新 first 指针指向第二个节点,并且使新的 first 节点的 prev 为 null。
                        删除链表的最后一个节点时,需要更新 last 指针指向倒数第二个节点,并且使新的 last 节点的 next 为 null。
                5、迭代器支持:
                        LinkedList 支持快速的迭代操作,因为迭代器可以直接通过节点的 next 或 prev 属性向前或向后移动。
                6、双向链表的优势:
                        双向链表允许双向遍历,并且在插入和删除元素时不需要移动任何元素,只需更改指针即可。

        LinkedList 非常适合需要频繁进行插入和删除操作的场景,但不适合需要频繁随机访问元素的情况。例如,当实现一个队列或栈时,LinkedList 可能是一个很好的选择,因为它支持高效的插入和删除操作

1.2.3、Set

        Set 接口是 Collection 接口的一个子接口,其特点如下:

                1、不可重复元素:Set 接口中不允许存在重复的元素。这意味着对于任意两个元素 e1 和 e2,它们之间不能满足 e1.equals(e2) 返回 true。因此,尝试添加一个与已存在元素相等的新元素将不会成功。
                2、无序:Set 中的元素是没有顺序的,也就是说,元素不是按照插入顺序或者任何其他预定义的顺序存储的。对于某些实现(如 HashSet),元素的实际存储顺序可能是不确定的。
                3、最多一个 null 元素:Set 实现允许存储一个 null 值,但不能存储多个 null 值,因为 null.equals(null) 会抛出 NullPointerException。
                4、线程不安全:大多数 Set 的实现类默认是线程不安全的。如果多个线程同时访问并修改 Set,则需要外部同步机制来保证数据的一致性和完整性。
                5、实现类:
                        HashSet:基于哈希表实现,提供了很好的插入和查找性能,但元素的迭代顺序是不确定的。
                        TreeSet:基于红黑树实现,提供了排序功能,可以按照自然顺序或者自定义比较器定义的顺序排序元素。
                        LinkedHashSet:继承自 HashSet,但是维护了一个运行于所有条目的双重链接列表,这样就可以保持元素的插入顺序。

        Set 接口本身并没有定义新的方法,它继承了 Collection 接口的所有方法。然而,由于 Set 的特性,一些方法的行为受到了限制,例如 add 方法不能添加已经存在的元素,不能通过索引来获取元素

1.2.3.1、HashSet

        HashSet 在 Java 中是一个实现了 Set 接口的类,它提供了基于哈希表的集合实现。HashSet 的底层实现基于 HashMap,具体细节如下:
                1、存储机制:HashSet 使用 HashMap 来存储元素,每一个元素都被当作 HashMap 的键(Key),而对应的值(Value)通常是一个固定的 PRESENT 对象(在 JDK 8 中是一个静态 final 对象 Null),表示 HashSet 中的元素不需要关联任何值。

// HashSet 中所有的Value 都是该对象
private static final Object PRESENT = new Object();

return map.put(e, PRESENT)==null;


                2、哈希码计算:当向 HashSet 添加一个元素时,HashSet 会调用该元素的 hashCode() 方法来计算其哈希码,然后使用这个哈希码来确定元素在 HashMap 中的位置。

// 将hash 值与无符号右移16 位之后的结果进行按位异或操作
// 目标是为了让结果更加分散
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

// 将数组长度 - 1 与hash 值按位与之后的结果作为存放位置
n = (tab = resize()).length;
i = (n - 1) & hash;


                3、冲突处理:如果多个元素的哈希码相同(即发生了哈希冲突),HashSet 将使用元素的 equals() 方法(需要关注具体如何实现)来检查元素是否相等。如果不相等,则这些元素会被存储在 HashMap 中的同一链表或红黑树的不同位置。

// hash 值相同且为同一对象或内容相同则被认为是重复内容
if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
   e = p;

// 转为红黑树上的节点
else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

// 遍历该位置处的链表
else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表长度超过8 就准备转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 存在重复内容
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

// 存在重复内容(e != null)的处理
if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // onlyIfAbsent 默认为false,表示需要更改现有值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }


                4、链表和红黑树:在 JDK 8 及以后版本中,HashMap 的内部实现使用了数组加链表或红黑树的结构。当链表长度超过一定阈值(默认为8)且数组长度超过一定阈值(默认64),链表会转换为红黑树,以提高查找效率。

// 如果数组为null 或者长度小于64,则对数组进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();


                5、插入操作:当向 HashSet 中添加一个新元素时,这个元素被放入 HashMap 的键中,而值部分是 PRESENT。如果 HashMap 中已经存在一个与新元素相等的键,则添加操作失败,不会改变集合状态。

if ((e = p.next) == null) {
        // 遍历链表之后仍未找到重复元素,则添加到链表末尾
        p.next = newNode(hash, key, value, null);
         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                treeifyBin(tab, hash);
                break;
             }


                6、删除操作:从 HashSet 中删除一个元素时,实际上是调用 HashMap 的 remove 方法来完成的。
                7、迭代顺序:HashSet 不保证迭代顺序,即遍历 HashSet 时,元素的顺序是不确定的,除非使用了 LinkedHashSet 来保持插入顺序。
                8、线程安全性:HashSet 默认是非线程安全的。如果多个线程并发地访问同一个 HashSet 实例,而其中至少一个线程修改了该 HashSet,则必须保持外部同步,否则可能会导致数据不一致。

                9、扩容机制:当table 数组已被使用超过0.75 * 长度(临界值)就会扩容至原来的两倍,并且更新临界值。

// 每次往HashSet 中添加元素的时候size 都会自增
// 也就是说临界值只与table 数组中的元素数量相关,与table 数组被占用的长度无关
// 比如仅在索引为1 的位置存在一条链表,它有12 个节点,那么下一次添加时就会触发扩容,table 的长度就会翻倍
if (++size > threshold)
            resize();

if (oldCap > 0) {
            // 如果旧容量不小于最大容量2^30 则临界值为最大容量值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 新容量和新临界值都扩大两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

        HashSet 能够提供高效的添加、删除和查找操作,通常这些操作的时间复杂度为 O(1),但在最坏的情况下(所有元素都散列到同一个位置),时间复杂度会退化为 O(n)

1.2.3.2、LinkedHashSet

1.3、Map

1.4、Collections

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值