知识点
1. 集合框架的基本概念
- Collection接口:了解
Collection
接口的基本方法和常用实现类。 - Collections类:熟悉
Collections
类中的静态方法,如排序、搜索、同步、不可变集合等操作。
2. List接口和实现类
- ArrayList:了解其内部实现、动态数组的扩容机制、随机访问性能优良、插入和删除操作性能较差。
- LinkedList:理解其基于双向链表的实现、插入和删除操作性能优良、随机访问性能较差。
- Vector:掌握其线程安全特性,了解与
ArrayList
的区别。
3. Set接口和实现类
- HashSet:理解基于哈希表的实现、无序性和不允许重复元素的特性、负载因子和初始容量的影响。
- LinkedHashSet:熟悉其在
HashSet
基础上维护插入顺序的特性。 - TreeSet:了解基于红黑树的实现、排序特性、对元素必须实现
Comparable
接口或提供Comparator
。
面试题
Collection接口的常用方法和实现类
在 Java 中,Collection
是所有集合框架接口的根接口。它定义了一组方法,用于操作一组对象,这些对象称为其元素。Collection
接口本身并没有直接的实现类,但是它的子接口,如 List
、Set
、Queue
等,有很多常见的实现类。
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)指的是在创建后不能被修改的集合。这意味着一旦不可变集合被创建,您不能添加、删除或更改其中的任何元素。不可变集合在一些场景下非常有用,例如:
- 线程安全:因为不可变集合不能被修改,它们天然是线程安全的。多个线程可以并发地读取同一个不可变集合而不需要同步。
- 防止意外修改:不可变集合可以防止集合被意外修改,确保数据的稳定性和一致性。
- 简化代码:使用不可变集合可以减少代码中的可变状态,使程序更容易理解和维护。
在 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);
}
}
创建不可变集合的方式
-
使用
Collections.unmodifiableXXX
方法:unmodifiableList(List<? extends T> list)
unmodifiableSet(Set<? extends T> s)
unmodifiableMap(Map<? extends K, ? extends V> m)
这些方法返回一个不可变视图,这意味着底层集合仍然可以修改,但通过不可变视图进行修改会抛出
UnsupportedOperationException
。 -
使用 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'}]
不可变集成元素不可变注意
-
不可变集合并不保证集合元素的不可变性:如果集合中的元素是可变对象,那么你仍然可以修改这些对象的属性。要确保完全不可变性,需要保证集合中的元素也是不可变的对象。
-
防止修改集合中的对象:如果需要防止修改集合中的对象,可以使用不可变对象(例如,使用
final
关键字修饰类和字段)或在创建不可变集合时,确保其中的对象是不可变的。 -
不可变对象的优势:在多线程环境下,不可变对象更容易确保线程安全,因为它们的状态不会改变。结合不可变集合和不可变对象,可以实现更健壮的并发程序。
ArrayList
ArrayList 是 Java 集合框架中的一个常用类,提供了一个动态数组实现。以下是对其内部实现、特点、性能和使用场景的详细介绍:
内部实现
ArrayList
是基于数组实现的。它内部维护了一个动态的数组,当需要添加更多元素时,数组会进行自动扩容。
动态数组的扩容机制
ArrayList
的初始容量可以在创建时指定。如果没有指定,则使用默认容量。当 ArrayList
超过其容量时,会进行扩容。扩容的过程如下:
- 创建一个新的数组,其容量通常是当前数组的 1.5 倍(或其他增长因子)。
- 将旧数组中的元素复制到新数组中。
- 替换旧数组为新数组。
这种扩容机制使得 ArrayList
能够动态地增长以适应增加的元素,但也引入了在扩容时进行元素复制的性能开销。
性能特点
- 随机访问性能优良:由于
ArrayList
基于数组实现,支持通过索引进行快速的随机访问,时间复杂度为 O(1)。 - 插入和删除操作性能较差:在数组中间进行插入或删除操作时,需要移动数组中的其他元素,时间复杂度为 O(n),其中 n 是数组的长度。因此,对于频繁的插入和删除操作,
ArrayList
的性能较差。
使用场景
ArrayList
适用于以下场景:
- 需要快速随机访问:当需要通过索引快速访问元素时,
ArrayList
是一个理想的选择。 - 读多写少的场景:在插入和删除操作较少,而读取操作较多的场景中,
ArrayList
性能优良。 - 动态数组需求:当数组的大小不确定且需要动态增长时,
ArrayList
是一个方便的选择。
最佳实践
- 初始化容量:在创建
ArrayList
时,尽量指定初始容量,以减少扩容次数,提高性能。List<String> list = new ArrayList<>(100); // 指定初始容量为 100
- 批量操作:如果有大量元素需要添加到
ArrayList
中,尽量使用addAll
方法进行批量添加,而不是逐个添加。List<String> list = new ArrayList<>(); List<String> newElements = Arrays.asList("a", "b", "c"); list.addAll(newElements);
- 尽量减少中间插入和删除操作:如果需要频繁地在中间进行插入和删除操作,考虑使用
LinkedList
或其他合适的数据结构。
需要注意的坑
- 扩容开销:当
ArrayList
进行扩容时,会有一次性的性能开销。如果初始容量估计不足,频繁的扩容会影响性能。 - 线程不安全:
ArrayList
不是线程安全的。在多线程环境下使用ArrayList
需要进行适当的同步,或使用线程安全的集合类如CopyOnWriteArrayList
或Collections.synchronizedList(new ArrayList<>())
。 - 避免
ConcurrentModificationException
:在使用迭代器遍历ArrayList
的同时,不能对ArrayList
进行结构性修改(添加、删除元素),否则会抛出ConcurrentModificationException
。可以使用Iterator
的remove
方法或使用ListIterator
进行安全的遍历和修改。
LinkedList
LinkedList 是 Java 集合框架中的一个常用类,提供了一个基于双向链表的实现。以下是对其内部实现、特点、性能和使用场景的详细介绍:
内部实现
LinkedList
是基于双向链表(Doubly Linked List)实现的。它由一系列节点组成,每个节点包含三个部分:
- 元素值(element):存储节点中的数据。
- 前驱指针(previous):指向前一个节点。
- 后继指针(next):指向后一个节点。
双向链表使得 LinkedList
可以在链表的任何位置进行高效的插入和删除操作。
插入和删除操作性能优良
由于 LinkedList
是基于链表实现的,插入和删除操作非常高效,尤其是在链表的头部或尾部进行操作时:
- 插入操作:在链表的任意位置插入元素时,只需要调整前驱指针和后继指针即可,时间复杂度为 O(1)。
- 删除操作:删除任意位置的元素时,同样只需调整相邻节点的指针,时间复杂度为 O(1)。
但是,如果需要在链表中间位置进行操作,需要先遍历到该位置,遍历的时间复杂度为 O(n)。
随机访问性能较差
由于链表不支持通过索引直接访问元素,必须从头部或尾部开始逐节点遍历:
- 随机访问:访问链表中任意位置的元素时,需要遍历链表,时间复杂度为 O(n)。这使得
LinkedList
的随机访问性能较差。
使用场景
LinkedList
适用于以下场景:
- 频繁的插入和删除操作:当应用程序中有大量的插入和删除操作,特别是在链表的头部或尾部,
LinkedList
是一个理想的选择。 - FIFO(先进先出)或 LIFO(后进先出):
LinkedList
可以作为队列(Queue)或栈(Stack)使用,提供高效的入队、出队、入栈、出栈操作。 - 迭代访问:当主要操作是遍历而不是随机访问时,
LinkedList
是一个合适的选择。
最佳实践
- 避免随机访问:尽量避免在
LinkedList
中进行随机访问操作,因为性能较差。需要随机访问时,考虑使用ArrayList
。 - 使用 ListIterator:当需要在遍历的同时进行插入或删除操作时,使用
ListIterator
提供的接口可以确保操作的安全性和效率。List<String> list = new LinkedList<>(); ListIterator<String> iterator = list.listIterator(); while (iterator.hasNext()) { String element = iterator.next(); if (condition) { iterator.add("newElement"); } }
需要注意的坑
- 随机访问性能:
LinkedList
的随机访问性能较差,因此在需要频繁随机访问的场景下,应避免使用LinkedList
。 - 内存开销:
LinkedList
的每个节点都需要额外的存储空间来保存前驱和后继指针,因此比ArrayList
占用更多的内存。 - 线程不安全:
LinkedList
不是线程安全的,在多线程环境下使用时需要进行同步处理,或使用Collections.synchronizedList(new LinkedList<>())
。
Vector
Vector
是 Java 中一个实现了 List
接口的类,和 ArrayList
类似,都是基于数组的数据结构。它们有许多相似的地方,但 Vector
和 ArrayList
之间也有一些重要的区别,尤其是在线程安全性方面。
Vector 的特点
-
线程安全:
Vector
是线程安全的。它的所有方法都使用了同步(synchronized
)关键字,因此可以在多线程环境中安全使用。- 由于其同步特性,
Vector
的性能比ArrayList
要低,特别是在单线程环境中。
-
扩容机制:
Vector
的默认扩容机制是每次容量不足时,将容量增加一倍。你也可以通过构造函数来指定扩容的增量。ArrayList
的扩容机制则是每次容量不足时,容量增加约 50%。
-
遗留类:
Vector
是 Java 1.0 中引入的遗留类(legacy class),而ArrayList
是 Java 2(JDK 1.2)引入的集合框架的一部分。- 尽管
Vector
仍然可以使用,但在大多数情况下,建议使用ArrayList
代替,除非需要线程安全。
与 ArrayList
的区别
-
线程安全性:
Vector
是线程安全的,因为它的所有公共方法都同步。ArrayList
不是线程安全的。如果在多线程环境中使用ArrayList
,需要手动同步。
-
性能:
- 由于同步开销,
Vector
的性能通常比ArrayList
要低,特别是在单线程环境中。 ArrayList
的性能更好,因为它没有同步开销。
- 由于同步开销,
-
扩容策略:
Vector
每次扩容时容量增加一倍。ArrayList
每次扩容时容量增加 50%。
-
API 差异:
Vector
提供了一些ArrayList
没有的方法,比如addElement
、elementAt
和firstElement
等。这些方法主要是为了兼容旧版的 API。
示例代码
下面是一个简单的示例,展示了 Vector
和 ArrayList
的基本使用:
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
接口的类,基于哈希表实现。它具有以下几个重要的特点:
-
基于哈希表的实现:
HashSet
内部使用HashMap
来存储元素。每个元素作为HashMap
的键,HashMap
的值则是一个固定的对象。- 由于哈希表的特性,
HashSet
提供了良好的增删查性能,平均时间复杂度为 O(1)。
-
无序性:
HashSet
中的元素是无序的。它不能保证元素的顺序,这与插入顺序无关。
-
不允许重复元素:
HashSet
不允许存储重复的元素。如果尝试添加一个已经存在的元素,add
方法会返回false
。
-
负载因子和初始容量:
- 初始容量:
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);
}
}
影响性能的因素
-
初始容量:
- 如果可以预估
HashSet
将要存储的元素数量,设置合适的初始容量可以减少扩容次数,提高性能。
- 如果可以预估
-
负载因子:
- 负载因子的设置影响哈希表的密度,从而影响查找和存储的效率。一般情况下,默认的 0.75 是一个较为折中的选择。
- 如果频繁进行插入和删除操作,适当调整负载因子可能会提升性能。
使用场景
-
去重:
HashSet
常用于存储不允许重复的元素集合。比如过滤重复的用户ID、商品列表等。
-
快速查找:
- 由于哈希表的高效查找特性,可以用
HashSet
来实现快速查找某个元素是否存在于集合中。
- 由于哈希表的高效查找特性,可以用
注意事项
- 元素的
hashCode
和equals
方法:HashSet
中的元素必须正确实现hashCode
和equals
方法,以确保元素的唯一性和查找的效率。- 如果元素的
hashCode
或equals
实现不当,会导致错误的行为或者性能问题。
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;
}
}
}
- 无序性:
- 如果需要有序集合,应该使用
LinkedHashSet
或TreeSet
,而不是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
:如果键key
为null
,返回哈希值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
:
h = 0x12345678
(哈希码)h >>> 16 = 0x00001234
(高16位右移到低16位)h ^ (h >>> 16) = 0x12345678 ^ 0x00001234 = 0x1234444C
(将高16位和低16位进行异或)
通过这种方式,哈希分布函数将输入哈希码混合为 0x1234444C
,使其具有更好的分布特性,减少冲突的可能性。
LinkedHashSet
LinkedHashSet
是 HashSet
的一个变种,它在 HashSet
的基础上增加了维护元素插入顺序的特性。以下是对 LinkedHashSet
的详细介绍:
LinkedHashSet
的特性
-
有序性:
LinkedHashSet
维护着一个双向链表,用于记录元素的插入顺序。因此,迭代LinkedHashSet
的元素时,返回的顺序与插入顺序一致。
-
无重复元素:
LinkedHashSet
继承自HashSet
,因此不允许包含重复的元素。每个元素在集合中是唯一的。
-
哈希表实现:
LinkedHashSet
内部使用HashMap
来存储元素,但它同时维护了一个链表来保持顺序。
内部工作原理
LinkedHashSet
使用一个内部类LinkedHashMap
来存储元素。LinkedHashMap
是HashMap
的子类,增加了维护插入顺序的功能。- 在
LinkedHashSet
中,每一个元素不仅存储在哈希表中,还在链表中维护它们的顺序。 - 当插入一个元素时,
LinkedHashSet
会在哈希表中添加这个元素,并在链表的尾部追加该元素。 - 当删除一个元素时,
LinkedHashSet
会从哈希表和链表中同时删除该元素。
使用场景
LinkedHashSet
适用于以下场景:
- 需要一个不允许重复元素的集合,并且需要维护元素的插入顺序。
- 希望在迭代集合时,元素的顺序与其插入顺序一致。
- 需要一个有序版本的
HashSet
,但不希望使用TreeSet
(TreeSet
基于红黑树,维护的是元素的自然顺序或指定的比较器顺序,而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
。
LinkedHashSet
是 HashSet
的有序版本,非常适合需要既保证元素唯一性又需要维护插入顺序的场景。
TreeSet
TreeSet
是 Java 集合框架中的一个类,基于 NavigableSet
接口实现,底层采用红黑树(Red-Black Tree)数据结构。它保证元素的排序性和集合的基本操作(如插入、删除、查询)的高效性。
基本特性
- 基于红黑树:
TreeSet
使用红黑树实现,保证了元素的有序性和操作的对数时间复杂度。 - 排序特性:
TreeSet
中的元素按照自然顺序(如果元素实现了Comparable
接口)或指定的顺序(通过Comparator
)进行排序。 - 唯一性:
TreeSet
不允许存储重复的元素。 - 元素要求:存储的元素必须实现
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]
获取第一个和最后一个元素
可以使用 first
和 last
方法获取 TreeSet
中的第一个和最后一个元素。
Integer firstElement = numbers.first(); // 1
Integer lastElement = numbers.last(); // 5
获取小于和大于指定元素的最大和最小元素
可以使用 lower
和 higher
方法获取小于和大于指定元素的最大和最小元素。
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]
这段代码的作用是从字典中获取以特定前缀开头的单词,并按字典顺序返回以该前缀开头的所有单词。具体来说:
- 首先,创建了一个
TreeSet
对象dictionary
,其中包含了一些单词。 - 然后,指定了一个前缀
prefix
。 - 接着,使用
tailSet
方法获取字典中大于或等于给定前缀的子集。 - 最后,使用
headSet
方法获取前缀开头的单词子集,其中不包括大于前缀开头的最小单词。
这样,suggestions
中包含了以 "ch"
开头的所有单词,并按字典顺序排序。
红黑树
红黑树是一种自平衡二叉搜索树,由于节点具有红色和黑色标记而得名。红黑树的节点具有以下性质:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(NIL 节点)都是黑色的。
- 如果一个节点是红色的,则它的两个子节点都是黑色的(即不能有两个连续的红节点)。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑色节点数量相等)。
为什么要标识红节点和黑节点?
红黑树之所以需要标识红节点和黑节点,是为了保证树的平衡性和性质的实现。通过规定节点的颜色,并且保持特定的插入和删除操作,可以保证红黑树的高效的自平衡特性,即树的高度在对数级别内。
红黑树设计的优缺点
优点:
- 平衡性好:红黑树能够自动保持树的平衡,插入、删除和查找的时间复杂度都是对数级别的。
- 高效性:红黑树的基本操作效率高,对于大规模数据的存储和操作很有效。
- 实现简单:相比其他平衡树结构,如 AVL 树,红黑树的实现相对简单。
缺点:
- 相对复杂:红黑树的算法相对于其他数据结构较为复杂,不易理解。
- 不适合频繁插入和删除操作:虽然红黑树在大部分情况下能够保持平衡,但频繁的插入和删除操作可能导致树的不平衡,影响性能。
适用场景
红黑树适用于需要高效地插入、删除和查找操作,并且对平衡性要求较高的场景,例如数据库索引、C++ STL 中的 std::map
、Java 中的 TreeMap
、TreeSet
等。
TreeSet 为什么底层实现要用红黑树?
TreeSet
是基于有序集合的数据结构,需要支持高效的插入、删除和查找操作,并且要求元素按照自然顺序或者指定顺序排序。红黑树作为一种自平衡二叉搜索树,满足了这些要求,能够保持树的平衡性并且支持对数级别的操作复杂度,因此是 TreeSet
底层实现的理想选择。
有没有可以替代性能更好的结构?
尽管红黑树在大多数情况下能够提供很好的性能,但在特定场景下可能存在更适合的替代结构。例如,当需要高效地执行大量的插入和删除操作时,跳表(Skip List)可能比红黑树更适合,因为跳表的插入和删除操作比较简单,而且可以通过调整层级来平衡整个结构。此外,对于特定范围的整数集合,使用位图或哈希表等数据结构可能更有效率。因此,在选择数据结构时,需要根据具体的需求和场景来权衡选择。