Effective Java 读书笔记

文章目录

第二章 创建对象和销毁对象

1. 用静态工厂方法代替构造器

静态工厂的优点

  • 静态工厂方法有名称

    构造器只是参数个数、顺序不同, 用户很难记住该使用哪个构造器

  • 静态工厂方法每次调用时不必都创建一个新对象

    可以将创建好的实例缓存起来重复利用,类似享元模式

  • 静态工厂方法可以返回原返回类型的任何子类型的对象

    这样我们在选择返回对象的类时就有了更大的灵活性,如接口

  • 静态工厂方法返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂的参数值

  • 静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在

    这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。

静态工厂的缺点

  • 类如果不含公有的或者受保护的构造器,就不能被子类化
  • 在API文档中,他们没有像构造器那样在API文档中明确标识出来,程序员很难发现他们

2. 遇到多个构造器参数时要考虑使用构建器

三种创建对象方式

  • 重叠构造器模式可行,但参数多的时候,客户端代码会很难编写,也难以阅读
  • 如果使用 JavaBeans模式,但是在构造过程中可能处于不一致的状态
  • 建议采用建造者(Builder)模式,即能保证像重叠构造器的安全性,也能保证像 JavaBeans 模式那么好的可读性

3. 用私有构造器或者枚举类型强化 Singleton 属性

实现单例的方式

  • 私有构造器 + 公有静态变量
  • 私有构造器 + 公有静态方法
  • 枚举

Tips: 私有构造器,为了防止通过反射创建对象,可以在构造器内加入抛出异常

4. 通过私有构造器强化不可实例化的能力

对于只要静态成员的类,不希望被实例化,如工具类

5. 优先考虑依赖注入来引用资源

静态工具类和 Singleton 类不适合于需要引用底层资源的类,应该将这些资源传给构造器(静态工厂或者构建器)

6. 避免创建不必要的对象

反例:

// 参数 test 本身就是一个 String 实例,没有必要使用 new 创建新对象
String s = new String("test");

// 每次使用 String.matches 都会生成一个 Pattern 实例
// 解决:将 Pattern 实例编译成 Pattern(不可变)

// 自动拆箱与装箱,每次计算时都要拆箱、装箱
private static sum() {
  Long sum = 0;
  for(int i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
  }
  return sum;
}

7. 消除过期的对象引用

内存泄漏来源

  • 元素被释放,但未清空该元素包含的对象引用
  • 缓存
  • 监听器

8. 避免使用终结方法和清除方法

缺陷:

  • 终结方法(finalizer)和清除方法(cleaner)都是不可预测的,两者都有非常严重的性能损失
  • 终结方法还有一个严重的安全问题:终结方法攻击

tips:自 Java 9 开始,清除方法代替了终结方法

合理用途:

  • 当资源的所有者忘记调用它的 close 方法时,终结方法和清洁方法可以充当安全网
  • 与对象的本地对等体一起使用

9. try-with-resources 优先于 try-finally

只要资源实现了 AutoCloseable 接口,资源最后能被自动关闭。

如果打开资源和关闭资源都发生了异常,第一个异常记录不会被第二个异常记录抹除,并且从第二个异常起会被禁止。

第三章 对所有对象都通用的方法

10. 覆盖 equals 时请遵守通用约定

如果类需要属于自己的 “逻辑相等”,那么应该覆盖 equals 方法,这种类叫 “值类”。

有一种 “值类” 不需要覆盖 equals 方法,如枚举类,这种类每个值至多存在一个对象,在这种情况下,逻辑相等和对象相等是一回事。

equals 方法实现了等价关系,需要遵循以下规范:

  • 自反性:当 x != null 时,x.equals(x) == true 成立,即自身等于自身;
  • 对称性:当 x、y != null 时,x.equals(y) == y.equals(x) 成立;
  • 传递性:当 x、y、z != null,x.equals(y),y.equals(z) 时,x.equals(z) 成立;
  • 一致性:如果两个对象相等,那么他们始终相等,除非它们中有一个对象被修改了;
  • 非空性:所有的对象都不能等于 null

11. 覆盖 equals 方法时总要覆盖 hashCode

hashCode 约定:

  • 如果一个对象没有被修改,那么不管调用几次 hashCode 方法都将返回相同的值
  • 如果两个对象通过 equals 方法比较是相等的,那么调用这两个对象的 hashCode 方法将产生相同的整数结果
  • 如果两个对象通过 equals 方法比较是不相等的,那么不要求 hashCode 方法生成相同的整数结果

Tips:给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)性能

12. 始终要覆盖 toString

覆盖 toString 方法不是必须的,但 toString 方法应该以美观的格式返回一个关于对象的简洁、有用的描述。

13. 谨慎地覆盖 clone

14. 考虑实现 Comparable 接口

compareTo 方法与 equals 方法相似,不同的是 compareTo 方法要求相同类型的对象才能对比,否则将抛出类型转化异常。

第四章 类和接口

15. 使类和成员的可访问性最小化

隐藏内部数据和实现细节,把 API 与实现清晰地隔离开,即解耦。

类和接口的访问级别:

  • 包级私有(package-private)

    默认级别,是包的实现的一部分,而不是该包导出的 API 的一部分,所以后期如果要修改或者删除,都不会影响到现有客户端程序。

  • 公有(public)

成员的访问级别:

  • 私有(private)

    只有在声明该成员的顶级类内部才可以访问

  • 包级私有(package-private)

    默认级别,声明该成员的类的同级包的任何类都可以访问

  • 受保护的(protected)

    声明该成员的类的同级包的任何类都可以访问,声明该成员的类及其子类都可以访问

  • 公有(public)

error:让类具有公有的静态 final 数组域,或者返回这种域的访问方法,都是错误的。

solution:可以让公有数组变成私有,同时提供一个公有方法返回不可变列表或者拷贝。

16. 要在公有类中使用访问方法而非公有域

如果类需要在它所在的包之外的进行访问,应该提供公有的访问方法,而不是暴露可变的数据域。

17. 使可变性最小化

不可变的类可以被共享,并且未其他对象提供了大量的构建。

不可变的类的状态永远不变,不存在临时不一致的可能性,具有失败原子性。

不可变类也有缺点,对于每个不同值都需要一个单独的对象。

18. 复合优先于继承

继承打破了封装性,子类依赖于超类中特定功能的实现细节,当超类的实现发生变化时,子类可能会遭到破坏,子类必须随着超类的变化而变化。

超类如果在后续的版本中增加了方法,并且非常不巧的与某个子类的方法的签名相同但返回类型不同,那么这样的子类将无法通过编译。

复合:不扩展现有的类,而是在新的类中增加一个私域,它引用现有类的一个实例。

19. 要么设计继承并提供文档说明,要么禁止继承

如果一个类要被设计为可继承的,那么必须有文档说明它可覆盖的方法的自用性,精确地描述覆盖每个方法所带来的影响。

文档格言:好的 API 文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。

20. 接口优于抽象类

接口允许构造非层次结构的类型框架,一个类可以多实现接口,按照需求组合接口,安全地增加类的功能。

接口定义了一种类型,允许有多种实现。

如果给接口提供一个抽象骨架实现类,就可以把接口和抽象类的优点结合起来,这种方式就是模板方法模式。

21. 为后代设计接口

在 Java8 之后,接口有了缺省方法,接口能够新增方法而现有的实现类可以不做变更。

22. 接口只用于定义类型

有一种接口成为常量接口,它的成员都是静态成员,且为常量,这种常量接口把内部细节都暴露出来了,是不合理的。如果需要导出常量,可以将常量添加到某个紧密相关的类中。

23. 类层次优于标签类

标签类是对类层次的一种简单模仿。

标签类的行为依赖于标签值,可以将每种标签值都子类化。

24. 静态成员类优于非静态成员类

非静态成员类的每个实例都隐含地与外围类的一个外围实例相关联,这种关联关系需要消耗非静态成员实例的空间,并且会增加构造的时间。

25. 限制源文件为单个顶级类

尽管编译器允许在一个 java 文件定义多个顶级类,但是没有什么好处,反而可能导致给一个类提供多个定义。

第五章 泛型

26. 请不要使用原生态类型

概念

  • 泛型(generic):声明中具有一个或多个类型参数,如 List、List、List<?>
  • 原生态类型(raw type):不带任何实际类型参数,如 List

尽管使用原生态类型是合法的,但是没有泛型在安全性和描述性方面的优势,使用原生态类型不能在编译期间发现问题。

泛型是通过擦除实现的,泛型在编译期间明确告知编译器需要检查类型,之后编译器会自动转化类型,如果类型不一致,在运行期将抛出类型转换异常。

泛型是 Java 5 引入的,为了兼容之前的代码,允许原生态类型和泛型共存,泛型擦除之后,就可以与之前没有使用泛型的代码互通,确保平滑过渡。

27. 消除非受检的警告

尽可能的消除每一个非受检警告,让问题在编译期间就暴露出来。

如果无法消除警告,可以使用注 @SuppressWarnings(“unchecked”),该注解可以用在任意粒度的级别中,使用时应该尽可能在小的范围内使用,并且附上注释记录禁止该警告的原因。

28. 列表由于数组

数组和泛型的重要区别:

  • 数组是协变的,而泛型是可变的

    // 协变:某个对象不是数组的基类型,我们也可以把它赋值给数组元素
    // 编译通过,但运行时将抛出异常
    Object[] objectArray = new Long[1];
    objectArray[0] = "I don't fit in"; // throws ArrayStoreException
    
    // 编译失败
    List<Object> objectList = bew ArrayList<Long>(); // 非法类型
    objectList.add("I don't fit in");
    
  • 数组是具体化的,而泛型是可以被擦除的

    数组在运行时知道和强化它们的元素类型,如果企图将 String 保存到 Long 数组中,将会得到一个 ArrayStoreException。

    泛型只有在编译时强化它们的类型信息,并且在运行时丢弃(或擦除)它们的元素类型信息。

    数组和泛型不能很好的混用,如果此时得到了编译时错误或者警告,应该用列表代替数组。

29. 优先考虑泛型

使用泛型可以避免客户端自行转换类型,使用泛型不仅安全而且也更方便。

如果时间允许,旧的类也可以改造成泛型,对新用户来说会更轻松,而且也不会影响现有的客户端。

30. 优先考虑泛型方法

同第 29 条

31. 利用有限制通配符来提升 API 的灵活性

技巧:PECS 表示 product-extend,consumer-super

32. 谨慎并用泛型和可变参数

33. 优先考虑类型安全的异构容器

第六章 枚举和注解

34. 用 enum 代替 int 常量

特定于常量的方法实现:在枚举类中定义一个抽象的方法,每个实例都被要求覆盖,可以将行为与常量关联起来。

public enum Operation {
    PLUS {
        @Override public double apply(double x, double y) { return x + y;}
    };

    public abstract double apply(double x, double y);
}

35. 用实例域代替序数

如果需要使用枚举的序数,应该将它保存到实例域中,而不是调用 ordinal 方法。

36. 用 EnumSet 代替位域

37. 用 EnumMap 代替序数索引

38. 用接口模拟可拓展的枚举

39. 注解优于命名模式

40. 坚持使用 Override 注解

如果想要覆盖父类方法,要使用 Override 注解,告知编译器,编译器可以替你防止某些错误,如程序员错误地将重载认为是覆盖。

41. 用标记接口定义类型

第七章 Lambda 和 Stream

42. Lambda 优先于匿名类

使用匿名函数和 Lambda 的比较

// 使用匿名函数
List<Integer> list = Arrays.asList(1, 5, 3);
Collections.sort(list, new Comparator<Integer>() {
  @Override
  public int compare(Integer o1, Integer o2) {
    return o1.compareTo(o2);
  }
});

// 使用 Lambda
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));

Lambda 使特定于常量的方法实现更加简单

// 使用 Lambda 之前
public enum Operation {
  PLUS {
    @Override public double apply(double x, double y) { return x + y;}
  };

  public abstract double apply(double x, double y);
}

// 使用 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.apllyAsDouble(x, y);
  }
}

由于 Lambda 没有名称和文档,Lambda 表达式的参数名称尽可能见名知意,这能使 Lambda 的可读性更强。一行是最理想的情况,如果超过三行,则可读性下降,应考虑别的方式。

最后一点,Lambda 无法获得自身的引用。在 Lambda 中关键字 this 是指外围实例,在匿名类中关键字 this 是指匿名类实例,因此如果需要从函数对象的主体内部访问自身,就必须使用匿名类。

43. 方法引用优于 Lambda

使用 Lambda 和方法引用的比较

// 使用 Lambda
List<Integer> list = Arrays.asList(1, 5, 3);
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));

// 使用 方法引用
Collections.sort(list, Integer::compareTo);

方法带的参数越多,使用方法引用消除的样版代码越多。

大多数情况下,方法引用比 Lambda 简洁得多,如果不是,则不要使用。

44. 坚持使用标准的函数接口

java.util.Function 包下有数十个接口,记住 6 个基础接口即可,其余接口可以推断出来,下面是接口及其函数签名

接口函数签名范例备注
UnaryOperatorT apply(T t)String::toLowerCase一元操作符
BinaryOperatorT apply(T t1, Tt2)Integer::add二元操作符
Predacateboolean test(T t)Collection::isEmpty预测
Function<T, R>R apply(T t)Arrays::asList函数
SupplierT get()Integer::new生产者
Comsumervoid accept(T t)System.out::println消费者

现有的大多数标准函数接口都只支持基本类型参数,千万不要使用带包装类的基础函数接口代替基本函数接口,当使用装箱基本类型进行批量处理,会导致致命的性能问题。

45. 谨慎使用 Stream

在没有显示类型的情况下,仔细命名 Lambda 参数,对于 Stream 的可读性至关重要。

一些适合使用 Stream 的场景:

  • 转换元素,map(Student::getName)
  • 过滤元素,filter(student -> student.getAge() > 18)
  • 分组,groupingBy(Student::age)
  • 搜索满足某些条件的元素

46. 优先选择 Stream 中无副作用的函数

java.util.stream.Collectors 收集器提供了几类重要的函数

  • Collectors.toList
  • Collectors.toSet
  • Collectors.toMap
  • Collectors.groupBy
  • Collectors.joining

47. Stream 要优先用 Collection 作为返回类型

48. 谨慎使用 Stream 并行

并行 Stream 不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预计的行为(如安全性失败)。

诸如 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 等数据结构,它们可以被精确、轻松地分成任意大小的子范围,可以通过并行流获得性能提升。

Stream 类库用来执行这个分隔任务的抽象是分隔迭代器,它是由 Stream 和 Iterable 中的 spliterator 方法返回的。

程序中所有的并行 Stream pipeline 都是在一个通用的 fork-join 池中运行的,因此只要有一个 pipeline 运行异常,都会损害到系统中其他不相关部分的性能。

第八章 方法

49. 检查参数的有效性

对于公有的和受保护的方法,要用 Javadoc 的 @throws 标签在文档中说明违反参数限制时会抛出的异常;对于私有方法通常应该使用断言来检查它们的参数。

Objects 提供了 nonNull、requireNonNull、checkIndex 等方法用于参数校验。

50. 必要时进行保护性拷贝

保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象。

51. 谨慎设计方法签名

方法名称应始终遵循标准的命名习惯,首要目标是易于理解。

对于参数类型,要优先使用接口而不是类;对于 boolean 参数,要优先使用两个元素的枚举类型。

52. 慎用重载

不应该出现两个具有相同参数数目的重载方法,容易混淆。

反例:List 的 remove(E e) 和 remove(int index) 是截然不同的方法。

53. 慎用可变参数

每次调用可变参数方法都会导致一次数组分配和初始化,这会影响性能。

54. 返回零长度的数组或者集合,而不是 null

如果返回 null,客户端需要对 null 进行处理。

55. 谨慎返回 Optional

如果无法返回结果并且当没有返回结果时客户端必须执行特殊的处理,那么就应该声明该方法返回 Optional。

56. 为所有导出的 API 元素编写文档注释

方法的文档注释应该简洁地描述出它和客户端之间的约定,重点是这个约定说明这个方法做了什么,而不是说明它是如何完成的。

第九章 通用编程

57. 将局部变量的作用域最小化

第 57 条与 第 15 条在本质上是类似的,将局部变量的作用域最小化,可以增强代码的可读性和维护性,并降低出错的可能性。

要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方进行声明。几乎每一个局部变量的声明都应该包含一个初始化表达式,即声明变量的同时要初始化。但也有例外,如 try-catch,如果希望在 catch 语句块使用 try 语句块中的变量,则需要提前声明。

循环提供了特殊的机会来将变量的作用域最小化,foreach 循环和普通 for 循环都允许声明循环变量,它们的作用域被限定在正好需要的范围之内,而 while + 迭代器则做不到,由此引发出错的问题可能很难发现,因此 foreach 和 for 都优于 while。

// 使用 foreach【推荐】
for(Element e : collect) {
  ...
}

// 使用普通 for
for(Iterator<Element> i = list.iterator(); i.hasNext(); ) {
  Element e = i.next();
  ...
}

// 使用 while + 迭代器,变量 i 的作用域过大【避免】
Iterator<Element> i = list.iterator();
while(i.hasNext()) {
  Element e = i.next();
  ...
}

// 另一个while + 迭代器,由于复制-粘贴使用了旧的变量,这在编译时无法发现
Iterator<Element> i2 = list.iterator();
while(i.hasNext()) {
  Element e = i2.next();
  ...
}

58. for-each 循环由于传统的 for 循环

foreach 循环是增强的 for 语句,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。注意,使用 foreach 循环并不会有性能上的损失,它是 Java 5 引进的新特性,本质上还是使用迭代器。

有三种常见情况无法使用 foreach 循环:

  • 解析过滤

    遍历集合时删除选定的元素,需要使用显示的迭代器,以便使用它的 remove 方法,如果是 Java 8,则可以使用 Collection 的 removeIf 方法,从而避免显示的遍历。

  • 转换

    遍历列表或者数组时,如果要操作元素,需要列表的迭代器或者数组索引 2,以便设置元素的值。

  • 平行迭代

    如果需要并行的遍历多个集合,就需要显示地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以同步前进。

59. 了解和使用类库

通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。

从经济的角度分析表明,类库代码受到的关注远远超过大多数普通程序员在同样的功能上所能给与的投入。

标准类库很庞大,但每个程序员都应该熟悉以下包及其子包:

  • java.lang
  • java.util
  • java.io

60. 如果需要精确的答案,请避免使用 float 和 double

float 和 double 是用于浮点运算的,为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的,因此不适用于货币计算。

如果要进行货币计算,请使用 Bigdecimal。

如果在数值范围没有超过 9 位(int)或者 18 位 (long),可以将货币数值转成最小的货币单位后,再用 int 或者 long 进行计算。

61. 基本类型优先于装箱基本类型

基本类型和装箱基本类型有三点区别,如果不注意都会陷入麻烦:

  • 基本类型只有值,而装箱基本类型除了有值还是一个对象

    对装箱基本类型运用 == 操作符几乎总是错的(== 比较的是两个对象的引用)

  • 基本类型不能为 null,而装箱基本类型允许为 null

    当在一项操作中混用基本类型和装箱基本类型时,装箱基本类型会自动拆箱,进而引发 NPE

  • 基本类型通常比装箱基本类型更节省时间和空间

    参考第 6 条 避免创建不必要的对象

62. 如果其他类型更适合,则尽量避免使用字符串

不适合场景:

  • 字符串不适合代替代替其他的值类型,如文件

  • 字符串不适合代替枚举类型

  • 字符串不适合代替聚合类型

    // 希望用特殊字符作为分隔符,这样不仅繁琐且容易出错,str1 或者 str2 也可能包含了这个分隔符
    // 正确的做法应该编写一个类来描述这个数据集
    String key = str1 + "#" str2;
    
  • 字符串也不适合代替能力表

63. 了解字符串连接的性能

当两个字符串被连接在一起时,它们的内容都要被拷贝,需要创建新的对象。因此如果存在频繁的字符串拼接,请使用 StringBuffer 或者 StringBuilder 来代替 String。

64. 通过接口引用对象

用接口作为类型,程序会更灵活,如果要更换实现,只需要更改构造器的名称。

// 正例
List<String> list = new ArrayList<>();
// 反例
ArrayList<String> list = new ArrayList<>();

65. 接口优先于反射机制

66. 谨慎地使用本地方法

67. 谨慎地进行优化

不要为了性能而牺牲合理的结构,要努力编写好的程序而不是快的程序。

在每次试图做优化之前和之后,要对性能进行测量。

68. 遵守普遍接受的命名惯例

对于首字母缩写,有一种特殊情况,如果连续出现多个首字母缩写,如 HTTPURL,而使用 HttpUrl 则看起来更清晰 ,类似的还有 XxDto。

不可实例化的类通常是复数形式,如 Collections。

用 able 或者 ible 结尾的形容词来命名类,表示这种类具备某种能力,如 Runnable

第十章 异常

69. 只针对异常的情况下才使用异常

异常应该只用于异常的情况下,他们永远不应该用于正常的控制流。

对 API 来说,设计良好的 API 不应该强迫它的客户端为了正常的控制流而使用异常。

70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常

如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常。

用运行时异常(未受检异常)来表明编程错误,如果是自己实现的自定义异常,应该都是 RuntimeException 的子类。

71. 非必要情况下避免使用受检异常

虽然受检异常可以提升程序的可读性,但过渡使用会使 API 使用起来非常痛苦。

72. 优先使用标准的异常

下表概括了最常见的可重用异常

异常使用场合
IllegalArgumentException非 null 的参数值不正确
IllegalStateException不适合方法调用的状态
NullPointerException在禁止使用 null 的情况下参数为 null
IndexOutOfBoundsException下标参数值越界
ConcurrentModificationException在禁止并发修改的情况下,检测到对象的并发修改
UnsupportedOperationException对象不支持用户请求的方法

73. 抛出与抽象对应的异常

如果方法抛出的异常与它所执行的任务没有明显的关系,这种情形将会使人不知所措。当方法传递由低层抽象抛出的异常时,往往会发生这种情况。为了避免这个问题,更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法称为异常转译。

// exception translation
try {
  // do something
} catch (LowerLevelException e) {
  throw new HignerLevelException(...);
}

74. 每个方法抛出的所有异常都要建立文档

始终要单独地声明受检异常,并且利用 Javadoc 的 @throws 标签,准确地记录下抛出每个异常的条件。

75. 在细节消息中包含失败-捕获信息

76. 努力使失败保持原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。失败原子性对于受检异常而言尤为重要,因为调用者期望能从这种异常中进行恢复。

77. 不要忽略异常

如空的 catch 块会使异常达不到应有的目的。但如果选择忽略异常,catch 块中应该包含一条注释,说明为什么可以这么做,并且将异常的变量命令为 ignored。

第十一章 并发

78. 同步访问共享的可变数据

同步的意义:同步不仅可以阻止一个线程看到对象对于不一致的状态之中,还可以保证一个线程的所做的修改能被其他线程看到。

可变数据最好避免共享,如果要同步访问共享的可变数据,可以用 volatile、synchronized 修饰。

79. 避免过度同步

80. executor、task、stream 优于线程

使用 Executor 框架使用多线程,而不是自行创建线程。

在 Executor Framework 中,工作单元(task)和执行机制是分开的。task 有两种:Runnable、Clallable(有返回值)

并行 stream 是在 fork-join 池上编写的。

81. 并发工具优先于 wait 和 notify

java.util.concurrent 中更高级的工具分成三类:

  • Execuor Framework

  • 并发集合(Concurrent Collection)

    并发集合为标准的集合接口提供了高性能的并发实现,这些实现现在内部自己管理同步。应该使用并发集合代替同步集合。

  • 同步器(Synchronizer)

    同步器是使线程能够等待另一个线程的对象,允许它们协调协作。

    最常用的同步器是 CountDownLatch(倒计数锁存器)和 Semaphore,功能最强大的同步器是 Phaser,较不常用的同步器有 Cyclicbarrier 和 Exchanger。

82. 线程安全性的文档化

在一个方法声明中出现 synchronized 修饰符,这是个实现细节,并不是导出的 API 的一部分,它并不一定表明这个方法就是线程安全的。

一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全级别。以下列出几种常见的级别:

  • 不可变的(immutable)

    这个类的实例是不变的,所以不需要外部的同步,如 String、Long

  • 无条件的线程安全(unconditionally thread-safe)

    这个类的实例是可变的,但是内部有同步,所以不需要外部的同步,如 AtomicLong、ConcurrentHashMap

  • 有条件的线程安全(conditionally thread-safe)

    除了有部分方法需要外部同步之外,这种线程安全级别与无条件的线程安全相同,如 Collection.synchronizedMap,文档描述了它们的迭代器要求外部同步。

  • 非线程安全(not thread-safe)

    客户端必须利用自己选择的外部同步包围每个方法的调用,如 HashMap

  • 线程对立的(thread-hostile)

    即使客户端利用自己选择的外部同步包围每个方法的调用,线程也不安全,线程对立的根源在于没有同步地修改静态数据。

83. 慎用延迟初始化

84. 不要依赖于线程调度器

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。

第十二章 序列化

这个章节不知道有啥好记录的。。。可能层次还没到,不能理解

85. 其他方法优先于 Java 序列化

86. 谨慎地实现 Serializable 接口

87. 考虑使用自定义的序列化形式

88. 保护性地编写 readObject 方法

89. 对于实例控制,枚举类型优于 readResolve

90. 考虑用序列化代理代替序列化实例

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火车站卖橘子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值