「Java基础」你没看错,HashSet就是这么简单!

Set概述

Set也是集合中的一个容器,类似于一个罐子,程序可以依次把若干个对象放进Set,但是Set无法保存元素添加的顺序,Set不允许包含相同的元素,如果把两个相同的元素加入同一个Set中,则添加失败,add()方法会返回false,并且新元素不会被添加。Set接口主要有两个实现类HashSet、TreeSet。

HashSet

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet 按 Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。HashSet 具有以下特点。

11.2.1 HashSet常用API

方法

描述

add(Object o)

添加元素

remove(Object o)

移除元素

isEmpty()

判断元素是否为空

size()

获取set中元素个数

下面通过示例:

package cn.bytecollege;

import java.util.HashSet;
import java.util.Set;

public class HashSetDemo {
	public static void main(String[] args) {
		Set set = new HashSet<>();
		//添加元素
		set.add("张无忌");
		//移除元素
		set.remove("张无忌");
		//此时set中没有元素
		System.out.println(set.isEmpty());
	}
}

HashSet遍历

  1. 因为HashSet中的元素没有索引,所以不支持for循环直接遍历,但是可以使用foreach进行遍历
package cn.bytecollege;

import java.util.HashSet;
import java.util.Set;
/**
 * 本例将演示使用foreach遍历Set
 * @author MR.W
 *
 */
public class HashSetForDemo {
	public static void main(String[] args) {
		Set set = new HashSet<>();
		
		set.add("乔峰");
		set.add("虚竹");
		set.add("段誉");
		
		for (String name : set) {
			System.out.println(name);
		}
		
	}
}
  1. HashSet继承了Iterable接口,因此HashSet还可以使用迭代器遍历。
package cn.bytecollege;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class HashSetIteDemo {
	public static void main(String[] args) {
		Set set = new HashSet<>();
		
		set.add("乔峰");
		set.add("虚竹");
		set.add("段誉");
		
		Iterator it = set.iterator();
		while(it.hasNext()) {
			System.out.println(it.next());
		}
		
		for (Iterator iterator = set.iterator(); iterator.hasNext();) {
			System.out.println(iterator.next());
		}
		
	}
}

重写hashCode

HashSet 中每个能存储元素的"槽位"(slot)通常称为"桶"(bucket),如果有多个元素的 hashCode值相同,但它们通过equals()方法比较返回 false,就需要在一个"桶"里放多个元素,这样会导致性能下降。

下面给出重写 hashCode() 方法的基本规则。

下面给出重写hashCode() 方法的一般步骤。

  1. 把对象内每个有意义的实例变量(即每个参与 equals方法比较标准的实例变量)计算出一个 int 类型的 hashCode 值,计算方式如下:

实例变量类型

计算方式

实例变量类型

计算方式

boolean

hashCode=(f?1:0);

float

hashCode=Float.floatToIntBits(f)

整型

hashCode=(int)f;

double

long l = Double.doubleToLongBits(f);

hashCode = (int)(l^l>>>32);

long

hashCode=(int)(f^(f>>>32))

引用类型

hashCode = f.hashCode();

  1. 用第1步计算出来的多个hashCode 值组合计算出一个 hashCode 值返回。
return f1.hashCode() + (int)f2;

为了避免直接相加产生偶然相等 (两个对象的 f、f 实例变量并不相等,但它们的 hashCode 的和恰好相等),可以通过各实例变量的 hashCode 值乘以任意一个质数后再相加。例如如下代码;

return f1.hashCode()*19 + (int)f2*31;

HashSet使用场景

根据HashSet的特点可以知道HashSet中存放的元素不能重复,利用这个特性就可以做一些去重的业务,例如在给定字符串HelloWorld中统计出现的字符。

package cn.bytecollege;

import java.util.HashSet;
import java.util.Set;

public class HashSetDemo {
	public static void main(String[] args) {
		
		String str = "HelloWorld";
		//将字符串转换成字符数组
		char[] ch = str.toCharArray();
		
		Set set = new HashSet<>();
		//遍历字符数组将所有元素添加进set去重
		for (Character c : ch) {
			set.add(c);
		}
		//遍历set
		for (Character c : set) {
			System.out.println(c);
		}
	}
}

LinkedHashSet

HashSet有一个子类LinkedHashSet,LinkedHashSet集合同HashSet一样,也是根据元素的hashCode值来确定元素的存储位置,并且通过链表维护元素的顺序,换句话说,遍历LinkedHashSet时,LinkedHashSet将会按照元素的添加顺序来访问集合里的元素。

由于LinkedHash需要维护元素插入的顺序,因此性能略低于HashSet。下面通过示例学习LinkedHashSet

package cn.bytecollege;

import java.util.LinkedHashSet;
import java.util.Set;

/**
 * 本例将演示LinkedHashSet
 * @author MR.W
 *
 */
public class LinkedHashSetDemo {
	public static void main(String[] args) {
		Set set = new LinkedHashSet<>();
		set.add("乔峰");
		set.add("虚竹");
		set.add("段誉");
		for (String name : set) {
			System.out.println(name);
		}
	}
}

从示例代码以及HashSet的遍历我们可以看出LinkedHashSet维护了添加顺序,也就是说添加进容器的元素和取出的元素顺序是一致的。

如果在某些场景下要使用set容器保存元素,并且需要维护set中的存放元素的顺序,那么就可以使用LinkedHashSet

TreeSet

TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。

TreeSet常用API

与 HashSet 集合相比,TreeSet 还提供了如下几个额外的方法。

下面通过示例学习TreeSet的常用方法:

package cn.bytecollege;

import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        treeSet.add(10);
        treeSet.add(20);
        treeSet.add(30);
        treeSet.add(40);
        treeSet.add(50);
        //如果 TreeSet 采用了定制排序,则该方法返回定制排序所使用的Comparator;如果 TreeSet 采用了自然排序,则返回 null。
        System.out.println(treeSet.comparator());
        //返回集合中的第一个元素。
        System.out.println(treeSet.first());
        //返回集合中的最后一个元素。
        System.out.println(treeSet.last());
        //返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是 TreeSet集合里的元素)。
        System.out.println(treeSet.lower(25));
        //返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是 TreeSet 集合里的元素)
        System.out.println(treeSet.higher(25));
        //返回此Set 的子集合,范围从fromElement(包含)到 toElement (不包含)。
        System.out.println(treeSet.subSet(20,50));
        //返回此 Set 的子集,由小于 toElement 的元素组成
        System.out.println(treeSet.headSet(30));
        //返回此 Set 的子集,由大于或等于 fromElement 的元素组成。
        System.out.println(treeSet.tailSet(30));
    }
}

根据 上面程序的运行结果即可看出,TreeSet 并不是根据元素的插入顺序进行排序的。而是根据元素实际值的大小来进行排序的。

与 HashSet 集合采用 hash 算法来决定元素的存储位置不同,TreeSet 采用红黑树的数据结构来存储集合元素。那么 TreeSet 进行排序的规则是怎样的呢? TreeSet 支持两种排序方法∶自然排序和定制排序。在默认情况下,TreeSet 采用自然排序。

11.4.2 自然排序

TreeSet 会调用集合元素的 compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。

Java 提供了一个 Comparable接口,该接口里定义了一个 compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如 obj1.compareTo(obj2),如果该方法返回 0,则表明这两个对象相等;如果该方法返回一个正整数,则表明 obj1 大于 obj2;如果该方法返回一个负整数,则表明 obj1小于obj2。

下面通过示例学习该接口,首先定义Student类,类中包含name和age属性:

package cn.bytecollege;
public class Student {
	private String name;
	private int age;
	public Student(String name) {
		super();
		this.name = name;
	}
}
package cn.bytecollege;

import java.util.Set;
import java.util.TreeSet;

public class HashSetDemo {
	public static void main(String[] args) {
		Set set = new TreeSet<>();
		Student s1 = new Student("张三",18);
		Student s2 = new Student("李四",18);
		
		set.add(s1);
		set.add(s2);
	}
}

当运行以上程序时,发现程序抛出了异常:

这是因为Student没有继承Comparable接口,如果把一个对象添加到TreeSet时,则该对象的类必须继承Comparable接口,否则程序将会抛出异常。

下面的程序将演示正确的添加方式:

package cn.bytecollege;
public class Student implements Comparable{
	private String name;
	private int age;
	public Student(String name,int age) {
		super();
		this.name = name;
		this.age = age;
	}
	@Override
	public int compareTo(Student o) {
		return this.age-o.age;
	}
}
package cn.bytecollege;

import java.util.Set;
import java.util.TreeSet;

public class HashSetDemo {
	public static void main(String[] args) {
		Set set = new TreeSet<>();
		Student s1 = new Student("张三",18);
		Student s2 = new Student("李四",19);
		
		set.add(s1);
		set.add(s2);
	}
}

Student类继承Comparable接口并重写compareTo()方法后再次向TreeSet中添加,可以顺利添加,并且可以按照age属性的大小进行排序。

当把一个对象加入 TreeSet 集合中时,TreeSet 调用该对象的 compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置。如果两个对象通过 compareTo(Object obj)方法比较相等,新对象将无法添加到 TreeSet 集合中。

对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是∶两个对象通过compareTo(Object obj)方法比较是否返回 0——如果通过 compareTo(Object obj)方法比较返回 0,TreeSet 则会认为它们相等;否则就认为它们不相等。

定制排序

TreeSet 的自然排序是根据集合元素的大小,TreeSet 将它们以升序排列。如果需要实现定制排序,例如以降序排列,则可以通过 Comparator 接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,该方法用于比较 o1 和 o2 的大小∶ 如果该方法返回正整数,则表明o1 大于o2;如果该方法返回0,则表明 o1等于 o2;如果该方法返回负整数,则表明 o1小于 o2。

如果需要实现定制排序,则需要在创建 TreeSet 集合对象时,提供一个 Comparator 对象与该TreeSet集合关联,由该 Comparator 对象负责集合元素的排序逻辑。

下面通过示例学习Comparator的用法,继续使用上例中的Student类:

package cn.bytecollege;
public class Student{
	private String name;
	private int age;
	public Student(String name,int age) {
		super();
		this.name = name;
		this.age = age;
	}
}
package cn.bytecollege;

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public class ComparatorDemo {
	public static void main(String[] args) {
		Set set = new TreeSet(new Comparator() {
			@Override
			public int compare(Student o1, Student o2) {
				return o1.age-o2.age;
			}
		});
		
		Student s1 = new Student("张三",18);
		Student s2 = new Student("李四",19);
		Student s3 = new Student("王五",20);
		
	}
}

各个Set性能分析

HashSet 和 TreeSet 是 Set 的两个典型实现,到底如何选择 HashSet 和 TreeSet 呢?HashSet 的性能总是比 TreeSet 好(特别是最常用的添加、查询元素等操作),因为 TreeSet 需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的 Set 时,才应该使用 TreeSet,否则都应该使用 HashSet。

HashSet 还有一个子类∶LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet 比 HashSet要略微慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历 LinkedHashSet 会更快。

展开阅读全文

页面更新:2024-06-06

标签:遍历   示例   变量   顺序   实例   元素   接口   对象   两个   基础   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top