Set是Java中一个重要的集合类,因为Set中集合的元素是无序的,并且不会有重复的元素。下面对Set的两个子类进行介绍。
Hashset
一.HsahSet概述
HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
HashSet继承AbstractSet类,实现Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。Set接口是一种不包括重复元素的Collection,它维持它自己的内部排序,所以随机访问没有任何意义。
基本属性:
1 // 底层使用HashMap来保存HashSet中所有元素。
2 private transient HashMap<E, Object> map;
3
4 // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。
5 private static final Object PRESENT = new Object();
构造函数:
从构造函数中可以看出HashSet所有的构造都是构造出一个新的HashMap,其中最后一个构造函数,为包访问权限是不对外公开,仅仅只在使用LinkedHashSet时才会发生作用。
二.HsahSet实现
因为HashSet是基于HashMap,所以对于HashSet,其方法的实现过程是非常简单的。
1. iterator()
iterator()方法 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。底层实际调用底层HashMap的keySet来返回所有的key。 可见HashSet中的元素,只是存放在了底层HashMap的key上, value使用一个static final的Object对象标识。
1 public Iterator<E> iterator() {
2 return map.keySet().iterator();
3 }
2.size()
返回此set中的元素的数量(set的容量)。底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数,即HashMap容器的大小。
1 public int size() {
2 return map.size();
3 }
3.isEmpty()
isEmpty()判断HashSet()集合是否为空,如果此set不包含任何元素,则返回true。 底层实际调用HashMap的isEmpty()判断该HashSet是否为空。
1 public boolean isEmpty() {
2 return map.isEmpty();
3 }
4.contains(Object o)
contains(),判断某个元素是否存在于HashSet()中,存在返回true,否则返回false。更加确切的讲应该是要满足这种关系才能返回true:(o==null ? e==null : o.equals(e))。底层调用containsKey判断HashMap的key值是否为空。
1 public boolean contains(Object o) {
2 return map.containsKey(o);
3 }
5.add()
add()如果此 set 中尚未包含指定元素,则添加指定元素。如果此Set没有包含满足(e==null ? e2==null : e.equals(e2)) 的e2时,则将e2添加到Set中,否则不添加且返回false。由于底层使用HashMap的put方法将key = e,value=PRESENT构建成key-value键值对,当此e存在于HashMap的key中,则value将会覆盖原有value,但是key保持不变,所以如果将一个已经存在的e元素添加中HashSet中,新添加的元素是不会保存到HashMap中,所以这就满足了HashSet中元素不会重复的特性。
1 public boolean add(E e) {
2 return map.put(e, PRESENT) == null;
3 }
6.remove()
remove()如果指定元素存在于此 set 中,则将其移除。底层使用HashMap的remove方法删除指定的Entry。
1 public boolean remove(Object o) {
2 return map.remove(o) == PRESENT;
3 }
7.clear()
clear()从此 set 中移除所有元素。底层调用HashMap的clear方法清除所有的Entry。
1 public void clear() {
2 map.clear();
3 }
8.clone()
底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并没有复制这些元素本身。
1 public Object clone() {
2 try {
3 HashSet<E> newSet = (HashSet<E>) super.clone();
4 newSet.map = (HashMap<E, Object>) map.clone();
5 return newSet;
6 } catch (CloneNotSupportedException e) {
7 throw new InternalError();
8 }
9 }
TreeSet
TreeSet不支持快速随机遍历,只能通过迭代器进行遍历!
Java中的TreeSet是Set的一个子类,TreeSet集合是用来对象元素进行排序的,同样他也可以保证元素的唯一。
那TreeSet为什么能保证元素唯一,它是怎样排序的呢?先看一段代码:
public static void demo() {
TreeSet<Person> ts = new TreeSet<>();
ts.add(new Person("张三", 23));
ts.add(new Person("李四", 13));
ts.add(new Person("周七", 13));
ts.add(new Person("王五", 43));
ts.add(new Person("赵六", 33));
System.out.println(ts);
}
执行结果:
出错,会抛出一个异常:java.lang.ClassCastException
显然是出现了类型转换异常。原因在于我们需要告诉TreeSet如何来进行比较元素,如果不指定,就会抛出这个异常
如何解决:
如何指定比较的规则,需要在自定义类(Person)中实现```Comparable```接口,并重写接口中的compareTo方法
public class Person implements Comparable<Person> {
private String name;
private int age;
...
public int compareTo(Person o) {
return 0; //当compareTo方法返回0的时候集合中只有一个元素
return 1; //当compareTo方法返回正数的时候集合会怎么存就怎么取
return -1; //当compareTo方法返回负数的时候集合会倒序存储
}
}
为什么返回0,只会存一个元素,返回-1会倒序存储,返回1会怎么存就怎么取呢?原因在于TreeSet底层其实是一个二叉树机构,且每插入一个新元素(第一个除外)都会调用```compareTo()```方法去和上一个插入的元素作比较,并按二叉树的结构进行排列。
1. 如果将```compareTo()```返回值写死为0,元素值每次比较,都认为是相同的元素,这时就不再向TreeSet中插入除第一个外的新元素。所以TreeSet中就只存在插入的第一个元素。
2. 如果将```compareTo()```返回值写死为1,元素值每次比较,都认为新插入的元素比上一个元素大,于是二叉树存储时,会存在根的右侧,读取时就是正序排列的。
3. 如果将```compareTo()```返回值写死为-1,元素值每次比较,都认为新插入的元素比上一个元素小,于是二叉树存储时,会存在根的左侧,读取时就是倒序序排列的。
利用上述规则,我们就可以按照年龄来排序了。代码如图
public int compareTo(Person o) {
int num = this.age - o.age; //年龄是比较的主要条件
return num == 0 ? this.name.compareTo(o.name) : num;//姓名是比较的次要条件
}
按照姓名排序(依据Unicode编码大小),代码如下:
public int compareTo(Person o) {
int num = this.name.compareTo(o.name); //姓名是主要条件
return num == 0 ? this.age - o.age : num; //年龄是次要条件
}
按照姓名长度排序,代码如下:
public int compareTo(Person o) {
int length = this.name.length() - o.name.length(); //比较长度为主要条件
int num = length == 0 ? this.name.compareTo(o.name) : length; //比较内容为次要条件
return num == 0 ? this.age - o.age : num; //比较年龄为次要条件
}
以上是TreeSet如何比较自定义对象,接下来我们再来看一下TreeSet如何利用比较器比较元素。
需求:现在要制定TreeSet中按照String长度比较String。
//定义一个类,实现Comparator接口,并重写compare()方法,
class CompareByLen /*extends Object*/ implements Comparator<String> {
@Override
public int compare(String s1, String s2) { //按照字符串的长度比较
int num = s1.length() - s2.length(); //长度为主要条件
return num == 0 ? s1.compareTo(s2) : num; //内容为次要条件
}
}
public static void demo4() {
//需求:将字符串按照长度排序
TreeSet<String> ts = new TreeSet<>(new CompareByLen()); //Comparator c = new CompareByLen();
ts.add("aaaaaaaa");
ts.add("z");
ts.add("wc");
ts.add("nba");
ts.add("cba");
System.out.println(ts);
}
总结
- 特点
- TreeSet是用来排序的, 可以指定一个顺序, 对象存入之后会按照指定的顺序排列
- 使用方式
- a.自然顺序(Comparable)
- TreeSet类的add()方法中会把存入的对象提升为Comparable类型
- 调用对象的compareTo()方法和集合中的对象比较
- 根据compareTo()方法返回的结果进行存储
- b.比较器顺序(Comparator)
- 创建TreeSet的时候可以制定 一个Comparator
- 如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序
- add()方法内部会自动调用Comparator接口中compare()方法排序
- 调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数
- c.两种方式的区别
- TreeSet构造函数什么都不传, 默认按照类中Comparable的顺序(没有就报错ClassCastException)
- TreeSet如果传入Comparator, 就优先按照Comparator
- a.自然顺序(Comparable)
最后举个例子
package settext;
import java.util.HashSet;
import java.util.Iterator;
import java.util.TreeSet;
public class SetDemo01 {
public static void main(String[] args) {
// TreeSet ts = new TreeSet();
// ts.add("aaa");
// ts.add("aba");
// ts.add("bbb");
// ts.add("bbc");
// ts.add("cac");
//
// Iterator it = ts.iterator();
// while(it.hasNext()){
// System.out.println(it.next());
// }
HashSet hs = new HashSet();
hs.add("ccd");
hs.add("sdv");
hs.add("evv");
hs.add("fwe");
hs.add("ccd");
Iterator it = hs.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
}
加一段代码,有兴趣可以看一眼
package settext;
import java.util.Iterator;
import java.util.TreeSet;
import javax.management.RuntimeErrorException;
public class SetDemo01 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new Person("zzq1", 19));
ts.add(new Person("zzq2", 20));
ts.add(new Person("zzq3", 20));
Iterator it = ts.iterator();
while(it.hasNext()){
Person per = (Person)it.next();
System.out.println(per.getAge()+"......"+per.getName());
}
}
}
class Person implements Comparable{
private String name;
private int age;
Person(String name , int age){
this.name = name;
this.age = age;
}
public int compareTo(Object obj){
if(!(obj instanceof Person))
throw new RuntimeException("不是学生对象");
Person p = (Person)obj;
System.out.println(this.name+"...对比..."+p.name);
if(this.age > p.age){
return 1;
}
if(this.age == p.age){
return this.name.compareTo(p.name);
}
else {
return -1;
}
}
public int getAge(){
return age;
}
public String getName(){
return name;
}
}
可以自己拿去试一下(————)
这是运行结果