Skip to content

Commit

Permalink
完善部分知识点
Browse files Browse the repository at this point in the history
  • Loading branch information
hollis.zhl committed Mar 15, 2021
1 parent 506a4db commit 88a1cea
Show file tree
Hide file tree
Showing 4 changed files with 357 additions and 4 deletions.
6 changes: 4 additions & 2 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@

* [枚举的线程安全性问题](/basics/java-basic/enum-thread-safe.md)

* 为什么不建议在对外接口中使用枚举
* [为什么不建议在对外接口中使用枚举](/basics/java-basic/stop-use-enum-in-api.md)

* IO

Expand Down Expand Up @@ -396,7 +396,9 @@

* 为什么0.1+0.2不等于0.3

* 为什么不能使用BigDecimal的equals比较大小
* [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md)

* 为什么不能直接使用double创建一个BigDecimal

* Java 8

Expand Down
175 changes: 175 additions & 0 deletions docs/basics/java-basic/stop-use-enum-in-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
最近我们的线上环境出现了一个问题线上代码在执行过程中抛出了一个IllegalArgumentException分析堆栈后发现最根本的的异常是以下内容

java.lang.IllegalArgumentException:
No enum constant com.a.b.f.m.a.c.AType.P_M


大概就是以上的内容看起来还是很简单的提示的错误信息就是在AType这个枚举类中没有找到P_M这个枚举项

于是经过排查我们发现在线上开始有这个异常之前该应用依赖的一个下游系统有发布而发布过程中是一个API包发生了变化主要变化内容是在一个RPC接口的Response返回值类中的一个枚举参数AType中增加了P_M这个枚举项

但是下游系统发布时并未通知到我们负责的这个系统进行升级所以就报错了

我们来分析下为什么会发生这样的情况

### 问题重现

首先下游系统A提供了一个二方库的某一个接口的返回值中有一个参数类型是枚举类型

> 一方库指的是本项目中的依赖 二方库指的是公司内部其他项目提供的依赖 三方库指的是其他组织公司等来自第三方的依赖

public interface AFacadeService {

public AResponse doSth(ARequest aRequest);
}

public Class AResponse{

private Boolean success;

private AType aType;
}

public enum AType{

P_T,

A_B
}


然后B系统依赖了这个二方库并且会通过RPC远程调用的方式调用AFacadeService的doSth方法

public class BService {

@Autowired
AFacadeService aFacadeService;

public void doSth(){
ARequest aRequest = new ARequest();

AResponse aResponse = aFacadeService.doSth(aRequest);

AType aType = aResponse.getAType();
}
}


这时候如果A和B系统依赖的都是同一个二方库的话两者使用到的枚举AType会是同一个类里面的枚举项也都是一致的这种情况不会有什么问题

但是如果有一天这个二方库做了升级在AType这个枚举类中增加了一个新的枚举项P_M这时候只有系统A做了升级但是系统B并没有做升级

那么A系统依赖的的AType就是这样的

public enum AType{

P_T,

A_B,

P_M
}


而B系统依赖的AType则是这样的

public enum AType{

P_T,

A_B
}


这种情况下**,在B系统通过RPC调用A系统的时候如果A系统返回的AResponse中的aType的类型位新增的P_M时候B系统就会无法解析一般在这种时候RPC框架就会发生反序列化异常导致程序被中断。**

### 原理分析

这个问题的现象我们分析清楚了那么再来看下原理是怎样的为什么出现这样的异常呢

其实这个原理也不难这类**RPC框架大多数会采用JSON的格式进行数据传输**,也就是客户端会将返回值序列化成JSON字符串而服务端会再将JSON字符串反序列化成一个Java对象

而JSON在反序列化的过程中对于一个枚举类型会尝试调用对应的枚举类的valueOf方法来获取到对应的枚举

而我们查看枚举类的valueOf方法的实现时就可以发现,**如果从枚举类中找不到对应的枚举项的时候就会抛出IllegalArgumentException**:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}


关于这个问题其实在阿里巴巴Java开发手册中也有类似的约定

![-w1538][1]

这里面规定"**对于二方库的参数可以使用枚举,但是返回值不允许使用枚举**"这背后的思考就是本文上面提到的内容

### 扩展思考

**为什么参数中可以有枚举?**

不知道大家有没有想过这个问题其实这个就和二方库的职责有点关系了

一般情况下A系统想要提供一个远程接口给别人调用的时候就会定义一个二方库告诉其调用方如何构造参数调用哪个接口

而这个二方库的调用方会根据其中定义的内容来进行调用而参数的构造过程是由B系统完成的如果B系统使用到的是一个旧的二方库使用到的枚举自然是已有的一些新增的就不会被用到所以这样也不会出现问题

比如前面的例子B系统在调用A系统的时候构造参数的时候使用到AType的时候就只有P_T和A_B两个选项虽然A系统已经支持P_M了但是B系统并没有使用到

如果B系统想要使用P_M那么就需要对该二方库进行升级

但是返回值就不一样了返回值并不受客户端控制服务端返回什么内容是根据他自己依赖的二方库决定的

但是其实相比较于手册中的规定,**我更加倾向于在RPC的接口中入参和出参都不要使用枚举。**

一般我们要使用枚举都是有几个考虑

* 1枚举严格控制下游系统的传入内容避免非法字符

* 2方便下游系统知道都可以传哪些值不容易出错

不可否认使用枚举确实有一些好处但是我不建议使用主要有以下原因

* 1如果二方库升级并且删除了一个枚举中的部分枚举项那么入参中使用枚举也会出现问题调用方将无法识别该枚举项

* 2有的时候上下游系统有多个如C系统通过B系统间接调用A系统A系统的参数是由C系统传过来的B系统只是做了一个参数的转换与组装这种情况下一旦A系统的二方库升级那么B和C都要同时升级任何一个不升级都将无法兼容

**我其实建议大家在接口中使用字符串代替枚举**,相比较于枚举这种强类型字符串算是一种弱类型

如果使用字符串代替RPC接口中的枚举那么就可以避免上面我们提到的两个问题上游系统只需要传递字符串就行了而具体的值的合法性只需要在A系统内自己进行校验就可以了

**为了方便调用者使用可以使用javadoc的@see注解表明这个字符串字段的取值从那个枚举中获取。**

public Class AResponse{

private Boolean success;

/**
* @see AType
*/
private String aType;
}


对于像阿里这种比较庞大的互联网公司,**随便提供出去的一个接口可能有上百个调用方**,而接口升级也是常态,**我们根本做不到每次二方库升级之后要求所有调用者跟着一起升级**,这是完全不现实的并且对于有些调用者来说他用不到新特性完全没必要做升级

还有一种看起来比较特殊但是实际上比较常见的情况就是有的时候一个接口的声明在A包中而一些枚举常量定义在B包中比较常见的就是阿里的交易相关的信息订单分很多层次每次引入一个包的同时都需要引入几十个包

对于调用者来说我肯定是不希望我的系统引入太多的依赖的,**一方面依赖多了会导致应用的编译过程很慢并且很容易出现依赖冲突问题。**

所以在调用下游接口的时候如果参数中字段的类型是枚举的话那我没办法必须得依赖他的二方库但是如果不是枚举只是一个字符串那我就可以选择不依赖

所以我们在定义接口的时候会尽量避免使用枚举这种强类型规范中规定在返回值中不允许使用而我自己要求更高就是即使在接口的入参中我也很少使用

最后我只是不建议在对外提供的接口的出入参中使用枚举并不是说彻底不要用枚举我之前很多文章也提到过枚举有很多好处我在代码中也经常使用所以切不可因噎废食

当然文中的观点仅代表我个人具体是是不是适用其他人其他场景或者其他公司的实践需要读者们自行分辨下建议大家在使用的时候可以多思考一下

[1]: https://www.hollischuang.com/wp-content/uploads/2020/11/16066271055035-scaled.jpg
174 changes: 174 additions & 0 deletions docs/basics/java-basic/stop-using-equlas-in-bigdecimal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
BigDecimal相信对于很多人来说都不陌生很多人都知道他的用法这是一种java.math包中提供的一种可以用来进行精确运算的类型

很多人都知道在进行金额表示金额计算等场景不能使用doublefloat等类型而是要使用对精度支持的更好的BigDecimal

所以很多支付电商金融等业务中BigDecimal的使用非常频繁而且不得不说这是一个非常好用的类其内部自带了很多方法如加除等运算方法都是可以直接调用的

除了需要用BigDecimal表示数字和进行数字运算以外代码中还经常需要对于数字进行相等判断

关于这个知识点在最新版的阿里巴巴Java开发手册中也有说明

![][1]

这背后的思考是什么呢

我在之前的CodeReview中看到过以下这样的低级错误

if(bigDecimal == bigDecimal1){
// 两个数相等
}


这种错误相信聪明的读者一眼就可以看出问题,**因为BigDecimal是对象所以不能用`==`来判断两个数字的值是否相等。**

以上这种问题在有一定的经验之后还是可以避免的但是聪明的读者看一下以下这行代码你觉得他有问题吗

if(bigDecimal.equals(bigDecimal1)){
// 两个数相等
}


可以明确的告诉大家以上这种写法可能得到的结果和你预想的不一样

先来做个实验运行以下代码

BigDecimal bigDecimal = new BigDecimal(1);
BigDecimal bigDecimal1 = new BigDecimal(1);
System.out.println(bigDecimal.equals(bigDecimal1));


BigDecimal bigDecimal2 = new BigDecimal(1);
BigDecimal bigDecimal3 = new BigDecimal(1.0);
System.out.println(bigDecimal2.equals(bigDecimal3));


BigDecimal bigDecimal4 = new BigDecimal("1");
BigDecimal bigDecimal5 = new BigDecimal("1.0");
System.out.println(bigDecimal4.equals(bigDecimal5));


以上代码输出结果为

true
true
false


### BigDecimal的equals原理

通过以上代码示例我们发现在使用BigDecimal的equals方法对1和1.0进行比较的时候有的时候是true当使用intdouble定义BigDecimal时),有的时候是false当使用String定义BigDecimal时)。

那么为什么会出现这样的情况呢我们先来看下BigDecimal的equals方法

在BigDecimal的JavaDoc中其实已经解释了其中原因

Compares this BigDecimal with the specified Object for equality. Unlike compareTo, this method considers two BigDecimal objects equal only if they are equal in value and scale (thus 2.0 is not equal to 2.00 when compared by this method)


大概意思就是,**equals方法和compareTo并不一样equals方法会比较两部分内容分别是值value和标度scale)**


对应的代码如下

![][2]

所以我们以上代码定义出来的两个BigDecimal对象bigDecimal4和bigDecimal5的标度是不一样的所以使用equals比较的结果就是false了

尝试着对代码进行debug在debug的过程中我们也可以看到bigDecimal4的标度时0而bigDecimal5的标度是1

![][3]

到这里我们大概解释清楚了之所以equals比较bigDecimal4和bigDecimal5的结果是false是因为标度不同

那么为什么标度不同呢为什么bigDecimal2和bigDecimal3的标度是一样的当使用intdouble定义BigDecimal时),而bigDecimal4和bigDecimal5却不一样当使用String定义BigDecimal时

### 为什么标度不同

这个就涉及到BigDecimal的标度问题了这个问题其实是比较复杂的由于不是本文的重点这里面就简单介绍一下吧大家感兴趣的话后面单独讲

首先BigDecimal一共有以下4个构造方法

BigDecimal(int)
BigDecimal(double)
BigDecimal(long)
BigDecimal(String)


以上四个方法创建出来的的BigDecimal的标度是不同的

#### BigDecimal(long) 和BigDecimal(int)

首先最简单的就是**BigDecimal(long) 和BigDecimal(int),因为是整数所以标度就是0** :

public BigDecimal(int val) {
this.intCompact = val;
this.scale = 0;
this.intVal = null;
}

public BigDecimal(long val) {
this.intCompact = val;
this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null;
this.scale = 0;
}


#### BigDecimal(double)

而对于BigDecimal(double) ,**当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候其实创建出来的值并不是整好等于0.1而是0.1000000000000000055511151231257827021181583404541015625这是因为doule自身表示的只是一个近似值。**

那么无论我们使用new BigDecimal(0.1)还是new BigDecimal(0.10)定义他的近似值都是0.1000000000000000055511151231257827021181583404541015625这个那么他的标度就是这个数字的位数即55

![][4]

其他的浮点数也同样的道理对于new BigDecimal(1.0)这样的形式来说因为他本质上也是个整数所以他创建出来的数字的标度就是0

所以因为BigDecimal(1.0)和BigDecimal(1.00)的标度是一样的所以在使用equals方法比较的时候得到的结果就是true

#### BigDecimal(string)

而对于BigDecimal(double) ,**当我们使用new BigDecimal("0.1")创建一个BigDecimal 的时候其实创建出来的值正好就是等于0.1那么他的标度也就是1。**

如果使用new BigDecimal("0.10000"),那么创建出来的数就是0.10000标度也就是5

所以因为BigDecimal("1.0")和BigDecimal("1.00")的标度不一样所以在使用equals方法比较的时候得到的结果就是false

### 如何比较BigDecimal

前面我们解释了BigDecimal的equals方法其实不只是会比较数字的值还会对其标度进行比较

所以当我们使用equals方法判断判断两个数是否相等的时候是极其严格的

那么如果我们只想判断两个BigDecimal的值是否相等那么该如何判断呢

**BigDecimal中提供了compareTo方法这个方法就可以只比较两个数字的值如果两个数相等则返回0。**

BigDecimal bigDecimal4 = new BigDecimal("1");
BigDecimal bigDecimal5 = new BigDecimal("1.0000");
System.out.println(bigDecimal4.compareTo(bigDecimal5));


以上代码输出结果

0


其源码如下

![][5]

### 总结

BigDecimal是一个非常好用的表示高精度数字的类其中提供了很多丰富的方法

但是他的equals方法使用的时候需要谨慎因为他在比较的时候不仅比较两个数字的值还会比较他们的标度只要这两个因素有一个是不相等的那么结果也是false

如果读者想要对两个BigDecimal的数值进行比较的话可以使用compareTo方法


[1]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004945569932.jpg
[2]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004955317132.jpg
[3]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004956382289.jpg
[4]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004965161081.jpg
[5]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004972460075.jpg
[6]: https://www.hollischuang.com/wp-content/uploads/2020/09/16004976158870.jpg
6 changes: 4 additions & 2 deletions docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)

* [枚举的线程安全性问题](/basics/java-basic/enum-thread-safe.md)

* 为什么不建议在对外接口中使用枚举
* [为什么不建议在对外接口中使用枚举](/basics/java-basic/stop-use-enum-in-api.md)

* IO

Expand Down Expand Up @@ -453,7 +453,9 @@ Gitee Pages 完整阅读:[进入](http://hollischuang.gitee.io/tobetopjavaer)

* 为什么0.1+0.2不等于0.3

* 为什么不能使用BigDecimal的equals比较大小
* [为什么不能使用BigDecimal的equals比较大小](/basics/java-basic/stop-using-equlas-in-bigdecimal.md)

* 为什么不能直接使用double创建一个BigDecimal

* Java 8

Expand Down

0 comments on commit 88a1cea

Please sign in to comment.