Java高手的30k之路|面试攻略|精通List Set

知识点

1. 集合框架的基本概念

  • Collection接口:了解Collection接口的基本方法和常用实现类。
  • Collections类:熟悉Collections类中的静态方法,如排序、搜索、同步、不可变集合等操作。

2. List接口和实现类

  • ArrayList:了解其内部实现、动态数组的扩容机制、随机访问性能优良、插入和删除操作性能较差。
  • LinkedList:理解其基于双向链表的实现、插入和删除操作性能优良、随机访问性能较差。
  • Vector:掌握其线程安全特性,了解与ArrayList的区别。

3. Set接口和实现类

  • HashSet:理解基于哈希表的实现、无序性和不允许重复元素的特性、负载因子和初始容量的影响。
  • LinkedHashSet:熟悉其在HashSet基础上维护插入顺序的特性。
  • TreeSet:了解基于红黑树的实现、排序特性、对元素必须实现Comparable接口或提供Comparator

面试题

Collection接口的常用方法和实现类

在 Java 中,Collection 是所有集合框架接口的根接口。它定义了一组方法,用于操作一组对象,这些对象称为其元素。Collection 接口本身并没有直接的实现类,但是它的子接口,如 ListSetQueue 等,有很多常见的实现类。

Collection 接口的基本方法

Collection 接口定义了一组基本操作集合的方法,以下是一些常用方法:

  • boolean add(E e):向集合中添加元素。如果集合因调用而发生更改,则返回 true
  • boolean addAll(Collection<? extends E> c):向集合中添加指定集合中的所有元素。如果集合因调用而发生更改,则返回 true
  • void clear():移除集合中的所有元素。
  • boolean contains(Object o):如果集合包含指定的元素,则返回 true
  • boolean containsAll(Collection<?> c):如果集合包含指定集合中的所有元素,则返回 true
  • boolean isEmpty():如果集合不包含元素,则返回 true
  • Iterator<E> iterator():返回集合中元素的迭代器。
  • boolean remove(Object o):移除集合中指定的元素(如果存在)。如果集合因调用而发生更改,则返回 true
  • boolean removeAll(Collection<?> c):移除集合中那些也包含在指定集合中的所有元素。如果集合因调用而发生更改,则返回 true
  • boolean retainAll(Collection<?> c):仅保留集合中那些包含在指定集合中的元素。如果集合因调用而发生更改,则返回 true
  • int size():返回集合中元素的个数。
  • Object[] toArray():返回包含集合中所有元素的数组。
  • <T> T[] toArray(T[] a):返回包含集合中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。

Collection 接口的常用实现类

Collection 接口的子接口有多个,每个子接口有其特定的实现类。以下是一些常用的实现类:

List 接口的实现类
  • ArrayList:基于数组实现,允许快速随机访问元素。增删元素时,可能需要调整数组大小,效率较低。

    List<String> arrayList = new ArrayList<>();
    
  • LinkedList:基于双向链表实现,适合于频繁的插入和删除操作,随机访问性能较差。

    List<String> linkedList = new LinkedList<>();
    
  • Vector:类似于 ArrayList,但它是线程安全的。

    List<String> vector = new Vector<>();
    
  • Stack:继承自 Vector,表示一个后进先出(LIFO)的栈。

    Stack<String> stack = new Stack<>();
    
Set 接口的实现类
  • HashSet:基于哈希表实现,不保证集合的迭代顺序,允许 null 元素。

    Set<String> hashSet = new HashSet<>();
    
  • LinkedHashSet:继承自 HashSet,但使用链表维护元素的插入顺序。

    Set<String> linkedHashSet = new LinkedHashSet<>();
    
  • TreeSet:基于红黑树实现,保证集合的元素处于排序状态。

    Set<String> treeSet = new TreeSet<>();
    
Queue 接口的实现类
  • LinkedList:也实现了 Queue 接口,可以用作队列。

    Queue<String> queue = new LinkedList<>();
    
  • PriorityQueue:基于优先级堆实现的优先级队列,元素按自然顺序或指定的比较器顺序排序。

    Queue<String> priorityQueue = new PriorityQueue<>();
    
Map 接口的实现类

虽然 Map 不是 Collection 的子接口,但它也是 Java 集合框架的重要组成部分:

  • HashMap:基于哈希表实现,允许 null 键和 null 值,不保证顺序。

    Map<String, String> hashMap = new HashMap<>();
    
  • LinkedHashMap:继承自 HashMap,但使用链表维护插入顺序。

    Map<String, String> linkedHashMap = new LinkedHashMap<>();
    
  • TreeMap:基于红黑树实现,保证键的排序顺序。

    Map<String, String> treeMap = new TreeMap<>();
    

Collections常用方法

Collections 类是 Java 集合框架的一部分,提供了许多静态方法来操作或返回集合。以下是 Collections 类中的一些常用静态方法,它们可以进行排序、搜索、同步、创建不可变集合等操作。

排序操作

  • sort(List<T> list): 对指定的列表按自然顺序进行升序排序。

    List<String> list = new ArrayList<>(Arrays.asList("Banana", "Apple", "Cherry"));
    Collections.sort(list);
    System.out.println(list); // 输出: [Apple, Banana, Cherry]
    
  • sort(List<T> list, Comparator<? super T> c): 使用指定的比较器对指定的列表进行排序。

    List<String> list = new ArrayList<>(Arrays.asList("Banana", "Apple", "Cherry"));
    Collections.sort(list, Comparator.reverseOrder());
    System.out.println(list); // 输出: [Cherry, Banana, Apple]
    

搜索操作

  • binarySearch(List<? extends Comparable<? super T>> list, T key): 使用二分法搜索指定列表中的指定对象。列表必须按自然顺序排序。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    int index = Collections.binarySearch(list, "Banana");
    System.out.println(index); // 输出: 1
    
  • binarySearch(List<? extends T> list, T key, Comparator<? super T> c): 使用指定的比较器对列表进行二分法搜索。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    int index = Collections.binarySearch(list, "Banana", Comparator.reverseOrder());
    System.out.println(index); // 输出: -3 (未找到)
    

同步操作

  • synchronizedList(List<T> list): 返回指定列表的同步(线程安全)列表。

    List<String> list = Collections.synchronizedList(new ArrayList<>());
    
  • synchronizedSet(Set<T> s): 返回指定集合的同步(线程安全)集合。

    Set<String> set = Collections.synchronizedSet(new HashSet<>());
    
  • synchronizedMap(Map<K, V> m): 返回指定映射的同步(线程安全)映射。

    Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
    

不可变集合

  • unmodifiableList(List<? extends T> list): 返回指定列表的不可变视图。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    List<String> unmodifiableList = Collections.unmodifiableList(list);
    
  • unmodifiableSet(Set<? extends T> s): 返回指定集合的不可变视图。

    Set<String> set = new HashSet<>(Arrays.asList("Apple", "Banana", "Cherry"));
    Set<String> unmodifiableSet = Collections.unmodifiableSet(set);
    
  • unmodifiableMap(Map<? extends K, ? extends V> m): 返回指定映射的不可变视图。

    Map<String, String> map = new HashMap<>();
    map.put("key1", "value1");
    Map<String, String> unmodifiableMap = Collections.unmodifiableMap(map);
    

其他有用的方法

  • reverse(List<?> list): 反转指定列表中元素的顺序。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    Collections.reverse(list);
    System.out.println(list); // 输出: [Cherry, Banana, Apple]
    
  • shuffle(List<?> list): 使用默认随机源对指定列表进行置换。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    Collections.shuffle(list);
    System.out.println(list); // 输出: 随机顺序的列表
    
  • min(Collection<? extends T> coll): 返回给定集合中的最小元素。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    String minElement = Collections.min(list);
    System.out.println(minElement); // 输出: Apple
    
  • max(Collection<? extends T> coll): 返回给定集合中的最大元素。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    String maxElement = Collections.max(list);
    System.out.println(maxElement); // 输出: Cherry
    
  • replaceAll(List<T> list, T oldVal, T newVal): 用新值替换列表中所有出现的旧值。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Apple"));
    Collections.replaceAll(list, "Apple", "Orange");
    System.out.println(list); // 输出: [Orange, Banana, Orange]
    
  • frequency(Collection<?> c, Object o): 返回指定集合中等于指定对象的元素数量。

    List<String> list = new ArrayList<>(Arrays.asList("Apple", "Banana", "Apple"));
    int freq = Collections.frequency(list, "Apple");
    System.out.println(freq); // 输出: 2
    
  • copy(List<? super T> dest, List<? extends T> src): 将源列表中的所有元素复制到目标列表中。

    List<String> src = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
    List<String> dest = new ArrayList<>(Arrays.asList("Orange", "Grapes", "Pineapple"));
    Collections.copy(dest, src);
    System.out.println(dest); // 输出: [Apple, Banana, Cherry]
    

不可变集合

不可变集合(immutable collection)指的是在创建后不能被修改的集合。这意味着一旦不可变集合被创建,您不能添加、删除或更改其中的任何元素。不可变集合在一些场景下非常有用,例如:

  1. 线程安全:因为不可变集合不能被修改,它们天然是线程安全的。多个线程可以并发地读取同一个不可变集合而不需要同步。
  2. 防止意外修改:不可变集合可以防止集合被意外修改,确保数据的稳定性和一致性。
  3. 简化代码:使用不可变集合可以减少代码中的可变状态,使程序更容易理解和维护。

在 Java 中,可以使用 Collections 类的静态方法来创建不可变集合。例如:

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

public class ImmutableCollectionsExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 创建不可变集合
        List<String> immutableList = Collections.unmodifiableList(list);

        // 试图修改不可变集合会抛出 UnsupportedOperationException
        // immutableList.add("Orange"); // 会抛出异常

        System.out.println(immutableList);
    }
}

创建不可变集合的方式

  1. 使用 Collections.unmodifiableXXX 方法

    • unmodifiableList(List<? extends T> list)
    • unmodifiableSet(Set<? extends T> s)
    • unmodifiableMap(Map<? extends K, ? extends V> m)

    这些方法返回一个不可变视图,这意味着底层集合仍然可以修改,但通过不可变视图进行修改会抛出 UnsupportedOperationException

  2. 使用 Java 9 引入的工厂方法

    • List.of(E... elements)
    • Set.of(E... elements)
    • Map.of(K k1, V v1, K k2, V v2, ...)

    这些方法直接创建不可变集合。例子:

    import java.util.List;
    import java.util.Set;
    import java.util.Map;
    
    public class ImmutableCollectionsExample {
        public static void main(String[] args) {
            // 创建不可变列表
            List<String> immutableList = List.of("Apple", "Banana", "Cherry");
    
            // 创建不可变集合
            Set<String> immutableSet = Set.of("Apple", "Banana", "Cherry");
    
            // 创建不可变映射
            Map<String, String> immutableMap = Map.of(
                "key1", "value1",
                "key2", "value2"
            );
    
            System.out.println(immutableList);
            System.out.println(immutableSet);
            System.out.println(immutableMap);
        }
    }
    

注意事项

  • 不可变集合一旦创建,它们的内容是固定的,任何修改操作都会抛出 UnsupportedOperationException
  • 使用 Collections.unmodifiableXXX 方法创建的不可变集合实际上是对原集合的一个不可变视图。如果原集合改变,不可变视图也会改变。
  • 使用 Java 9 的工厂方法创建的不可变集合是真正的不可变集合,它们不受任何外部变化的影响。
  • 不可变集合指的是集合本身不能被修改,即不能添加、删除或替换元素。然而,如果集合中的元素是可变的对象(即对象的属性可以被改变),那么即使集合本身是不可变的,你仍然可以修改这些对象的属性。这并不会导致异常或错误。

举个例子:

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "'}";
    }
}

public class ImmutableCollectionsExample {
    public static void main(String[] args) {
        List<Person> list = new ArrayList<>();
        list.add(new Person("Alice"));
        list.add(new Person("Bob"));
        list.add(new Person("Charlie"));

        // 创建不可变集合
        List<Person> immutableList = Collections.unmodifiableList(list);

        // 尝试修改集合本身会抛出异常
        // immutableList.add(new Person("David")); // 会抛出异常

        // 但是可以修改集合中对象的属性
        Person p = immutableList.get(0);
        p.setName("Alice Updated");

        System.out.println(immutableList);
    }
}

在上面的例子中,尽管 immutableList 是一个不可变集合,不能添加或删除元素,但我们仍然可以修改 Person 对象的属性。运行这段代码不会抛出异常,输出将是:

[Person{name='Alice Updated'}, Person{name='Bob'}, Person{name='Charlie'}]
不可变集成元素不可变注意
  1. 不可变集合并不保证集合元素的不可变性:如果集合中的元素是可变对象,那么你仍然可以修改这些对象的属性。要确保完全不可变性,需要保证集合中的元素也是不可变的对象。

  2. 防止修改集合中的对象:如果需要防止修改集合中的对象,可以使用不可变对象(例如,使用 final 关键字修饰类和字段)或在创建不可变集合时,确保其中的对象是不可变的。

  3. 不可变对象的优势:在多线程环境下,不可变对象更容易确保线程安全,因为它们的状态不会改变。结合不可变集合和不可变对象,可以实现更健壮的并发程序。

ArrayList

ArrayList 是 Java 集合框架中的一个常用类,提供了一个动态数组实现。以下是对其内部实现、特点、性能和使用场景的详细介绍:

内部实现

ArrayList 是基于数组实现的。它内部维护了一个动态的数组,当需要添加更多元素时,数组会进行自动扩容。

动态数组的扩容机制

ArrayList 的初始容量可以在创建时指定。如果没有指定,则使用默认容量。当 ArrayList 超过其容量时,会进行扩容。扩容的过程如下:

  1. 创建一个新的数组,其容量通常是当前数组的 1.5 倍(或其他增长因子)。
  2. 将旧数组中的元素复制到新数组中。
  3. 替换旧数组为新数组。

这种扩容机制使得 ArrayList 能够动态地增长以适应增加的元素,但也引入了在扩容时进行元素复制的性能开销。

性能特点

  • 随机访问性能优良:由于 ArrayList 基于数组实现,支持通过索引进行快速的随机访问,时间复杂度为 O(1)。
  • 插入和删除操作性能较差:在数组中间进行插入或删除操作时,需要移动数组中的其他元素,时间复杂度为 O(n),其中 n 是数组的长度。因此,对于频繁的插入和删除操作,ArrayList 的性能较差。

使用场景

ArrayList 适用于以下场景:

  1. 需要快速随机访问:当需要通过索引快速访问元素时,ArrayList 是一个理想的选择。
  2. 读多写少的场景:在插入和删除操作较少,而读取操作较多的场景中,ArrayList 性能优良。
  3. 动态数组需求:当数组的大小不确定且需要动态增长时,ArrayList 是一个方便的选择。

最佳实践

  1. 初始化容量:在创建 ArrayList 时,尽量指定初始容量,以减少扩容次数,提高性能。
    List<String> list = new ArrayList<>(100); // 指定初始容量为 100
    
  2. 批量操作:如果有大量元素需要添加到 ArrayList 中,尽量使用 addAll 方法进行批量添加,而不是逐个添加。
    List<String> list = new ArrayList<>();
    List<String> newElements = Arrays.asList("a", "b", "c");
    list.addAll(newElements);
    
  3. 尽量减少中间插入和删除操作:如果需要频繁地在中间进行插入和删除操作,考虑使用 LinkedList 或其他合适的数据结构。

需要注意的坑

  1. 扩容开销:当 ArrayList 进行扩容时,会有一次性的性能开销。如果初始容量估计不足,频繁的扩容会影响性能。
  2. 线程不安全ArrayList 不是线程安全的。在多线程环境下使用 ArrayList 需要进行适当的同步,或使用线程安全的集合类如 CopyOnWriteArrayListCollections.synchronizedList(new ArrayList<>())
  3. 避免 ConcurrentModificationException:在使用迭代器遍历 ArrayList 的同时,不能对 ArrayList 进行结构性修改(添加、删除元素),否则会抛出 ConcurrentModificationException。可以使用 Iteratorremove 方法或使用 ListIterator 进行安全的遍历和修改。

LinkedList

LinkedList 是 Java 集合框架中的一个常用类,提供了一个基于双向链表的实现。以下是对其内部实现、特点、性能和使用场景的详细介绍:

内部实现

LinkedList 是基于双向链表(Doubly Linked List)实现的。它由一系列节点组成,每个节点包含三个部分:

  1. 元素值(element):存储节点中的数据。
  2. 前驱指针(previous):指向前一个节点。
  3. 后继指针(next):指向后一个节点。

双向链表使得 LinkedList 可以在链表的任何位置进行高效的插入和删除操作。

插入和删除操作性能优良

由于 LinkedList 是基于链表实现的,插入和删除操作非常高效,尤其是在链表的头部或尾部进行操作时:

  • 插入操作:在链表的任意位置插入元素时,只需要调整前驱指针和后继指针即可,时间复杂度为 O(1)。
  • 删除操作:删除任意位置的元素时,同样只需调整相邻节点的指针,时间复杂度为 O(1)。

但是,如果需要在链表中间位置进行操作,需要先遍历到该位置,遍历的时间复杂度为 O(n)。

随机访问性能较差

由于链表不支持通过索引直接访问元素,必须从头部或尾部开始逐节点遍历:

  • 随机访问:访问链表中任意位置的元素时,需要遍历链表,时间复杂度为 O(n)。这使得 LinkedList 的随机访问性能较差。

使用场景

LinkedList 适用于以下场景:

  1. 频繁的插入和删除操作:当应用程序中有大量的插入和删除操作,特别是在链表的头部或尾部,LinkedList 是一个理想的选择。
  2. FIFO(先进先出)或 LIFO(后进先出)LinkedList 可以作为队列(Queue)或栈(Stack)使用,提供高效的入队、出队、入栈、出栈操作。
  3. 迭代访问:当主要操作是遍历而不是随机访问时,LinkedList 是一个合适的选择。

最佳实践

  1. 避免随机访问:尽量避免在 LinkedList 中进行随机访问操作,因为性能较差。需要随机访问时,考虑使用 ArrayList
  2. 使用 ListIterator:当需要在遍历的同时进行插入或删除操作时,使用 ListIterator 提供的接口可以确保操作的安全性和效率。
    List<String> list = new LinkedList<>();
    ListIterator<String> iterator = list.listIterator();
    while (iterator.hasNext()) {
        String element = iterator.next();
        if (condition) {
            iterator.add("newElement");
        }
    }
    

需要注意的坑

  1. 随机访问性能LinkedList 的随机访问性能较差,因此在需要频繁随机访问的场景下,应避免使用 LinkedList
  2. 内存开销LinkedList 的每个节点都需要额外的存储空间来保存前驱和后继指针,因此比 ArrayList 占用更多的内存。
  3. 线程不安全LinkedList 不是线程安全的,在多线程环境下使用时需要进行同步处理,或使用 Collections.synchronizedList(new LinkedList<>())

Vector

Vector 是 Java 中一个实现了 List 接口的类,和 ArrayList 类似,都是基于数组的数据结构。它们有许多相似的地方,但 VectorArrayList 之间也有一些重要的区别,尤其是在线程安全性方面。

Vector 的特点

  1. 线程安全

    • Vector 是线程安全的。它的所有方法都使用了同步(synchronized)关键字,因此可以在多线程环境中安全使用。
    • 由于其同步特性,Vector 的性能比 ArrayList 要低,特别是在单线程环境中。
  2. 扩容机制

    • Vector 的默认扩容机制是每次容量不足时,将容量增加一倍。你也可以通过构造函数来指定扩容的增量。
    • ArrayList 的扩容机制则是每次容量不足时,容量增加约 50%。
  3. 遗留类

    • Vector 是 Java 1.0 中引入的遗留类(legacy class),而 ArrayList 是 Java 2(JDK 1.2)引入的集合框架的一部分。
    • 尽管 Vector 仍然可以使用,但在大多数情况下,建议使用 ArrayList 代替,除非需要线程安全。

ArrayList 的区别

  1. 线程安全性

    • Vector 是线程安全的,因为它的所有公共方法都同步。
    • ArrayList 不是线程安全的。如果在多线程环境中使用 ArrayList,需要手动同步。
  2. 性能

    • 由于同步开销,Vector 的性能通常比 ArrayList 要低,特别是在单线程环境中。
    • ArrayList 的性能更好,因为它没有同步开销。
  3. 扩容策略

    • Vector 每次扩容时容量增加一倍。
    • ArrayList 每次扩容时容量增加 50%。
  4. API 差异

    • Vector 提供了一些 ArrayList 没有的方法,比如 addElementelementAtfirstElement 等。这些方法主要是为了兼容旧版的 API。

示例代码

下面是一个简单的示例,展示了 VectorArrayList 的基本使用:

import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

public class Main {
    public static void main(String[] args) {
        // 创建一个 ArrayList
        List<String> arrayList = new ArrayList<>();
        arrayList.add("A");
        arrayList.add("B");
        arrayList.add("C");

        // 创建一个 Vector
        List<String> vector = new Vector<>();
        vector.add("A");
        vector.add("B");
        vector.add("C");

        // 打印 ArrayList 内容
        System.out.println("ArrayList: " + arrayList);

        // 打印 Vector 内容
        System.out.println("Vector: " + vector);
    }
}

结论

  • 使用 Vector 时,要注意其同步特性带来的性能开销。除非明确需要线程安全,否则在大多数情况下,推荐使用 ArrayList
  • 如果需要线程安全的列表,可以考虑使用 Collections.synchronizedList 方法将 ArrayList 包装成线程安全的版本,或者使用 CopyOnWriteArrayList

HashSet

HashSet 的特点

HashSet 是 Java 中实现 Set 接口的类,基于哈希表实现。它具有以下几个重要的特点:

  1. 基于哈希表的实现

    • HashSet 内部使用 HashMap 来存储元素。每个元素作为 HashMap 的键,HashMap 的值则是一个固定的对象。
    • 由于哈希表的特性,HashSet 提供了良好的增删查性能,平均时间复杂度为 O(1)。
  2. 无序性

    • HashSet 中的元素是无序的。它不能保证元素的顺序,这与插入顺序无关。
  3. 不允许重复元素

    • HashSet 不允许存储重复的元素。如果尝试添加一个已经存在的元素,add 方法会返回 false
  4. 负载因子和初始容量

    • 初始容量:HashSet 创建时可以指定初始容量,默认为 16。
    • 负载因子:当 HashSet 中元素数量超过当前容量乘以负载因子时,会进行扩容,默认负载因子为 0.75。较高的负载因子减少了空间开销,但增加了查找时间。较低的负载因子则相反。

示例代码

import java.util.HashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        Set<String> hashSet = new HashSet<>();

        // 添加元素
        hashSet.add("A");
        hashSet.add("B");
        hashSet.add("C");

        // 尝试添加重复元素
        boolean isAdded = hashSet.add("A");
        System.out.println("元素 A 是否被添加: " + isAdded);

        // 打印 HashSet 内容
        System.out.println("HashSet: " + hashSet);

        // 删除元素
        hashSet.remove("B");
        System.out.println("删除元素 B 后的 HashSet: " + hashSet);

        // 检查元素是否存在
        boolean containsC = hashSet.contains("C");
        System.out.println("HashSet 是否包含元素 C: " + containsC);
    }
}

影响性能的因素

  1. 初始容量

    • 如果可以预估 HashSet 将要存储的元素数量,设置合适的初始容量可以减少扩容次数,提高性能。
  2. 负载因子

    • 负载因子的设置影响哈希表的密度,从而影响查找和存储的效率。一般情况下,默认的 0.75 是一个较为折中的选择。
    • 如果频繁进行插入和删除操作,适当调整负载因子可能会提升性能。

使用场景

  1. 去重

    • HashSet 常用于存储不允许重复的元素集合。比如过滤重复的用户ID、商品列表等。
  2. 快速查找

    • 由于哈希表的高效查找特性,可以用 HashSet 来实现快速查找某个元素是否存在于集合中。

注意事项

  1. 元素的 hashCodeequals 方法
    • HashSet 中的元素必须正确实现 hashCodeequals 方法,以确保元素的唯一性和查找的效率。
    • 如果元素的 hashCodeequals 实现不当,会导致错误的行为或者性能问题。
import java.util.HashSet;
import java.util.Set;

public class Example {

    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        set.add(new Person("a", 1));
        set.add(new Person("a", 1));
        System.out.println(set.size()); // 输入结果 2
    }

    static class Person {
        private String name;
        private int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
}
  1. 无序性
    • 如果需要有序集合,应该使用 LinkedHashSetTreeSet,而不是 HashSet

HashMap put源码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这段代码是 HashMap 中用来计算键的哈希值的部分代码。具体来说,它是哈希分布函数的一部分,用来提高哈希码的质量,减少冲突。

代码解释

  • int h;:声明一个整数变量 h
  • (key == null) ? 0:如果键 keynull,返回哈希值 0。在 HashMap 中,null 键是允许的,并且总是被存储在哈希桶数组的第一个位置。
  • : (h = key.hashCode()) ^ (h >>> 16):否则,计算键的哈希码,并通过将哈希码的高16位与低16位进行异或运算来混合哈希码,以减少哈希冲突。

详细说明

  • key.hashCode():调用键的 hashCode() 方法,得到键的哈希码。这是一个标准的 Java 方法,所有对象都继承自 Object 类。
  • h = key.hashCode():将哈希码赋值给变量 h
  • h >>> 16:将哈希码的高16位右移到低16位位置,高位用零填充。这实际上是把哈希码分成高16位和低16位。
  • h ^ (h >>> 16):将哈希码的高16位和低16位进行异或运算。这个操作的目的是将哈希码的高位信息和低位信息混合起来,使得哈希码的分布更加均匀,减少冲突。

目的

这个哈希分布函数的目的是通过对哈希码进行二次混合,来减少哈希冲突。在 HashMap 中,哈希冲突会导致多个键被映射到同一个哈希桶,从而降低性能。通过将高位和低位的信息混合,哈希分布函数能使键的哈希值更加均匀地分布在哈希桶数组中,提高哈希表的性能。

示例

假设我们有一个键,它的哈希码为 0x12345678

  1. h = 0x12345678(哈希码)
  2. h >>> 16 = 0x00001234(高16位右移到低16位)
  3. h ^ (h >>> 16) = 0x12345678 ^ 0x00001234 = 0x1234444C(将高16位和低16位进行异或)

通过这种方式,哈希分布函数将输入哈希码混合为 0x1234444C,使其具有更好的分布特性,减少冲突的可能性。

LinkedHashSet

LinkedHashSetHashSet 的一个变种,它在 HashSet 的基础上增加了维护元素插入顺序的特性。以下是对 LinkedHashSet 的详细介绍:

LinkedHashSet 的特性

  1. 有序性

    • LinkedHashSet 维护着一个双向链表,用于记录元素的插入顺序。因此,迭代 LinkedHashSet 的元素时,返回的顺序与插入顺序一致。
  2. 无重复元素

    • LinkedHashSet 继承自 HashSet,因此不允许包含重复的元素。每个元素在集合中是唯一的。
  3. 哈希表实现

    • LinkedHashSet 内部使用 HashMap 来存储元素,但它同时维护了一个链表来保持顺序。

内部工作原理

  • LinkedHashSet 使用一个内部类 LinkedHashMap 来存储元素。LinkedHashMapHashMap 的子类,增加了维护插入顺序的功能。
  • LinkedHashSet 中,每一个元素不仅存储在哈希表中,还在链表中维护它们的顺序。
  • 当插入一个元素时,LinkedHashSet 会在哈希表中添加这个元素,并在链表的尾部追加该元素。
  • 当删除一个元素时,LinkedHashSet 会从哈希表和链表中同时删除该元素。

使用场景

LinkedHashSet 适用于以下场景:

  • 需要一个不允许重复元素的集合,并且需要维护元素的插入顺序。
  • 希望在迭代集合时,元素的顺序与其插入顺序一致。
  • 需要一个有序版本的 HashSet,但不希望使用 TreeSetTreeSet 基于红黑树,维护的是元素的自然顺序或指定的比较器顺序,而 LinkedHashSet 维护的是插入顺序)。

示例代码

import java.util.LinkedHashSet;

public class LinkedHashSetExample {
    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();

        linkedHashSet.add("A");
        linkedHashSet.add("B");
        linkedHashSet.add("C");
        linkedHashSet.add("D");
        linkedHashSet.add("E");

        // 尝试添加重复元素
        linkedHashSet.add("A");

        // 迭代 LinkedHashSet
        for (String element : linkedHashSet) {
            System.out.println(element);
        }
    }
}

输出

A
B
C
D
E

性能

  • 插入、删除、查找:与 HashSet 类似,LinkedHashSet 的插入、删除和查找操作的时间复杂度为 O(1)。
  • 内存开销:由于维护了一个双向链表来记录插入顺序,LinkedHashSet 相比 HashSet 需要更多的内存。

注意事项

  • 当不需要维护插入顺序时,优先使用 HashSet,以节省内存。
  • 如果需要维护自然顺序或自定义顺序,请使用 TreeSet

LinkedHashSetHashSet 的有序版本,非常适合需要既保证元素唯一性又需要维护插入顺序的场景。

TreeSet

TreeSet 是 Java 集合框架中的一个类,基于 NavigableSet 接口实现,底层采用红黑树(Red-Black Tree)数据结构。它保证元素的排序性和集合的基本操作(如插入、删除、查询)的高效性。

基本特性

  1. 基于红黑树TreeSet 使用红黑树实现,保证了元素的有序性和操作的对数时间复杂度。
  2. 排序特性TreeSet 中的元素按照自然顺序(如果元素实现了 Comparable 接口)或指定的顺序(通过 Comparator)进行排序。
  3. 唯一性TreeSet 不允许存储重复的元素。
  4. 元素要求:存储的元素必须实现 Comparable 接口,或者在创建 TreeSet 时提供一个 Comparator 对象。
基于红黑树的实现

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

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色。
  • 所有叶子节点(NIL 节点)是黑色。
  • 如果一个节点是红色的,则它的两个子节点都是黑色的(即没有两个红色节点相连)。
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
TreeSet 的排序特性
自然排序

TreeSet 中的元素实现了 Comparable 接口时,元素按照自然顺序排序。

import java.util.TreeSet;

public class NaturalOrderingExample {
    public static void main(String[] args) {
        TreeSet<Integer> numbers = new TreeSet<>();
        numbers.add(5);
        numbers.add(3);
        numbers.add(9);
        numbers.add(1);

        // 输出按自然顺序排序的集合
        System.out.println(numbers); // [1, 3, 5, 9]
    }
}
自定义排序

在创建 TreeSet 时,可以提供一个 Comparator 对象,以指定元素的排序规则。

import java.util.Comparator;
import java.util.TreeSet;

public class CustomOrderingExample {
    public static void main(String[] args) {
        Comparator<String> lengthComparator = Comparator.comparingInt(String::length);

        TreeSet<String> strings = new TreeSet<>(lengthComparator);
        strings.add("apple");
        strings.add("banana");
        strings.add("cherry");
        strings.add("date");

        // 输出按长度排序的集合
        System.out.println(strings); // [date, apple, banana, cherry]
    }
}
使用 TreeSet 的常见操作
添加元素

TreeSet 中添加元素使用 add 方法。

TreeSet<String> set = new TreeSet<>();
set.add("apple");
set.add("banana");
set.add("cherry");
删除元素

TreeSet 中删除元素使用 remove 方法。

set.remove("banana");
查询元素

可以使用 contains 方法检查 TreeSet 中是否包含特定元素。

boolean containsApple = set.contains("apple");
遍历元素

可以使用增强的 for 循环或迭代器遍历 TreeSet 中的元素。

for (String fruit : set) {
    System.out.println(fruit);
}
TreeSet 中的高级操作
获取子集

可以使用 subSet 方法获取 TreeSet 的子集。

TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);

SortedSet<Integer> subSet = numbers.subSet(2, 4);
System.out.println(subSet); // [2, 3]
获取第一个和最后一个元素

可以使用 firstlast 方法获取 TreeSet 中的第一个和最后一个元素。

Integer firstElement = numbers.first(); // 1
Integer lastElement = numbers.last(); // 5
获取小于和大于指定元素的最大和最小元素

可以使用 lowerhigher 方法获取小于和大于指定元素的最大和最小元素。

Integer lower = numbers.lower(3); // 2
Integer higher = numbers.higher(3); // 4
代码示例:使用 Comparable 和 Comparator
import java.util.*;

public class TreeSetExample {
    public static void main(String[] args) {
        // 使用 Comparable 接口进行自然排序
        TreeSet<Person> peopleByAge = new TreeSet<>();
        peopleByAge.add(new Person("Alice", 30));
        peopleByAge.add(new Person("Bob", 25));
        peopleByAge.add(new Person("Charlie", 35));

        System.out.println("People by age:");
        for (Person person : peopleByAge) {
            System.out.println(person);
        }

        // 使用 Comparator 进行自定义排序
        Comparator<Person> nameComparator = Comparator.comparing(Person::getName);
        TreeSet<Person> peopleByName = new TreeSet<>(nameComparator);
        peopleByName.add(new Person("Alice", 30));
        peopleByName.add(new Person("Bob", 25));
        peopleByName.add(new Person("Charlie", 35));

        System.out.println("People by name:");
        for (Person person : peopleByName) {
            System.out.println(person);
        }
    }

    static class Person implements Comparable<Person> {
        private String name;
        private int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        @Override
        public int compareTo(Person other) {
            return Integer.compare(this.age, other.age);
        }

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

TreeSet 典型的使用场景

1. 排序集合

在需要保持集合中的元素按照自然顺序或自定义顺序排序时,TreeSet 是一个理想选择。它可以自动管理元素的排序,无需手动排序。

TreeSet<String> sortedNames = new TreeSet<>();
sortedNames.add("Alice");
sortedNames.add("Bob");
sortedNames.add("Charlie");
System.out.println(sortedNames); // [Alice, Bob, Charlie]

2. 去重和排序结合

TreeSet 不允许重复元素,因此在需要既去重又保持排序的集合中,TreeSet 是一个不错的选择。

TreeSet<Integer> uniqueSortedNumbers = new TreeSet<>();
uniqueSortedNumbers.add(3);
uniqueSortedNumbers.add(1);
uniqueSortedNumbers.add(2);
uniqueSortedNumbers.add(1); // 重复元素不会被添加
System.out.println(uniqueSortedNumbers); // [1, 2, 3]

3. 范围查询

TreeSet 提供了高效的子集操作,可以快速获取某个范围内的元素。这在需要频繁进行范围查询的场景中非常有用。

TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);

SortedSet<Integer> subSet = numbers.subSet(2, 4); // 包含2,不包含4
System.out.println(subSet); // [2, 3]

4. 有序的数据结构

TreeSet 维护元素的自然顺序或自定义顺序,适用于实现有序的数据结构,如有序列表、优先级队列等。

class Task implements Comparable<Task> {
    String name;
    int priority;

    Task(String name, int priority) {
        this.name = name;
        this.priority = priority;
    }

    @Override
    public int compareTo(Task other) {
        return Integer.compare(this.priority, other.priority);
    }

    @Override
    public String toString() {
        return name + ": " + priority;
    }
}

TreeSet<Task> taskQueue = new TreeSet<>();
taskQueue.add(new Task("Task1", 5));
taskQueue.add(new Task("Task2", 1));
taskQueue.add(new Task("Task3", 3));

System.out.println(taskQueue); // [Task2: 1, Task3: 3, Task1: 5]

5. 排序并去重的合并操作

在合并多个集合时,使用 TreeSet 可以自动排序并去重。

TreeSet<Integer> set1 = new TreeSet<>(Arrays.asList(1, 2, 3));
TreeSet<Integer> set2 = new TreeSet<>(Arrays.asList(3, 4, 5));

set1.addAll(set2);
System.out.println(set1); // [1, 2, 3, 4, 5]

6. 事件调度系统

在事件调度系统中,事件根据时间戳排序,使用 TreeSet 可以方便地管理事件队列,并在需要时快速检索和删除事件。

class Event implements Comparable<Event> {
    String description;
    long timestamp;

    Event(String description, long timestamp) {
        this.description = description;
        this.timestamp = timestamp;
    }

    @Override
    public int compareTo(Event other) {
        return Long.compare(this.timestamp, other.timestamp);
    }

    @Override
    public String toString() {
        return description + ": " + timestamp;
    }
}

TreeSet<Event> eventQueue = new TreeSet<>();
eventQueue.add(new Event("Event1", 1000));
eventQueue.add(new Event("Event2", 500));
eventQueue.add(new Event("Event3", 1500));

System.out.println(eventQueue); // [Event2: 500, Event1: 1000, Event3: 1500]

7. 文本自动补全

在文本自动补全功能中,可以使用 TreeSet 存储和管理字典词汇表,以便快速查找和匹配前缀。

TreeSet<String> dictionary = new TreeSet<>(Arrays.asList("apple", "banana", "cherry", "date", "fig", "grape"));
String prefix = "ch";
SortedSet<String> suggestions = dictionary.tailSet(prefix);
suggestions = suggestions.headSet(prefix + Character.MAX_VALUE);

System.out.println(suggestions); // [cherry]

这段代码的作用是从字典中获取以特定前缀开头的单词,并按字典顺序返回以该前缀开头的所有单词。具体来说:

  1. 首先,创建了一个 TreeSet 对象 dictionary,其中包含了一些单词。
  2. 然后,指定了一个前缀 prefix
  3. 接着,使用 tailSet 方法获取字典中大于或等于给定前缀的子集。
  4. 最后,使用 headSet 方法获取前缀开头的单词子集,其中不包括大于前缀开头的最小单词。

这样,suggestions 中包含了以 "ch" 开头的所有单词,并按字典顺序排序。

红黑树

红黑树是一种自平衡二叉搜索树,由于节点具有红色和黑色标记而得名。红黑树的节点具有以下性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL 节点)都是黑色的。
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的(即不能有两个连续的红节点)。
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑色节点数量相等)。

为什么要标识红节点和黑节点?

红黑树之所以需要标识红节点和黑节点,是为了保证树的平衡性和性质的实现。通过规定节点的颜色,并且保持特定的插入和删除操作,可以保证红黑树的高效的自平衡特性,即树的高度在对数级别内。

红黑树设计的优缺点

优点:
  1. 平衡性好:红黑树能够自动保持树的平衡,插入、删除和查找的时间复杂度都是对数级别的。
  2. 高效性:红黑树的基本操作效率高,对于大规模数据的存储和操作很有效。
  3. 实现简单:相比其他平衡树结构,如 AVL 树,红黑树的实现相对简单。
缺点:
  1. 相对复杂:红黑树的算法相对于其他数据结构较为复杂,不易理解。
  2. 不适合频繁插入和删除操作:虽然红黑树在大部分情况下能够保持平衡,但频繁的插入和删除操作可能导致树的不平衡,影响性能。

适用场景

红黑树适用于需要高效地插入、删除和查找操作,并且对平衡性要求较高的场景,例如数据库索引、C++ STL 中的 std::map、Java 中的 TreeMapTreeSet 等。

TreeSet 为什么底层实现要用红黑树?

TreeSet 是基于有序集合的数据结构,需要支持高效的插入、删除和查找操作,并且要求元素按照自然顺序或者指定顺序排序。红黑树作为一种自平衡二叉搜索树,满足了这些要求,能够保持树的平衡性并且支持对数级别的操作复杂度,因此是 TreeSet 底层实现的理想选择。

有没有可以替代性能更好的结构?

尽管红黑树在大多数情况下能够提供很好的性能,但在特定场景下可能存在更适合的替代结构。例如,当需要高效地执行大量的插入和删除操作时,跳表(Skip List)可能比红黑树更适合,因为跳表的插入和删除操作比较简单,而且可以通过调整层级来平衡整个结构。此外,对于特定范围的整数集合,使用位图或哈希表等数据结构可能更有效率。因此,在选择数据结构时,需要根据具体的需求和场景来权衡选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值