title: Effective Java 第二版
date: 2019/06/13 11:07
二、创建和销毁对象
1、静态工厂方法代替构造器
好处:
- 具有名字,便于理解这个方法返回的对象有什么特殊含义
- 不用每次都创建一个对象
- 可以返回任意子类型的对象(配合继承)
可以返回不是public的类的对象,例如:Arrays
可以只修改静态方法的实现,提升性能,构造就需要修改原有的业务代码
- 简化构造参数列表(部分参数有默认值)
缺点:
- 如果没有构造器的话,那么这个类就不能继承(可以使用复合代替继承)。
- 可能会与其它静态方法混淆,因为并不知道这个是用来创建对象的。
2、如果构造器参数过多要考虑使用建造者模式
静态工厂方法和构造器创建对象有一个局限性,就是当构造参数过多的时候,客户端代码不好写且难以阅读。(因为有些参数是可以不传的,但是还是占着构造的参数列表中的位置)
如果javabean模式解决(采用空参构造,然后set参数进去)好处是代码可读性,但也有缺陷,因为它阻止了这个对象不可变的可能,所以还有保证他线程安全。
不可变对象的好处?
- 线程安全,不需要担心数据会被其它线程修改
- 可以很好的用作Map键值和Set元素
不可变对象最大的缺点就是创建对象的开销,因为每一步操作都会产生一个新的对象。
Integer a = 0; a++ 改变的是a这个对象,而不是a内部的值
建造者模式:
保证了aaa对象的不可变性
public class AAA {
private final String a;
private final String b;
private AAA(Builder builder) {
this.a = builder.a;
this.b = builder.b;
}
public static class Builder {
private final String a;
private String b;
public Builder(String a) {
this.a = a;
}
public Builder b(String b) {
this.b = b;
return this;
}
public AAA build() {
return new AAA(this);
}
}
public static void main(String[] args) {
Builder builder = new Builder("a");
AAA aaa = builder.b("b").build();
}
}
4、将构造器私有,防止客户端代码将不可new的类new了
5、避免创建不必要的对象
1、不要这样写
for(...) {
String a = new String("xxx");
}
// 这样一次循环创建了2个对象
2、尽量选用小int
3、重用不可变对象
例:不要用Boolean a = new Boolean(),用Boolean a = Boolean.valueof("true")(有缓存)
每次调用都要创建一次对象的那种,如果对象不变的话,可以声明为静态的,在类加载的时候加载(亦可以延迟加载)。
6、消除过期的对象引用
1、自己管理内存,忘记释放
public class AAA {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_NUMBER = 16;
public AAA() {
elements = new Object[DEFAULT_NUMBER];
}
public void push(Object o) {
this.ensureCapacity();
elements[size++] = o;
}
/**
* 当pop的时候,没有将该对象引用清除,导致内存泄漏
*
* @return
*/
public Object pop() {
return elements[size--];
}
/**
* 正确版
*
* @return
*/
public Object pop() {
Object o = elements[size];
elements[size] = null;
size--;
return o;
}
/**
* 检查数组容量
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
2、缓存
可以使用WeakHashMap代表缓存
三、Object类中的方法
8、equals方法
- 自反性:对象必须等于自身
- 对称性:a.equals(b) <==> b.equals(a) 充分必要条件
- 传递性:a.equals(b), b.equals(c) => a.equals(c)
// equals遇上继承
public class BBB extends AAA {
private String b;
@Override
public boolean equals(Object o) {
if (this == o) return true;
// 如果输入对象和该对象不匹配,返回false【这一步就是为了保证 传递性(害怕不同类型的对象有不同的验证方法,从而违反传递性)】
if (o == null || getClass() != o.getClass()) return false;
// 调用父类的方法,如果不相同,那么返回false
if (!super.equals(o)) return false;
BBB bbb = (BBB) o;
return Objects.equals(b, bbb.b);
}
使用
getClass() != o.getClass()
方式代替instance of
有一定的局限性,因为instance of
可以判断是否与属于自己的类型和自己的子类型,而getClass() != o.getClass()
只能判断是否与自己的类型相同但是为啥java7默认用的就是
getClass() != o.getClass()
呢?源于java.util.Date和java.sql.Timestamp两者的对象违反对称性。
Date date = new Date();
Timestamp t1 = new Timestamp(date.getTime());
System.out.println("Date equals Timestamp ? : " + date.equals(t1));// true
System.out.println("Timestamp equals Date ? : " + t1.equals(date);// false
// Date的equals代码,由于使用了instance of,导致了 date.equals(t1) ==> true
public boolean equals(Object obj) {
return obj instanceof Date && getTime() == ((Date) obj).getTime();
}
// Timestamp的equals代码
public boolean equals(java.lang.Object ts) {
if (ts instanceof Timestamp) {
return this.equals((Timestamp)ts);
} else {
// 不属于时间戳类型,直接返回false,导致了 t1.equals(date) ==> false
return false;
}
}
public boolean equals(Timestamp ts) {
// 先调用Date类的equals方法,然后再比较纳秒
if (super.equals(ts)) {
if (nanos == ts.nanos) {
return true;
} else {
return false;
}
} else {
return false;
}
}
如果我们帮他改一改,让它满足对称性:
// Timestamp的equals代码【改后】
public boolean equals(java.lang.Object ts) {
if (ts instanceof Timestamp) {
return this.equals((Timestamp)ts);
} else if (ts instanceof Date) {
// 如果属于Date类型,直接调用父类equals方法。
return super(ts);
}
else {
// 不属于时间戳类型,直接返回false,导致了 t1.equals(date) ==> false
return false;
}
}
然后就会发现,他不满足传递性:
Date date = new Date();
Timestamp t1 = new Timestamp(date.getTime());
Timestamp t2 = new Timestamp(date.getTime());
t2.setNanos(t2.getNanos() + 1);// 给时间戳增加一纳秒
t1.equals(date); // true
date.equals(t2); // true
t1.equals(t2); // false
总结:由于Date再equals方法中使用了
instance of
,导致了它与子类之间没有了【对称性】和【传递性】;而且我们也不能改 Timestamp 的equals方法,那样还破坏了Timestamp类的传递性。所以这就是为什么要使用getClass() != o.getClass()
方式的原因。
- 一致性:如果不可变对象相等,那么就必须始终保持相等
- 所有对象不能equals null
基本数据类型除了double、float类型,使用==比较:
public class BBB extends AAA {
private String b;
private double c;
private int d;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
BBB bbb = (BBB) o;
return Double.compare(bbb.c, c) == 0 &&
d == bbb.d &&
Objects.equals(b, bbb.b);
}
性能:
- 先比较最有可能不一致的域
- 不要比较冗余域(例如:使用姓名能推算出来的字段)
9、覆盖equals方法的类必须也覆盖hashcode方法
Object规范:
如果两个对象根据equals比较时相等的,那么他们的hashcode方法产生的结果也必须是相等的。
当可变对象插入HashSet中,如果对象中的值改变,不会修改他在HashSet中的位置(可能导致内存泄漏):
public static void main(String[] args) {
HashSet<BBB> bbbs = new HashSet<>();
BBB bbb = new BBB();
bbb.setB("1");
bbbs.add(bbb);
bbb.setB("2");
bbbs.remove(bbb);
bbbs.forEach(System.out::println);
}
>> BBB{b='2'}
原因:remove是根据hashcode删除的。
public static void main(String[] args) {
HashSet<BBB> bbbs = new HashSet<>();
BBB bbb = new BBB();
bbb.setB("1");
bbbs.add(bbb);
bbb.setB("2");
bbbs.add(bbb);
bbbs.forEach(System.out::println);
}
>> BBB{b='2'}
BBB{b='2'}
原因:插入时,先比较hashcode,再比较equals;2个对象的hashcode已经不同了
12、Comparable接口
public interface Comparable<T> {
/**
* 将当前对象和指定对象o相比,如果大于o返回一个正整数,等于o返回0,小于o返回负整数
*/
public int compareTo(T o);
}
compare约定与equals约定差不多;强烈建议:(o1.compareTo(o2) == 0) == o1.equals(o2)
,否则可能会破坏依赖于比较关系的类(TreeSet、TreeMap)
// 此处的范性表示当前类的类型和TreeSetTest类型比较
@Data
@ToString
@AllArgsConstructor
public class TreeSetTest implements Comparable<TreeSetTest> {
private int a;
private int b;
@Override
public int compareTo(TreeSetTest o) {
return Integer.compare(this.getA(), o.getA());
}
public static void main(String[] args) {
TreeSetTest o1 = new TreeSetTest(1, 2);
TreeSetTest o2 = new TreeSetTest(1, 3);
TreeSetTest o3 = new TreeSetTest(2, 2);
TreeSet<TreeSetTest> treeSetTests = new TreeSet<>();
treeSetTests.add(o1);
treeSetTests.add(o2);
treeSetTests.add(o3);
treeSetTests.forEach(System.out::println);
}
}
>> TreeSetTest(a=1, b=2)
TreeSetTest(a=2, b=2)
缺少了一个。
其它:如果不实现Comparable接口,会抛出:TreeSetTest cannot be cast to java.lang.Comparable异常
由于compare约定中只要求了返回值的符号,而没有约定返回值的大小,所以如果比较的值是int型,可以使用return o1.a - o2.a
这种方式提高速度。但是要注意,两者不能相差超过Integer.MAX_VALUE
四、类和接口
13、将类和成员的可访问性最小化
封装:模块隐藏所有实现细节,模块之间通过他们的API通信
好处:
- 解除模块之间的耦合关系;可以并行开发
- 提高模块的可重用性
Java通过访问控制技术实现:
1)对于类和接口上面的修饰符,只有两种访问级别:包级私有(没有修饰符)和公有的(public修饰)。
- 包级私有:以后的发行版可以对其进行修改、替换、删除。
- 公有:永远支持他
如果一个包级私有的类有且仅有一个类使用,可以考虑将该类变成私有内部类。
内部类的访问修饰符:
- public:所有地方都可以通过
new A().new B()
的方式访问到。 - private:只有类本身可以使用。
- protected:和不写修饰符的含义一样,为包级私有;子类无法继承内部类。
2)对于成员(方法、变量/常量、内部类)有四中访问级别。
- 子类对成员的访问修饰,不得低于父类的。
- 变量或常量指向的是一个可变对象的引用时,它的修饰符不能是公有的。否则就放弃了对他们的值进行限制的能力。
上面的第二条对于静态的同样使用,常量类中的常量除外,要尽量不要让常量中引用的是可变对象。
如果常量中存的是List、Map,可以使用枚举或不可变类的封装。
// 不可变List
ImmutableList.of(".pdf", ".doc", ".docx", ".txt", ".ini");
// 不可变Map
ImmutableMap.Builder<Integer, String> reviewPointShapeMapBuilder = ImmutableMap.builder();
reviewPointShapeMapBuilder.put(200201, "differentExtent");
reviewPointShapeMap = reviewPointShapeMapBuilder.build();
Builder的实现,上面有讲
当然,还可以将List变成私有的,然后使用get方法返回这个List的一个clone(return list.clone()
)
14、公有类的变量访问使用get/set方法
15、不可变类设计
规定:
- 保证类不被继承(final修饰或构造器私有),防止恶意子类假装对象的状态已经改变
- 所有的参数都使用final修饰,防止修改
- 所有的参数都是私有的,如果参数指向的是可变对象,有可能被恶意修改。
优点:
- 整个生命周期不会发生变化
- 线程安全,可以随意共享对象。因为根本无法修改这个变量
- 内部的信息也可以共享:
final int[] mag;
// 取反,mag这个数组被两个BigInteger对象公用了
public BigInteger negate() {
return new BigInteger(this.mag, -this.signum);
}
缺点:
每做一次修改就是一个新的值,造成性能开销。
为了解决这个问题,javaer为BigInteger类提供了一个包级私有的可变配套类MutableBigInteger加快计算。
其它:
可以将常用的对象缓存起来,提供一些静态工厂直接获取对象。
hashcode方法可以在第一次调用的时候计算出来,然后缓存起来。
在写一个类的时候,要尽可能的限制它的可变性,除非必须可变,这样可以降低出错。
16、复合优先于继承
谨慎继承不是自己的类
public class InstrumentedHashSet<E> extends HashSet<E> {
// 统计这个set被插入的元素个数
private int addCount = 0;
@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 static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 4, 3);
InstrumentedHashSet<Integer> set = new InstrumentedHashSet<>();
set.addAll(integers);
System.out.println(set.addCount);
}
}
>> 8
由于这个类不是我们写的,我们并不知道父类的addAll方法调用了add方法;所以导致结果不对。
当然我们可以根据测试的结果修改我们的代码,但是,如果有一天父类修改了addAll方法的实现,他并不会管我们这个实现类,从而再次导致错误。
还有,如果我们有一个需求:要在add元素到set中之前做判断;我们可以通过重写add、addAll方法来实现,如果日后父类又多加了一个addxxx的方法,如果客户端通过这个方法添加元素,就不受我们的控制,从而导致错误。
上面的问题都是由于重写导致的,其实,如果我们继承之后新增方法也会导致问题,如果我们新增的方法和父类后来添加的方法,**方法签名一样,返回值类型不同**,会导致子类编译不通过。
所以如果要继承不是自己的类,优先考虑使用复合。
复合的方式有一个缺点,就是不能用作回调,想想@Async为啥this.xxx()调用不好使。
只有当子类真的是父类的子类型(is-a关系),才应该使用继承。
18、使用接口优先于抽象类
骨架类:
例如AbstractList、AbstractSet、AbstractMap等都是骨架类
// 其中List继承了Collection接口,AbstractCollection实现了Collection接口
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
实现了一个接口(规范相同行为),继承了一个抽象类(实现相同的部分)
19、不要将接口用作储存常量的地方
接口是用来被实现的,而不是让你存放常量的,而且如果使用接口存放常量,客户端类编译过程中会将常量直接编译进去,从而导致,修改了常量(替换了接口的class文件)但是实际执行还是原来的常量。
22、内部类
内部类有四种:
1、静态内部类
可以把它看作是一个普通的类,只是被声明到了一个类的内部。
通常作为公有的辅助类,例如:Calc.Optional.PLUS 表示计算器类的加法操作
2、非静态内部类
主要目的:为外围类提供服务。例如Map.Entry<K,V>
类
非静态内部类的实例隐含着一个与外围实例之间的关联。这个关联关系消耗了空间和构造的时间开销。
非静态内部类可以使用外围类的所有成员和方法。
public class NestTest {
private String a;
public class A {
public void test() {
String a = NestTest.this.a;
}
}
}
非静态内部类中不能有static方法。
由于它的每一个实例都有一个额外的引用指向外围类的实例,所以如果可以不用到外围类的实例信息(非静态成员、方法),那么就要考虑使用静态内部类了。
3、匿名内部类
4、局部类
五、泛型
25、列表优先于数组
// 合法,运行时会报错(运行时对类型检查)
Object[] objects = new Long[3];
objects[1] = "xxx";
// 不合法,编译时报错(编译时对类型进行检查)
ArrayList<Long> longs = new ArrayList<>();
longs.add("xxx")
使用列表可以在编译的时候就提醒我们报错。
注意:Java不能创建泛型数组(new List<E>[]、new E[])
这是Java初期的设计缺陷导致的,由于初期Java不支持泛型,为了不修改JVM代码,所以将Java中的泛型在运行时进行擦除。
而Java对数组中的类型在运行时检查,而泛型在运行时已经被擦除掉了。
假设Java支持泛型数组
// 创建一个List<String>[]
List<String>[] lists = new ArrayList<String>[3];
// Object[]是List<String>[]的父类型,所以可以直接赋值
Object[] objects = lists;
// 由于对数组中的东西是运行时检查的,但是运行时List<String>已经被擦除成List了,Arrays.asList(123)也被擦除成List,所以不会报错
objects[0] = Arrays.asList(123);
// 当获取数组中集合中的字符串的时候,由于编译器会自动在取出的时候加上强转,所以会抛出类型转换失败异常。
System.out.println(lists[0].get(0));
为了防止这种情况的发生,Java直接产生编译时错误
从而会导致,可变参数方法使用泛型会提示警告。
public static void main(String[] args) {
func(Collections.singletonList(231));
}
// 禁止警告
@SafeVarargs
private static void func(List<Integer>... args) {
}
27、使用泛型方法
泛型方法的泛型可以通过返回值的泛型和入参的泛型来确定。
28、使用有限制的通配符
List<String>
不是List<Object>
的子类型,所以无法强转
interface Stack<E> {
void push(E e);
void pushAll(Iterable<E> es);
E pop();
}
class StackImpl<E> implements Stack<E> {
@Override
public void push(E e) {
}
@Override
public void pushAll(Iterable<E> es) {
es.forEach(this::push);
}
@Override
public E pop() {
return null;
}
}
public static void main(String[] args) {
Stack<Number> stack = new StackImpl<>();
stack.push(123);
// 这句话会报编译时错误,类型不正确,因为Iterable<Integer>不是Iterable<Number>的子类
stack.pushAll(Arrays.asList(1, 2, 3).iterator());
}
解决办法,可以将上面代码中的所有<E>
改成<? extends E>
假如新增一个popAll方法,将所有元素放入给定的集合中:
@Override
public void popAll(Collection<E> es) {
while (栈不为空) {
es.add(this.pop());
}
}
// 这样也会报编译时错误,因为List<Number>不是List<Object>的子类型
public static void main(String[] args) {
Stack<Number> stack = new StackImpl<>();
List<Object> objects = new ArrayList<>();
stack.popAll(objects);
}
解决办法,可以将代码中的所有<E>
改成<? super E>
为啥? extends Fruit
不能add元素
public class ListTest {
// 这里传入的List是【Fruit的子类】集合,但是编译器并不知道?代表的是哪个子类,所以无法add元素
public void func2 (List<? extends Fruit> list) {
// list.add(new Fruit()); 报错
Fruit fruit = list.get(0);
}
// 这里传入的List是【Apple的超类】集合,所以编译器知道Apple和它的子类都可以被添加到这个集合中
public void func3(List<? super Apple> list) {
list.add(new Apple());
list.add(new RedFuShi());
// list.add(new Fruit()); 报错
// list.add(new Orange()); 报错
// 由于编译器并不知道List中Apple的超类到底是谁,所以使用Object声明
Object object = list.get(0);
}
}
class Fruit {}
class Apple extends Fruit {}
class RedFuShi extends Apple {}
class Orange extends Fruit {}
6、枚举和注解
30、枚举
枚举其实就是用来存放一组固定常量的(例如:季节就可以是一个枚举,因为它是一种东西,而且里面包含的常量也是固定的)。
枚举的高级用法:
/**
* 计算器的加减乘除枚举
*/
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
public double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x - y;
case TIMES:
return x * y;
case DIVIDE:
return x / y;
default:
throw new RuntimeException();
}
}
}
上面的代码有一个问题,那就是如果最后不向外抛出异常,就没办法编译通过,但从逻辑上我们可以知道,上面的代码永远不会抛出异常。所以可以使用下面的方式优化:
public enum Operation {
PLUS("+") {
@Override
double apply(double x, double y) {
return x + y;
}
}, MINUS("-") {
@Override
double apply(double x, double y) {
return x - y;
}
}, TIMES("*") {
@Override
double apply(double x, double y) {
return x * y;
}
}, DIVIDE("/") {
@Override
double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
// 最好把这个抽象方法抽取出来作为一个接口
// 实现接口的方法,可以由枚举类自己实现,也可以让枚举常量(对象)自己实现
abstract double apply(double x, double y);
@Override
public String toString() {
return symbol;
}
}
// 每个枚举天生就带有一个valueOf方法,通过枚举常量值来获取枚举对象的。
public static void main(String[] args) {
Operation plus = Operation.valueOf("PLUS");
System.out.println("plus = " + plus);
}
>> plus = +
如果有多个枚举常量共享想通过的行为,可以考虑使用策略枚举
所谓策略枚举就是使用了策略模式的枚举,例如要获取每天的工资:
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
private static final int HOURS_PER_SHIFT = 8;//正常工作时数
/**
* 工资计算
*
* @param hoursWorked 工作时间(小时)
* @param payRate 每小时工资
* @return
*/
double pay(double hoursWorked, double payRate) {
//基本工资,注这里使用的是double,真实应用中请不要使用
double basePay = hoursWorked * payRate;
//加班工资,为正常工资的1.5倍
double overtimePay;
switch (this) {
case SATURDAY:
case SUNDAY://双休日加班工资
overtimePay = hoursWorked * payRate / 2;
break;
default: //正常工作日加班工资
overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
break;
}
return basePay + overtimePay;//基本工资+加班工资
}
}
上面的代码没错也很简单,但是如果新加一天,然后忘了修改下面的pay方法,就会导致新加的那天可能是要加班工资,但是还按照平时工资来计算的(虽然一般不可能发生),策略枚举就是为了解决这个问题的:
// 将策略(计算方式)通过构造传入
public enum PayrollDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
//the strategy enum type
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hoursWorked, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}
对未知枚举的操作,可以这样写:
private static <T extends Enum<T> & Operate> void test(Class<T> c) {
// 返回此枚举类的元素,如果此Class对象不表示枚举类型,则返回null。
T[] enumConstants = c.getEnumConstants();
}
七、方法
38、在方法执行前,对参数进行检查
对于公有方法,应该在注释上明确写出方法参数的限制,并使用@throws
表明违反参数值限制会抛出的异常。
/**
* xxx
*
* @param bigInteger xxx不能小于等于0
* @return xxx
* @throws RuntimeException 如果xxx小于等于0将会抛出该异常
*/
public BigInteger mod(BigInteger bigInteger) {
if (bigInteger.signum() <= 0) {
throw new RuntimeException();
}
...
}
对于非公有方法,这些方法是我们使用的,推荐使用断言assert:
private void func(int a) {
assert a < 0;
...
}
构造器的参数也要进行检查。
39、必要时进行保护性copy
class Period {
private final Date startTime;
private final Date endTime;
public Period(Date startTime, Date endTime) {
if (startTime.compareTo(endTime) > 0) {
throw new RuntimeException("xxx");
}
this.startTime = startTime;
this.endTime = endTime;
}
}
public static void main(String[] args) {
Date startTime = new Date();
Date endTime = new Date();
Period period = new Period(startTime, endTime);
// 由于Date类不是不可变对象,所以客户端如果这样恶意操作,将会导致时间不对
endTime.setTime(123L);
}
我们只需要修改一下构造,就可以防止这种情况:
this.startTime = new Date(startTime.getTime());
this.endTime = new Date(endTime.getTime());
这里我们重新new了一个Date对象,而不是使用clone()方法,原因是因为Date类可以被继承,防止子类恶意重写clone()方法。
40、
1、方法名称要易于理解,同一个包内风格要一致。
2、参数列表不要过长
解决办法:
- 把方法拆分,一个方法只需要少量参数
- 创建辅助类(通常是静态内部类),代表一种东西(例如:一个方法有6个参数,但是有4个是和狗相关的,那么就可以建一个静态内部类来表示他)
- 创建一个类,把所有参数set进去。
41、慎重使用重载,尤其是方法参数个数相同的时候
覆盖是在运行时进行的。
重载是在编译期进行选择的。
自动装箱和泛型的出现 破坏了List接口
E remove(int index);
boolean remove(Object o);
如果List的泛型是Integer类型的,那么调用remove方法就不知道调用哪个了。
42、当返回值是数组或者集合的时候,不要返回null
if (list == null) {
return new ArrayList(0);
}
八、通用程序设计
45、局部变量作用域最小化
用到的时候再声明
48、如果需要精确的答案,不要用double和float
用Big浮点或int/long型代替
49、尽量使用基本类型
但是类中的全局变量(例如POJO),要使用包装类型,因为基本数据类型默认值是0(比如,数据库中存的是null,但是实体却告诉我值是0,这显然是不正确的)
九、异常
57、不要使用异常做流程控制
还有我们设计的API也不要强迫客户端来使用异常来做控制流:
// 如果Iterator类没有提供hasNext()方法,那么我们只能使用下面这种方式来进行书写
Iterator<Integer> iterator = Arrays.asList(1, 2, 3).iterator();
try {
while (true) {
System.out.println(iterator.next());
}
} catch (Exception e) {
e.printStackTrace();
}
所以我们的Api可以采用以下两种做法:
- 提供状态测试方法:例如hasNext()方法
- 可识别的返回值:一般用于,对象被高并发访问时
58、Api中使用异常
受检异常:期望Api调用者能够适当的恢复。
运行时异常:Api调用者没有遵守Api的规范。
错误:JVM使用
60、优先使用Java提供的运行时异常
异常 | 使用场合 |
---|---|
IllegalArgumentException | 客户端传递的参数不合适 |
IllegalStateException | 传递的对象状态不合适(iterator.remove().remove()) |
NullPointerException | 传递的参数为空 |
IndexOutOfBoundsException | 传递的参数对数组的操作下标越界了 |
ConcurrentModificationException | 禁止并发的时候,对对象进行并发修改了 |
UnsupportedOperationException | 对象不支持用户请求的方法(不可变List的set方法) |
62、异常转译
如果不能阻止或处理底层的异常,要使用异常转译。除非底层方法抛出的异常恰好能用于高层。
63、异常的消息中包含失败的信息(参数等)
64、如果抛出异常了,要将之前的修改全部恢复(原子性)
可以通过调整计算处理的顺序解决
十、并发
67、避免在synchronized代码块中调用外面的方法;不要过度同步
Demo看书,里面有笔记
68、使用线程池不要直接使用线程
69、使用并发工具不要使用wait、notify
wait、notify代码过于复杂,容易写错
// 使用ConcurrentHashMap模拟String.intern()方法
private static final Map<String, String> map = new ConcurrentHashMap<>();
public static String intern(String s) {
String s1 = map.putIfAbsent(s, s);
return s1 == null ? s : s1;
}
// 使用双重检查锁进行优化(节省时间)
public static String intern(String s) {
String s1 = map.get(s);
if (s1 == null) {
s1 = map.putIfAbsent(s, s);
if (s1 == null) {
s1 = s;
}
}
return s1;
}
第三版新增
42、lambda表达式代替匿名类
编译器是通过泛型来推导lambda表达式中的类型的
// 有了lambda表达式,就可以将上面的枚举进行优化
public enum Operation {
PLUS("+", (x, y) -> x + y);
...
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
如果代码很简单,可以使用lambda表达式(少于3行)
lambda无法获得自身的引用(this,虽然他是一个匿名类)
43、优先使用方法引用(::)
44、Stream流只支持int、double、long
不能处理char型
pdf版的看不进去。。。