Effectiive java 读书笔记(二)

第四章:类和接口


(1)使类和成员的可访问性最小化

   好的设计模块对于外部的其它模块隐藏了其内部的实现数据和其它实现细节,然后模块之间只能通过API进行通信,一个块不需要知道其它块的内部工作情况,这称为信息隐藏。

   JAVA提供很多机制来实现信息隐藏,比如访问控制机制:实体的可访问性是由该实体声明所在的位置,以及该实体声明所出现的访问修饰符共同决定的.

一条重要规则:尽可能地使每一个类或者类成员不被外界访问。     

      注意一点:有一条规则限制了降低方法的可访问性的能力。如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别(访问级别 从低到高:private,package-private,protected,public),这样可以确保任何使用超类实例的地方也可以使用子类


(2)在公有类中使用访问方法而非公有域

公有类永远都不应该暴露可变的域,让公有类直接暴露域虽然从来都不是好办法,但是如果域不可变,做法的危害性会小一点

(3)使可变性最小化

为了使类成为不可变的五条规则:

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

2.保证类不会被扩展

3.使所有域都是final的

4.使所有的域都成为私有的

5.确保对于任何可变组件的互斥访问

坚决不要为每个get方法编写一个相应的 set 方法。除非你有很好的理由要让类成为可变的类如果类不能被被做成是不可变的,仍然一个尽可能限制它的可变性构造器应该创建完全初始化的对象,并建立起所有的约束关系,不要在构造器或者静态工厂外再提供公有的初始化方法

(4)复合优先于继承

与方法调用不同,继承打破了类的封装特性:

1.超类的实现有可能会随着发行版本的不同而有所变化,如果变化,子类可能遭到破坏

2.如果超类在后续的版本中可以获得新的方法,而不覆盖现有的方法,子类可能产生安全问题.如果子类提供好了 一个签名相同但返回类型不同的方法,子类将无法编译通过

//这里我们需要扩展HashSet类,提供新的功能用于统计当前集合中元素的数量,
//实现方法是新增一个私有域变量用于保存元素数量,并每次添加新元素的方法中
//更新该值,再提供一个公有的方法返回该值。
    public class InstrumentedHashSet<E> extends HashSet<E> {
        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<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
        public int getAddCount() {
            return addCount;
        }
    }
      该子类覆盖了HashSet中的两个方法add和addAll,而且从表面上看也非常合理,然而他却不能正常的工作,见下面的测试代码

public static void main(String[] args) {
         InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
        s.addAll(Arrays.asList("Snap","Crackle","Pop"));
         System.out.println("The count of InstrumentedHashSet is " + s.getAddCount());
     }
    //The count of InstrumentedHashSet is 6
从输出结果中可以非常清楚的看出,我们得到的结果并不是我们期望的3,而是6。这是什么原因所致呢?在HashSet的内部,addAll方法是基于add方法来实现的,而HashSet的文档中也并未列出这样的细节说明。了解了原因之后,我们应该取消addAll方法的覆盖,以保证得到正确的结果。然而仍然需要指出的是,这样的细节既然未在API文档中予以说明,那么也就间接的表示这种未承诺的实现逻辑是不可依赖的,因为在未来的某个版本中他们有可能会发生悄无声息的发生变化,而我们也无法通过API文档获悉这些。还有一种情况是超类在未来的版本中新增了添加新元素的接口方法,因此我们在子类中也必须覆盖这些方法,同时也要注意一些新的超类实现细节。由此可见,类似的继承是非常脆弱的,那么该如何修订我们的设计呢?答案很简单,复合优先于继承, 复合的实现方法如下:

如果要编写一个类的话,最好是将其要扩展的类作为新类的一个域,即在新的类中增加一个私有域,它引用现有类的一个实例,这种设计也叫做复合,因为现有的类变成了新类的一个组件。新类中 每个实例方法都可以调用被包含现有类实例中对应的方法,并返回它的结果(称为转发)

这个实现分为两个部分:类本身和可重用的转发类,包含了所有的转发方法如下代码:

public class InstrumentedSet<E>implements Set<E> {

   private Set set;

 

   public InstrumentedSet(Set<E> set) {

      this.set = set;

   }

 

   @Override

   public int size() {

      return set.size();

   }

 

   @Override

   public boolean isEmpty() {

      return set.isEmpty();

}

……………..

}

调用方式:

Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));

Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));

这相当于是InstrumentedSet将Set给包装了起来。当然InstrumentedSet也叫做包装类。包装类不适合回调,因为回调是将自己引用传递给其他对象,以便其他对象后面回调。因为包装起来的对象并不知道它外面的包装类,所以它传递一个指向自己的引用,回调时就避开了外面的包装对象

这相当于是InstrumentedSet将Set给包装了起来。当然InstrumentedSet也叫做包装类。包装类不适合回调,因为回调是将自己引用传递给其他对象,以便其他对象后面回调。因为包装起来的对象并不知道它外面的包装类,所以它传递一个指向自己的引用,回调时就避开了外面的包装对象

看例子:(参考http://www.cnblogs.com/stephen-liu74/archive/2012/01/18/2228349.htm)

//转发类
    class ForwardingSet<E> implements Set<E> {
        private final Set<E> s;
        public ForwardingSet(Set<E> s) {
            this.s = s;
        }
        @Override public int size() {
            return s.size();
        }
        @Override public void clear() { 
            s.clear(); 
        }
        @Override public boolean add(E e) {
            return s.add(e);
        }
        @Override public boolean addAll(Collection<? extends E> c) {
            return s.addAll(c);
        }
        ... ...
    }
    //包装类
    class InstrumentedHashSet<E> extends ForwardingSet<E> {
        private int addCount = 0;
        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<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
        public int getAddCount() {
            return addCount;
        }
    }

    由上面的代码可以看出,这种设计最大的问题就是比较琐碎,需要将接口中的方法基于委托类重新实

总结:

有当真正确定A和B是“is-a”的时候,才使用到继承,否则尽量将使用复合和转发的方式来实现一个类在决定使用继承而不是复合之间,还应该问自己最后一组问题。对于你试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把这些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。


(5)接口优于抽象类

     1.现在的类可以很容易被更新,以实现新的接口。一般来说,无法更新现有的类来扩展新的抽象类

     2.接口是定义混合类型的理想选择  例如Comparable是一个混合类型接口,它允许任选的功能可被混合到类型的主要功能中,抽象类不能用于定义混合类型。同样也是因为它们不能被更新到现有的类中:类不可能有一个以上的父类,java不提供class的多重继承机制

     3.接口允许我们构造非层次结构类型框具有更高的灵活性

     接口的缺点是不允许包含方法的实现。但是通过对你导出的每个重要接口都提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。如果设计得当,骨架实现可以使程序员很容易提供他们自己的接口实现

 下面是Map.Entry的骨架实现类:

public abstract class AbstractMapEntry implements Map.Entry {
  // Primitives
  public abstract Object getKey();
  public abstract Object getValue();
  //  ...
  // Implements the general contract of Map.Entry.equals
  public boolean equals(Object o) {
    if (o == this) return true;
    if (!o instanceOf Map.Entry))
      return false;
    Map.Entry arg = (Map.Entry) o;
    
    return eq(getKey(), arg.getKey()) &&
        eq(getValue(), arg.getValue());
  }
  
  // Since Object equals was overriden, we better override hashCode!
  public int hashCode() {
    return
      (getKey() == null ? 0: getKey().hashCode()) ^
      (getValue() == null ? 0: getValue().hashCode());
  }

}

使用抽象类来定义允许多个实现的类型,与使用接口相比有个明显优势:抽象类的演变比接口演变要容易得多,一般来说,想要在公有接口中增加方法,而不破坏实现这个接口的所有现有的类,这是不可能的

举例如下(参考 http://blog.csdn.net/jiafu1115/article/details/6741523)

Considering the following code:

//未演化前的代码:

public abstract class AbrBase{

         public void a();

         public void b();

};

public class Sub1 extends AbrBase{

         public void a(){

         }

         public void b(){

         }

};
public interface IBase{

         public void c();

         public void d();

}; 
public class Sub2 implements IBase{

         public void c(){

         }

         public void d(){

         }

};


//进化后代码

public abstract class AbrBase{

         public void a();

         public void b();

public void e(){// 为抽象类添加一新的具体的方法,注意抽象方法也不行

}

}
public class Sub1 extends AbrBase{//在抽象类添加一具体方法后,子类可以不用改动

         public void a(){

         }

         public void b(){

         }

};
public interface IBase{

         public void c();

         public void d();

public void f(); //为接口添加一新的方法
<span style="font-family:Times New Roman;">}</span>

public class Sub2 implements IBase{

         public void c(){

         }

         public void d(){

         }

         public void f(){ //子类必须修改实现新添加的方法,否则编译将不能通过

         }
};


解决办法:提供抽象骨架类

//进化之前代码

public interface IBase{

         public void c();

         public void d();

};

 

public abstract class AbrBase implements IBase{

         //primitives

         public void a();

         public void b();

 

         //implenments the method of IBase interface

         public void c(){

         }

         public void d(){

         }

};

 

public class Sub extends AbrBase{

         public void a(){

         }

         public void b(){

         }

//继承AbrBase对IBase的实现

};

进化后代码:

public interface IBase{

         public void c();

         public void d();

public void f(); //为接口添加一新的方法

};

 

public abstract class AbrBase implements IBase{

         //primitives

         public void a();

         public void b();

 

         //implenments the method of IBase interface

         public void c(){

         }

         public void d(){

         }

         public void f(){ //修改AbrBase以实现IBase新增加的method

         }

};

public class Sub extends AbrBase{//无需改变,继承超类对f()方法的实现

         public void a(){

         }

         public void b(){

         }

//继承AbrBase对IBase的实现

};


简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类


(6)类层次优于标签类

class Figure {
        enum Shape { RECT,CIRCLE };
        final Shape s;  //标签域字段,标识当前Figure对象的实际类型RECT或CIRCLE。
        double length;  //length和width均为RECT形状的专有域字段
        double width;
        double radius;    //radius是CIRCLE的专有域字段
        Figure(double radius) {                    //专为生成CIRCLE对象的构造函数
            s = Shape.CIRCLE;
            this.radius = radius;
        }
        Figure(double length,double width) {    //专为生成RECT对象的构造函数
            s = Shape.RECT;
            this.length = length;
            this.width = width;
        }
        double area() {
            switch (s) {                        //存在大量的case判断来确定实际的对象类型。
            case RECT:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError();
            }
        }
    }
  像Figure这样的类通常被我们定义为标签类,他实际包含多个不同类的逻辑,其中每个类都有自己专有的域字段和类型标识,然而他们又都同属于一个标签类,因此被混乱的定义在一起。在执行真正的功能逻辑时,如area(),他们又不得不通过case语句再重新进行划分。现在我们总结一下标签类将会给我们的程序带来哪些负面影响。
      1.    不同类型实例要求的域字段被定义在同一个类中,不仅显得混乱,而且在构造新对象实例时,也会加大内存的开销。
      2.    初始化不统一,从上面的代码中已经可以看出,在专为创建CIRCLE对象的构造函数中,并没有提供length和width的初始化功能,而是借助了JVM的缺省初始化。这样会给程序今后的运行带来潜在的失败风险。
      3.    由于没有在构造函数中初始化所有的域字段,因此不能将所有的域字段定义为final的,这样该类将有可能成为可变类。
      4.    大量的swtich--case语句,在今后添加新类型的时候,不得不修改area方法,这样便会引发因误修改而造成错误的风险。顺便说一下,这一点可以被看做《敏捷软件开发》中OCP原则的反面典型。
      那么我们需要通过什么方法来解决这样的问题呢?该条目给出了明确的答案:利用Java语句提供的继承功能。见下面的代码:

abstract class Figure {
        abstract double area();
    }
    class Circle extends Figure {
        final double radius;
        Circle(double radius) {
            this.radius = radius;
        }
        double area() {
            return Math.PI * (radius * radius);
        }
    }
    class Rectangle extends Figure {
        final double length;
        final double width;
        Rectangle(double length,double width) {
            this.length = length;
            this.width = width;
        }
        double area() {
            return length * width;
        }
    }
,这种基于类层次的设计规避了标签类的所有问题,同时也大大提供了程序的可读性和可扩展性

(7)用函数对象表示策略

    Java没有提供函数指针,但是可以用对象引用实现统一的功能。调用对象上的方法通常是执行该对象(that Object)上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象(other Objects)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(Function Object),如JDK中Comparator,我们可以将该对象看做是实现两个对象之间进行比较的"具体策略对象",如:

           class StringLengthComparator {
           public int compare(String s1,String s2) {
              return s1.length() - s2.length();
         }
     }
    这种对象自身并不包含任何域字段,其所有实例在功能上都是等价的,因此可以看作为无状态的对象。这样为了提供系统的性能,避免不必要的对象创建开销,我们可以将该类定义为Singleton对象,如:
class StringLengthComparator {
        private StringLengthComparator() {}    //禁止外部实例化该类
        public static final StringLengthComparator INSTANCE = new StringLengthComparator();
        public int compare(String s1,String s2) {
            return s1.length() - s2.length();
        }
    }
StringLengthComparator类的定义极大的限制了参数的类型,这样客户端也无法再传递任何其他的比较策略。为了修正这一问题,我们需要让该类成为Comparator<T>接口的实现类,由于Comparator<T>是泛型类,因此我们可以随时替换策略对象的参数类型,如:

class StringLengthComparator implements Comparator<String> {
         public int compare(String s1,String s2) {
             return s1.length() - s2.length();
      }
   }

总结:函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,可以考虑使用匿名类来声明和实例化这个具体的策略类。当一个具体策略是设计用来重复使用的时候,他的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
   
(8)优先考虑静态成员类


     嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的目的应该是为它的外围类(enclosing class)提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类(top-level class)。嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了第一种之外,其他三种都被称为内部类(inner class)。

    从语法上讲,静态成员类和非静态成员类之间唯一的区别是,静态成员类的声明中包含了修饰符static。尽管他们的语法非常的相似,但是这两种嵌套类有很大的不同。非静态成员类的每个示例都隐含着与外围类的一个外围实例(enclosing instance)相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。一个嵌套类是否可以在它的外围类的实例之外独立存在,是决定使用静态成员类还是非静态成员类的一个重要依据。

    如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的生命中,使它成为静态成员类,而不是非静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时仍然得以保留。如果没有外围实例的情况下,也需要分配实例,就不能使用非静态成员类,因为非静态成员类的实例必须要有一个外围实例

    匿名类不同于Java语言中的任何其他语法单元,它是没有名字的。它不是外围类的一个成员。它并不与其他成员一起被声明,而是在被使用的点上,同时被声明和实例化。因此匿名类只能被用在代码中它将被实例化的那个点上。还有,因为它没有名字,所以在它被实例化后就不能再对它进行引用。

    匿名类出现在表达式中间,所以它们应该非常简短,太长的话影响可读性。

    匿名类的一种常见用法就是动态的创建函数对象

    Arrays.sort(args, <strong>new Compartor()</strong> {  
        public int compare(Object o1, Object o2) {  
            return ((String)o1).length() - ((String)o2).length();  
        }  
    });  
    

     局部类是四种嵌套类中用的最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。局部类与其他三种嵌套类中的每一种都有一些共同的属性。与成员类一样,局部类有名字,可以被重复使用。与匿名类一样,只有当局部类实在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。与匿名类一样,它们必须简短以便不会影响到可读性

总结:如果一个嵌套类需要在单个方法之外仍然可见的,或者它太长了,不适合于放在方法内部,就应该使用成员类。如果成员类的每个示例都需要一个指向外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则,就做成局部类。



























  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值