Set集合介紹
Set集合的概念
??Set集合類(lèi)似于一個(gè)容器笛厦,程序把很多對(duì)象保存到Set集合中,Set集合對(duì)添加順序不記錄旨枯,當(dāng)有重復(fù)的對(duì)象保存到Set集合時(shí)趟佃,不會(huì)新增后加的重復(fù)對(duì)象。
Set集合的特點(diǎn)
- Set集合無(wú)重復(fù)元素友多,
add()
方法添加相同元素時(shí)牲平,返回false; - Set集合
add()
方法不記錄順序域滥;
HashSet類(lèi)
HashSet介紹
??HashSet是按照哈希算法進(jìn)行存儲(chǔ)元素的纵柿,具有良好的查詢和存取性能。
HashSet特點(diǎn)
- 集合元素值可以為null启绰;
- 不保證元素的排列順序昂儒,有可能排列順序與添加順序不同;
- 非同步集合委可,多線程訪問(wèn)HashSet時(shí)宫峦,是不安全的,需要通過(guò)同步代碼保證同步衣盾。
- 元素不可重復(fù)相同劲绪,通過(guò)
equals()
和hashCode()
方法一起判斷是否相同。
HashSet添加元素過(guò)程
add()
方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
??通過(guò)add()
方法向HashSet存入元素時(shí)卡者,HashSet會(huì)調(diào)用該對(duì)象的hashCode()
方法獲取hashCode值
蒿囤,然后根據(jù)hashCode值
決定這個(gè)元素在HashSet的存儲(chǔ)位置。如果有兩個(gè)元素通過(guò)equals()
方法返回true崇决,但hashCode()
方法返回值不同材诽,則HashSet會(huì)存儲(chǔ)到集合的不同位置,依舊可以成功添加該元素恒傻。
示例1:HashSet中元素對(duì)象的相同性依據(jù)
1)創(chuàng)建ClassA
類(lèi)脸侥,重寫(xiě)equals()
方法
public class ClassA {
@Override
public boolean equals(Object obj) {
return true;
}
}
2)創(chuàng)建ClassB
類(lèi),重寫(xiě)hashCode()
方法
public class ClassB {
@Override
public int hashCode() {
return 0;
}
}
3)創(chuàng)建ClassC
類(lèi)碌冶,重寫(xiě)equals()
和hashCode()
方法
public class ClassC {
@Override
public int hashCode() {
return 1;
}
@Override
public boolean equals(Object obj) {
return true;
}
}
4)測(cè)試主類(lèi):
public class DemoApplication {
public static void main(String[] args) {
// 創(chuàng)建集合
HashSet hashSet = new HashSet();
// 添加元素
hashSet.add(new ClassA());
hashSet.add(new ClassA());
hashSet.add(new ClassB());
hashSet.add(new ClassB());
hashSet.add(new ClassC());
hashSet.add(new ClassC());
System.out.println("hashSet: ");
hashSet.forEach(obj -> System.out.println(obj));
}
}
5)運(yùn)行結(jié)果:
hashSet:
com.example.andya.demo.bean.ClassB@0
com.example.andya.demo.bean.ClassB@0
com.example.andya.demo.bean.ClassC@1
com.example.andya.demo.bean.ClassA@1edf1c96
com.example.andya.demo.bean.ClassA@368102c8
??從上述運(yùn)行結(jié)果可以看出:
1)HashSet集合添加順序和集合內(nèi)部元素順序不一定相同湿痢;
2)HashSet添加元素時(shí):
-
ClassA
類(lèi)重寫(xiě)了equals()
方法,但是兩個(gè)對(duì)象的hashCode()
返回了不同的hashCode值
,所以HashSet會(huì)將這兩個(gè)對(duì)象保存在哈希表中的不同位置譬重。 -
ClassB
類(lèi)重寫(xiě)了hashCode()
方法拒逮,但是兩個(gè)對(duì)象的equals()
方法返回的是不同的對(duì)象地址,所以HashSet會(huì)將這兩個(gè)對(duì)象保存到同一個(gè)位置臀规,并通過(guò)鏈表鏈接滩援。這種方式不建議有,因?yàn)榧现谐霈F(xiàn)鏈?zhǔn)浇Y(jié)構(gòu)來(lái)存儲(chǔ)相同hash值的元素時(shí)塔嬉,查詢速度會(huì)變慢玩徊,性能下降。 - 只有
ClassC
是當(dāng)作一個(gè)對(duì)象來(lái)添加到集合中谨究,只有當(dāng)equals()
和hashCode()
方法都重寫(xiě)的時(shí)候恩袱,才可以作為判斷對(duì)象是否相同的依據(jù)。
3)總結(jié):若需要將某個(gè)類(lèi)的對(duì)象保存到HashSet集合時(shí)胶哲,我們需要重寫(xiě)這個(gè)類(lèi)的equals()
和hashCode()
方法畔塔,只有保證了兩個(gè)對(duì)象通過(guò)equals()
方法返回true,hashCode()
方法返回值相同時(shí)鸯屿,才是相同對(duì)象澈吨。
重寫(xiě)hashCode()方法的準(zhǔn)則
- 可重復(fù)性:程序運(yùn)行時(shí),同一個(gè)對(duì)象多次調(diào)用
hashCode()
方法返回的是相同值寄摆; - 當(dāng)兩個(gè)對(duì)象通過(guò)
equals()
方法比較后返回的是true谅辣,則兩個(gè)對(duì)象的hashCode()
方法應(yīng)返回相同的hash值; - 對(duì)象中用作
equals()
方法比較標(biāo)準(zhǔn)的實(shí)例變量婶恼,都應(yīng)該用于計(jì)算hashCode
值桑阶;
HashSet集合為何查詢速度快?
??我們回答這個(gè)問(wèn)題前熙尉,先看一下HashSet添加元素的流程联逻,使用add()
方法添加元素時(shí)搓扯,HashSet會(huì)根據(jù)這個(gè)元素的hashCode值
計(jì)算存儲(chǔ)位置進(jìn)行存儲(chǔ)检痰。當(dāng)我們查詢?cè)貢r(shí),也是通過(guò)該元素的hashCode值
快速定位到該元素在集合中的位置锨推,其實(shí)HashSet底層是通過(guò)HashMap
實(shí)現(xiàn)的铅歼。可以觀察到源碼:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private transient HashMap<E,Object> map;
//構(gòu)造方法
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//add方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
LinkedHashSet
LinkedHashSet介紹
??LinkedHashSet是HashSet子類(lèi)换可,同樣是根據(jù)元素的hashCode值判斷元素的存儲(chǔ)位置椎椰,且同時(shí)使用鏈表維護(hù)元素的順序,給人的直觀效果就是集合的順序就是元素插入的順序保存的沾鳄。
??下面我們通過(guò)一組示例來(lái)看一下LinkedHashSet的保存元素次序效果慨飘。
LinkedHashSet示例
1)主類(lèi)
public class DemoApplication {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add("1");
linkedHashSet.add("2");
linkedHashSet.add(3);
System.out.println("舊的集合:" + linkedHashSet);
linkedHashSet.remove("1");
linkedHashSet.add(1);
System.out.println("新的集合:" + linkedHashSet);
}
}
2)運(yùn)行結(jié)果
舊的集合:[1, 2, 3]
新的集合:[2, 3, 1]
??從上述運(yùn)行結(jié)果中,我們可以看到LinkedHashSet的元素保存順序即為添加元素的順序。
TreeSet
TreeSet介紹
??TreeSet是SortedSet接口實(shí)現(xiàn)類(lèi)瓤的,TreeSet是一種排序集合休弃,該對(duì)象中只能添加一種類(lèi)型元素,否則圈膏,會(huì)拋出java.lang.ClassCastException
異常塔猾;
??與HashSet集合采用hash算法來(lái)決定元素的存儲(chǔ)位置有些不一樣,TreeSet是采用紅黑樹(shù)這種數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)集合元素稽坤。
TreeSet方法
除了Collection集合的常用方法外丈甸,TreeSet集合還有如下常用方法:
-
Comparator<? super E> comparator()
:如果TreeSet采用定制排序,該方法返回定制排序所使用的Comparator尿褪,如果TreeSet采用自然排序睦擂,則返回null; -
E first()
:返回集合中的第一個(gè)元素杖玲; -
E last()
:返回集合中的最后一個(gè)元素祈匙; -
E lower(E e)
:返回集合找找那個(gè)微語(yǔ)指定元素之前的元素(小于指定元素的最大元素) -
E higher(E e)
:返回集合中位于指定元素之后元素(大于指定元素的最小元素) -
SortedSet<E> subSet(E fromElement, E toElement)
:返回Set的子集合,從fromElement(包含)到toElement(不包含) -
SortedSet<E> headSet(E toElement)
:返回此Set的子集天揖,該子集是由小于toElement的元素組成夺欲; -
SortedSet<E> tailSet(E toElement)
:返回此Set的子集,該子集是由大于toElement的元素組成今膊;
示例
1)運(yùn)行主類(lèi):
public class DemoApplication {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(2);
treeSet.add(1);
treeSet.add(6);
treeSet.add(3);
treeSet.add(8);
System.out.println(treeSet);
//查看排序性
treeSet.remove(3);
treeSet.add(5);
System.out.println(treeSet);
System.out.println("first()方法: " + treeSet.first());
System.out.println("last()方法: " + treeSet.last());
System.out.println("lower()方法: " + treeSet.lower(5));
System.out.println("higher()方法: " + treeSet.higher(5));
System.out.println("subSet()方法: " + treeSet.subSet(2,6));
System.out.println("headSet()方法: " + treeSet.headSet(5));
System.out.println("tailSet()方法: " + treeSet.tailSet(5));
}
}
2)運(yùn)行結(jié)果:
[1, 2, 3, 6, 8]
[1, 2, 5, 6, 8]
first()方法: 1
last()方法: 8
lower()方法: 2
higher()方法: 6
subSet()方法: [2, 5]
headSet()方法: [1, 2]
tailSet()方法: [5, 6, 8]
??從上述運(yùn)行結(jié)果可以看出些阅,TreeSet不是根據(jù)添加元素的順序進(jìn)行排序,而是通過(guò)元素實(shí)際大小進(jìn)行排序斑唬。
TreeSet的compareTo()方法詳解
??TreeSet支持兩種排序方式:自然排序和自定義排序(定制排序)市埋。
自然排序
介紹:
??TreeSet是通過(guò)Comparable接口的compareTo(Object obj)
方法比較元素之間的大小關(guān)系,然后將元素按照升序排列恕刘,即為自然排序缤谎。
實(shí)現(xiàn)原理:
??實(shí)現(xiàn)Comparable接口,并實(shí)現(xiàn)compareTo(Object obj)
方法褐着,返回一個(gè)整數(shù)值坷澡。當(dāng)一個(gè)對(duì)象調(diào)用該方法與另一個(gè)對(duì)象進(jìn)行比較時(shí),如obj1.compareTo(obj2)
含蓉,該方法若返回0频敛,則表明obj1和obj2這兩個(gè)對(duì)象相等;若返回一個(gè)正整數(shù)馅扣,則表明obj1大于obj2斟赚;若返回一個(gè)負(fù)整數(shù),則表明obj1小于obj2差油。然后根據(jù)紅黑樹(shù)結(jié)構(gòu)尋找存儲(chǔ)位置拗军,當(dāng)元素相同,若此時(shí)使用集合add()方法時(shí),無(wú)法將新對(duì)象添加到集合內(nèi)发侵。
??TreeSet通過(guò)元素對(duì)應(yīng)的類(lèi)重寫(xiě)的compareTo(Object obj)
方法來(lái)比較對(duì)象大小侈咕,就要求重寫(xiě)元素對(duì)象對(duì)應(yīng)類(lèi)的equals()
方法時(shí),需要保證該方法與compareTo(Object obj)
方法保持一致的結(jié)果器紧,當(dāng)兩個(gè)對(duì)象通過(guò)equals()
方法返回true時(shí)耀销,兩個(gè)對(duì)象通過(guò)compareTo(Object obj)
方法應(yīng)該返回0。
示例
1)對(duì)象類(lèi)
public class OnePerson implements Comparable<OnePerson>{
private int age;
private String name;
public OnePerson(int age, String name) {
this.age = age;
this.name = name;
}
//重寫(xiě)equals()方法與compareTo()方法保持一致的效果
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj != null && obj.getClass() == OnePerson.class) {
OnePerson onePerson = (OnePerson) obj;
return onePerson.age == this.age;
}
return false;
}
//重寫(xiě)compareTo()方法铲汪,以age作為排序比較標(biāo)準(zhǔn)
@Override
public int compareTo(OnePerson o) {
return this.age > o.age
? 1 : this.age < o.age
? -1 : 0;
}
@Override
public String toString() {
return "OnePerson{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
2)運(yùn)行類(lèi)
public class DemoApplication {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
OnePerson onePerson1 = new OnePerson(5, "xiaoming");
OnePerson onePerson2 = new OnePerson(3, "yaoyao");
OnePerson onePerson3 = new OnePerson(8, "huangming");
treeSet.add(onePerson1);
treeSet.add(onePerson2);
treeSet.add(onePerson3);
treeSet.forEach(onePerson -> System.out.println(onePerson));
}
}
3)運(yùn)行結(jié)果
OnePerson{age=3, name='yaoyao'}
OnePerson{age=5, name='xiaoming'}
OnePerson{age=8, name='huangming'}
??從上述運(yùn)行結(jié)果看出熊尉,對(duì)象類(lèi)OnePerson添加到TreeSet時(shí)是按照age大小進(jìn)行排序添加進(jìn)集合。
自定義排序
介紹:
??自然排序是根據(jù)元素大小進(jìn)行升序排列掌腰,如果需要實(shí)現(xiàn)降序等排列需要自定義排序狰住,通過(guò)Comparator接口,該接口內(nèi)有int compare(T o1, T o2)
方法齿梁,使用該方法比較o1和o2的大写咧病:若返回正整數(shù),則表示o1>o2勺择;若返回負(fù)整數(shù)创南,則表示o1<o2;若返回0省核,則表示o1==o2.
實(shí)現(xiàn)原理:
??創(chuàng)建TreeSet集合對(duì)象時(shí)稿辙,提供一個(gè)Comparator對(duì)象與該集合進(jìn)行關(guān)聯(lián),在Comparator對(duì)象中實(shí)現(xiàn)集合元素的排序邏輯气忠。
示例
1)對(duì)象類(lèi)
public class OnePerson2{
private int age;
private String name;
public OnePerson2(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "OnePerson2{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
2)運(yùn)行主類(lèi):
Lambda方式
public class DemoApplication {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet((o1, o2) ->
{
OnePerson2 onePerson1 = (OnePerson2)o1;
OnePerson2 onePerson2 = (OnePerson2)o2;
//根據(jù)OnePerson對(duì)象的age屬性來(lái)判斷大小邻储,age越大,OnePerson對(duì)象越小旧噪,降序
return onePerson1.getAge() > onePerson2.getAge() ? -1
: onePerson1.getAge() < onePerson2.getAge() ? 1 : 0;
});
treeSet.add(new OnePerson2(5, "xiaohong"));
treeSet.add(new OnePerson2(2, "huangming"));
treeSet.add(new OnePerson2(9, "yaoling"));
treeSet.forEach(onePerson -> System.out.println(onePerson));
}
}
compare()方式
public class DemoApplication {
public static void main(String[] args) {
TreeSet<OnePerson2> treeSet = new TreeSet(new Comparator<OnePerson2>() {
@Override
public int compare(OnePerson2 o1, OnePerson2 o2) {
return o1.getAge() > o2.getAge() ? -1
: o1.getAge() < o2.getAge() ? 1 : 0;
}
});
treeSet.add(new OnePerson2(5, "xiaohong"));
treeSet.add(new OnePerson2(2, "huangming"));
treeSet.add(new OnePerson2(9, "yaoling"));
treeSet.forEach(onePerson -> System.out.println(onePerson));
}
}
3)運(yùn)行結(jié)果
OnePerson2{age=9, name='yaoling'}
OnePerson2{age=5, name='xiaohong'}
OnePerson2{age=2, name='huangming'}
??從上述運(yùn)行結(jié)果看出吨娜,通過(guò)Comparator接口的lambda表達(dá)式實(shí)現(xiàn)了自定義降序。
EnumSet
EnumSet介紹
??EnumSet是一個(gè)枚舉類(lèi)的集合類(lèi)淘钟,集合內(nèi)的所有元素都必須是指定枚舉類(lèi)型的枚舉值宦赠,枚舉類(lèi)型在創(chuàng)建EnumSet時(shí)顯式或者隱式地指定;EnumSet也是有順序的日月,該順序是由枚舉值在Enum類(lèi)中的定義順序決定袱瓮。
??EnumSet不允許加入null元素,若嘗試添加null元素爱咬,會(huì)拋出NullPointerException
異常。
EnumSet常用方法
-
EnumSet<E> allOf(Class<E> elementType)
:創(chuàng)建一個(gè)包含指定枚舉類(lèi)全部枚舉值的EnumSet集合绊起。 -
EnumSet<E> noneOf(Class<E> elementType)
:創(chuàng)建一個(gè)元素類(lèi)型為指定枚舉類(lèi)型的空EnumSet集合精拟。 -
EnumSet<E> of(E first, E... rest)
:創(chuàng)建同一類(lèi)型枚舉值的一個(gè)或多個(gè)枚舉值的EnumSet集合。 -
EnumSet<E> range(E from, E to)
:創(chuàng)建一個(gè)包含從from到to枚舉值范圍內(nèi)所有枚舉值的EnumSet集合。 -
EnumSet<E> complementOf(EnumSet<E> s)
:創(chuàng)建一個(gè)元素類(lèi)型和指定的EnumSet集合相同的EnumSet集合蜂绎,新EnumSet集合包含原EnumSet集合不包含的栅表,新的EnumSet集合和原集合所有元素加起來(lái)是枚舉類(lèi)的所有枚舉值。 -
EnumSet<E> copyOf(EnumSet<E> s)
:創(chuàng)建一個(gè)與指定EnumSet集合具有相同元素類(lèi)型师枣、相同元素值的EnumSet集合怪瓶。 -
EnumSet<E> copyOf(Collection<E> c)
:使用一個(gè)普通集合創(chuàng)建EnumSet集合。但是該普通集合中的元素必須是枚舉值践美,否則會(huì)拋出ClassCastException
異常洗贰。
示例
1)枚舉類(lèi)
public enum ColourEnum {
RED,
ORANGE,
YELLOW,
GREEN,
BLUE,
INDIGO,
PURPLE
}
2)運(yùn)行主類(lèi)
public class DemoApplication {
public static void main(String[] args) {
//allOf()獲取枚舉類(lèi)的全部枚舉值
EnumSet<ColourEnum> colourEnumEnumSet = EnumSet.allOf(ColourEnum.class);
System.out.println("allOf()方法:" + colourEnumEnumSet);
//創(chuàng)建空集合,指定集合元素是ColourEnum類(lèi)的枚舉值
EnumSet<ColourEnum> colourEnumEnumSet1 = EnumSet.noneOf(ColourEnum.class);
colourEnumEnumSet1.add(ColourEnum.GREEN);
colourEnumEnumSet1.add(ColourEnum.RED);
System.out.println("noneOf()方法:" + colourEnumEnumSet1);
//指定枚舉值創(chuàng)建枚舉集合
EnumSet<ColourEnum> colourEnumEnumSet2 = EnumSet.of(ColourEnum.BLUE, ColourEnum.ORANGE);
System.out.println("of()方法:" + colourEnumEnumSet2);
//指定范圍創(chuàng)建枚舉集合
EnumSet<ColourEnum> colourEnumEnumSet3 = EnumSet.range(ColourEnum.ORANGE, ColourEnum.INDIGO);
System.out.println("range()方法:" + colourEnumEnumSet3);
//補(bǔ)充枚舉值陨倡,合集是整個(gè)ColourEnum
EnumSet<ColourEnum> colourEnumEnumSet4 = EnumSet.complementOf(colourEnumEnumSet3);
System.out.println("complementOf()方法:" + colourEnumEnumSet4);
//復(fù)制枚舉集合
EnumSet<ColourEnum> colourEnumEnumSet5 = EnumSet.copyOf(colourEnumEnumSet4);
System.out.println("copyOf()方法:" + colourEnumEnumSet5);
}
}
3)運(yùn)行結(jié)果
allOf()方法:[RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PURPLE]
noneOf()方法:[RED, GREEN]
of()方法:[ORANGE, BLUE]
range()方法:[ORANGE, YELLOW, GREEN, BLUE, INDIGO]
complementOf()方法:[RED, PURPLE]
copyOf()方法:[RED, PURPLE]
各種Set集合的比較
性能比較
1)HashSet性能比TreeSet好(添加敛滋、查詢),只有當(dāng)需要保持排序的Set時(shí)兴革,才使用TreeSet绎晃,否則使用HashSet。
2)HashSet比LinkedHashSet性能好(添加杂曲、刪除)庶艾。
3)LinkedHashSet比HashSet性能好(遍歷查詢)。
4)EnumSet是所有Set性能最好的擎勘,但缺陷是只能保存同一個(gè)枚舉類(lèi)的枚舉值作為集合元素落竹。
5)HashSet、TreeSet和EnumSet都是線程不安全的货抄,需要通過(guò)Collections工具類(lèi)的synchronizedSet(Set<T> s)
或synchronizedSortedSet(SortedSet<T> s)
方法來(lái)包裝Set集合述召。