一:java.util.Set(interface)
Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。
public interface Set extends Collection {
// Query Operations
}
接下来将简单介绍Set下的几个实现类,如:HashSet,TreeSet,LinkedHashSet。
二:java.util.HashSet(class)
对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层维护了一个HashMap,其采用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,查看 HashSet 的源代码,可以看到如下代码:
public class HashSet extends AbstractSet
implements Set, Cloneable, java.io.Serializable {
// 使用 HashMap 的 key 保存 HashSet 中所有元素
private transient HashMap map;
// 定义一个虚拟的 Object 对象作为 HashMap 的 value
private static final Object PRESENT = new Object();
...
// 初始化 HashSet,底层会初始化一个 HashMap
public HashSet(){
map = new HashMap();
}
// 以指定的 initialCapacity、loadFactor 创建 HashSet
// 其实就是以相应的参数创建 HashMap
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap(initialCapacity);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy){
map = new LinkedHashMap(initialCapacity
, loadFactor);
}
// 调用 map 的 keySet 来返回所有的 key
public Iterator iterator(){
return map.keySet().iterator();
}
// 调用 HashMap 的 size() 方法返回 Entry 的数量,就得到该 Set 里元素的个数
public int size(){
return map.size();
}
// 调用 HashMap 的 isEmpty() 判断该 HashSet 是否为空,
// 当 HashMap 为空时,对应的 HashSet 也为空
public boolean isEmpty(){
return map.isEmpty();
}
// 调用 HashMap 的 containsKey 判断是否包含指定 key
//HashSet 的所有元素就是通过 HashMap 的 key 来保存的
public boolean contains(Object o){
return map.containsKey(o);
}
// 将指定元素放入 HashSet 中,也就是将该元素作为 key 放入 HashMap
public boolean add(E e){
return map.put(e, PRESENT) == null;
}
// 调用 HashMap 的 remove 方法删除指定 Entry,也就删除了 HashSet 中对应的元素
public boolean remove(Object o){
return map.remove(o)==PRESENT;
}
// 调用 Map 的 clear 方法清空所有 Entry,也就清空了 HashSet 中所有元素
public void clear(){
map.clear();
}
...
}
1. 此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。
2. HashSet按Hash算法来存储集合中的元素。因此具有很好的存取和查找性能。
3. HashSet具有以下特点:
- 不能保证元素的排列(迭代)顺序 即HashSet的元素存放顺序和添加进去时候的顺序没有任何关系;特别是它不保证该顺序恒久不变。
- HashSet不是线程安全的。
- 此类允许使用 null 元素,但是不允许出现重复元素。
4. 当想HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode() 方法来得到该对象的hashCode值,然后根据hashCode值决定该对象在HashSet中的存储位置。
5. 如果两个元素的equals()方法值返回ture,但它们的hashCode值返回值不相等,hashSet将会把他们存储在不同的位置。
对HashSet类中一些基本方法的使用,参照API:
首先,先定义一个Person类,完成基本的封装操作,并提供hashCode()和equals()方法。
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 为什么在hashCode()方法中有质数的存在。
*
* eg:
* Person p1: name: 100, age: 22
* Person p2: name: 22, age: 100
* 如果只是普通的相加,那么上述两个对象的 hashCode值相等,
* 但是通过质数prime * 其中某个数,那么在相加,两者的hashCode值就不相等了。
*
* */
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
/**
* 提供 构造方法,get set方法, equals方法。
* */
}
在定义一个测试类,完成基本方法的使用:
public class Test1HashSet {
public static void main(String[] args) {
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("Berg",22);
Person p2 = new Person("Xujun",22);
Person p3 = new Person("Berg",22);
Person p4 = new Person("HashSet",23);
Person p5 = new Person("SetHash",24);
System.out.println( p1.equals(p3));
System.out.println( p1.hashCode() + " \t " + p3.hashCode() );
set.add( p1 );
set.add( p2 );
set.add( p3 );
set.add( p4 );
set.add( p5 );
// 1. 怎么确保不可重复, 重写 HashCode() 和 equals();
//2.存的顺序与取得顺序不一致。
//3.迭代输出 循环 的方法
//4. 注意输出结果是无序的。
//5. 如果有序,只需要将 HashSet 改为LinkedHashSet
System.out.println( " 输出第1种方案 : " + set.size() );
for( Person s : set){
System.out.println( s + " " +s.hashCode());
}
System.out.println( " 输出第二种方案 : ");
Object [] objs = set.toArray(); // toArray() 返回一个包含 set 中所有元素的数组。
for( Object o : objs){
System.out.println( o ); // o.hashCode()
}
// 迭代 :
System.out.println( " 输出第3种方案 : ");
// 接口不能实例化 ,
//返回对此 set 中元素进行迭代的迭代器。
Iterator<Person> its = set.iterator();
while( its.hasNext() == true){ //如果仍有元素可以迭代,则返回 true。
Person s = its.next(); //返回迭代的下一个元素。
System.out.println( s );
}
}
}
三:java.util.LinkedHashSet(class)
具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。注意,插入顺序不 受在 set 中重新插入的 元素的影响。(如果在 s.contains(e) 返回 true 后立即调用 s.add(e),则元素 e 会被重新插入到 set s 中。)
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {}
1. LinkedHashSet是HashSet的子类。
2. LinkedHashSet集合根据元素的hashCode值来决定元素的存储位置,但他同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
3. LinkedHashSet 插入性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能。
4.LinkedHashSet不允许集合元素重复。
5.此实现也不同步,即线程不安全的。
eg: 将上述HashSet代码实现中 注释部分的第五点 要求满足即可,即:
将HashSet 更改为 LinkedHashSet即可看到迭代输出结果是有序的,且元素不重复。
四:java.util.TreeSet<E>
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
//...
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
//...
}
基于TreeMap的NavigableSet实现。使用元素的自然顺序对元素进行排序。当然也可以自己定制排序方法。
4.1 自然排序:
在原有的Person类中,让其实现 Comparable 接口,具体实现 compareTo()方法。
package com.berg.se.bean;
public class Person implements Comparable<Person> {
private String name;
private int age;
//.....省略
@Override
public int compareTo(Person p) {
if(p instanceof Person ){
// return this.name.compareTo( p.name); 按升序排序
return p.name.compareTo( this.name ); // 按降序排序 根据名字
}else{
throw new ClassCastException("非Person类型。");
}
}
}
注意:
public int compareTo( T o):
若返回0,代表两个元素相等,若返回正数,代表当前元素大,若返回负数,代表当前元素小
TreeSet会调用每个元素的compareTo()方法去和集合中的每个已经存在的元素去比较,进而决定当前元素在集合中的位置。
然后在看看具体的TreeSet操作:
public class Test3TreeSet {
public static void main(String[] args) {
TreeSet<Person> ts = new TreeSet<Person>();
Person p1 = new Person("AA", 19);
Person p2 = new Person("BB", 20);
Person p3 = new Person("CC", 21);
Person p4 = new Person("EE", 23);
Person p5 = new Person("DD", 19);
Person p6 = new Person("AA", 19);
ts.add(p1);
ts.add(p5);
ts.add(p4);
ts.add(p3);
ts.add(p2);
ts.add(p6);
Iterator<Person> iterator = ts.iterator();
while (iterator.hasNext()) {
Person p = iterator.next();
System.out.println(p);
}
//返回此 set 的部分视图,其元素从 fromElement(包括)到 toElement(不包括)。
System.out.println( " \n*************************部分视图: \n");
SortedSet<Person> ss = ts.subSet(p4,p2);
Iterator<Person> iterator1 = ss.iterator();
while (iterator1.hasNext()) {
Person p = iterator1.next();
System.out.println(p);
}
}
}
4.2 定制排序:
不让其Person类实现Comparable接口。而是将Comparator这个对象当做参数传递给TreeSet,让其元素实现排序效果。可以降低耦合。
Person2 并没有实现 Comparable接口。
public class Test3TreeSet2 {
public static void main(String[] args) {
Comparator<Object> comparator = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
if( o1 instanceof Person2 && o2 instanceof Person2){
Person2 p1 = (Person2) o1;
Person2 p2 = (Person2) o2;
return p2.getAge() - p1.getAge();
}else{
throw new ClassCastException("非Person2类型。");
}
}
};
TreeSet<Person2> ts = new TreeSet<Person2>(comparator);
Person2 p1 = new Person2("AA", 19);
Person2 p2 = new Person2("BB", 20);
Person2 p3 = new Person2("CC", 21);
Person2 p4 = new Person2("EE", 23);
Person2 p5 = new Person2("DD", 19);
Person2 p6 = new Person2("AA", 19);
ts.add(p1);
ts.add(p5);
ts.add(p4);
ts.add(p3);
ts.add(p2);
ts.add(p6);
Iterator<Person2> iterator = ts.iterator();
while (iterator.hasNext()) {
Person2 p = iterator.next();
System.out.println(p);
}
}
}
再来了解下TreeSet与TreeMap的区别于联系:
TreeMap 和 TreeSet 是 Java Collection Framework 的两个重要成员,其中 TreeMap 是 Map 接口的常用实现类,而 TreeSet 是 Set 接口的常用实现类。虽然 TreeMap 和TreeSet 实现的接口规范不同,但 TreeSet 底层是通过 TreeMap 来实现的(如同HashSet底层是是通过HashMap来实现的一样),因此二者的实现方式完全一样。而 TreeMap 的实现就是红黑树算法。
1. TreeSet和TreeMap的关系
与HashSet完全类似,TreeSet里面绝大部分方法都市直接调用TreeMap方法来实现的。
相同点:
TreeMap和TreeSet都是有序的集合,也就是说他们存储的值都是排好序的。
TreeMap和TreeSet都是非同步集合,因此他们不能在多线程之间共享,不过可以使用方法Collections.synchroinzedMap()来实现同步
运行速度都要比Hash集合慢,他们内部对元素的操作时间复杂度为O(logN),而HashMap/HashSet则为O(1)。
不同点:
最主要的区别就是TreeSet和TreeMap分别实现Set和Map接口
TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序)
TreeSet中不能有重复对象,而TreeMap中可以存在
TreeMap的底层采用红黑树的实现,完成数据有序的插入,排序。
********************************************************************************************************
注意hashCode方法:
1. HashSet集合判断两个元素相等的标准:两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等,
2. 如果两个对象通过equals()方法返回true,这两个对象的hashCode()值也应该相等。
3. 重写hashCode()方法时基本原则:
- 同一个对象多次调用hashCode()方法应该返回相同的值。
- 当两个对象的equals()方法比较返回true时,这两个对象的hashCode()方法的返回值也应该相等。
- 对象中用作equals()方法比较的字段,都应该用来计算hashCode值。