Collection 接口
Collection
接口提供了一系列用于操作和管理集合的方法,包括添加、删除、查询、遍历等。它是所有集合类的根接口,包括 List
、Set
、Queue
等。
Collection 接口常见方法
-
add(E element)
:向集合中添加元素。 -
addAll(Collection col)
:将 col 中的所有元素添加到集合中 -
boolean remove(Object obj)
:通过元素的equals方法判断是否是要删除的那个元素,只删除找到的第一个元素 -
boolean removeAll(Collection col)
:取两集合差集 -
boolean retain(Collection col)
:把交集的结果存在当前的集合中,不影响col -
boolean contains(Object obj)
:判断集合中是否包含指定的元素。 -
boolean containsAll(Collection col)
:调用元素的equals方法来比较的。用两个两个集合的元素逐一比较 -
size()
:返回集合中的元素个数。 -
isEmpty()
:判断集合是否为空。 -
clear()
:清空集合中的所有元素。 -
iterator()
:返回用于遍历集合的迭代器。 -
hashCode()
: 获取集合对象的哈希值 -
Object[] toArray()
:转换成对象数组
Set 接口
-
Set
接口是Collection
的子接口,Set集合中的元素是无序,同时不可重复的,可以存放null元素 -
Set
接口的实现类有HashSet
、treeSet
、LinkedHashSet
Set
接口是一个不包含重复元素的集合。Set
接口的主要特点是:
-
无序性:
Set
中的元素是没有顺序的,除非使用特定的实现类(如LinkedHashSet
)。无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。 -
不可重复性:
Set
不允许重复的元素。指添加、重写的元素按照equals()
判断时,返回 false,需要同时重写equals()
方法和hashCode()
方法。
Set 接口常用方法
Set 常见实现类
Set 接口在 Java 中有多种实现类,其中最常用的是 HashSet
、LinkedHashSet
和 TreeSet
。Set 的三个实现类都线程不安全的
HashSet
HashSet
是 Java 中实现 Set
接口的一个常用类。它基于哈希表实现,不允许包含重复元素,并且不保留元素的插入顺序。HashSet
允许存储 null 元素。
HashSet主要特点
-
不允许重复元素:
HashSet
内部使用哈希表来存储元素,利用哈希值来快速定位元素位置,因此不会存储重复的元素。 -
无序集合:
HashSet
不保持元素的插入顺序,即无法按照添加顺序或元素值的顺序访问元素。 -
允许存储 null 元素:
HashSet
可以存储 null 元素,但只能存储一个 null,因为重复元素不被允许。
HashSet 四种构造方法
HashSet()
:构建一个空的 HashSet 对象,其初始容量为默认值 16 ,负载因子为默认值 0.75
Set<String> set = new HashSet<>();
public HashSet(){
map = new HashMap<>();
}
HashSet
的构造方法中可以看出,底层实际是实现了HashMap
-
HashSet(Collection <? extends E> col)
:创建一个包含指定集合 col中的元素的新 HashSet 对象。将其初始化为给定集合中的元素。 -
HashSet(int initCapacity)
:创建一个空的HashSet
对象,同时指定初始容量initCapacity
,负载因子为默认值 0.75 -
HashSet(int initCapacity,float loadFactor)
:创建一个空的HashSet
对象,指定初始容量initCapacity
和负载因子loadFactor
负载因子:比如说当前的容器容量是 16,负载因子是 0.75, 16\*0.75 = 12
,也就是说,当容量达到了12的时候就会进行扩容操作。简单来说相当于扩容机制的一个阈值,当超过这个阈值的时候就会触发扩容。
HashSet 使用示例
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
// 创建一个 HashSet
Set<String> names = new HashSet<>();
// 添加元素
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 尝试添加重复元素
names.add("Alice"); // 不会被添加,因为不允许重复元素
// 检查集合是否包含指定元素
System.out.println(names.contains("Alice")); // 输出: true
// 遍历集合中的所有元素
for (String name : names) {
System.out.println(name);
}
// 移除元素
names.remove("Bob");
System.out.println(names); // 输出: [Alice, Charlie] (顺序不确定)
// 清空集合
names.clear();
System.out.println(names.isEmpty()); // 输出: true
}
}
HashSet 扩容机制
resize() 数组扩容方法:
-
判断 table 表是否为null,如果为null,则为table表进行容量开辟
newCap = DEFAULT_INITIAL_CAPACITY;
默认的初始值为 DEFAULT_INITIAL_CAPACITY(16);newThr = (int)(DEFAULT_LOAD_FACTOR (0.75)* DEFAULT_INITIAL_CAPACITY);
扩容阈值为:newThr= 16 * 0.75 = 12;当集合容量达到12时再次调用 resize() 方法进行扩容
-
第二步:当进行第二次扩容,以及之后每一次扩容的时候,每次到达扩容阈值的时候,容量扩容到原先的两倍
newCap = oldCap << 1
,新的扩容阈值为:newThr = newCap * 0.75 = 24,以此类推(12,24,36,48.....)
HashSet 添加数据底层源码
添加元素,调用 map.put()
方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
首先进行添加元素时,要先通过计算其 hash 值来确认要添加到数组位置索引
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);
}
- 计算出 key(传进来的元素)的 hashCode 值
- 将计算出的 hashCode 值再无符号右移16位得到最终的hash值
putVal()
方法的源码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent ,boolean evict) {
//这里定义的都是一些辅助变量
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
- 第一次添加元素时先判断 table 表是否为 null,
//如果为null将通过resize()方法扩容给table赋初始容量(16)
//接下来每一次都是当集合容量达到扩容阈值时调用resize()方法进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
- 每一次向集合中添加元素的时候,会调用该元素的 hashCode() 方法得到一个地址值,接着将得到的地址值放进 tab 数组中进行查询,若当前位置为 null 直接将元素添加到当前位置。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
- 如果当前位置已经存放元素,那么会先判断当前传进来的对象和已有对象是否是同一对象,或者调用equals方法进行比较,如果满足其一,新的元素将会覆盖原先对象的值
else {
Node<K,V> e;
K k;
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);
//经过比较当前传入元素与当前元素所处tab数组位置处的元素不是同一对象,
//则与当前位置对象next所以指的对象一一比较
//如果p.next==null就直接将当前元素添加去。
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
- 判断当前集合容量是否达到扩容阈值,若果到达扩容阈值就先进行扩容,然后再将元素添加进去, 反之直接添加即可。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
LinkedHashSet
LinkedHashSet
是 Java 中的一个实现了 Set 接口的类,它是 HashSet 的子类。与 HashSet
不同,LinkedHashSet
保留了元素的插入顺序,因此可以按照插入顺序迭代访问元素。它基于 HashTable
实现,同时使用链表来维护元素的插入顺序。
LinkedHashSet 主要特点
-
不允许重复元素:与
HashSet
一样,在LinkedHashSet
中不能存储重复的元素。 -
有序集合:
LinkedHashSet
保留了元素插入的顺序,因此迭代遍历LinkedHashSet
可以按照插入顺序访问元素。 -
允许存储 null 元素:
LinkedHashSet
可以存储一个 null 元素,但只能存储一个 null,重复的 null 元素不被允许
LinkedHashSet 的四种构造方法
LinkedHashSet()
:创建一个具有默认容量(16),负载因子(0.75)的新的空连接散列集。
Set<String> set = new LinkedList<>();
-
LinkedHashSet(Collection <? extends E> col)
:创建一个包含指定集合 col中的元素的新 LinkedHashSet 对象。将其初始化为给定集合中的元素。 -
LinkedHashSet(int initCapacity)
:创建一个空的 LinkedHashSet 对象,同时指定初始容量 initCapacity,负载因子为默认值 0.75 -
LinkedHashSet(int initCapacity, float loadFactor)
:创建一个空的LinkedHashSet
对象,指定初始容量 initCapacity 和负载因子 loadFactor
LinkedHashSet 使用示例
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetExample {
public static void main(String[] args) {
// 创建一个 LinkedHashSet
Set<String> names = new LinkedHashSet<>();
// 添加元素
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 尝试添加重复元素
names.add("Alice"); // 不会被添加,因为不允许重复元素
// 检查集合是否包含指定元素
System.out.println(names.contains("Alice")); // 输出: true
// 遍历集合中的所有元素
for (String name : names) {
System.out.println(name);
}
// 移除元素
names.remove("Bob");
System.out.println(names); // 输出: [Alice, Charlie] (按插入顺序)
// 清空集合
names.clear();
System.out.println(names.isEmpty()); // 输出: true
}
}
LinkedHashSet 底层机制
-
底层扩容机制与
HashSet
扩容机制相同 -
LinkedHashSet
底层是一个LinkedHashMap
,底层维护了一个数组+双向链表
它根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序, 其遍历顺序和插入顺序一致。 -
LinkedHashSet
中有 head 和 tail, 分别指向链表的头和尾。每一个节点有before 和 after 属性在添加一个元素时,先求 hashCode 值,再求索引,确定该元素在table表中的位置,然后将添加的元素加入到双向链表(如果该元素已经存在,则不添加)
p.next= newElement;
newElement.pre = p;
添加元素底层源码
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
LinkedHashSet
的底层大多实现原理与 HashSet
相同,同时实现了 LinkedHashMap
table[]
数组的类型为 HashMap $Node []
,且数组里每一个结点的类型为 LinkedHashMap $Entry
当传进元素时,会先将元素创建为 Node<K,V> ,然后将Node<K,V>里的K-V封装到数据类型为 Entry<k,v>
的 entrySet<Entry<k,v>>
集合中去
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
// LinkedHashMap 的静态内部类 Entry 继承自 HashMap 的静态内部类 Node
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//底层Node是没有任何直接遍历方法,因此会将Node<k,v>实现Entry<k,v>接口,
//通过Entry<k,v>里的getKey()和getValue()方法来获取元素
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
TreeSet
TreeSet 是 Java 中的一个实现了 SortedSet
接口的类,它基于红黑树(Red-Black Tree) 的数据结构实现。与 HashSet 和 LinkedHashSet 不同,TreeSet 是有序的集合,可以保持元素的自然排序(例如,数字按升序,字符串按字典序)
SortedSet
接口是 Java 集合框架中的一种有序集合,它继承自Set
接口,并添加了一些与集合元素排序相关的方法。SortedSet
保证集合中的元素按照特定的顺序排列,并且不允许出现重复的元素。
SortedSet
接口的常用实现类是TreeSet
,它基于红黑树数据结构实现了有序集合,可以自动对元素进行排序。
TreeSet 相较于 HashSet 性能较差
TreeSet 主要特点
-
不允许重复元素:与其他
Set
实现类一样,TreeSet
中不能存储重复的元素。 -
有序集合:
TreeSet
会根据元素的自然顺序进行排序。如果元素不具备自然顺序,则需要在创建TreeSet
时提供一个Comparator
对象来指定排序规则。 -
支持高效的查找和遍历:由于
TreeSet
内部使用红黑树,它的插入、删除和查找操作的时间复杂度都是 O(log n) -
不是线程安全的
-
JDK8 以后,集合中的元素不可以是 null(如果为空,则会抛出异常
java.lang.NullPointerException
)
TreeSet 的四种构造方法
TreeSet()
:创建一个空的TreeSet
,它按照元素的自然顺序进行排序。
Set<Stirng> set = new TreeSet<>();
TreeSet<String> set = new TreeSet<>();
TreeSet(Comparator<? super E> comparator)
:创建一个空的TreeSet
,并使用指定的比较器对元素进行排序。比较器可以自定义,用于指定元素的排序规则。
TreeSet<String> set = new TreeSet<>(new MyComparator());
-
TreeSet(Collection <? extends E> col)
:创建一个 TreeSet,并将指定集合 col 中的元素添加到 TreeSet 中。元素将按照自然顺序进行排序。 -
TreeSet(Sorted <E> sortedSet)
:创建一个 TreeSet,并使用指定排序集合的比较器对元素进行排序。这样可以将一个已经排序好的集合转换为 TreeSet。
SortedSet<String> sortedSet = new TreeSet<>();
sortedSet.add("apple");
sortedSet.add("banana");
sortedSet.add("orange");
TreeSet<String> treeSet = new TreeSet<>(sortedSet);
在使用自定义对象作为 TreeSet 元素时,需要确保对象实现了 Comparable
接口或传入了合适的比较器。否则可能会抛出 ClassCastException
异常。
TreeSet 使用示例
import java.util.TreeSet;
import java.util.Set;
public class TreeSetExample {
public static void main(String[] args) {
// 创建一个 TreeSet
Set<String> names = new TreeSet<>();
// 添加元素
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 尝试添加重复元素
names.add("Alice"); // 不会被添加,因为不允许重复元素
// 检查集合是否包含指定元素
System.out.println(names.contains("Alice")); // 输出: true
// 遍历集合中的所有元素
for (String name : names) {
System.out.println(name);
}
// 移除元素
names.remove("Bob");
System.out.println(names); // 输出: [Alice, Charlie] (按字母顺序)
// 清空集合
names.clear();
System.out.println(names.isEmpty()); // 输出: true
}
}
TreeSet 底层实现机制
-
TreeSet
底层使用的是红黑树实现,对于元素之间排序,如果不指定自定义的外部比较器 ——Comparator
,那么插入的对象必须实现内部比较器——Comparable
接口,元素按照实现此接口的compareTo()
方法去排序。 -
TreeSet
判断两个对象不相等的方式:两个对象通过equals
方法返回false,或者通过CompareTo
方法比较没有返回0 -
在不使用默认排序的情况下,可以重写 compare() 方法来实现自定义排序
compare() 底层源码
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String)o2);
}
// String、Integer等类均已实现Comparable接口,无需另外实现
compareTo()方法
compareTo
方法定义在 Comparable
接口中,用于比较一个对象与另一个对象的顺序。
int compareTo(T object)
@Override
public int compareTo(Person o) {
if (this.age > o.getAge()) {
return 1;
}
if (this.age < o.getAge()) {
return -1;
}
return 0;
}
- 如果返回值 < 0,则表示当前对象小于与其比较的对象。
- 如果返回值 == 0,则表示当前对象等于与其比较的对象。
- 如果返回值 > 0,则表示当前对象大于与其比较的对象
在实际使用中,当需要对自定义对象进行排序时,通常实现 Comparable
接口,并在其中重写 compareTo
方法。该方法的具体实现根据业务需求来决定。
添加元素
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
- 创建一个
Entry<K,V>
类型的 root(根)结点,之后每次添加的子结点类型都为 Entry<K,V>
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
- 每次添加元素的时候,都会调用
compare()
方法判断当前添加的元素与集合中已有元素是否为同一元素
如果不是则直接添加,同时根据compare()
方法返回值来判断添加的位置
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
-
传进来的子节点先与根结点进行判断:
- 如果大于根结点,则让结点与根结点的子结点进行比较
- 如果传入元素小于任意子结点的左右结点其中一个结点,则让该结点作为该元素的双亲结点
-
传入元素与双亲结点进行比较,如果大于双亲结点添加到右子树,如果小于双亲结点,则添加到左子树,否则直接返回值
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//将传进来的值封装成Entry<K,V>类型,然后根据判断放进双亲结点的子节点中
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
Comparable 和 Comparator的区别
Comparable
接口和 Comparator
接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:
-
Comparable 接口出自 java.lang 包 它有一个
compareTo(Object obj)
方法用来排序 -
Comparator 接口 出自 java.util 包它有一个
compare(Object obj1, Object obj2)
方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()
方法或compare()
方法,当我们需要对某一个集合实现两种排序方式:
比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,可以:
- 重写
compareTo()
方法 - 使用自制的
Comparator
方法 - 以两个
Comparator
来实现歌名排序和歌星名排序,使用两个参数版的Collections.sort()
HashSet、LinkedHashSet 和 TreeSet 三者的异同
-
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。 -
HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同:-
HashSet
的底层数据结构是哈希表(基于HashMap
实现)。 -
LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。 -
TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
-
-
底层数据结构不同又导致这三者的应用场景不同:
-
HashSet
用于不需要保证元素插入和取出顺序的场景 -
LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景 -
TreeSet
用于支持对元素自定义排序规则的场景。
-
总结
Set
接口是 Java 集合框架中的一个重要接口,它提供了存储唯一元素的能力。通过不同的实现类,可以满足不同的需求场景。在使用 Set
接口时,需要注意元素的唯一性是基于 hashCode()
和 equals()
方法的,因此必须确保这两个方法被正确实现。同时,也需要根据实际需求选择合适的实现类。