《Effective Java》------类和接口(1)

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/hanxueyu666/article/details/78641607

一、使类和成员的访问性最小

设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只能通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为信息隐藏,是软件的基本原则之一。

尽可能地使每个类或者成员不被外界访问

对于顶层(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的和公有的。如果类和接口能够被做成包级私有的,它就应该被做成包级私有。通过把类或者接口做成包级私有,它实际上成了这个包的实现的一部分,而不是该包导出的API的一部分,在以后的发型版本中,可以对它进行修改、替换。或者删除,而无需担心会影响到现有的客户端程序。如果你把它做成公有的,你就有责任永远支持它,以保持它们的兼容性

总而言之,你应该尽可能地降低可访问性。你仔细地设计了一个最小的公有的API之后,应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情况之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都不是可变的

二、在公有类中使用访问方法而非公有域

对于可变的域,可以用get、set方法,而不是直接暴露成公有域。

三、使可变性最小化

不可变的类只是其实例不能被修改的类。每个实例中包含的所有信息必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变

成为不可变类,要遵循下面五条规则
1、不要提供任何会修改对象状态的放法
2、保证类不会被扩展。
一般做法是使这个类成为final的
3、使所有的域都是final
4、使所有的域都成为私有的
5、确保对于任何可变组件的互斥访问
如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用


public class Complex {  
  
    private final double re;  
    private final double im;
  
    /** 
     * 创建一个新的实例  Complex 
     * 
     * 
     */  
    public Complex (double re,double im) {  
        this.re=re;  
        this.im=im;  
    }  
  
    //Accessor with no corresponeding mutators  
    public double realPart(){return re; }  
  
    public double imaginaryPart(){ return im;}  
  
     
    public Complex add(Complex c){  
        return new Complex(re+c.re,im+c.im);  
    }  
  
    public Complex subtract(Complex c){  
        return  new Complex(re-c.re,im-c.im);  
    }  
  
    public Complex multiply(Complex c){  
        return  new Complex(re*c.re-im*c.im,  
                re*c.im+im*c.re);  
    }  
  
    public Complex divide(Complex c){  
        double tmp=c.re*c.re+c.im*c.im;  
        return  new Complex(  
                (re*c.re+im*c.im)/tmp,  
                (im*c.re-re*c.im)/tmp);  
    }  
  
    @Override  
    public boolean equals(Object obj) {  
        if(obj == this)return true;  
        if(!(obj instanceof  Complex)) return false;  
        Complex c= (Complex) obj;  
        //See page 43 to find out why we use compare instead off ==  
        return Double.compare(re,c.re)==0 && Double.compare(im,c.im)==0;  
    }  
  
    @Override  
    public int hashCode() {  
        int result=17+hashDouble(re);  
        result=31*result+hashDouble(im);  
        return result;  
    }  
  
    private int hashDouble(double val){  
        long longBits =Double.doubleToLongBits(re);  
        return (int)(longBits ^ ( longBits >>> 32 ));  
    }  
  
    @Override  
    public String toString() {  
        return "("+re+"+"+im+"i)";  
    }  
}  


不可变对象本质上是线程安全的,它们不要求同步

不可变类真正唯一的缺点,对于每一个不同的值都需要一个单独的对象。创建这种对象的代价可能很高,特别是对于大型对象的情况。例如,假设你有一个上百万位的BigInteger,想要改变它的地位:

BigInteger moby = ...;
moby = moby.flipBIt(0);

flitBit方法创建了一个新的BigInteger实例,也有上百万位长,它与原来的对象只差一位不同。这项操作所消耗的时间和空间与BigInteger的成正比



坚决不要为每个get方法编写一个相应的set方法。除非有很好地理由要让类成为可变的类,否则就应该是不可变的。

如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性,除非令人信服的理由要是域变成是非final的,否则要使每个域都是final的

构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这样做


四、复合优先于继承

继承(inheritance)是实现代码重用的有力手段,但它并非永远是完成这项任务的最佳工作。使用不当会导致软件变得很脆弱。在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处于同一个程序员的控制下。对于专门为了继承而设计的并且具有很好的文档说明的类来说,使用继承也是非常安全的。然而,对于普通的具体类进行跨超包边界的继承则是非常危险的。本条目并不适用于接口继承(一个类实现一个接口,或者一个接口扩展另一个接口)。
        与方法调用不同的是,继承打破了封装性。子类信赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有变化,子类有可能会被破坏。因而子类必须跟着其超类的更新而变化,除非超类是专门为了扩展而设计的,并且具有很好地文档。且看下面的例子:

// Broken - Inappropriate use of inheritance!  
public class InstrumentedHashSet<E> extends HashSet<E> {  
    // 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(Collections<? extends E> c) {  
        addCount += c.size();  
        return super.addAll(c);  
    }  
    public int getAddCount() {  
        return addCount;  
    }  
}  

 这段代码看起来没有什么问题,我们用addCount来跟踪添加的元素的数量,但是
InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();  
s.addAll(Arrays.asList("a", "b", "c");  
        当执行完这句代码后,使用s.addCount来返回加入的元素个数时却显示为6,问题在于,addAll方法会调用add方法来完成功能,在HashSet超类链中,AbstractCollection实现了addAll方法,代码如下:
public boolean addAll(Collection<? extends E> c) {  
    boolean modified = false;  
    for (E e : c)  
        if (add(e))  
            modified = true;  
    return modified;  



可见,当使用addAll方法添加元素时addCount增加了3,而后在addAll方法内部又迭代的调用了add方法,addCount又增加了3,结果为addCount变成了6。为了修改这个问题,可以去年被覆盖的addAll方法,但是它的功能正确性需要信赖于HashSet的addAll方法是在add方法上实现的这一事实,这种自用性(self-use)是实现细节,而不是承诺,不能保证在java平台的所有实现中都保持不变,不能保证随着上发行版本的不同而不发生变化。
        导致子类脆弱的一个相关的原因是:它们的超类在后续的发行版本中可以获得新的方法,假设一个程序的安全性信赖于这样的事实:所有被插入到某个集合的元素都满足某个先决条件。下面的做法就可以确保这一点:对集合进行子类化,并覆盖所有能够添加元素的方法以便确保在加入每个元素之前它是满足这个先决条件的。如果在后续的发行版本中,超类中没有增加能插入元素的新方法,那么这种方法可以正常工作。然而,一旦超类增加了这样的新方法,则很可能仅仅由于调用了这个未被子类覆盖的新方法,而将不合法的元素添加到子类的实例中。
        这两个问题的来源都是因为“覆盖”。如果在扩展一个类的时候仅仅是增加新的方法而不覆盖现有的方法,这也许看来相对安全一些,但是设想一下,如果超类在后续的发行版本中获得了一个新方法,并且和子类中的某一方法只是返回类型不同,那么这样的子类将针法通过编译。如果给子类提供的方法带有与新的超类方法完全相同的方法(签名和返回类型都相同),这又变成了子类覆盖超类的方法问题。此外,子类的方法是否则够遵守新的超类的方法的约定也是个值得怀疑的问题,因为当编写子类方法的时候,这个约定根本还没有面世。
        使用“复合(composition)”可以解决上述的问题,不用扩展现有的类,而是在新的类中增加一个私有域。通过“转发”来实现与现有类的交互,这样得到手类将会非常稳固。它不信赖于现有类的实现细节。即使现有的类增加了新方法,也不会影响到新类。请看如下的例子:
// Wrapper class - uses composition in place of inheritance  
public class InstrumentedSet<E> extends ForwardingSet<E> {  
    private int addCount = 0;  
      
    public InstrumentedSet(Set<E> s) {  
        super(s);  
    }  
      
    @Override  
    public boolean add(E e) {  
        addCount++;  
        return super.add(e);  
    }  
      
    @Override  
    public boolean addAll(Collection<? extends E> c) {  
        addCount += c.size();  
        return super.addAll(c);  
    }  
      
    public int getAddCount() {  
        return addCount;  
    }  
}  
  
// Reusable forwarding class  
public class ForwardingSet<E> implements Set<E> {  
    private final Set<E> s;  
    public ForwardingSet(Set<E> 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<E> 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<? extends E> 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> 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(); }  
}  


总而言之,继承功能非常强大,但是也存在诸多问题,因为它违背了封装原则。只有当子类真正是超类的子类型时,才适合继承。换句话说,对于两个类A和B,只有当两者之间确实存在is--a关系时,类B才应该扩展A。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性,为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大

五、要么为继承二设计,并提供文档说明,要么就禁止继承


猜你喜欢

转载自blog.csdn.net/hanxueyu666/article/details/78641607