上一节我们讲了List,这一节来看看Set集合。

说到Set,最大的特点是无序,且元素是不重复的。但TreeSet还是有序的,为什么这个说呢?往下看吧!

先看代码,先来证明一下是有序的还是无序的。

java set可以有序进 java set有序无序_HashSet

从代码上和输出的结果可以明显的看出,输出的和添加的顺序是不一样的。

再来看看TreeSet是有序的。看代码

java set可以有序进 java set有序无序_HashSet_02

咦,仔细一看,也是无序的啊。对,没错,还是无序的,但你再仔细一看,发现还是有序的。撇开添加时候的顺序,只看输出的结果,是不是有序的。

嗯嗯,这就对了,这就是TreeSet和HashSet的最大的区别,两者都是无序的,但TreeSet会按照特定的顺序对里面的元素进行排序,而Hash却不会。这就是前面说的,为什么Set是无序的,而TreeSet是有序的,这里所说的有序和无序并不能混为一谈,前者是指插入的顺序和查询出来的顺序是不一致的,后者指的是,所插入的元素会按照元素自身的排序方式(自然排序)进行排序。例如String,会按照数字先排序,然后按照字母的顺序排序。所以,要分清楚,有序和无序。

说完无序,Set集合的另一个特点就是元素不能重复。不能重复的是怎么来的呢?先看一看代码:

java set可以有序进 java set有序无序_java_03

看这段代码。line15和line24都是一个set集合,所不同的是,一个存储的是String,一个存储的是Student,Student在line32-33,一个空类,里面什么都没有,line16-18,以及line25-26是往set集合中插入数据,然后就是遍历集合,都很简单,一看就明白,接下来看输出的内容,第一个集合,插入三个元素,最终只打印出2个,有一个没插进去,这很好解释,重复了嘛,我们在插入的时候插入了两个String-->“123”,然后再看第二个集合打印出来的数据(其实这是对象在内存中的地址),先不要管打印出来的是什么,看个数,两个嘛,对吧,再回去看看第二个集合是怎么插进去的,很简单,都是new了一个Student就直接插进去了,没做任何的操作,为什么呢?这两个Student不是一样的么? 重点这就来了:

看代码:

java set可以有序进 java set有序无序_java set可以有序进_04

代码只做了小小的改动,line25-30做了一点改动,目的只有一个,那就是打印出他们的HashCode是不是相同,我们看打印出来的东西,分割线下第一行,那两个int值是否相同,很明显就不一样嘛,对吧。没错就是不一样,这也可以看得出,其实Set集合插入的时候要对比一下HashCode是否相同,如果相同就不插入,不相同就视为不是同一个对象,就插入。信不信,不信你可以把前面String的三个HashCode打印出来看看第三个HashCode是不是和第一个的HashCode是不是相同的,难道它就只对比HashCode么?没那么简单。往下看:

java set可以有序进 java set有序无序_Set_05

我们现在重写了一下Student的hashCode() 方法,全部返回1,那么所有的HashCode就相同咯,再看打印出来的,打印出来的第一个行可以看到,两个HashCode是相同的,但我们遍历集合的时候还是可以打印出两条数据,那么可以得出的是,集合里面确实有两个元素,唉,你这骗子,前面还跟我说HashCode,我还真信了,到这里又不靠谱了,别急,往下看,

java set可以有序进 java set有序无序_HashSet_06

还是原来的代码,还是原来的配方,还是原来的味道,哈哈。跟上面的不同的是:我在Student里面重写了equal()方法,方法里面也很简单,就一句,拿两个对象的HashCode

进行对比,相同就返回true,不同就返回false咯。然后看打印输出的是什么,我们这里可以看到,遍历集合的时候,只遍历出来一个元素,那就很简单的说明集合里面就只有一个元素。(卧槽,你这是截图截出来的,万一你截图你故意少截一行咋办,不信你就去试试呗。嘿嘿。)

好了,例子都到这里了,下面来梳理一下set集合在添加的时候具体的执行的流程,它是怎么分辨出两个元素是否是同一个元素?

首先,在插入的时候,会去对比两个元素的HashCode,如果两个的HashCode不同,那么就不管3721直接插入,如果两个元素的HashCode是相同的话,那么就执行它的equal()方法,如果equal返回的是true,那么就会认定是同一个元素,就不再插入,就这么简单,首先对比HashCode,然后再对比equal(),这里要说明的是,插入的时候是跟目前集合里面所有的元素都对比一遍。还有一点,set集合可以插入空对象(null),但同样的道理,有且只能插入一个。

扯完了Set,接下来看看HashSet 呗,其实HashSet 真没什么好扯的,都跟上面的差不多,有一点要知道的是:HashSet底层的数据结构是哈希表。其余没什么了,我也就不扯了,重复一遍,没意义。

干脆来扯扯HashCode和equal()的设计的巧妙性吧!上面的例子,Student的HashCode我是直接返回的是1,实际的场景中。我们可以将学生的年龄或者学号作为HashCode,或者在equal中直接对比学生的学号即可,只要学生的学号一致就认为是同一个学生,以保证其唯一性。为什么要设置巧妙一点呢?很简单嘛,假如你HashCode直接返回的是学生的年龄,那么是不是很多学生的年龄是一致的,所以是不是还要执行equal,要执行多一个方法要不要耗性能?对吧,所以如果可以直接通过HashCode来对对象进行对比的话,是不是省事很多,当然为什么要重写呢?我不重写不就像第一个一样么?直接new一个过去的时候会自动添加,不会添加不进去。这样的话,你试想一下,如果都是这样做的话,那么我new 学生对象的时候,我学号,年龄性别等都传同样的进去,也就是同一个学生,这样是不是一个学生可以无限的插入,你觉得有意义么?对吧,所以。HashCode要做到恰到好处。

接下来看看TreeSet,TreeSet也没什么好讲的,既然TreeSet里面的元素是有序的。那么就来说说元素的有序是怎么来的?

先把滚动条往上拉,拉到第二个例子,这里可以看到我们的TreeSet是有序的。既然是有序的,那么具体是按照什么排的呢?我们又能不能自己修改其排序呢?

其实呢,排序也很简单,我们只需要在对象里面实现Compareable接口即可排序,里面你想怎么写就怎么写,想怎么排序就怎么排序。

接下来看代码:

java set可以有序进 java set有序无序_Set_07

代码还是一样,只不过不同的是:Student实现了Compareable接口。可以对学生进行排序,排序的方式是按照年龄的升序排序。那降序怎么办呢?如果我有几个可以排序的方式,我可以按照身高排序啊。

看代码:

java set可以有序进 java set有序无序_Set_08


这段代码是按照年龄的降序排序的,可以看到我CompareTo方法中排序的时候取的是身高,然后仔细看看,前一段的代码是this在前,O在后,这一段是O在前,this在后。所以,我们要改变对象的排序方式,很简单。那我多种排序方式一起排怎么办?这个时候你就要深入的了解compareTo方法了。在A.compareTo(B)中。AB都是整形的数据。如果A大于B,那么这个方法返回来的数是1,如果小于,则返回-1.如果等于,则返回0。既然方法的返回值理清楚了这就很简单了嘛。如果是先按照年龄排序,然后按照身高排序。那么你可以先对年龄进行排序,如果年龄的CompareTo方法返回的不是0,那么直接返回,如果是0,继续比较身高。


java set可以有序进 java set有序无序_java set可以有序进_09

compareTo方法就这么写,多简单。如果不信,尽管可以去试试,哈哈!


都了解了,但你发现没有,这有一个很大的毛病,如果我现在是按照年龄排序。将来我要按照身高排序,怎么办?

难道我去改代码吗?要去改就麻烦了,所以,实际使用中,我们是比较少去实现Compareable接口的,你可以看看TreeSet是不是还有一个构造方法,可以传递一个Compare 进去。对,没错,我们只要自定义一个Compare 在TreeSet实例化的时候传递进去即可,就可以不动原来的代码。

咦,新的问题 又出现了,那如果类实现了Compareable,然后又传进去了一个comparator,那怎么办呢?以哪个为准呢?或者两者都有效,只是先按照这个排,然后按照那个排。这里可以明确的跟你说,如果两个都有,以我们传递进去的为准!