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开发笔记(序)章节目录》
Java开发笔记(六十五)集合:HashSet和TreeSet的更多相关文章
- Java开发笔记(十五)短路逻辑运算的优势
前面提到逻辑运算只能操作布尔变量,这其实是不严谨的,因为经过Java编程实现,会发现“&”.“|”.“^”这几个逻辑符号竟然可以对数字进行运算.譬如下面的代码就直接对数字分别开展了“与”.“或 ...
- Java开发笔记(一百五十)C3P0连接池的用法
JDBC既制定统一标准兼容了多种数据库,又利用预报告堵上了SQL注入漏洞,照理说已经很完善了,可是人算不如天算,它在性能方面不尽如人意.问题出在数据库连接的管理上,按照正常流程,每次操作完数据库,都要 ...
- Java开发笔记(九十五)NIO配套的文件工具Files
NIO不但引进了高效的文件通道,而且新增了更加好用的文件工具家族,包括路径组工具Paths.路径工具Path.文件组工具Files.先看路径组工具Paths,该工具提供了静态方法get,输入某个文件的 ...
- Java开发笔记(一百五十一)Druid连接池的用法
C3P0连接池自诞生以来在Java Web领域反响甚好,业已成为hibenate框架推荐的连接池.谁知人红是非多,C3P0在大型应用场合中暴露了越来越多的局限性,包括但不限于下列几点:1.C3P0管理 ...
- Java开发学习(二十五)----使用PostMan完成不同类型参数传递
一.请求参数 请求路径设置好后,只要确保页面发送请求地址和后台Controller类中配置的路径一致,就可以接收到前端的请求,接收到请求后,如何接收页面传递的参数? 关于请求参数的传递与接收是和请求方 ...
- Java开发笔记(十六)非此即彼的条件分支
前面花了大量篇幅介绍布尔类型及相应的关系运算和逻辑运算,那可不仅仅是为了求真值或假值,更是为了通过布尔值控制流程的走向.在现实生活中,常常需要在岔路口抉择走去何方,往南还是往北,向东还是向西?在Jav ...
- Java学习笔记(十五)——javadoc学习笔记和可能的注意细节
[前面的话] 这次开发项目使用jenkins做持续集成,PMD检查代码,Junit做单元测试,还会自动发邮件通知编译情况,会将javadoc生成的文档自动发到一个专门的服务器上面,每个人都可以看,所以 ...
- Java学习笔记二十五:Java面向对象的三大特性之多态
Java面向对象的三大特性之多态 一:什么是多态: 多态是同一个行为具有多个不同表现形式或形态的能力. 多态就是同一个接口,使用不同的实例而执行不同操作. 多态性是对象多种表现形式的体现. 现实中,比 ...
- Java开发笔记(十九)规律变化的for循环
前面介绍while循环时,有个名叫year的整型变量频繁出现,并且它是控制循环进出的关键要素.不管哪一种while写法,都存在三处与year有关的操作,分别是“year = 0”.“year<l ...
- JAVA学习第六十五课 — 正則表達式
正則表達式:主要应用于操作字符串.通过一些特定的符号来体现 举例: QQ号的校验 6~9位.0不得开头.必须是数字 String类中有matches方法 matches(String regex) 告 ...
随机推荐
- apicloud实现各种自定义弹层组件
- 10. vue axios 请求未完成时路由跳转报错问题
axios 请求未完成时路由跳转报错问题 前两天项目基本功能算是完成了,在公司测试时遇到了遇到了一个问题,那就是在请求未完成时进行路由跳转时会报错,想了几种办法来解决,例如加loading,请求拦截, ...
- nodejs内存溢出
npm-v 报错,错误信息如下: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScri ...
- Java作业 十一(2017-11-13)
/*关键字*/ package com.baidu.www; abstract class A { private String name; public A(String name) { this. ...
- Hadoop源码分析(1):HDFS读写过程解析
一.文件的打开 1.1.客户端 HDFS打开一个文件,需要在客户端调用DistributedFileSystem.open(Path f, int bufferSize),其实现为: public F ...
- 产品经理教你如何构建电商电销 CRM 系统
在电销或网销行业中老板们会经常问到,上个月渠道投放花了多少钱,来了多少量,转化率怎么样,获得了多少新线索,获客成本如何,销售额是多少? 劈天盖地的各种数据需求飞来,没有一个像样的系统该如何是好?这时候 ...
- [Swift]LeetCode23. 合并K个排序链表 | Merge k Sorted Lists
Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity. E ...
- [SQL]LeetCode196. 删除重复的电子邮箱 | Delete Duplicate Emails
Write a SQL query to delete all duplicate email entries in a table named Person, keeping only unique ...
- [SQL]LeetCode262.行程和用户 | Trips and Users
SQL架构 Create table If Not Exists Trips (Id )) Create table If Not Exists Users (Users_Id ), Role ENU ...
- 『追捕盗贼 Tarjan算法』
追捕盗贼(COCI2007) Description 为了帮助警察抓住在逃的罪犯,你发明了一个新的计算机系统.警察控制的区域有N个城市,城市之间有E条双向边连接,城市编号为1到N. 警察经常想在罪犯从 ...