Chinaunix首页 | 论坛 | 博客
  • 博客访问: 359075
  • 博文数量: 76
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2363
  • 用 户 组: 普通用户
  • 注册时间: 2013-07-21 22:30
文章分类
文章存档

2014年(38)

2013年(38)

分类: Java

2014-07-25 13:40:19

继承是实现代码重用的方法之一,但使用不当则会导致诸多问题。


继承会破坏封装性,对一个具体类进行跨包访问级别的继承很危险。

即,子类依赖父类的实现细节。

如果父类的实现细节发生变化,子类则可能遭到破坏。



举个例子,扩展HashSet,记录HashSet实例创建以来一共进行了多少次添加元素的操作。

HashSet有两个添加元素的方法——add(E e)和addAll(Collection c)。

那就覆盖这两个方法,在添加操作执行前记录次数:
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
    
public class InstrumentedHashSet extends HashSet {
    // The number of attempted element insertions
    private int addCount = 0;
 
    public InstrumentedHashSet() {
    }
 
    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }
 
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
 
    @Override
    public boolean addAll(Collection c) {
        addCount += c.size();
        return super.addAll(c);
    }
 
    public int getAddCount() {
        return addCount;
    }
}


测试一下,通过addAll方法添加3个元素:
1
2
3
4
5
    
    public static void main(String[] args) {
        InstrumentedHashSet s = new InstrumentedHashSet();
        s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
        System.out.println(s.getAddCount());
    }


结果是:

wKioL1PLrWjBf_fsAADIJ3WV64w978.jpg


导致这种结果的原因很简单。

参考AbstractCollection中的add(E e)和addAll(Collection c),

add(E e)中只有一段throw new UnsupportedOperationException();

而addAll(Collection c)的文档注释中有这么一段话:

*

Note that this implementation will throw an

* UnsupportedOperationException unless add is

* overridden (assuming the specified collection is non-empty).



解决这个问题的方法很简单,只需要去掉覆盖的addAll方法即可。

但这样却不能解决根本问题,即HashSet的addAll方法不保证在以后的发行版本中不发生变化。

即,子类实现依赖父类实现,父类发生变化时子类遭到破坏。

也许我们可以覆盖父类方法重新实现,虽然解决问题,但这样费力不讨好,毫无意义。



另外,父类增加或者移除方法也会对子类产生影响。

举个例子,子类扩展了某个集合类,覆盖了所有添加元素的方法,在添加元素之前对元素进行检查,让所有元素满足某个条件。

如果在后来的版本中,父类增加了新的添加元素的方法,而子类没有覆盖该方法,导致非法元素添加到集合中。



反之,也有可能出现这种情况。

即便父类的实现没有问题,但也可以因为子类实现不当而破坏父类的约束。

比如,父类恰好增加了和子类相同签名和返回类型的方法。



于是,为了应对这些情况,可以使用复合模式(composition)代替继承。

即,在一个forwarding class中增加一个private field引用现有类的实例,forwarding class中的方法对应现有类的方法。

代码如下:
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
70
71
72
73
74
75
76
77
78
    
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
 
public class ForwardingSet implements Set {
    private final Set s;
 
    public ForwardingSet(Set s) {
        this.s = s;
    }
 
    public void clear() {
        s.clear();
    }
 
    public boolean contains(Object o) {
        return s.contains(o);
    }
 
    public boolean isEmpty() {
        return s.isEmpty();
    }
 
    public int size() {
        return s.size();
    }
 
    public Iterator iterator() {
        return s.iterator();
    }
 
    public boolean add(E e) {
        return s.add(e);
    }
 
    public boolean remove(Object o) {
        return s.remove(o);
    }
 
    public boolean containsAll(Collection c) {
        return s.containsAll(c);
    }
 
    public boolean addAll(Collection c) {
        return s.addAll(c);
    }
 
    public boolean removeAll(Collection c) {
        return s.removeAll(c);
    }
 
    public boolean retainAll(Collection c) {
        return s.retainAll(c);
    }
 
    public Object[] toArray() {
        return s.toArray();
    }
 
    public T[] toArray(T[] a) {
        return s.toArray(a);
    }
 
    @Override
    public boolean equals(Object o) {
        return s.equals(o);
    }
 
    @Override
    public int hashCode() {
        return s.hashCode();
    }
 
    @Override
    public String toString() {
        return s.toString();
    }
}


使用时直接继承forwarding class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    
public class InstrumentedSet extends ForwardingSet {
    private int addCount = 0;
 
    public InstrumentedSet(Set s) {
        super(s);
    }
 
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
 
    @Override
    public boolean addAll(Collection c) {
        addCount += c.size();
        return super.addAll(c);
    }
 
    public int getAddCount() {
        return addCount;
    }
}


forwarding class通过Set接口提供了相应方法。

这种设计也有其灵活性,继承只能选择Set的某个特定实现,但使用复合我们可以选择任何接口实现。

比如:
1
2
3
4
5
6
    
public static void main(String[] args) {
    InstrumentedSet s = new InstrumentedSet(
            new HashSet());
    s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
    System.out.println(s.getAddCount());
}



如何从继承和复合之间做出选择?

比较抽象的说法是:只有子类和父类确实存在"is-a"关系的时候使用继承,否则使用复合。

或者比较实际点的说法是,如果TypeB只需要TypeA的部分行为,则考虑使用复合。

阅读(2290) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~