Effective java 总结3 - 类和接口

  1. List item

Effective java 总结 - 类和接口

类和接口是Java语言的核心,也是基本抽象单元

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

封装、信息隐藏是软件设计的基本原则之一

  • 解耦
  • 提高可重用性
  • 降低了构建系统的风险

Java提供访问控制协助信息隐藏:尽可能使每个类或者成员不被外界访问

类和接口

  • 包级私有(package-private)
  • 公有 (public)
  • 如果一个包级私有的顶层类(or接口)只在某一个类的内部被用到,应该考虑使用嵌套类

成员

  • 私有的 private
  • 包级私有的 (package-private) 缺省访问级别
  • 受保护的 protected
  • 共有的 public
  • 同一个包的一个类需要访问另一个类的成员的时候,才应该删除private修饰符
  • 子类中的方法覆盖父类的方法,则子类的访问级别不可以低于父类的访问级别。特例:一个类实现了一个接口,则接口中的所有方法必须声明为共有的
  • 公有类的实例域不可以是共有的:因为包含公有可变域的类通常不是线程安全的
  • final域如果包含可变对象的引用,虽然本身不能被修改,但是它引用的对象却可以被修改

让类具有公有的静态final数组域,或者返回这种域的访问方法,是错误的:

public static final Thing[] VALUES = {};

解决1:使公有数组变成私有,并增加一个公有的不可变列表

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

解决2:使数组私有,添加公共方法,返回私有数组拷贝

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

模块系统(java 9 新增两个隐式访问级别)

主要用于提供咨询

一个模块就是一组包,模块可以通过其模块声明中的导出声明显示导出一部分包

模块中未被导出的包在模块之外是不可访问的。 例如,JDK本身:Java类库中未导出的包在其模块之外不可访问

总结

  • 始终尽可能合理降低程序元素的可访问性
  • 确保公有的静态final域所引用的对象都是不可变的

第16条 要在公有类而非公有域中使用访问方法

没有提供封装的类:退化类

class Point {
    public double x;
    public double y;
}

面向对象编程认为: 类可以在所在的包之外进行访问,应该提供访问方法。上面的例子需要用包含私有域和公有访问方法代替:

class Point{
    private double x;
    private double y;
    
    Point(double x, double y){
        this.x = x;
        this.y = y;
    }
    
    public double getX() {return x;}
    public double getY() {return y;}
    
    public void setX(double val) {x = val;}
    public void setY(double val) {y = val;}
}

如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误

虽然这从来都不是一个好的办法,但是如果域是不可变的,危害就小一些。

eg: 域被读取的时候无法采取辅助行动,但是可以强加约束条件

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 IllegaArgumentException(...);
        if(minute < 0 || minute >= MINUTES_PER_HOUR)
            throw new IllegaArgumentException(...);
        this.hour = hour;
        this.minute = minute;
    }
}

总结

  • 公有类永远不应该暴露可变的域
  • 有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论类是可变的还是不可变的

第17条 使可变性最小化

不可变类: 实例不能修改的类,每个实例包含的所有信息必须在创建时提供,并在对象生命周期内不变

eg: String, 基本类型的包装类, BigInteger, BigDecimal

不可变类易于设计,实现和使用,且更加安全

类成为不可变类的规则

  1. 不提供任何会修改对象状态的方法(设值方法)
  2. 保证类不会被扩展(一般声明类为final, or构造器私有且添加共有的静态工厂方法)
  3. 声明所有的域都是final的 ( 线程安全)
  4. 声明所有的域都是私有的
  5. 确保对于任何可变组件的互斥访问
    • 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用
    • 不用客户端提供的对象引用初始化不可变类的域
    • 不从任何访问方法中返回该对象引用
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 Complex plus(Complex c){
        return new Complex(re + c.re, im + c.im);
    }
    
    public Complex minus(Complex c){
        return new Complex(re - c.re, im - c.im);
    }
    
    public Complex times(Complex c){
        return new Complex(re * c.re - im * c.im, re * c.re + im * c.im);
    }
    
    public Complex divideBy(Complex c){
        double tmp = c.re * re + c.im * im;
        return new Complex((re * c.re + im * c.im) / tmp, (re * c.re - im * c.im) / tmp);
    }
    
    @Overrive public boolean equals(Object o){
        if (o == this) return true;
        if (!(o instanceof Complex)) return false;
        Complex c = (Complex)o;
        return Double.equals(c.re, re) == 0 && Double.equals(c.im, im) == 0;
    }
    
    @Override public int HashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }
    
    @Override public String toString(){
        return "()" + re + "+" + im + "i)";
    }
}

类中方法创建并返回新实例的方法称为 函数方法,该方法不会改变参数对象的值,名称一般都为介词(plus…

与之对应的是过程 或者 命令式的方法,会导致参数的状态改变。

BigInteger,BigDecimal类没有遵守 函数方法,导致了许多用法错误。

不可变类的优缺点

  • 不可变对象比较简单,只有一种状态

  • 不可变对象本质上是线程安全的,不要求同步

  • 不可变类可以被自由地共享: 对于频繁用到的值,提供共有的静态final实例

    • public static final Complex ZERO = new Complex(0, 0);
      public static final Complex ONE = new Complex(1, 0);
      
    • 提供一些静态工厂,基本类型的包装类和BigInteger都含有静态工厂,降低内存占用和垃圾回收成本

    • 永远不需要提供保护性拷贝

  • 不仅可以共享不可变对象,也可以共享其内部信息

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

  • 不可变对象提供了失败的原子性(状态永远不可能临时不一致

  • 唯一的缺点:每个不同的值都需要一个单独的对象

    • 问题:一个多步骤操作,产生了大量的新对象,除最后的结果,其余都被丢弃,具有很差的性能
    • 解决1:多步骤的操作作为基本类型提供
    • 解决2:提供一个公有的可变配套类,例如String的StringBuilder

为了确保类的不可变性,除了使类变成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);
    }
    ...
}

BigInteger,BigDecimal类是非final的,且提供了公有的构造器,为了防止其实例为子类,非BigInteger等父类,在假设它可能是可变的情况下,需要进行保护性拷贝

public static BigInteger safeInstance(BigInteger val) {
    return val.getclass == BigInteger.getclas ? val : new BigInteger(val.toByteArray());
}

如果需要让自己不可变的类实现Serializable接口,并且包含一个或者多个可变对象的域,就必须提供显式的readObject or readResolve方法,或者使用ObjectOutputStream.writeUnsharedh和ObjectInputStream.-readUnshared方法,否则攻击者可能从不可变的类创建可变实例。

总结

  1. 不要为每个get方法提供一个set方法,除非有很好的理由让类变成可变的类,否则该类就是不可变的
  2. 确认有必要实现很好的性能,才应该为不可变类提供公有的可变配套类
  3. 如果类不能做为不可变类,仍应该尽可能限制它的可变性
  4. 除非有很好的理由使域变成非final的,否则要使每个域都是private final 的
  5. 构造器应该创建完全的初始化对象,并建立起所有的约束关系

第18条 复合优于继承

该继承为实现继承而非接口继承

实现继承:一个类扩展另一个类

接口继承:一个类实现一个接口,或者一个接口扩展另一个接口

适合继承的地方

  1. 包的内部使用,子类和超类的实现都在一个程序员控制之下
  2. 专门为继承设计的类,且具有很好的文档说明

缺点:

  1. 继承打破了封装性:超类的实现如果随版本改变,子类可能被破坏,即子类必须跟随超类演变
  2. 子类覆盖超类方法的安全问题(脆弱性),超类方法的实现细节为自用性,不具备承诺,谨慎使用
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;
    
    public InstrumentedHashSet() {}
    
    public InstrumentedHashSet(int initCap, float loadFactor){
        super(initCap, loadFactor);
    }
    
    @Override public Boolean add(E e){
        addCount++;
        return super.add(e);
    }
    
    @Override public Boolean addAll(Collection<? extends E> c){
        addCount += c.size;
        return super.addAll(c);
    }
    
    public int getCount(){
        return addCount;
    }
}

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("a", "b", "c")); // java 9 
// s.addAll(Arrays.asList("a", "b", "c")); // before java 9
s.getCount(); // 结果是6!

问题所在

父类中的addAll 方法内部实际调用的是add方法 ,调用流程为:子.addAll() -> 父.addAll() -> 子.add() -> 父.add()

解决 – 复合

不扩展现在有的类,而是在新类中增加一个私有域,引用现有类的一个实例,使现有类变为新类的组件

转发方法: 新类中的每个实例方法都可以调用被包含现有类实例中的方法,并返回结果

这样的类非常稳固,且不依赖现有类的实现细节

// Wrapper class 复合切断了继承的覆盖 流程: 
// InstrumentedSet.addAll -> ForwardingSet.addAll -> Set.addAll() -> Set.add()
public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;
    
    public InstrumentedSet(Set<E> s) {super(s);}
    
    @Override public Boolean add(E e){
        addCount++;
        return super.add(e);
    }
    
    @Override public Boolean addAll(Collection<? extends E> c){
        addCount += c.size;
        return super.addAll(c);
    }
    
    public int getCount(){
        return addCount;
    }
}

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;                            // 引用现有实例
    
    public ForwardingSet(Set<E> s) { this.s = s; }     // 转发方法
    
    public void clear() {s.clear();}
    public boolean contains(Object o) {return s.contains(o); }
    public boolean add(Object o) { return s.add(o); }
    public boolean addAll(Collections<?> c) { return s.addAll(c); }
    ...
    @Override public boolean equals(Object o) { return s.equals(o); }
    ...
}

Set接口保存了HashSet类的功能特性,这里的包装类可以被用来包装任何Set实现

Set<Instance> times = new InstrumentedSet<Instance>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<E>(new HashSet<>(INIT_CAPACITY));
// 临时替换一个没有计数特性的Set实例
static void walk(Set<Dog> dogs){
    InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs); ...
}

InstrumentedSet类被称为包装类,设计模式为 修饰者模式

包装类不适合回调框架,在回调框架中,对象把自身的引用传递给其他对象,用于后续调用,而被包装起来的对象并不知道它外面的包装对象。

Guava为所有的集合接口提供了转发类

只有当子类真正是超类的子类型时,才适合继承 ( is-a 关系)

继承机制会将超类API中的所有缺陷传播到子类中,而复合允许设计新的API来隐藏这些缺陷

子类和超类在不同包中,将导致子类的脆弱性,可以用复合和转发来替代继承,包装类比子类更加健壮,安全


第19条 要么设计继承并提供说明文档,要么禁止继承

专门为了继承而设计的类

  • 必须具有良好的文档说明 它可覆盖方法的自用性
  • 精确描述覆盖每个方法带来的影响
  • 在那些情况下会调用到覆盖的方法
  • 如果方法调用到了可覆盖的方法,必须注释包含 实现要求
  • 需要描述清除那些有可能未定义的实现细节
  • 类必须以精心挑选的受保护的方法的形式,提供适当的钩子(Hook),以便其进入内部工作中
  • 尽可能少暴露受保护的成员
  • 唯一测试方法就是编写子类,且必须在发布之前先编写子类对类进行测试
  • 类的构造器绝不可以调用可以被覆盖的方法(超类的构造器先与子类构造器执行)
public class Super {
    public Super() {
        overrideMe();
    }
    public void overrideMe(){}
}

public final class Sub extends Super{
    private final Instant instant;
    public Sub() {
        instant = Instant.now();
    }
    
    @Override public void overrideMe() {
        System.out.println(instant);
    }
    
    public static void main(String[] args){
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

程序期望:打印两次时间, 实际结果: 一次为Null, 一次时间

通过构造器调用私有的方法,final方法和静态方法是安全的,这些都是不可以被覆盖的方法

为了继承而设计类时,Cloneable和Serializable的特殊困难

  • clone和readObject方法类似于构造器,因此,都不可以调用可覆盖的方法

  • 类如果实现Serializable接口,必须使readResolve或则writeReplace称为受保护的方法,而不是私有方法

总结

  • 为了继承而设计类,对这个类会有实质性的限制
  • 对于一些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化
  • 禁止子类化的方法:类声明为final , 构造器私有or包级私有
  • 能够安全进行子类化的一般的类
    • 完全消除这个类中可覆盖方法的自用特性
    • 将每个可覆盖方法的代码体移到一个私有的辅助方法中
    • 用“直接调用可覆盖方法的私有辅助方法” 代替 “可覆盖方法的每个自用调用”

第20条 接口优于抽象类

接口和抽象类

java8为继承引入了缺省方法,这两种机制都允许为某些实例方法提供实现

主要区别:

  • 为了实现由抽象类定义的类型,类必须成为抽象类的子类,所以抽象类作为类型定义受到了限制

  • 现有的类可以很容易被更新,以实现新的接口;但是一般不能更新现有的类来扩展新的抽象类

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

    mixin : 类除了实现它的基本类型之外,还可以实现这个mixin类型,表明他提供了一些可供选择的行为

  • 抽象类不能被用于定义mixin, 因为类不能有一个以上的父类,单继承原则

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

public interface A{
    String aString();
}

public interface B{
    String bString();
}

public interface C extends A, B{
    String cString();
}

接口使得安全地增强类的功能成为可能

接口通过缺省方法提供实现协助是有限的,接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外),更重要的一点:无法给不受控制的接口增加缺省方法

抽象骨架实现类 – AbstractInterface

模板方法模式

通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来

接口负责定义类型,提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法

Collecttions Framework 为每个重要的集合都提供了一个骨架实现:AbstractCollection, AbstractSet, AbstractList, AbstractMap

骨架实现类的好处:为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”的严格限制

实现抽象骨架实现类

  1. 继承接口,并确认接口的基本方法,这些方法将称为骨架类的抽象方法
  2. 为接口的基本方法提供缺省方法实现(不能为Object方法:equals,hashCode…提供缺省方法)
  3. 类中可以包含任何非公有的域,以及适合该任务的所有方法
  4. 对于骨架实现类,好的文档是必须要的

如何实现

实现了这个接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类,这种方法称为 模拟多重继承

// 接口
interface IAndroidPhone {
    void powerOn();
    void setVolume(int volume);
    void downloadApp(String appName);
}

// 骨架抽象实现类
abstract class AbsAndroidPhone implements IAndroidPhone{ // 继承接口
    @Override public void powerOn(){  // 提供默认实现
        System.out.println("shotdown");
    }
    
    @Override public void set Volume(int volume){ // 提供默认实现
        if(valume < 0) volume = 0;
        else if (volume > 100) volume = 100;
        System.out.println(volume);
    }
}

// 具体实现类
class XiaoMiPhone implements IAndroidPhone{  // 继承接口
    private XiaoMiPhoneImp xi = new XiaoMiPhoneImp(); // 内部私有类的实例
    
    @Override public void powerOn(){ // 转发到内部私有类的实例上
        xi.powerOn();
    }
    
    @Override public void set Volume(int volume){ // 转发到内部私有类的实例上
		xi.Volume(volume);
    }
    @Override void downloadApp(String appName){ // 转发到内部私有类的实例上
		xi.downloadApp(appName);
    }
    
    private static class XiaoMiPhoneImp extends AbsAndroidPhone{ // 继承骨架抽象实现类
        @Override void downloadApp(String appName){ // 覆盖抽象类的方法 单独定制
            System.out.println("xiao mi app");
        }
    }
}

总结

  • 接口通常是定义允许多个实现的类型的最佳途径
  • 如果导出了一个重要的接口,应该坚决考虑同时提供抽象骨架实现类
  • 尽可能地通过缺省方法在接口中提供骨架实现,以便接口的所有实现类都能使用

第21条 为后代设计接口

java8中,增加了缺省方法构造,目的是给现有的接口添加方法

缺省方法包含了缺省实现,但在接口中增加缺省方法是有风险的(Apache版本的SyncronizedCollection类同步问题)

  • 接口增加缺省方法,接口的现有实现不会出现编译通过但运行失败的情况
  • 尽量避免利用缺省方法在现有接口上添加新的方法
  • 谨慎设计接口是至关重要的

第22条 接口只用于定义类型

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

常量接口是对接口的不良引用: 接口不包含方法,只有静态的final域,每个域导出一个常量

正确使用:枚举类 or 常量类

public class Demo{
    private Demo(){} // 阻止实例化
    
    public static final int MIN_VALUE = 111;
    ...
}

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


第23条 类层次优于标签类

标签风格的类:过于冗长、容易出错, 且效率低下,是对类层次的简单效仿

子类型化可以来定义能表示多种风格对象的单个数据类型

当遇到一个包含标签域的现有类时,应该考虑重构到一个层次结构中


第24条 静态成员类优于非静态成员类

嵌套类:定义在另一个类的内部的类,只为外围类提供服务

成员内部类: 静态成员类、非静态成员类

编译后的class文件名:外部类名字$内部类名字

局部(方法)内部类:匿名类、局部类

编译后的class文件名:外部类名字$序号内部类名字(序号从1开始)

静态成员类

  • 可以访问外围类的所有成员(包含私有成员
  • 同外部类的静态成员,遵循同样的访问性规则
  • 类实例可以在外围类的实例之外独立存在
class Outer{
    static class Inner{...}
}

非静态成员类

  • 每个实例都隐含的与外围类的一个外围实例相关联(内部类的实例被创建时,与外围实例的关系随之建立
  • 类的实例方法可以调用外围的实例方法 or 用修饰过的this构造获得外围的实例引用

常见用法:

1、定义一个Adapter,eg: Map接口的实现使用非静态成员类实现集合视图(keySet, entrySet, values

2、Set,List的集合的迭代器

public class MySet<E> extends AbstractSet<E> {
    @Override 
    public Iterator<E> iterator(){
        return new MyIterator();
    }
    private class MyIterator implements Iterator<E> {
        ...
    }
}

成员内部类总结:

  • 如果声明成员类不要求访问外围实例,需声明为静态成员类,如果省略static,则每个实例都包含一个额外的指向外围对象的引用(该引用不可见),消耗时间空间,可能外围实例垃圾回收时却仍然保留造成内存泄漏

匿名类

  • 没有名字,非外围类的成员
  • 被使用的同时被声明和实例化,可以出现在代码中任意允许存在表达式的地方
  • 无法声明一个匿名类实现接口 or 扩展类,不能执行 instanceof 测试
  • 需保持简短,否则影响性能
  • 当且仅当在非静态环境中定义时,才有外围实例
  • 动态创建小型函数对象和过程对象的最佳方式,但现在优先选择 lambda 表达式
interface Demo{
    void test();
}
public class Outer{
    public static void main(String args[]){
        mi(new Demo(){
           public void m1(){
               System.out.println("m1");
           } 
        });
    } 
    public static void m1(Demo de){
        de.test();
    }
}

局部类

  • 使用少,在任何可以声明局部变量的地方可以声明局部类
public class Outer{
    void m1(){
        class Inner{
            void i1(){};
        }
        Inner in = new Inner();
        in.i1();
    }
}

第25条 限制源文件为单个顶级类

java 编译器允许一个源文件中定义多个顶级类,但没有好处,只有风险

永远不要把多个顶级类or接口放在一个源文件中

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值