effective Java

《Effective Java》读书笔记 —— 类和接口

2017-04-13  本文已影响399人  666真666

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

访问修饰符:

顶层的(非嵌套)类和接口,两种访问级别:

成员(域、方法、嵌套类和嵌套接口)

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

如果一个包级私有的顶层类只是在某一个类的内部使用,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。

规则二:如果方法覆盖了超类中第一个方法,子类中的访问级别就不允许低于超类的访问级别,确保任何可使用超类的地方都可以使用子类
规则三:接口中的所有方法都必须是public
规则四:实例域不能是公有的

如果域时非final的,或者是一个指向可变对象的final引用,那么一旦使这个域称为公有,就放弃了在这个域中的值进行限制的能力,也就放弃了这个域的不可变能力

包含公有可变域的类并不是线程安全的。

规则四:静态域不要是公有的(除了暴露静态常量)

要对外暴露静态域,必须是基本类型的值,或者是不可变对象。

长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的方法,总是不正确的。

以下错误:

public static final Tings[] VALUES = {...};

解决方案:公有数组私有化,并增加一个公有的不可变列表

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

或者:添加公有方法,返回私有数组的拷贝

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

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

公有类永远不要暴露可变的类。

3.使可变性最小化

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

Java 平台类库中的不可变类:String、基本类型的包装类、BigInteger、BigDecimal。不可变类对应配套的可变类:StringBuilder、BitSet。本应该是不可变,但却是可变的类:Date、Point。

不可变类的优点:

不可变类的缺点:

String对象不可变性的优缺点

使类成为不可变,遵循的规则:

通过构造器初始化所有成员,构造器初始化成员时,需要进行深浅拷贝,如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值

     public final class ImmutableDemo {  
         private final int[] myArray;  
         public ImmutableDemo(int[] array) {  
             this.myArray = array; // wrong  
         }  
     }
     这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。
     为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:
     public final class MyImmutableDemo {  
         private final int[] myArray;  
         public MyImmutableDemo(int[] array) {  
             this.myArray = array.clone();   
         }   
     }

不可变类的设计

对于访问方法,一般会返回一个新的实例,而不是修改这个实例,大多数不可变类使用这种模式,称为函数的做法。

不可变的类一般会提供一些静态工厂,它们把频繁被请求的实例缓存起来,使客户端之间可以共享这些实例,而不用创建新的实例,降低内存占用和垃圾回收成本。所有基本类型的包装类和BigInteger都有这样的静态工厂。

不可变对象可以被自由共享,所以根本不需要做任何拷贝,因为拷贝始终等于原始的对象,所以不需要为不可变的类提供clone方法或者拷贝构造器。

不仅可以共享不可变对象,也可以共享它们的内部信息。

不可变对象为其他对象提供了大量的构件。

有关序列化,如果让自己的不可变类实现序列化,就必须显式提供 readObject 或者 readResolve,否则反序列化可能会产生新的实例。

尽量使用不可变类,不要为每个get方法编写一个相应的set方法

举例

说明:这个类表示一个复数,加减运算都是返回一个新的实例,而不是在原来的实例上修改,称为函数的做法。

public final class Complex {
    private final double re;
    private final double im;
    
    public Complex(double re, double im) {
        this.re = re;
        this.im = 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 sub(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }
}

将构造函数改为私有的,并添加静态工厂来替代公有构造器

public final class Complex {
    private final double re;
    private final double im;
    
    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }
    ...
}

4.复合优先于继承

与方法调用不同的是,继承打破了封装性。子类依赖于其超类中特定功能的实现细节。所以子类必须要跟着其超类的更新而演变,导致子类很脆弱。

继承

组合

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

类的文档必须精确描述覆盖每个方法所带来影响,也就是说,覆盖的方法必须说明其自用性

类必须通过某种形式提供适当的钩子(hook),以便能够进入它的内部工作流程中,这种形式可以是精心选择的受保护的方法

6.接口优于抽象类

接口优点

接口和抽象类区别

骨架实现类

虽然接口不允许包含默认实现,但是,可通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作

在选择抽象类和接口时,并不是二选一的答案,或干脆枪毙掉抽象类。其实,你可以把接口和抽象类的优点结合起来,对于你希望导出(对外提供)的每一个重要接口都提供一个抽象类(骨架实现类)。接口的作用仍然是定义类型,骨架实现类负责所有与接口实现相关的工作

7.接口只用于定义类型

当类实现接口时,接口就充当可以引用这个类的实例的类型。类实现了接口,就表明客户端对这个类的实例实施某些动作。

接口应该只被用来定义类型,不应该被用来导出常量

接口用于导出常量

常量接口:只包含静态的final域。导出常量的一种形式。常量接口模式是对接口的不良使用

缺点:

常量接口的例子

public interface PhysicalConstants {
    static final double AAA = 0.1;
    static final double BBB = 0.1;
    static final double CCC = 0.1;
}

导出常量的合理方案:

工具类的方式:

public class PhysicalConstants {
    private PhysicalConstants() {};
    static final double AAA = 0.1;
    static final double BBB = 0.1;
    static final double CCC = 0.1;
}

8.类层次优于标签类

有时,可能遇到带有两种甚至更多风格的实例的类,并包含表示实例风格的标签域。

下面例子,此类表示圆形或者矩形

demo

标签类缺点:

解决方案:子类化

定义一个包含抽象方法的抽象类,公共方法定义在抽象类

demo

9.用函数对象表示策略(策略模式)

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

举例:比较器函数代表一种为元素排序的策略。

Java没有提供函数指针,可以用对象引用实现此功能。

比较器实例

StringLengthComparator 实例就是用于字符串长度比较的具体策略。

StringLengthComparator 是无状态的(没有域),所以单例比较合适。

class StringLengthComparator implements Comparator<String> {
    private StringLengthComparator() {};
    private static final StringLengthComparator INSTANCE = new StringLengthComparator();
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

定义一个策略接口

public interface Comparator<T> {
    public int compare(T t1, T t2);
}

使用比较策略

Arrays.sort(stringArray, new Comparator(String)(){
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
})

10.优先考虑静态成员类

嵌套类:被定义在另一个类的内部的类。嵌套类存在的目的只是为了它的外围类提供服务。

嵌套类包括:

静态成员类

最简单的一种嵌套类,可看作是普通的类,可以访问外围类的所有成员,包括私有成员。

公有静态成员类常见用法,是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。

私有静态成员类常见用法,用来代表外围类所代表的对象的组件。例如,Map实例,Map的内部都有一个Entry对象,对应于Map的key和value。

非静态成员类(内部类)

必须和外围类的一个实例相关联,可以用this来访问

常见用法:Adapter

如果成员类不要求访问外围实例,就要声明成静态成员类,不然每个实例都会包含一个额外的指向外围对象的引用

上一篇下一篇

猜你喜欢

热点阅读