java中的集合浅析3——番外篇

equals()与hashCode()

我们知道,java中所有的类都有一个父类,就是Object类。
Object类中有两个方法:

1
2
boolean equals(Object obj);
int hashCode();

equals()方法

equals()方法的源码如下:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

可以看到,默认的equals()方法就是比较两个对象的内存地址。
若object1.equals(object2)为true,则表示equals1和equals2实际上是引用同一个对象。
Object的equals()方法能满足我们的基本需求,但如果我们需要比较一些自定义的对象的时候,就需要对其进行重写了。
JDK中的String,Math等封装类都对equals()进行了重写。
下面是String的equals()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}

可以看到,String的equals()方法在引用比较的基础上,还进行了内容的比较。
在Java规范中,它对equals()方法的使用必须要遵循如下几个规则:

  1. 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
  2. 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
  3. 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
  4. 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
  5. 非空性:对于任何非空引用值 x,x.equals(null) 都应返回 false。

还有一个注意点,涉及到继承时,覆写equals时推荐使用getClass进行类型判断,而不是使用instanceof。

hashCode()方法

hashCode()方法返回了一个Object的哈希值。
这个值可以被java中的哈希数据结构利用(HashMap和HashTable等),用来在散列存储结构中确定对象的存储地址。
一个好的hash函数应该是这样的:为不相同的对象产生不相等的hashCode。
Object的hashCode()方法默认是native方法。
默认的hashCode实现一般是内存地址对应的数字,所以不同的对象,hashCode()的返回值是不一样的。
String类中定义的hashcode()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int hashCode() {  
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;

for (int i = 0; i < len; i++) {
h = 31 * h + val[off++];
}
hash = h;
}
return h;
}

equals()与hashCode()关系

按照java设计要求:

  • 当equals(object)相同时,hashCode()的返回值也要尽量相同;
  • 当equals(object)不相同时,hashCode()的返回没有特别的要求,但是也是尽量不相同以获取好的性能。

推荐是修改equals()的同时也修改hashCode()。
看一下HashMap中的比对两个对象的算法就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
final Entry<K,V> More ...getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}

浅拷贝深拷贝

  • 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。
    换言之,浅拷贝仅仅复制所拷贝的对象,而不复制它所引用的对象。
  • 深拷贝:被复制对象所有非引用变量都含有与原来的对象相同的值,同时所有引用类型的成员变量指向了新创建的实例,这些实例被初始化为形式参数实例值。
    换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

也就是说浅拷贝只复制一个对象,传递引用;而深拷贝对对象内部的引用均复制,它是创建一个新的实例,并且复制实例。

Object的clone()方法

在运行时刻,Object中的clone()识别出你要复制的是哪一个对象,然后为此对象分配空间,并进行对象的复制,将原始对象的内容一一复制到新对象的存储空间中。

1
2
3
4
5
Person p = new Person();  
Person p1 = p; // p与p1的地址完全相同,指向的是同一个对象

Person p = new Person();
Person p1 = (Person) p.clone(); // p与p1的地址不同,指向两个不同的对象

继承自java.lang.Object类的clone()方法是浅复制

实现深拷贝

实现Cloneable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Employer implements Cloneable{
private String username;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

class Employee implements Cloneable{
private String username;
private Employer employer;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Employer getEmployer() {
return employer;
}
public void setEmployer(Employer employer) {
this.employer = employer;
}

@Override
public Object clone() throws CloneNotSupportedException {
//克隆Employee对象并手动的进一步克隆Employee对象中包含的Employer对象
Employee employee = (Employee)super.clone();
employee.setEmployer((Employer) employee.getEmployer().clone());
return employee;
}
}

注意,如果要实现彻底的深拷贝,就要将引用链上的每一级对象都进行深拷贝。
比如,类C中有类B的引用,类B中有类A的引用,类A中只有非引用变量,这个时候,要实现彻底的对C类的深拷贝,除了C类实现Cloneable,B类也要实现Cloneable,A类可以不实现。

实现Serializable接口

序列化即是把对象写到流里面的过程;反序列化即是把对象从流中读取出来的过程。
写在流里的是对象的一个拷贝,而原来的对象仍然在JVM里面。
实现Serializable的前提是对象以及对象内部所有用到的对象都是可序列化的,否则就需要考虑把那些不可序列化的对象标记为transient,从而把它排除到复制范围之外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Employer2 implements Serializable{
private static final long serialVersionUID = 1L;
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

class Employee2 implements Serializable{
private static final long serialVersionUID = 3969438177161438988L;
private String name;
private Employer2 employer;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Employer2 getEmployer() {
return employer;
}
public void setEmployer(Employer2 employer) {
this.employer = employer;
}
/**
* 实现深复制的方法
*/

public Object deepCopy() throws IOException, ClassNotFoundException{
//字节数组输出流,暂存到内存中
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//序列化
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
//反序列化
return ois.readObject();
}
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Employer2 employer = new Employer2();
employer.setName("arthinking");
Employee2 employee = new Employee2();
employee.setName("Jason");
employee.setEmployer(employer);
//通过深复制创建employee2
Employee2 employee2 = (Employee2) employee.deepCopy();
employee2.getEmployer().setName("Jason");

System.out.println(employee.getEmployer().getName());
System.out.println(employee2.getEmployer().getName());
}

关于serialVersionUID

serialVersionUID主要是为了解决对象反序列化的兼容性问题。
如果没有提供serialVersionUID,对象序列化后存到硬盘上之后,再增加或减少类的filed,当反序列化时,就会出现Exception,造成不兼容问题。
但当serialVersionUID相同时,它就会将不一样的field以type的缺省值反序列化。这样就可以避开不兼容问题了。
serialVersionUID是long类型的,一般有两种方式生成。

  • 默认的是1L:

    1
    private static final long serialVersionUID = 1L;
  • 另外一个则是根据类名、接口名、成员方法以及属性等生成一个64位的哈希字段:

    1
    private static final long serialVersionUID = 3969438177161438988L;

ArrayList的拷贝

  • 使用clone()方法,比如ArrayList newArray = oldArray.clone();
  • 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
  • 使用Collection的copy方法

注意,前两者是浅拷贝

fail-fast机制

fail-fast机制是java集合(Collection)中的一种错误机制。
当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

原理

我们来看看ArrayList如何抛出ConcurrentModificationException异常。
ConcurrentModificationException是在操作Iterator时抛出的异常。
ArrayList的Iterator是在父类AbstractList.java中实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package java.util;

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

...

// AbstractList中唯一的属性
// 用来记录List修改的次数:每修改一次(添加/删除等操作),将modCount+1
protected transient int modCount = 0;

// 返回List对应迭代器。实际上,是返回Itr对象。
public Iterator<E> iterator() {
return new Itr();
}

// Itr是Iterator(迭代器)的实现类
private class Itr implements Iterator<E> {
int cursor = 0;

int lastRet = -1;

// 修改数的记录值。
// 每次新建Itr()对象时,都会保存新建该对象时对应的modCount;
// 以后每次遍历List中的元素的时候,都会比较expectedModCount和modCount是否相等;
// 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size();
}

public E next() {
// 获取下一个元素之前,都会判断“新建Itr对象时保存的modCount”和“当前的modCount”是否相等;
// 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}

public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();

try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

...
}

可以发现在调用next()和remove()时,都会执行checkForComodification()。
若 “modCount不等于expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。
而在ArrayList中,无论是add()、remove(),还是clear(),只要涉及到修改集合中的元素个数时,都会改变modCount的值。

解决方案

在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
例如ArrayList对应的就是CopyOnWriteArrayList。
而HashMap则是用上文提过的ConcurrentHashMap。

Iterator和Enumeration

在Java集合中,我们通常都通过 “Iterator(迭代器)” 或 “Enumeration(枚举类)” 去遍历集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package java.util;

public interface Enumeration<E> {

boolean hasMoreElements();

E nextElement();
}

package java.util;

public interface Iterator<E> {
boolean hasNext();

E next();

void remove();
}

它们有什么区别呢?

  • Enumeration 是JDK 1.0添加的接口。
    使用到它的函数包括Vector、Hashtable等类,这些类都是JDK 1.0中加入的,Enumeration存在的目的就是为它们提供遍历接口。
    Enumeration本身并没有支持同步,而在Vector、Hashtable实现Enumeration时,添加了同步。
  • Iterator 是JDK 1.2才添加的接口,它是为了HashMap、ArrayList等集合提供遍历接口。
    Iterator是支持fail-fast机制的:当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

因为Iterator支持fail-fast机制,所以要进行的步骤会多一些,所以Enumeration比Iterator的遍历速度更快。