集合类随笔(二)
Java中的集合类里面很多方法都有优雅的代码实现,回顾基础,下面我们来探索Java中的
TreeSet
和TreeMap
的实现
集合类大致接口分布如下:
Collection
|- Set
|-HashSet
|-TreeSet
...
|-List
...
TreeSet
关于Set这种类型的集合,作为一个Java程序员,最快想到的可能就是我们常用的HashSet
,而关于TreeSet
,它的特性就在于它的元素是排好序的元素,当然这个排好序
并不是指集合里的元素的顺序和添加元素的顺序一致(即我们常说的Set无序不可重复的无序),而是指TreeSet
集合中的元素是按一定规则排序的
public static void main(String[] args) {
TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(new Student(12,"zhansan"));
treeSet.add(new Student(15,"zhansan"));
treeSet.add(new Student(14,"zhansan"));
treeSet.stream().forEach(System.out::println);
}
class Student implements Comparable<Student>{
int age;
String name;
@Override
public int compareTo(Student o) {
return this.age - o.age;
}
//... 其他的构造方法和toString方法省略
}
运行上面代码,控制台输出结果如下:
Student{age=12, name='zhansan'}
Student{age=14, name='zhansan'}
Student{age=15, name='zhansan'}
因此,我们可以看到,最后Set中的数据已经按照添加进入的对象的年龄排好序
我们往Set中添加的是Student对象,观察Student类的实现,我们可以看到Student类实现了Comparable接口,并且实现了compareTo()
方法,在TreeSet
的add方法中,调用了compareTo()
方法,完成元素的添加操作
当然,在往TreeSet
中添加元素时,所添加元素所属的类可能并没有实现Comparable接口
,我们来试一试,看看会导致啥情况
public static void main(String[] args) {
TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(new Student(12,"zhansan"));
treeSet.add(new Student(15,"zhansan"));
treeSet.add(new Student(14,"zhansan"));
treeSet.stream().forEach(System.out::println);
}
class Student{
int age;
String name;
//... 其他的构造方法和toString方法省略
}
控制台会报以下异常:
Exception in thread "main" java.lang.ClassCastException: com.llm.test.TempTest$Student cannot be cast to java.lang.Comparable
at java.util.TreeMap.compare(TreeMap.java:1294)
at java.util.TreeMap.put(TreeMap.java:538)
at java.util.TreeSet.add(TreeSet.java:255)
at com.llm.test.TempTest.main(TempTest.java:9)
所以,要往TreeSet
中添加自定义的类时,得实现Comparable接口
,这里问题就来了,要往TreeSet
中添加的元素必须实现Comparable接口
吗?
答案是:当然有其他的方法啦
在Java中有两个接口可以用来实现排序,一个是Comparable接口
,另一个当然就是集合类中排序常用的Comparator
接口啦
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// ... 其他默认方法省略
}
那么这个接口怎么使用呢?
我们来看TreeSet
的构造器
可以看到有一个构造器需要传入的参数就是Comparator
接口的实现:
所以,修改上面的代码如下:
public static void main(String[] args) {
TreeSet<Student> treeSet = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
});
treeSet.add(new Student(12,"zhansan"));
treeSet.add(new Student(15,"zhansan"));
treeSet.add(new Student(14,"zhansan"));
treeSet.stream().forEach(System.out::println);
}
代码运行结果和实现Comparable
接口的情况一致,如需要探索,可自行验证
小总结
要往TreeSet
中添加自定义的元素,有两种方法
- 使用自然排序:也就是在定义的类上面实现
Comparable
接口,然后重写compareTo()
方法 - 使用定制排序:也就是实现
Comparator
接口,然后把通过构造器注入到TreeSet
里面
要是自定义的类没有使用这两种措施,就会报ClassCastException
异常
底层原理剖析
上面对
TreeSet
的使用做了简单的介绍,优雅的代码在于它的实现
我们使用如下代码作为探索的入口:
treeSet.add(new Student(12,"zhansan"));
class Student implements Comparable<Student>{
//...
}
TreeSet
的底层使用的是一个TreeMap
,TreeSet
是TreeMap
的key
,然后使用一个默认的值作为value
,和HashMap
相似
public boolean add(E e) {
return m.put(e, PRESENT)==null; // private static final Object PRESENT = new Object();
}
里面使用的变量m是在使用默认构造方法的时候进行赋值的:
public TreeSet() {
this(new TreeMap<E,Object>());
}
//这个NavigableMap是TreeMap的父类
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
所以,TreeSet
的add方法调用的是TreeMap
的put(key,val)
方法,分析TreeSet
的add方法,需要分析TreeMap
的put(…)方法
在分析TreeMap
的put()方法实现原理之前,我们需要了解一个重要的数据结构:红黑树
红黑树是一种平衡二叉查找树:
- 平衡代表左右子树的高度差不大于1
- 二叉查找树的意思就是在普通二叉的的基础上满足一个很重要的特性,就是
左子节点 < 根节点 < 右子节点
有关红黑树的实现可以参考如下地址:
https://baike.baidu.com/item/%E7%BA%A2%E9%BB%91%E6%A0%91/2413209?fr=aladdin
下面就是TreeMap
的put()方法的具体代码,我们再来往下分析
public V put(K key, V value) {
//root是整个树结构的根节点
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // 可以对键是否为空进行检查
//如果检查通过,当前put的键值对设置为根节点
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// comparator是一个成员变量,如果在构造函数中传入了Comparator,这个值就是传入的Comparator,否则值为空
//当然,通过这个值如果不为空,就使用Comparator对象进行比较,反之则将key强制类型转换为Comparable类型
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
//这里调用自定义的Comparator的compare(...)方法,把返回值赋值给cmp变量
cmp = cpr.compare(key, t.key);
//下面进行循环找到key的位置
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
//设置节点的value
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//这里把key转换成Comparable类型,如果key没有实现Comparable接口,会报类型转换异常
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> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
大致原理就是这样,里面关于TreeSet
的两种排序情况都做了介绍,源码追到TreeMap
里面,细节还有很多,如果有更多时间,更当仔细研究