一 顶层接口
Set接口
Set集合是一个只能包含非重复元素的容器,继承Collection接口。
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();
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}
复制代码
都是基本的查询修改方法。
AbstractSet抽象类
在AbstractSet中实现了 equals方法,比较两个Set是否相等,从地址、类型、size、最后是否包含几个方面入手判断,set中都是非重复元素,如果相等的两个set,肯定是互相包含的。
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的计算还是将每个元素的hash值相加完成。
另一个值得注意的方法是removeAll,这里面首先根据set.size是否大于c.size 有两种删除逻辑。
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
if (size() > c.size()) {
for (Iterator<?> i = c.iterator(); i.hasNext(); )
// 调用set的remove方法
modified |= remove(i.next());
} else {
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
// 调用迭代器中的删除逻辑
i.remove();
modified = true;
}
}
}
return modified;
}
复制代码
(1)当size>c.size时调用iterator的remove方法,最终实际调用的是底层数据存储结构的删除方法,会调用底层存储结构的比较器来判断是否相等。
(2)当size<=c.size时调用set的remove方法,实际是AbstractCollection中实现的remove方法,比较元素是否相等使用的是元素自带的比较器。
如果你使用的是TreeSet,其底层数据结构是TreeMap,而TreeMap是可以指定集合比较器的。当指定的集合比较器和元素自身的比较器不一样时,就会出现同一个元素使用两种比较器得出两个不同结果,最终导致删除异常。
举例如下:
public class Tests{
static void test(String... abc) {
// 指定集合使用忽略大小写的比较器
Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
s.addAll(Arrays.asList("a", "b"));
s.removeAll(Arrays.asList(abc));
System.out.println(s);
}
public static void main(String[] args) {
// 只有一个元素,set.size>c.size 走集合自带比较器,忽略大小写 A等于a,传入A删除了a
test("A"); // out: [b]
// 两个元素,set.size = c.size 走元素自带比较器,A 不等于 a,不能删除a
test("A", "C"); // out: [a, b]
}
}
复制代码
二 具体实现
HashSet
-
HashSet底层基于HashMap实现,只能插入唯一元素,支持插入null,不保证插入顺序。
-
add、remove、contains、size等方法的时间复杂度都是O(1)。
-
非线程安全,多线程使用请使用线程安全的Collections.synchronizedSet。
-
迭代支持快速失败,迭代过程中如果出现结构性修改会报ConcurrentModificationException异常。
存储结构
上面说到HashSet基于HashMap实现,在Java中基于基类进行扩展,一般有两种方式,继承和组合。
(1)继承基础类,覆写其中的方法实现扩展,比如LinkedHashMap就继承了HashMap,覆写了其中几个回调方法,在其中实现了队双端链表的操作逻辑。
(2)组合基础类,通过持有基础类实例,调用基础类中的方法来实现功能的扩展,HashSet就是这种方式。
使用组合来实现功能扩展,好处在于:组合很灵活,可以任意组合其他现有基础类,哪怕和基础类不是一个体系也行(Set和Map 就是两类容器);组合类中的方法命名限制少,不需要和基础类保持一致。在工作中遇到复杂的业务对象时也可以考虑使用组合多个简单类来实现。
public class HashSet<E> extends AbstractSet<E>implements Set<E>,
Cloneable, java.io.Serializable{
// set中数据实际存储在map中,确切说存储在map的Key中
private transient HashMap<E,Object> map;
// map是二元存储结构,这里的PRESENT就是map中的Value,所有键值对共享一个value
private static final Object PRESENT = new Object();
// 默认实现
public HashSet() {
map = new HashMap<>();
}
// 使用集合类创建hashset
public HashSet(Collection<? extends E> c) {
// 指定初始容量
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
}
复制代码
小结:
(1)Map是二元结构而Set是一元结构,这里使用PRESENT作为value的默认填充值,解决差异问题。
(2)在使用已有集合对象创建HashSet时,HashMap的初始容量有两种方式,(c.size()/.75f) + 1的值在填充元素之后,正好比阀值少1,不会触发扩容;要么就直接指定为16。这种确定初始容量的方式尽可能避免了扩容。
增加元素
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
复制代码
这里直接使用了put方法,使用PRESENT作为默认值填充value,
删除元素
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
复制代码
删除元素同样使用map自己的remove方法,同样又用到了PRESENT进行了一次校验。
上面两个方法还改变了原有方法的返回值。
小结:HashMap的主要还是关注如何使用组合方式实现扩展,使用一个共享的成员变量作为填充值。以及确定HashMap初始容量的方式Max(实际数量/0.75f+1 , 默认大小)
TreeSet
基于TreeMap实现,非线程安全,元素有序,不允许插入null值。一般在需要对非重复元素进行排序时使用TreeSet。
存储结构
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<E,Object>());
}
}
复制代码
TreeSet的存储结构和HashSet类似,底层实现是TreeMap,确切来说是一个NavigableMap实现类。
复用思路
TreeSet 中复用基类有两种方式,第一种方式是直接包装使用基础类中的功能,如下所示:
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
复制代码
第二种方式是在TreeSet中定义了接口规范,让底层类来实现接口,如下所示:
public Iterator<E> iterator() {
// map类中持有KeySet<K>类型的成员变量
return m.navigableKeySet().iterator();
}
// KeySet的实现在TreeMap中,实现了NavigableSet接口。
static final class KeySet<E> extends AbstractSet<E> implements NavigableSet<E> {
}
// NavigableSet接口是Set这个体系中的接口规范
public interface NavigableSet<E> extends SortedSet<E> {}
复制代码
综上就是NavigableSet接口定义了TreeSet迭代规范,TreeMap中的子类实现该接口。
小结:如果组合类对外提供的方法可以通过基础类中的方法组合调用实现,就用思路一实现。但如果组合类对外提供的api无法直接使用基础类中现有的方法实现,那就需要基础类提供更深层的支持,组合类提供接口规范,让基础类实现,组合类不用关心实现细节,按接口规范调用基础类中的实现即可。