Collection集合是单列集合,其下有两个分支,分别是List集合和Set集合。
List集合,可以存放重复的元素,比如其下的ArrayList集合。
Set集合,不可以存放重复的元素,比如其下的HashSet集合。
本文主要探讨这个“重复”针对的是什么。
先来准备一个Person类,下文将用Person对象作为集合元素。
public class Person {
private int age;
public Person(int age) {
this.age = age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" + "age=" + age + '}';
}
}
集合为了存放元素,一般使用add方法来追加元素,所以下面分析一下add方法。
List集合的可重复性
在ArrayList集合中,最基本的add方法的相关源码:
transient Object[] elementData; // 元素数组
private int size; // 元素数量
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 根据元素数量控制元素数组的容量
elementData[size++] = e; // 元素数量加1,且新元素追加到元素数组中
return true;
}
可以看出来,ArrayList集合在新增元素时,对新元素是否重复,没有任何相关的判定,直接就给新增了,符合了List集合的可重复性特征。
Set集合的不可重复性
在HashSet集合中,最基本的add方法的相关源码:
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object(); //
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
Set集合是单列集合,但在底层是用HashMap双列集合来实现,PRESENT属性仅仅为了拿来占用其中一列。
再看一下HashMap集合的相关方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 与新元素的hashCode方法有关
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 与新元素的equals方法有关
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 与新元素的equals方法有关
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
我们可以看出来,HashSet集合追加新元素的主要逻辑就在HashMap集合的putVal方法里。
更能看出来,判断新元素是否重复,是与新元素的hashCode方法和equals方法有关。
再细心一点还能看出来,判断新元素是否重复,是先判断hashCode是否重复,如果重复,再用equals方法判断是否重复。
于是,Person类通过重写父类Object的hashCode方法和equals方法,就可以自定义不可重复性的规则,例如:
- 重写hashCode方法,hashCode不再由内存地址产生,而是由age产生。
- 重写equals方法,不再判断内存地址,而是判断对象内容。
public class Person {
private int age;
public Person(int age) {
this.age = age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" + "age=" + age + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age;
}
@Override
public int hashCode() {
return Objects.hash(age);
}
}
于是就可以得到如下的理想情况:
public static void main(String[] args) {
Person person1 = new Person(30);
Person person2 = new Person(30);
Set set = new HashSet();
set.add(person1); // 第一次追加30岁的人,能追加
set.add(person2); // 已经追加过30岁的人了,不能再追加
System.out.println(set);
// 输出结果为:[Person{age=30}]
}
但是这里有个坑,其实重复的条件是“曾经追加过”,而不是集合中拥有。
不信来看下面的情况:
public static void main(String[] args) {
Person person = new Person(30);
Set set = new HashSet();
set.add(person); // 第一次追加30岁的人,能追加
person.setAge(20); // 岁数改了,不再是30岁的人
set.add(person); // 第一次追加20岁的人,能追加
System.out.println(set);
// 输出结果为:[Person{age=20}, Person{age=20}]
}
这种情况就是对象在中途改内容了,然后再被追加成功的情况。
最终Set集合内,拥有两个元素,且都指向了同一个对象,但是,第一个元素的hashCode是由30产生的,第二个元素的hashCode是由20产生的,所以集合内存放的这两个元素依然是不重复的。