代码编写提升篇

怎么设计一个简单又直观的接口?

保持接口的简单直观

比如说, 是否可以授权一个用户使用某一个在线服务呢? 这个问题就可以分解为两个小问题:

  1. 该用户是否为已注册的用户?
  2. 该用户是否持有正确的密码?

分解问题时, 我们要注意分解的问题一定要“相互独立, 完全穷尽”

如何理解这个原则呢?

先来说一下“相互独立”这个要求。 问题分解后, 我们要仔细琢磨, 是不是每一个小问题都是独立
的, 都是可以区分的事情。
我们以上面的分解为例子, 仔细看会发现这种划分是有问题的。 因为只有已经注册的用户, 才会
持有正确的密码。 而且, 只有持有正确密码的用户, 才能够被看作是注册用户。 这两个小问题之
间, 存在着依赖关系, 就不能算是“相互独立”。
我们要消除掉这种依赖关系。
变更后, 就需要两个层次的表达。 第一个层次问题是, 该用户是否为已注册的用户? 这个问题,
可以进一步分解为两个更小的问题: 用户持有的用户名是否已注册? 用户持有的密码是否匹
配?

  1. 该用户是否是已注册的用户?
    a. 用户名是否已注册?
    b.用户密码是否正确?

除了每一项都要独立之外, 我们还要琢磨, 是不是把所有能够找到的因素, 都找到了? 也就是
说, 我们是否穷尽了所有的内容, 做到了“完全穷尽”?你可能早已经注意到了上述问题分解的缺陷。 如果一个服务, 对所有的注册用户开放, 上面的分解就是完备的。 否则, 我们就漏掉了一个重要的内容, 不同的注册用户, 可以访问的服务可能是不同的。 也就是说如果没有访问的权限, 那么即使用户名和密码正确也无法访问相关的服务。
如果我们把漏掉的加上, 这个问题的分解可以进一步表示为:

  1. 该用户是否是已注册的用户?
    a. 用户名是否已注册?
    b.用户密码是否正确?
    2.该用户是否有访问的权限?

问题的分解过程, 对应的就是软件的接口以及接口之间的联系

一个接口一件事情

这件事是独立的。这件事是完整的

减少依赖关系

这就引出了另一个要求, 接口一定要“皮实”。

@exception SignatureException if this signature object is not

  • initialized properly.
    */
    public final void update(byte[] data) throws SignatureException {
    // snipped
    } /
    **
  • Returns the signature bytes of all the data updated.
  • The format of the signature depends on the underlying
  • signature scheme.

A call to this method resets this signature object to the state * it was in when previously initialized for signing via a * call to {@code initSign(PrivateKey)}. That is, the object is * reset and available to generate another signature from the same * signer, if desired, via new calls to {@code update} and * {@code sign}. * * @return the signature bytes of the signing operation's result. * * @exception SignatureException if this signature object is not * initialized properly or if this signature algorithm is unable to * process the input data provided. */ public final byte[] sign() throws SignatureException { // snipped } // snipped }使用方式要“傻” 所有接口的设计, 都是为了最终的使用。 方便、 皮实的接口, 才是好用的接口。 接口要很容易理 解, 能轻易上手, 这就是方便。 此外还要限制少, 怎么用都不容易出错, 这就是皮实。

线程同步什么时候需要同步呢?

需要同步的场景, 要同时满足三个条件:

  1. 使用两个以上的线程;
  2. 关心共享资源的变化;
  3. 改变共享资源的行为。

我们要想尽办法避免线程的同步

线程同步也是这样的, 同步需要排队, 同步的管理需要时间。 所以, 实践中, 我们要想尽办法避免线程的同步。 如果实在难以避免, 就减少线程同步的排队时间。

  1. 使用单线程;
  2. 不关心共享资源的变化;
  3. 没有改变共享资源的行为。

使用了限定词“final”的类变量, 只能被赋值一次, 而且只能在实例化之前被赋值。 这样的变量,
就是不可变的量。 如果一个类的所有的变量, 都是不可变的, 那么这个类也是不可变的

所以, 我们要养成一个习惯, 看到声明的变量, 就要琢磨, 这个变量能不能声明成不可变的量?
现有的代码设计, 这个变量如果不是不可变的, 我们也要琢磨, 有没有办法修改接口设计或者实
现代码, 把它改成不可变的量? 设计一个类时, 要优先考虑, 这个类是不是可以设计成不可变的
类? 这样就可以避免很多不必要的线程同步, 让代码的效率更高, 接口更容易使用。

减少线程同步时间

减少线程同步的排队时间, 换一个说法, 就是减少同步线程的阻塞时间。

线程同步, 就是StringBuffer比StringBuilder慢的原因之一

  1. Java的编译器会优化常量字符串的连接, 我们可以放心地把长的字符串换成多行;
  2. 带有变量的字符串连接, StringBuilder效率更高。 如果效率敏感的代码, 建议使用
    StringBuilder。 String的连接操作可读性更高, 效率不敏感的代码可以使用, 比如异常信
    息、 调试日志、 使用不频繁的代码;
  3. 如果涉及大量的字符串操作, 使用StringBuilder效率更高;
  4. 除非有线程安全的需求, 不推荐使用线程安全的StringBuffer。

怎么减少内存使用, 减轻内存管理负担?

使用更少的内存

减少内存的使用, 办法有且只有两个。
第一个办法是减少实例的数量。 第二个办法是减小实例的尺寸

减少实例数量

避免不必要的实例

避免使用原始数据类

这里涉及到Java原始数据类型的自动装箱(boxing) 与拆箱(unboxing) 的类型转换。

这个装箱的过程, 就产生了不必要的实例。 如果这样的转换数量巨大, 就会有明显的性能影响

使用单实例模式

减小实例的尺寸

所谓减少实例的尺寸, 就是减少这个实例占用的内存空间。 这个空间, 不仅包括实例的变量标识符占用的空间, 还包括标识符所包含
对象的占用空间。

有两类理想的共享资源, 一类是一成不变(immutable) 的资源, 另一类是禁止修改(unmodifiable) 的资源。

不可变的类

上一次, 在讨论线程同步问题时, 我们也讨论了不可变的类。 由于不可变的类一旦实例化, 就不再变化, 我们可以放心地在不同的地方使用它的引用, 而不用担心任何状态变化的问题。

无法修改的对象

还有一类对象, 虽然不是不可变的类的实例, 但是它的修改方法被禁止了。 当我们使用这些对象的代码时, 没有办法对它做出任何修改。 这样, 这些对象就有了和不可变的实例一样的优点, 可以放心地引用。

黑白灰, 理解延迟分配的两面性

对于局部变量, 我们应该坚持“需要时再声明, 需要时再分配”的原则。
对于类的变量, 我们依然应该优先考虑“声明时就初始化”的方案。 如果初始化涉及的计算量比较
大, 占用的资源比较多或者占用的时间比较长, 我们可以根据具体情况, 具体分析, 采用延迟初
始化是否可以提高效率, 然后再决定使用这种方案是否划算

延迟分配:

在前面讨论怎么写声明的时候, 为了避免初始化的遗漏或者不必要的代码重复, 我们一般建议“声明时就初始化”。 但是, 如果初始化涉及的计算量比较大, 占用的资源比较多或者占用的时间比较长, 声明时就初始化的方案可能会占用不必要的资源, 甚至成为软件的一个潜在安全问
题。
这时候, 我们就需要考虑延迟分配的方案了。 也就是说, 不到需要时候, 不占用不必要的资源。

延迟初始化

延迟分配的思路, 就是用到声明时再初始化, 这就是延迟初始化。 换句话说, 不到需要的时候,
就不进行初始化

public class CodingExample {
private Map<String, String> helloWordsMap;
private void setHelloWords(String language, String greeting) {
if (helloWordsMap == null) {
helloWordsMap = new HashMap<>();
} h
elloWordsMap.put(language, greeting);
} //
snipped
}

上面的例子中, 实例变量helloWordsMap只有需要时才初始化。 这的确可以避免内存资源的浪
费, 但代价是要使用更多的CPU。 检查实例变量是否已经能初始化, 需要CPU的额外开销。 这
是一个内存和CPU效率的妥协与竞争。
而且, 除非是静态变量, 否则使用延迟初始化, 一般也意味着放弃了使用不可变的类可能性。 这
就需要考虑多线程安全的问题。 上面例子的实现, 就不是多线程安全的。 对于多线程环境下的计
算, 初始化时需要的线程同步也是一个不小的开销。
比如下面的代码, 就是一个常见的解决延迟初始化的线程同步问题的模式。 这个模式的效率, 还
算不错。 但是里面的很多小细节都忽视不得, 看起来都很头疼。 我每次看到这样的模式, 即便明
白这样做的必要性, 也恨不得先休息半天, 再来啃这块硬骨头。

public class CodingExample {
private volatile Map<String, String> helloWordsMap;
private void setHelloWords(String language, String greeting) {
Map<String, String> temporaryMap = helloWordsMap;
if (temporaryMap == null) { // 1st check (no locking)
synchronized (this) {
temporaryMap = helloWordsMap;
if (temporaryMap == null) { // 2nd check (locking)
temporaryMap = new ConcurrentHashMap<>();
helloWordsMap = temporaryMap;
}
}
} t
emporaryMap.put(language, greeting);
} //
snipped
}

延迟初始化到底好不好, 要取决于具体的使用场景。
一般情况下, 由于规范性带来的明显优势,我们优先使用“声明时就初始化”这个方案。
所以, 我们要再一次强调, 只有初始化占用的资源比较多或者占用的时间比较长的时候, 我们才开始考虑其他的方案。 复杂的方法, 只有必要时才使用。
※注: 从JDK11开始, HashMap的实现做了改进, 缺省的构造不再分配实质性的数组。 以后我
们写代码时, 可以省点心了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值