EffectiveJava--类和接口

编程技术  /  houtizong 发布于 3年前   96
本章内容:
1. 使类和成员的可访问性最小化
2. 在公有类中使用访问方法而非公有域
3. 使可变性最小化
4. 复合优先于继承
5. 要么为继承而设计,并提供文档说明,要么就禁止继承
6. 接口优于抽象类
7. 接口只用于定义类型
8. 类层次优于标签类
9. 用函数对象表示策略
10. 优先考虑静态成员类(嵌套类)

1. 使类和成员的可访问性最小化
    设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为信息隐藏或封装,是软件设计的基本原则之一。信息隐藏可以有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立的开发、测试、优化、使用、理解和修改。
    Java程序设计语言提供了许多机制来协助信息隐藏。访问控制机制决定了类、接口和成员的可访问性,实体的可访问性是由该类实体声明所在的位置以及该实体声明中所出现的访问修饰符(private、protected、public)共同决定的。规则是尽可能地使每个类或者成员不被外界访问,换句话说应该使用与你正在编写的软件功能一致、尽可能最小的访问级别。

访问级别:
私有的(private):只有在声明该成员的顶层类内部才可以访问这个成员。
包级私有的:声明该成员的包内部的任何类都可以访问这个成员,缺省的访问级别。
受保护的:声明该成员的类的子类和该成员的包内部的类可以访问这个成员。
公有的:在任何地方都可以访问该成员。

(1)如果类或者接口能够做成包级私有的,它就应该被做成包级私有。这样在以后如果需要进行修改、替换、或者删除无需担心会影响到现有的客户端程序。如果你把它做成公有的,你就有责任永远支持它,以保持它们的兼容性。
(2)如果一个类只是在某一个类的内部被用到,就应该考虑使它成为那个类的私有嵌套类。
(3)如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。如果一个类实现了一个接口,那么接口中所有的类方法在这个类中也都必须被声明为公有的,因为接口中的所有方法都隐含着公有访问级别。
(4)不能为了测试,而将类、接口或者成员变成包的导出的API的一部分。可以让测试作为被测试的包的一部分来支行,从而能够访问它的包级私有的元素。
(5)实例域决不能是公有的。如果域是final的,或者是一个指向可变对象的final引用,那么一旦使这个域成为公有的,就放弃了对存储在这个域中的值进行限制的能力。这意味着,你也放弃了强制这个域不可变的能力。同时,当这个域被修改的时候,你也失去了对它采取任何行动的能力。因此包含公有可变域的类并不是线程安全的。
(6)长度非零的数组总是可变的,所以类具有公有的静态final数组域和返回这种域的方法都是错误的,如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容。
    public static final Thing[] VALUES={....};
    许多IDE会产生返回指向私有数组域的引用的访问方法,这样就会产生上面的问题。修正这个问题有两种方法:
    一是使公有数组变成私有的,并增加一个公有的不可变列表。
    二是使数组变成私有的,并添加一个公有方法,它返回私有数组的一个备份。

总而言之,你应该始终尽可能地降低可访问性。在设计了一个最小的公有API之后,应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。

2. 在公有类中使用访问方法而非公有域
    如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。
class Point{
    private int a;
    private int b;
    public Point(int a, int b){
        this.x = y;
        this.y = y;
    }
    public int getA(){return a;}
    public int getB(){return b;}
    public void setA(int a){this.a = a;}
    public void setB(int b){this.b = b;}
}
    如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。如有必要,不改变包之外的任何代码而只改变内部数据表示法也是可以的。
    让公有类直接暴露域虽然从来都不是种好办法,但是如果域是不可变的,这种做法的危害就比较小一些。

3. 使可变性最小化
    不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。不可变类比可变类更加易于设计、实现和使用。它们不容易出错,且更加安全。如String、基本类型的包装类、BigInteger、BigDecimal等。
    为了使类成为不可变,要遵循下面五条规则:
(1)不要提供任何会修改对象状态的方法(mutator)
(2)保证类不会被扩展,防止子类假装对象的状态已经改变,从而破坏该类的不可变行为。一般防止子类化就是使这个类成为final的。
(3)使所有的域都是final的,或者让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器
(4)使所有的域都成为私有的,防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象
(5)确保对于任何可变组件的互斥访问

    不可变对象本质上是线程安全的,它们不要求同步。不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例,要做到这一点,一个很简便的办法就是对于频繁用到的值,为它们提供公有的静态final常量,
        public static final String ERROR_TYPE = "xxx";
    这种方法可以被进一步扩展,不可变的类可以提供一些静态工厂,把频繁被请求的实例缓存起来,从而当现有实例可以符合请求的时候,就不必创建新的实例。

    不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。
    如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作, 可以提供一种包级私有的可变配套类,如String类的可变配套类StringBuilder。
    坚决不要为每个get方法编写一个相应的set方法,除非有很多的理由要让类成为可变的类,否则就应该是不可变的。

4. 复合优先于继承
    继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具。使用不当会导致软件变得脆弱。在包的内部使用继承是非常安全的,在这里子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承也是非常安全的。
    继承打破了封装性,换句话说,子类依赖于其超类中特定功能的实现细节,如果超类的实现随着版本的不同而有所变化,子类可能会遭到破坏,即使子类的代码完全没有改变。因而子类必须要跟着其超类的更新而演变。

    幸运的是,有一种办法可以避免前面提到的所有问题。不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称做“复合”,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这被称为转发,新类中的方法被称为转发方法。这样得到的类将会非常稳固,它不依赖于现有类的实现细节,即使现有的类添加了新的方法,也不会影响新的类。新类被称为包装类,这也正是Decorator模式,注意包装类不适合用在回调框架中。实践中包装类不会对性能造成很大的影响。
    只有当子类真正是超类的子类型时,才适合用继承。即是说只有当两者之间确实存在“is-a”关系的时候,一个类才能扩展另一个类。

5. 要么为继承而设计,并提供文档说明,要么就禁止继承
    首先,该类的文档必须精确地描述覆盖每个方法带来的影响。换句话说,该类必须有文档说明它可覆盖的方法的自用性。即对于每个公有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的(所谓可覆盖的方法是指非final的,公有的或受保护的)。如果方法调用到了可覆盖的方法,在它的文档注释的末尾应该包含关于这些调用的描述信息。
    为了能编写出更加有效的子类,类必须通过某种形式提供适当的钩子(hook),以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域。
    构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。
    如果类是为了继承而被设计的,实现Cloneable或Serializable接口都不是个好注意,因为它们把一些实质性的负担转嫁到了扩展这个类的程序员的身上。当然可以采取一些特殊的手段使得子类实现这些接口(在谨慎的覆盖clone和谨慎的实现Serialable接口两节中有说到),即无论是clone还是readObject方法都不可以调用可覆盖的方法,不管是直接还是间接的方式。如果你决定实现Serializable接口,并且该类有一个readResolve或者writeReplace方法,就必须使readResolve或者writeReplace成为受保护的方法,而不是私有的方法,否则子类将会不声不响的忽略掉这两个方法。
    对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。禁止子类化一种方法就是把类声明为final的,另一种方法就是把所有的构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替代构造器。

6. 接口优于抽象类
    这两者机制之间最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许,接口则不允许。另一个重要的区别在于定义的类必须成为抽象类的一个子类,而java只允许单继承,故抽象类作为类型定义受到了极大的限制。
    接口是定义mixin(增加功能混合到类型的主要功能上,如Comparable)的理想选择。mixin是指这样的类型:它除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供了某些可供选择的行为。抽象类不能用于定义mixin。
    通过对你导出的每个接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。如AbstractCollection、AbstractSet等。骨架实现的美妙之处在于,它们为抽象提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制。
   
    使用抽象类来定义允许多个实现的类型,与使用接口相比有一个明显的优势:抽象类的演变比接口的演变要容易得多。在抽象类中增加新方法并且实现后在所有的子类实现都将提供这个新的方法。对于接口这样做是行不通的。
    因此,设计公有的接口要非常谨慎,接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。
    总结,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性和功能更为重要的时候,在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。最后,应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。

7. 接口只用于定义类型
    当类实现接口时,接口就充当可以引用这个类的实例的类型(type)。因此类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了其他目的而使用接口是不恰当的。但除了常量接口(没有包含任何方法,只包含静态final域),使用这些常量的类实现这个接口,以避免用类名来修改常量名。
    常量接口模式是对接口的不良使用。如果这些常量最好被看作枚举类型的成员,就应该使用枚举类型。否则,应该使用不可实例化的工具类来导出这些常量。工具类通常要求客户端用类名来修饰这些常量名。也可以使用静态导入,避免用类名修饰常量名
    总结,接口应该只被用来定义类型,不应该被用来导出常量。

8. 类层次优于标签类
    标签类是指类中包含了表示实例风格的标签域,它们中充斥着枚举声明,标签域以及条件语句。标签类过于冗长、容易出错,并且效率低下。如下:
    // Tagged class - vastly inferior to a class hierarchy!
    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();
            }
        }
    } 
    替换标签类更好的方法是子类型化,标签类正是类层次的一种简单的仿效。
    为了将标签类转变成类层次,首先要为标签类中的每个方法都定义一个包含抽象方法抽象类,这每个访求的行为都依赖于标签值。接下来,为每种原始标签类都定义根类的具体子类。上面的类更改如下:
// 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; }

    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; }
}
class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

    这个类层次纠正了前面提到过的标签类的所有缺点。它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查。
    标签类很少有适用的时候。当你想要编写一个包含显示标签域类时,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。

9. 用函数对象表示策略
    某些机制用于允许函数的调用者通过传入第二个函数,来指定自己的行为。这种机制被称为策略模式。
    Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象上某项操作,然而我们也可能定义这样一种对象,它的方法执行其他对象(这些对象被显式传递给这些方法)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。如下:
    class StringLengthComparator {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }
    指向StringLengthComparator对象的引用可以被当做是一个指向该比较器的“函数指针”,可以在任意一对象字符串上被调用。换句话说,StringLengthComparator实例是用于字符串比较操作的具体策略。

    作为典型的具体策略类,StringLengthComparator类是无状态的,他没有域,所以这个类的所有实例在功能功能上都是等价的。因此,他作为Singleton是非常合适的,可以节省不必要的对象创建开销。
    class StringLengthComparator2 {
        private StringLengthComparator2(){}
        public static final StringLengthComparator2 INSTANCE = new StringLengthComparator2();
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }

    为了客户端能传递任何其他的比较策略,我们需要定义一个策略接口,如下:
    public interface Comparable<T> {
        public int compareTo(T t1, T t2);
    }

    class StringLengthComparator2 implements Comparable<String>{ ... }

    具体的策略类往往使用匿名类声明,如下:
    Arrays.sort(stringArray,new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return s1.length() - s2.length();
        }
    });

    但是,这样每次执行调用的时候都创建一个新的实例。如果他被重复执行,考虑将实例对象存储到一个私有的静态final域里,并重用他:
    public class Host {
        private static class StrLenCmp implements Comparator<String>, Serializable {
            @Override
            public int compare(String o1, String o2) {
                return o1.length() - o2.length();
            }
        }
        public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
    }

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

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

    静态成员类是最简单的一种嵌套类,最好把它看作是普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。

    从语法上讲,静态成员类和非静态成员类之间的唯一区别是,静态成员类的声明中包含修饰符static。尽管他们的语法非常相似,但是两种嵌套类有很大的不同。 非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。 如果嵌套类的实例可以在他外围类的实力之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
    当非静态成员类的实例被创建的时候,他和外围之间的关联关系也随之被建立起来;而且,这种关联关系以后不能被修改。通常情况下,当在外围类的某个实例方法的内部调用非静态成员类的构造器时,这种关系被自动建立起来。 使用表达式enclosingInstance.new MemberClass(args)来手工建立这种关联关系也是有可能的,但是很少使用。正如你所预料的那样,这种关联关系需要消耗费静态成员类的实例空间,并且增加了构造的时间开销。

    非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关类的实例。

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

    匿名类不同于Java程序设计语言中的其他任何语法单元,正如你所想象的,匿名类没有名字。它不是处置类的一个成员,它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中,它才有外围实例。但是即使它们出现在静态的环境中,也不可能拥有任何静态成员。
    匿名类除了在它们被声明的时候之外,是无法将它们实例化的。你不能执行instanceof测试,或者做任何需要命名类的其他事情。你无法声明一个匿名类来实现多个接口,或者扩展一个类。匿名类的客户端无法调用任何成员,除了从它的超类型中继承得到之外。由于匿名类出现在表达式中,它们必须保持简短(大约10行或者更少),否则会影响程序的可读性。
    匿名类的一种常见用法是动态的创建函数对象(如上一知识点)。 另一种常见用法是创建过程对象(如Runnable、Thread或TimerTask实例)。 第三种常见的用法是在静态工厂方法的内部。

    局部类是四种嵌套类中用得最少的类。在任何可以声明局部变量的地方,都可以声明局部类。与匿名类一样,它们必须非常简短,以便不会影响可读性。

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

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!

留言需要登陆哦

技术博客集 - 网站简介:
前后端技术:
后端基于Hyperf2.1框架开发,前端使用Bootstrap可视化布局系统生成

网站主要作用:
1.编程技术分享及讨论交流,内置聊天系统;
2.测试交流框架问题,比如:Hyperf、Laravel、TP、beego;
3.本站数据是基于大数据采集等爬虫技术为基础助力分享知识,如有侵权请发邮件到站长邮箱,站长会尽快处理;
4.站长邮箱:[email protected];

      订阅博客周刊 去订阅

文章归档

文章标签

友情链接

Auther ·HouTiZong
侯体宗的博客
© 2020 zongscan.com
版权所有ICP证 : 粤ICP备20027696号
PHP交流群 也可以扫右边的二维码
侯体宗的博客