文章目录
- 1空指针
- 2小数的计算
- 3包装类型
- 4Java8 Stream
- 5日期格式化
先来一个简单一点,就从空指针开始吧
1空指针
//多级调用空指针userService.getUser("张三").getUserInfo().getUserName();
//例如getUser("张三")、getUserInfo()都可能出现null//equals空指针,当我们的str通过某个方法获取(或者传参),而这个方法可能返回null,其代码等价于如下
String str = null; //xxxService.doSomeThing();
if(str.equals("张三")){System.out.println("我是张三");
}//集合遍历,当我们的list通过某个方法获取(或者传参),而这个方法可能返回null,其代码等价于如下
List<User> list = null; //xxxService.doSomeThing();
for (User user : list) {System.out.println(user);
}//包装类型的空指针,如果使用包装类型,包装类型是可能为null,对其进行算术操作,会出控制
Integer num = null;
System.out.println(num + 1);
2小数的计算
如下图所示,我们挑选了几个数字,做了加减乘除的操作,代码很简单,结果很意外。
这样的代码很容易就不经意间写出来了,然后BUG就产生了。因此,如果你的代码中有用到包含小数进行加减乘除的操作,及其容易出现意想不到的结果,得到的数会很接近你的目标小数,但是不是你的目标数,如果此时做大小、等于的判断以及高精度,就很容易出现BUG了。
其主要原因主要有如下
- 二进制表示不精确:计算机使用二进制表示浮点数,而十进制的小数无法精确表示为有限长度的二进制小数。某些十进制小数在二进制表示中是无限循环的,因此在转换过程中会有截断或舍入误差。
- 浮点数运算规则:浮点数的加法、减法、乘法和除法运算都遵循IEEE 754标准的规则。这些规则对于某些十进制小数可能会产生舍入误差,尤其是在进行多次运算时。
- 存储空间限制:浮点数通常使用有限的位数来表示,例如float类型使用32位,double类型使用64位。这种有限的存储空间限制了浮点数的精度,因此在进行计算时可能会丢失一些细微的精度。
解决办法:
- 需要精确的计算:提高精度,使用高精度的计算,可以使用BigDecimal类,它提供了高精度的十进制计算。使用BigDecimal可以增加浮点数的精度(不能完全消除),但同时也需要更多的计算和内存开销。因此,根据具体需求,需要权衡精度和性能之间的取舍。一般适用于比较严谨或者数值比较大的场景,例如金钱的计算,例如某公司流上千亿流水,某银行的存款有百万亿,这种情况下,即使是亿分之1的精度,每次计算那也是会丢失几百上千,甚至上万的丢失。
- 不需要精确的计算:降低精度,保留低精度的结果进行比较,忽略细小的进度差距。例如格式化输出、四舍五入等
//格式化
double result = 0.1 * 0.2;
System.out.println(String.format("%.2f", result)); // 输出:0.02,保留两位小数//四舍五入 乘法结果乘以100后使用Math.round()进行四舍五入,然后除以100.0来保留两位小数
double result = 0.1 * 0.2;
System.out.println(String.format("%.2f", result)); // 输出:0.02,保留两位小数//四舍五入,setScale()方法设置保留小数的位数,并指定舍入模式为RoundingMode.HALF_UP进行四舍五入
BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.2");
BigDecimal result = num1.multiply(num2).setScale(2, RoundingMode.HALF_UP);
System.out.println(result); // 输出:0.02,保留两位小数
备注:BigDecimal的正确使用,如下的三个例子中,可以发现第一种实现方式一样存在精度问题,这是为什么勒?
我们查看valueOf的源码发现,其首先将浮点数转换成字符串,也就是上面我们的第三种方式
究其原因,浮点数类型(如 double)是基于二进制表示的,而某些十进制数无法精确表示为有限长度的二进制小数,new BigDecimal(0.1)实际上是将 double 类型的值转换为 BigDecimal,而new BigDecimal(“0.1”)会将传入的参数作为字符串进行精确表示。
当我们使用浮点数参数,代码扫描工具会提示,使用valueOf替换,而使用字符串参数则不会提示。
3包装类型
在java中,我们看一下基本数据类型有8种。
整数类型:
byte:8位有符号整数,取值范围为-128到127。
short:16位有符号整数,取值范围为-32,768到32,767。
int:32位有符号整数,取值范围为约-21亿到21亿。
long:64位有符号整数,取值范围为约-922万亿亿到922万亿亿。浮点数类型:
float:32位IEEE 754单精度浮点数,取值范围约为±1.4e-45到±3.4e+38,精度约为6-7位小数。
double:64位IEEE 754双精度浮点数,取值范围约为±4.9e-324到±1.8e+308,精度约为15位小数。字符类型:
char:16位Unicode字符,表示单个字符。布尔类型:
boolean:表示真或假的值,只有两个取值:true和false。
每个基本数据类型都有对应的包装类,用于提供额外的功能和操作。以下是基本数据类型及其对应的包装类:
byte:对应的包装类是 Byte
short:对应的包装类是 Short
int:对应的包装类是 Integer
long:对应的包装类是 Long
float:对应的包装类是 Float
double:对应的包装类是 Double
char:对应的包装类是 Character
boolean:对应的包装类是 Boolean
在这里,我们以Integer为例说明,先看下面的代码,如果你以前不知道这个坑,那么铁定会有点无语
1基本类型有默认值
现象:num0_1和num0_2都没有进行赋值,直接打印值时,基本类型的num0_1为0,包装类型的num0_2为null,
原因:基本类型会有一个默认值,而包装类型则没有默认值。
建议:使用基本类型时,避免使用默认值。使用包装类型时,赋予初始值,防止空指针。
例如:使用int表示状态:0表示成功, 1表示失败,此时有个问题,不管是设置成功状态,还是状态没有设置值,其值都是0, 因此可以改为,用1表示成功,2表示失败
2==比较
现象:num1和num2都是127,使用==比较,结果为true,而num5和num6都是128,结果却为false
原因:使用 == 运算符比较两个对象时,会比较它们的引用是否指向同一个对象。这意味着如果两个包装类型对象的引用指向相同的对象,则 == 操作符返回 true,否则返回 false。
对于整数类型的包装类 Byte、Short、Integer 和 Long,Java提供了一个对象缓存池(Object Pool),范围是 -128 到 127。
这意味着当创建一个值在这个范围内的整数类型包装类对象时,会从缓存池中获取已存在的对象,而不是创建新的对象。
当 num1 和 num2 的值都为 127 时,它们在范围内,并且 Byte、Short、Integer 和 Long 类型的对象在范围内的情况下会被缓存。因此,使用 == 比较时,它们引用的是相同的对象,所以结果为 true。
当 num5 和 num6 的值都为 128 时,它们超出了缓存池的范围。在这种情况下,每次创建包装类型对象时,都会创建一个新的对象。因此,虽然它们的值相同,但它们引用的是不同的对象,所以使用 == 比较时结果为 false。
建议:使用包装类提供的 equals() 方法,它会比较对象的值而不仅仅是引用
3构造方法
现象:num3和num4也是127,也在缓存范围值内,==比较结果是false
原因:Integer的构造方法中,并没有使用缓存,缓存是在valueOf方法中使用的,因此是用new Integer(127) 并不会用缓存
建议:使用包装类提供的 equals() 方法,它会比较对象的值而不仅仅是引用
我的IDE环境中,安装了一些代码扫描插件,我们可以看到,此时提示我们,String和包装类型的比较应该使用equals()
4Java8 Stream
Java8中的Stream可以简化集合数据的处理和操作,最常见的排序,过滤,去重等,这里我们以排序为例进行操作
filter(Predicate):根据指定条件过滤流中的元素。
map(Function):将流中的每个元素映射到另一个元素。
sorted():对流中的元素进行排序。
distinct():去除流中的重复元素。
limit(n):限制流中元素的数量为 n。
skip(n):跳过流中的前 n 个元素。
我们按照年龄进行排序后,然后取第一个元素,将第一个用户的年龄修改为100,我们发现,原始数据也被修改了
原因:过滤后的集合中,保存的是对象的引用,该引用只有一份数据。也就是说,只要有一个地方,把该引用对象的成员变量的值,做修改了,其他地方也会同步修改。
5日期格式化
正确的日期格式化:yyyy-MM-dd
错误的日期格式化:YYYY-MM-dd
我们分别使用SimpleDateFormat和java8中的DateTimeFormatter来格式化,有些时间格式化是正确的,有些时间格式化又是错误的。
先看一个时间,2023年12月30日(格式正确的时间)
在看一个时间,2023年12月31日(格式错误的时间)
因此,如果你使用的日期格式如果为YYYY-MM-dd,那就是有时时间对,有时时间不对,够你排查喝一壶了