以下为我在《Effective Java》中留下的读书笔记,对一些重要的知识点提取出了摘要.
13、使类和成员的可访问性最小化
递增顺序的四种访问级别:私有的(private) < 包级私有的(default,缺省值) < 受保护的(protected) < 公有的(public)
private 和 default 一般不会影响导出的API,protected 和 public 修饰的类成员将会是类导出API的一部分.如果这个类实现了Seralizable接口,即使是private 或 default 的这些域就有可能会被泄露(leak)到导出的API中;
如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别.(Java硬性规则)这样确保了里氏替换原则,任何可以使用超类实例的地方也都可以使用子类的实例.
实例域决不能是公有的.一旦这个域公有,就放弃了对存储在这个域中的值进行限制的能力.
注意,长度非零的数组总是可变的.实例代码如下:
package privateArray;
import java.util.Arrays;
public class Array {
public static final String[] VALUES = {"hello","world"};
public static void main(String[] args) {
VALUES[1] = "scc";
System.out.println(Arrays.toString(VALUES));
}
}
运行结果如下:
控制长度非零的数组,可以有以下两种解决方案:
使公有数组变成私有的,并增加一个公有的不可变列表:
package privateArray;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Array {
private static final String[] PRIVATE_VALUES = {"hello","world"};
public static final List<String> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
}
使数组变成私有的,并增加一个公有方法,它返回私有数组的一个备份:
package privateArray;
public class Array {
private static final String[] PRIVATE_VALUES = {"hello","world"};
public static final String[] values(){
return PRIVATE_VALUES.clone();
}
}
补充:Collections.unmodifiableList(); 得到一个不可变的List
14、在共有类中使用访问方法而非公有域
如果类是包级私有的或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误.所以,如果有必要,可以将私有嵌套类的公有域限制在外围类中.
公有类永远都不应该暴露可变的域
15、使可变性最小化
Java类库包含的不可变类:String、基本类型的包装类、BigInteger和BigDecimal
为了使类成为不可变,遵循的五条规则:
1、不要提供任何会修改对象状态的方法
2、保证类不会被拓展
3、使所有的域都是final的
4、使所有的域都成为私有的
5、确保对于任何可变组件的互斥访问
如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用.并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用.在构造器、访问方法和readObject方法中请使用保护性拷贝技术.
不可变对象本质上是线程安全的,它们不要求同步.
频繁用到的不可变对象应该考虑缓存起来. 提供静态工厂.例如所有基本类型的包装类和BigInteger都有这样的静态工厂.
不仅可以共享不可变对象,甚至也可以共享它们的内部信息. 例如,BigIntefer类内部使用了符号数值表示法,符号用一个int类型的值来表示,数值则用一个int数组表示.negate方法产生一个新的BigInteger,其中数值是一个的,符号则是相反的.
让类的所有构造器都变成私有的或者包级私有的并添加公有的静态工厂来代替公有的构造器,以此保证不可变的类变成final
BigInteger和BigDecimal 可能被继承,所以在编写一个类,如果安全性依赖于BigInteger或者BigDecimal参数的不可变性,就必须进行检查,以确定这个参数是否为"真正的“BigInteger或BigDecimal,而不是不可信任子类的实例.如果是不可信任子类的实例,就对它进行保护性拷贝:
public static BigInteger safeInstance(BigInteger val){
if(val.getClass() != BigInteger.class)
return new BigInteger(val.toByteArray());
return val;
}
补充:java内存模型、BigInteger源码、readObject\readResolve、ObjectOutputStream.writeUnshared\ObjectOutputStream.readUnshared
16、复合优先于继承
子类依赖于其超类中特定的实现细节.超类的实现由可能随着发行版本的不同而又变化,如果真的发生了变化 ,子类可能会遭到破坏.即使子类在拓展一个类的时候,仅仅是增加新的方法,也有可能超类在后续的发行版本中出现同样名字的方法.
除非拥有is-a关系,超类是专门为了拓展而设计的,并且具有很好的文档才使用继承.
补充:Properties不应该拓展Hashtable
疑惑:委托、回调、代理
17、要么为继承而设计,并提供文档说明,要么就禁止继承
为继承而设计的类必须有文档说明它可覆盖的方法的自用性. (专门针对实现子类的程序员的信息)对于每个public或protected或构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的.
更一般地,类必须在文档中说明,在哪些情况下它会调用可覆盖的方法.
构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用.
如果决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,无论是clone还是readObject都不可以调用可覆盖的方法
,不管是直接还是间接.
对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化.
补充:钩子
18、接口优于抽象类
现有的类可以很容易被更新,以实现新的接口
接口是定义mixin的理想选择
接口允许我们构造非层次结构的类型框架
接口使得安全地增强类的功能成为可能.(包装类模式)
简而言之,接口通常是定义允许多个实现的类型的最佳途径.除非当演变的容易性比灵活性和功能更为重要的时候.因为
抽象类可以添加新的具体方法,演变比接口的演变要容易.
补充:骨架实现(skeletal implementation),为接口提供一个抽象的骨架实现类,按照惯例,骨架实现类被称为AbstractInterface.例如,AbstractCollection、AbstractSet、AbstractList......
19、接口只用于定义类型
常量接口模式是对接口的不良使用
常量接口替代办法:
如果这些常量与某个现有的类或者接口紧密相关,就把这些常量添加到这个类或者接口中.例如Integer和Double都导出了MIN_VALUE和MAX_VALUE常量.
如果这些常量最好被看做枚举类型成员,就用枚举类型.
否则使用不可实例化的工具类.
静态导入机制 import static com.effectivejava.science.PhysicalConstants.*; (常量工具类) 就能避免用类名来修饰常量名
补充:二进制兼容性
20、类层次优于标签类
标签类
package classlayer;
public class Figure {
enum Shape{
RECTANGLE,CIRCLE
};
//Tag field
final Shape shape;
double length;
double width;
double radius;
public Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
public 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();
}
}
}
类层次
package classlayer;
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;
}
}
21、用函数对象表示策略
函数对象表示策略的例子:Comparator
作为经典的具体策略类是无状态的,它没有域,所以作为一个Singleton是非常合适的.
具体的策略类往往使用匿名类声明.
策略接口
“宿主类”(host class)还可以导出公有的静态域.例如String.CASE_INSENSITIVE_ORDER域是一个比较器.
补充:lambda表达式
22、优先考虑静态成员类
四种嵌套类:静态成员类、 非静态成员类、匿名类、局部类(这三个是内部类)
静态成员类可以看作是普通的类,它是外围类的一个静态成员,它可以访问外围类中任何访问限制符修饰的成员.
静态成员类的一种常见用法是作为公有的辅助类.例如Calculator.Operation.PLUS\Calculator.Operation.MINUS
非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联.在非静态成员类的实例方法内部,可以调用外围实例上的方法.
非静态成员类的一种常见用法是定义一个Adapter.例如,Map接口的实现往往使用非静态成员类来实现它们的集合视图,这些集合视图是由Map的keySet、entrySet和Values方法返回的.诸如Set和List这种集合接口的实现往往也使用非静态成员类来实现它们的迭代器.
若成员类不需要访问外围类实例,则用静态成员类
匿名类是在使用的同时被声明和实例化.当且仅当匿名类出现在非静态的环境中时,它才有外围实例.匿名类不能再次扩展一个类或者再实现新的接口;
匿名类的常见用法: 动态地创建函数对象(Comparator)
创建过程对象(Runnable、Thread)
在静态工厂方法的内部
局部类用得很少:
private void method_nmc(){
class Hello{
}
new Hello();
}