Effect Java 阅读笔记(一)
来源:互联网 发布:代理商域名转到阿里云 编辑:程序博客网 时间:2024/06/03 17:06
Chapter2 创建和销毁对象
1. 考虑用静态工厂方法代替构造器
一个静态工厂的小例子
//以下方法得到的对象是事先构造好的不可变对象,反复利用public static Boolean valueOf(boolean b){ return b? Boolean.TRUE : Boolean.FALSE;}
使用静态工厂的优势
- 有名称,见文知意(当一个类需要多个带有相同签名的构造器时,就可用静态工厂方法代替构造器,并且慎重的选择它们的名字以便于区分)
- 不必在每次调用的时候都创建新的对象(例如Singleton、上面例子)
可以返回原类型的任何子类型的对象
Effj : API可以返回对象,同时又不会使对象的类型变成公有的,以这种方式隐藏实现类会使API变得十分简洁
在创建参数化类型实例的时候,静态工厂方法使代码变得简洁
//有了静态工厂方法,编译器可以替你找到类型参数(类型推导 type inference)public static <K, V> HashMap<K, V> newInstance(){ return new HashMap<K, V>();}
- 静态工厂方法的缺点
- 类如果不含有公有的或者受保护的构造器,就不能被子类化(也有好处,鼓励程序员使用复合,而不是继承)
- 它们与其他静态方法没有什么区别,在API文档中,没有明确标识
2. 遇到多个构造器参数时要考虑使用构建器(Builder)
简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是不错的选择
3. 用私有构造器或者枚举类型强化Singleton属性
Singleton指仅仅被实例化一次的类,Singleton通常备用来代表那些本质上唯一的组件
Singleton实现1
//Singleton with public final filedpublic class Elvis{public static final Elvis INSTANCE = new Elvis();private Elvis(){....}public void leaveTheBuilding(){.....}}
Singleton实现2
//Singleton with static factorypublic class Elvis{private static final Elvis INSTANCE = new Elvis();private Elivis(){....}public static getInstance(){ return INSTANCE; }public void leaveTheBuilding(){...}}
Singleton实现3
//Enum singleton - the preferred approachpublic Enum Elvis{INSTANCE;public void leaveTheBuilding(){...}}
单元素的枚举类型已经成为实现Singleton的最佳方法。(它更加简洁,无偿提供了序列化机制,绝对防止多次实例化)
4. 通过私有构造器强化不可实例化的能力
显示指定构造器是私有的(则该类不可被实例化,也同时不可被子类化)
ps : 企图通过将类做成抽象类来强制该类不可被实例化是行不通的,该类可被继承,继而实例化
5. 避免创建不必要的对象
- 静态工厂方法优先于构造函数,避免不必要的对象创建
- 重用不可变对象
- 延迟初始化实现更复杂,但性能并没有很大的提升
- 优先使用基本数据类型而不是装箱类型,当心无意识的自动装箱
- 当对象的创建相当重量级时,才应该通过维护自己的对象池来避免创建对象
本条提及 “当你应该重用对象的时候,请不要创建对象”,对应的在39条说 “当你应该创建对象的时候,请不要重用现有对象” 。
6. 消除过期对象的引用
7. 避免使用终结方法(finalizer)
- 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的
Chapter3 对于所有对象都通用的方法
8. 覆盖equals时请遵守通用约定
- 几点规则
- 自反性
- 对称性
- 传递性
- 一致性
- 实现高质量equals的诀窍
- 使用==操作符检查 “参数是否为这个引用”
- 使用instanceof操作符检查 “参数是否为正确的类型”
- 把参数转换为正确的类型(上一步的instanceof可以保证这一点)
- 对于该类中的每个关键域(significant)检查参数中的域是否与该对象中相应的域匹配
- 当你编写完equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的
- 几点告诫
- 覆盖equals时总要覆盖hashCode
- 不要企图让equals方法过于智能
- 不要将equals申明中的Object换成其它对象
9. 覆盖equals时总要覆盖hashCode
public final class PhoneNumber { private final int areaCode; private final int prefix; private final int lineNumber; public PhoneNumber(int areaCode, int prefix, int lineNumber) { rangeCheck(areaCode, 999, "area code"); rangeCheck(prefix, 999, "prefix"); rangeCheck(lineNumber, 9999, "lineNumber"); this.areaCode = areaCode; this.prefix = prefix; this.lineNumber = lineNumber; } private void rangeCheck(int arg, int max, String name) { if (arg < 0 || arg > max) throw new IllegalArgumentException(name + ":" + arg); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof PhoneNumber)) return false; PhoneNumber pn = (PhoneNumber) o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; } @Override public int hashCode() { return 42; } public static void main(String[] args) { //如果没有覆盖hashCode,两个相等的实例具有不同的散列码 //要保证equals比较相等的两个实例返回相同的hashCode,散列集合才有效 Map<PhoneNumber, String> m = new HashMap<>(); m.put(new PhoneNumber(707, 867, 5309), "jeny"); String phoneNumber = m.get(new PhoneNumber(707, 867, 5309)); }}
复写hashCode方法的一种实现
- 把某个非0的常数值,比如说17,保存在一个名为result的int型变量中
- 对于对象中的每个关键域f(指equals方法中涉及的每个域),完成以下步骤
- 为该域计算int类型的散列码c
- 如果该域是boolean型,则计算 (f ? 1 : 0) ;
- 如果该域是byte、char、short、或者int、类型,则计算 (int)f ;
- 如果该域是long类型,则计算 (int)(f ^ *(f >> 32));
- 如果该域是float类型,则计算 Float.floatToIntBits(f) ;
- 如果该域是double类型,则计算 Double.douebleToLongBits(f) ,然后按上述第三小步计算散列值
- 如果该域是一个对象的引用, 并且该类的equals方法通过递归调用equals的方式来比较这个域,这同样为这个递归调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用hashCode。 如果这个域的值为null,则返回0(或者其他某个常数,通常为0) ;
- 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列值。如果数组域中的每个元素都很重要,可以利用Arrays.hashCode方法
- 按照下面的公式,把上述步骤计算得到的散列码c合并到result中
result = 32 * result + c
- 返回result
- 完成了hashCode方法验证以后,问问自己“相等的实例是否都具有相等的散列码”
private volatile int hashCode;@Overridepublic int hashCode(){int result = hashCode;if(result == 0){ result = 17; result += 31 * result + areaCode; result += 31 * result + prefix; result += 31 * result + lineNumber; hashCode = result;}return result;}
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能
10. 始终要覆盖toString方法
返回的字符串应该是简洁的,但信息丰富,并且易于阅读
11. 谨慎的覆盖clone方法
12. 考虑实现Compareable接口
Chapter4 类和接口
13.使类和成员的可访问性最小
尽可能的让每个类或者成员不被外界访问
- 如果类或者接口能做成包级私有的,那么它就应该被做成包级私有的
- 如果一个包级私有的顶层类(或者接口)只在某一个类的内部使用到,考虑做成私有嵌套类
- 降低不必要共有类的可访问性,比降低包级私有的顶层类更重要的多
- 接口中所有的方法都隐含着公有访问级别
- 子类中复写的方法,其访问权限不能低于超类对应方法的访问权限
实例域决不能是公有的
类具有公有的静态final数组域,或者返回这种与的访问方法,这几乎总是错的
//Potential security holepublic static final Thing[] VALUES = {...};
- 长度非0的数组总是可变的。
- 如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容,这是安全漏洞的常见根源。
解决方法:
可以使数组变成私有的,并增加一个公有的不可变列表
private static final Thing[] PRIVATE_VALUES = {...};//build a unmodified view for the static arraypublic static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
可以使数组变成私有的,并添加一个公有的方法,它返回私有数组的一个备份
private static final Thing[] PRIVATE_VALUES = {...};public static final Thing[] values(){ return PRIVATE_VALUES.clone();}
14. 在公有类中使用访问方法而非公有域
- 公有类不应该直接暴露数据域(数据域私有,提供getter、setter)
- 如果类是包级私有的,或者是私有的嵌套类,则直接暴露它的数据域没有本质的错误
总之,公有类永远都不应该暴露可变的域
15. 使可变性最小化
不可变类:实例不能被修改的类,每个实例包含的信息都必须在创建该实例时就提供,并且在整个生命周期(lifetime)内固定不变。(String, 基本类型的包装类,BigInteger,BigDecimal)=>不可变类比可变类更易于设计、实现和使用。他们 不容易出错且更加安全
- 不可变类遵循的5条规则
- 不要提供任何会修改对象状态的方法
- 保证类不会被扩展/继承(一般做法是使类成为final,也可让构造器私有化,并添加公有的静态工厂来替代公有的构造器)
- 使所有的域都是final的
- 使所有的域都成为私有的(可防止客户端获得域引用的可变对象的权限,同时防止客户端直接修改这些对象)
- 确保对于任何可变组件的互斥访问(如果类具有指向可变对象的域,则必须确保客户端无法获得该对象的引用;在构造器、访问方法和readObject方法中都应该采用保护性拷贝技术(defensive copy))
- 不可变类本质上是线程安全的,它不要求同步,可被自由的共享
- 不可变类唯一的缺点就是对于每个不同的值都要有一个单独的对象
- 坚决不不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类成为可变的类,否则就应该是不可变的(唯一的缺点就是在特定的情况下存在潜在的性能缺陷)
- 如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性
16. 复合优先于继承
使用继承存在的问题
假设我们要实现统计HashSet历史添加元素的个数,我们可能会有如下实现
class InstrumentedHashSet<E> extends HashSet<E>{private int addCount = 0;@Overridepublic boolean add(E e) { addCount++; return super.add(e);}@Overridepublic boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c);}public int getAddCount() { return addCount;}}
但当我们执行如下操作测试的时候,发现输出的是6(这是因为HashSet的addAll方法是基于add方法实现的)
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();s.addAll(Arrays.asList("asd", "dsaf", "asdf"));System.out.println(s.getAddCount());
对于上述问题,我们只要去掉对addAll方法的复写即可,但是同时它功能的正确性依赖于超类中addAll方法是基于add方法实现这一事实(这依赖于父类的实现细节,一旦父类的addAll方法更改实现方式,这个类可能变得不可用)
导致子类脆弱的原因
如果子类中复写了超类中的某个方法,并且这个实现依赖于超类中的某个实现细节, 如果 下一个发行版中该超类的对应实现改变了 ,这个子类就有可能出错
如果子类不复写超类中的方法,只是扩展了一些方法。则 如果超类在后续的发行版本中获得了一个新的方法,并且该方法的签名与你在子类中扩展的方法相同,只是返回值类型不同 。那么此时子类将无法通过编译
采用复合/转发的方法来代替InstrumentedHashSet类。(包含类本身和可重用的转发类 forwarding classs, 包含了所有转发方法,没有其他方法)
//Wrapper class - uses composition in place of inheritanceclass InstrumentedHashSet<E> extends ForwardingSet<E>{ private int addCount = 0; public InstrumentedHashSet(Set<E> s) { super(s); } @Override public boolean add(E o) { addCount++; return super.add(o); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; }}//Reusable forwarding classclass ForwardingSet<E> implements Set<E>{ private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } @Override public int size() { return s.size(); } @Override public boolean isEmpty() { return s.isEmpty(); } @Override public boolean contains(Object o) { return s.contains(o); } @Override public Iterator<E> iterator() { return s.iterator(); } @Override public Object[] toArray() { return s.toArray(); } @Override public <T> T[] toArray(T[] a) { return s.toArray(a); } @Override public boolean add(E e) { return s.add(e); } @Override public boolean remove(Object o) { return s.remove(o); } @Override public boolean containsAll(Collection<?> c) { return s.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { return s.addAll(c); } @Override public boolean retainAll(Collection<?> c) { return s.retainAll(c); } @Override public boolean removeAll(Collection<?> c) { return s.removeAll(c); } @Override public void clear() { s.clear(); }}
上述InstrumentedHashSet类是一个包装类,可以用来包装任何Set实现;
InstrumentedHashSet对一个集合进行了修饰,为它增加了计数特性
包装类几乎没有什么缺点,需要注意的是,包装类不适合用在回调框架(Callback framework)当中
简而言之,继承功能非常强大,但也存在诸多问题,因为它违背了封装原则。只有当子类和超类之前确实存在继承关系(is-a),使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承兼顾会导致脆弱性。
- Effect Java 阅读笔记(一)
- java编程思想阅读笔记(一)
- Thinking in Java阅读笔记(一)
- 《Effective Java》阅读笔记(一)
- 《JAVA网络编程》阅读笔记(一)
- Java编程思想阅读笔记(一)
- java编程思想阅读笔记(一)
- 阅读笔记(一)
- 阅读Java核心编程做的笔记(一)
- Effective Java 阅读笔记 enum 第6章(一)
- 深入理解Java 虚拟机阅读笔记(一)
- YAMON阅读笔记(一)
- 论文阅读笔记(一)
- javascript_core阅读笔记(一)
- FlaskBB阅读笔记(一)
- 《启示录》阅读笔记(一)
- SparkInternal阅读笔记(一)
- Gallery3D源码阅读笔记(一) RenderView.java
- 动态规划之硬币表示问题
- HDU 6034 Balala Power!(多校1)
- Centos 7.3安装Zabbix3.2
- C# WPD (windows portable devices) 检测WPD设备 获取设备信息
- Ubuntu C++ 环境的搭建
- Effect Java 阅读笔记(一)
- 图像检索系列一:Deep Learning of Binary Hash Codes for Fast Image Retrieval
- BZOJ 1083: [SCOI2005]繁忙的都市
- POJ:Subsequence
- WPD 从便携设备拷贝文件到PC文件不完整的解决办法
- BDIP-BVLC纹理
- [机器学习入门] 李宏毅机器学习笔记-32 (Recurrent Neural Network part 1;循环神经网络 part 1)
- HDU
- 邻接表数组实现