Java 容器类(二)——Set

一、Set 接口

Java 用 Set 接口来描述集合(集合不允许有重复值)。

Set 不保存重复的元素。Set 中最常被使用的是测试归属性,可以很容易地查询某个对象是否存在于某个 Set 中。因此,Set 最重要的操作就是查找。通常选择 Set 的 HashSet 实现,因为 HashSet 对快速查找进行了优化。

Set 接口继承自 Collection 接口,实际上是与 Collection 完全一样的接口,只是行为不同。因此,Set 并没有额外的功能。

Set 中,元素是无序的且不可重复。除此之外,Set 与 List 大致相同,方法基本一致。

二、Set 的分类

  • HashSet(散列集)——使用散列函数存储元素,因此元素是无序的,但查找速度快;

  • TreeSet(树集)——将元素存储在红黑树数据结构中,并且能够以某种规则对元素进行排序;

  • LinkedHashSet——也使用散列函数存储元素保证较快的查找速度,但内部使用链表维护元素的顺序(插入的次序)。

三、HashSet 测试

1、集合中的有序、无序以及排序的概念区分

Java 集合中,有序、无序指的是在进行插入操作时,插入位置的顺序性。

先插入的位置在前,后插入的位置在后,则为有序,反之为无序。也就是说,插入数据时和取出数据时的顺序是一样的,即为有序。

有序和排序容易混淆,排序是指集合内的元素是按照升序或降序来排列的。

2、关于 HashSet 无序的测试

代码一:

public static void main(String[] args) {
	Set set = new HashSet<>();
	set.add(1);
	set.add(2);
	set.add(3);
	System.out.println(set);
  }

测试结果:

[1, 2, 3]

代码二:

public static void main(String[] args) {
	Set set = new HashSet<>();
	set.add(9);
	set.add(5);
	set.add(558);
	set.add(7);
	System.out.println(set);
}

测试结果:

[5, 7, 9, 558]

对比上面两段代码,代码一是有序的(巧合),代码二是无序的,并且两个结果都是排好序的结果。

解释: 一般来说,HashSet 是无序的,它既不能保证存储和取出的顺序一致,也不能保证自然顺序(排序)。

分析:

上面两段代码中,所创建的 HashSet 是 Integer 类型的,这也是最巧的一点。因为 HashSet 在存储元素的时候,将元素用 hashCode() 运算后得到自己在数组中的位置,而 Integer 类型 hashCode() 的返回值就是其 int 值本身,所以在这一步其实已经实现了排序功能。

这是因为,JDK7 到 JDK8,其内部发生了一些变化,不同版本的 JDK 下运行结果不同。按照 JDK8 的 hash 算法,数据在经过 HashMap.hash() 运算后仍然是自己本身的值,且没有发生哈希冲突。

对于有序、无序,是说元素存入集合的顺序。若元素存储顺序和取出顺序一致,就是有序,否则就是无序。因此,在使用 for 循环添加元素时,无论是随机添加还是有序添加,遍历的结果都是按照值的大小排序输出。

代码二中随机添加元素,存储和取出的顺序不一样,所以我们说 HashSet 是无序的。代码一的情况只是一种巧合。

所以说,HashSet 只是不保证有序,并不是保证无序。

3、HashSet 存储自定义类

HashSet 存储 int、String 等基本类型的数据时,使用默认的 hashCode() 和 equals() 方法对数据进行计算后存储。而存储自定义类时,需要重写 hashCode() 和 equals() 方法,用自定义的规则去除掉重复的元素。示例如下:

// 球员类
class Player{
	String name;// 姓名
	int number;// 号码
	Player(String name, int number){
		this.name = name;
		this.number = number;
	}
	
	@Override
	// 重写hashCode()
	public int hashCode(){
		return name.hashCode() + number;
	}
	
	@Override
	// 重写equals()
	public boolean equals(Object obj){
		// 若equals()返回true,则表示插入元素的内容与集合中某元素相同,就不再插入这个元素了
		return this.name.equals(((Player)obj).name) &&
				this.number == ((Player)obj).number;
	}
}

public class SetTest {
	
	public static void main(String[] args) {
		Set<Player> set = new HashSet<>();
		set.add(new Player("kobe", 24));
		set.add(new Player("curry", 30));
		set.add(new Player("jordan", 23));
		set.add(new Player("kobe", 24));
		Iterator<Player> it = set.iterator();
		while(it.hasNext()){
			Player p = (Player)it.next();
			System.out.println(p.name + ", " + p.hashCode() + ", " + p.number);
		}
	}

}

测试结果:

jordan, -1154271045, 23
kobe, 3297471, 24
curry, 95027365, 30

四、TreeSet 测试

  • TreeSet 虽然是有序的,但是并没有具体的索引,所以在插入数据时,TreeSet 中原有的数据可能需要重新排序,导致 TreeSet 插入和删除的效率低;
  • 并且由于没有具体的索引,TreeSet 没有 get() 方法来获取指定位置的元素( HashSet 也没有),所以它们只能通过迭代器来获取指定位置的元素;
  • TreeSet 是一个有序集合,其元素按照升序排列,默认是按照自然顺序排列,也就是说 TreeSet 中的对象元素需要实现 Comparable 接口来实现自比较功能;
  • 使用 TreeSet 存储自定义类的对象时,自定义类要实现 Comparable 接口并重写 compareTo 方法,以提供比对形式,TreeSet 则会根据自定义的比对方式对元素进行排序。

1、TreeSet 存储 Java 已有的类——自然排序

存储 Java 已有的类对象时,如 Integer、String 等,使用默认的自然排序方式。代码如下:

Set<Integer> set = new TreeSet<>();
set.add(5);
set.add(2);
set.add(78);
set.add(6);
for(Integer i : set){
	System.out.print(i + " ");
}

测试结果:

2 5 6 78 

2、TreeSet 存储自定义类——使用 Comparable 接口

TreeSet 存储自定义类时,自定义类要实现 Comparable 接口并重写 compareTo() 方法,在 compareTo() 方法中定义比较规则,决定排序的方式,这种排序方式称为内排序。测试如下:

/*
 * 学生类,姓名和成绩两个属性;
 * 重写hashcode()方法和equals()方法,去除重复元素;
 * 实现Comparable接口,重写compareTo()方法,定义排序规则。
*/ 
class Student implements Comparable<Student>{
	private String name;
	private int score;
	
	public Student(){ super();}
	
	public Student(String name, int score){
		super();
		this.name = name;
		this.score = score;
	}

	public String getName() { return name; }

	public void setName(String name) { this.name = name; }

	public int getScore() { return score; }

	public void setScore(int score) { this.score = score; }
	
	@Override
	// 重写hashCode()
	public int hashCode(){
		return name.hashCode();
	}
	
	@Override
	// 重写equals()
	public boolean equals(Object obj){
		// 若equals()返回true,则表示插入元素的内容与集合中某元素相同,就不再插入这个元素了
		return this.name.equals(((Student)obj).name) &&
				this.score == ((Student)obj).score;
	}
	
	public String toString(){
		return "[" + name + ", " + score + "]";
	}

	@Override
	public int compareTo(Student o) {
		// 利用String类的compareTo()方法进行比较
		// 用成绩进行排序
		return String.valueOf(this.score).compareTo(String.valueOf(o.score));
	}
	
}

public class TreeSetTest1 {
	
	public static void main(String[] args) {
		TreeSet<Student> set = new TreeSet<>();
		set.add(new Student("张三", 98));
		set.add(new Student("李四", 95));
		set.add(new Student("王麻子", 97));
		set.add(new Student("王麻子", 97));
		System.out.println(set);
	}

}

测试结果:

[[李四, 95], [王麻子, 97], [张三, 98]]

由结果看出,TreeSet 实现了自定义的按成绩排序的功能,并且在添加元素过程中滤除了重复元素。

3、TreeSet 存储自定义类——使用 Comparator 接口

TreeSet 存储自定义类时,也可以自定义一个外比较器,实现 Comparator 接口并重写 compare() 方法,这种排序方式称为外排序。测试如下:

// 学生类,重写hashcode()方法和equals()方法,去除重复元素
class Student{
	private String name;
	private int score;
	
	public Student(){ super();}
	
	public Student(String name, int score){
		super();
		this.name = name;
		this.score = score;
	}

	public String getName() { return name; }

	public void setName(String name) { this.name = name; }

	public int getScore() { return score; }

	public void setScore(int score) { this.score = score; }
	
	@Override
	// 重写hashCode()
	public int hashCode(){
		return name.hashCode();
	}
	
	@Override
	// 重写equals()
	public boolean equals(Object obj){
		// 若equals()返回true,则表示插入元素的内容与集合中某元素相同,就不再插入这个元素了
		return this.name.equals(((Student)obj).name) &&
				this.score == ((Student)obj).score;
	}
	
	public String toString(){
		return "[" + name + ", " + score + "]";
	}
	
}

// 自定义比较器,定义排序规则
class Student_Comparator implements Comparator<Student>{

	@Override
	public int compare(Student o1, Student o2) {
		// 先比较成绩,如果成绩相同,再比较姓名
		if(o1.getScore() == o2.getScore()){
			// 调用String类的compareTo()方法
			return o1.getName().compareTo(o2.getName());
		}else{
			return o1.getScore() - o2.getScore();
		}
	}
	
}

public class TreeSetTest2 {
	
	public static void main(String[] args) {
		// 创建Set时,要将比较器传给容器对象
		TreeSet<Student> set = new TreeSet<>(new Student_Comparator());
		set.add(new Student("d", 98));
		set.add(new Student("b", 95));
		set.add(new Student("e", 97));
		set.add(new Student("e", 97));
		set.add(new Student("f", 97));
		System.out.println(set);
	}

}

测试结果:

[[b, 95], [e, 97], [f, 97], [d, 98]]

由结果看出,TreeSet 实现了自定义的先成绩再姓名的排序功能,并且在添加元素过程中滤除了重复元素。

五、Set 的遍历

Set 没有 get() 方法,不能用 for 循环。

方式一:foreach 循环

Set<Integer> set = new TreeSet<>();
set.add(5);
set.add(2);
set.add(78);
set.add(6);
for(Integer i : set){
	System.out.print(i + " ");
}

方式二:迭代器

上面的用 HashSet 存储自定义的 Player 类对象,就是使用迭代器进行遍历。如下:

Set<Player> set = new HashSet<>();
set.add(new Player("kobe", 24));
set.add(new Player("curry", 30));
set.add(new Player("jordan", 23));
set.add(new Player("kobe", 24));
Iterator<Player> it = set.iterator();
while(it.hasNext()){
	Player p = (Player)it.next();
	System.out.println(p.name + ", " + p.hashCode() + ", " + p.number);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值