【老白学 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(四)
相关推荐
lsx2024064 分钟前
SQL MID()
开发语言
Dream_Snowar7 分钟前
速通Python 第四节——函数
开发语言·python·算法
西猫雷婶9 分钟前
python学opencv|读取图像(十四)BGR图像和HSV图像通道拆分
开发语言·python·opencv
鸿蒙自习室9 分钟前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
星河梦瑾9 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富13 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想14 分钟前
JMeter 使用详解
java·jmeter
言、雲17 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
TT哇24 分钟前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
汪洪墩1 小时前
【Mars3d】设置backgroundImage、map.scene.skyBox、backgroundImage来回切换
开发语言·javascript·python·ecmascript·webgl·cesium