美文网首页effective Java
《Effective Java》读书笔记 —— 类和接口

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

作者: 666真666 | 来源:发表于2017-04-13 18:01 被阅读399次

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

    访问修饰符:

    • private
    • protected
    • public

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

    • 包级私有(package-private)
    • public

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

    • private:只有在声明该成员的顶层类内部才可以访问
    • package-private:声明该成员的包内部的任何类都可以访问,是默认访问级别
    • protected:在声明类和子类中可以访问
    • public:任何地方可以访问
    规则一:尽可能使每个类或者成员不被外界访问

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

    规则二:如果方法覆盖了超类中第一个方法,子类中的访问级别就不允许低于超类的访问级别,确保任何可使用超类的地方都可以使用子类
    规则三:接口中的所有方法都必须是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对象不可变性的优缺点

    • 字符串常量池的需要.
    • 线程安全考虑
    • 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载
    • 支持hash映射和缓存

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

    • 不要提供任何会修改对象状态(属性)的方法
    • 保证类不会被扩展,不会有子类,破坏该类的不可变行为
      • 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。使所有的域都是final的。
    • 使所有的域(属性)都是final
    • 使所有的域都是private,防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象
    • 确保对于任何可变组件的互斥访问
      • 如果类具有指向可变对象的域,必须确保该类的客户端无法获得执行这些对象的引用
      • 在构造器中,永远不要用客户端提供的对象引用来初始化这样的域
      • 在访问方法中,也不要返回该对象引用
        • 普通对象,直接new 一个新的对象
        • 数组这类复杂对象,可使用 clone方法
      • 在构造器、访问方法和 readObject 方法中请使用保护性拷贝技术

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

         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.复合优先于继承

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

    继承

    • 继承打破了封装性
    • 父类内部细节对于子类是可见的,继承的代码复用是一种白盒式代码复用,如果基类的实现发生改变,那么派生类也将随之改变,导致子类的行为不可预知
    • 只有两者存在is-a的关系,才使用继承,如果不是,则使用组合

    组合

    • 在新的类中增加一个私有域,它引用现有类的一个实例
    • 继承必须在编译器确定继承哪个类,组合可以采用面向接口编程,类的组合关系可以在运行期确定

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

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

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

    6.接口优于抽象类

    接口优点

    • 现有的类可以很容易被更新,以实现新的接口
    • 接口是定义mixin(混合类型)的理想选择
      • 类不可能有一个以上的父类,类层次结构中也没有适当的地方插入mixin
    • 接口允许我们构造非层次接口的类型框架

    接口和抽象类区别

    • 接口里不能定义静态方法;抽象类里可以定义静态方法。
    • 接口里不包含构造器,抽象类可以包含构造器。抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
    • 接口里不能包含初始化块,但抽象类可以包含初始化块。
    • 接口里不包含已经提供实现的方法,只能包含抽象方法,;抽象类则完全可以包含普通方法。
    • 接口里只能定义静态常量,不能定义其他变量。抽象类既可以定义普通变量,也可以定义静态常量。
    • 注意:在接口里定义的接口、枚举类、变量默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统都会自动使用public static对他们进行修饰,同理,在抽象类里,会默认使用public abstract修饰方法。

    骨架实现类

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

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

    7.接口只用于定义类型

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

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

    接口用于导出常量

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

    缺点:

    • 实现常量接口,会导致把这样的实现细节泄漏到该类导出的API中
    • 如果非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

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

    相关文章

      网友评论

        本文标题:《Effective Java》读书笔记 —— 类和接口

        本文链接:https://www.haomeiwen.com/subject/clabattx.html