比较器
Comparator是外部比较器,用于比较来对象与对象之间的,两个对象进行比较,多用于集合排序
Comparable可以认为是一个内比较器,根据对象某一属性进行排序的。
1. 使用场景
内置比较器(Comparable)的使用场景:
- 内置比较器适用于对象本身已经具有默认的排序规则,并且这个排序规则是对象的固有属性。
- 当对象实现了
Comparable
接口并重写了compareTo
方法后,可以直接使用内置的排序方法(如Collections.sort
)进行排序。- 内置比较器实现了对象的自然排序,即对象本身具有默认的排序规则。
以下是内置比较器的使用示例场景:
- 对象具有固有的排序属性,例如
String
类的自然排序是按照字典顺序排序。- 对象的排序规则是对象自身的属性,例如
Student
类的compareTo
方法根据学生的分数进行排序。- 使用内置的排序方法进行排序时,无需额外提供比较器。
外置比较器(Comparator)的使用场景:
- 外置比较器适用于对象的排序规则是外部定义的,与对象本身的属性无关。
- 当对象的排序规则不是对象本身的固有属性,或者需要根据不同的比较规则进行排序时,可以使用外置比较器。
- 外置比较器可以根据需要定义多个不同的比较规则,并在需要时传递给排序方法或集合类的构造函数。
以下是外置比较器的使用示例场景:
- 对象的排序规则与对象本身的属性无关,例如按照对象的名称长度进行排序。
- 需要根据不同的比较规则进行排序,例如根据学生的分数、姓名、年龄等属性进行排序。
- 在排序方法或集合类的构造函数中需要传入自定义的比较器。
总结:
- 内置比较器适用于对象本身具有默认的排序规则的情况,无需额外提供比较器。
- 外置比较器适用于对象的排序规则与对象本身的属性无关,或者需要根据不同的比较规则进行排序的情况,需要提供自定义的比较器。
1. Comparator(外部比较器)
package com.wz.collection02;
public class Student {
private String name;
private int age;
private char sex;
public Student(String name, int age, char sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public char getSex() {
return sex;
}
@Override
public String toString() {
return name + "-" + age +"-" + sex ;
}
}
package com.wz.collection02;
import java.util.ArrayList;
import java.util.Comparator;
public class Collection {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
students.add(new Student("ZhangSan",18,'男'));
students.add(new Student("LiSi",15,'女'));
students.add(new Student("WangWu",22,'女'));
students.add(new Student("ZhaoLiu",28,'男'));
students.add(new Student("SunQi",20,'女'));
// Comparator<Student> comparator = new Comparator<Student>() {
// @Override
// public int compare(Student o1, Student o2) {
// int len1 = o1.getName().length();
// int len2 = o2.getName().length();
// return Integer.compare(len1,len2);
// }
// };
Comparator<Student> comparator = (o1, o2) -> {
int len1 = o1.getName().length();
int len2 = o2.getName().length();
return Integer.compare(len1,len2);
};
students.sort(comparator);
students.forEach(System.out::println);
}
}
结果:
2. Comparable(内置比较器)
自然排序:
package com.wz.collection01;
public class Student implements Comparable<Student>{
private final String name;
private final double score;
public Student(String name, double score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return name + "=>" + score ;
}
@Override
public int compareTo(Student other) {
return Double.compare(score, other.score);
}
}
package com.wz.collection01;
import java.util.*;
public class CollectionDemo {
public static void main(String[] args) {
Random ran = new Random();
List<Student> students = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Student student = new Student("stu" + i, ran.nextInt(100));
System.out.println(student);
students.add(student);
}
System.out.println("-----------------");
//对集合进行排序,这里没有提供外排序器,因此要求集合中的元素必须实现自然排序接口
Collections.sort(students);
students.forEach(System.out::println);
}
}
结果:
这段代码展示了如何使用
Comparable
接口来实现对象的自然排序。在这个示例中,Student
类实现了Comparable<Student>
接口,并重写了compareTo
方法来定义学生对象之间的比较规则。在
CollectionDemo
类的main
方法中,通过循环创建了 10 个学生对象,并将它们添加到students
列表中。然后,使用Collections.sort
方法对students
列表进行排序。由于Student
类实现了Comparable
接口,因此可以直接调用sort
方法进行排序。在
compareTo
方法中,我们使用Double.compare
方法来比较学生对象的分数。这样,排序结果将按照学生分数的升序进行排列。最后,通过使用
forEach
方法遍历排序后的students
列表,并使用方法引用System.out::println
打印每个学生对象。注意:
Comparable
接口用于实现对象的自然排序,即对象本身具有默认的排序规则。如果需要根据不同的比较规则进行排序,可以使用比较器(Comparator)来实现。
TreeMap
1. TreeMap中比较器使用
TreeMap
会自动对键进行排序。它是基于红黑树(Red-Black Tree)实现的,它通过对键进行自然顺序或自定义比较器的比较来保持键的有序状态。
TreeMap
不是线程安全的,key不可以存入null。
在
TreeMap
中,键是唯一的,并且按照它们的排序顺序进行存储。默认情况下,TreeMap
使用键的自然顺序进行排序。例如,对于TreeMap
,键按照字符串的字典顺序进行排序。
package com.wz.collection03;
import java.util.TreeMap;
public class TreeMapTest {
public static void main(String[] args) {
TreeMap<String, Integer> map = new TreeMap<>();
map.put("aa", 1);
map.put("a", 2);
map.put("aaa", 3);
map.put("ba", 5);
map.put("bc", 4);
map.put("ab", 3);
map.put("bb", 2);
map.forEach((k,v) -> System.out.println(k + "=>" + v));
}
}
结果:
TreeMap中使用外排序器
package com.wz.collection04;
import java.util.Comparator;
import java.util.TreeMap;
public class TreeMapTest {
public static void main(String[] args) {
Comparator<Student> comparator = (o1, o2) -> {
int len1 = o1.getName().length();
int len2 = o2.getName().length();
return Integer.compare(len1,len2);
};
TreeMap<Student, String> map = new TreeMap<>(comparator);
map.put(new Student("ZhangSan",18,'男'),"202312");
map.put(new Student("LiSi",15,'女'),"202311");
map.put(new Student("WangWu",22,'女'),"202301");
map.put(new Student("ZhaoLiu",28,'男'),"202321");
map.put(new Student("SunQi",20,'女'),"202305");
map.forEach((k,v)-> System.out.println(k+"=>"+v));
}
}
结果:
2. TreeMap底层
- 成员变量
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
//用于比较键的比较器。它可以是用户提供的自定义比较器,或者如果键类型实现了 Comparable 接口,则使用键类型自身的自然顺序比较器。
private final Comparator<? super K> comparator;
//红黑树的根节点,每个节点是一个Entry
private transient Entry<K,V> root;
//TreeMap 中键值对的数量
private transient int size = 0;
//对 TreeMap 进行结构修改的次数
private transient int modCount = 0;
}
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;//节点的键
V value;//节点的值
Entry<K,V> left;//指向左子节点的引用。
Entry<K,V> right;//指向右子节点的引用。
Entry<K,V> parent;//指向父节点的引用。
boolean color = BLACK;//表示节点的颜色,true 表示红色,false 表示黑色
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
//返回节点的键。
public K getKey() {
return key;
}
//返回与节点键关联的值。
public V getValue() {
return value;
}
//替换当前与节点键关联的值,并返回替换前的值。
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
//判断节点是否与给定的对象相等。
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
//返回节点的哈希码。
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
public String toString() {
return key + "=" + value;
}
}
- put()
- 首先判断树是否为空树,若为空树,则直接创建根节点并返回。
- 若树不为空,则从根节点开始,沿着树的路径,根据键的比较结果向左或向右查找插入位置。
- 如果找到了相同键值的节点,则替换节点的值并返回旧值。
- 如果找到了合适的插入位置,则创建一个新的节点,将其插入到树中。
- 插入节点后,需要进行相关的调整操作,以保持二叉搜索树的性质。
- 最后更新树的大小和修改计数器,然后返回 null。
put
方法的时间复杂度为 O(log n),其中 n 是树中节点的数量。这是由于它需要沿着树的高度进行搜索和插入操作
public V put(K key, V value) {
//获取根节点 t。
Entry<K,V> t = root;
//如果根节点为空,表示树为空,直接创建新的节点作为根节点,并将键值对存入,然后更新树的大小和修改计数,并返回 null。
if (t == null) {
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
//如果根节点不为空,根据是否存在比较器来选择使用比较器进行比较还是使用键的自然顺序进行比较。
int cmp;
Entry<K,V> parent;
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 接口,并根据键的自然顺序来定位节点的插入位置
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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);
}
//如果未找到与要插入键相同的节点,创建新的节点 e,将其插入到正确的位置上。根据比较结果,如果要插入的键小于当前节点的键,将新节点作为当前节点的左子节点,否则作为右子节点。
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//插入节点后,调用 fixAfterInsertion 方法进行红黑树的平衡操作。
fixAfterInsertion(e);
//更新树的大小和修改计数,并返回 null。
size++;
modCount++;
return null;
}
- get()
在 TreeMap 类中,get(Object key) 方法用于获取指定键对应的值。
- 首先,判断传入的 key 是否为 null。如果是 null,则调用 getEntryForNullKey 方法来获取与 null 键关联的节点。
- 如果 key 不为 null,则调用 getEntry 方法来获取与给定键关联的节点。该方法会先根据比较器或键的自然顺序定位到与给定键等值或最接近的节点,然后再根据键的比较结果进一步判断要查找的节点在左子树还是右子树中。
- 如果找到了对应的节点,则返回该节点的值;如果没有找到,则返回 null,表示未找到匹配的键。
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
//判断是否存在比较器comparator
if (comparator != null)
//如果存在比较器,则调用 getEntryUsingComparator 方法使用比较器进行键的查找。
return getEntryUsingComparator(key);
//如果不存在比较器,则要求键实现 Comparable 接口,并将给定的键强制转换为 Comparable 类型(k)。
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
//获取根节点(root)作为起始节点(p)。
Entry<K,V> p = root;
//进入循环,将当前节点(p)与给定键进行比较。
while (p != null) {
int cmp = k.compareTo(p.key);
//如果给定键小于当前节点的键,说明要查找的键位于当前节点的左子树,将当前节点的左子节点设为当前节点(p = p.left)。
if (cmp < 0)
p = p.left;
//如果给定键大于当前节点的键,说明要查找的键位于当前节点的右子树,将当前节点的右子节点设为当前节点(p = p.right)。
else if (cmp > 0)
p = p.right;
else
//如果给定键与当前节点的键相等,找到了对应的节点,直接返回该节点(return p)。
return p;
}
//如果当前节点(p)为空(不存在对应键的节点),则返回 null。
return null;
}
- remove()
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
//元素个数减一
size--;
// 如果被删除的节点p的左孩子和右孩子都不为空,则查找其替代节
if (p.left != null && p. right != null) {
// 查找p的替代节点
Entry<K,V> s = successor (p);
p. key = s.key ;
p. value = s.value ;
p = s;
}
Entry<K,V> replacement = (p. left != null ? p.left : p. right);
if (replacement != null) {
// 将p的父节点拷贝给替代节点
replacement. parent = p.parent ;
// 如果替代节点p的父节点为空,也就是p为跟节点,则将replacement设置为根节点
if (p.parent == null)
root = replacement;
// 如果替代节点p是其父节点的左孩子,则将replacement设置为其父节点的左孩子
else if (p == p.parent. left)
p. parent.left = replacement;
// 如果替代节点p是其父节点的左孩子,则将replacement设置为其父节点的右孩子
else
p. parent.right = replacement;
// 将替代节点p的left、right、parent的指针都指向空
p. left = p.right = p.parent = null;
// 如果替代节点p的颜色是黑色,则需要调整红黑树以保持其平衡
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
// 如果要替代节点p没有父节点,代表p为根节点,直接删除即可
root = null;
} else {
// 如果p的颜色是黑色,则调整红黑树
if (p.color == BLACK)
fixAfterDeletion(p);
// 下面删除替代节点p
if (p.parent != null) {
// 解除p的父节点对p的引用
if (p == p.parent .left)
p. parent.left = null;
else if (p == p.parent. right)
p. parent.right = null;
// 解除p对p父节点的引用
p. parent = null;
}
}
}
LinkedHashMap
LinkedHashMap是Java集合框架中的一个类,它继承了HashMap并且使用链表维护元素的插入顺序或者访问顺序。与HashMap不同的是,它可以按照元素插入的顺序或者访问的顺序来遍历元素。LinkedHashMap中的元素是按照它们被插入到映射中的顺序存储的,因此它也可以保持元素的插入顺序不变。
Set
Java中的Set接口是一种集合接口,它继承自Collection接口,表示一组不重复的元素。Set接口的特点是不允许包含重复元素,每个元素在Set中只能出现一次。
Set接口的常见实现类有:
- HashSet:基于哈希表的实现,不保证元素的顺序,允许使用null元素。
- TreeSet:基于红黑树的实现,按照元素的自然顺序或者自定义的比较器进行排序,不允许使用null元素。
- LinkedHashSet:基于哈希表和链表的实现,维护元素的插入顺序,允许使用null元素。
Set接口定义了一系列方法,包括:
- 添加元素:add(element)
- 删除元素:remove(element)
- 判断是否包含某个元素:contains(element)
- 获取Set的大小:size()
- 清空Set:clear()
- 遍历Set:使用迭代器(iterator)或者增强for循环进行遍历
import java.util.HashSet;
import java.util.Set;
public class SetExample {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
// 添加元素
set.add("apple");
set.add("banana");
set.add("orange");
// 判断是否包含某个元素
boolean containsApple = set.contains("apple");
System.out.println("Contains apple: " + containsApple);
// 删除元素
set.remove("banana");
// 获取Set的大小
int size = set.size();
System.out.println("Set size: " + size);
// 遍历Set
for (String element : set) {
System.out.println(element);
}
}
}
1. HashSet底层
HashSet是Java中的一个集合类,它实现了Set接口,并且不允许集合中存在重复元素。HashSet的底层实际上是基于HashMap实现的。
HashSet使用一个HashMap实例来存储和管理集合元素。对于HashSet中的每个元素,实际上是作为HashMap的键(key)存储的,而对应的值(value)被设置为一个常量PRESENT。HashMap的键(key)具有唯一性,因此HashSet中不允许有重复元素。由于HashSet使用了HashMap来存储元素,因此HashSet具有快速的插入、删除和查找操作。
HashSet的底层实现原理如下:
- 创建一个HashMap实例作为HashSet的底层数据结构。
- 当调用HashSet的add方法添加元素时,实际上是将元素作为HashMap的键(key)添加到HashMap中,并将对应的值(value)设置为常量PRESENT。
- 当调用HashSet的contains方法判断元素是否存在时,实际上是对HashMap的containsKey方法进行调用来判断是否存在对应的键。
- 当调用HashSet的remove方法删除元素时,实际上是将元素作为HashMap的键(key)进行删除操作。
- HashSet还利用了HashMap的哈希表特性,对元素进行散列存储,可以提高查找效率。
注意:
由于HashSet是基于HashMap实现的,因此HashSet并不能保证元素的顺序。元素在HashSet中的顺序是由元素的哈希值决定的,而非添加的顺序。如果需要按照顺序存储元素,可以考虑使用LinkedHashSet,它使用了链表来维护元素的插入顺序。
总结来说,HashSet的底层实现是利用HashMap来存储和管理元素,并且HashSet中的元素是无序且不允许重复的
用法:
package com.wz.collection05;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetTest {
public static void main(String[] args) {
//HashSet底层采用的是HashMap来存储元素,因为HashMap的key是唯一的,
//因此,HashSet存储的元素也是唯一的。因为HashMap存储元素是无序的,因此
//HashSet存储的元素也是无序的
Set<String> set = new HashSet<>();
System.out.println(set.add("a"));//true
System.out.println(set.add("a"));//false
System.out.println(set.add("b"));//true
System.out.println(set.add("c"));//true
System.out.println(set.size());//3
for(String str : set){
System.out.println(str);
}
for(Iterator<String> iter = set.iterator(); iter.hasNext();){
String next = iter.next();
System.out.println(next);
}
set.forEach(System.out::println);
System.out.println(set.remove("a"));//true
System.out.println(set.remove("a"));//false
}
}
2. TreeSet
TreeSet是Java中的一个集合类,它实现了SortedSet接口,并且基于红黑树(Red-Black Tree)数据结构进行存储和管理集合元素。与HashSet不同的是,TreeSet中的元素是有序的,根据元素的自然排序或自定义比较器进行排序。
TreeSet的底层实现原理如下:
1.创建一个红黑树(Red-Black Tree)作为TreeSet的底层数据结构。
2.当调用TreeSet的add方法添加元素时,新元素将按照特定的排序规则插入到红黑树中。
3.当调用TreeSet的contains方法判断元素是否存在时,红黑树会根据排序规则进行查找。
4.当调用TreeSet的remove方法删除元素时,红黑树会对相应的节点进行删除操作。
由于TreeSet使用红黑树作为底层数据结构,它具有以下特性:
元素是有序的,按照自然排序或自定义比较器排序。
插入、删除、查找元素的时间复杂度为O(logN),其中N是集合中元素的数量。
TreeSet不允许插入空值。
TreeSet是非线程安全的,如果需要在多线程环境中使用,可以使用Collections类的synchronizedSortedSet方法或ConcurrentSkipListSet类。
需要注意的是,由于TreeSet使用红黑树作为底层数据结构,对于包含大量元素的集合,TreeSet的性能可能比HashSet稍低。因此,在选择使用HashSet还是TreeSet时,需要根据具体的需求和性能要求进行权衡。
3. LinkedHashSet
LinkedHashMap是Java集合框架中的一个实现类,它继承自HashMap类,并且在HashMap的基础上增加了按照插入顺序或者访问顺序进行迭代的功能。
与HashMap不同,LinkedHashMap在内部使用了一个双向链表来维护元素的顺序。每个元素包含一个前驱指针和一个后继指针,这样就可以按照插入顺序或者访问顺序(最近访问的元素放在最后)来迭代元素。
LinkedHashMap的主要特点包括:
保持插入顺序或访问顺序:LinkedHashMap可以按照元素的插入顺序或者最近访问的顺序进行迭代。通过构造函数可以选择按照插入顺序或者访问顺序进行迭代。
继承自HashMap:LinkedHashMap继承了HashMap的大部分功能,包括高效的查找、插入和删除操作。它使用哈希表来存储元素,具有快速的查找性能。
非线程安全:LinkedHashMap是非线程安全的,如果需要在多线程环境下使用,可以考虑使用ConcurrentLinkedHashMap或者使用Collections工具类的synchronizedMap方法包装LinkedHashMap。
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapExample {
public static void main(String[] args) {
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
// 添加元素
linkedHashMap.put("apple", 1);
linkedHashMap.put("banana", 2);
linkedHashMap.put("orange", 3);
// 遍历LinkedHashMap
for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + ": " + value);
}
}
}
创建了一个LinkedHashMap对象,并向其中添加了三个键值对。然后使用entrySet方法获取LinkedHashMap中的所有键值对,并通过迭代器遍历输出每个键值对的内容。由于LinkedHashMap会保持插入顺序,所以输出的结果将按照元素的插入顺序进行展示。