Effective Java读书笔记 -- 第四章:类和接口

    类和接口是Java程序设计语言的核心,也是Java语言的基本抽象单元。Java语言提供了许多强大的基本元素,供程序员用来设计类和接口。


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

    设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之前只通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为信息隐藏或封装,是软件设计的基本原则之一。

    Java程序设计语言提供了许多机制来协助信息隐藏。访问控制机制决定了类、接口和成员的可访问性。

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

    对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别:

        1)私有的(private)——只有在声明该成员的顶层类内部才可以访问这个成员。

        2)包级私有的(package-private)——声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省(default)访问级别”,如果没有为成员钉钉访问修饰符,那就是这个访问级别。

        3)受保护的(protected)——声明该成员的类和它的子类可以访问这个成员,并且,声明该成员的包内部的任何类也可以访问这个成员。

        4)公有的(public)——在任何地方都可以访问该成员。

    

第十四条:在公有类中使用访问方法而非公有域

    坚持面向对象程序设计思想的看法的是正确的:如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变类的内部表示法的灵活性。

    然而,如果是包级私有的,或者是私有的嵌套类,这几暴露它的数据域并没有本质的错误。

    总之,公有类永远都不该暴露可变的域。


第十五条:使可变性最小化

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

    为了使类成为不可变,要遵循下面五条规则:

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

        2)保证类不会被扩展。使这个类成为final的。

        3)使所有的域都为final的。

        4)使所有的域都成为私有的。

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

    不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。

    “不可变对象可以被自由地共享”导致的结果是:永远也不需要进行保护性拷贝。

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

    不可变对象为其他对象提供了大量的构件,无论是可变的还是不可变的对象。

    不可变对象真正唯一的缺点是:对于每一个不同的值都需要一个单独的对象。

    让不可变的类变成final的另一种方法就是,让类的所有构造器都变成私有的或者包级私有的,并添加共有的静态工厂来代替公有的构造器。


第十六条:复合优先于继承

    继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具。使用不当会导致软件变得很脆弱。

    与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。

    如果在适合于使用复合的地方使用了继承,则会不必要地暴露实现细节。这样得到的API会把你限制在原始的实现上,永远限定了类的性能。

    简而言之,继承的功能非常强大,但是也存在诸多问题,因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。


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

    对于不是为了继承而设计、并且没有文档说明的“外来”类进行子类化是很危险的。而对于专门为了继承而设计并且具有良好文档说明的类而言,该类的文档必须精确地描述覆盖每个方法所带来的影响。

    好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。

    为了允许继承,类必须遵守其他一些约束:构造器不能调用可被覆盖的方法,无论是直接调用还是间接调用。

package JavaDay5_28;

import java.util.Date;

/**
 * @author [email protected]
 * @date 18-5-28  下午7:04
 */

public class Demo1 {
    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

class Super {
    public Super() {
        overrideMe();
    }

    public void overrideMe() {}
}

class Sub extends Super {
    private final Date date;

    public Sub() {
        date = new Date();
    }

    @Override
    public void overrideMe() {
        super.overrideMe();
        System.out.println(date);
    }
}

    对于以上栗子,可能期望输出当前日期两次,然后第一次输出为null,输出结果如下:

    

    因为overrideMe方法被Super构造器调用时,构造器Sub还没有机会初始化data域。


第十八条:接口优于抽象类

    Java程序设计语言提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。这两种机制最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。一个更为重要的区别在于,为了实现抽象类定义的类型,类必须成为抽象类的一个子类。任何一个类,只要它定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,而不管这个类是处于类层次的哪个位置。因为Java只允许单继承,所以抽象类作为类型定义受到了极大的限制。

    现有的类可以很容易被更新,以实现新的接口。

    接口是定义mixin(混合类型)的理想选择。

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

    通过包装类模式,接口使得安全地增强类的功能成为可能。

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

    设计共有的接口要非常谨慎。接口一旦被公开发行,并且已经被广泛实现,再想改变这个接口几乎是不可能的。

    简而言之,接口通常是定义允许多个实现的类型的最佳途径。


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

    当类实现接口时,接口就充当可以引用这个类的实例的类型。

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


第二十条:类层次优于标签类

    子类型化是面向对象语言提供的能表示多重风格的单个数据类型。以下为一个标签类的栗子:

package JavaDay5_28;

/**
 * @author [email protected]
 * @date 18-5-28  下午7:57
 */

public class Figure {
    enum Shape { RECTANGLE, CIRCLE};

    final Shape shape;

    double length;
    double width;

    double radius;

    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    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();
        }
    }
}

    为了将标签类转变成类层次,要为标签类中的每个方法都定义一个包括抽象方法的抽象类,每个方法的行为都依赖于标签值。以下为转换为类层次的代码:

package JavaDay5_28;

/**
 * @author [email protected]
 * @date 18-5-28  下午8:02
 */

abstract class FigureDemo {
    abstract double area();
}

class Circle extends FigureDemo {
    final double radius;

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

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

class Rectangle extends FigureDemo {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width;
    }
}

    简而言之,标签类很少有使用的时候,大多数标签类都可以被类层次所代替。


第二十一条:用函数对象表示策略

    有些语言支持函数指针、代理、lambda表达式,或者支持蕾丝的机制,允许程序把“调用特殊函数的能力”存储起来并传递这种能力。这种机制通常用于允许函数的调用者通过传入第二个函数,来指定自己的行为。

    Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象的方法通常就是执行该对象上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。

    函数指针的主要用途就是实现策略模式。


第二十二条:优先考虑静态成员类

    嵌套类是指被定义在另一类的内部的类。嵌套类存在的目的应该只是为它的外围类提供服务。如果嵌套类将来可能回用于其他的某个环境中,他就应该是顶层类。

    嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类,其他三中都被成为内部类。

    静态成员类是最简单的一种嵌套类。它可以访问外围类的所有成员,包括那些声明为私有的成员。当它被声明为私有的,它就只能在外围类的内部才可以被访问。静态成员类的一种常用用法就是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。

    从语法上将,静态成员类和非静态成员类的唯一的区别就是P:静态成员类的声明中包含修饰符static。

    如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。

    匿名类不同于Java程序设计语言中的其他任何语法单元。匿名类灭有名字。他不是外围类的一个成员。它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名类除了在它们被声明的时候之外,是无法被实例化的。匿名类的一种常见用法就是动态地创建函数对象。

局部类是四中嵌套类中用得最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。


总结:本章主要讨论了类和接口的相关知识,看完之后,领悟到Java是一门真正意义上的面向对象的语言,应该多使用接口或者抽象类来编程,并注意编程中关于接口编程的问题。共勉。

猜你喜欢

转载自blog.csdn.net/weixin_41704428/article/details/80482986