Effective Java 3rd Edition — 第四章 类与接口(2)

Item 21 : Design interfaces for posterity

为后续维护、使用者设计好的接口

Java8之后允许interface提供默认方法,但是如果你要在一个已经发布的接口添加默认方法需要认证考虑带来的影响,所有实现类是否能够提供正确的结果,
虽然默认方法实现类都不需要提供实现,但是需要注意虽然编译时候不会报错,但是可能运行后就不是你想要的结果。

比如Java8里 Collection 接口的removIf方法:

default boolean removeIf(Predicate<? super E> filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}

Collections的内部类SynchronizedCollection就实现了Collection接口,并且重写了removeIf方法,因为这是一个同步集合,需要加锁:

@Override
public boolean removeIf(Predicate<? super E> filter) {
	synchronized (mutex) {return c.removeIf(filter);}
}

试想如果没有重写该方法,多线程调用该方法的时候会出现什么结果?

所以尽量不要提供默认方法特别是已经存在的接口,除非有特别的需要或者必须,仔细思考对已有实现的影响。

Item 22 : Use interfaces only to define types

接口只用于定义实现类

接口(interface)只能用于定义实现类型,而不能用来定义常量,虽然说用起来是非常方便,
如果用于定义常量,容易导致api泄露,试想如果不小心实现了一个常量接口,常量值就无法保证不变了。

比如 java.io.ObjectStreamConstants就是一个反例,但是其常量都用final声明了。

如果非要导出常量,推荐下面几种方式:

  • 如果常量依赖某个接口或者类,把他绑定到某个类导出,比如Integer导出了MAX_VALUEMIN_VALUE
  • 使用枚举
  • 使用一个常量类
    // Constant utility class
    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;
    }
    

    java7之后允许基础类型数据出现下划线,所以对于int,double,float都最好使用下划线来隔开长数字,方便阅读。

总之接口只用来定义实现类型,而不是常量。

Item 23 : Prefer class hierarchies to tagged classes

抽象类层次结构

尽量的去抽象类的层次结构,用一个书中的例子来进行说明:

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

上面的类就称为标签类(tagged classes)比如一个形状可能有矩形和圆形,他们共有的属性有面积,但是上面的结构有下面不足:

  1. 类结构复杂并且凌乱
  2. 代码可读性差
  3. 内存占用大
  4. 构造方法复杂,多种形状需要多个构造器
  5. final关键字使用麻烦,需要在构造方法中初始化
  6. 扩展性差,如果你不修改源码根本没办法添加形状,就算修改了也要添加相应的构造方法和switch...case
  7. 根据类名根本无法区分类型,抽象的不够好

但Java面向对象的思想提供了一个层级的方式来抽象一个形状,
先抽象出形状共有的属性和行为,比如面积,然后扩展这个类,用子类来表示不同的形状,在子类中定义各自的特有属性:

// 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; }
	@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; }
}

上面的结构就清楚很多,扩展性也强,其实就是强调了Java抽象和封装的概念。

Item 24 : Favor static member classes over nonstatic

尽量使用静态成员类作为嵌套类

这一节主要介绍的是四种嵌套类:

  • 静态成员类(static member classes)
    static修饰,常用于对外部类没有引用的情况;
    共有的常用于 helper ,比如
    私有的常用于内部组件,比如Map的内部对象Entry;
  • 非静态成员类(nonstatic member classes)
    通常是要引用外部类或者使用外部类的属性等等才会使用;
    由于持有对外部类的引用,对象可能无法被gc清理,导致内存泄漏,使用时要注意;
  • 匿名类(anonymous classes)
    没有名字,通常都是使用的时候声明并且初始化的;
    限制很多,不能使用和类名相关的东西比如 instanceOf,不能实现接口,继承类;
    使用的时候保持代码在10行以内,同时不要影响代码可读性;
    lambda表达式出现之前,匿名类通常是在做一些一次性操作时候使用,java8之后推荐使用lambda表达式了
  • 局部类(local classes)
    是最少使用的,可以在任何声明变量的地方声明,并且遵守相同的作用域,和其他三种有着相同之处:
    1.和成员类一样有名字,可以重用;
    2.和匿名类一样如果要持有外部类的引用不能声明在静态部分,同时也不能包含静态成员,尽量短保持代码可读性;

Item 25 : Limit source files to a single top-level class

不要在一个文件中定义多个顶级类

// Two classes defined in one file. Don't ever do this!
class Utensil {
	static final String NAME = "pan";
}
class Dessert {
	static final String NAME = "cake";
}

比如上面两个类,又在另外一个文件中定义了,那么最终使用的顺序是取决于类传入编译器的先后顺序的,所以尽量避免这种情况的出现。

  
BugHome版权所有丨转载请注明出处:https://minei.me/archives/319.html
  

发表评论

电子邮件地址不会被公开。 必填项已用*标注