1.重写equals方法需满足几个约定:自反性(这是最简单的,仅意味着对象必须等于自身)、对称性(a.equals(b) == true, 那b.equals(a)也应该== true)、传递性(a.equals(b) == true; b.equals(c) == true,那a.equals(c)也应该== true)、一致性(如果a.equals(b) == true,那么多次执行a.equals(b)也应该== true)。然而事情不总是完美的,“我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定”,这是面向对象语言中关于等价关系的一个基本问题。
2. 重写equals方法必须同时重写hashCode方法。重写规则为:
a. 声明一个int变量并命名为result,初始化为对象中第一个关键域(也就是第一个属性)的散列码
b. 剩下的每一个关键域都完成以上步骤,如果是基本类型,则用Type.hashCode()方法,Type指的是装箱基本类型的类。如果是数组,则把每一个元素当成一个关键域来处理,可以用Arrays.hashCode()方法
1 public class Point { 2 private final short x; 3 private final int y; 4 private final int[] z; 5 6 public int hashCode(){ 7 int result = Short.hashCode(x); 8 result = result * 31 + Integer.hashCode(y); 9 result = result * 31 + Arrays.hashCode(z); 10 return result; 11 }
3. 实现cloneable接口的通用约定:
a. x.clone() != x;
b. x.clone().getClass() == x.getClass();
c. x.clone().equals(x) == true;
具体该如何克隆,要进行分析。类中有不可变的域,和类中有可变的域的克隆方式是不同的。
有不可变域的类应该直接用super.clone(),这样clone出来的实例的域与原来的类的域是一样的且不可变的(例子:PhoneNumber类里面有个不可变域, 号码private final static int number)
1 public PhoneNumber clone(){ 2 try { 3 return (PhoneNumber) super.clone(); 4 }catch (CloneNotSupportedException e){ 5 throw new AssertionError(); 6 } 7 }
有可变域的类应该要让可变域也clone(), 否则clone出来的实例的域与原来的类的域是共用的,这样就会造成灾难性后果(例子:Stack类里面有个可变域,private Object[] elements)
1 public Stack clone(){ 2 try { 3 Stack result = (Stack) super.clone(); 4 result.elements = elements.clone(); 5 return result; 6 }catch (CloneNotSupportedException e){ 7 throw new AssertionError(); 8 } 9 }
总之,实现cloneable接口后要重写clone方法,并且是公有的方法,返回类型是类本身,然后拷贝任何需要拷贝的域
4.内存泄漏
这是一个Stack类,里面的pop()方法造成了内存泄漏
1 public class Stack { 2 private Object[] elements; 3 /*size是用来记录当前stack里有多少个元素的,而不是stack的容量 4 * 当size等于容量时,会进行扩容*/ 5 private int size = 0; 6 private static final int DEFAULT_INITIAL_CAPACITY = 16; 7 8 public Stack(){ 9 this.elements = new Object[DEFAULT_INITIAL_CAPACITY]; 10 } 11 12 public void push(Object o){ 13 ensureCapacity(); 14 elements[size++] = o; 15 } 16 17 public void ensureCapacity(){ 18 if (elements.length == size){ 19 elements = Arrays.copyOf(elements, size * 2 + 1); 20 } 21 } 22 23 public Object pop(){ 24 if (size == 0){ 25 throw new EmptyStackException(); 26 }30 return elements[--size]; 31 } 32 }
这是因为,从Stack里pop出来的对象,即使不再被其他程序引用了,也不会被垃圾回收,Stack内部会一直维护这这些对象的过期引用。把pop方法改成以下,就能解决内存泄漏的问题了
1 public Object pop(){ 2 if (size == 0){ 3 throw new EmptyStackException(); 4 } 5 Object result = elements[--size]; 6 /*清空老旧对象的引用,以防内存泄漏*/ 7 elements[size] = null; 8 return result; 9 }
5. Comparable接口
注意:有些类,它的compareTo方法与equals方法不一致,比如BigDecimal类。现有两个实例new BigDecimal("1.0")和new BigDecimal("1.00"),如果把这两个实例放进一个HashSet里,集合中将会有两个元素,因为通过equals方法比较是不相等的;如果把这两个实例放进一个TreeSet里,集合中只有一个元素,因为通过compareTo方法比较是相等的。
Java 7之后,就不建议直接用<, >来比较,而且容易出错,可以直接用包装类的compare方法来比较,如;
1 public class MobilePhoneNumber implements Cloneable, Comparable<MobilePhoneNumber>{ 2 private final int firstThreeNumber; 3 private final int middleThreeNumber; 4 private final int lastFourNumber; 5 6 public int compareTo(MobilePhoneNumber mpn){ 7 int result = Integer.compare(firstThreeNumber, mpn.firstThreeNumber); 8 if (result == 0){ 9 result = Integer.compare(middleThreeNumber, mpn.middleThreeNumber); 10 if (result == 0){ 11 result = Integer.compare(lastFourNumber, mpn.lastFourNumber); 12 } 13 } 14 return result; 15 }
或者用做一个静态比较器
1 private static final Comparator<MobilePhoneNumber> COMPARATOR = 2 Comparator.comparingInt((MobilePhoneNumber mpn) -> mpn.firstThreeNumber) 3 .thenComparingInt(mpn -> mpn.middleThreeNumber) 4 .thenComparingInt(mpn -> mpn.lastFourNumber); 5 6 public int compareTo(MobilePhoneNumber mpn){ 7 return COMPARATOR.compare(this, mpn); 8 }
总而言之,实现一个对排序敏感的类时,都应该让这个类实现Comparable接口,以便它的实例可以轻松地被分类、搜索,以及用在基于比较的集合中。每当在compareTo方法的实现中比较域值时,避免使用<, >,而应该用装箱基本类型的静态compare方法,或者在Comparator接口中使用比较器构造方法
6. 类的修饰与设计
a. 公有类的实例域决不能是公有的。如果实例域是非final的,或者是一个指向可变对象的final引用,如果不用private修饰的话,就等于放弃了对存储在这个域中的值进行限制的能力,也就意味着你放弃了强制这个域不可变的能力。因此,包含公有可变域的类通常不是线程安全的。但有一种情况是可以用public静态域来修饰的,就是这些域要么包含基本类型的值,要么包含指向不可变对象的引用(需要大写),如
1 public static final int HOURS_PER_DAY = 24; 2 public static final PhoneNumber PHONE_NUMBER = new PhoneNumber(22226688);
b. 公有类永远都不应该public可变的域,但如果public的是不可变的域,相对来说危害小一点
c. 不可变类指它的实例不能被修改的类,每个实例中包含的所有信息必须在创建的时候就提供了(如String类,基本类型包装类,BigDecimal类)。遵循下面五条规则:
i.不要提供任何会修改对象状态的方法(也成为设值方法)
ii. 保证类不会被扩展,也就是声明为final类
iii. 声明所有的域为final
iv.声明所有的域为私有的
v. 确保对于任何可变组件的互斥访问(也就是如果类有指向可变对象的域,则必须确保客户端无法获得指向这些对象的引用)
d. 不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由地共享,所以应该鼓励客户端尽可能地重用现有的实例(办法是,对于频繁使用的值,用public static final修饰)
e. 不可变对象无偿地提供了失败的原子性,所以它们的状态永远不变,因此不存在临时不一致的可能性
f. 不可变类真正唯一的缺点就是,对于每个不同的值都需要一个单独的对象
g. 除非有很好的理由要让类成为可变的类,否则它就应该是不可变的
h. 如果类不能被做成不可变的,仍然应该尽可能地限制它的安全性,降低对象可以存在的状态数
i. 除非有令人信服的理由要使域变成非final的,否则每个域都是private final的
7.复合优先于继承
a. 与方法调用不同的是,继承打破了封装性。超类的实现由可能会随版本的不同而有所变化,导致子类遭到破坏
b. 只有在子类确实是超类的子类型时,才适合用继承,也就是子类B确实也是超类A,否则应该使用复合
c. 构造器绝不能调用可被重写的方法,同样如果超类实现了Cloneable和Serializable接口,clone方法或是readObject方法都不可以调用可被覆盖的方法
8. 接口优于抽象类
抽象的骨架实现类(skeletal implementation)可以结合接口和抽象类的优点,然后其他类再去继承这个抽象骨架实现类,这就是模板方法(Template Method)模式
9. 类层次优于标签类
标签类:
1 /*标签类。一个类里面有多种风格,这是错误的*/ 2 public class Figure { 3 enum Shape {RECTANGLE, CIRCLE}; 4 final Shape shape; 5 /*给长方形用的*/ 6 double length; 7 double width; 8 /*给圆形用的*/ 9 double radius; 10 11 public Figure(double radius){ 12 shape = Shape.CIRCLE; 13 this.radius = radius; 14 } 15 public Figure(double length, double width){ 16 shape = Shape.RECTANGLE; 17 this.length = length; 18 this.width = width; 19 } 20 /*计算面积*/ 21 public double area(){ 22 switch (shape){ 23 case CIRCLE: 24 return Math.PI * radius * radius; 25 case RECTANGLE: 26 return length * width; 27 default: 28 throw new AssertionError(shape); 29 } 30 } 31 }
类层次:
1 public abstract class Figure_ { 2 abstract double area(); 3 } 4 5 class Circle extends Figure_{ 6 final double radius; 7 public Circle(double radius){ 8 this.radius = radius; 9 } 10 public double area(){ 11 return Math.PI * radius * radius; 12 } 13 } 14 15 class Rectangle extends Figure_{ 16 final double length; 17 final double width; 18 public Rectangle(double length, double width){ 19 this.length = length; 20 this.width = width; 21 } 22 public double area(){ 23 return length * width; 24 } 25 }
10. 静态成员类优于非静态成员类
a. 静态成员类是最简单的一种嵌套类,它可以访问外围类的所有成员,包括那些声明为private的成员,可以把静态成员类看做是外围类的一个静态域。静态成员类的一种常见用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。比如Calculator类的一个公有静态成员类是Operation枚举类,之后客户端就可以使用Calculator.Operation.PLUS和Calculator.Operation.MINUS
b. 非静态成员类的不同是,非静态成员类的实例是要和外围类的实例有关的,是要访问外围实例的
11. 永远不要把多个顶级类或者接口放在一个Java源文件中
12. 泛型
a. 永远不要使用原生态类型,如List, 而是应使用泛型,如List<E>,List<?>(如果不关心类型的话就用?)。但是很特殊的是,不能将任何元素(除了null之外)放到Collection<?>中
b. 列表优于数组。数组是协变的,泛型是可变的,也就是说,如果Sub是Super的子类型,那么Sub[]就是Super[]的子类型,但List<Sub>既不是List<Super>的超类,也不是其子类
c. 数组是具体化的,在运行时会知道和强化它们的元素类型,所以一般运行了才找到错误。而泛型是通过擦除实现的,一般编译的时候强化类型信息,并运行时丢弃,所以一般编译时就找到错误
d. 由于泛型一般<>里填具体的类型参数,所以当你想提升API的灵活性时,需要用有限通配符。PECS原则:producer-extends, consumer-super. 如(在这里Collection是消费者,Set是生产者):
1 public <E> void popAll(Collection<? super E> dst){ } 2 3 public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2){ }
13. 枚举
a. Java的枚举类型功能比其他语言中的对应类强大很多,Java枚举本质上是int值
b. 可以向枚举类型中添加方法和域。为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。因为枚举天生就是不可变得,所以域必须是final的,最好是private的
c. 每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举。但枚举类型中的常量集并不一点要始终保持不变。如果有多个(但并非所有)枚举常量同时共享相同的行为,则要考虑用策略枚举:
1 /*计算每天的工资,基本8小时工资+加班工资 2 策略枚举,strategy enum,将加班工资计算移到另一个私有的嵌套枚举PayType中*/ 3 public enum PayrollDay { 4 MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY(PayType.WEEKEND),SUNDAY(PayType.WEEKEND); 5 6 private final PayType payType; 7 PayrollDay(PayType payType){ 8 this.payType = payType; 9 } 10 PayrollDay(){ 11 this(PayType.WEEKDAY); 12 } 13 int pay(int minsWorked, int payRate){ 14 return payType.pay(minsWorked, payRate); 15 } 16 17 private enum PayType{ 18 WEEKDAY{ 19 int overtimePay(int minsWorked, int payRate){ 20 return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2; 21 } 22 }, 23 WEEKEND{ 24 int overtimePay(int minsWorked, int payRate){ 25 return minsWorked * payRate / 2; 26 } 27 }; 28 29 abstract int overtimePay(int mins, int payRate); 30 private static final int MINS_PER_SHIFT = 8 * 60; 31 32 int pay(int minsWorked, int payRate){ 33 int basePay = minsWorked * payRate; 34 return basePay + overtimePay(minsWorked, payRate); 35 } 36 } 37 }
d. 如果要传入多个枚举常量参数的话,可以用EnumSet<>;如果枚举常量和其他的域有关联关系的话,可以用EnumMap<>
14. 在每个覆盖超类的方法声明上用@Override注解。不然如果重写时写错了参数类型或数量,就变成重载而不是重写了
15. Lambda表达式
a. Lambda是不用写参数的类型的,当然如果想更清晰的话可以写上去
b. 使用Lambda,编译器是通过泛型来进行类型推导来获取大部分类型信息的。如果编译器推导不了类型,就必须在Lambda中手工指定类型
c. Lambda没有名称和文档,如果一个计算本身不是自描述的,或者超出了三行,那就别用Lambda
d. 尽可能不要(除非迫不得已)序列化一个Lambda
e. 千万不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例
16. Stream
a. 滥用Stream会使程序代码更难以读懂和维护
b. 适合用Stream的情况:可以取代for循环、需要过滤元素、利用单个操作操作元素(如添加,连接或者计算其最小值)、将元素放到一个集合中、搜索满足某些条件的序列:
1 /*不用Stream*/ 2 List<String> alphabet = Arrays.asList("a", "b", "c"); 3 List<String> upperAlphabet = new ArrayList<>(); 4 for (int i = 0; i < alphabet.size(); i++) { 5 upperAlphabet.add(alphabet.get(i).toUpperCase()); 6 } 7 System.out.println(upperAlphabet); 8 /*用Stream*/ 9 List<String> alphabet = Arrays.asList("a", "b", "c"); 10 List<String> upperAlphabet = alphabet.stream().map(a -> a.toUpperCase()).collect(Collectors.toList()); 11 System.out.println(upperAlphabet);
c. forEach操作应该只用于报告Stream计算的结果,而不是执行计算
d. 为了正确使用Stream,必须了解收集器。最重要的收集器工厂是toList, toSet, toMap, groupingBy和joining
e. 对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型。千万别在内存中保存巨大的序列,将它作为集合返回即可
f. 千万不要任意地并行Stream,否则可能适得其反。想在Stream上通过并行获得更好性能,最好是通过ArrayList, HashMap, HashSet和concurrentHashMap实例,数组,int范围和long范围等。这些数据结构的共性是,都可以被精确、轻松地分成任意大小的子范围,试并行线程中的分工变得更轻松。
17. 方法
有三种技巧可以缩短过长的参数列表:
a. 第一种是把一个方法分解成多个方法,每个方法只需要这些参数的一个子集,但有可能会导致方法过多
b. 创建辅助类,用来保存参数的分组。比如编写一个表示扑克牌的类,会发现经常要传递一个双参数的序列来表示纸牌的点数和花色,这时候就可以增加辅助类来表示一张纸牌,并把每个参数序列都换成这个辅助类的单个参数
c. 从对象构建到方法调用都采用Builder模式
对于参数类型,要优先使用接口而不是类。比如,应该要用Map接口作为参数而不是HashMap,否则如果客户端传入的是TreeMap,就会导致不必要的、可能非常昂贵的拷贝操作
对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的。也就是说,程序编译时选择正确的重载方法,而在运行时选择正确的覆盖方法:
1 /*重载,结果输出都是Unknown collection*/ 2 public static String classify(Set<?> s){ 3 return "Set"; 4 } 5 public static String classify(List<?> l){ 6 return "List"; 7 } 8 public static String classify(Collection<?> c){ 9 return "Unknown collection"; 10 } 11 12 public static void main(String[] args) { 13 Collection<?>[] collections = {new HashSet<String>(), new ArrayList<Integer>(), new HashMap<String, Long>().values()}; 14 for (Collection<?> c:collections) { 15 System.out.println(classify(c)); 16 } 17 } 18 }
安全而保守的策略是,永远也不要导出两个具有相同参数数目的重载方法。而且其实始终可以给方法起不同的名称,而不使用重载机制
18. 返回零长度的数组或者集合,而不是null。因为如果返回null的话,接下来(或者客户端使用的时候)要加一个if (xx != null...)的判断,很多人会忘记写这个判断。Java 8有Optional<T>类,可以存放单个非null的T引用,或者什么内容都没有。所以,返回null可以用Optionl.empty()取代,有内容则返回Optional.of(result)。但是,容器类型包括集合、映射、Stream、数组和optional,都不应该被包装在optional中。综合来说,还是慎用Optional,因为很多情况都不适用。。
19. 通用编程
a. 要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明,如果太早进行声明,会使读者造成混乱
b. for循环的作用域正好被限定在需要的范围之内。如果循环终止之后不再需要循环变量的内容,for循环优先于while循环
c. 将局部变量的作用域最小化的一种技巧就是使方法小而集中。如果把两个操作合并在同一个方法里,其中一个操作相关的变量就有可能混进另一个操作里面,所以解决方法是把方法分成两个:每个操作用一个方法完成
d. for-each循环的优势(完全隐藏Iterator迭代器或者传统for循环的索引变量;避免了嵌套式迭代时易犯的错误;方便简洁):
1 /*打印两颗骰子所有的组合*/ 2 enum Face {ONE, TWO, THREE, FOUR, FIVE, SIX} 3 public static void main(String[] args) { 4 Collection<Face> faces = EnumSet.allOf(Face.class); 5 /*嵌套式迭代的错误,只会打印出ONE ONE, TWO TWO...SIX SIX*/ 6 for (Iterator<Face> i = faces.iterator(); i.hasNext();){ 7 for (Iterator<Face> j = faces.iterator(); j.hasNext();){ 8 System.out.println(i.next() + " " + j.next()); 9 } 10 } 11 /*嵌套式迭代的修正,打印正常*/ 12 for (Iterator<Face> i = faces.iterator(); i.hasNext();){ 13 Face f = i.next(); 14 for (Iterator<Face> j = faces.iterator(); j.hasNext();){ 15 System.out.println(f + " " + j.next()); 16 } 17 } 18 /*for-each循环,打印正常且简洁*/ 19 for (Face face:faces){ 20 for (Face face1:faces){ 21 System.out.println(face + " " + face1); 22 } 23 } 24 }
e. 有三种常见情况无法用for-each循环:解构过滤(如果需要遍历集合并删除选定的元素,就需要用显式的迭代器),转换(如果需要遍历列表或者数组,并取代它的部分或者全部元素值,就需要列表迭代器或者数组索引),平行迭代(如果需要平行的遍历多个集合,就像上面代码错误的例子一样,需要外循环和内循环同时前进,则需要显式的迭代器或者索引控制变量)
f. 使用标准类库,充分利用编写这些类库的专家的知识,以及前人的使用经验
g. Java 7之后,要用ThreadLocalRandom.current().nextInt(),而不是用new Random().nextInt(),前者比后者快很多
h. 每个Java程序员都应该熟悉java.lang, java.util, java.io及其子包中的内容。有几个类库值得一提:Collections Framework(集合框架), Stream类库, java.util.concurrent
i. 如果标准类库中找不到所需要的的功能,那就找高级的第三方类库,比如Google的Guava类库,如果再找不到,就只能自己实现功能了
j. 如果需要精确的答案,要避免使用float和double(尤其是货币计算),而要使用BigDecimal, int, long(如果数值范围没有超过9位十进制数字,就可以使用int;如果不超过18位数字,就可以使用long,如果超过18位数字,则必须使用BigDecimal;BigDecimal的优点是精度最高且有8种舍入模式,缺点是使用不方便且速度很慢)
k. 对装箱基本类型运用==操作符几乎总是错误的。当一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型会自动拆箱
l. 如果其他类型更适合,尽量避免使用字符串。字符串不适合代替枚举类型、聚合类型(有多个组件的实体,应该单独编写一个类来描述,通常是一个私有的静态成员类)、能力表。字符串会比其他类型更加笨拙、速度更慢也更容易出错
m. 连接n个字符串而重复地使用字符串连接操作符(+),需要n的平方级的时间(因为字符串不可变,所以连接两个字符串时,它们的内容都需要拷贝)
n. 如果没有合适的接口,就用类而不是接口来引用对象(最好用类层次中提供了必要功能的最小的具体类来引用)。还有,如果类实现了接口但它也提供了接口中不存在的额外方法,就适合直接用类来引用对象
o. 反射机制给定一个类对象,可以获得构造器、方法、域的实例;允许一个类来使用另一个类,即使当前者被编译时后者还不存在;。但反射机制的代价有:损失了编译时类型检查的优势、执行反射访问所需要的代码非常笨拙和冗长、性能损失
p. 使用本地方法(比如C或者C++编写的方法)来提高性能的做法不值得提倡
q. 命名惯例中,常量域(final,名称全部大写)是唯一推荐的用下划线的情形
r. 静态工厂的常用名称包括from, of, valueOf, instance, getInstance, newInstance, getType, newType
20. 异常
a. 异常应只用于异常的情况下,而永远不应该用于正常的控制流。设计良好的API不应强迫它的客户端为了正常的控制流而使用异常
b. 一些常用的异常:IllegalArgumentException(传递的参数值不合适时就会抛出),IllegalStateException(对象状态非法,如某对象还没被初始化就被调用就会抛出),IndexOutOfBoundsException, ConcurrentModificationException(一个专门用于单线程的对象被并发地修改就会抛出)
c. 异常链指的是catch(父类(也就是低层)xxException e){ throw new 子类(也就是高层)xxException(e); }
d. catch块不能为空,如果为空的话要用注释说明为什么
21. 并发
a. 读或者写一个变量是原子性的,除非这个变量的类型是long或者double
b. Java语言规范虽然保证线程在读取原子数据时不会看到其他任意的数值,但是并不保证线程写入的值对于另一个线程是可见的。所以为了线程之间进行可靠的通信,也为了互斥访问,同步是必要的
c. 当多个线程共享可变数据的时候,每个读或者写数据的线程都必须进行同步
d. 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制,也就是在同步的区域内部不要调用设计成被覆盖的方法或者是外来的方法
e. 通常来说,应该在同步区域内做尽可能少的工作。获得锁、检查共享数据、根据需要转换数据、释放锁。
f. Runnable的run方法是void的,而Callable的call方法是有返回值的,通常与FutureTask<T>一起使用。FutureTask的get方法会阻塞,直到有结果返回:
1 public class TestCallable implements Callable { 2 public String call(){ 3 return "SS"; 4 } 5 public static void main(String[] args) throws InterruptedException, ExecutionException { 6 TestCallable tc = new TestCallable(); 7 FutureTask<String> ft = new FutureTask<>(tc); 8 new Thread(ft).start(); 9 System.out.println(ft.get()); 10 } 11 }
g. 应该优先使用ConcurrentHashMap而不是使用Collections.synchronizedMap
h. 对于间歇式的定时,始终应该优先使用System.nanoTime而不是使用System.currentTimeMillis,因为前者更精准,而且不受系统的实时时钟的调整所影响
i. 尽量用并发工具而不是用wait和notify。如果要用,始终应该使用wait循环模式来调用wait方法,永远不要在循环之外调用wait方法
j. 一个类为了可被多个线程安全使用,必须在文档中清楚地说明它的线程安全性级别:
*不可变的(immutable, 类的实例是不可变的,不需要外部的同步,如String, Long, BigInteger)
*无条件的线程安全(unconditionally thread-safe, 类的实例可变,但有着足够的内部同步,实例可被并发使用,无需外部同步,如AtomicLong, ConcurrentHashMap)
*有条件的线程安全(conditionally thread-safe, 有些方法需要外部同步,其他线程安全级别与无条件的线程安全相同,如Collections.synchronized的包装返回的集合,迭代器要求外部同步)
*非线程安全的(类的实例可变,并发时所有方法都要外部同步,如通用的集合实现ArrayList, HashMap)
*线程对立的(thread-hostile, 即使外部同步也不能安全地并发。不再建议使用)
k. 如果处于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式:
1 private volatile FieldType field; 2 /*double check idiom*/ 3 private FieldType getField(){ 4 FiledType result = field; 5 /*first check(without lock)*/ 6 if (result == null){ 7 synchronized (this){ 8 /*second check(with lock)*/ 9 if (field == null){ 10 field = result = computeFieldValue(); 11 } 12 } 13 } 14 }
l. 不要让程序依赖于线程调度器(最好保证线程的平均数量不明显多于处理器的数量),不要依赖Thread.yield或者线程优先级,这些都会使程序不健壮且不可移植。线程池不要开太大,任务不要太小(否则分配的开销会影响性能)
22. 序列化
a. 为了避免序列化攻击,最好不要反序列化,永远不要反序列化不被信任的数据
b. JSON是基于文本的,最初是为了JavaScript开发的;protobuf是google为了在服务器之间保存和交换结构化数据而设计的,最初是为了C++开发的,是二进制的所以更有效,提供了另一种文本表示法pbtxt便于人类阅读
c. 实现Serializable接口的代价:一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性、增加了出现BUG和安全漏洞的可能性、随着类发行新版本,相关的测试负担也会增加