《Effective Java Third》第四章总结:类和接口

https://blog.csdn.net/liyagangcsdn/article/details/68946795

https://www.cnblogs.com/bushi/articles/6525044.html

https://blog.csdn.net/weixin_34236869/article/details/85811863

https://blog.csdn.net/zjq_1314520/article/details/71307285

第四章 类和接口

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

将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,隐藏内部数据和其他实现细节的程度;
一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来;
然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知;
这一概念,被称为信息隐藏或封装,是软件设计的基本原则

信息隐藏很重要有很多原因,其中大部分来源于它将组成系统的组件分离开来,允许它们被独立地开发,测试,优化,使用,理解和修改,这加速了系统开发,因为组件可以并行开发;
它减轻了维护的负担,因为可以更快速地理解组件,调试或更换组件,而不用担心损害其他组件;
虽然信息隐藏本身并不会导致良好的性能,但它可以有效地进行性能调整:一旦系统完成并且分析确定了哪些组件导致了性能问题(67),则可以优化这些组件,而不会影响别人的正确的组件;
信息隐藏增加了软件重用,因为松耦合的组件通常在除开发它们之外的其他环境中证明是有用的;
隐藏信息降低了构建大型系统的风险,因为即使系统不能运行,各个独立的组件也可能是可用的。

因为可访问性高就意味着需要提供支持,以保持兼容性

总结:应该尽可能地减少程序元素的可访问性(在合理范围内),即让每个类或成员尽可能不可访问;
在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分;
除了作为常量的公共静态 final 字段之外,公共类不应该有公共字段。 确保 public static final 字段引用的对象是不可变的。

16.在公共类中使用访问方法而不是公共字段

使用set/get访问私有属性而不是直接使用公有属性

如果一个类在其包之外是可访问的,则提供访问方法来保留更改类内部表示的灵活性。

虽然公共类直接暴露属性并不是一个好主意,但是如果属性是不可变的,那么危害就不那么大了;
当一个属性是只读的时候,除了更改类的 API 外,你不能改变类的内部表示形式,也不能采取一些辅助的行为,但是可以加强不变性;
例如,下面的例子中保证每个实例表示一个有效的时间:

public final class Time {
    private static final int HOURS_PER_DAY    = 24;
    private static final int MINUTES_PER_HOUR = 60;
    public final int hour;
    public final int minute;

    public Time(int hour, int minute) {
        if (hour < 0 || hour >= HOURS_PER_DAY)
           throw new IllegalArgumentException("Hour: " + hour);
        if (minute < 0 || minute >= MINUTES_PER_HOUR)
           throw new IllegalArgumentException("Min: " + minute);
        this.hour = hour;
        this.minute = minute;
    }
}

但是,如果一个类是包级私有的,或者是一个私有的内部类,那么暴露它的数据属性就没有什么本质上的错误——假设它们提供足够描述该类提供的抽象

17.最小化可变性

不可变类简单来说是它的实例不能被修改的类,包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。

好处:
不可变类比可变类更容易设计,实现和使用;
他们不太容易出错,更安全。

要使一个类不可变,请遵循以下五条规则:

  1. 不要提供修改对象状态的方法(也称为 mutators)。

  2. 确保这个类不能被继承。 这可以防止无意或恶意的子类以其对象状态可改变的方式,而损害超类的不可变行为。
    防止子类化通常是通过 final 修饰类;
    或可以使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法,这种方法往往是最好的选择

    public 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);
        }
    }
    
  3. 把所有属性设置为 final。 通过系统强制执行,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为,正如内存模型 所述。

  4. 把所有的属性设置为 private。 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公共 final 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(15 和 16 )。

  5. 确保对任何可变组件的互斥访问。
    如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。
    永远不要向提供对象引用的客户端初始化这样的字段,也不要从访问器返回字段。在构造函数、访问器和 readObject 方法(Item-88)中创建防御性副本(Item-50

如果可以准确预测客户端要在不可变类上执行哪些复杂的操作,那么包级私有可变伙伴类的方式可以正常工作;
如果不是的话,那么最好的办法就是提供一个公开的可变伙伴类;
这例子: String 类。它的可变伙伴类是 StringBuilder(及 StringBuffer 类)。

不可变对象本质上是线程安全的; 它们不需要同步,可以被自由地共享

不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。

不可变对象为其他对象提供了很好的构件(building blocks),无论是可变的还是不可变的;
如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了;
例子:不可变对象可以构成 Map 对象的键和 Set 的元素,一旦不可变对象作为 Map 的键或 Set 里的元素,即使破坏了 MapSet 的不可变性,也不用担心它们的值会发生变化。

一个不可变的类可以提供静态的工厂( 1 )来缓存经常被请求的实例,以避免在现有的实例中创建新的实例;
所有基本类型的包装类和 BigInteger 类都是这样做的;
使用这样的静态工厂会使客户端共享实例而不是创建新实例,从而减少内存占用和垃圾回收成本;
在设计新类时,选择静态工厂代替公共构造方法,可以在以后增加缓存的灵活性,而不需要修改客户端。

18.组合优于继承

在包中使用继承(extends)是安全的,但是从普通的具体类跨越包级边界继承是危险的

与方法调用不同,继承打破了封装。

使用组合,不依赖于现有类的实现细节。即使将新的方法添加到现有的类中,也不会对新类产生影响

只有在两个类之间存在「is-a」关系的情况下,B 类才能继承 A 类。

在决定使用继承来代替组合之前,应该问最后一组问题:
对于试图继承的类,它的 API 有没有缺陷呢?
如果有,你是否愿意将这些缺陷传播到你的类的 API 中?
继承传播父类的 API 中的任何缺陷,而组合可以让你设计一个隐藏这些缺陷的新 API。

总结:继承是强大的,但它是有问题的,因为它违反封装。
只有在子类和父类之间存在真正的子类型关系时才适用。
即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。
为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。

包装类的缺点很少。 一个警告是包装类不适合在回调框架(callback frameworks)中使用:
在回调框架中,对象把自身的引用传递给其他对象,用于后续的调用(回调);
导致SELF问题:被包装的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时却避开了外面的包装对象。

19.要么设计继承并提供文档说明,要么禁用继承

类必须准确地描述重写每个方法带来的影响,即该类必须文档说明可重写方法的自用性(self-use)

为了继承而进行的设计不仅仅涉及自用模式的文档设计;
为了使程序员能够编写出更加有效的子类,而无须承受不必要的痛苦,类必须以精心挑选的 protected 方法的形式,提供适当的钩子(hook),以便进入其内部工作中
或者在罕见的情况下,提供受保护的属性。

测试为继承而设计的类的唯一方法是编写子类,经验表明,三个子类通常足以测试一个可继承的类

**构造方法绝不能直接或间接调用可重写的方法。**父类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,子类中的重写方法被调用。 如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行。

CloneableSerializable 接口在设计继承时会带来特殊的困难。 对于为继承而设计的类来说,实现这些接口通常不是一个好主意,因为这会给继承类的程序员带来很大的负担。 可以在(13和86)中找到解决方案

有些情况显然是可以使用的,比如抽象类,包括接口的骨架实现(skeletal implementations)( 20 )。 还有其他的情况显然是不可使用的,比如不可变的类(17 )。
而普通的类,传统上,它们既不是 final 的,也不是为了子类化而设计和文档化的,但是这种情况是危险的。每次修改这样的类,则继承此类的子类将被破坏。 这不仅仅是一个理论问题。 在修改非 final 的具体类的内部之后,接收与子类相关的错误报告并不少见,这些类没有为继承而设计和文档化。
解决方案:在没有想要安全地子类化的设计和文档说明的类中禁止子类化。 有两种方法禁止子类化。
第一种是较容易的是声明类为 final
第二种是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。 这个方案在内部提供了使用子类的灵活性

如果一个具体的类没有实现一个标准的接口,那么你可能会通过禁止继承来给一些程序员带来不便;
如果你觉得你必须允许从这样的类继承,一个合理的方法是确保类从不调用任何可重写的方法,并文档说明这个事实。

总结:除非知道真正需要子类,否则最好通过将类声明为 final,或者确保没有可访问的构造器来禁止类被继承。

20.接口优于抽象类

因为 Java 只允许单一继承,所以格限制了抽象类作为类型定义的使用;
而对于接口来说,任何定义所有必需方法并服从通用约定的类都可以实现一个接口,而不管类在类层次结构中的位置。

接口允许构建非层级类型的框架。
因为可以实现多个接口,等于可以加上很多不同的接口,不会冲突;
而使用继承来实现的话会为每个受支持的属性组合包含一个单独的类,造成组合爆炸

推荐的模式:可以通过提供一个抽象骨架实现类来结合接口和抽象类的优点。
接口定义了类型,可能提供了一些默认方法;
而骨架实现类在基本接口方法之上实现了其余的非基本接口方法。扩展骨架实现需要完成实现接口的大部分工作。这是模板方法模式;
尽可能地,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。

21.为后代设计接口

除非必要,否则应该避免使用默认方法向现有接口添加新方法。在这种情况下,你应该仔细考虑现有接口实现是否可能被默认方法破坏,如同步容器调用了默认实现,而默认实现没有实现同步,则被破坏
而在创建新接口时,默认方法对于提供标准方法实现非常有用,以减轻实现接口的任务量(20)。

因此,在发布每个新接口之前对其进行测试非常重要。
多个程序员应该以不同的方式测试每个接口。
至少,你应该以三种不同的实现为目标。
同样重要的是编写多个客户端程序,用这些程序使用每个新接口的实例来执行各种任务。
这将大大有助于确保每个接口满足其所有预期用途。
这些步骤将允许你在接口被发布之前发现它们的缺陷,而你仍然可以轻松地纠正它们。
虽然在接口被发布之后可以纠正一些接口缺陷,但是你不能指望这种方式。

22.接口仅用来定义类型

https://blog.csdn.net/zjq_1314520/article/details/71307285

常量接口模式是使用接口的糟糕方式,这样的接口不包含任何方法;它仅由静态 final 字段组成,每个字段导出一个常量;
类在内部使用某些变量,这纯粹是实现细节,而实现常量接口会导致这个实现细节泄漏到类的导出 API 中;
对于类的用户来说,类实现一个常量接口没有什么价值。事实上,这甚至会让他们感到困惑;
更糟糕的是,它代表了一种承诺:如果在将来的版本中修改了类,使其不再需要使用常量,那么它仍然必须实现接口以确保二进制兼容性;
如果一个非 final 类实现了一个常量接口,那么它的所有子类的命名空间都会被接口中的常量所污染。

如果想导出常量,有几个合理的选择:
如果这些常量与现有的类或接口紧密绑定,则应该将它们添加到类或接口;
如果将这些常量看作枚举类型的成员,那么应该使用 enum 类型导出它们(34);
否则,你应该使用不可实例化的工具类(4)导出常量。

从 Java 7 开始,合法的下划线对数字字面量的值没有影响,而如果使用得当的话可以使它们更容易阅读。

总结:接口只能用于定义类型。 不应该仅用于导出常量。

23.类层次结构优于标签类

某种类的实例有两个或更多的风格,并且包含一个标签字段(tag field,可能为枚举类),表示实例的风格:

class Figure {
    enum Shape { RECTANGLE, CIRCLE };

    // Tag field - the shape of this figure
    final Shape shape;

    // These fields are used only if shape is RECTANGLE
    double length;
    double width;

    // This field is used only if shape is CIRCLE
    double radius;

    // Constructor for circle
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // Constructor for rectangle
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
          case RECTANGLE:
            return length * width;
          case CIRCLE:
            return Math.PI * (radius * radius);
          default:
            throw new AssertionError(shape);
        }
    }
}

这样的标签类具有许多缺点。
它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 switch 语句。
可读性更差,因为多个实现在一个类中混杂在一起。
内存使用增加,因为实例负担属于其他风格不相关的领域。
字段不能成为 final,除非构造方法初始化不相关的字段,导致更多的样板代码。
构造方法在编译器的帮助下,必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。
如果你添加一个风格,你必须记得给每个 switch语句添加一个 case,否则这个类将在运行时失败。
最后,一个实例的数据类型没有提供任何关于风格的线索。
总之,标签类是冗长的,容易出错的,而且效率低下。

所以Java为定义一个能够表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。
标签类仅仅是一个类层次的简单的模仿。

要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。
Figure 类中,只有一个这样的方法,就是 area 方法。
这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。
同样,如果有所有的方法使用的数据字段,把它们放在这个类。
Figure 类中不存在这种与类型无关的方法或字段。

接下来,为原始标签类的每种类型定义一个根类的具体子类。
在我们的例子中,有两个类型:圆形和矩形。
在每个子类中包含特定于改类型的数据字段。
在我们的例子中,半径字段是属于圆的,长度和宽度字段都是矩形的。
还要在每个子类中包含根类中每个抽象方法的适当实现。

对应于 Figure 类的类层次:

// Class hierarchy replacement for a tagged class
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) { this.radius = radius; }

    @Override 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;
    }
    @Override double area() { return length * width; }
}

24.静态成员类优于非静态成员类

有四种嵌套类:静态成员类,非静态成员类,匿名类和局部类。 除了第一种以外,剩下的三种都被称为内部类(inner class)

静态成员类的一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。

如一个描述计算器支持的操作的枚举类型。 Operation 枚举应该是 Calculator 类的公共静态成员类。

而私有静态成员类的常见用法是表示由它们的宿主类表示的对象的组件,如Map中的Entry

静态成员类是其宿主类的静态成员,并遵循与其他静态成员相同的可访问性规则;
可以访问所有宿主类的成员,甚至是那些被声明为私有类的成员

非静态成员类的一个常见用法是定义一个 Adapter
此类可以调用宿主实例上的方法,或者使用限定的构造[JLS,15.8.4] 获得对宿主实例的引用

如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。
如果忽略了这个修饰符,那么每个实例都会有一个隐藏的对其外部实例的额外引用。 存储这个引用需要占用时间和空间。
更严重的是还会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中( 7 ),由此产生的内存泄漏可能是灾难性的。
由于引用是不可见的,所以通常难以检测到。

如果所讨论的类是导出类的公共或受保护成员,则在静态和非静态成员类之间正确选择是非常重要的。
成员类是导出的 API 元素,则不能在不违反向后兼容性的情况下将非静态成员类更改为静态成员类。

在出现 lambda 表达式之前,匿名类是创建小函数对象和处理对象的首选方法,但 lambda 表达式现在是首选。 匿名类的另一个常见用途是实现静态工厂方法:

static List<Integer> intArrayAsList(int[] a) {
    Objects.requireNonNull(a);
    
    return new AbstractList<>() {
        @Override 
        public Integer get(int i) {
            return a[i];  
        }

        @Override 
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val;     
            return oldVal; 
        }

        @Override 
        public int size() {
            return a.length;
        }
    };
}

局部类是四种嵌套类中使用最少的。
一个局部类可以在任何可以声明局部变量的地方声明,并遵守相同的作用域规则。

总结:
如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。
如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。
假设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类;否则,把它变成局部类。

25.将源文件限制为单个顶级类

在单个源文件中定义多个顶级类存在重大风险。
风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。 (两个不同名的源文件包含两个类,这两个类在两个源文件中相同,如A.java中有A、B类,B.java中也有A、B类,但是只是类名相同,而方法定义的行为不同,那么这样编译生成的类文件以及生成的程序的行为与源文件传递给编译器的顺序就有关了。)
使用哪个定义会受到源文件传递给编译器的顺序的影响。

如果试图将多个顶级类放入单个源文件中,请考虑使用静态成员类作为将类拆分为单独的源文件的替代方法。
如果这些类从属于另一个类,那么将它们变成静态成员类通常是更好的选择,因为它提高了可读性,并且可以通过声明它们为私有( 15 )来减少类的可访问性。

总结:
永远不要将多个顶级类或接口放在一个源文件中。
遵循这个规则保证在编译时不能有多个定义。
这又保证了编译生成的类文件以及生成的程序的行为与源文件传递给编译器的顺序无关。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值