今天跟大家来浅谈一下TreeSet
HashSet的底层是HashMap,而HashMap的数据结构是哈希(散列)表: 是一个数组,里面存储的是链表
TreeSet的底层是TreeMap,而TreeMap的数据结构是红黑数,是一个自平衡的二叉树!
1.TreeSet有两种排序方式:
默认排序:自然顺序进行排序。
注意: 如果使用的是自然排序,那么添加到集合的该元素,必须实现java.lang.Comparable接口,
否则会出现java.lang.ClassCastException
有的类,已经实现了Comparable接口比如String: 这也是为什么添加String,他没有出现ClassCastException异常,
比较器排序: java.util.Comparator接口
2.到底使用哪种,取决于TreeSet的构造方法
如果是无参构造:是使用默认排序: TreeSet()
如果是有参构造:是使用比较器排序: TreeSet(Collection<? extends E> c)
都和根节点比较:
小的往左放
大的往右放
相等不搭理,就相当于是去除重复,这里是把最后添加的那个相等的给去掉!!
其实使用哪种排序方式都行,最重要的是你怎么给他定义排序规则!!
案例一:
public class Demo2 {
public static void main(String[] args) {
TreeSet<String> set1 = new TreeSet<String>();
set1.add("3");
set1.add("7");
set1.add("2");
set1.add("5");
System.out.println(set1);
}
}
最终输出结果是:[2, 3, 5, 7]
为什么会这样输出呢?,我们上面说过,TreeSet有两种排序方式,如果是无参构造,说明是自然排序
说明存储的元素必须实现 java.lang.comparable接口,否则会出现 ClassCastException 类型转换异常!
java.lang.comparable接口中只有一个方法
int compareTo(T o):比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,
则分别返回负整数、零或正整数。
接下来我们看看,String类是否实现了这个这个接口
其实所谓的自然排序,其实也是制定好了排序规则!
接下来我们用TreeSet存储一个没有实现java.lang.Comparable的接口,看会发生什么?
class Student{
String name;
int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
public class Test2 {
public static void main(String[] args) {
//创建集合
TreeSet<Student1> set = new TreeSet<Student1>();
//创建元素
Student1 s1 = new Student1("胡歌", 20);
Student1 s2 = new Student1("苏炳添", 20);
//添加元素到集合
set.add(s1);
set.add(s2);
System.out.println(set);
}
}
结果控制台报错:
这是怎么回事呢?
一:我们跟进TreeSet.add() 源码
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable{
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
public TreeSet() {
this(new TreeMap<E,Object>());
}
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
二:这是我从TreeSet源码中抽取出来的,由上面可知,在创建TreeSet 的时候,无参构造会调用有参构造并且传递了一个TreeMap,然后给本类的成员变量 m 赋值 ,由此可见 NavigableMap m = new TreeMap();
所以说TreeSet的add() , 本质上是调用的 TreeMap的put() 方法
三:然后我们跟进TreeMap的put() 源码
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;
}
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);
}
else {
if (key == null)
throw new NullPointerException();
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;
}
在①处会执行compare() 方法,该方法的源码
final int compare(Object k1, Object k2) {
return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
: comparator.compare((K)k1, (K)k2);
}
它会将k1强制转换为Comparable接口,这里的k1就是Student类,由于Student类并不是Comparable的实现类,所以会产生
类转换异常!!
那么这里只需将Student 类实现java.lang.Comparable接口即可,并重写compareTo() 方法,在里面自定义排序规则
注意事项
那是不是只要实现了java.lang.Comparable接口的类就全部可以往TreeSet里面放呢?见如下案例
我们知道,String类,Integer类都实现了java.lang.Comparable接口,我们做个测试
public class Test3 {
public static void main(String[] args) {
TreeSet set = new TreeSet();
set.add("1");//添加String类型
set.add(2);//添加整型
System.out.println(set);
}
}
还是发生这个错误,这是为什么呢?不是都已经实现了java.lang.Comparable接口了吗?
这是因为在整数2的时候,他会调用comparaTo() 方法去和字符串 “1” 比较,而Integer类的compareTo()
他需要的是一个整型,而你给他传了个字符串!,所以会发生异常!
因为只有相同类的两个实例才会比较大小!
结论:向TreeSet中添加的应该是同一个类的对象,否则也会发生ClassCastException!!
接下来说一下比较器排序:java.util.comparator接口,使用它做一个案例
需求是这样的,只要对象的成员变量值,相同我们就认为是重复的!!
public class Demo1 {
public static void main(String[] args) {
//无参构造是自然排序
//TreeSet<Student> set = new TreeSet<Student>();
TreeSet<Student1> set = new TreeSet<Student1>(new Comparator<Student1>() {
@Override
public int compare(Student1 o1, Student1 o2) {
//1.先比年龄
int num = o1.getAge() - o2.getAge();
//2.年龄相同,比名字长度
int num2 = num == 0 ? o1.getName().length() - o2.getName().length() : num;
//3.名字长度相同,按名字首字母排序, 英文是 "z".compara("y") 是正整数, 但 "张".compare("羊") 是负整数!!
//就是如果比的是英文首字母: 那么就是升序, 中文就是降序!
int num3 = num2 == 0 ? o1.getName().compareTo(o2.getName()) : num2;
return num3;
}
});
Student1 s1 = new Student1("张三",18);
Student1 s2 = new Student1("李四",19);
Student1 s3 = new Student1("王五",18);
Student1 s4 = new Student1("赵六",20);
Student1 s5 = new Student1("王五",18);
Student1 s6 = new Student1("王五",21);
set.add(s1);
set.add(s2);
set.add(s3);
set.add(s4);
set.add(s5);
set.add(s6);
/**
* 需求:如果成员变量值相同,就说明重复!,
* 1. 并且先按年龄降序排!
* 2.年龄相同再按姓名长度排!
* 3.姓名长度相同再按姓名内容排!
*/
System.out.println(set);
}
}
TreeSet判断元素是否重复的标准:
是看compare()方法是不是返回0,如果是返回0,后面这个元素过滤!
举个栗子:
最后:两种排序的区别是
自然排序(元素具备比较性)
比较器排序(集合具备比较性)
举个栗子:
像我们大一刚进教室(容器),我们都是按从矮到高坐,这就是元素具备比较性
然而到了考试的时候,教室里哪里贴了你的考号,你就坐哪,这就是集合具备比较性(在容器中,已经定义好了规则)