Java 规约笔记

OOP 规约


任何货币金额,均以最小货币单位且整型类型来进行存储。

比如说人民币的最小单位是分,那假设一个商品的价格是1元钱,那就存到数据库的 price 字段,字段类型是 int 或者 bigint,值为 100,单位是分,也就是100分。

我就在这个问题上入过坑,几年前一个系统中的价格字段用的是浮点数,单位还是元,后面在价格计算上很是麻烦,而且浮点数的计算并不是完全准确的,尤其是涉及到小数位的时候。最后还是把字段调成了 int 类型,代码上涉及到价格的地方都重构了。

所以,后来当我看到手册中的这条规约的时候,有种相见恨晚的感觉。我用了惨痛的经验才换来这个教训,如果早点看到呢,岂不是省了很多事。

浮点数之间的等值判断,基本数据类型不能用==来比较,而要规定可接受的误差范围。包装数据类型不能用 equals 来判断,而要使用 compareTo 来判断。

在这里插入图片描述

在这里插入图片描述

如上所示 BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法。

equals()方法会比较值和精度 (1.0 与 1.00 返回结果为 false) ,而 compareTo()则会忽略精度。

定义数据对象 DO 类时,属性类型要与数据库字段类型相匹配。

正例: 数据库字段的 bigint 必须与类属性的 Long 类型相对应。

反例: 某个案例的数据库表 id 字段定义类型 bigint unsigned,实际类对象属性为 Integer,随着 id 越来越大,超过 Integer 的表示范围而溢出成为负数。

Java中PO、DO、TO、DTO、 VO、 BO、POJO 、DAO的概念

DO 类属性类型与数据库字段类型匹配表

禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。

说明: BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。

如:BigDecimal g = new BigDecimal(0.1F); 实际的存储值为:0.10000000149

正例: 优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。

BigDecimal recommend1 = new BigDecimal(“0.1”);

BigDecimal recommend2 = BigDecimal.valueOf(0.1);

关于基本数据类型与包装数据类型的使用标准有如下三点:

1、所有的POJO类属性(成员变量)必须使用包装数据类型。

2、RPC方法的返回值和参数必须使用包装数据类型。

3、所有的局部变量使用基本数据类型。

说明: 可以考虑为我们远程 RPC 调用的方法返回值可能是包装类型,故此时我们接收时,应该优先考虑为包装类型。

集合处理


使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。

反例: 直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误。

正例:

List<String> list = new ArrayList<>(2);
list.add("guan");
list.add("bao");
String[] array = list.toArray(new String[0]);

说明: 使用 toArray 带参方法,数组空间大小的 length:

1) 等于 0,动态创建与 size 相同的数组,性能最好。

2) 大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。

3) 等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。

4) 大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。

集合初始化时,指定集合初始值大小。

initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。

反例: HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,resize()方法总共会调用 8 次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。

并发处理

SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类。

simpleDateFormat.parse(“2020-10-30 12:59:59”); 这条语句内部操作流程可简单介绍为:

1.clear(Calendar ) 操作,把时间信息初始为初始状态。而传入的 Calendar 是全局变量,即共享变量。

2.给日期设置值

所以当线程 A 已经设好时间值了,之后被阻塞。然后线程 B 也调用 parse 方法,然后先执行 clear 操作。当线程 A 醒来时,就会发现之前设好的时间值被清空了,或者是变成了线程 B 正在处理的时间值。
在这里插入图片描述
正例:
在这里插入图片描述

必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。

objectThreadLocal.set(userInfo);
try {
	// ...
} finally {
	objectThreadLocal.remove();
}

在这里插入图片描述

在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。

说明一: 如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。

说明二: 如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock 对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出 IllegalMonitorStateException 异常。

说明三: 在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。


正例:

Lock lock = new XxxLock();
// ...
lock.lock();
try {
	doSomething();
	doOthers();
} finally {
	lock.unlock();
}

表达异常的分支时,少用 if-else 方式 ,这种方式可以改写成卫语句的形式

卫语句介绍: 如果2条分支都是正常行为,就应该使用形如 if-else 的条件表达式;如果某个条件极其罕见或者代表异常返回,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。即卫语句是指用来检查罕见条件或者产生异常的分支。

说明: 如果非使用 if()…else if()…else…方式表达逻辑,避免后续代码维护困难,请勿超过 3 层。

正例: 超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句示例如下:

public void findBoyfriend (Man man) {
	if (man.isUgly()) {
		System.out.println("本姑娘是外貌协会的资深会员");
		return;
	}
	if (man.isPoor()) {
		System.out.println("贫贱夫妻百事哀");
		return;
	}
	if (man.isBadTemper()) {
		System.out.println("银河有多远,你就给我滚多远");
		return;
	}
	System.out.println("可以先交往一段时间看看");
}

对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用 Long 类型。

说明: Java 服务端如果直接返回 Long 整型数据给前端,JS 会自动转换为 Number 类型(注:此类型为双精度浮点数,表示原理与取值范围等同于 Java 中的 Double)。Long 类型能表示的最大值是 2 的 63 次方 -1,在取值范围之内,超过 2 的 53 次方 (9007199254740992)的数值转化为 JS 的 Number 时,有些数值会有精度损失。扩展说明,在 Long 取值范围内,任何 2 的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,
双精度浮点数的尾数位只有 52 位。

反例: 通常在订单号或交易号大于等于 16 位,大概率会出现前后端单据不一致的情况,比如,“orderId”: 362909601374617692,前端拿到的值却是: 362909601374617660。

服务器内部重定向(请求转发)必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成,否则会因线上采用 HTTPS 协议而导致浏览器提示“不安全”,并且还会带来 URL 维护不一致的问题。

在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。

说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);

反例:

// 没有使用预编译
private void func(...) {
	if (Pattern.matches(regexRule, content)) {
		...
	}
}
// 多次预编译
private void func(...) {
	Pattern pattern = Pattern.compile(regexRule);
	Matcher m = pattern.matcher(content);
	if (m.matches()) {
		...
	}
}

正例:

private static final Pattern pattern = Pattern.compile(regexRule);

private void func(...) {
	Matcher m = pattern.matcher(content);
	if (m.matches()) {
		...
	}
}

正则的预编译主要注意两点:

  1. Pattern 表达式要提前定义,不要在需要的地方临时定义;

  2. Pattern 表达式要定义为 static final 静态变量,以避免执行多次预编译。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值