EffectiveJava笔记03

9. 通用编程

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

在第一次使用它的地方进行声明。局部变量的作用域从它被声明的点开始扩展,到 外围块的结束。如果变量在“使用它的块”之外被声明,当程序退出该块之后,该变量仍然可见。如果变量在目标使用区域之前或之后意外使用,后果是灾难性的。

循环中提供特殊的机会使变量的作用域最小化。可以声明循环变量(loop variable),for循环优先于while循环。

最后一种是使方法小而集中。每个操作用一个方法完成。

58. for-each循环优于传统for循环

官方称为增强for循环,可以解决所有问题。完全隐藏迭代器或索引变量,避免混乱和出错的可能。同样适合于集合和数组,也简化了将容器的实现类型从一种转换到另一种的过程。

对于嵌套for循环,优势更加明显。

for(Suit suit: suits)
  for(Rank rank: ranks)
    deck.add(new Card(suit,rank));

有三种情况不能使用for-each

  • 解构过滤 如果需要遍历集合,并删除选定的元素,需要使用显式的迭代器,以便调用remove方法,java8的Collection的removeIf方法,可避免显式的遍历。
  • 转换 如果需要遍历列表和数组,并取代它的部分或全部元素值,需要列表迭代器或数组索引,以便设定元素的值。
  • 平行迭代 如果需要并行的遍历多个结,需要显式的控制迭代器或索引变量,以便所以迭代器或索引变量都能同步前进。

如果不得不从头开始编写自己的Iterator实现,如表示一组元素的类型,应坚决实现Iterator接口,这样用户可以用for-each循环遍历类型。

59. 了解和使用类库

假设你希望产生位于0和某个上界之间的随机整数,要考虑 使用Random.nextInt(int),无需自己设计,充分利用专家的指示和使用经验。

从java7开始,选择随机数生成器时,大多数使用ThreadLocalRandom。会产生更高质量的随机数,且速度更快。对于Fork Join Pool和并行Stream,使用SplittableRandom。

扫描二维码关注公众号,回复: 6780324 查看本文章
ThreadLocalRandom.current().nextInt(int);

使用标准类库的第二个好处是,不必纠结底层细节。

第三个好处是,性能会随着时间推移不断提高,因为被当做工业标准用,有足够动力提高性能。

第四个好处是,会随着时间推移增加新的功能。

最后一个好处是,使自己的代码融入主流,更易读、易维护、易被大多数开发人员重用。

每当java平台有重要的发行时,都会发布一个网页说明新特性,如java8-feat。

每个程序员都应熟悉java.lang、java.util、java.io以及子包的内容。

如果需求越特殊,有可能类库工具不满足你的需求,如果在java类库中找不到需要的功能,下一个选择应该是在高级的第三方类库中寻找,如Google的开源Guava类库。

总之,不要重复造轮子。从经济学角度,类库代码受到的关注远超多数普通程序员在同样的功能上所能给予的投入。

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

不适合货币计算。解决方案是用BigDecimal、int或long进行货币计算。

BigDecima两个缺点:不方便,速度慢。好处是允许你完全控制舍入,可从8个舍入模式中选择一种。商务运算非常方便,如果性能非常关键,且不介意自己处理十进制小数点,涉及的数值又不太大,使用int或long,不超过9位使用int,不超过18位使用long,可能超过18位用BigDecimal

 public static void main(String[] args) {
        final BigDecimal TEN_CENTS = new BigDecimal(".10");
        int itemsBought = 0;
        BigDecimal funds = new BigDecimal("1.00");
        for (BigDecimal price = TEN_CENTS;
                funds.compareTo(price)>=0;
                price = price.add(TEN_CENTS)){
            funds = funds.subtract(price);
            itemsBought++;
        }
        System.out.println(itemsBought+" items bought.");
        System.out.println("Money left over: $"+funds);
    }

上面的案例使用BigDecimal类型代替double,使用BigDecimal的String构造器,而不是用double构造器。避免将不正确的值引入计算中。结果发现可以支付4颗糖果,还剩0.00元。

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

更加简单,更快,风险也小。

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

不适合代替其他的值类型。

不适合代替枚举类型。

不适合代替聚合类型。如果一个实体有多个组件,用字符串表示该实体不合适,更好的做法是,简单的编写一个类描述这个数据集,通常是一个私有的静态成员类。

字符串也不适合代替能力表(capabilities)。有时候,字符串被用于对某种功能进行授权访问。如考虑设计一个提供线程局部变量的机制,其实1.2以后就有了ThreadLocal类,线程安全

public final class ThreadLocal<T>{
  public ThreadLocal();
  public void set(T value);
  public T get();
}

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

为了连接n个字符串而重复的使用字符串连接操作,需要n的平方级的时间。

使用StringBuilder代替String来获取可接受的性能。

64. 通过接口引用对象

程序更加灵活,如果没有合适的接口,用类层次结构中提供了必要功能的最小的具体类引用对象。

65. 接口优先于反射机制

核心反射机制(core reflection facility),java.lang.reflect包,提供“通过程序访问任意类”的能力。给定一个Class对象,可获得Constructor、Method、Field实例,分别代表该Class实例所表示的类的构造器、方法和域。这些对象提供了“通过程序访问类的成员名称、域类型、方法签名等信息”的能力

Method.invoke可以调用任何类的任何对象上的任何方法(遵从常规的安全限制)。反射机制允许一个类使用另一个类,即使当前者被编译时后者还不存在。

代价是:

  • 损失了编译时类型检查的优势,包括异常检查
  • 执行反射访问所需的代码非常笨拙和冗长,阅读性差
  • 性能损失,慢很多

反射非常强大,对特定的复杂系统编程任务,非常必要,但也有缺点。如果你编写的程序必须要与编译时未知的类一起工作,就应该仅仅使用反射机制实例化对象,而访问对象时则使用编译时已知的某个接口或超类。

66. 谨慎使用本地方法

Java Native Interface(JNI)允许java程序调用本地方法(native method),也就是用本地编程语言(如C或者C++)编写的方法。提供“访问特定于平台的机制”的能力,如访问注册表(registry)。访问本地遗留代码库的能力,编写注重性能的部分。

随着java平台的成熟,提供越来越多以前只有宿主平台才有的特性。如java9增加的进程API,提供访问操作系统进程的能力。

本地方法的缺陷是不安全。与平台相关,不可自由移植。此外,垃圾回收不是自动的,甚至无法追踪本地内存使用情况。

67. 谨慎优化

要努力编写好的程序而非快的程序。

在设计过程中就要考虑性能问题

要努力避免那些限制性能的设计决策 最难更改的是制定了模块之间交互关系以及模块和外界交互关系的组件。最主要的是API、交互层(wire-level)协议及永久数据格式。这些设计组件不仅在事后难以改变,且可能对系统本该达到的性能产生严重的限制。

要考虑API设计决策的性能后果 使公有的类型成为可变的,可能会导致大量不必要的保护性拷贝,见50条。同样,在适合使用复合模式的公有类中使用继承,会将该类和它的超类永远束缚在一起,人为的限制子类的性能。最后一个例子是,在API中使用实现类型而非接口,会将你束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用。如第64条。

API设计对性能影响非常实际。以java.awt.Component类中的getSize方法为例。这个注重性能的方法将返回Dimension实例。与此密切相关的决定是Dimension的实例是可变的,迫使该方法的任何实现都必须为每个调用分配一个新的Dimension实例。分配数百万个不必要的小对象仍然损害性能。

替换方案:理想情况是Dimension不可变;另一种是用两个方法代替getSize,分别返回Dimension对象的单个基本组件。java2就已经有两个这样的方法加入到Component API中。

每次试图优化前后都要对性能进行测量。要猜出程序把时间花在哪些地方并不容易,你认为程序慢的地方可能并没有问题性能剖析工具可以提供运行时的信息。另一种工具是jmh,是微基准测试框架(microbenchmarking framework),提供非并行的可见java代码性能详情的能力。

java程序设计语言没有很强的性能模型(performance model),各种基本操作的相对开销也没有明确定义。程序员编写的代码和CPU执行的代码存在“语义沟”(semantic gap)。

第一个步骤是检查所选择的算法,再多的底层优化也无法弥补算法的选择不当。

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

分为两种:字面的(typographical)和语法的(grammatical)

不可实例化的工具类通常复数名词命名,如Collections。接口命名和类相似如Collection,或以able或ible结尾的形容词命名,如Runnable、Iterable或Accessible。

对于返回boolean值得方法,通常is开头,很少用has,如isDigit、isEmpty

java beans规范中get开头的形式形成了早期的可重用组件架构的基础。getAttribute和setAttribute

有些方法的名称值得专门提及。转换对象类型的实例方法,返回不同类型的独立对象的方法,经常被称为toType,如toString或toArray。返回视图view(详见第6条)的方法经常被称为asType,如asList。返回一个与被调用对象同值的基本类型的方法,经常被称为typeValue,如intValue。静态工厂的第1条讲过。

10. 异常

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

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

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

可恢复的抛出受检异常;程序错误的,抛出运行时异常;不确定是否可恢复,抛出未受检异常。

71. 避免不必要使用受检异常

72. 优先使用标准异常

最经常被重用的是IllegalArgumentException,参数不合适抛出

另一个经常的是IllegalStateException。因为接收对象的状态而使调用非法,抛出该异常,如某个对象尚未正确初始化,就想调用该对象。

当然特定情况,如不允许null值得参数中传递null值,抛出NullPointerException。序列下标的参数传递越界的值,抛出IndexOutOfBoundsException异常

另一个通用异常是ConcurrentModificationException。

最后一个是UnsupportedOperationException,对象不支持所请求的操作,抛出该异常。

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

更高层的实现应该捕获低层的异常,同时抛出可以按高层抽象进行解释的异常。称为异常转译(exception translation)。一种特殊的异常转译称为异常链(exception chaining)。

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

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

不要在细节消息中包含密码、密钥以及类似的信息

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

失败的方法调用应该使对象保持在被调用之前的状态。称为失败原子性(failure atomic)。

一种类似的获得失败原子性的方法是,调整计算处理过程的顺序,使得任何可能失败的计算部分都在对象状态被修改之前发生。

另一种是,在对象的一份临时拷贝上执行操作,操作完成后再用临时拷贝中的结果代替对象的内容。

如果违反条例,API文档应清楚的表明对象将处于什么状态。遗憾的是,大量API文档都未能做到这一点。

77. 不要忽略异常

11. 并发

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

为了线程间进行可靠的通信,也为了互斥访问,同步是必要的

千万不要用Thread.stop()方法,本质上不安全。

多个线程共享可变数据时,每个读或写数据的线程必须执行同步。

79. 避免过度同步

如果编写一个可变的类,有两种选择:省略所有的同步,如果想要并发使用,就允许客户端在必要的时候从外部同步,或者通过内部同步,使这个类变成线程安全的(见82条),还可以因此获得明显比外部锁定整个对象更高的并发性。java.util中的集合采用前一种,而java.util.concurrent采用后一种。

总之,尽量将同步区域内部的工作量限制到最小。

80. executor、task和stream优先于线程

java平台的java.util.concurrent包包含Executor Framework,是个很灵活的基于接口的任务执行工具,创建了非常好用的工作队列,只需一行代码

ExecutorService exec = Executors.newSingleThreadExecutor();

下面是为执行而提交一个runnable方法

exec.execute(runnable);

下面是告诉executor如何优雅的终止(如果没这么做,虚拟机可能不会退出):

exec.shutdown();

可以利用executor service完成更多的工作。如可以等待完成一项特殊的任务,可以等待一个任务集合中的任何任务或所有任务完成(利用invokeAny或invokeAll),可等待executor service优雅的完成终止(利用awaitTermination方法),可在任务完成时逐个的获取这些任务的结果(利用ExecutorCompletionService),可调度在某个特殊的时间段定时运行或者阶段性的运行的任务(利用ScheduledThreadPoolExecutor)等。

如果想用线程池,可直接操作ThreadPoolExecutor类

如果编写小程序或轻量负载的服务器,用Executors.newCachedThreadPool是个不错的选择。对大负载的服务器,最好使用Executors.newFixedThreadPool。

在Executor Framework中,工作单元和执行机制是分开的。关键的抽象是工作单元,称为任务task,有两种Runnable和近亲Callable(类似,可返回值,抛出任意异常)。执行任务的通用机制是executor service。本质上讲,Executor Framework的工作是执行,Collections Framework的工作是聚合(aggregation)。

java7中Executor Framework得到扩展,支持fork-join任务,通过称作fork-join池的特殊executor服务运行。fork-join任务用ForkJoinTask实例表示,可被分为更小的子任务,包含ForkJoinPool的线程不仅要处理这些任务,还要从另一个线程中“偷”任务,以确保所有的线程保持忙碌。从而提高CPU使用率,提高吞吐量,降低延迟。

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

concurrent包中更高级的工具分为三类:Executor Framework、并发集合(Concurrent Collection)以及同步器(Synchronizer)。

并发集合为标准的集合接口如List、Queue和Map提供了高性能的并发实现。在内部自己管理同步。应该优先使用ConcurrentHashMap,而不是Collections.synchronizedMap。

有些集合接口已经通过阻塞操作(blocking operation)进行扩展,会一直阻塞到成功执行为止。如BlockingQueue扩展Queue接口,添加了take在内的方法。这样允许将阻塞队列用于工作队列(work queue),也叫生产者-消费者队列。大多数ExecutorService实现(包括ThreadPoolExecutor)都使用了一个BlockingQueue。

同步器(synchronizer)是使线程能够等待另一个线程的对象,允许他们协调动作。最常用的是CountDownLatch和Semaphore。较不常用的是CyclicBarrier和Exchanger。功能最强大的是Phaser。

倒计数锁存器(CountDown Latch)是一次性的障碍,允许一个或多个线程等待一个或多个其他线程做某些事情。唯一的构造器带有一个int类型的参数。

TODO

如果用wait,应始终用wait循环模式来调用wait方法,不要在循环之外调用wait。

没有理由在新代码中用wait方法和notify方法,即使有,也很少。

82. 线程安全的文档化

一个类为了可被多个线程安全调用,必须在文档中说明线程安全级别:

  • 不可变的 immutable 如String、Long和BigInteger
  • 无条件的线程安全 unconditionally thread-safe,实例可变,但有足够的内部同步,实例可被并发使用,无需外部同步。如AtomicLong和ConcurrentHashMap
  • 有条件的线程安全 conditionally thread-safe,除了有些方法为了安全的并发使用需要外部同步之外,级别与无条件的相同。如Collections.synchronized包装返回的集合,迭代器要求外部同步
  • 非线程安全 not thread-safe 实例可变,客户端需要自己选择外部同步包围每个方法调用(或调用序列)。通用的集合实现如ArrayList和HashMap
  • 线程对立 thread-hostile 不能安全的被多线程使用,即使所有的方法调用被外部同步包围,一般没人会有意的写线程对立的类

lock域应该始终声明为final。

83. 慎用延迟初始化

大多数情况下,正常的初始化优先于延迟初始化(lazy initialization)。

如果出于性能考虑需要对静态域使用延迟初始化,使用lazy initializaiton holder class模式。保证类被用的时候初始化

private static class FieldHolder {
  static final FieldType field = computeFieldValue();
}
private static FieldType getField(){ return FieldHolder.field; }

当getField第一次被调用,第一次读取FieldHolder.field,导致FieldHolder类得到初始化。好处是,getField方法没有被同步,且只执行一个域访问,实际上并未增加访问成本。

出于性能考虑对实例域使用延迟初始化,使用双重检查模式(double-check idiom)。

private volatile FieldType field;
private FieldType getField(){
  FieldType result = field;
  if(result == null){ // first check,not locking
    synchronized (this){
      if(field == null){ // second check, with locking
        field = result = computeFieldValue();
      }
    }
  }
  return result;
}

域需要被声明为volatile。对需要用到局部变量result可能不解。作用是确保field只在被初始化的情况下读取一次。提升性能。

虽然可以对静态域双检锁,但是没理由这么做,因为lazy initialization holder class idiom是更好的选择

双重检查模式的两个变量值得一提。有时需要延迟初始化一个可接受重复初始化的实例域。可使用双重检查模式的一个变量,负责分配第二次检查,称单检查模式,注意field仍为volatile

private volatile FieldType field;
private FieldType getField(){
  FieldType result = field;
  if(result == null){    
     field = result = computeFieldValue();
  }
  return result;
}

总之,大多数域应该正常初始化,而不是延迟初始化。如果为达到性能,或破坏有害的初始化循环,必须用延迟初始化一个域,使用相应的方法。对实例域,双重检查模式(double-check idiom),静态域,使用lazy initialization holder class idiom,可接受重复初始化的实例域,可考虑单检查模式(single-check idiom)

84. 不要依赖线程调度器

不要企图通过调用Thread.yield来“修正”该程序

依赖线程调度器,程序既不健壮,也不具有可移植性。

12. 序列化

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

为了避免java序列化的风险,有许多其他机制可完成对象和字节序列的转化,同时还带来如跨平台支持、高性能、一个大型的工具生态系统和一个广阔的专家社区。称为跨平台的结构化数据表示法(cross-platform structured-data representation)。避免和序列化混淆。

这些表示法的共同点是,远比java序列化简单的多。最前沿的是json和谷歌的protocal buffers,也叫protobuf。

json基于文本,protobuf是二进制的。

最好永远不要反序列化不被信任的数据。

如果无法避免序列化,又不能绝对确保被反序列化的数据的安全性,利用java9的新增的对象反序列化过滤(object deserialization filtering),已经移植到java的早期版本(java.io.ObjectInputFilter)。可在数据流被反序列化之前,定义一个过滤器。操作类的粒度。默认接受类,同时拒绝可能存在危险的黑名单(blacklisting);默认拒绝类,同时接受假定安全的白名单(whitelisting)。白名单优于黑名单,因为黑名单只能抵御已知的攻击。有个工具叫SWAT(Serial Whitelist Application Trainer),可自定替应用准备好白名单

如果重新设计系统,一定要用跨平台的结构化数据表示法代替,如json或protobuf。

86. 谨慎实现Serializable接口

实现该接口的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。它的字节流编码变成了它的导出的API的一部分。

该限制的一个例子是和流的唯一标识符(stream unique identifier)有关,通常称为序列版本UID(serial version UID)

第二个代价是,增加了出现bug和安全漏洞的可能性

第三个代价是,随着类发行新版本,相关的测试负担也会增加

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

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/94983344