码迷,mamicode.com
首页 > 编程语言 > 详细

effective java 读书笔记——类和接口

时间:2015-04-14 16:08:20      阅读:205      评论:0      收藏:0      [点我收藏+]

标签:

上周因为准备考试等一堆原因,没空看书,今天补上一点。

 

类和接口是java程序设计语言的核心,它们也是java语言的基本抽象单元,java语言提供了很多强大的基本元素,供程序员设计类和接口,这一章讲的是一些指导原则,可以设计出更加有用,健壮和灵活的类和接口。

第1条:使类和成员的可访问性最小化

首先说一个概念:模块之间只能通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念叫做“信息隐藏”,或者“封装”。(对,这就是面向对象的中封装继承多态三大特性之一的封装)

信息隐藏之所以重要,是因为:它可以有效的解除系统各模块之间的耦合关系,使得这些模块可以独立的开发,测试,优化,使用,理解和修改。这样可以加快系统开发的速度,同时也减轻了维护的负担。

Java语言提供了许多机制来协助信息隐藏。访问控制机制决定了类,接口和成员的可访问性。实体的可访问性是由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符(private,protected,public)共同决定的。第一规则是:尽可能使每个类或者成员不被外界访问。

对于成员,有4种访问级别,按照可访问性递增顺序排列:

private: 只在声明该成员的顶层 类内部才可以访问。

package private: 声明该成员的包内部的任何类都可以访问该成员,是缺省的访问级别(即如果没指定访问级别,就默认是package private的。)

protected:声明该成员的类的子类可以访问该成员,并且,声明该成员的包内部的任何类都可以访问。

public:在任何地方都可以访问该成员。

在判定一个成员的访问级别时,不仅要考虑我们想要这个成员暴露的程度,还有一些注意事项:

1.实例域绝对不能是public的。一旦使实例域成为共有,就放弃了读存储在这个域中的值进行限制的能力。或者说,对于一个实例域来说,它自己内部的属性应该由它自己掌控,但是如果它是Public的,那么所有人都可以掌控它的内部的值,可以拿两个国家来想象,如果一个国家内部的事务被另一个国家插手了,这感觉好丧权辱国。。。所以,对象也是有自尊的!绝对不能把它声明成Public。

2.对于final域,如果final修饰的是常量的话,可以通过public static final 来暴露这些常量。但如果final修饰的是引用的话,就不能用Public了,因为final的本意是不能修改,虽然引用的本身不会改变,但是它指向的对象却可能发生变化,这就违背了final的本意。

3.注意,长度非0的数组总是可变的,所以,类具有public static final的数组域,或是返回这种数组域的访问方法,这总是错误的。因为这样客户端能够修改数组中的内容,这是安全漏洞的一个常见原因。例如:

public static fnal Thing[] VALUES=  {...};   //这样是错的

可以这样改进:

private static final Thing[] PRIVATE_VALUES={...};
public static final List<Thing> VALUES = 
     Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

或者这样:

private static final Thing[] PRIVATE_VALUES={...};
public static final Thing[] values(){ 
    return PRIVATE_VALUES.clone();
}

第2条:在共有类中使用访问方法而非共有域

这一条很简单,就是尽量使用getter和setter方法来访问共有类中的数据,而不是让数据自己public。例子:

class Point{  //这是不好的写法
    public double x;
    public double y;
}
class Point{
//这是较好的写法,使用访问方法访问共有域
    private double x;
    private double y;
    public Point(double x,double y){
        this.x = x;
        this.y = y;
    }
    public double getX(){ return x;}
    public double getY(){  return y;}
    public void setX(double x){this.x = x;}
    public void setY(double y){ this.y = y;}    
}

第3条:使可变性最小化

首先介绍不可变类:不可变类是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。Java中有许多不可变的类,比如String,BigInteger,BigDecimal。不可变类比可变类更易于设计,实现和使用,它们不容易出错,而且更加安全。

设计不可变类有下面5个原则:

1.不会提供任何会修改对象状态的方法。

2.保证类不会被扩展。(可以声明为final)

3.使所有域都是final的。

4.使所有的域都是私有的。这样可以防止客户端获得被访问域的引用的可变对象的权限,并防止客户端直接修改这些对象。

5.确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用,并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。(比如你家养了条狗,你肯定不会希望别人可以随随便便就把它牵走吧,或者是你出门上班,走的时候家里是条萨摩耶,回来时候变成哈士奇了,请别哭晕在厕所……)

这里有一个不可变类的例子,复数,但是太长了我懒得打……好吧,来个一小部分

public final class Complex{
    private final double re;
    private final double im;

    public Complex(double re,double im){...}
    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);
    }
    ...
    @Override public boolean equals(Object o){...}
    @Override public int hashCode(){...}
    @Override public String toString(){...}

那么,这个类就在这里啦,注意看这里的基本算数运算:加减乘除。它们创建并返回新的实例,而不是修改当前实例,这种方法被称作functional方法,因为这些方法返回一个函数的结果,这些函数对操作数进行运算但并不修改它。与之对应的是“过程的procedural”方法,会导致操作数的状态发生改变。

不可变对象本质上是线程安全的,他们不要求同步,所以,不可变对象可以被自由的共享。不仅尅共享不可变对象,甚至也可以共享它们的内部信息。

不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。创建这种对象的代价可能很高。如果你执行一个需要很多步骤的操作,每个步骤会产生一个新的对象,但是我们只用最后的结果,其他对象最终都会被丢弃,这时会产生性能问题。处理这个问题有2种方法:1.猜测常用的操作,把它们作为i基本类型提供。2.提供一个共有的可变配套类。比如String和StringBuilder。

 

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

第4条:复合优先于继承

与方法调用不同的是,继承打破了封装性,换句话说,子类依赖于父类中某些功能的实现细节。父类的实现可能因为版本的变化有所变化,那么子类的功能可能会被破坏,即使它的代码并没有改变。

复合是不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。现有的类变成了新类的一个组件。

复合中,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这被称为“转发”。比如

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.contans(o);}//转发
    public boolean isEmpty(){return s.isEmpty();}//转发
    ...
}

继承可能导致的问题:1.子类中的函数调用了父类的super.f(),而父类的f()中其实又调用了父类的f2()函数,这种“自用性”是实现细节,不是承诺,不能保证Java平台的所有实现都不变,可能会因为版本不同而发生改变。

2.如果在子类中添加了一个父类中没有的类,但是不巧,在下一版本中父类也有了同名的类,那么可能会变成一个覆盖方法,或重载方法,又或者,子类中的这个方法无法遵守父类中方法的约定。

如果在适合用复合的地方使用了继承,则会暴露实现细节,这样得到的API会把你限制在原始的实现上,永远限定了类的性能。更严重的是,由于暴露了内部细节,客户端有可能就直接访问这些细节,会导致语义上的混淆,甚至直接修改超类,从而破坏子类的约束条件。

在决定使用继承而不是符合之前,还应问自己最后一组问题:对于你试图扩展的类,它的AI中有没有缺陷?如果有,你是否愿意把那些缺陷传播到类的API中?

总之,只有确实是is a 关系时,才应该使用继承。

第5条:要么为继承而设计,并提供文档说明,要么就禁止继承

对于为了继承设计的类,文档必须全面:

1.文档必须精确描述覆盖每个方法所带来的影响。对于每个公有的或者受保护的方法或构造器,文档必须指明该方法调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的。

2.必须在文档中说明,哪些情况下它会调用可覆盖的方法。

对于为了继承而设计的类,唯一的测试方法就是编写子类。

第6条:接口优于抽象类

接口与抽象类的区别在于,抽象类允许包含某些方法的实现,但是接口不允许。

1.现有的类可以很容易被更新,以实现新的金额口。

2.接口是定义混合类型的理想选呢。

3.接口允许我们构造非层次结构的类型框架。

通过对你导出的每个重要接口都提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。编写骨架实现类必须认真研究接口,确定哪些方法是最基本的,其他的方法可以根据它们来实现,这些基本方法将成为骨架实现类中的抽象方法。然后,为接口中的其他方法提供具体的实现。

第7条:接口只用于定义类型

 当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了其他任何目的而定义接口是不恰当的。

之所以说接口应该只用于定义类型,是因为Java中有一种对接口的不良使用:常量接口。这是反面的典型,比如java.io.ObjectStreamConstants。不值得消防。

effective java 读书笔记——类和接口

标签:

原文地址:http://www.cnblogs.com/cangyikeguiyuan/p/4415507.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!