📓
Be a Javaer
  • Introduction
  • 第 1 章 Java编程开发入门
    • 第 1 节 Java开发准备
    • 第 2 节 Java基本概念
    • 第 3 节 Java数据类型划分
    • 第 4 节 Java运算符
    • 第 5 节 Java程序逻辑控制
    • 第 6 节 Java方法的定义及使用
  • 第 2 章 Java面向对象编程
    • 第 1 节 类与对象
    • 第 2 节 深入分析类与对象
    • 第 3 节 数组的定义与使用
    • 第 4 节 String类的基本概念
    • 第 5 节 String类的常用方法
    • 第 6 节 this关键字
    • 第 7 节 引用传递
    • 第 8 节 数据表与简单Java类映射
    • 第 9 节 对象比较
    • 第 10 节 static关键字
    • 第 11 节 代码块
    • 第 12 节 内部类
    • 第 13 节 链表的定义与使用
    • 第 14 节 继承性
    • 第 15 节 覆写
    • 第 16 节 数组操作
    • 第 17 节 辅助概念
      • final关键字
      • 多态性
    • 第 18 节 抽象类的定义及使用
    • 第 19 节 接口的定义及使用
    • 第 20 节 Object类
    • 第 21 节 拓展概念
      • 匿名内部类
      • 包装类
    • 第 22 节 包的定义及使用
    • 第 23 节 访问控制权限
      • 单例设计模式
      • 多例设计模式
    • 第 24 节 异常的捕获及处理
    • 第 25 节 Java5新特性
      • 可变参数
      • foreach循环
      • 静态导入
    • 第 26 节 泛型
    • 第 27 节 枚举
    • 第 28 节 Annotation
    • 第 29 节 Java7新特性
      • AutoCloseable
      • Try-with-resources
    • 第 30 节 Java8新特性
      • 接口定义增强
      • Lambda表达式
      • 方法引用
      • 函数式接口
  • 第 3 章 Java高级编程
    • 第 1 节 Java多线程基础实现
    • 第 2 节 线程常用操作方法
    • 第 3 节 线程的同步与死锁
    • 第 4 节 生产者与消费者
    • 第 5 节 Java基础类库
      • StringBuffer
      • Runtime
      • System
      • finalize
      • Cleaner
      • 对象克隆
    • 第 6 节 数字操作类
      • Math类
      • Random类
      • 大数字操作类
    • 第 7 节 日期处理类
      • Date类
      • 日期格式化
      • Calendar类
    • 第 8 节 比较器
    • 第 9 节 正则表达式
      • 常用的正则标记
      • String类对正则的支持
      • java.util.regex包支持
    • 第 10 节 反射机制
    • 第 11 节 动态代理
    • 第 12 节 反射与Annotation
    • 第 13 节 国际化程序实现
    • 第 14 节 开发支持类库
      • Arrays类
      • UUID类
      • Optional类
      • ThreadLocal类
      • 定时器
      • Base64加密工具
    • 第 15 节 文件操作
    • 第 16 节 字节流与字符流
    • 第 17 节 IO辅助概念
      • 字符编码
      • 内存流
      • 管道流
      • RandomAccessFile
    • 第 18 节 打印流
    • 第 19 节 System类对IO的支持
    • 第 20 节 对象序列化
    • 第 21 节 IO高级应用
      • 缓冲输入流
      • Scanner
    • 第 22 节 网络编程
    • 第 23 节 类集框架
    • 第 24 节 List集合
    • 第 25 节 集合输出
    • 第 26 节 Map集合
    • 第 27 节 Set集合
    • 第 28 节 集合工具类
      • Stack
      • Queue
      • Properties
      • Collections工具类
    • 第 29 节 数据流Stream
    • 第 30 节 JDBC简介
    • 第 31 节 Statement接口
    • 第 32 节 PreparedStatment接口
    • 第 33 节 批处理与事务处理
  • 第 4 章 Oracle数据库基础
    • 第 1 节 Oracle简介
    • 第 2 节 Oracle安装与配置
    • 第 3 节 SQLPlus命令
    • 第 4 节 SQL简介与数据表分析
    • 第 5 节 SQL简单查询
    • 第 6 节 SQL限定查询
    • 第 7 节 查询排序
    • 第 8 节 综合练习:基础查询
    • 第 9 节 单行函数
    • 第 10 节 多表查询
    • 第 11 节 分组统计查询
    • 第 12 节 子查询
    • 第 13 节 综合案例:复杂查询
    • 第 14 节 数据更新操作
    • 第 15 节 事务处理
    • 第 16 节 数据伪列
    • 第 17 节 数据表的创建与管理
    • 第 18 节 约束的创建与管理
    • 第 19 节 综合案例:数据表操作
    • 第 20 节 序列的定义与使用
  • 第 5 章 JavaWeb基础
  • 第 6 章 走向单体地狱
  • 第 7 章 GitFlow工作流指南
    • 版本控制
    • Git
    • 集中式工作流
    • 功能分支工作流
    • GitFlow 工作流
    • Forking 工作流
    • Pull Requests
  • 第 8 章 微服务入门
    • 第 1 节 微服务简介
    • 第 2 节 Linux
    • 第 3 节 Docker
    • Docker 仓库
    • Ubuntu 安装 Docker
    • Docker 镜像加速器
    • 第 4 节 Docker Compose
    • 第 5 节 GitLab
    • 第 6 节 Nexus
    • 第 7 节 Harbor
  • 第 9 章 再谈微服务
  • 第 10 章 Spring Boot
  • 第 11 章 Spring Cloud Netflix
  • 第 12 章 Apache Dubbo Zookeeper
  • 第 13 章 Spring Cloud Alibaba
  • 第 14 章 Vue
  • 第 15 章 Kubernetes
  • 第 16 章 Spring Security oAuth2
  • 第 17 章 Flutter
  • Redis
    • Redis 入门
    • Redis 的数据类型
    • Redis 事务
    • Jedis
    • Spring Boot 整合 Redis
    • Redis 配置文件
    • Redis 持久化
    • Redis 发布/订阅
    • Redis 主从复制
    • Redis Sentinel
    • Redis 缓存故障
  • Glossary
由 GitBook 提供支持
在本页
  • Set接口简介
  • HashSet
  • TreeSet
  • 关于数据排序的说明
  • 关于重复元素的说明
  • 一些简单的源码解析

这有帮助吗?

  1. 第 3 章 Java高级编程

第 27 节 Set集合

上一页第 26 节 Map集合下一页第 28 节 集合工具类

最后更新于5年前

这有帮助吗?

在Collection接口下又有另外一个比较常用的子接口为Set接口(20%),Set接口并不像List接口那样对于Collection接口进行了大量的扩充,而是简单的继承了Collection接口。也就没有了之前List集合所提供的的get()方法。Set集合最大的特点就是不允许保存重复元素。

Set接口简介

在JDK1.9之前Set集合与Collection集合的定义并无差别,Set继续使用了Collection接口中的方法进行操作,但是从JDK1.9之后,Set集合也像List集合一样扩充了一些static方法,Set集合的定义如下:

public interface Set<E> extends Collection<E>

范例: 验证Set集合特征

public class Application {
    public static void main(String[] args) throws Exception {
        Set<String> all = Set.of("Hello", "World", "Shieh", "Hello", "World");
        all.forEach(System.out::println);
    }
}

当使用of()时如果参数中存在重复元素则会直接抛出异常。Set集合的常规使用形式是通过子类进行实例化,所以Set接口下有两个常用的子类:HashSet、TreeSet。

HashSet

HashSet是Set接口里面使用最多的一个子类,其最大的特点就是保存的数据都是无序的,HashSet的定义如下:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

范例: 观察HashSet子类的特点

package com.alpha.demo;
import java.util.HashSet;
import java.util.Set;
public class TestDemo { 
	public static void main(String[] args) throws Exception {
		Set<String> all = new HashSet<String>();
		all.add("Mori");
		all.add("Hello");
		all.add("Hello"); // 重复数据
		all.add("World");
		System.out.println(all);
	}
}

通过代码可以发现,Set集合下没有重复元素(这一点是Set接口的特征),同时发现在里面所保存的数据是没有任何顺序的,即:HashSet子类的特征属于无需排列。

TreeSet

Set接口的另一个子类就是TreeSet,与HashSet最大的区别在于TreeSet集合里面所保存的数据是有序的,TreeSet的定义如下:

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

范例: 使用TreeSet子类

package com.alpha.demo;
import java.util.Set;
import java.util.TreeSet;
public class TestDemo { 
	public static void main(String[] args) throws Exception {
		Set<String> all = new TreeSet<String>();
		all.add("M");
		all.add("B");
		all.add("B"); // 重复数据
		all.add("A");
		System.out.println(all);
	}
}

此时的程序使用了TreeSet子类,发现没有重复数据,并且所保存的内容自动排序。

关于数据排序的说明

既然TreeSet子类保存的内容可以进行排序,那么下面不如就编写一个自定义的类来完成数据的保存。

集合就是一个动态的对象数组,那么如果想要为一组对象进行排序,在Java里面必须要使用比较器,应该使用Comparable完成比较。在比较方法里面需要将这个类的所有属性都一起参与到比较之中。

范例: TreeSet排序

package com.alpha.demo;
import java.util.Set;
import java.util.TreeSet;
class Book implements Comparable<Book> {
	private String title;
	private double price;
	public Book(String title, double price) {
		this.title = title;
		this.price = price;
	}
	@Override
	public String toString() {
		return "Book [title=" + title + ", price=" + price + "]\n";
	}
	@Override
	public int compareTo(Book o) {
		if (this.price > o.price)
			return 1;
		else if (this.price < o.price)
			return -1;
		if (this.title == null) 
			if (o.title != null)
				return -1;
			else
				return 0;
		return this.title.compareTo(o.title); // 调用了String类的比较大小
	}
}
public class TestDemo { 
	public static void main(String[] args) throws Exception {
		Set<Book> all = new TreeSet<Book>();
		all.add(new Book("Java", 69.8));
		all.add(new Book("Java", 69.8)); // 全部属性相同
		all.add(new Book("JSP", 69.8)); // 部分属性相同
		all.add(new Book("Oracle", 79.8)); // 全都不同
		System.out.println(all);
	}
}

通过检测可以发现TreeSet类主要是依靠Comparable接口中的compareTo()方法判断是否是重复数据,如果返回的是0,那么就认为是重复数据,不会被保存。

关于重复元素的说明

Comparable接口只能够负责TreeSet子类进行重复元素的判断,它并不是真正的用于能够进行重复元素验证的操作。如果要想判断重复元素那么只能够依靠Object类中所提供的方法:

  • 取得哈希码:public int hashCode();

    • 先判断对象的哈希码是否相同,依靠哈希码取得一个对象的内容;

  • 对象比较:public boolean equals(Object obj);

    • 再讲对象的属性进行依次的比较;

package com.alpha.demo;
import java.util.HashSet;
import java.util.Set;
class Book {
	private String title;
	private double price;
	public Book(String title, double price) {
		this.title = title;
		this.price = price;
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		long temp;
		temp = Double.doubleToLongBits(price);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		result = prime * result + ((title == null) ? 0 : title.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Book other = (Book) obj;
		if (Double.doubleToLongBits(price) != Double.doubleToLongBits(other.price))
			return false;
		if (title == null) {
			if (other.title != null)
				return false;
		} else if (!title.equals(other.title))
			return false;
		return true;
	}
	@Override
	public String toString() {
		return "Book [title=" + title + ", price=" + price + "]\n";
	}
}
public class TestDemo { 
	public static void main(String[] args) throws Exception {
		Set<Book> all = new HashSet<Book>();
		all.add(new Book("Java", 69.8));
		all.add(new Book("Java", 69.8)); // 全部属性相同
		all.add(new Book("JSP", 69.8)); // 部分属性相同
		all.add(new Book("Oracle", 79.8)); // 全都不同
		System.out.println(all);
	}
}

以后在非排序的情况下,只要是判断重复元素依靠的永远都是hashCode()与equals()。

一些简单的源码解析

HashSet是不允许存在重复元素的,分析其源码,来观察其底层的实现原理:

public class HashSet<E>
        extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable
{

    /**
     * 内部维护着一个HashMap
     */
    private transient HashMap<E,Object> map;

    // map中key的默认值
    private static final Object PRESENT = new Object();

    /**
     * HashSet的默认构造函数为内部维护的HashMap实例化
     */
    public HashSet() {
        map = new HashMap<>();
    }

    /**
     * 添加时将PRESENT与要添加的元素作为map的key与value添加至维护的map中
     *
     * @param e element to be added to this set
     * @return {@code true} if this set did not already contain the specified
     * element
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}

此时可以看出,HashSet内部是实现几乎都是依赖着HashMap。同理,TreeSet的内部也维护着一个TreeMap。

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{

    private transient NavigableMap<E,Object> m;

    private static final Object PRESENT = new Object();

    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

    public TreeSet() {
        this(new TreeMap<>());
    }
}

所以Set的实现只是对于Map的一种特殊用法。