Effective Java中文版
Know Why Never do.
先知道基本编程基本规则,才知道什么时候可以打破
[TOC]
创建和销毁对象
静态工厂方法代替构造器
静态工厂提交代码的可读性,并且让调用者不必为选择什么参数构造器烦恼
静态工厂能够避免创建多的重复对象
静态工厂能够返回那些你自定义该对象的子对象(比如private的,更灵活)
public class Service{
private Service(){};//Prevents instantiation
private static final Map<String,Provider> providers =
new ConcurrentHashMap<String,Provider>();
//Provider registraion API
public static void registerDefaultProvider(Provider p){
registerProvider(name,p);
}
public static void registerProvider(String name,Provider p){
providers.put(name,p);
}
//Provider unregistration API
public static void unreginsterProviders(){
providers.remove();//不穿参数全部清空
}
public static void unreginsterSelectProvider(String name){
providers.remove(name);//移除某个
}
//Service asscess API
public static Service newInstance(){
return newInstance(DEFAULT_PROVIDER_NAME);
}
public static Service new Instance(String name){
Provider p = providers.get(name);
if(p==null) throw Exception;
return p.newService();
}
}
静态工厂在参数化类型实例时更加简洁
Map<String,List<String>> m = new HashMap<String,List<String>();
==>结合泛型
Map<String,List<String>> m = HashMap.newInstance();
public static <K,V> HashMap<K,V> newInstance(){
return new HashMap<K,V>;
}
常见静态工厂方法举例
ValueOf ---返回实例和其参数具有相同值,类型转换方法
of --- valueOf 简写
getInstance --- 唯一的实例
newInstance --- 返回不同的实例
getType --- 返回对象类型
newType --- 返回不同的对象类型
多构造器参数时考虑构造器
创建带参数实例选择 | 优点 | 缺点 |
---|---|---|
重叠构造器 | 适合<=3参数确定,如自定义view,开销低 | 参数过多时,可读性差,混乱和出错 |
JavaBean模式 | 可读性好,多参不容易出错 | 出现不一致状态,违法类的不可变原则,线程不安全 |
Builder模式 | 适合>3,可读性强,灵活,参数约束,可扩展 | 额外开销,重复代码 |
//使用重叠构造器
mNativeAdLoader = new NativeAdLoader(mContext,unitId,"ab:123",超时时间);
//定义重叠构造器
public class NativeAdLoader{
public NativeAdLoader(Context context,String unitId){
this(context,unitId,"");
}
public NativeAdLoader(Context context,String UnitId,String stragegy){
this(context,unitId,stragegy,0)
}
public NativeAdLoader(Context context,String unitId,String stragegy,long timeout){
this.context = context;
this.unitId = unitId;
this.stragegy = stragegy;
this.timeout = timeout;
}
}
//使用JavaBean
mNativeAdLoader = new NativeAdLoader();
mNativeAdLoader.setContext(mContext);
mNativeAdLoader.setUnitId(unitId);
mNativeAdLoader.setStrategy("ab123");
mNatvieAdLoader.setTimeout(超时时间);
//定义JavaBean
public class NativeAdLoader{
public NativeAdLoader(){}
public void setContext(Context context){
this.mContext = context;
}
public void setUnitId(String unitId){
this.unitId = unitId;
}
public void setStrategy(String strategy){
this.strategy = strategy;
}
public void setTimeout(long timeout){
this.timeout = timeout;
}
}
//使用构键器
mNativeAdLoader = new NativeAdLoader.Builder(mContext,"unitId")
.forNativeAdSourcesByStrategy("ab:123",超时时间)
.build();
//定义构建器
public class NativeAdLoader{
public static class Builder{
//必要参数
protected Context mContext;
private String mUnitId;
//可选参数
private String mStrategy;
private long mTimeout
public Builder(Context context,String unitId){
this.mContext = context;
this.mUnitId = unitId;
}
public Builder forNativeAdSourcesByStrategy(String strategy,long timeout){
this.mStrategy = strategy;
this.mTimeout = timeout;
return this;
}
public NativeAdLoader build(){
return new NativeAdLoader(this)
}
}
public NativeAdLoader(Builder builder){
this.context = builder.context;
this.unitID = builder.unitID;
this.strategy = builder.strategy;
this.timout = builder.timeout;
}
}
强化Singleton属性
常规化标准单例写法
//Singleton with static factory
public class AdsManager{
//private(私有不可直接引用) + static(方便静态方法) + final(最终对象)+ 类加载时实例化
private static final AdsManager INSTANCE = new AdsManager();
private AdsManager(){};//私有化
public static AdsManager getInstance(){//getInstance 表明该对象的单例性
return INSTANCE;
}
//Remainder ommitted
}
解决多线程不安全问题(懒加载)
public class AdsManager{
private static transient AdsManager mInstance;//瞬时
private AdsManager(){};
public static AdsManager getInstance(){
if(mInstance == null){
synchronize(AdsManager.class){
if(mInstance ==null){
mInstance = new AdsManager();
}
}
}
return mInstance;
}
}
完美枚举写法(java 1.5上)
public enum AdsManager{
INSTANCE;
}
//简洁可读,防止多次实例化,提供序列化机制,无法反射攻击
私有构造器强化不可实例化能力
//工具类或者不想要被实例化的类
public class UtilsClass{
//将默认的构造器私有化并直接抛异常
private void UtilsClass(){
throw new AssertionError();
}
//Remainder omitted
}
避免创建不必要对象
避免多次创建同一个对象
String s = new String("123"); ==> String s = "123";
//Date,Calendar,TimeZone 只创建一次
Class Person{
private final Date birthDate;
private static final Date BOOM_START;
private static final Date BOOM_END;
//静态代码块写法,类加载仅执行一次
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(...);
BOOM_START = gmtCal.getTime();
getCal.set(...);
BOOM_END = gmtCal.geTime();
}
//坏处如果该方法不用,就会依然创建以上对象
public boolean isBabyBoomer(){
return birthDate.compareTo(BOOM_START)>=0&&
birthDate.compareTo(BOOM_END)<0;
}
}
避免无意识的自动装箱,优先使用基本数据类型
public static void main(String[] args){
Long sum = 0L;//申明时特别注意,避免使用装箱
for(long i=0;i<Integer.MAX_VALUE;i++){
sum += i;
}
System.out.println(sum);
}
到底重用还是创建附加对象考虑安全,风格,性能,清晰,简洁,功能,不要一概而论
消除过期对象的引用
内存泄漏的常见来源-过期对象
//栈实现类
public class Stack {
private Object[] elements;
private int size=0;
private static final int DEFAULT_INITIAL_CAPACITY=16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size==0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;//eliminate obsolete reference
return result;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements,2*size+1);
}
}
}
//什么是过期引用,活动部分,非活动部分?
//垃圾回收机制会自动回收非活动部分内存,而程序员要做的就是将该过期引用对象告诉垃圾,这是非活动部分。
内存泄漏的常见来源-缓存
1.WeakHashMap
代表弱引用缓存(缓存项过期自动删除,缓存项生命周期由该键的外部对象决定时)
2.不容易确定缓存的生命周期,缓存随着时间推移变得越来越没价值,定时清除Timer
或ScheduledThreadPoolExecutor
3.缓存添加新条目时清理,LinkedHashMap.removeEldestEntry
4.更复杂直接使用java.lang.ref???
内存泄漏的常见来源-监听器或其他回调
1.没有显式的取消注册
2.不确定什么时候取消,只保存回调的弱引用,常见保持成WeakHashMap中的键
保证执行终止方法
//io流,connection,DB等
try{
//do something
}catch(Exception){
}finally{//must do
.close
.cancel
.terminate();
}
所有对象都通用的方法
覆盖equals时请遵守通用约定
等价关系
自反性(reflexive) x!=null&&x.equals(x) ==>true
对称性(symmetric) x!=null&&y!=null && x.equals(y) ==>y.equals(x)
传递性(transitive)x,y,z!=null && x.equals(y)&&y.equals(z) ==>x.equals(z)
一致性(consistent)x,y!=null &&x,y not change ==>x.equals(y) always true/false
非null性 (nevernull)x!=null && x.equals(null) ==>false
原理篇(略)
高质量equals诀窍
-
== 代替 equals ,是时候降低成本了
-
使用instanceof操作检查类型
-
参数转换类型(默认进行了instanceof测试)
-
这样写(先比较最有可能不一样的)
Double.compare Arrays.equals field == null? o.field == null : field.equals(o.field); 如果field 和 o.field 是相同对象的引用,更快 field==o.field || (field!=null&&field.equals(o.field))
-
写完equals方法自问自己,是否对称,传递,一致
-
覆盖equals总要覆盖hashCode
-
不要将equals申明的Object替换成其他类型
//没有覆盖Object.equals public boolean equals(MyCleass o){ ... } Override 注解好处就会告诉你,编你不过
覆盖equals时总要覆盖hashCode
为什么非要这样?
1.不这样违反Object.hashCode的通用约定(如果两个对象的equals方法是比较相等的,那么调用这两个对象任意一个hashCode方法必须产生相同的整数结果)
2.不这样导致该类无法结合基于散列的集合正常工作,比如HashMap、HashSet、HashTable
class PhoneNumber{
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(...){
//igore construct
}
@Override public boolean equals(Object O){
//igore equals
}
//Broken - NO HashCode Method
}
Map<PhoneNumber,String> m = new HashMap<PhoneNumber,String>();
m.put(new PhoneNumber(123,456,789),"lizhaoxiong");
==>m.get(new PhoneNumber(110,456,789)) 会返回null ***
==>m.get(new PhoneNumber(123,456,789)) 会返回"lizhaoixong"吗?待test
如果一个对象的属性值发生变化,那么这个对象的hashCode值还相同吗?test
解决方案
@Override
public int hashCode(){
int result = 17;
result = 31*result + areaCode;
result = 31*result + prefix;
result = 31*result + lineNumber;
resutl result;
}
思路:
随便找个int 数;
不同域转换int:
boolean(f?1:0)| byte,short,int (int)f |(int)(f^(f>>>32))
|Float.floatToIntBits(f)|Double.doubleToLongBits(f)+Float.floatToIntBits(f)|
|对象,递归调用equals和hashCode。|数组,Arrays.hashCode,递归每个元素hashCode再组合排列
公式:result = 31*result+c;
1.为什么是31?
移位和减法代替乘法,JVM优化性能(31*i = (i<<5)-i)
2.直接result int的常亮值的坏处?
线性时间、平方级时间?降级了,散列表 ==> 链表
3.怎么使用延迟初始化?
if(result=0)才进行计算,上面代码
4.什么是散列函数?
5.String、Integer、Date的hashCode方法返回的是什么确切值?这样的坏处?
始终要覆盖toString
为什么非要这么做?
1.虽然这不是强制的,但这是规范。
2.如果不覆盖试着比较下 类名@16进制散列 VS 自定义(简洁、信息丰富、易于阅读)
3.toString应该返回值得关注的信息 或 诊断信息
4.toString 要么返回统一规范化格式信息 要么你注释说明好
谨慎地覆盖clone
提供一种无需调用构造器就可以创建对象思路
拷贝的精确含义取决于该对象的类
要把这个搞懂,必须形成一个专题,就目前而言没有多大意义
考虑实现Comparable接口
compareTo 和 equals 区别?
1.compareTo没有在Object声明
2.compareTo 是Comparable接口唯一方法,实现该接口比较对象数组Array.sort(a)
比较返回的是int类型值
3.当比较对象类型不同时直接抛出ClassCastException不进行比较,equals返回false
4.Comparable接口是参数化的,comparable方法是静态类型,不必进行类型转化和类型检查,有问题直接无法编译,如果参数null,直接NullPointException
5.CompareTo方法中域的比较是顺序比较而不是等同性比较。
啥时候用该接口?(collection imp)
由于Java平台的所有值类都实现该接口,当你在编写一个值类需要明显的内在排序关系,比如字母、数值、时间等,坚决考虑实现该接口
public interface Comparable<T>{
int compareTo(T t);
}
自定义Comparator专门用于你自定的排序
int 比较><= ,float和double用Double.compare/Float.compare
多个关键域比较,从最关键的开始,逐步进行到所有重要域,出现非0结果结束返回该结果
public int compareTo(People p){
if(inner < p.inner) return -1;
if(inner > p.inner) return 1;
if(out < p.out) return -1;
if(out > p.out) return 1;
...
return 0;
}
//确定差值不大于INTERGER.MAX_VALUE 2^31 -1
public int compareTo(People p){
int diffInner = inner -p.inner;
if(diffInner!=0) return diffInner;
int diffOut = out - p.out;
if(diffOut!=0) return diffOut;
return 0;
}
类和接口
类和成员的可访问性最小化
尽可能地降低可访问性
在公有类中使用访问方法非公有城
Android 和 java 的公有类的成员变量编写到底是追求简洁还是安全
class Point {
public int x;
public int y;
}
VS
class Point {
private int x;
private int y;
public Point (int x,int y){
this.x = x;
this.y = y;
}
public void setX(int x){
this.x = x;
}
public void setY(int y){
this.y = y;
}
public int getX(){return this.x}
public int getY(){return this.y}
}
//构建时约束条件
public Point (int x, int y){
if(x<0&&y<0){
throw new IllegalArgumentException("x,y" + x + y)
}
this.x = x;
this.y = y;
}
可变性最小化
如何把类变成不可变类?(参照String、BigInteger源码)
-
不对外提供修改对象状态的任何方法
-
保证类不会被扩展,比如声明final类和private构造器(valueof)
-
声明域都是final
-
声明域都是private
-
使用函数对操作数运算并不修改它
public Complex add(Complex c){ return new Complex(x+c*x,y+c*y) }
为什么要使用不可变类?
-
相比可变类的复杂空间和不确定性,不可变稳定可靠
-
不可变对象本质是线程安全的,不要求同步
//为了鼓励多使用,应该这样写 public static final Complex ZERO = new Complex(0,0); public static final Complex ONE = new Complex(1,0); ==>近一步单例化,提高内存占用
-
对象可以被自由共享(剔除拷贝构造器)
-
内部信息共享
-
Building blocks
-
就算是可变类,也要让其可变性最小(比如TimerTask状态只有执行和取消)
-
唯一缺点:创建这种对象代价可能很高,为提高性能使用可变配套类
可变配套类 String 的可变配套类是StringBuilder和淘汰的StringBuffer BigInteger 是BitSet
复合优于继承
Why?
- 跨越包边界的继承是非常危险的,脆弱
- 集成打破了封装性,子类以来超类的实现细节,超类一旦变化,子类遭到破坏
复合/转发?
想要给现有类增加一个功能/特点,在新类中增加一个私有域,它引用现有类的一个实例,使现有类成为新类的一个组件,这样的设计叫做复合(composition)
新类的每个方法都可以调用被包含的现有类的实例对应的方法,并返回它的结果,称为转发(forwarding)
在转发的基础上,返回结果时做定制化处理,这就是装饰/包装(wrapper)
```java Class A { public void methodA(){}; public void methodB(){}; }
Class BWrapper { private A a; public BWrapper(A a){this.a=a};
public void wrapA(){
a.methodA();
}
public void wrapB(){
a.methodB();
}
public void wrapC(){
a.methodA();
a.methodB();
}
}
包装类不适合回调框架(SELF问题)
慎用继承!
用继承前问问自己,两者 is-a的关系吗?每个B确实也是A吗?不能确定就不该扩展A。那么就让B应该包含A的一个私有实例,并且暴露较小、简单的API,A本质不是B的一部分,只是它的实现细节。
(Java 违反这一原则的,栈Stack不是向量vector,属性列表Properties不是散列表Hashtable)
#### 要么为继承而设计提供文档说明,要么禁止继承
为什么写基类的要比写实现的值钱?
好的API文档应该描述给定方法<u>做了什么工作</u>,而不是描述<u>如何做到的</u>
使用此类的方法要注意什么,应该干什么,不能干什么
类必须通过某种形式提供适当的钩子(hook)
编写基于继承的基类后一定要编写子类进行测试
构造器决不能调用可被覆盖的方法
对于并非为了安全子类化而设计的类要禁止子类化,有几种方法
1. 声明类为final
2. 构造器私有化或者包级私有并增加公有静态工厂代替构造器(单例)
3. 利用包装类模式代替继承实现更多功能
#### 接口优于抽象类
**区别**
- 抽象类可包含实现,接口不允许
- 抽象类依赖继承进行子类实现,由于Java单继承原则丧失灵活性
- 接口灵活,扩展性强,并且非层次。高手用继承,多用接口好
**包装类设计模式体现着点**
**多接口模拟多重继承**
**骨架实现**(先设计公有接口,可能抽象的基类简单的公有实现,具体子类实现)
= 简单实现+抽象类
**公有接口设计切记**:多多测试接口,不然一旦上线发行,就不好修改了
#### 接口只用于定义类型
**常量接口模式**
实现细节泄漏API、可能后期无价值、污染代码(反面教程ObjectStreamConstants)
**有固定常量的需求**
1. 枚举类型
2. 不可实例化的工具类(大量引用类名)
3. 静态导入
#### 类层次优于标签类
#### 函数对象表示策略
函数指针
策略模式
元素排序策略:通过传递不同比较器函数,就可以获得各种不同的排列顺序
函数对象:Java没有提供函数指针,使用对象引用实现同样功能,这种对象的方法是对其他对象的操作,
且一个类导出一个方法,它的实例实际等同于指向该方法的指针,这样的实例叫做函数对象。
#### 优先考虑静态成员类
**嵌套类有四种:**
**静态成员类(非内部类)**:
特点— 理解为恰好声明到类里面的普通类,可访问外围类的成员,声明私有也可供外围类内部使用
场景— 公有辅助类,像枚举,像Calculator.Operation.MINUS
**非静态成员类(内部类)**:
特点— 每个实例都包含一个额外指向外围对象的引用,非静态成员类强依赖外围类,并在发行版本中不可由非静转静
场景— Iterator、HashMap的Entry
**匿名类(内部类)**:
特点— 没有名字,不是外围类成员,声明时实例化,不可Instanceof,不可扩展,必须简短
场景— 动态创建函数对象,匿名的Sort方法的Comparator、创建过程对象Runnable、Thread、TimeTask
、静态工程内部
**局部类(内部类)**:
特点— 使用少见
场景— 任何可以声明局部变量的地方
经典问题:静态内部类和非静态内部类的区别?
## 泛型
#### 请不要在新代码中使用原生态类型
泛型的作用 — 在编译时期告知是否插入类型错误的对象,而非再运行时期在报错,泛型检查。
泛型定义 — 声明一个多个类型参数的类或者接口,java1.5版本开始支撑,例如List<String>。
原生态类型 — 不带任何类型参数的泛型类型,List<E> 原生态时List,只是为了和引入泛型之前的遗留代码兼容
```java
//未使用泛型
private final Collection students = ...;
students.add(new Teather());
for(Iterator i = students.iterator;i.hasNext();){
Student s = (Student) i.next();//Throws ClassCastException
}
//使用泛型
private final Collection<Student> students = ...;
students.add(new Teather()); // 提前Error
//for-each 和for
for(Student s: students){}
for(Iterator<Student> i = students.iterator();i.hasNext();){
Student s = i.next();//No cast necessary
}
泛型和原生区别 — 泛型检查
泛型子类化 — List 是原生态类型List的子类型,而不是参数化类型List 的子类型
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(44));
String s = strings.get(0);
//编译通过,但是收到警告
private static void unsafeAdd(List list, Object o){
list.add(o);
}
//无法编译通过,List<String> 不是List<Object>的子类
private static void unsafeAdd(List<Object> list,Object o){
list.add(o);
}
无限制通配符类型 — Set<?> 不确定和不关心实际参数类型
无限制通配符类型Set<?>和原生态类型Set之间有什么区别?
安全的!不是什么都能插入Set<?>,需要声明 Extends ?
什么时候使用原生态(泛型信息可以在运行时被擦除) — 1.类文字 2.和instanceof有关
static void newElements(Set s1,Set s2){
for(Object o1 : s1){
if(s2.contains(o1)){...}
}
}
//会报错
static void newElements(Set<?> s1,Set<?> s2){
for(Object o1 : s1){
if(s2.contains(o1)){...}
}
}
//利用泛型来使用instanceof
if(0 instanceof Set){
Set<?> m = (Set<?>)o;
}
消除非受检警告
非受检警告 — 强制转化、方法调用、普通数组、转化警告
简单 ==>多用泛型,即可消除
难的 ==>@SuppressWarnings("unchecked") //一定要使用确认注释原因
列表优先于数组
数组和泛型的区别?
- 数组是协变且可以具体化的;泛型是不可变且可以被擦除的。
- 数组和泛型不能同时(混合)使用(泛型数组创建错误)
为什么List不是List的超类也不是子类?
因为类型信息已经被擦除了。
为什么第一是列表第二是数组?
错误发现原则— 能在编译时发现就不要放到运行时发现,泛型列表(擦除—编译时检查类型信息,运行时擦除类型信息)优先于数组(运行时检查元素类型约束),这个就像地铁安检一样,你进入地铁前对你进行检查(泛型列表),而当你已经进入地铁后,再来人对你进行检查(数组),危险极大。
类泛型化
如何学习编写泛型呢?
- 把类型参数Object名称变成E
- 将相应的类型参数替换所有Object类型
- 这时会有错误或警告,两种错误和解决方案见如下代码
《具体查看Stack源码》
elements = new E[DEFAULT_INITIAL_CAPACITY];
==>
@suppressWarnings("unChecked")
elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
E result = elements[--size];
==>
E result = (E)elements[--size]
==>
@SuppressWarnings("unCkecked")
E result = (E)elements[--size];
总之需要你确认没有安全问题
class DelayQueue<E extends Delayed> implements BlockingQueue<E>;
E被称之为有限制的类型参数
每个类型都是它的子类型
方法泛型化
public static <E> Set<E> union(Set<E> s1,Set<E> s2){
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
//消除代码chenyu
Map<String,List<String>> ana = new HashMap<String,List<String>>();
==>
Map<String,List<String>> ana = newHashMap();
public static <K,V> HashMap<K,V> newHashMap(){
return new HashMap<K,V>();
}
//递归类型限制Comparable接口
public interface Comarable<T>{
int compareTo(T o);
}
//根据元素的自然顺序计算列表的最大值
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i = list.iterator();
T result = i.next();
while(i.hasNext()){
T t = i.next();
if(t.compareTo(result)>0){
result = t;
}
}
return result;
}
利用限制通配符提升API的灵活性
泛型参数类型特点?
- 不可变
- List 不是List的子类,但自己是自己的子类
提高灵活性的两种方式?
PECS(producer-extends,consummer-super)
合理的选择严格的参数类型、生产者参数、消费者参数,包装类型安全和灵活性
public class Stack<E> { public void push(E e); public E pop(); } + pushAll + popAll //子类无法push进去,由于Integer是Numble的子类,但是Integer插入不了声明为Numble的 public void pushAll(Iterable<E> src){ for(E e: src){ push(e); } } 有限制的通配符类型==> public void pushAll(Iterable<? extends E> src){ for(E e: src){ push(e); } } //如果声明E是Object,由于Object是Numble的父类,但是Numble却无法调用Object的 public void popAll(Collection<E> dst){ while(!isEmpty()){ dst.add(pop()); } } 有限制的通配符类型==> public void popAll(Connection<? super E> dst){ while(!isEmpty()){ dst.add(pop()) } } //升级eg1: static <E> E reduce(List<? extends E> list ,Function<E> f); //升级eg2:返回类型仍然为Set<E> ,不要用通配符类型作为返回类型,除非非常强调灵活性 public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2) //升级eg3: public static <T extends Comparable<? super T>> T max(List<? extends T> list) //注意类型匹配: max(List<? extends T> list){ Iterator<? extends T> i = list.iterator(); } //无限制的类型参数,不能将元素放回刚刚从中取出的列表 public static void swap(List<?> list,int i,int j){ list.set(i,list.set(j,list.get(i))); } ==> 调用swapHelper(List<E> list,int i,int j)
优先考虑类型安全的异构容器
泛型使用场景
集合(Set、Map、List)、单元素容器(ThreadLocal、AtomicReference)
- 特点是每个容器只能有固定树木的类型参数
- 每个键都可以有一个不同的参数化类型
- 异构体泛型化Class String.class 属于Class类型
如何定制键类型呢?
Map<Class,Object> favorites = new HashMap,Object>()
枚举和注解
用emum代替int/String常量
为什么?
- 硬编码的书写错误,编译时不报错,运行时报错
- int 打印效果不佳
- 枚举 = int + 打印 + 任意添加方法和域
- 枚举的本质是单例泛型化,即单元素的枚举
- 枚举中覆盖toString容易打印
- 将不同的行为和枚举常量关联起来
- 不必在意枚举的性能成本,有机会用就果断用
用EnumMap代替索引数组
1.多数组最好转化为Map枚举集合+泛型设计!
2.EnumMap内部使用了通过序数索引的数组,并隐藏了实现细节
3.数组最大的问题是无法保证序数和数组索引之间的关系
4.嵌套的EnumMap关系(142页)EnumMap<..., EnumMap<...>>。
5.使用Enum.ordinal是傻叉
//将所有花园的花根据种类打印出来 //1. 打印的map、key是type、vlaue是花对象集合(定义) //2. 遍历原有集合(type),重写put //3. == 遍历花园得到花对象 Map<Flower.Type, Set<Flower>> flowersByType = new EnumMap<Flower.Type, Set<Flower>>(Flower.Type.class); for(Flower.Type t: Flower.Type.values()) { --values方法可以这样吗?返回的是key对应的对象吗? flowersByType.put(t,new HashSet<Flower>()); } for(Flower f:garden){ flowersByType.get(f.type).add(f); } System.out.println(flowerByType);
用实例域代替序数引用(ordinal)
ordinal方法返回每个常量类型的数字位置(序数)
该方法被设计给EnumSet和EnumMap使用,请完全避免自行使用ordinal方法
// 错误事例 public enum Ensemble{ ONE,TWE,THTREE...; public int numberOfDay(){ return ordinal()+1; } } // 正确事例 public enum Ensemble{ ONE(1),TWE(2),THREE(3)...; private int num; Ensemble(int day){num = day} public int numOfDay(){return num;} }
用EnumSet代替位域
为什么?
- 避免手工操作错误和不雅观代码!
- EnumSet就是用单个long表示,性能不比位域差
// bad code public class Text{ public static final int STYLE_BOLD = 1 << 0; //1 public static final int STYLE_ITALIC = 1 << 1; //2 public void applyStyles(int styles){...} ==> text.applyStyles(STYLE_BOLD | STYLE_ITALIC); } //nice code public class Text{ public enum Style{BOLD,ITALIC... } public void applyStyles(Set<Style> styles){...} ==> text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC)); }
用接口模拟可伸缩的枚举
1.基本数据类型的枚举如何通过扩展枚举实现呢
2.通过Collection<? Extends Operation> 这个有限制通配符类型
3.虽然无法编写可扩展枚举类型,但可以通过编写接口即实现该接口实现进行模拟
注解优先于命名模式
命名模式的3个缺陷:
- 第三方库和框架要求指定名称,结果写错了也不报错,即没执行却给人测试正确假象
- 无法确保他们只用于相应的程序元素上
- 无法和将参数值和程序元素关联
注解类型:
@Retention(RetentionPolicy.RUNTIME) -- 运行时保留 @Target(ElementType.METHOD) -- 作用域,在方法中才合理 public @interface Test {} //Retention(生命范围,源代码,class,runtime) //Documented,Inherited,Target(作用范围,方法,属性,构造方法等)
- 好程序员多用注解,一般程序员没机会定义注解类型的
- 但是最后有机会使用注解考虑,很多IDE和静态分析工具或第三方支持库提供的注解库
- 注解实现的原理是什么以及如何自定义注解和你常使用的注解
坚持使用Override注解
防止由于重载或者或者无意识地覆盖产生的覆盖(有覆盖需求就加上吧)
用标记接口定义类型
到底是使用标记接口还是标记注解?
- 是否是仅仅供类和接口使用还是供任何程序元素使用
- 是否是永远限制标记只用于特殊接口元素
参考:Serializable标记接口(被序列化接口)
方法
如何处理参数和返回值?
如何设计方法签名?
如何为方法编写文档?
--专注可用性、健壮性、灵活性
检查参数的有效性
1.错误发生前检查参数有效性,降低调试工作难度
2.使用@throws标签(tag)标明违反参数值限制会抛出的异常,如ArithmeticException
IllegalArgumentException、IndexOutOfBoundsException、NullPointerException
3.使用断言做有效性检查
4.只要有效性检查有一次失败,那么你为此做的努力便可以连本带利的偿还
/** * @throws NullPointerException if object is null */ public static <T> T requireNonNull(final T object, final String message) { if (object == null) { throw new NullPointerException(message); } return object; }
必要时进行保护性拷贝
不要使用Clone进行保护性拷贝
保护性拷贝是在检查参数的有效性前进行的
Date类事可变的,把它交给如Date.getTime()返回long进行时间表示
谨慎设计方法签名
- 养成良好的命名习惯,方法名称体现品味
- 不要追求便利提供过多方法,简洁,less is more
- 避免过长的参数列表,顺序错误和难以记忆
- 把一个方法拆分成多个方法,比如List的sublist与indexOf和lastIndexOf方法结合
- 创建辅助类(helper class)
- 采用Builder模式
- 参数类型,优先使用接口而非类,比如我们参数申明Map而不是HashMap或TreeMap,List非ArrayList
- 对于boolean参数或者确定固定值的参数,使用枚举类型显得更加优雅
慎用重载
重载是在编译时期决定的 -- 重载方法当方法名称相同,在调用时容易造成调此非此。
如何避免胡乱使用重载 -- 永远不要使用两个具有相同参数数目的重载方法。
在创建多个功能相似的,仅仅传入参数类型不同的方法时,尽可能通过方法命名区分而非参数类型区分。
编写API时,请不要让调用程序员感到混淆的,调用不明确,避免重载的导致难以发现的错误。
覆盖是运行时期的,覆盖更加安全。-- so 避免编译时的类型影响
public static String classify(Collection<?> c){ return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknow Collection" }
慎用可变参数
当我们编写方法,明确了参数的类型但是不确定参数的数量的时候,就用可变参数
可变参数的实现本质sum(int... args) 就是sum(int [] args)
==>先创建一个数组,数组的大小为在调用方法传递的参数数量
//可变参数解决无参数问题 public static int min(int firstArg,int... args){ int min = firstArg; for(int arg: args){ min = arg < min?arg:min; } return min; }
可变参数是为printf而设计,最主要的作用体现在printf和反射机制
Arrays.asList ==> Arrays.toString()
从性能角度,可变参数每次的调用都会导致一次数组的分配和初始化,so 灵活性 VS 性能
返回零长度的数组或者集合而不是null
每次都得为空指针考虑,所以请尽量不要返回null,除非你是有意识的
永远不会分配零长度数组的代码写法
//返回零长度的数组 private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; public Cheese[] getCheeses[]{ return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY); } //返回空集合 return Collections.emptyList(); //具体参考Collections的EmptyList系列的几个内部类
为所有导出的API元素编写文档注射
How to Write Doc Comments
为每个被导出的类、接口、构造器、方法、域声明增加文档注释
方法的文档注释:
- 应当简洁的说明该方法做了什么而非它是如何完成的
- 该方法的能被调用所有前提条件@throws标签或者@param和后置条件
- 可能产生的负作用,比如启动了一个后台线程,那么该方法的线程安全性需要描述
- 标签齐全和说明规范@param @return(名称短语,参数或返回值) @throws(if(如果),名称短语(异常条件))
,并且由标签开头的文字不用句点结束
- @code 标签{@code index<0} 多行代码
{@code index>=0}
- @literal(字符)标签 {@literal |x+y|<|x|+|y|}
- 文档可读性应当优于源代码的可读性
概要描述(Summary Description)-- 文档注释的第一句话,对你写的API一句话描述体现你的总结能力了
更多说明:
- 当注解、接口、枚举这种代码量精简到爆的时候,文档注释要多到爆。
- 类的线程安全性和可序列化性是需要被描述的
- 接口或者基类的文档注释优先级高于超类和实现类
- HTML有效性检查器运行由Javadoc产生的HTML文件
- 使用文档注释应当强制性、一致风格、统一标准
通用程序设计
讨论局部变量的处理、控制结构、类库的用法、各种数据类型的用法以及reflection 和 native method的用法
及优化和命名惯例
局部变量的作用域最小化
- 在第一次使用它的地方声明,而不是等到用到该变量的时候,如果变量是在它目标使用区域之前或之后使用的话,后果可能很严重哦
- 每个变量的声明都应该包含一个初始化表达式 (try catch除外)
- 如果循环终止不再需要循环变量内容,for循环优于while(重用元素错误和增强可读性及更简短)
- 把局部变量放到方法里面或者代码块里面是得作用域最小化,有效避免被另一个操作
for-each循环优于for循环
Why?
遍历集合和遍历数组for循环是比while循环更好,但是由于迭代器和索引变量的存在很容易出错
使用for-each循环完全隐藏迭代器和索引变量,适合集合和数组
性能上稍有优势,它对数组的索引边界值只是计算一次
嵌套遍历时,内外数量不一致会抛出NoSuchElementException,嵌套循环可能需要在第一层循环内把遍历的值记录下来,而for-each完美的处理了嵌套的问题
advise!
如果要编写类型表示一组元素,即使不让其实现Collection,也要让其实现Iterable,这样就可以使用for-each
无法使用for-each的三种情况!
过滤 -- 要删除指定元素调用remove方法(修改)
替换 — 遍历列表或数组时,要取代替换元素时 (修改)
平行迭代
了解使用类库
Random三个缺点 ==> 伪随机数生成器 Random.nextInt(int) ,So
- 类似的标准类库经过专家认证和无数次的验证和使用
- 不要浪费时间为那些和工作不相关的问题提供特别解决方案,focus your app,not it
- 让自己的代码融入主流,更易读,更易维护,被大多数开发人员重用
advise!
- 很多程序员不使用类库原因是不知道,当然Java类库很庞大,但也要做到熟悉java.lang java.util java.io及每次重要发行版本的新特性,然后在需要的时候进行查阅
- 重点关注下Collections Framework(集合框架)
- 重点关注下1.5版本的java.util.concurrent包的并发使用工具,简化多线程的编程任务
- 如果你做的事十分常见,请不要重复造轮子
如果需要精确的答案避免使用float和double
一种办法是复杂的使用BigDecimal,不方便较慢,要自己使用十进制小数点(货币)
还有一种办法,是使用int(简单)或者long(复杂)
基本类型优于引用类型
Java分为基本数据类型和引用类型,每个基本数据类型都有一个引用类型
区别?
- 基本数据类型只有值
- 引用类型多个null
- 基本类型更加节省时间和空间
对于引用数据类型使用==操作符总是错误的
请把能声明成基本数据类型变量,避免反复的装箱和拆箱,导致性能下降
什么时候使用引用类型呢?
- 集合中的元素、键和值,Java不允许ThreadLocal类型
- 反射的方法调用是必须使用装箱基本类型
如果其他类型更合适,尽量避免使用字符串
不适合使用字符串的场景:
- 它原本是int、float、BigInteger类型、是或否的boolean类型,是什么就是什么!
- 不适合代替枚举类型
- 不适合代替聚集类型(String compoundKey = className + "#" +i.next()),简单编写一个私有静态成员类吧
- 不适合设计成不可伪造唯一的键 -- capability,参见ThreadLocal的get()方法,发现key已经隐藏
当心字符串连接的性能
当是简单输出或者显示使用字符串连接 +
但当拼接的量级别变大,或者使用for循环进行拼接请使用StringBuilder代替String
多使用StringBuilder的append方法
通过接口引用对象
更一般的讲,如果有合适的接口类型存在,那么参数、返回值、变量和域都应该使用接口类型声明
养成接口作为类型的好习惯吧,使你的程序更灵活吧
Vector<Subscriber> subscribers = new Vector<Subscriber>(); ==> good code : List<Subscriber> subscribers = new Vector<Subscriber>(); 比如ThreadLocal类的之前的HashMap 要换成 IdentityHashMap那不是一句话的事,而且不用担心影响
如果对象的类是基于类的框架(abstract class),就应当用相关的基类来引用这个对象,比如TimerTask
接口优先于反射机制
反射提供了通过程序访问类的成员名称、域类型、方法等信息的能力
反射的设计初衷:
为了基于组件的应用创建工具设计,这类工具需要装载类。普通应用程序在运行时不应该以反射方式访问对象。目前反射机制使用场景:浏览器、对象监视器、代码分析工具、解释型的内嵌式系统、RPC系统。现实app中当编译时无法获取到的类或者过时方法等
反射的缺点:
- 丧失编译时的类型检查的好处
- 反射访问的代码非常难以阅读和笨拙和冗长
- 性能损失(受到机器影响,2到50倍)
使用注意:当你编写程序与编译时的未知类一起工作时
谨慎使用本地方法
本地方法的三种用途:访问特定平台的机制,注册表和文件锁,访问遗留代码和数据能力,提供系统性能
使用本地方法提高性能时不值得提倡的,原来是可行的现在随着JVM的块和Java平台完善,比如BigInteger
本地语言不是安全的,可能需要编写胶合代码,并且单调乏味和难以阅读
本地代码中的一个Bug可能让你痛不欲生
谨慎地进行优化
名人名言:
很多计算机上的过失都被归咎于效率(没有必要达到的效率)
计较效率上的小小的得失,不成熟的优化才是一切问题的根源
在优化方面我们应该遵守两条规则:第一,不要进行优化 第二,没有绝对清晰的优化方案之前,请不要进行优化
advise!
API的设计对性能的影响是非常实际的,使用哪些性能优化过的API特别重要,保证接口的灵活性也特别重要
不要费力的编写快速的程序,尽力编写好的程序,速度自然而然就随之而来
借助性能方面的工具是不错的手段
遵守普遍接受的命名惯例
字面惯例的例子
标示符类型 例子 包 com.xiaoyu.util, org.xiaoyu.dao.impl 类或者接口 HttpServlet, AsyncTask(单词首字母大写) 方法或者域 toString, equal, isUpperCase(首单词首字母小写,之后单词首字母大写) 常量域 IP_ADDR(全部大写,单词之间加下划线) 局部变量 stuNumber,mString(与方法命名类似) 类型参数 T,E,V,K等等 语法命名惯例
标示符类型 例子 类 名称或名称短语,Timer,BufferedWriter,ChessPiece 接口 Collection,Comparator,Runnable,Iterable,Accessible 注解 BindingAnnotation,Inject,ImplementedBy,Singleton 方法或者域 动词或动词短语,append或drawImage,返回boolean is开头 isEmpty,isEnabled方法返回非boolean对象的函数或属性:speed(),color(),getTime(),getA+setA转换对象类型:toType,toString,toArray返回view:asList返回和调用对象同值基本类型方法:typeValue、intValue静态工厂的常用名称:valueOf、getInstance、newInstance、getType、newType 异常
发挥异常优点,提高程序的可读性、可靠性和可维护性,介绍下异常的指导原则
只针对异常的情况才使用异常
异常的错误作用:
- 模糊代码的意图
- 降低了它的性能
- 掩盖Bug
- 增加调试过程的复杂性
so,异常应该只用于异常的情况,永远不应该用于正常的控制流
“状态测试方法” “可识别的返回值”
对可恢复情况使用受检异常,对编程错误使用运行时异常
Java 程序设计提供三种可抛出结构(throwable)
- 受检异常(checked exception)
- 运行时异常(runtime exception)表明编程错误
- 错误(error)
永远不要定义Exception的子类,只会困扰API的用户
避免不必要的使用受检异常
catch 块总是具有断言失败的特征
优先使用标准的异常
标准意味着可被重用
可被重用异常排行榜
- IllegalArgumentExcetion,当传递的参数不合适触发--非法参数
- IllegalStateExcetion,当对象未被正确初始化,调用者企图调用该对象触发--非法状态
- IndexOutOfBoundsException,参数导致越界—特殊的非法参数
- ConcurrentModificationException,被并发修改
- UnsupportedOperationException,对象不支持该操作
- ArithmeticException、NumberFormatException
抛出和抽象相对应异常
最让人困惑的异常莫过于抛出的异常和执行任务没有明显的联系,往往由底层抽象抛出的异常?!
如何避免?— 异常转译
public E get(int index) { try { return listIterator(index).next(); } catch (NoSuchElementException exc) { throw new IndexOutOfBoundsException("Index: "+index); //异常链 try{ ... //Use lower-level bidding }catch(LowerLevelException cause){ throw new HigherLevelException(cause); }
当然也不要滥用,可以给低层传递参数前检查高层方法的参数有效性,从而避免底层方法抛出异常
而如果无法避免底层异常,让高层绕开这些异常,隔离!并记录下来
每个方法抛出的异常都要有文档
在细节消息中包含能捕获失败的信息
如果失败的情形不容易重现,想要通过系统日志分析会非常困难
so,编写代码时把细节打印出来
努力使失败保持原子性
- 也就是在方法的一开始就抛出异常,避免执行下面的可能会修改的代码
- 在对象状态被修改之前发生异常
- 使用恢复代码(recovery code),使对象回滚到操作开始之前的状态
- 在临时拷贝上执行操作,比如Collections.sort,即使排序失败也保证列表原样
当然也不要刻意追求,错就是错了,另外API文档应当清楚的指名对象将会处于什么状态
不要忽略异常
不考虑后果直接try catch是极其不负责任的行为,起码你解决不了,输出些日志出来也行
并发
同步访问共享的可变数据
Synchronized 同一时刻,只有一个线程可以执行某一个方法或者代码块
互斥的理解:当一个对象呗一个线程修改时,可以阻止另一个线程观察到对象内部不一致的状态(状态一致性)
boolean域的读写操作是原子的,让第一个线程轮询(开始false),通过第二个线程改变(false)表明第一个线程终止自己了
i++不是原子操作
不共享可变数据或者共享不可变数据以及非要共享可变数据就必须执行同步
public class StopThread{ private static boolean stopRequested; private static synchronized void requestStop(){ stopRequested = true; } private static synchronized boolean stopRequested(){ return stopRequested; } main(){ Thread backgroudThread = new Thread( new Runnable(){ public void run(){ int i = 0; while(!stopRequested()) i++; } } ); backgroundThread.start(); TimeUnit.SECOND.sleep(1); requestStop(); } } or ==> private static volatile boolean stopRequested;//就不需要写同步方法了
避免过度同步
过度同步可能会导致性能降低、死锁、不确定行为
死锁原因:它企图锁定某某,但它无法获得该锁
分析StringBuffer内部同步问题
分拆锁、分离锁、非阻塞并发控制
executor和task优先于线程
//代替new thread 的Executor Framework 三步曲 ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(runnable); executor.shutdown(); //轻载的服务器,够用不需要设置啥 Executors.newCachedThreadPool //大负载产品,固定线程数目的线程池 Executors.newFixedThreadPool //最大限度的使用 ThreadPoolExecutor
Executor Famework 所做的工作就是执行
Collections Framework 所做的工作就是聚集
使用ScheduledThreadPoolExecutor代替Timer,更灵活。Timer只用一个线程,面对长期任务影响准确性,一旦线程抛出未被捕获的异常,timer就会停止。而executor支持多个线程,并能够优雅的从抛出的未受检异常任务中恢复。
并发工具优先于wait和notify
应该用更高级的并发工具来代替 wait和notify(手动太困难)
并发java.util.concurrent:Executor Framework、Concurrent Collection、Synchronizer
并发集合:ConcurrentMap、BlockingQueue(生产者和消费者队列)
同步器:CountDownLatch/CyclicBarrier—倒计时锁存器、Semaphore
System.nanoTime更加准确更加精确,它不受系统的时钟的调整影响(System.currentTimeMills)
线程安全性的文档化
线程安全的级别:
- 不可变 -- String、Long、BigInteger
- 无条件线程安全 -- 已经内部处理无需关心,Random、ConcurrentHashMap
- 有条件线程安全 -- Collections.synchronized包装返回的集合,迭代器需要外部同步
- 非线程安全 -- ArrayList、HashMap
- 线程对立 -- 不考虑
私有锁,把锁对象封装在它所同步的对象中(private + final)适用于无条件线程安全
总之,有条件的线程安全必须写文档指名了“哪个方法调用序列需要外部同步,并且获得哪把锁”
慎用延迟初始化
双层检查 :减少延迟初始化的性能开销,再加上volatile
不要依赖线程调度器
不健壮、不可移植性
避免使用线程组
把堆栈轨迹定向一个特定域应用程序的日志中Thread.setUncaughtExceptionHandler
序列化
将一个对象编码成一个字节流,即序列化;相反,即为反序列化。
序列化代理模式
谨慎地实现Serializable接口
一个类被序列化的直接开销非常低,但是后期的长期开销确是实实在在的,一旦一个类被发布,大大降低这个类实现的灵活性。
旧版本来序列化一个类,在用新版本反序列化,往往会导致程序的失败。
UID — 序列化版本serialVersionUID,如果你没有声明显式的序列化UID,那么一旦类发生改变,那么自动生成的UID也会发生变化,兼容性遭到破坏,运行时导致InvalidClassException异常
反序列化作为隐藏的构造器,具备和其他构造器相同特点,很容易出现Bug和安全漏洞
随着类的新版本发布,相关测试负担加重(确保序列化和反序列化过程成功)
哪些类需要序列化(Date BigInteger、Throwable—异常从服务队传客户端、Component—GUI的发送保持恢复、HttpServlet—回话状态可缓存、存在目的为了融于这种类型的框架)
哪些类不应该实现序列化(thread pool、为继承设计的类+用户接口—为扩展存在的,不然会背负沉重负担)
ReadObjectNoData
内部类不因该是实现序列化
考虑使用自定义的序列化形式
transient表示的变量默认不序列化(瞬时),writeObject和readObject表示具体的序列化形式
@serial标签
StringList
保护性地编写readObject方法
readObject方法相当于公有构造器,如同其他构造器一样需要检查参数有效性,必要时对参数进行保护性拷贝
对于实例控制,枚举类型优先于readResolve
一个Singleton如果实现Serializable的话,就不再是单例了,除非在readResolve做文章
考虑用序列化代替序列化实例
当不能在客户端扩展类编写readObject或者writeObject时候,考虑使用序列化代理模式
读后感 ?
了解规则,看到背后,打破规则,创造新规则
转载自:https://juejin.cn/post/6844903928992104456