Set集合
java的Set集合只能存放无序的,不能重复的数据, Set集合与Collection的用法基本类似,可以这么说,Set集合就是Collection(但Set集合不能存放相同的元素,如果使用add添加相同的元素,add会返回false,且添加的元素也没有添加进去)
HashSet类
HashSet 是 Set 接口的典型实现,HashSet 按 Hash算法来存储集合中的元素,因此具有很好的存取和查找性能 。
HashSet具备的特点
1、不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化 。
2、HashSet 不是同步的,如果多个线程同时访问 一个HashSet,假设有两个或者两个以上线程同时 修改了 HashSet 集合时,则必须通过代码来保证其同步 。
3、集合元素值可以是 null 。
HashSet的存储流程
当程序往HashSet集合中添加一个元素时,HashSet 会调用该对象的 hashCode()方法来得到该对象的
hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置 。 如果有两个元素通过 equals()方法比较返回 true
,但它们的 hashCode()方法返回值不相等
, HashSet 将会把它们存储在不同的位置,依然可以添加成功
。
HashSet 集合判断两个元素相等的标准是两个对象通过 equals()方法比较相等,并且两个对象的 hashCode()方法返回值也相等。
实例代码
import java.util.HashSet;
/**
* @author: 随风飘的云
* @date 2022/03/18 23:28
*/
class animal{
public boolean equals(Object obj){
return true;
}
}
class Cat{
public int hashCode(){
return 1;
}
}
class Dog{
public boolean equals(Object obj){
return true;
}
public int hashCode(){
return 2;
}
}
public class HashSetTest {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add(new animal());
set.add(new animal());
set.add(new Cat());
set.add(new Cat());
set.add(new Dog());
set.add(new Dog());
set.stream().forEach(System.out::println);
}
}
结果:
结果分析
首先animal类重写的equals方法,该方法永远返回true,Cat类中重写了hasCode方法,它永远返回1,Dog重写了hashCode方法和equals方法,分别返回2和true,程序中一共分别添加了两个animal类,Cat类,Dog类,但是输出的除了只有一个Dog类,其他的都是两个输出的。只有一个解析:生成的两个animal和Cat都被HashSet集合当做两个不同的类
了,而生成的两个Dog类只被当做一个类
。因此可以得出HashSet集合判断两个不同的类是通过hashCode方法和equals方法同时进行的。
HashSet使用特点
当把一个对象放入 HashSet 中时, 如果需要重写该对象对应类的 equals()方法 ,则也应该重写其 hashCode()方法 。 规则是 : 如果两个对象通过 equals()方法比较返回 true,这两个对象的 hashCode 值也应该相同 。
不重写可能性后果一
如果说没有重写这两个方法,假如有两个对象通过equals方法判断得出返回true,但是他们的hashCode方法返回的hash值是不同的,HashSet集合认定这两个对象是不一样的
,把他们存放进去,这就与Set集合的不可重复性相反
。
不重写可能性后果二
两个对象的 hashCode()方法返 回 的 hashCode 值相同 , 但它们通过 equals()方法 比较返回 false
时将更麻烦 :因为两个对象的 hashCode 值相同
, HashSet 将试图把它们保存在同一个位置
, 但又不行(否则将只剩下一个对象) ,所以实际上会在这个位置用链式结构来保存多个对象
; 而 HashSet 访问集合元素时也是根据元素的 hashCode 值来快速定位
的,如果 HashSet 中两个以上的元素具有相同的 hashCode值,将会导致性能下降
。
重写hashCode基本规则
1、在程序运行过程中,同一个对象多次调用 hashCode()方法应该返回相同的值 。
2、当两个对象通过 equals()方法比较返回true 时,这两个对象的 hashCode()方法应返回相等的值
3、对象中用作 equals()方法比较标准的实例变量,都应该用于计算hashCode 值 。
计算方式
1、把对象内每个有意义的实例变量( 即每个参与 equals()方法比较标准的实例变量) 计算出一个int 类型的 hashCode 值
2、用第 1 步计算出来的多个 hashCode 值组合计算出 一个 hashCode 值返回 。
return f1 . hashCode() + (int)f2;
3、为了避免直接相加产生偶然相等(两个对象的f1,f2实例变量并不相等,但它们的 hashCode 的和恰好相等),可以通过为各实例变量 的 hashCode 值乘以任意一个质数后再相加。
return f1.hashCode()*19 + (int)f2 * 31;
hashCode的内置的实例代码
// 来源于AbstractSet
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
实例代码
import java.util.HashSet;
import java.util.Iterator;
/**
* @author: 随风飘的云
* @date 2022/03/18 23:53
*/
class R
{
int count;
public R(int count)
{
this.count = count;
}
public String toString()
{
return "R[count:" + count + "]";
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj != null && obj.getClass() == R.class)
{
R r = (R)obj;
return this.count == r.count;
}
return false;
}
public int hashCode()
{
return this.count;
}
}
public class HashSetTest2 {
public static void main(String[] args) {
HashSet hs = new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
// 打印HashSet集合,集合元素没有重复
System.out.println(hs);
// 取出第一个元素
Iterator it = hs.iterator();
R first = (R)it.next();
// 为第一个元素的count实例变量赋值
first.count = -3; // ①
// 再次输出HashSet集合,集合元素有重复元素
System.out.println(hs);
// 删除count为-3的R对象
hs.remove(new R(-3)); // ②
// 可以看到被删除了一个R元素
System.out.println(hs);
System.out.println("hs是否包含count为-3的R对象?"
+ hs.contains(new R(-3))); // 输出false
System.out.println("hs是否包含count为-2的R对象?"
+ hs.contains(new R(-2))); // 输出false
}
}
结果:
LinkedHashSet类
HashSet 还有 一个子类 LinkedHashSet(与HashSet一样,不允许相同的对象或数据存储到LinkedHashSet里
) , LinkedHashSet 集合也是根据元素的 hashCode 值来决定元素的存储位置
,但它同时使用链表
维护元素的次序 ,这样使得元素看起来是以插入的顺序保存的 。 也就是说,当遍历 LinkedHashSet 集合里的元素时, LinkedHashSet 将会按元素的添加顺序来访问集合里的元素
。
LinkedHashSet 需要维护元素的插入顺序 ,因此性能略低于 HashSet 的性能
,但在迭代访问 Set 里的全部元素时将有很好的性能,因为它以链表来维护内部顺序 。
实例代码
import java.util.LinkedHashSet;
/**
* @author: 随风飘的云
* @date 2022/03/19 0:23
*/
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedHashSet books = new LinkedHashSet();
books.add("哈哈哈");
books.add("我你他");
System.out.println(books);
// 删除
books.remove("哈哈哈");
// 添加
books.add("哈哈哈");
System.out.println(books);
}
}
结果:
TreeSet 类
TreeSet 是 SortedSet 接口的实现类,可以对Set集合中的元素进行排序,是不同步的。TreeSet可以确保集合元素处于排序状态 。
TreeSet的Api方法
Comparator comparator(): 如 果 TreeSet 采用了定制排序,则该方法返回定制排序所使用的Comparator; 如 果 TreeSet 采用了自然排序,则返回 null 。
Object first(): 返回集合中的第一个元素 。
Object last(): 返回集合中的最后一个元素 。
Object lower(Object e) : 返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参 考元素不需要是 TreeSet 集合里的元素) 。
Object higher(Object e): 返回集合中位于指定元素之后的元素(即大于指定元素的最小元素, 参考元素不需要是 TreeSet 集合里的元素) 。
SortedSet subSet(Object fromElement, Object toElement): 返回此 Set 的子集合,范围从 fromElement (包含〉到 toElement (不包含) 。
SortedSet headSet(Object toElement): 返回此 Set 的子集,由小于 toElement 的元素组成 。
SortedSet tailSet(Object tomElement): 返回此 Set 的子集 ,由大于或等于tomElement 的元素组成 。
实例代码
import java.util.TreeSet;
/**
* @author: 随风飘的云
* @date 2022/03/19 1:05
*/
public class TreeSetTest {
public static void main(String[] args) {
TreeSet nums = new TreeSet();
// 向TreeSet中添加四个Integer对象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
// 输出集合元素,看到集合元素已经处于排序状态
System.out.println(nums);
// 输出集合里的第一个元素
System.out.println(nums.first()); // 输出-9
// 输出集合里的最后一个元素
System.out.println(nums.last()); // 输出10
// 返回小于4的子集,不包含4
System.out.println(nums.headSet(4)); // 输出[-9, 2]
// 返回大于5的子集,如果Set中包含5,子集中还包含5
System.out.println(nums.tailSet(5)); // 输出 [5, 10]
// 返回大于等于-3,小于4的子集。
System.out.println(nums.subSet(-3 , 4)); // 输出[2]
}
}
结果:
结果分析:
从上面的程序中可以看出,TreeSet不是根据元素的插入顺序来排序的,而是根据元素的实际大小来排序的。HashSet集合是根据hash算法来决定元素的存储位置不同,而TreeSet是根据红黑树的数据结构来存储集合元素。TreeSet集合支持两种排序规则,自然排序和定制排序,默认情况下TreeSet支持自然排序。
TreeSet的排序之自然排序
TreeSet 会调用集合元素的 compareTo(Object obj)
方法来比较元素之间的大小关系
,然后将集合元素按升序排列
, 这种方式就是自然排序。
当一个对象调用该方法与另一个对象进行比较时,例如 obj1.compareTo(obj2) ,如果该方法返回 0 ,则表明这两个对象相等
;如果该方法返回一个正整数 ,则表 明 obj1 大于obj2
;如果该方法返回一个负整数 ,则表明 obj1小于 obj2
。(要想实现自然排序,必须要实现Comparable接口,否则就会引发运行时异常-ClassCastException)
实例代码:
import java.util.TreeSet;
/**
* @author: 随风飘的云
* @date 2022/03/19 1:14
*/
class Pet{
}
public class TreeSetErrTest {
public static void main(String[] args) {
TreeSet set = new TreeSet();
// set.add(new Pet());(都会报错)
set.add(new Dog());
set.add(new Pet());
}
}
结果:
注意:
类在实现 compareTo(Object obj)方法时,都需要将被 比较对象 obj 强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小 。 否则也会报ClassCastException异常
实例代码:
import java.util.TreeSet;
/**
* @author: 随风飘的云
* @date 2022/03/19 1:14
*/
public class TreeSetErrTest {
public static void main(String[] args) {
TreeSet set = new TreeSet();
set.add(new String());
set.add(new int[]{
0});
}
}
结果:
TreeSet集合添加元素流程
1、当把一个对象加入 TreeSet 集合中时, TreeSet 调用该对象的 compareTo(Object obj)方法与容器中的其他对象比较大小。
2、根据红黑树结构找到它的存储位置 。 如果两个对象通过 compareTo(Object obj)方法比较相等,新对象将无法添加到 TreeSet 集合中 。
实例代码:
import java.util.TreeSet;
/**
* @author: 随风飘的云
* @date 2022/03/19 1:24
*/
class Z implements Comparable {
int age;
public Z(int age)
{
this.age = age;
}
// 重写equals()方法,总是返回true
public boolean equals(Object obj)
{
return true;
}
// 重写了compareTo(Object obj)方法,总是返回1
public int compareTo(Object obj)
{
return 1;
}
}
public class TreeSetTest2 {
public static void main(String[] args) {
TreeSet set = new TreeSet();
Z z1 = new Z(6);
set.add(z1);
// 第二次添加同一个对象,输出true,表明添加成功
System.out.println(set.add(z1)); //①
// 下面输出set集合,将看到有两个元素
System.out.println(set);
// 修改set集合的第一个元素的age变量
((Z)(set.first())).age = 9;
// 输出set集合的最后一个元素的age变量,将看到也变成了9
System.out.println(((Z)(set.last())).age);
}
}
结果:
注意:
如果两个对象通过 equalsO方法比较返回 true 时,这两个对象通过 compareTo(Object obj)方法比较应返回 0 。
TreeSet的排序之定制排序
实现定制排序,则需要在创建 TreeSet 集合对象时,提供一个 Comparator 对象与该 TreeSet集合关联,由该 Comparator 对象负责集合元素的排序逻辑 。 由于 Comparator 是一个函数式接口,因此可使用 Lambda 表达式来代替 Comparator 对象 。
实例代码:
import java.util.TreeSet;
/**
* @author: 随风飘的云
* @date 2022/03/19 1:31
*/
class M{
int age;
public M(int age) {
this.age = age;
}
public String toString() {
return "M [age:" + age + "]";
}
}
public class TreeSetTest4 {
public static void main(String[] args) {
// 此处Lambda表达式的目标类型是Comparator
// 重写了Comparator中的compare方法。
TreeSet ts = new TreeSet((o1 , o2) ->{
M m1 = (M)o1;
M m2 = (M)o2;
// 根据M对象的age属性来决定大小,age越大,M对象反而越小
return m1.age > m2.age ? -1
: m1.age < m2.age ? 1 : 0;
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
System.out.println(ts);
}
}
结果:
EnumSet类
EnumSet 是一个专为枚举类设计的集合类, EnumSet 中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建 EnumSet 时显式或隐式地指定。 EnumSet 的集合元素也是有序的,EnumSet 以枚举值在 Enum 类内的定义顺序来决定集合元素的顺序。
EnumSet 在内部以位向量的形式存储,因此占用的内存很小,批量操作的执行速度非常快。EnumSet 集合不允许加入 null 元素,如果EnumSet集合想判断是否包含null元素或者是试图删除null元素都不会报出异常,而且删除null操作会返回false。
EnumSet类提供的API方法
EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的 EnumSet 集合 。
EnumSet complementOf(EnumSet s): 创建一个其元素类型与指定 EnumSet 里元素类型相同的
EnumSet 集合,新 Enu日lSet 集合包含原 EnumSet 集合所不包含的、此枚举类剩下的枚举值(即新 EnumSet 集合和原 EnumSet 集合的集合元素加起来就是该枚举类的所有枚举值)。
EnumSet copyOf(Collection c): 使用 一个普通集合来创建 EnumSet 集合 。
EnumSet copyOf(EnumSet s): 创建一个与指定 EnumSet 具有相同元素类型、相同集合元素的EnumSet 集合 。
EnumSet noneOf(Class elementType): 创建一个元素类型为指定枚举类型的空 EnumSet 。
EnumSet of(E first, E... rest): 创建一个包含一个或多个枚举值 的 EnumSet 集合,传入的多个枚举值必须属于同一个枚举类。
EnumSet range(E from, E to): 创建一个包含从 from 枚举值到 to 枚举值范围 内所有枚举值的EnumSet 集合 。
实例代码:
import java.util.EnumSet;
/**
* @author: 随风飘的云
* @date 2022/03/19 1:42
*/
enum TimeClass{
YEAR,
MONTH,
DAY,
HOUR,
MINUTE,
SECOND;
}
public class EnumSetTest {
public static void main(String[] args) {
//创建一个 EnumSet 集合 , 集合元素就是 Season 枚举类的全部枚举值
EnumSet set = EnumSet.allOf(TimeClass.class);
System.out.println(set);
// 创建空集合,指定其集合元素是 Season 类的枚举值
EnumSet set1 = EnumSet.noneOf(TimeClass.class);
System.out.println(set1);
set1.add(TimeClass.YEAR);
set1.add(TimeClass.MONTH);
set1.add(TimeClass.DAY);
System.out.println(set1);
//以指定枚举值创建 EnumSet 集合
EnumSet set2 = EnumSet.of(TimeClass.YEAR, TimeClass.MONTH, TimeClass.DAY);
System.out.println(set2);
// 创建一个包含从 from 枚举值到 to 枚举值范围 内所有枚举值的EnumSet 集合 。
EnumSet set3 = EnumSet.range(TimeClass.YEAR, TimeClass.SECOND);
System.out.println(set3);
// set2+set4 = 枚举类的所有元素
EnumSet set4 = EnumSet.complementOf(set2);
System.out.println(set4);
}
}
结果:
各种Set集合性能分析
HashSet 和 TreeSet 是 Set 的两个典型实现。 HashSet 的性能总是比 TreeSet 好(特别是最常用的添加 、 查询元素等操作) , 因为 TreeSet 需要额外的红黑树算法来维护集合元素的次序
。 只有当需要一个保持排序的 Set
时,才应该使用 TreeSet,否则都应该使用 HashSet 。HashSet 还有一个子类 : LinkedHashSet ,对于普通的插入、删除操作 , LinkedHashSet 比 HashSet要略微慢一点
, 这是由维护链表所带来的额外开销
造成的 , 但由于有了链表,遍历 LinkedHashSet 会更快 。
EnumSet 是所有 Set 实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。必须指出的是,Set 的三个实现类 HashSet 、 TreeSet 和 EnumSet 都是线程不安全
的 。如果有多个线程同时访问 一个 Set 集合,并且有超过一个线程修改了该 Set 集合 ,则必须手动保证 Set 集合的同步性
。通常可 以通过 Collections 工具类的 syncbronizedSortedSet 方法来 "包装"该 Set 集合
。 此操作最好在创建时进行 , 以防止对 Set 集合的意外非同步访问 。