黑马程序员——Java集合框架—Set

时间:2023-02-18 17:46:20
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------


Set概述

Set是对数学上“集”的抽象。

Set集合中的元素是无序的。

Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,添加操作将会失败,add方法返回false。

Set集合判断两个对象是否相同不是使用“==”运算符,而是根据equals方法。也就是说,如果两个对象用equals方法比较返回true,Set集合就会认为这两个对象是相同的,就不会同时存储这两个对象。


Set接口

Set接口是Collection接口的子接口,在《黑马程序员——Java集合框架—概述》中我们展示了Collection接口的源码,下面我们来看一下Set接口的源码:

package java.util;

public interface Set<E> extends Collection<E> {

int size();

boolean isEmpty();

boolean contains(Object o);

Iterator<E> iterator();

Object[] toArray();

<T> T[] toArray(T[] a);

boolean add(E e);

boolean remove(Object o);

boolean containsAll(Collection<?> c);

boolean addAll(Collection<? extends E> c);

boolean retainAll(Collection<?> c);

boolean removeAll(Collection<?> c);

void clear();

boolean equals(Object o);

int hashCode();
}

我们对比Collection接口和Set接口的源码可以发现,Set接口继承了Collection接口,但是它们的源码是相同的。也就是说,虽然Set是Collection的子接口,但是Set没有添加任何额外的功能。可以认为,Set就是Collection只是行为不同(多态的表现)。


AbstractSet抽象类和SortedSet接口

Set接口有一个抽象子类和一个子接口。


AbstractSet的源码:

public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {

protected AbstractSet() {
}

//重写了父类中的equals方法
public boolean equals(Object o) {
if (o == this)
return true;

if (!(o instanceof Set))
return false;
Collection c = (Collection) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}

//重写了父类中的hashCode方法
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}

//重写了父类中的removeAll方法
public boolean removeAll(Collection<?> c) {
boolean modified = false;

if (size() > c.size()) {
for (Iterator<?> i = c.iterator(); i.hasNext(); )
modified |= remove(i.next());
} else {
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}

//抽象方法,没有实现
//public abstract Iterator<E> iterator();
//public abstract int size();
}

AbstractCollection抽象类实现了Collection接口,而且Set接口继承了AbstractCollection抽象类,所以AbstractSet抽象类相当于实现了Collection接口(最起码AbstractSet类中包含了Collection接口中的全部方法)。
从《黑马程序员——Java集合框架—概述》中可知,AbstractCollection抽象类中只包含2个抽象方法(iterator、size),AbstractSet中也只包含这2个抽象方法(AbstractSet没有实现这2个方法)。


我们知道Set集合中的元素是无序的,但Java在Set集合中提供了一个SortedSet接口来表示有序Set集合。

下面看一下SortedSet接口的源码:

package java.util;

public interface SortedSet<E> extends Set<E> {

//返回一个Comparator,代表有序Set集合排序机制
Comparator<? super E> comparator();

//取子集,从formElement(包含)到toElement(不含)
SortedSet<E> subSet(E fromElement, E toElement);

//取子集,由小于toElement的所有元素组成
SortedSet<E> headSet(E toElement);

//取子集,由大于等于fromElement的所有元素组成
SortedSet<E> tailSet(E fromElement);

//返回集合中的第一个元素
E first();

//返回集合中的最后一个元素
E last();
}

因为集合是有序的了,所以集合拥有以上方法是很正常的。


HashSet类

HashSet是AbstractSet抽象类的子类,所以它是无序集合,不能有重复元素。

HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个类,HashSet按Hash算法来存储集合中的元素,因此具有很好的存储和查找性能。

从Set接口的源码可以看到,Set接口中没有包含获取固定集合元素的方法,比如get方法,所以在HashSet中也不包含这种方法,也就是说,如果想要获得HashSet集合中的元素,只能通过遍历该集合。

HashSet具有以下特点:

1、不能保证元素的排列顺序,顺序有可能发生变化(即不保证集合元素迭代的顺序与集合元素插入的顺序相同,集合内部的元素也是无序的)。

2、HashSet不是同步的,如果多个线程同时访问一个HashSet,则必须通过代码来保证其同步。

3、集合元素值可以是null。

HashSet底层是使用HashMap实现的,使用HashMap的key来保存向HashSet中添加的元素,同时使用一个Object对象来充当HashMap的value。

package org.lgy.study.collection;

import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;

public class SetTest{
public static void main(String[] args){
Set s = new HashSet();

/*
代码片段1:
HashSet中可以存放null,但只能存放一个null值(因为set中的元素是不能重复的)。
关于为什么HashSet中可以存放null值,请参见HashMap的有关内容。
*/
SetA sa1 = null, sa2 = null;
s.add(sa1);
s.add(sa2);
System.out.println("s.size() = " + s.size() + "\n");

/*
代码片段2:
向HashSet中添加元素时,系统会调用该元素的hashCode方法计算出哈希值,进而计算出存储该元素的地址值。
如果该地址值处没有存储内容,则直接把该元素存储在该位置。
如果该地址值处已经存储类内容,则调用待添加元素的equals方法,与该位置已有的元素进行比较,
如果有equals方法返回true,则不再存储该元素,add方法返回false;
如果所有equals方法都返回false,则根据冲突解决算法存储下该元素。
*/
s.clear();
s.add(new SetA(true, (byte)10, (short)2, 3, 4L, 5.0F, 6.0));
s.add(new SetA(true, (byte)20, (short)2, 3, 4L, 5.0F, 6.0));
s.add(new SetA(true, (byte)30, (short)2, 3, 4L, 5.0F, 6.0));
s.add(new SetA(true, (byte)10, (short)2, 3, 4L, 5.0F, 6.0));
//System.out.println(s);
Iterator iterator = s.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println();
/*
代码片段3:
当向HashSet中添加可变元素时,必须十分小心,如果程序修改了集合中的对象,
有可能导致该对象与集合中的其它对象相等,从而导致HashSet无法准确访问该对象。
*/
s.clear();
SetB b1 = new SetB(10);
s.add(b1);
SetB b2 = new SetB(20);
s.add(b2);
SetB b3 = new SetB(30);
s.add(b3);
SetB b4 = new SetB(40);
s.add(b4);
System.out.println(s);
//下面一行代码把b2的count值修改为30,使b2等于b3
b2.setCount(30);
System.out.println(s);
//删除count为30的元素,被删掉的元素为b3,而不是b1
System.out.println("s.remove(new SetB(30)) = " + s.remove(new SetB(30)));
System.out.println(s);
/*
再次删除count为30的元素(即打算删除b2),但是无法删除
为什么会删除失败呢?
当试图删除count为30的b2时,系统会调用其hashCode方法获得其哈希值30,进而求出其存储地址,
但是存储b2时,其存储地址是按原来的count=20求得,所以按count=30求到的是b3的地址,因为b3已经
被删除了,所以删除失败。
*/
System.out.println("s.remove(new SetB(30)) = " + s.remove(new SetB(30)));
System.out.println(s);
/*
使用b2原来的count值(即count=20)删除b2,删除失败
为什么会删除失败呢?
通过count=20求得的地址值确实可以找到存储的b2,当找到b2后,系统会调用集合中已存储对象的equals方法
(即把待删除的对象作为参数来调用集合中对象的equals方法),结果没有返回true的对象,所以删除失败。
*/
System.out.println("s.remove(new SetB(20)) = " + s.remove(new SetB(20)));
System.out.println(s);
}
}

/*
结果:
s.size() = 1

10 hashCode... ...
20 hashCode... ...
30 hashCode... ...
10 hashCode... ...
10 equals 10
20 hashCode... ...
org.lgy.study.collection.SetA@1118010d
30 hashCode... ...
org.lgy.study.collection.SetA@11180153
10 hashCode... ...
org.lgy.study.collection.SetA@111800c7

[SetB[20], SetB[40], SetB[10], SetB[30]]
[SetB[30], SetB[40], SetB[10], SetB[30]]
30 equals 30
s.remove(new SetB(30)) = true
[SetB[30], SetB[40], SetB[10]]
s.remove(new SetB(30)) = false
[SetB[30], SetB[40], SetB[10]]
20 equals 30
s.remove(new SetB(20)) = false
[SetB[30], SetB[40], SetB[10]]
*/

class SetB{
private int count;

public SetB(int count){
this.count = count;
}

public void setCount(int count){
this.count = count;
}

public String toString(){
return "SetB[" + this.count + "]";
}

public int hashCode(){
return count;
}

public boolean equals(Object obj){
if(this == obj)
return true;
if(obj != null && obj instanceof SetB){
SetB b = (SetB)obj;
System.out.println(this.count + " equals " + b.count);
return this.count == b.count;
}
return false;
}
}

class SetA{
private boolean boolField;
private byte byteField;
private short shortField;
private int intField;
private long longField;
private float floatField;
private double doubleField;

public SetA(boolean boolField, byte byteField, short shortField, int intField, long longField, float floatField, double doubleField){
this.boolField = boolField;
this.byteField = byteField;
this.shortField = shortField;
this.intField = intField;
this.longField = longField;
this.floatField = floatField;
this.doubleField = doubleField;
}

//重写hashCode方法
public int hashCode(){

int boolFieldHashCode = (boolField ? 0 : 1) * 3;
int byteFieldHashCode = ((int)byteField) * 7;
int shortFieldHashCode = ((int)shortField) * 11;
int intFieldHashCode = intField * 13;
int longFieldHashCode =((int)(longField ^ (longField >>> 32))) * 17;
int floatFieldHashCode = Float.floatToIntBits(floatField) * 23;
long doubleFieldTemp = Double.doubleToLongBits(doubleField);
int doubleFieldHashCode = (int)(doubleFieldTemp ^ (doubleFieldTemp >>> 32)) * 29;
int hash = boolFieldHashCode + byteFieldHashCode + shortFieldHashCode + intFieldHashCode +
longFieldHashCode + floatFieldHashCode + doubleFieldHashCode;
System.out.println(this.byteField + " hashCode... ...");
return hash;
}

//重写equals方法
public boolean equals(Object obj){

if(this == obj)
return true;
if(obj != null && obj instanceof SetA){
SetA a = (SetA)obj;
System.out.println(this.byteField + " equals " + a.byteField);
return this.boolField == a.boolField && this.byteField == a.byteField && this.shortField == a.shortField &&
this.intField == a.intField && this.longField ==a.longField &&
this.floatField == a.floatField && this.doubleField == a.doubleField;
}
return false;
}
}

LinkedHashSet

LinkedHashSet是HashSet的子类,但它同时使用链表来维护元素的次序,这样使得元素是以插入的顺序来保存的,也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。

看一下LinkedHashSet的源码:

package java.util;

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

private static final long serialVersionUID = -2851667679971038690L;


public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}


public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}


public LinkedHashSet() {
super(16, .75f, true);
}


public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
}

LinkedHashSet的源码非常简单,只有4个构造方法,并且这4个构造方法全部是调用父类即HashSet的构造方法来完成初始化。

当调用LinkedHashSet的add、remove等方法时,实际上调用的是HashSet继承的方法。

下面看一个简单的示例:

package org.lgy.study.collection;

import java.util.Set;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Iterator;

public class LinkedHashSetTest{
public static void main(String[] args){
Set s = new LinkedHashSet();
s.add("疯狂Java讲义");
s.add("疯狂Android讲义");
s.add("轻量级Java EE");
s.add("经典Java EE");
System.out.println("LinkedHashSet: " + s);
Iterator it = s.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println("\n");

Set s2 = new HashSet();
s2.add("疯狂Java讲义");
s2.add("疯狂Android讲义");
s2.add("轻量级Java EE");
s2.add("经典Java EE");
System.out.println("HashSet: " + s2);
Iterator it2 = s2.iterator();
while(it2.hasNext()){
System.out.print(it2.next() + " ");
}
}
}
/*
结果:
LinkedHashSet: [疯狂Java讲义, 疯狂Android讲义, 轻量级Java EE, 经典Java EE]
疯狂Java讲义 疯狂Android讲义 轻量级Java EE 经典Java EE

HashSet: [经典Java EE, 疯狂Android讲义, 轻量级Java EE, 疯狂Java讲义]
经典Java EE 疯狂Android讲义 轻量级Java EE 疯狂Java讲义
*/


TreeSet

TreeSet继承了AbstractSet抽象类,并实现了SortedSet接口。TreeSet可以确保集合中的元素处于有序状态,与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小进行排序。

TreeSet的基本用法示例:

package org.lgy.study.collection;

import java.util.TreeSet;

public class TreeSetTest{
public static void main(String[] args){
TreeSet ts = new TreeSet();
ts.add(5);
ts.add(2);
ts.add(10);
ts.add(-9);
//输出集合里的元素,虽然添加元素时是无序的,但由于TreeSet是有序的,所以输出时是有序的
System.out.println(ts);
//输出集合里的第一个元素(默认就是最小的元素)
System.out.println("ts.first() = " + ts.first());
//输出集合里的最后一个元素(默认就是最大的元素)
System.out.println("ts.last() = " + ts.last());
//返回小于4的子集,不包含4(做为截取标准的元素 即4 可以是集合之外的元素)
System.out.println("ts.headSet(4, false) = " + ts.headSet(4, false));
System.out.println("ts.headSet(2, true) = " + ts.headSet(2, true));
System.out.println("ts.headSet(2, false) = " + ts.headSet(2, false));
//返回大于4的子集,不包含4(作为截取标准的元素 即4 可以是集合之外的元素)
System.out.println("ts.tailSet(4, false) = " + ts.tailSet(4, false));
System.out.println("ts.tailSet(5, true) = " + ts.tailSet(5, true));
System.out.println("ts.tailSet(5, false) = " + ts.tailSet(5, false));
//取子集
System.out.println("ts.subSet(2, true, 5, true) = " + ts.subSet(2, true, 5, true));
System.out.println("ts.subSet(2, false, 5, false) = " + ts.subSet(2,false, 5, false));
}
}

下面看一下TreeSet的排序规则。

TreeSet支持2种排序方式:自然排序和定制排序。

自然排序指TreeSet调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合按升序排列。

Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2),如果返回0,则表明这2个对象相等;如果方法返回一个正整数,则表明obj1大于obj2;如果方法返回一个负整数,则表明obj1小于obj2。

如果试图把一个对象加到TreeSet中,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。

对于TreeSet集合,它判断2个对象是否相等的唯一标准是:2个对象通过compareTo方法比较是否返回0,如果返回0,TreeSet则会认为它们相等,否则就认为它们不相等。