Effective Java: 类和接口

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

  1. 尽可能的使每个类或者成员不被外界访问
  2. 对于顶层类,接口,只有两种访问级别: 包级私有(package-private)和公有(public)
  3. 对于成员,有四种访问级别(private,package-private,protect,public)
  4. 如果一个类只对一个类可见,则应该将其定义为私有的内部类,而没必要public的类都应该定义为package private
  5. 子类的访问级别不允许低于父类的访问级别.

小结

  1. 应该始终尽可能的降低可访问性
  2. 除了公有静态final域外,公有类都不应该包含非公有域.

14.在共有类中使用访问方法,而非共有域

示例

  class Point {
    public double x; //
    public double y;
  }
  1. 如果是公有类的时候,应该使用私有成员,并提供setter方法(除非不可变域)
  2. 包级私有,或内部类,直接暴露则没有本质的错误.
class Point {
    private double x;
    private double y;

    public 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 x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }
}

15.使可变性最小

建议

  1. 不提供可以改变本对象状态的方法
  2. 保证类不会被扩展(防止子类改变)
  3. 使所有的域都是 final
  4. 使所有的域都称为private
  5. 确保对于任何可变组件的互斥访问

示例:

Complex.java

public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

示例中除了标准的 Object 方法之外,运算操作都是创建新的实例,而不改变原来对象,这种做法被称为 函数的(functional)做法 .

提示:

  • 不可变对象比较简单
  • 不可变对象本质上是线程安全的
  • 不仅可以共享对象,甚至可以共享不可变对象的内部信息.[BigInteger内部的内部数组]
  • 不可变对象为其他对象提供了 大量的构件(building blocks)[map keyset element]

缺点

  1. 不可变对象对于每个不同的值 都需要创建一个单独的对象.
  2. 由于1的问题.导致多步操作会有性能问题.这种问题的解决办法就是提供一个公有的可变配套类,如StringStringBuilder.

保持不可变性

为了保持不被子类化,可以使用final修饰.除此之外,还有一种方法:

  1. 让类的构造器变成 private/package-private,添加静态工厂来代替 公有构造器,见[第一条]
  2. 使用静态工厂方法,具体实现类可以有多个,还能进行object cache[第一条]

小结

  1. 坚决不要为每个 get 方法编写一个 对应的 set方法,除非有很好的理由.
  2. 如果类不可做成 不可变的,也该尽量 限制其可变性(final 域)
  3. 当实现Serializable,一定要实现readObject/readResolve方法,或者使用ObjectOutputStream.writeUnshared/ObjectInputStream.readUnshared

案例: TimeTask : 可变,但状态空间被有意的设计的很小.

16.复合优于继承

简介

专为 继承设计的,包内部 继承非常安全,是代码重用的有效手段.
对于 具体类 进行跨越包边界的继承是危险的.

继承缺点

  • 打破封装性,子类依赖父类实现细节,父类改变,子类会遭到破坏
    比如: HashSet记录添加元素个数的方法,addAll内部调用了add,导致程序错误.

示例代码:InstrumentedHashSet.java

  • 在后续升级版本中,如果父类新增了与子类相同签名,返回值不同的方法,子类无法通过编译,如果新增了签名和返回值都相同的方法,则会发生覆盖

复合方式

  • 新类中增加一个引用现在类私有域,通过转发实现

示例: ForwardingSet.java

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) {this.s = s;}

    public boolean add(E e) {return s.add(e);}
}

缺点

  • 不适用于callback frameworks,在回调框架中,需要将自身传递给调用对象来回调.
    而被包装起来的对象并不知道 它被包装的 外面的对象, 因此导致回调失败.

继承和符合选择问题

  • 继承应该在is-a的场景中使用
  • 继承除了会继承父类的API功能,也会继承父类的设计缺陷,而组合则可以隐藏成员类的设计缺陷,通过转发暴露部分API

17.要么为继承而设计,要么禁用继承

简介

  1. 一个类必须在文档中说明,每个可重写的方法,在该类的实现中的哪些地方会被调用。调用时机、顺序、结果产生的影响,包括多线程、初始化等情况。
  2. 被继承类应该通过谨慎选择protected的方法或成员,来提供一些hook,以便能进入到它的内部工作流程中.如:java.util.AbstractList::removeRange
  3. 构造器不能调用可被覆盖的方法,因为父类构造函数比子类构造函数先执行,导致子类重写方法比子类构造器先执行
  4. 如果实现了Serializable/Cloneable,无论是 clone还是readObject都不可以调用可复写方法
  5. 如果实现了SerializablereadResolve/writeReplace必须是protected,而非private,否则子类会忽略.

总结

  • 为继承而设计的类,对这个类会有一些实质性的限制.
  • 不是为继承而设计的类,应禁止子类化.

18.接口优于抽象类

简介

Java 语言提供了两种机制: 接口和抽象类,
Java是单继承,多实现的,因此抽象类作为类型定义受到了很大的限制.

  • 通过接口,现有类可以很容易的被更新
  • 接口是mixin(混合类型)的理想选择(提供某些可供选择的行为)
  • 接口允许我们构建非层次结构的框架(利用接口多继承特性)
public interface Singer{}
public interface Writer{}
public interface SingerWriter extends Singer ,Writer{}

有效的避免了继承导致的臃肿的类层次(组合爆炸).

注意事项

  • 对每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来
  • 通过把对接口的实现转发到一个扩展了骨架实现的内部私有实例上,称作模拟多重实现,示例:Map.EntryAbstractMapEntry
  • 骨架类是为了继承而设计的[参考第17条规范]
  • 还有一种简单实现,AbstractMap.SimpleEntry(可能是基本实现,可能是空实现)

区别

  • 抽象类演变比接口方便,可以在后续版本中新增新的方法,并提供默认实现,现有实现类都提供这个新的方法,接口则不行

小结

  • 接口一旦发行,并广泛实现后,想要改变几乎不可能.
  • 通过接口定义类型,可以允许多实现(多继承)
  • 但是演进需求大于灵活性、功能性时,抽象类更合适
  • 提供接口时,提供一个骨架实现,同时审慎考虑接口设计

19.接口只用于定义类型

简介

接口充当了可以引用这个类的实例类型Type.

不良使用

常量接口: 如:

public interface Constants{
static final int AAA  = 0;
}

这种接口,会使用户糊涂,并使接口污染,如果这个类被修改了,
意味着,在后续版本中,不再需要这些常量了,它依然必须实现这个接口.

合理方案

  • 只在当前类和接口中导出这些常量.
    如: Integer.MAX_VALUE
  • 不可实例化的工具类中导出常量,配合静态导入(static import)
  • 接口应该 只被用来定义类型,而不是定义常量.

20.类层次优于标签类

概述

标签类,就是在内部定义一个tag变量,由其控制功能的转换,如下类型

class Figure {
    enum Shape {RECTANGLE, CIRCLE};
    // Tag field - the shape of this figure
    final Shape shape;
}

标签类过于冗长,容易出错

解决方案

通过子类化解决.为每种标签都定义具体的子类.

示例代码: Figure

abstract class Figure {}

class Rectangle extends Figure {}
class Circle extends Figure {}

好处:

  • 代码简单清除,没有样板代码
  • 所有域都是final的,在初始化构造器时就初始化数据域
  • 多个程序员可以独立的扩展类层次结构

建议

当编写包含显示标签域的代码时,应考虑标签是否可以取消.

21.用函数对象表示策略

简介

  • 允许程序把调用特殊函数的能力存储起来并传递这种能力.这种机制通常允许函数调用者传入第二个参数来指定自己的行为.

  • 定义一种对象,它的方法执行其他对象上的操作,这样的类被称为函数对象.如Compare接口的实现类.

  • 这种对象实例,可以称为其他对象具体策略

  • 这种策略类,没有状态,没有域,所有实例都是等价的,因此最好用单例来实现,如:

//第一种
 Arrays.sort(new short[]{}, new Comparator<String>() {
          @Override public int compare(String o1, String o2) {
            return 0;
          }
        });

//第二种
  class StringLengthComparator implements Comparator<String> {
    private StringLengthComparator() {
    }

    public static final StringLengthComparator INSTANCE = new StringLengthComparator();

    @Override public int compare(String o1, String o2) {
      return o1.length() - o2.length();
    }
  }

总结

  • 函数指针的主要途径是实现策略模式
  • 需要声明一个接口来表示该策略(如Comparator<T>,一般是泛型接口)
  • 当一个具体的策略只使用一次时,使用匿名内部类
  • 当一个具体策略被重复使用时,使用单例来实现

22.优先考虑静态成员类

简介

有四种嵌套类: 静态成员类,非静态成员类,匿名类与局部类.除了第一种,其他三种都被称为内部类.

静态成员

  • 静态成员类是最简单的一种嵌套类,可以看成是一种恰好被声明在另一个类的内部的普通类. 和其他静态成员规则一致.
  • 静态成员类的一种常见用法是作为公有的辅助类.仅当与外部类一起使用时才有意义.
  • 可以在外围实例之外独立存在,用于表示(封装)外部类的一些成员.

非静态成员

  • 每个实例都隐含着外围类的一个实例关联.(内存泄漏一般也是由此引起),持有强引用
  • 不可以在外围实例之外独立存在
  • 如果内部类不需要引用外部类的成员和方法,则一定要将其定义为static,避免空间/时间开销,避免内存泄漏

匿名类

  • 没有名字,不是外围类的成员,在使用的时候被声明和实例化
  • 有诸多限制,不能instanceof,无法扩展一个类或者接口.
  • 通常像Runnable,Thread在内部类中使用居多

局部类

  1. 局部类是使用最少的类,在任何可以声明局部变量地方都可以声明局部变量.
  2. 是否static取决于其定义的上下文
  3. 可以在作用域内重复使用
  4. 不能有static成员

总结

如果成员类每个实例都需要一个指向其外围实例的引用,用非静态
否则,用静态

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值