对于一些隐藏的坑如何优雅的避过。
1. 关于浮点数的处理
定义:浮点数是属于有理数中某特定子集的数字表示,在计算机中用以近似表示任意某个实数。
1. 货币类型的存储
货币类型的存储推荐使用最小货币单位且整型类型来进行存储。 例如人名币的最小单位是分,那么存在数据库里面就是以分来存,1块就是100分
2. 浮点数据类型的问题
最重要的问题:精度丢失
单精度的数据的存储格式为:一个符号为,8个指数位,23个有效数字,如下图
单精度数据在存储的时候,首先会把整数部分改写成十二进制,然后把小数部分采用乘二取整来计算二进制位数,比如5.2,首先会把5-->101,然后把0.2采用乘二取整来转为二进制,这样转换出来其实它是一个无限循环的小数,而单精度的有效长度只有23位,这样就会造成截取,就造成了精度的损失。
3. 避坑
- 浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equal比较。因为浮点数之间的计算并不是准确的某个值,我们可以设定一个区间,当两个浮点数的差值在这个区间的时间,就认为它是相等的。
BigDecimal
的等值比较应该使用compareTo
方法,而不是equals
方式。因为equals
方法会比较值和精度(1.0与1.00返回结果位false),但是compareTo
会忽略精度。- 禁止使用构造方法
BigDecimal(double)
的方式直接把double
值转为BigDecimal
对象。因为构造方法能够保留double
的最长长度。
2. 日期类型数据的处理
1. 新老API对比
- Java传统日期API
- Date: 表示特定的瞬间,精确到毫秒
- SimpleDateFormate: 继承DateFormat类,主要用来格式转换
- Calender: 一个工具类,为特定瞬间和一组日历字段之间的转换以及操作日历段提供了方法
- 问题:所有的日期都是线程不安全的。
- Java8新增的日期API
- LocalDate: 日期 2021-11-09
- LocalTime: 时刻 12:00:00
- LocalDateTime: 具体时间 2021-11-09 12:00:00
- 线程安全的,并且每个类型都有单独的操作类
2. 避坑
- 日期转换时,传入的pattern的日期表示年份的统一用小写的
y
。因为大写的Y
在跨年的那一周的时候,可能会被格式化为下一年的日期。比如21年的12月31号,就有可能被格式化成22年的12月31号。 - 搞清楚日期中各个字母的作用,并且为了防止出错,建议在工具类中定义好所有的pattern。 - M:月份 - m:分钟 - H:24小时 - h:12小时
- 使用
System.currentTimeMillis()
,而不要多走一步用new Date().getTime()
- 要使用更加精确的纳秒级的话,推荐使用
System.nanoTime
- 在JDK8中,针对统计时间等场景,推荐使用
Instant
类
3. 控制语句的避坑规范
1. switch避坑
- 每个Case要么使用continue/break/return等来终止,要么注释说明程序执行到哪一个case会被终止
- 在一个Switch块内,都必须包含一个
default
在最后 - 当switch括号内的判断为String,并且属于外部传参是,必须先进行控制判断
if/else/for/while/for
语句都必须使用大括号,这样可读性会高一点- 表达异常的分支时,少使用
if-else
方式
2. 三目运算符踩坑
三目运算符在运算的时候会自动进行装箱,拆箱操作,在下面两种类型下面会执行拆箱操作
- 表达式1或者表达式2的值只要有一个是原始类型,如果一个是int一个是Integer,Integer会被拆箱为int
- 表达式1和表达式2的类型不一样时,会强制拆箱成表示范围更大的那个值 例子:
int a = 1;
Integer b = null;
Integer c = false?a:b
此时会把b进行拆箱,但是因为Integer为空,所以此时会报NPE的错误。
3. 控制语句习惯
- 不要在条件表达式中插入赋值语句
- 不要在条件表达式中进行及其复杂的判断语句,以提高可读性
- 避免采用反逻辑运算符
4. 高并发场景下的判断
- 避免使用
等于
来作为中断或者推出的条件。 比如在秒杀活动中,会有很大的并发,如果是以等于作为判断的话,此时当库存减少到-1的时候,还会继续去售卖,因为会直接到小于0。所以这里最好使用小于等于0来作为判断。
当然以上还是有可能多卖出一点的,这里还可以在执行扣减库存sql中添加数量的判断,比如update xx set count = (count - 1) where id = x and (count - 1) > 0
5. 参数的校验
- 需要参数校验的场合
- 调用频次低的方法
- 执行时间开销很大的方法,因为一个参数的问题导致执行失败,有点得不偿失
- 需要极高稳定性和可用性的方法
- 对外提供的接口
- 敏感权限入口
- 公共接口需要入参保护,特别是批量处理的
- 不需要参数校验的
- 极有可能会被循环调用的方法。在循环体外就做好参数的判断,减少重复判断
- 底层调用频次高的方法,因为在外层就已经做好了判断,就不需要在最底层进行判断了
- 被声明为private的方法,因为私有的,自己已经知道了入参信息,就不需要进行判断了
4. OOP规约
OOP:Object-Oriented Programming 面向对象编程
1. 面向对象的四大特征
- 抽象:对需求进行业务抽象和建模分析,通过模型的行为组合共同去解决一个问题
- 封装:对象功能内聚的表现形式,使模块之间的耦合度变低,更加具有维护性
- 继承:继承使子类能够继承父类,获得父类的部分属性和而行为,使模块更有复用性
- 多态:使模块在复用基础上更加具有拓展性,使系统更加有想象空间
2. 七个设计原则
- 单一原则:一个类只负责一个功能领域的相关职责。特点:高内聚,低耦合
- 里氏替换原则:所有父类能够使用的地方,子类都一定能使用。这里老师举了几个很有意思的例子:鲨鱼和鱼都是鱼,从鲨鱼能够游泳,我们可以推出来鱼也可以游泳。但是我们不能从鲨鱼有牙齿推出来鱼也有牙齿。
- 接口隔离原则:接口的设计的粒度一定要小,同一个接口的方法要强内聚于同一个特征。
- 组合复用原则:尽量使用对象组合,而不是用继承来达到复用的目的。要防止“接口污染”
- 依赖倒置原则:面向接口编程。上层模块不依赖于底层模块,细节信息依赖于抽象。
- 迪米特原则:接口之间需要关注的信息尽可能的少。使用一个接口,只需要关注他的输入和输出就行。
- 开闭原则:对拓展开放,对修改要关闭。(ps:最重要的一个原则,但是也是最难实现的一个,因为需求会一直变化,有时候不得不区修改代码。 这里就要考验架构设计的能力了,需要考虑到需求的持续变化,以及识别和隔离各个变化点)这里与开闭原则有个相反的定律,叫做熵增定律。所有的一切都是不断变化的,随着变化会越变越复杂。而开闭原则就是一个熵减,可以减少熵增定律带来的影响。
3. 避坑
- 所有的覆写方法,都必须要加上
@Override
防止写错 - 可变参数,必须写在参数的最后面。并且避免使用Object
- 所有的整型包装类对象之间值的比较,全部使用
equals
方法比较 - 应该用常量或确定有值的一方最为调用方,防止NPE
- 所有的POJO类属性比较使用包装类
- 定义POJO类时,不要设定默认值
- get方法和set方法中,不要添加逻辑
- 禁止在POJO类中,使用isXxx()方法。如果存在is_choose这类的字段,可以在数据对应层进行对应POJO中的值
- 构造方法里面不要加业务逻辑,需要的话加载init方法中
注:推荐idea安装阿里规范插件,会自动提示很多不规范的代码,根据规范修改即可,简单方便快捷