此篇用以致敬:那些年,我们一起学过的TreeSet。
相信很多学过Java的小孩子们,都知道TreeSet有两大特性:来,大声喊出来
一、有序;二、值唯一
一、写作背景
有木有感觉好高大
在最初我们学习集合框架时,Collection接口以及他的宝宝List接口和Set接口一定是我们最先开始接触的。而Set接口是个神奇的接口,为什么呢?因为他有一个淘气的TreeSet。嗯嘛嘛(之所以淘气嘛,是因为博主刚开始自己学不会TreeSet,一下就掉进坑里,哼哧哼哧爬不上来,啊哈哈哈哈)
二、TreeSet的排序分析
在TreeSet中,默认是升序排序哦
public class testTreeSet {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
set.add(5);
set.add(4);
set.add(3);
set.add(2);
set.add(1);
System.out.println(set);
}
}
输出结果:
我们来看看,他为什么默认是升序?
(1)TreeSet是基于TreeMap实现的;
(2)查看TreeSet的add()方法;
在add()方法中,竟然有个m,让我们来猜猜m是什么呢?
m是NavigableMap的一个实例;
那么NavigableMap又是什么?
不难发现,TreeMap实现了NavigableMap接口
搞清楚m后,继续回到TreeMap的add()方法中,发现了put(),啊,熟悉的put,为了查看这个源码,发现源码在Map接口中,但是没有方法体,所以还是去TreeMap中去看,毕竟TreeSet还是基于TreeMap(好像分析了挺久,怎么回到了(1),不管了,继续征程。。。)
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();
@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);
}
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;
}
从put()方法中,阅读源码发现,将元素构成了一棵树,但是不允许有重复元素,
(1)先将第一个元素作为根节点,
(2)比较已存在结点和新结点的值; 大于根结点,将它置为右结点; 小于根结点值,将它置为左结点; 等于根结点值,不存储;
(3)其他元素按照步骤依次存储 以5,4,3,2,1为例,将会构造出这样一棵树:
这样就剖析结束了吗,呐,按照剧情走,还有。。。
在仔细观察put()后,发现在比较值时,使用了compare(),其中有一个对象是cpr,这个对象是一个由K值决定类型的比较器对象;稍稍解释一下,就是如果K值类型是Integer,那么就可以通过cpr调用Integer类中的compare()方法
得出结论:Integer、String等对象Object,java中已经为这些对象写了compare();
总结:通过源码我们可以发现,存入元素的时候,它创建了一个树,第一个元素就是树的根节点,后面的元素依次从树的根节点开始向后比较(创建比较器,利用comparator()方法进行比较),小的就往左边放,大的就往右边放,而相同的就不放进去(实现了唯一性)。取出元素的时候,它采用中序遍历的方法(左子树 根节点 右子树)遍历整个树,达到有序。
自定义对象的保存
那么如果一个自定义对象要存储到TreeSet集合中该怎么办呢?
假如源程序是这样的,自定义对象为person对象,测试一下:
class Person{
private Integer age;
private String name;
public Person(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
}
public class testTreeSet {
public static void main(String[] args) {
Set<Person> set = new TreeSet<>();
set.add(new Person(18,"zhangsan"));
set.add(new Person(19,"lisi"));
set.add(new Person(20,"wangwu"));
System.out.println(set);
}
}
输出结果:
呀,报错了
不要着急,不要着急,休息一下,休息一下
三、TreeSet保存自定义对象时的排序分析
TreeSet保存自定义对象,有两种方式保证有序和值唯一的特性;
法一:自定义类实现Comparable接口,覆写compareTo()方法,比较规则可自己设定;
法二:构造自定义类的外部比较器类,覆写compare()方法;将比较器对象传入TreeSet中,比较规则可自己设定;
1.内部比较
自定义类自己实现了Comparable接口
Comparable接口
(1)java.lang.Comparable:内部排序接口
(2)类实现了Comparable表示此类具备可比较的性质
(3)比较方法compareTo(T o);
public int compareTo(T o) {}
返回正数:表示当前对象>目标对象
返回0:表示当前对象=目标对象
返回负数:表示当前对象<目标对象
实现内部比较的Person类如下:
class Person implements Comparable<Person>{
private Integer age;
private String name;
public Person(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public int compareTo(Person o) {
//按照年龄排序
//如果年龄相同,去比较姓名;不相同就返回年龄差值
int num = this.age - o.age;
int num1 = num == 0 ? this.name.compareTo(o.name) : num;
return num1;
}
}
测试一下,测试类如下:
public class testTreeSet {
public static void main(String[] args) {
Set<Person> set = new TreeSet<>();
set.add(new Person(18,"zhangsan"));
set.add(new Person(19,"lisi"));
set.add(new Person(20,"wangwu"));
for(Person p : set){
System.out.println("年龄为:"p.getAge()+",姓名为:"+p.getName());
}
}
}
测试结果:
2.外部比较器
从外部传入一个该类的比较器对象,该类的比较器类实现了Comparator接口
Comparator接口
(1)java.util.Comparator:外部排序接口(策略模式)
(2)类本身不具备可比较的特性,专门有一个类来比较自定义类的大小
(3)比较方法compare(T o1,T o2);
public int compare(T o1,T o2) {}
返回正数:表示当前对象>目标对象
返回0:表示当前对象=目标对象
返回负数:表示当前对象<目标对象
实现外部比较器的Person类:
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
class Person{
private Integer age;
private String name;
public Person(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
}
/*按照年龄升序排序,实现了外部比较器Comparator接口*/
class PersonAgeSec implements Comparator<Person>{
@Override
public int compare(Person o1, Person o2) {
if(o1.getAge() > o2.getAge()){
return 1;
}
if(o1.getAge() < o2.getAge()){
return -1;
}
return 0;
}
}
测试类,嗯嘛嘛,跟上面一样,哈哈哈:
public class testTreeSet {
public static void main(String[] args) {
Comparator p = new PersonAgeSec();
Set<Person> set = new TreeSet<>(p);//将比较器对象传入TreeSet的构造方法中
set.add(new Person(18,"zhangsan"));
set.add(new Person(19,"lisi"));
set.add(new Person(20,"wangwu"));
for(Person p : set){
System.out.println("年龄为:"+p.getAge()+",姓名为:"+p.getName());
}
}
}
测试结果:
四、其他Set接口子类重复元素判断
使用TreeSet子类进行数据保存的时候,重复元素的判断依靠的是Comparable接口完成,但这并不是全部Set接口判断重复元素的方式;在HashSet子类中,判断重复元素的方式依靠的是Object类中的两个方法;如下:
1.hash码:public native int hashCode();
2.对象比较:public boolean equals(Object obj);
在Java中进行对象比较的操作:
(1)通过一个对象的唯一编码找到一个对象的信息;
(2)编码匹配后调用equals()方法进行内容的比较;
栗子:
package com.collection.Set;
import java.util.*;
class Person1{
private Integer age;
private String name;
public Person1(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o){
if(this == o) return true;
if(o == null || this.getClass() != o.getClass()) return false;
Person1 person1 = (Person1) o;
return Objects.equals(name,person1.name) &&
Objects.equals(age,person1.age);
}
@Override
public int hashCode(){
return Objects.hash(name,age);
}
}
public class setTest {
public static void main(String[] args) {
Set<Person1> set = new HashSet<>();
set.add(new Person1(20,"A"));
//重复元素
set.add(new Person1(20,"A"));
set.add(new Person1(23,"y"));
set.add(new Person1(21,"B"));
for(Person1 p : set){
System.out.println("年龄为:"+p.getAge()+",姓名为:"+p.getName());
}
}
}
输出结果:
如果要想标识出对象的唯一性,一定需要equals()和hashCode()方法共同调用
结论:
(1)如果两个对象equals()相等,那么他们的hashCode必然相等;
(2)如果两个对象hashCode相等,那么他们的equals不一定相等,因为可能产生hash碰撞;
对象的判断必须两个方法equals()、hashCode()返回值都相同才判断为相同;
写的不恰当的可留言呐