Effective Java 读书笔记(三):类与接口
1 最小化类和成员的可访问性
(1)封装
- 封装对组成系统的组件进行解耦,从而允许这些组件独立开发,测试,优化,使用,理解和修改。
- 封装提高了软件的复用性,因为组件间的耦合度低使得它们不仅在开发环境,而且在别的环境也能变得有用。
- 封装降低了开发大型系统的风险,因为即使系统不可用了,但这些独立的组件却有可能仍可用。
(2)对于成员(域,方法,嵌套类,或者嵌套接口),都有四种可能的访问级别
- private:成员只能被声明它的顶级类访问。
- default:成员可以被声明它的包下面的所有类访问。
- protected:成员可以被声明它的类的子类访问,同时,声明它的包下面的所有类也可以访问它。
- public:成员可以在任何地方被访问。
(3)模块
- Java 9在中,作为模块系统(module system)引入了两种额外的隐式访问级别。
- 一个模块就是一组包,就像一个包是一组类一样。
- 一个模块可以通过在它的模块声明(一般包含在module-info.java的源文件中)的导出声明显式地导出它的一些包。
- 模块的非导出包的公有和受保护成员在模块外部是无法访问的,而在模块内部,访问性不受导出声明的影响。
(4)demo
// Potential security hole!
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();
}
2 在公有类中使用访问方法,而不是公有域
(1)概述
- 如果一个类在包外可以被访问,就应该提供访问方法,以此来保留改变类内部展示的灵活性。
- 如果一个类是包级私有或者是个私有嵌套类,那么暴露它的数据域也没有什么本质上的错误。
(2)demo
// Degenerate classes like this should not be public!
class Point {
public double x;
public double y;
}
// Encapsulation of data by accessor methods and mutators
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;
}
}
- 假设需要限制x,y的范围为[1,100],第一种方式是无法实现的,你可能说可以在外部代码限制,但实际上这破环了单一职责原则。
3 使可变性最小化
(1)不可变类的5个原则
- 不要提供修改对象状态的方法(没有setter方法)
- 确保这个类不能被拓展(设置为final,防止被继承)
- 所有域设置为final,只能赋值一次且无法修改
- 所有域设置为私有
- 确保对任何可变组件(引用属性)的互斥访问:对引用属性进行保护性拷贝。
(2)demo
// Immutable complex number class
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 double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}
// plus代表相加返回新的对象,函数式编程,状态不可变。
// add代表相加返回this对象,命令式编程,状态可变。
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.im + im * c.re);
}
public Complex dividedBy(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Complex)) return false;
Complex c = (Complex) o;
// See page 47 to find out why we use compare instead of ==
return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
}
@Override
public int hashCode() {
return 31 * Double.hashCode(re) + Double.hashCode(im);
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}
(3)优势
- 不可变对象天生就是线程安全的,它们不要求同步。
- 不可变对象可以自由地被共享。
- 不可变类可以提供静态工厂将频繁被请求的实例缓存起来,从而避免重复创建现有实例。(Integer的-128~127)
- 永远都不用进行保护性拷贝(降低内存占用)
- 不仅可以共享不可变对象,它们的内部信息也可以被共享。(BigInteger的实现符号使用int,数据使用int[],取反操作不需要clone数组)
- 不可变对象为其它对象提供了大量的构件
- 不可变对象提供了免费的失败原子机制(根本不会改变)
(4)缺点
- 对于每个不同的值都需要一个对应的对象
(5)设计原则
- 类应该都是不可变的,除非有个很好的理由需要它们是可变的。
- 如果一个类不能做成不可变,那就尽可能限制它的可变性。(如:CountDownLatch,当计数变为0就不能再使用了。)
- 构造器应该完全初始化对象,并建立好不变性。
4 组合优于继承
(1)继承
- 继承违反了封装原则:未来父类升级,将导致子类的失效(相同签名不同返回导致编译不通过,相同签名且重写了父类未来的方法将导致不可预测的错误)。
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
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 getAddCount() {
return addCount;
}
}
// jdk7
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList({"Snap", "Crackle", "Pop"})); // 结果为3
// jdk9
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList({"Snap", "Crackle", "Pop"})); // 结果为6
// jdk升级后,HashSet内部addAll方法依赖于add方法
(2)组合
// 包装类:装饰者模式
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 getAddCount() {
return addCount;
}
}
// 转发类:组合模式
// Reusable forwarding class
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 isEmpty() {
return s.isEmpty();
}
public int size() {
return s.size();
}
public Iterator<E> iterator() {
return s.iterator();
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
public Object[] toArray() {
return s.toArray();
}
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}
- Guava就为所有的集合接口提供了转发类,无需自己实现转发类。
- SELF问题:包装者对象不适用于回调框架,因为在回调框架里,对象需要将自身引用传给别的对象,以便别的对象在后续进行调用。因为被包装对象并不知道它的包装者,它将一个引用传给它自己(this)同时回调也避开了包装者。
- 只有当类B和类A是“is-a”的关系时,类B才应该继承类A。
5 若要设计继承,则提供文档说明,否则禁止继承
- 必须在这个类的文档里为可覆盖方法说明它的自用性(self-use):对于每个公有方法或受保护方法,文档里都必须指明这个方法调用了哪些可覆盖方法,是以什么顺序调用的,每个调用的结果是如何影响接下来的处理过程。
- 测试一个用于被继承的类的唯一方式是编写子类:经验表明,三个子类就足以测试一个可扩展的类了。而且这些子类应该由父类作者之外的人来编写。
- 一个类允许被继承时:
- 构造器一定不能调用可覆盖方法:构造器里调用不可覆盖的方法,即私有方法,final方法和静态方法,则是安全的。
- 实现Cloneable接口或Serializable接口时,clone方法或readObject方法,都不应该直接或间接地调用一个可覆盖方法。
- 实现Serializable接口且拥有readResolve方法或writeReplace方法时,需要设置为受保护的,而不是私有的。
- 防止子类化:
- final修饰类
- 构造器设置为私有并提供公有的静态工厂创建方法
6 接口优于抽象类
(1)接口优势
-
现有类可以很容易地被改造以实现一个新的接口。
-
接口是定义混合类型(mixins)的理想选择。
-
接口构造非层次结构的框架。
-
通过包装者类模式,使用接口使得安全地增强类的功能成为可能。
-
混合模式demo
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
// 既是歌手又是作曲人
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
(2)接口
- 域只允许用public static修饰
- 方法:
- private static修饰
- public static修饰
- public default修饰
public interface Interface {
public static int test = 0;
public static void publicTest() {
privateTest();
}
private static void privateTest() {
System.out.println(test);
}
public default void defaultTest() {
System.out.println("defaultTest");
}
public abstract void abstractTest();
}
public class InterfaceImpl implements Interface {
public void extendTest() {
defaultTest();
}
@Override
public void abstractTest() {
System.out.println("abstractTest");
}
public static void main(String[] args) {
InterfaceImpl extend = new InterfaceImpl();
extend.extendTest();
extend.defaultTest();
System.out.println(Interface.test);
Interface.publicTest();
}
}
(3)接口与实现类结合使用:模板类
-
模板实现类在基本接口方法上实现剩余的非基本接口方法(模板方法模式)。
-
demo1:继承模板类
// AbstractList是模板类,基于它可以非常容易实现新的类
// 适配器模式:int[] -> List<Integer>
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;
}
};
}
- demo2:模板类编写
// Map.Entry最主要的两个方法:getKey和getValue(必需实现) setValue(可选,只有可修改的Map才需要实现)
// hashCode、equals和toString方法可以由模板类提供默认实现
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
// Entries in a modifiable map must override this method
@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
// Implements the general contract of Map.Entry.equals
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Map.Entry)) return false;
Map.Entry<?,?> e = (Map.Entry) o;
return Objects.equals(e.getKey(), getKey()) && Objects.equals(e.getValue(), getValue());
}
// Implements the general contract of Map.Entry.hashCode
@Override
public int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
@Override
public String toString() {
return getKey() + "=" + getValue();
}
}
7 接口拓展
- java8之后,jdk中的集合相关接口都添加了lambda的支持,但是这只对继承模式的实现类有效,对于采用组合模式的拓展类可能导致问题(装饰者模式)。比如:本来是需要加锁的,但是新加入的default方法并没有加锁,就可能导致并发问题。
- 所以说对于接口的拓展(添加默认方法或者添加抽象方法)都需要慎重,实在没有好的实现不如新创建一个接口去进行拓展,避免因为疏忽和造成不可预测的错误。
8 接口只用来定义类型
- 通过常量接口模式来使用接口是很糟糕的:如果一个非final类实现了一个常量接口,那么它的所有子类的命名空间都将被接口的常量污染。
public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
- 替代方案:
public class PhysicalConstants {
private PhysicalConstants() { } // Prevents instantiation
public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
public static final double BOLTZMANN_CONST =1.380_648_52e-23;
public static final double ELECTRON_MASS = 9.109_383_56e-31;
}
9 优先使用类层次,而不是标签类
- 标签类:
- 混乱的模板代码:枚举、标签域和switch语句等
- 标签类实例负担许多不相关的域,导致内存占用增加。
- switch-case可能导致运行时错误
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);
}
}
}
- 类层次:
- 代码清晰简单
- 域都不可变
- 消除运行时失败
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;
}
}
10 优先考虑静态成员类
(1)嵌套类
- 嵌套类是定义在另一个类中的类。
- 一个嵌套类的存在应该是为了服务它的外围类。
- 如果一个嵌套类还可以用于别的上下文,那么它就应该是个顶层类
- 分类:
- 静态成员类
- 非静态成员类
- 匿名类
- 局部类
(2)静态成员类
- 可视为普通类
- 可访问外围类的所有成员(包括private修饰的)
- 一般作为一个公有的辅助类,只有与它的外围类一起用时才有意义。
public class Calculator{
public static enum Operation{
PLUS,MINUS;
}
}
- 私有静态成员类通常被用来展示代表外围类对象的组件。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
static class Node<K,V> implements Map.Entry<K,V>{}
}
- demo
public class OuterClass {
private static int outTest = 1;
public static class InnerClass {
private int inTest;
public void test() {
System.out.println(outTest);
}
}
public static void main(String[] args) {
InnerClass innerClass = new OuterClass.InnerClass();
innerClass.test();
}
}
(3)非静态成员类
- 非静态成员类的每个实例都是隐式地和它的外围类的实例关联在一起。(内存占用比较高,因为它拥有外围类实例的引用)
- 在非静态成员类的实例方法内部,你可以调用外围实例的方法或通过标识了this的构造来获取外围实例的引用。
- 非静态成员类的一个通常的用法是,定义一个适配器 ,而这个适配器让外围类的实例被看成是某个不相关类的实例。
// 如:List -> Iterator
public class MySet<E> extends AbstractSet<E> {
// Bulk of the class omitted
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
}
}
- demo
public class OuterClass {
private int outTest = 1;
public class InnerClass {
private int inTest;
public void test() {
System.out.println(outTest);
// System.out.println(OuterClass.this.outTest);
}
public OuterClass getOuterClass(){
return OuterClass.this;
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
innerClass.test();
}
}
(4)匿名类
- 匿名类可以没有名字。
- 匿名类并不是它的外围类的一个成员。
- 匿名类出现在静态的上下文当中,也不能拥有除了常量型变量的任何的静态成员,这些常量型变量是final的基本类型或初始化常量表达式的字符串属性。
- 匿名类需要在声明的时候初始化。
- 匿名类无法使用instanceof来判断。
- 匿名类无法继承或实现其他接口了。
- 匿名类的常见用法是实现静态工厂方法,参见第6节的demo1。
- 匿名类是创建小的函数对象和处理对象的首选方式(现在被lambda取代)
public static void main(String[] args) {
try(AutoCloseable myCloseable = new AutoCloseable() {
@Override
public void close() {
System.out.println("close success!");
}
};) {
System.out.println("use myCloseable");
} catch (Exception e) {
e.printStackTrace();
}
}
(5)局部类
- 局部类与局部变量一样,只存在局部作用域。
public static void main(String[] args) {
class A{
private int test = 1;
};
A a = new A();
System.out.println(a.test);;
}
(6)用途
- 如果一个嵌套类必须在方法外部可见,或者放在方法内部会显得太长时,就使用成员类。
- 如果成员类的实例需要拥有该类的外围类的引用,就将其做成非静态;不然,就将其做成静态。
- 假设一个类应当在方法内部,若你需要只从一个地方创建实例而且已经存在一个类型能说明这个类的特征,那么将其做成匿名类;否则,就将其做成局部类。
参考: