【老白学 Java】HashSet 应用 - 卡拉 OK(五)

HashSet 应用 - 卡拉 OK(五)

文章来源:《Head First Java》修炼感悟。

通过前几篇文章,老白也了解了基本排序方法,接下来要讨论的是数据重复问题。 ArrayList 不会阻止添加重复数据,那是 Set 集合类的职责。 本篇文章我们就来了解一下 Set 集合相关特性。

一、两个对象怎样才算相等

假如有两个引用变量,那么怎样才算相等呢? 是引用到了堆上的同一个对象,还是两个对象都有相同成员? 下面分别说说两种情况:

1、引用相等

这种表示引用了堆上同一个对象,如果对两个引用调用 hashCode() 方法,必定会返回相同的 hashcode(堆上的每个对象都是唯一的),而此时使用 == 对两个引用变量进行比较肯定也是相等的,因为是同一个对象的字节组合。 例如:

java 复制代码
if (obj1.hashCode() == obj2.hashCode() && obj1.equals(obje2)) {
	// 认为 obj1 与 obj2 相等
}

2、对象相等

这表示堆上的两个对象具有相同成员,从感官上认为相等。 由于 hashcode 的唯一性,这两个对象的 hashcode 并不相等,所以如果想让它们相等就必须覆盖掉从原始对象 Object 继承到的 hashCode()equals() 方法,重新实现让它们返回相同的 hashcodetrue。 例如覆盖 Song 类方法:

java 复制代码
// 覆盖父类方法,传入的歌曲对象名与当前歌名进行比较
// 比较对象是 String 类型,String 已经覆盖过此方法,所以可以和直接调用
public boolean equals(Object song) {
	Song s = (Song) song;
	return this.getTitle().equals(s.getTitle());
}
// 覆盖父类方法,注意使用了同一个实例变量 title
// 同样的,String 也覆盖过 hashCode() 方法,可以直接调用
public int hashCode() {
	return title.hashCode();
}

经过以上处理后,即歌名相同就可以认为两个 Song 对象重复。

二、HashSet 如何判断对象是否重复

元素加入 HashSet 前,需要检查集合中是否存在相同的 hashcode 元素,如果没有则认为没有重复,可以直接加入;如果存在相同的 hashcode,还要进一步使用 equals() 检查是否真的相等,以此来确定是否重复加入对象。

所以,如果你希望曲目文件中不会出现相同歌名,就要覆盖掉 hashCode()equals() 方法,让 Java 认为不相同的两个 Song 对象变为相同的,从而只会添加一个到 HashSet 中。

1、更新后的曲目清单

text 复制代码
Pink Moon/Nick Drake/5/80
Somersault/Zero 7/4/84
Shiva Moon/Prem Joshua/6/120
Circles/BT/5/110
Deep Channel/Afro Celts/4/120
Passenger/Headmix/4/100
Listen/Tahiti 80/5/90
Listen/Tahiti 80/5/90
Listen/Tahiti 80/5/90
Circles/BT/5/110

可以看到,这份清单中有些重复元素,接下来准备去掉这些重复曲目。

2、修改 Song 类代码

java 复制代码
/**
 * 文件:Song.java
 *
 * 描述:歌曲信息类,用于保存歌曲名称、歌手等信息。
 * 版本:v6.0
 */
public class Song implements Comparable<Song> {

	// 定义歌曲信息
	String title;
	String artist;
	String rating;
	String bpm;

	// 默认构造方法
	public Song() {}

	// 构造器,新建对象时传入歌曲信息
	public Song(String t, String a, String r, String b) {
		title = t;
		artist = a;
		rating = r;
		bpm = b;
	}

	// 覆盖父类方法,传入的歌曲对象名与当前歌名进行比较
	// 比较对象是 String 类型,String 已经覆盖过此方法,所以可以和直接调用
	public boolean equals(Object song) {
		Song s = (Song) song;
		return this.getTitle().equals(s.getTitle());
	}

	// 覆盖父类方法,注意使用了同一个实例变量 title
	// 同样的,String 也覆盖过 hashCode() 方法,可以直接调用
	public int hashCode() {
		return title.hashCode();
	}

	// 实现接口方法
	public int compareTo(Song s) {
		return title.compareTo(s.getTitle());
	}
	
	// 返回歌曲名称
	public String getTitle() {
		return title;
	}

	// 返回歌手名称
	public String getArtist() {
		return artist;
	}

	// 返回歌曲等级
	public String getRating() {
		return rating;
	}

	// 返回歌曲节拍信息
	public String getBpm() {
		return bpm;
	}

	// 重写方法,返回歌曲标题
	public String toString() {
		return title;
	}
}

Song 类文件覆盖了 hashCode()equals() 方法,强制把歌名相同的 Song 当做重复元素,不会被加入 HashSet 中。

3、修改 Karaoke 类代码:

java 复制代码
/**
 * 文件:Karaoke6.java
 * 
 * 描述:模拟 KTV 曲目清单,学习使用集合排序。
 * 		使用 HashSet 替换 ArrayList,确保曲目清单中不会出现重复歌曲。
 * 版本:v6.0
 */
import java.io.*;
import java.util.*;

public class Karaoke6 {

	/**
	 * 用于对歌手名字进行比较的内部类,实现了Comparator接口
	 */
	class ArtistCompare implements Comparator<Song> {

		// 对传入的Song对象的歌手名字的字符串进行比较
		// 并返回一个整数值给 Collections 的比较方法
		public int compare(Song one, Song two) {
			return one.getArtist().compareTo(two.getArtist());
		}
	}

	// 用来保存所有曲目的列表
	ArrayList<Song> tracks = new ArrayList<Song>();

	// 执行入口
	public void go() {
		loadSongs();
		// 原始顺序
		System.out.println("original: " + tracks);

		// 按曲目排序
		// Collections.sort(tracks);
		// System.out.println("by title: " + tracks);
		
		// 使用了 HashSet,把曲目清单全部添加到 Set 集合中
		HashSet<Song> songSet = new HashSet<Song>();
		songSet.addAll(tracks);
		System.out.println("using Set: " + songSet);

		// 按歌手名字排序
		// ArtistCompare ac = new ArtistCompare();
		// Collections.sort(tracks, ac);
		// System.out.println("by artist: " + tracks);
	}

	// 载入曲目文件
	private void loadSongs() {
		try {
			// 先不理会下面语句的含义,
			// 只需知道能读取 songs.txt 文件内容就可以了
			File file = new File("songs.txt");
			BufferedReader reader = new BufferedReader(new FileReader(file));

			String line = null;
			while ((line = reader.readLine()) != null) {
				addSong(line);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	// 解析曲目
	private void  addSong(String token) {
		String[] tokens = token.split("/");
		Song s = new Song(tokens[0], tokens[1], tokens[2], tokens[3]);
		tracks.add(s);
	}

	// 程序入口
	public static void main(String[] args) {
		new Karaoke6().go();
	}
}

代码使用 HashSet 替代 ArrayList。

4、编译执行

可以看到,CirclesListen 的重复元素已经被去除。 目前代码还没有经过排序,这是按原始顺序输出的结果。

三、关于 hashCode 和 equals 的绕口令

  • 相同 hashcode 值的两个对象并不一定相等;
  • 如果两个对象相等,则 hashcode 必定也相等;
  • 如果两个对象相等,其一使用 equals() 必定返回 true,反之亦然;
  • 如果 equals() 被覆盖,则 hashCode() 也必须覆盖;
  • hashcode 是唯一的,如果没有覆盖 hashCode() 方法,两个对象无论如何也不会认为是相等的;
  • equals() 用于测试两个引用变量是否引用同一个对象,如果没有覆盖这个方法,那么两个对象永远不会认为是相等的;
  • 如果 equals()true,那么 hashCode() 必定相等;而如果 hashCode() 相等则不一定 equals()true

结束语

从下一篇开始,老白会使用另一种集合类进行无重复排序,请保持关注。


《 上一篇 泛型应用 - 卡拉 OK(四)
相关推荐
不会打代码呜呜呜呜8 分钟前
小白零基础--CPP多线程
开发语言·c++·算法
程柯梦想9 分钟前
Maven修改默认编码格式UTF-8
java·maven
涛ing9 分钟前
【5. C++ 变量作用域及其深入探讨】
java·linux·c语言·开发语言·c++·ubuntu·vim
Hi Man38 分钟前
Python之如何在Visual Studio Code 中写的python程序打包成可以在Windows系统下运行的.exe程序
开发语言·vscode·python
CHANG_THE_WORLD1 小时前
C++并发编程指南04
开发语言·c++
字节全栈_mMD1 小时前
Flink Connector 写入 Iceberg 流程源码解析_confluent icebergsinkconnector
java·大数据·flink
powershell 与 api1 小时前
C#,shell32 + 调用控制面板项(.Cpl)实现“新建快捷方式对话框”(全网首发)
开发语言·windows·c#·.net
SomeB1oody1 小时前
【Rust自学】19.2. 高级trait:关联类型、默认泛型参数和运算符重载、完全限定语法、supertrait和newtype
开发语言·后端·rust
小园子的小菜1 小时前
RocketMQ中的NameServer主要数据结构
java·中间件·rocketmq·java-rocketmq
平凡君2 小时前
缓存的今生今世
java·spring·缓存