分布式系统设计策略

在分布式环境下,有几个问题是普遍关心的,我们称之为设计策略:

如何检测当前节点还活着?
如何保障高可用?
容错处理
负载均衡

一、心跳检测

没有检测到心跳的时候,不代表节点死亡,可能是忙碌中。

通过下面两种方式来检测:

  • 周期检测心跳机制
  • 累计失效检测机制

周期检测心跳机制

Server端每间隔 t 秒向Node集群发起监测请求,设定超时时间,如果超过超时时间,则判断“死亡”。

累计失效检测机制

在周期检测心跳机制的基础上,统计一定周期内节点的返回情况(包括超时及正确返回),以此计算节点的“死亡”概率。另外,对于宣告“濒临死亡”的节点可以发起有限次数的重试,以作进一步判断。

通过周期检测心跳机制、累计失效检测机制可以帮助判断节点是否“死亡”,如果判断“死亡”,可以把该节点踢出集群

二、高可用设计

系统高可用性的常用设计模式包括三种:主备(Master-SLave)、互备(Active-Active)和集群(Cluster)模式。

1.主备模式

当主机宕机时,备机接管主机的一切工作

2.互备模式

互备模式指两台主机同时运行各自的服务工作且相互监测情况

3.集群模式

多个节点在运行,同时可以通过主控节点分担服务请求

三、容错性

容错的处理是保障分布式环境下相应系统的高可用或者健壮性,典型的案例就是对于缓存穿透问题的解决方案

我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据 库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,或者有人恶意攻击

如频繁发起为id为“-1”的条件进行查询,可能DB就挂掉了。

那这种问题有什么好办法解决呢?

一个比较巧妙的方法是,可以将这个不存在的key预先设定一个值。比如,key=“null”。在返回这个null值的时候, 我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是null,则可以认为这时候key有值 了,从而避免了透传到数据库,把大量的类似请求挡在了缓存之中。

四、负载均衡


分布式算法

一、分布式理论:一致性算法 Paxos

1. Paxos解决了什么问题?

Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致

2. 概念

提案proposal

  • 提案编号(proposal ID)
  • 提案的值(value)

Paxos的三种角色

  • Proposer提案人
  • Acceptor决策者
  • Learners终决策的学习者 (就是最终将决策完的value,落实到下来。到物理机)

3、Paxos的流程

提案要求

对于任意的Mn和Vn,如果提案[Mn,Vn]被提出,那么肯定存在一个由半数以上的Acceptor组成的集合S,满足以下两个条件中的任意一个:

  • 要么S中每个Acceptor都没有接受过编号小于Mn的提案。
  • 要么S中所有Acceptor批准的所有编号小于Mn的提案中,编号大的那个提案的value值为Vn

proposer生成提案

第一,Proposer选择一个新的提案编号N,然后向某个Acceptor集合(半数以上)发送请求,要求该集合中的每个 Acceptor做出如下响应(response)

(a)Acceptor向Proposer承诺保证不再接受任何编号小于N的提案。

(b)如果Acceptor已经接受过提案,那么就向Proposer反馈已经接受过的编号小于N的,但为大编号的提案的值

我们将该请求称为编号为N的Prepare请求

第二,如果Proposer收到了半数以上的Acceptor的响应,那么它就可以生成编号为N,Value为V的提案[N,V]。这里的V是所有的响应中编号大的提案的Value。如果所有的响应中都没有提案,那 么此时V就可以由Proposer 自己选择。

生成提案后,Proposer将该提案发送给半数以上的Acceptor集合,并期望这些Acceptor能接受该提案。我们称该请求为Accept请求

accept接受提案

一个Acceptor可能会受到来自Proposer的两种请求,分别是Prepare请求和Accept请求,对这两类请求作出响应的条件分别如

  • Prepare请求:Acceptor可以在任何时候响应一个Prepare请求
  • Accept请求:在不违背Accept现有承诺的前提下,可以任意响应Accept请求

算法优化

Acceptor忽略编号小于当前最大变好的Prepare请求

1608776443408

4. Learner学习被选定value

方案一:

Learner获取一个已经被选定的提案的前提是,该提案已经被半数以上的Acceptor批准,因此,简单的做法就是一旦Acceptor批准了一个提案,就将该提案发送给所有的Learner

很显然,这种做法虽然可以让Learner尽快地获取被选定的提案,但是却需要让每个Acceptor与所有的Learner逐个进行一次通信,通信的次数至少为二者个数的乘积

方案二:

另一种可行的方案是,我们可以让所有的Acceptor将它们对提案的批准情况,统一发送给一个特定的Learner(称为主Learner), 各个Learner之间可以通过消息通信来互相感知提案的选定情况,基于这样的前提,当主Learner被通知一个提案已经被选定时,它会负责通知其他的learner

在这种方案中,Acceptor首先会将得到批准的提案发送给主Learner,再由其同步给其他Learner。因此较方案一而言,方案二虽然需要多一个步骤才能将提案通知到所有的learner,但其通信次数却大大减少了,通常只是 Acceptor和Learner的个数总和,但同时,该方案引入了一个新的不稳定因素:主Learner随时可能出现故障

方案三:

在讲解方案二的时候,我们提到,方案二大的问题在于主Learner存在单点问题,即主Learner随时可能出现故 障,因此,对方案二进行改进,可以将主Learner的范围扩大,即Acceptor可以将批准的提案发送给一个特定的 Learner集合,该集合中每个Learner都可以在一个提案被选定后通知其他的Learner。这个Learner集合中的 Learner个数越多,可靠性就越好,但同时网络通信的复杂度也就越高

5. 如何保障Paxos算法的活性

假设存在这样一种极端情况,有两个Proposer依次提出了一系列编号递增的提案,导致终陷入死循环,没有 value被选定

解决:通过选取主Proposer,并规定只有主Proposer才能提出议案。这样一来只要主Proposer和过半的Acceptor 能够正常进行网络通信,那么但凡主Proposer提出一个编号更高的提案,该提案终将会被批准,这样通过选择一个主Proposer,整套Paxos算法就能够保持活性

三、分布式理论:一致性算法 Raft

概念

Raft 是一种为了管理复制日志的一致性算法。

Raft将一致性算法分解成了3模块

  1. 领导人选举
  2. 日志复制
  3. 安全性

领导人角色

  • 领导者(leader):处理客户端交互,日志复制等动作,一般一次只有一个领导者
  • 候选者(candidate):候选者就是在选举过程中提名自己的实体,一旦选举成功,则成为领导者
  • 跟随者(follower):类似选民,完全被动的角色,这样的服务器等待被通知投票

节点异常

  • leader不可用
  • follower不可用
  • 多个candidate或多个leader
  • 新节点加入集群

异常的解决

1. leader 不可用;

  • 一般情况下,leader 节点定时发送 heartbeat 到 follower 节点。
  • 由于某些异常导致 leader 不再发送 heartbeat ,或 follower 无法收到 heartbeat 。
  • 当某一 follower 发生 election timeout 时,其状态变更为 candidate,并向其他 follower发起投票。
  • 当超过半数的 follower 接受投票后,这一节点将成为新的 leader,leader 的步进数加1并开始向follower同步日志
  • 当一段时间之后,如果之前的 leader 再次加入集群,则两个 leader 比较彼此的步进数,步进数低的leader将切换自己的状态为follower。
  • 较早前leader中不一致的日志将被清除,并与现有 leader中的日志保持一致。

2. follower 不可用;

  • 集群中的某个 follower 节点发生异常,不再同步日志以及接收 heartbeat。
  • 经过一段时间之后,原来的 follower 节点重新加入集群。
  • 这一节点的日志将从当时的 leader 处同步。

3. 多个 candidate 或多个 leader;

  • 初始状态下集群中的所有节点都处于 follower 状态。
  • 两个节点同时成为 candidate 发起选举。
  • 两个 candidate 都只得到了少部分 follower 的接受投票。
  • candidate 继续向其他的 follower 询问。
  • 由于一些 follower 已经投过票了,所以均返回拒绝接受。
  • candidate 也可能向一个 candidate 询问投票。
  • 在步进数相同的情况下,candidate 将拒绝接受另一个 candidate 的请求。
  • 由于第一次未选出 leader,candidate 将随机选择一个等待间隔(150ms ~ 300ms)再次发起投 票。
  • 如果得到集群中半数以上的 follower 的接受,这一 candidate 将成为 leader。
  • 稍后另一个 candidate 也将再次发起投票。
  • 由于集群中已经选出 leader,candidate 将收到拒绝接受的投票。
  • 在被多数节点拒绝之后,并已知集群中已存在 leader 后,这一 candidate 节点将终止投票请求、切换为 follower,从 leader 节点同步日志。

日志复制过程

  • 客户端的每一个请求都包含被复制状态机执行的指令。
  • leader把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。
  • 跟随者响应ACK,如果 follower 宕机或者运行缓慢或者丢包,leader会不断的重试,直到所有的 follower 终都复制了所有的日志条目。
  • 通知所有的Follower提交日志,同时领导人提交这条日志到自己的状态机中,并返回给客户端

分布式定理

1. 分布式理论:CAP定理

CAP 理论含义是,一个分布式系统不可能同时满足一致性(C:Consistency),可用性(A: Availability)和分区容错 性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的2个。

选项 描述
C 一致性 分布式系统当中的一致性指的是所有节点的数据一致,或者说是所有副本的数据一致
A 可用性 Reads and writes always succeed. 也就是说系统一直可用,而且服务一直保持正常
P 分区容错性 系统在遇到一些节点或者网络分区故障的时候,仍然能够提供满足一致性和可用性的服务

C - 如何实现一致性?

  1. 写入主数据库后要数据同步到从数据库
  2. 写入主数据库后,在向从数据库同步期间要将从数据库锁定, 等待同步完成后在释放锁,以免在写新数据后,向从数据
    库查询到旧的数据.

A - 如何实现可用性?

  1. 写入主数据库后要将数据同步到从数据库
  2. 由于要保证数据库的可用性,不可以将数据库中资源锁定
  3. 即使数据还没有同步过来,从数据库也要返回查询数据, 哪怕是旧数据,但不能返回错误和超时

P - 如何实现分区容错性?

  1. 尽量使用异步取代同步操作,举例 使用异步方式将数据从主数据库同步到从数据库, 这样节点之间能有效的实现松耦合;
  2. 添加数据库节点,其中一个从节点挂掉,由其他从节点提供服务

CAP只能3选2

  • 舍弃A(可用性),保留CP(一致性和分区容错性)

    一个系统保证了一致性和分区容错性,舍弃可用性。也就是说在极端情况下,允许出现系统无法访问的情况出现,这个 时候往往会牺牲用户体验,让用户保持等待,一直到系统数据一致了之后,再恢复服务。

  • 舍弃C(一致性),保留AP(可用性和分区容错性)

    这种是大部分的分布式系统的设计,保证高可用和分区容错,但是会牺牲一致性。

  • 舍弃P(分区容错性),保留CA(一致性和可用性)

    如果要舍弃P,那么就是要舍弃分布式系统,CAP也就无从谈起了。可以说P是分布式系统的前提,所以这种情况是不存在

2.分布式理论:BASE 理论

BASE:

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

1.Basically Available(基本可用)

  • 响应时间上的损失:响应时间增加
  • 功能损失:服务降级

2.Soft state(软状态)

允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本之间进行数据同步的过程中存在延迟。

3.Eventually consistent(最终一致性)

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

3.分布式理论:一致性协议 2PC

1. 什么是2PC?

  • 2 两个阶段
  • p 准备阶段
  • c 提交阶段

2. 2PC(二阶段提交)流程

阶段一:准备阶段

  1. 事务询问,协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
  2. 执行事务 (写本地的Undo/Redo日志)
  3. 各参与者向协调者反馈事务询问的响应

阶段二:提交阶段

  1. 发送提交请求:协调者向所有参与者发出 commit 请求。
  2. 事务提交:参与者收到 commit 请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
  3. 反馈事务提交结果:参与者在完成事务提交之后,向协调者发送 Ack 信息。
  4. 完成事务:协调者接收到所有参与者反馈的 Ack 信息后,完成事务。

3. 2PC的缺点

  • 同步阻塞:一个参与者提交阶段,只有等到所有参与者都提交了,才能做其他操作
  • 单点问题:协调者挂了
  • 数据不一致:commit失败
  • 过于保守:参与者挂了,只能通过协调者的策略来处理

4.分布式理论:一致性协议 3PC

3PC (将2PC的“提交事务请求”分为了两步),三阶段引入超时机制。同时在协调者和参与者中都引入超时机制,参与者会在协调者超时后,自动提交事务。

1. 3pc(三阶段提交)过程

阶段一 CanCommit

  • 事务询问
  • 参与者反馈响应

阶段二 PreCommit

  • 发送预提交请求
  • 事务预提交
  • 各参与者反馈结果

阶段三 DoCommit

  • 发送提交请求
  • 事务提交
  • 反馈提交结果
  • 完成事务

2.问题

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

总结:3pc相对于2pc,添加了超时机制,precommit中保障了各个节点状态是一致的。但是,无论是2pc 还是 3pc 都无法完全解决分布式一致性的问题


分布式理论

1. 分布式概念

分布式系统,就是一个业务拆分成多个子业务,分布在不同的服务器节点,共同构成的系统称为分布式系统

分布式与集群的区别

  • 集群:多个人在一起作同样的事 。

  • 分布式 :多个人在一起作不同的事 。

分布式系统的特点

  • 分布性
  • 对等性
  • 并发性
  • 缺乏全局时钟
  • 故障总是会发生

分布式的演变过程

  • 阶段一:单应用架构 (包括应用、数据库都在一起)
  • 阶段二:应用服务器与数据库服务器分离(应用服务器与数据库拆分)
  • 阶段三:应用服务器集群(将应用服务器拆分为集群)
  • 阶段四:应用服务器负载客户
  • 阶段五:数据库读写分离
  • 阶段六:添加搜索引擎缓解读库压力
  • 阶段七:添加缓存机制缓解读库压力
  • 阶段八:数据库水平/垂直拆分
  • 阶段九:应用拆分
  • 阶段十:服务化

2. 分布式面临的问题

1) 通信异常

就是网络情况不好,出现消息丢失和消息延迟等现象

2) 网络分区

因为网络异常,分布式系统中部分节点之间的网络延迟不断增大,最终只有部分节点可以正常通讯,另外的节点不能正常通讯,这个时候就会发生网络分区。这样分布式系统就会出现局部小集群,这就是脑裂问题。小集群回去执行原来整个系统要完成的事情。会产生对分布式一致性的挑战

3) 节点故障

节点故障是分布式系统下另一个比较常见的问题,指的是组成分布式系统的服务器节点出现的宕机或”僵死”现象,根据经验来说,每个节点都有可能出现故障,并且经常发生

4) 三态

三态是,成功,失败,超时。分布式系统中,由于网络是不可靠的,虽然绝大部分情况下,网络通信能够接收到成功或失败的响应,但当网络出现异常的情况下,就会出现超时现象,通常有以下两种情况:

  1. 由于网络原因,该请求并没有被成功的发送到接收方,而是在发送过程就发生了丢失现象。

  2. 该请求成功的被接收方接收后,并进行了处理,但在响应反馈给发送方过程中,发生了消息丢失现象。

3. 分布式理论:一致性

1)分布式一致性

分布式数据一致性,指的是数据在多份副本中存储时,各副本中的数据是一致的。

2)副本一致性

因为分布式系统会存在网络延迟等问题,在副本拷贝时,就会出现同步不一致的问题

3)数据一致性分类

  • 强一致性

    用户要求写什么,读出来就是什么,用户体验好,但实现起来困难

  • 弱一致性

    数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到

  • 最终一致性 (最终一致性就是弱一致性)

  • 读写一致性

    用户读取自己写入结果的一致性,保证用户永远能够第一时间看到自己更新的内容。 比如我们发一条朋友圈,朋友圈的内容是不是第一时间被朋友看见不重要,但是一定要显示在自己的列表上.

    解决方案:

    方案1:一种方案是对于一些特定的内容我们每次都去主库读取。 (问题主库压力大)

    方案2:我们设置一个更新时间窗口,在刚刚更新的一段时间内,我们默认都从主库读取,过了这个窗口之后,我们会挑选最近有过更新的从库进行读取

    方案3:我们直接记录用户更新的时间戳,在请求的时候把这个时间戳带上,凡是最后更新时间小于这个时间戳的从库都不予以响应。

  • 单调读一致性

    本次读到的数据不能比上次读到的旧。

    由于主从节点更新数据的时间不一致,导致用户在不停地刷新的时候,有时候能刷出来,再次刷新之后会发现数据不见了,再刷新又可能再刷出来,就好像遇见灵异事件一样

    解决方案:

    就是根据用户ID计算一个hash值,再通过hash值映射到机器。同一个用户不管怎么刷新,都只会被映射到同 一台机器上。这样就保证了不会读到其他从库的内容,带来用户体验不好的影响。

  • 因果一致性

    指的是:如果节点 A 在更新完某个数据后通知了节点 B,那么节点 B 之后对该数据的访问和修改都是基于 A 更新后的值。于此同时,和节点 A 无因果关系的节点 C 的数据访问则没有这样的限制。

  • 最终一致性

    最终一致性是所有分布式一致性模型当中最弱的。可以认为是没有任何优化的“最”弱一致性,它的意思是说,我不考虑所有的中间状态的影响,只保证当没有新的更新之后,经过一段时间之后,最终系统内所有副本的数据是正确的。 它最大程度上保证了系统的并发能力,也因此,在高并发的场景下,它也是使用最广的一致性模型。


并发

第 78 条:同步访问共享的可变数据

第 79 条:避免过度同步

第 80 条:executor 和 task 优先于线程

  • Executor.newCachedThreadPool:小程序,轻载的服务器
  • Executor.newFixedThreadPool:大负载的服务器
  • ThreadPoolExecutor:最大限度的控制
  • ScheduledThreadPoolExecutor:代替 java.util.Timer

第 81 条:并发工具优先于 wait 和 notify

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

优先使用 ConcurrentHashMap,而不是 Collections.synchronizedMap 或者 Hashtable

最常用的同步器是 CountDownLatch 和 Semaphore,不常用的是 Barrier 和 Exchanger。

对于间歇式定时,应该始终使用 System.nanoTime 而不是 System.cucurrentTimeMills。

第 82 条:线程安全性的文档化

一个类为了可被多个线程安全使用,必须在文档中清楚地说明它所支持的线程安全性级别。

第 83 条:慎用延迟初始化

对于延迟初始化,最好建议“除非绝对必要,否则就不要那么做”。延迟化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。

如果域只是在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。

  • 静态域:lazy initialization holder class 模式。
  • 实例域:双重检查模式。

第 84 条:不要依赖于线程调度器

不要让应用程序的并发性依赖于线程调度器

不要依赖 Thread.yield 和线程优先级


异常

第 69 条:只针对异常的情况才使用异常

如果在 try、catch、finally 块中都抛出了异常,只是只有一个异常可被传播到外界。

请不要在 try 块中发出对 return、break 或 continue 的调用,万一无法避免,一定要确保 finally 的存在不会改变函数的返回值(比如说抛异常、return 以及其他任何引起程序退出的调用)。因为那样会引起流程混乱或返回值不确定,如果有返回值最好在 try 与 finally 外返回。

如果构造器调用的代码需要抛出异常,就不要在构造器处理它,而是直接在构造器声明上 throws 出来

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

第 71 条:避免不必要地使用受检异常

第 72 条:优先使用标准异常

第 73 条:抛出与抽象对象相对应的异常

处理底层异常最好的方法首选是阻止底层异常的发生,如果不能阻止或者处理底层异常时,一般的做法是使用异常转换(包括异常链转换),除非底层方法碰巧可以保证抛出的异常对高层也合适才可以将底层异常直接从底层传播到高层。

第 74 条:每个方法抛出的异常都要有文档描述

如果一个方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这此异常类的某个超类。永远不要声明一个方法“throws Exception”,或者更糟的是声明“throws Throwable”。

要为你编写的每个方法所能摆好出的每个异常建立文档,对于未受检和受检异常,以及对于抽象的和具体的方法也都一样。

第 75 条:异常信息中要包含足够详细的异常细节消息

第 76 条:努力使失败保持原子性

作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态,如果这条规则被违反,则API文档中应该清楚的指明对象将会处于什么样的状态。

第 77 条:不要忽略异常

空的 catch 块至少应该包含一条说明,用来解释为什么忽略这个异常是合适的。


方法

第 49 条:检查参数的有效性

不仅需要检查参数的有效性,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出哪些异常,或导致函数返回哪些错误值等

第 50 条:必要时进行保护性拷贝

如果不能够容忍对象进入数据结构之后发生变,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入到数据结构中。

第 51 条:谨慎设计方法签名

  • 谨慎地选择方法的名称
  • 避免过长的参数列表,如果多于四个了就该考虑重构这个方法了
  • 对于参数类型、要优先使用接口而不是类。
  • 对于 boolean 参数,优先使用两个元素的枚举类型。

第 52 条:慎用重载

对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的。

  1. 安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。
  2. 如果方法使用可变参数,保守的策略是根本不要重载它。
  3. 在 Java 1.5 之后,需要对自动装箱机制保持警惕。

对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。我们应当保证:当传递同样的参数时,所有重载方法的行为必须一致。

第 53 条:慎用可变参数

有的时候在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。

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

private static final Cheese[] EMPTY_CHEESE_ARRAY= new Cheese[0];
Collections.emptyList();
Collections.emptySet();
Collections.emptyMap();

第 55 条: 返回 Optional 类型

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


Lambdas 与 Streams

第 42 条:lambdas 优于匿名类

从 Java 8 开始,lambda 是迄今为止表示小函数对象的最佳方式。 除非必须创建非函数式接口类型的实例,否则不要使用匿名类作为函数对象。

lambda 没有名称和文档,如果超过三行,不要使用 lambda 表达式。

第 43 条: 方法引用优于 lambdas

如果方法引用看起来更简短更清晰,请使用它们;否则,还是坚持 lambda。

方法引用类型 举例 等同的 Lambda
Static Integer::parseInt str -> Integer.parseInt(str)
Bound Instant.now()::isAfter Instant then = Instant.now(); t -> then.isAfter(t)
Unbound String::toLowerCase str -> str.toLowerCase()
Class Constructor TreeMap<K,V>::new () -> new TreeMap<K,V>
Array Constructor int[]::new len -> new int[len]

第 44 条: 优先使用标准的函数式接口

设计API时必须考虑 lambda 表达式。 在输入上接受函数式接口类型并在输出中返回它们。 一般来说,最好使用 java.util.function.Function 中提供的标准接口

接口 方法 示例 表示
UnaryOperator T apply(T t) String::toLowerCase 方法的结果和参数类型相同
BinaryOperator T apply(T t1, T t2) BigInteger::add 方法的结果和参数类型相同
Predicate boolean test(T t) Collection::isEmpty 方法接受一个参数并返回一个布尔值
Function<T,R> R apply(T t) Arrays::asList 参数和返回类型不同
Supplier T get() Instant::now 一个不接受参数和返回值(或“供应”)的方法
Consumer void accept(T t) System.out::println 接受一个参数而不返回任何东西

第 45 条: 明智审慎地使用 Stream

在没有显式类型的情况下,仔细命名 lambda 参数对于流管道的可读性至关重要。

使用辅助方法对于流管道中的可读性比在迭代代码中更为重要

重构现有代码以使用流,并仅在有意义的情况下在新代码中使用它们。

如果不确定一个任务是通过流还是迭代更好地完成,那么尝试这两种方法,看看哪一种效果更好。

第 46 条: 优先考虑流中无副作用的函数

第 47 条: 返回类型优先选择集合而不是流

如果返回集合是可行的,请执行此操作。如果返回集合是不可行的,则返回流或可迭代的。

如果在将来的 Java 版本中,Stream 接口声明被修改为继承 Iterable,那么可随意返回流。

第 48 条: 谨慎使用流并行

不恰当地并行化流的代价可能是程序失败或性能灾难。使用并行流之前,请确保您的代码在并行运行时保持正确,并在实际情况下进行仔细的性能度量。


枚举和注解

第 34 条:用 enum 代替 int 常量

当需要一组固定常量的时候,应该使用 enum 代替 int 常量。

第 35 条:用实例域代替序数

应该给 enum 添加 int 域,而不是使用 ordinal() 方法来导出与枚举关联的序数值。(几乎不应使用 ordinal() 方法,除非在编写像 EnumMap 这样的基于枚举的通用数据结构)

第36条:用 EnumSet 代替位域

  • EnumSet 实现了 Set 接口
  • 若枚举类型个数小于 64 个,则整个 EnumSet 就用单个 long 来表示,性能上比得上位运算
  • 总而言之因为枚举类型要用在集合(Set)中,所以没有理由用位域来表示.
//WRONG
public class Text{
private static final int STYLE_BOLD = 1 << 0;
private static final int STYLE_ITALIC = 1 << 1;
private static final int STYLE_UNDERLINE = 1 << 2;

public void applyStyles(int styles) {...}
}
//use
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

//RIGHT
public class Text{
public enum Style{STYLE_BOLD, STYLE_ITALIC, STYLE_UNDERLINE}

public void applyStyles(Set<Style> styles) {...}
}
//use
text.applyStyles(EnumSet.of(STYLE_BOLD, STYLE_ITALIC));

第 37 条:用 EnumMap 代替序数索引

序数索引是指依赖于枚举成员在枚举中的序数来进行数组索引

应该使用 EnumMap 来实现,EnumMap 内部是采用数组实现的,具有 Map 的丰富功能和类型安全以及数组的效率

Map<Plant.Type, Set<Plant>> plants = new EnumMap<Plant.Type, Set<Plant>>(Plant.Type.class); 

for(Plant.Type type : Plant.Type.valuse()){
plants.put(type, new HashSet<Plant>);
}

for(Plant p : garden){
plants.get(p.type).add(p);
}

第 38 条:用接口模拟可以伸缩的枚举

由于在 Java 中 enum 不是可扩展的,在某些情况下,可能需要对枚举进行扩展,比如操作类型(±*/等),就可以考虑:

  1. 定义一个接口,比如public interface Operation{…};
  2. 使枚举继承接口:比如public enum BasicOperation implements Operation{…}
  3. 使用时的 API 写成接口(比如,T extends Enum & Operation),而不是实现(比如BasicOperation
  4. 当需要扩展BasicOperation枚举时,就可以另写一个枚举,且 implements 接口Operation

第 39 条:注解优先于命名模式

第 40 条:坚持使用 Override 注解

第 41 条:用标记接口实现类型

标记接口可以在编译时就检查到相应的类型问题,而标记注解则要到运行时。

  • 如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解. 因为只有 类和接口可以用来实现或者扩展接口
  • 如果标记只应用给类和接口,就应该 优先使用标记接口而非注解

泛型

第 26 条:请不要在新代码中使用原生态类型

原生态类型只为了与引入泛型之前的遗留代码进行兼容和互用而提供的。

List 原生态类型

List 泛型

第 27 条:消除非受检警告

如果无法消除警告,同时可以证明引起警告的代码是类型安全的,只有在这种情况下才可以用一个 @SuppressWarnings(“unchecked”) 注解来禁止这条警告。

第 28 条:列表优先于数组

第 29 条:优先考虑泛型

第 30 条:优先考虑泛型方法

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

限定通配符包括两种:

  1. 表示类型的上界,格式为:<? extends T>,即类型必须为T类型或者T子类
  2. 表示类型的下界,格式为:<? super T>,即类型必须为T类型或者T的父类

第 32 条: 明智地结合泛型和可变参数

第 33 条:优先考虑类型安全的异构容器

异构容器

异构容器是指能够容纳不同类型对象的容器。像我们通常用的ListMap等容器,它们的原生态类型本身就是异构容器,一旦给它们设置了泛型参数,例如ListMap,它们就不再是异构容器。