Set集合介绍
Set集合的概念
Set集合类似于一个容器,程序把很多对象保存到Set集合中,Set集合对添加顺序不记录,当有重复的对象保存到Set集合时,不会新增后加的重复对象。
Set集合的特点
- Set集合无重复元素,
add()
方法添加相同元素时,返回false; - Set集合
add()
方法不记录顺序;
HashSet类
HashSet介绍
HashSet是按照哈希算法进行存储元素的,具有良好的查询和存取性能。
HashSet特点
- 集合元素值可以为null;
- 不保证元素的排列顺序,有可能排列顺序与添加顺序不同;
- 非同步集合,多线程访问HashSet时,是不安全的,需要通过同步代码保证同步。
- 元素不可重复相同,通过
equals()
和hashCode()
方法一起判断是否相同。
HashSet添加元素过程
add()
方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
通过add()
方法向HashSet存入元素时,HashSet会调用该对象的hashCode()
方法获取hashCode值
,然后根据hashCode值
决定这个元素在HashSet的存储位置。如果有两个元素通过equals()
方法返回true,但hashCode()
方法返回值不同,则HashSet会存储到集合的不同位置,依旧可以成功添加该元素。
示例1:HashSet中元素对象的相同性依据
1)创建ClassA
类,重写equals()
方法
public class ClassA {
@Override
public boolean equals(Object obj) {
return true;
}
}
2)创建ClassB
类,重写hashCode()
方法
public class ClassB {
@Override
public int hashCode() {
return 0;
}
}
3)创建ClassC
类,重写equals()
和hashCode()
方法
public class ClassC {
@Override
public int hashCode() {
return 1;
}
@Override
public boolean equals(Object obj) {
return true;
}
}
4)测试主类:
public class DemoApplication {
public static void main(String[] args) {
// 创建集合
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)运行结果:
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
从上述运行结果可以看出:
1)HashSet集合添加顺序和集合内部元素顺序不一定相同;
2)HashSet添加元素时:
ClassA
类重写了equals()
方法,但是两个对象的hashCode()
返回了不同的hashCode值
,所以HashSet会将这两个对象保存在哈希表中的不同位置。ClassB
类重写了hashCode()
方法,但是两个对象的equals()
方法返回的是不同的对象地址,所以HashSet会将这两个对象保存到同一个位置,并通过链表链接。这种方式不建议有,因为集合中出现链式结构来存储相同hash值的元素时,查询速度会变慢,性能下降。- 只有
ClassC
是当作一个对象来添加到集合中,只有当equals()
和hashCode()
方法都重写的时候,才可以作为判断对象是否相同的依据。
3)总结:若需要将某个类的对象保存到HashSet集合时,我们需要重写这个类的equals()
和hashCode()
方法,只有保证了两个对象通过equals()
方法返回true,hashCode()
方法返回值相同时,才是相同对象。
重写hashCode()方法的准则
- 可重复性:程序运行时,同一个对象多次调用
hashCode()
方法返回的是相同值; - 当两个对象通过
equals()
方法比较后返回的是true,则两个对象的hashCode()
方法应返回相同的hash值; - 对象中用作
equals()
方法比较标准的实例变量,都应该用于计算hashCode
值;
HashSet集合为何查询速度快?
我们回答这个问题前,先看一下HashSet添加元素的流程,使用add()
方法添加元素时,HashSet会根据这个元素的hashCode值
计算存储位置进行存储。当我们查询元素时,也是通过该元素的hashCode值
快速定位到该元素在集合中的位置,其实HashSet底层是通过HashMap
实现的。可以观察到源码:
public class HashSet
extends AbstractSet
implements Set, Cloneable, java.io.Serializable {
private transient HashMap map;
//构造方法
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子类,同样是根据元素的hashCode值判断元素的存储位置,且同时使用链表维护元素的顺序,给人的直观效果就是集合的顺序就是元素插入的顺序保存的。
下面我们通过一组示例来看一下LinkedHashSet的保存元素次序效果。
LinkedHashSet示例
1)主类
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)运行结果
旧的集合:[1, 2, 3]
新的集合:[2, 3, 1]
从上述运行结果中,我们可以看到LinkedHashSet的元素保存顺序即为添加元素的顺序。
TreeSet
TreeSet介绍
TreeSet是SortedSet接口实现类,TreeSet是一种排序集合,该对象中只能添加一种类型元素,否则,会抛出java.lang.ClassCastException
异常;
与HashSet集合采用hash算法来决定元素的存储位置有些不一样,TreeSet是采用红黑树这种数据结构存储集合元素。
TreeSet方法
除了Collection集合的常用方法外,TreeSet集合还有如下常用方法:
Comparator super E> comparator()
:如果TreeSet采用定制排序,该方法返回定制排序所使用的Comparator,如果TreeSet采用自然排序,则返回null;E first()
:返回集合中的第一个元素;E last()
:返回集合中的最后一个元素;E lower(E e)
:返回集合找找那个微语指定元素之前的元素(小于指定元素的最大元素)E higher(E e)
:返回集合中位于指定元素之后元素(大于指定元素的最小元素)SortedSet
:返回Set的子集合,从fromElement(包含)到toElement(不包含)subSet(E fromElement, E toElement) SortedSet
:返回此Set的子集,该子集是由小于toElement的元素组成;headSet(E toElement) SortedSet
:返回此Set的子集,该子集是由大于toElement的元素组成;tailSet(E toElement)
示例
1)运行主类:
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)运行结果:
[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]
从上述运行结果可以看出,TreeSet不是根据添加元素的顺序进行排序,而是通过元素实际大小进行排序。
TreeSet的compareTo()方法详解
TreeSet支持两种排序方式:自然排序和自定义排序(定制排序)。
自然排序
介绍:
TreeSet是通过Comparable接口的compareTo(Object obj)
方法比较元素之间的大小关系,然后将元素按照升序排列,即为自然排序。
实现原理:
实现Comparable接口,并实现compareTo(Object obj)
方法,返回一个整数值。当一个对象调用该方法与另一个对象进行比较时,如obj1.compareTo(obj2)
,该方法若返回0,则表明obj1和obj2这两个对象相等;若返回一个正整数,则表明obj1大于obj2;若返回一个负整数,则表明obj1小于obj2。然后根据红黑树结构寻找存储位置,当元素相同,若此时使用集合add()方法时,无法将新对象添加到集合内。
TreeSet通过元素对应的类重写的compareTo(Object obj)
方法来比较对象大小,就要求重写元素对象对应类的equals()
方法时,需要保证该方法与compareTo(Object obj)
方法保持一致的结果,当两个对象通过equals()
方法返回true时,两个对象通过compareTo(Object obj)
方法应该返回0。
示例
1)对象类
public class OnePerson implements Comparable{
private int age;
private String name;
public OnePerson(int age, String name) {
this.age = age;
this.name = name;
}
//重写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;
}
//重写compareTo()方法,以age作为排序比较标准
@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)运行类
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)运行结果
OnePerson{age=3, name='yaoyao'}
OnePerson{age=5, name='xiaoming'}
OnePerson{age=8, name='huangming'}
从上述运行结果看出,对象类OnePerson添加到TreeSet时是按照age大小进行排序添加进集合。
自定义排序
介绍:
自然排序是根据元素大小进行升序排列,如果需要实现降序等排列需要自定义排序,通过Comparator接口,该接口内有int compare(T o1, T o2)
方法,使用该方法比较o1和o2的大小:若返回正整数,则表示o1>o2;若返回负整数,则表示o1
创建TreeSet集合对象时,提供一个Comparator对象与该集合进行关联,在Comparator对象中实现集合元素的排序逻辑。
示例
1)对象类
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)运行主类:
Lambda方式
public class DemoApplication {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet((o1, o2) ->
{
OnePerson2 onePerson1 = (OnePerson2)o1;
OnePerson2 onePerson2 = (OnePerson2)o2;
//根据OnePerson对象的age属性来判断大小,age越大,OnePerson对象越小,降序
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 treeSet = new TreeSet(new Comparator() {
@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)运行结果
OnePerson2{age=9, name='yaoling'}
OnePerson2{age=5, name='xiaohong'}
OnePerson2{age=2, name='huangming'}
从上述运行结果看出,通过Comparator接口的lambda表达式实现了自定义降序。
EnumSet
EnumSet介绍
EnumSet是一个枚举类的集合类,集合内的所有元素都必须是指定枚举类型的枚举值,枚举类型在创建EnumSet时显式或者隐式地指定;EnumSet也是有顺序的,该顺序是由枚举值在Enum类中的定义顺序决定。
EnumSet不允许加入null元素,若尝试添加null元素,会抛出NullPointerException
异常。
EnumSet常用方法
EnumSet
:创建一个包含指定枚举类全部枚举值的EnumSet集合。allOf(Class elementType) EnumSet
:创建一个元素类型为指定枚举类型的空EnumSet集合。noneOf(Class elementType) EnumSet
:创建同一类型枚举值的一个或多个枚举值的EnumSet集合。of(E first, E... rest) EnumSet
:创建一个包含从from到to枚举值范围内所有枚举值的EnumSet集合。range(E from, E to) EnumSet
:创建一个元素类型和指定的EnumSet集合相同的EnumSet集合,新EnumSet集合包含原EnumSet集合不包含的,新的EnumSet集合和原集合所有元素加起来是枚举类的所有枚举值。complementOf(EnumSet s) EnumSet
:创建一个与指定EnumSet集合具有相同元素类型、相同元素值的EnumSet集合。copyOf(EnumSet s) EnumSet
:使用一个普通集合创建EnumSet集合。但是该普通集合中的元素必须是枚举值,否则会抛出copyOf(Collection c) ClassCastException
异常。
示例
1)枚举类
public enum ColourEnum {
RED,
ORANGE,
YELLOW,
GREEN,
BLUE,
INDIGO,
PURPLE
}
2)运行主类
public class DemoApplication {
public static void main(String[] args) {
//allOf()获取枚举类的全部枚举值
EnumSet colourEnumEnumSet = EnumSet.allOf(ColourEnum.class);
System.out.println("allOf()方法:" + colourEnumEnumSet);
//创建空集合,指定集合元素是ColourEnum类的枚举值
EnumSet colourEnumEnumSet1 = EnumSet.noneOf(ColourEnum.class);
colourEnumEnumSet1.add(ColourEnum.GREEN);
colourEnumEnumSet1.add(ColourEnum.RED);
System.out.println("noneOf()方法:" + colourEnumEnumSet1);
//指定枚举值创建枚举集合
EnumSet colourEnumEnumSet2 = EnumSet.of(ColourEnum.BLUE, ColourEnum.ORANGE);
System.out.println("of()方法:" + colourEnumEnumSet2);
//指定范围创建枚举集合
EnumSet colourEnumEnumSet3 = EnumSet.range(ColourEnum.ORANGE, ColourEnum.INDIGO);
System.out.println("range()方法:" + colourEnumEnumSet3);
//补充枚举值,合集是整个ColourEnum
EnumSet colourEnumEnumSet4 = EnumSet.complementOf(colourEnumEnumSet3);
System.out.println("complementOf()方法:" + colourEnumEnumSet4);
//复制枚举集合
EnumSet colourEnumEnumSet5 = EnumSet.copyOf(colourEnumEnumSet4);
System.out.println("copyOf()方法:" + colourEnumEnumSet5);
}
}
3)运行结果
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好(添加、查询),只有当需要保持排序的Set时,才使用TreeSet,否则使用HashSet。
2)HashSet比LinkedHashSet性能好(添加、删除)。
3)LinkedHashSet比HashSet性能好(遍历查询)。
4)EnumSet是所有Set性能最好的,但缺陷是只能保存同一个枚举类的枚举值作为集合元素。
5)HashSet、TreeSet和EnumSet都是线程不安全的,需要通过Collections工具类的synchronizedSet(Set
或synchronizedSortedSet(SortedSet
方法来包装Set集合。