• Java开发笔记(六十五)集合:HashSet和TreeSet


    对于相同类型的一组数据,虽然Java已经提供了数组加以表达,但是数组的结构实在太简单了,第一它无法直接添加新元素,第二它只能按照线性排列,故而数组用于基本的操作倒还凑合,若要用于复杂的处理就无法胜任了。为此Java设计了一大类的数据类型名叫容器,它们仿佛容纳物品的器皿一般,可大可小,既能随时往里塞入新物件,又能随时从中取出某物件。当然,依据不同的用途,容器也分为好几类,包括集合Set、映射Map、清单List等等,本文先从最基础的集合开始介绍。
    所谓集合,指的是一群同类聚集在一起,集合的最大特点就是里面的每个事物都是唯一的,即使重复加入也只算同一个元素。Java给集合分配的类型名称叫做“Set”,在使用之时还得在Set后面补充一对尖括号,里面填集合内部元素的数据类型。比如一个字符串集合,它的完整类型写法为“Set<String>”,下面便是声明字符串集合变量的代码例子:

    		Set<String> set;
    

    可是由于Set实际上属于接口,因此不能直接用来创建集合实例,在编程开发中,往往使用Set的两个实现类HashSet和TreeSet。
    HashSet的大名叫哈希集合,其内部采取哈希表来存储数据;而TreeSet的大名叫做二叉集合,其内部采取二叉树来存储数据。尽管HashSet与TreeSet二者的存储结构不同,但它们在编码调用时大体类似,所以接下来就以HashSet为例,概要描述集合的基本用法。
    一开始使用集合,当然也要先创建该集合的实例。创建集合实例的方式跟创建一个类的实例相同,都得调用它们的构造方法,集合实例的具体创建代码如下所示:

    		HashSet<String> set = new HashSet<String>();
    

    有了集合实例,再通过实例去调用具体的集合方法,以下是常用的集合方法说明:
    add:把指定元素添加到集合。
    remove:从集合中删除指定元素。
    contains:判断集合是否包含指定元素。
    clear:清空集合。
    isEmpty:判断集合是否为空。
    size:获取集合的大小(即所包含元素的个数)。
    从以上说明可见,这些集合方法还是蛮基础的,不但基础而且通用,不光是集合会用到这些方法,连映射和清单也要用到它们,因而后面在介绍映射和清单之时,就不再重复说明上述的基本方法了。
    接着来个集合的初步运用,功能很简单,仅仅往字符串集合添加五个字符串,然后获取并打印该集合的大小,示例代码如下:

    		HashSet<String> set = new HashSet<String>();
    		set.add("hello");
    		set.add("world");
    		set.add("how");
    		set.add("are");
    		set.add("you");
    		System.out.println("set.size()=" + set.size());
    

    运行上面的测试代码,可得日志结果为“set.size()=5”。不过这里只获得集合大小,若想知晓集合内部到底有哪些字符串,还得依次遍历该集合的所有元素才行。集合元素的遍历方式主要有三种:for循环遍历、迭代器遍历、forEach遍历,接下来分别进行介绍。

    1、for循环遍历
    这个for循环属于简化的for循环,早在遍历数组元素的时候,大家已经见识过了。废话不必多说,直接看下列代码好了:

    		// 第一种遍历方式:简化的for循环同样适用于数组和容器
    		for (String hash_item : set) {
    			System.out.println("hash_item=" + hash_item);
    		}
    

    运行以上的循环代码,输出了如下的日志信息,可见该集合的五个元素全都找到了:

    hash_item=how
    hash_item=world
    hash_item=are
    hash_item=hello
    hash_item=you
    

      

    2、迭代器遍历
    迭代器又称指示器,其作用类似于数据库的游标,以及C语言的指针。调用集合实例的iterator方法即可获得该集合的迭代器,初始的迭代器指向集合的存储地址;它的hasNext方法用来判断后方是否存在集合元素,倘若不存在则表示到末尾了;迭代器另有next方法用于获取下一个元素,同时迭代器移动到下一个地址。于是多次调用集合实例的next方法,即可逐次取出该集合的每个元素。下面是利用迭代器遍历集合的代码例子:

    		// 第二种遍历方式:利用迭代器循环遍历集合。
    		Iterator<String> iterator = set.iterator();
    		while (iterator.hasNext()) { // 迭代器后方是否存在元素
    			// 获取迭代器后方的元素
    			String hash_iterator = (String) iterator.next();
    			System.out.println("hash_iterator=" + hash_iterator);
    		}
    

      

    3、forEach遍历
    forEach是Java8新增的容器遍历方法,同样适用于映射和清单。它借助Lambda表达式能够完成最简化的遍历操作,仅仅一行代码就搞定了集合元素的循环输出功能,具体实现代码如下所示:

    		// 第三种遍历方式:使用forEach方法夹带Lambda表达式进行遍历
    		set.forEach(hash_each -> System.out.println("hash_each=" + hash_each));
    

    讲完了集合的三种遍历方式,按说集合的常见用法均涉及到了,那么为啥集合还要分成哈希集合与二叉集合两类呢?这缘于集合的基本特性规定了集合里的每个元素是唯一的,但并未规定集合里的元素需要按照顺序排列。从前面哈希集合的遍历结果可知,哈希集合里面保存的各元素是无序的,因为一个数据的哈希结果是散列值,天南地北到处跑,自然无法按照元素值进行排序。二叉集合的设计正是要解决这个顺序问题,由于二叉集合内部采取二叉树存储数据,每个新加入的元素都要与原住民们比较一番,好决定这个新元素是放在某个原住民的左节点还是右节点;因此,倘若把一组字符串先后加入二叉集合,那么每次新增元素的操作都会进行大小比较,最终得到的二叉集合必定是有序的。

    为了验证二叉集合的添加操作是否符合设计原理,接下来不妨创建一个二叉集合的实例,再往其中添加多个字符串,然后遍历打印该字符串集合的所有元素。据此重新编写后的二叉集合演示代码如下所示:

    		TreeSet<String> set = new TreeSet<String>();
    		set.add("hello");
    		set.add("world");
    		set.add("how");
    		set.add("are");
    		set.add("you");
    		// 第一种遍历方式:简化的for循环同样适用于数组和容器
    		for (String tree_item : set) {
    			System.out.println("tree_item=" + tree_item);
    		}
    

    运行上述的演示代码,观察以下的日志信息可知,这个二叉集合的遍历结果为按照字符串首字母升序排列:

    tree_item=are
    tree_item=hello
    tree_item=how
    tree_item=world
    tree_item=you
    

      

    需要注意的是,不管是哈希值计算,还是二叉节点比较,都需要元素归属的数据类型提供计算方法或者比较方法。对于包装类型、字符串等系统自带的数据类型来说,Java已经在它们的源码中实现了相关方法,所以这些数据类型允许程序员在集合中直接使用。然而如果是开发者自己定义的数据类型(新的类),就要求开发者自己来实现计算方法和比较方法了。
    譬如有个自定义的手机类MobilePhone,该类的定义代码见下:

    //定义一个手机类
    public class MobilePhone {
    	private String brand; // 手机品牌
    	private Integer price; // 手机价格
    
    	public MobilePhone(String brand, int price) {
    		this.brand = brand;
    		this.price = price;
    	}
    
    	// 获取手机品牌
    	public String getBrand() {
    		return this.brand;
    	}
    
    	// 获取手机价格
    	public int getPrice() {
    		return this.price;
    	}
    }
    

    现在给手机类分别创建对应的哈希集合与二叉集合,并对两种集合分别添加若干手机实例,结果会发现,手机的哈希集合居然会插入品牌与价格重复的元素!同时手机的二叉集合也变成乱序的了,因为编译器不晓得究竟要按照品牌排序还是按照价格排序。既然编译器无从判断待添加的元素是否重复,也无法判断新添加的元素根据哪个字段排序,程序员就得在手机类的定义代码中指定相关的判断规则了。

    就哈希集合的哈希值计算而言,自定义的手机类需要重写hashCode和equals方法,其中hashCode方法计算得到的哈希值对应于该对象的保存位置,而equals方法用来判断该位置上的几个元素是否完全相等。一方面,我们要保证品牌与价格都相同的两个元素,它们的哈希值必须也相等;另一方面,即使两个元素的品牌和价格不一致,它们的哈希值也可能恰巧相等,于是还需要equals方法进一步校验是否存在重复。按照上述要求,重写后的hashCode和equals方法代码示例如下:

    	// hashCode方法计算出来的哈希值对应于该对象的保存位置
    	@Override
    	public int hashCode() {
    		return brand.hashCode() + price.hashCode();
    	}
    
    	// 同一个存储位置上可能有多个对象(哈希值恰好相等),
    	// 此时系统自动调用equals方法判断是否存在相同的对象。
    	@Override
    	public boolean equals(Object obj) {
    		if (!(obj instanceof MobilePhoneHash)) {
    			return false;
    		}
    		MobilePhoneHash other = (MobilePhoneHash) obj;
    		// 手机品牌和手机价格都相等,才算是这两个手机相等
    		boolean equals = this.brand.equals(other.brand) && this.price.equals(other.price);
    		return equals;
    	}
    

    至于二叉集合的节点大小比较,则需手机类实现接口Comparable,并具体定义该接口声明的compareTo方法(该方法用来比较两个元素的大小关系)。其实这里的Comparable接口与数组排序用到的Comparator接口作用类似,都是判断两个对象谁大谁小。如果要求二叉集合里面的手机元素按照价格排序,则compareTo方法主要校验当前手机的价格与其它手机的价格。详细的接口实现代码如下所示:

    public class MobilePhoneTree implements Comparable<MobilePhoneTree> {
    	// 此处省略手机类的构造方法、成员属性与成员方法定义
    
    	// 二叉树除了检查是否相等,还要判断先后顺序。
    	// 相等和先后顺序的校验结果从compareTo方法获得。
    	@Override
    	public int compareTo(MobilePhoneTree other) {
    		if (this.price.compareTo(other.price) > 0) { // 当前价格较高
    			return 1;
    		} else if (this.price.compareTo(other.price) < 0) { // 当前价格较低
    			return -1;
    		} else {
    			return this.brand.compareTo(other.brand);
    		}
    	}
    }
    

    经过一番折腾之后,再对新定义的两个手机类分别对哈希集合与二叉集合开展验证,结果应当为:哈希集合不会插入重复的手机对象,并且二叉集合里的各手机元素按照价格升序排列。



    更多Java技术文章参见《Java开发笔记(序)章节目录

  • 相关阅读:
    Kotlin 实现类似 C# 的 Event 事件代码
    nim 语言实现迭代器
    nim 语言使用 concept 实现 c# 的interface
    如何在 asp.net core mvc 项目中管理前端插件的引用
    遇到一个在 WPF 中使用 MessageBox 弹出但在打开后却立即自动关闭的问题
    如何在项目生成成功后,自动构建 nuget 包并复制或发布到指定位置
    在类库开发中,如何设定多个 .net 框架目标
    如何在 IIS 中重定向 http 请求至 https
    在 docker 中部署 phpmyadmin 使用 nginx 代理 https 时出现错误无法登录
    禅道中配置电子邮件发信遇到 SMTP 错误:无法连接到 SMTP 主机,点击重试可以成功
  • 原文地址:https://www.cnblogs.com/pinlantu/p/10443582.html
Copyright © 2020-2023  润新知