Set接口概述
Set集合不允许存储重复元素,而且不保证元素是有序的(存入和取出的顺序有可能一致[有序],也有可能不一致[无序])。通过查看JDK文档,发现Set集合的功能和Collection的是一致的,所以Set集合取出的方法只要一个,那就是迭代器。
Set接口的常用子类
HashSet
查阅HashSet集合的API介绍,可发现:
此类实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set的迭代顺序,特别是它不保证该顺序恒久不变。此类允许使用null元素。
通过上面的这句话,我们可以总结出:
- HashSet集合采用哈希表结构存储数据,保证元素唯一性的方式依赖于hashCode()与equals()方法(后面会介绍到);
- HashSet集合不能保证元素的迭代顺序与元素存储顺序相同。
哈希表
哈希表概述
上面提到了HashSet集合采用哈希表结构存储数据,那什么是哈希表呢? 哈希表底层使用的也是数组机制,数组中也存放对象,而这些对象往数组中存放时的位置比较特殊,当需要把这些对象给数组存放时,会根据这些对象的特有数据结合相应的算法,计算出这个对象在数组中的位置,然后把这个对象存放在数组中。而这样的数组就称为哈希数组,即就是哈希表。
哈希表原理
当向哈希表中存放元素时,需要根据元素的特有数据结合相应的算法,这个算法其实就是Object类中的hashCode方法。由于任何对象都是Object类的子类,所以任何对象都拥有这个方法。即就是在哈希表中存放对象时,会调用对象的hashCode方法,算出对象在表中的存放位置,这里需要注意,如果两个对象hashCode方法算出来的结果一样,这种现象称为哈希冲突,这时会调用对象的equals方法,比较这两个对象是不是同一个对象,如果equals方法返回的是true,那么就不会把第二个对象存放在哈希表中,如果返回的是false,就会把这个对象通过地址链接法或拉链法存放在哈希表中。
哈希表结构存储数据的原理用图来表示:
总结
保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,想要保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。覆盖hashCode()方法是为了根据元素自身的特点确定哈希值,覆盖equals()方法是为了解决哈希值的冲突。
哈希表存储自定义对象
例,往HashSet中存储学生对象(姓名,年龄)。同姓名,同年龄视为同一个人,不存。
分析:HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一。
创建自定义对象Student:
public class Student {
private String name;
private int age;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
/*
* 覆盖hashCode方法,根据对象自身的特点定义哈希值。
*/
public int hashCode() {
final int NUMBER = 37;
return name.hashCode() + age * NUMBER; // 尽量减小哈希冲突
}
/**
* 还需要定义对象自身判断内容相同的依据,覆盖equals()方法。
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Student)) {
throw new ClassCastException("类型错误");
}
Student stu = (Student) obj;
return this.name.equals(stu.name) && this.age == stu.age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
创建HashSet集合,存储Student对象:
public class HashSetTest {
public static void main(String[] args) {
// 1,创建容器对象
Set set = new HashSet();
// 2,存储学生对象
set.add(new Student("xiaoqiang", 20));
set.add(new Student("wangcai", 27));
set.add(new Student("xiaoming", 22));
set.add(new Student("xiaoqiang", 20));
set.add(new Student("daniu", 24));
set.add(new Student("wangcai", 27));
// 3,获取所有学生
for (Iterator it = set.iterator(); it.hasNext();) {
Student stu = (Student) it.next();
System.out.println(stu.getName() + "::" + stu.getAge());
}
}
}
注意:对于判断元素是否存在,以及删除、添加等操作,依赖的方法也是元素的hashCode()和equals()方法。
LinkedHashSet
通过查阅LinkedHashSet的API介绍,我们可知道:
具有可预知迭代顺序的Set接口的哈希表和链接列表实现。此实现与HashSet的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。
可总结为:LinkedHashSet是一个特殊的Set集合,而且是有序的,底层是一个双向链表+哈希表。
public class LinkedHashSetDemo {
public static void main(String[] args) {
// 1,创建一个Set容器对象
Set set = new LinkedHashSet();
// 2,添加元素
set.add("abc");
set.add("heihei");
set.add("haha");
set.add("nba");
// 3,只能用迭代器取出
for (Iterator it = set.iterator(); it.hasNext();) {
System.out.println(it.next());
}
}
}
运行以上程序,可知LinkedHashSet是有序的。
TreeSet
TreeSet是线程不同步的,可以对Set集合中的元素进行排序,底层数据结构是二叉树(也叫红黑树),保证元素唯一性的依据是:比较方法的返回值是0。更通俗一点说就是比较方法的返回值是否是0,只要是0,就是重复元素,不存。
TreeSet对集合中的元素进行排序的方式有两种,如下:
TreeSet存储自定义对象,使用TreeSet排序的第一种方式
例1,往TreeSet集合中存储自定义对象学生。想按照学生的年龄进行排序。
先看TreeSet排序的第一种方式——我们自定义的Student类须实现Comparable接口(该接口强制让Student类具备比较性),覆盖compareTo方法。
int compareTo(T o)
:比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
自定义的Student类的代码为:
public class Student implements Comparable {
private String name;
private int age;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
/*
* 覆盖hashCode方法,根据对象自身的特点定义哈希值。
*/
public int hashCode() {
final int NUMBER = 37;
return name.hashCode() + age * NUMBER; // 尽量减小哈希冲突
}
/**
* 还需要定义对象自身判断内容相同的依据,覆盖equals()方法。
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Student)) {
throw new ClassCastException("类型错误");
}
Student stu = (Student) obj;
return this.name.equals(stu.name) && this.age == stu.age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
/**
* 学生就具备了比较功能。该功能是自然排序使用的方法。
* 自然排序就以年龄的升序排序为主。
*/
@Override
public int compareTo(Object o) {
Student stu = (Student)o;
// 验证TreeSet集合的add()方法调用了compareTo()方法
System.out.println(this.name + ":" + this.age + "......" + stu.name + ":" + stu.age) ;
if (this.age > stu.age)
return 1;
if (this.age < stu.age)
return -1;
return 0;
}
}
接下来编写一个测试类——TreeSetDemo.java,其代码为:
public class TreeSetDemo {
public static void main(String[] args) {
Set set = new TreeSet();
set.add(new Student("xiaoqiang", 20));
set.add(new Student("daniu", 24));
set.add(new Student("xiaoming", 22));
set.add(new Student("tudou", 18));
set.add(new Student("dahuang", 19));
// 3,只能用迭代器取出
for (Iterator it = set.iterator(); it.hasNext();) {
Student stu = (Student) it.next();
System.out.println(stu.getName() + "::" + stu.getAge());
}
}
}
运行以上程序,会发现TreeSet集合中存储的学生真是按照年龄来升序排序的。
接着我们面临的需求又发生了变化,同姓名同年龄的学生视为同一个人,是不用存入TreeSet集合中的,而且当年龄相同时,需要按照姓名的自然顺序排序。这时自定义的Student类的代码需要修改为:
package cn.liayun.domain;
public class Student implements Comparable {
private String name;
private int age;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
/**
* 覆盖hashCode方法,根据对象自身的特点定义哈希值。
*
*/
public int hashCode() {
final int NUMBER = 37;
return name.hashCode() + age * NUMBER;
}
/**
* 需要定义对象自身判断内容相同的依据,覆盖equals方法。
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Student)) {
throw new ClassCastException(obj.getClass().getName() + "类型错误");
}
Student stu = (Student) obj;
return this.name.equals(stu.name) && this.age == stu.age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
/**
* 学生就具备了比较功能。该功能是自然排序使用的方法。
* 自然排序就以年龄的升序排序为主。
*/
@Override
public int compareTo(Object o) {
Student stu = (Student) o;
// System.out.println(this.name + ":" + this.age + "......." + stu.name + ":" + stu.age);
/*
* 既然是同姓名同年龄是同一个人,视为重复元素,要判断的要素有两个。
* 既然是按照年龄进行排序,所以先判断年龄,再判断姓名。
*/
int temp = this.age - stu.age;
return temp == 0 ? this.name.compareTo(stu.name) : temp;
// return 1;
}
}
这时测试类——TreeSetDemo.java的代码应改为:
public class TreeSetDemo {
public static void main(String[] args) {
Set set = new TreeSet();
set.add(new Student("xiaoqiang", 20));
set.add(new Student("daniu", 24));
set.add(new Student("xiaoming", 22));
set.add(new Student("tudou", 18));
set.add(new Student("daming", 22));
set.add(new Student("dahuang", 19));
// 3,只能用迭代器取出
for (Iterator it = set.iterator(); it.hasNext();) {
Student stu = (Student) it.next();
System.out.println(stu.getName() + "::" + stu.getAge());
}
}
}
运行结果为:
图解TreeSet存储元素的自然排序和唯一性
思考一个这样的问题:元素变为怎么存进去的就怎么取出来的,怎么做呢?
这时可依据二叉树原理来实现,只要让compareTo()方法返回正数即可。
import java.util.*;
class Student implements Comparable { // 该接口强制让学生具备比较性
private String name;
private int age;
Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public int compareTo(Object obj) {
return 1;
}
}
class TreeSetDemo {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new Student("lisi02", 22));
ts.add(new Student("lisi007", 20));
ts.add(new Student("lisi09", 19));
ts.add(new Student("lisi08", 19));
Iterator it = ts.iterator();
while(it.hasNext()) {
Student stu = (Student)it.next();
System.out.println(stu.getName()+"..."+stu.getAge());
}
}
}
TreeSet存储自定义对象,使用TreeSet排序的第二种方式
例,将Student对象存储到TreeSet集合中,同姓名同年龄视为同一个人,不存,按照学生的姓名进行升序排序,而且当姓名相同时,需要按照学生的年龄进行升序排序。
分析:当元素自身不具备比较性时,或者具备的比较性不是所需要的,这时就需要让容器自身具备比较性。定义一个比较器,将比较器对象作为参数传递给TreeSet集合的构造函数。当两种排序都存在时,以比较器为主。
我们自定义的Student类的代码没必要修改。接着,自定义一个比较器实现Comparator接口,覆盖compare()方法。
package cn.liayun.comparator;
import java.util.Comparator;
import cn.liayun.domain.Student;
/**
* 自定义了一个比较器,用来对学生对象按照姓名进行升序排序。
* @author liayun
*
*/
public class ComparatorByName /*extends Object*/ implements Comparator {
@Override
public int compare(Object o1, Object o2) {
Student s1 = (Student)o1;
Student s2 = (Student)o2;
int temp = s1.getName().compareTo(s2.getName());
return temp == 0 ? s1.getAge() - s2.getAge() : temp;
}
}
最后编写一个测试类——TreeSetDemo2.java进行测试。
package cn.liayun.set.demo;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import cn.liayun.comparator.ComparatorByName;
import cn.liayun.domain.Student;
public class TreeSetDemo2 {
public static void main(String[] args) {
//初始化TreeSet集合时,明确一个比较器。
Set set = new TreeSet(new ComparatorByName());
set.add(new Student("xiaoqiang", 20));
set.add(new Student("daniu", 24));
set.add(new Student("xiaoming", 22));
set.add(new Student("tudou", 18));
set.add(new Student("daming", 22));
set.add(new Student("dahuang", 19));
for (Iterator it = set.iterator(); it.hasNext();) {
Student stu = (Student) it.next();
System.out.println(stu.getName() + "::" + stu.getAge());
}
}
}
练习
练习一、对多个字符串(不重复)按照长度排序(由短到长)。
分析:字符串本身具备比较性,但是它的比较方式不是所需要的,这时就只能使用比较器。
先自定义一个比较器。
package cn.liayun.test;
import java.util.Comparator;
public class ComparatorByLength implements Comparator {
@Override
public int compare(Object arg0, Object arg1) {
//对字符串按照长度比较
String s1 = (String) arg0;
String s2 = (String) arg1;
//比较长度,
int temp = s1.length() - s2.length();
//长度相同,再按照字典顺序比较
return temp == 0 ? s1.compareTo(s2) : temp;
}
}
再编写一个测试类Test.java进行测试:
public class Test {
public static void main(String[] args) {
sortStringByLength();
}
/*
* 练习一:对多个字符串(不重复)按照长度排序(由短到长)
*
* 思路:
* 1,多个字符串,需要容器存储。
* 2,选择哪个容器。字符串是对象,可以选择集合,而且不重复,选择Set集合
* 3,还需要排序,可以选择TreeSet集合。
*
*/
public static void sortStringByLength() {
Set set = new TreeSet(new ComparatorByLength()); // 自然排序的方式
set.add("haha");
set.add("abc");
set.add("zz");
set.add("nba");
set.add("xixixi");
for (Object obj : set) {
System.out.println(obj);
}
}
}
练习二、对多个字符串(重复),按照长度排序。
分析:自然排序可以使用String类中的compareTo方法,但是现在要的是长度排序,这就需要比较器了。所以须定义一个按照长度排序的比较器对象。
package cn.liayun.test;
import java.util.Comparator;
public class ComparatorByLength implements Comparator {
@Override
public int compare(Object arg0, Object arg1) {
//对字符串按照长度比较
String s1 = (String) arg0;
String s2 = (String) arg1;
//比较长度,
int temp = s1.length() - s2.length();
//长度相同,再按照字典顺序比较
return temp == 0 ? s1.compareTo(s2) : temp;
}
}
接下来编写一个测试类Test.java进行测试:
public class Test {
public static void main(String[] args) {
sortStringByLength();
}
/*
* 练习二:对多个字符串(重复),按照长度排序。
*
* 思路:
* 1,能使用TreeSet吗?不能。
* 2,可以存储到数组,List。这里先选择数组。
*/
public static void sortStringByLength() {
String[] strs = {"nba", "haha", "abccc", "zero", "xixi", "nba", "abccc", "cctv", "zero"};
// 自然排序可以使用String类中的compareTo方法,
// 但是现在要的是长度排序,这就需要比较器。
// 定义一个按照长度排序的比较器对象
Comparator comp = new ComparatorByLength();
// 排序就需要嵌套循环,位置置换
for (int x = 0; x < strs.length - 1; x++) {
for (int y = x + 1; y < strs.length; y++) {
// if (strs[x].compareTo(strs[y]) > 0) { // 按照字典顺序
if (comp.compare(strs[x], strs[y]) > 0) { // 按照长度顺序
swap(strs, x, y);
}
}
}
for (String s : strs) {
System.out.println(s);
}
}
private static void swap(String[] strs, int x, int y) {
String temp = strs[x];
strs[x] = strs[y];
strs[y] = temp;
}
}