类和接口
使类和成员的可访问性最小
信息隐藏(information hiding)/封装(encapsulation):隐藏模块内部数据和其他实现细节,通过API和其他模块通信,不知道其他模块的内部工作情况。
原因:有效地解除各模块之间的耦合关系
访问控制机制(access control):决定类,接口和成员的可访问性。由声明的位置和访问修饰符共同决定。
对于顶层的类和接口,两种访问级别:
- 包级私有的(package-private)
- 公有的(public)
对于成员(域,方法,嵌套类,嵌套接口),四种访问级别:
- 私有的
- 包级私有的
- 受保护的
- 公有的
公有的或受保护的成员是类的导出API的一部分。代表了对某个实现细节的公开承诺。
实例域决不能是公有的。
通过公有final静态域来暴露常量。
长度非零的数组总是可变的。类具有公有的静态final数组域或者返回这种域的访问方法,这几乎是错误的。
总结:
尽可能地降低可访问性。除了公有静态final域表示常量的特殊情形外,公有类都不应该包含静态域。并且要确保公有静态final域所引用的对象都是不可变的。
在公有类中使用访问方法而非公有域
//退化类
//没有封装
class Point{
public double x;
public double y;
}
//退化类应该被包含私有域和公有设值方法的类代替
class Point{
//私有域
private double x;
private double y;
public Point(double x,double y){
this.x = x;
this.y = y;
}
//公有的getter和setter方法
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;
}
}
若类是包级私有的或是私有的嵌套类,直接暴露数据域并没有本质的错误。
使可变性最小化
不可变类只是其实例不能被修改的类,实例的信息在创建的时候就提供并且固定不变。如String类就是不可变类。
使类成为不可变类的几条原则:
- 不提供修改状态属性的方法
- 保证类不会被扩展
- 使所有的域都是final的
- 使所有的域都是私有的
- 对于任何可变组件的互斥访问
不可变对象本质上是线程安全的,不要求同步。
不可变对象可以被自由地共享。对于频繁用到的值,提供公有的静态final常量。以重用现有的实例。
不可变对象为其他对象提供了大量的构件。
注:我觉得下面一段中译版翻译有误:
原文:[76页第5段]you don't worry about their values changing once they're in the map or set,
which would destory the map or set's invariants.译文:一旦不可变对象进入到映射(map)或者集合(set)中,尽管这破坏了映射或者集合的不变性约束,但是也不用担心它们的值会发生变化。
译文翻译得很生硬,破坏映射或集合的不变性约束指的应该是改变映射或集合的值而不是不可变对象进入映射或集合中。
修改后的译文:若不可变对象进入映射或集合,不会破坏映射或集合的不变性约束(因为它们的值不会发生变化)。
不可变类的缺点:对于每个不同的值都需要一个单独的对象。
多步操作,每步都会产生一个新的对象。除了最后的结果之外其他对象最终都会丢弃,这就会造成性能问题。
解决:
1>某个多步操作由基本类型提供
2>使用可变的配套类
常见的配套类有StringBuilder类,它是String类的配套类。
//String
String str = "";
long stringStartTime = System.currentTimeMillis();
for(int i = 0;i<1000000;i++){
str+=i;
}
long stringEndTime = System.currentTimeMillis();
System.out.println("String:"+(stringEndTime-stringStartTime));
//StringBuilder
StringBuilder sb = new StringBuilder();
long sbStartTime = System.currentTimeMillis();
for(int i = 0;i<1000000;i++){
sb.append(i);
}
long sbEndTime = System.currentTimeMillis();
System.out.println("StringBuilder:"+(sbEndTime-sbStartTime));
运行结果:
上面的例子就是使用String类和使用StringBuilder类执行多步操作,可以看出使用StringBuilder比String类快很多很多。
我们使用Javap -c
的命令来查看字节码:
可以看到str+=i;
实质上还是调用的StringBuilder,那照理说应该两者所花费的时间一样,为什么在这里差别这么大呢。
个人理解是因为,每次运行到str+=i;
都会新创建一个String对象。
下面通过Debug调试来证明我的猜测。
下面这是String对象每次添加数字到末尾的GIF动画,可看到每次str所指向的String对象的value值都不同。
而下面则是StringBuilder对象使用append方法添加数字的GIF动画,可看到它的值并没有变化。
确保类的不可变性,类本身不能被子类化:
- 使类成为final的
- 类所有的构造器私有的或包级私有的,添加公有静态工厂方法来代替公有构造器。
不可变类的缺点:在特定情况下存在潜在的性能问题。
复合优于继承
继承打破了封装性(子类依赖父类中特定功能的实现细节)
合理的使用继承的情况:
- 在包内使用
- 父类专门为继承为设计,并且有很好的文档说明
只有当子类真正是父类的子类型时,才适合用继承。
对于两个类A和B,只有两者之间存在"is-a"关系,类B才能拓展类A。
继承机制会把父类API中的所有缺陷传播到子类中,而复合允许设计新的API来隐藏这些缺陷。
复合(composition):不扩展现有的类,而是在新的类中增加一个私有域,引用现有类的一个实例。
转发(fowarding):新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回结果。
public class FowardSet<E> implements Set<E> {
//引用现有类的实例
private final Set<E> set;
public FowardSet(Set<E> set){
this.set = set;
}
/*
*转发方法
*/
@Override
public int size() {
return set.size();
}
@Override
public boolean isEmpty() {
return set.isEmpty();
}
@Override
public boolean contains(Object o) {
return set.contains(o);
}
@NotNull
@Override
public Iterator<E> iterator() {
return set.iterator();
}
@NotNull
@Override
public Object[] toArray() {
return set.toArray();
}
@NotNull
@Override
public <T> T[] toArray(T[] a) {
return set.toArray(a);
}
@Override
public boolean add(E e) {
return set.add(e);
}
@Override
public boolean remove(Object o) {
return set.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return set.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}
@Override
public void clear() {
set.clear();
}
@Override
public boolean equals(Object obj) {
return set.equals(obj);
}
@Override
public String toString() {
return set.toString();
}
@Override
public int hashCode() {
return set.hashCode();
}
}
/*
* 包装类(wrapper class),采用装饰者模式
*/
public class InstrumentedSet<E> extends FowardSet<E> {
private int addCount=0;
public InstrumentedSet(Set<E> set) {
super(set);
}
@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;
}
}
上面的例子中,FowardingSet是转发类,也是被包装类,而InstrumentedSet是包装类,它采用的是装饰者模式,而不是委托模式。
包装类不适合用在回调框架(callback framework)中,会出现SELF问题。
在回调框架中,对象把自身的引用传递给其他对象,用于后续的调用(回调)
SELF问题:被包装的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时却避开了外面的包装对象。
接口优于抽象类
两种机制用来定义允许多个实现的类型:接口和抽象类
区别:
- 现有的类可以很容易被更新,以实现新的接口。
- 接口是定义混合类型的理想选择。
- 接口允许我们构造非层次结构的类型框架。
抽象的骨架实现类(skelletal implementation class):把接口和抽象类的优点结合起来。接口负责定义类型,骨架实现类接管所有与接口实现相关的工作。
Java Collections中为每个重要的集合接口都提供了一个骨架实现。如AbstractCollection,AbstractSet,AbstractList,AbstractMap。
设计得当,骨架实现可以让程序员很容易就提供我们自己的接口实现。
模拟多重继承:实现了接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上
抽象类的演变比接口的演变容易多了:在后续的发行版本中,在抽象类中增加新的方法,始终可以增加具体的方法,它包含合理的默认实现。该抽象类的所有现有的实现都将提供这个新的方法。对于接口,这样做不行。
接口是定义允许多个实现的类型的最佳途径。当是否容易演变比灵活性和功能更为重要的时候,应该使用抽象类。
接口只用于定义类型
不应该使用接口来定义常量,可以使用枚举类型或者是不可实例化的工具类来定义常量。
/**
* 使用接口定义常量,不推荐使用
* 细节的实现,会被泄露到导出到API中
* 并且代表一种承诺,类为保证二进制兼容性,需要一直实现接口即使它不再需要使用这些常量了
*/
public interface MathConstants {
static final double PI = 3.14159265;
}
/**
* 使用枚举类型定义常量
*/
public enum MathConstantsWithEnum {
PI(3.14159265);
private double value;
private MathConstantsWithEnum(double value){
this.value = value;
}
}
/**
* 使用不可实例化的帮助类来定义常量
*/
public class MathConstantsUtil {
private MathConstantsUtil(){}
public static final double PI = 3.14159265;
}
import static com.xyz.johntsai.effectivejava.MathConstantsUtil.*;
/**
* 静态导入机制,避免用类名来修饰常量名
*/
public class TestStaticImport {
public static double getArea(double r){
return PI*r*r;
}
}
类层次优于标签类
/**
* 标签类
* 缺点:过于冗长,易错,效率低下,不易拓展
*/
public class Figure {
enum Shape{
RECTANGLE,CIRCLE
}
final Shape shape;
//用于三角形
double width;
double length;
//用于圆形
double radius;
//三角形的构造器
Figure(double length,double width){
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
Figure(double radius){
shape = Shape.CIRCLE;
this.radius =radius;
}
double getArea(){
switch (shape){
case RECTANGLE:
return this.length*this.width;
case CIRCLE:
return Math.PI*this.radius*this.radius;
default:
throw new AssertionError();
}
}
}
/**
* 类层次代替标签类
*/
/**
*Figure类是类层次的根
*/
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 width;
final double length;
Rectangle(int width,int length){
this.length = length;
this.width = width;
}
@Override
double area() {
return width*length;
}
}
用函数对象表示策略
String [] array = {"a","aa"};
//匿名内部类
Arrays.sort(array, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
});
//JDK1.8 支持lambda
Arrays.sort(array,(s1,s2)->s1.length()-s2.length());
//多次重复使用 用私有的静态成员类实现,并通过静态final成员导出
Arrays.sort(array,Host.STRING_COMPARATOR);
class Host{
private static class StrLengthComparator implements Comparator<String>,Serializable{
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
}
public static final Comparator<String> STRING_COMPARATOR = new StrLengthComparator();
}
优先考虑静态成员类
局部类:定义在代码块中(大括号里面有0个或多个语句),一般定义在方法内部
public class LocalClassExample {
static String regularExpression = "[^0-9]";
public static void validatePhoneNumber(String phoneNumber1,String phoneNumber2){
final int length = 10;
//局部类(Local class)
class PhoneNumber{
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber){
String currentNumber = phoneNumber.replaceAll(regularExpression,"");
formattedPhoneNumber = currentNumber.length()==length?currentNumber:null;
}
public String getNumber(){
return formattedPhoneNumber;
}
}
PhoneNumber number1 = new PhoneNumber(phoneNumber1);
PhoneNumber number2 = new PhoneNumber(phoneNumber2);
if(number1.getNumber()==null)
System.out.println("First number is invalid");
else
System.out.println("First number is"+number1.getNumber());
if(number2.getNumber()==null)
System.out.println("Second number is invalid");
else
System.out.println("Second number is"+number2.getNumber());
}
public static void main(String[] args) {
validatePhoneNumber("1234567890","456-7890");
}
}