java中那些让你傻傻分不清楚的小细节

Wesley13
• 阅读 361

扩展右上角“设为星标”能第一时间看到好文章

大家好,我是苏三,又和大家见面了。

祝大家新年快乐,身体健康,财源滚滚,万事如意。

最近有篇文章在开源中国上火了,让我挺惊喜的。

java中那些让你傻傻分不清楚的小细节

我以前在上面发表文章,一般只有几个阅读量,稍微好点的有几十,如果被推荐也只有几百。像这种有3.2W阅读的情况,还是头一次遇到,真的活久见。非常感谢源码中国平台,让我的文章可以被更多的人看见。

前言

最近我们通过sonar静态代码检测,同时配合人工代码review,发现了项目中很多代码问题。除了常规的bug和安全漏洞之外,还有几处方法用法错误,引起了我极大的兴趣。我为什么会对这几个方法这么感兴趣呢?因为它们极具迷惑性,可能会让我们傻傻分不清楚。

1. replace会替换所有字符?

很多时候我们在使用字符串时,想把字符串比如:ATYSDFA*Y中的字符A替换成字符B,第一个想到的可能是使用replace方法。

如果想把所有的A都替换成B,很显然可以用replaceAll方法,因为非常直观,光从方法名就能猜出它的用途。

那么问题来了:replace方法会替换所有匹配字符吗?

jdk的官方给出了答案。

java中那些让你傻傻分不清楚的小细节

该方法会替换每一个匹配的字符串。

既然replacereplaceAll都能替换所有匹配字符,那么他们有啥区别呢?

  1. replace有两个重载的方法。

其中一个方法的参数:char oldChar 和 char newChar,支持字符的替换。

source.replace('A', 'B')

另一个方法的参数是:CharSequence target 和 CharSequence replacement,支持字符串的替换。

source.replace("A", "B")
  1. replaceAll方法的参数是:String regex 和 String replacement,基于正则表达式的替换。普通字符串替换:

    source.replaceAll("A", "B")

正则表达替换(将*替换成C):

source.replaceAll("\\*", "C")

顺便说一下,将*替换成C使用replace方法也可以实现:

source.replace("*", "C")

无需对特殊字符进行转义。

不过,千万注意,切勿使用如下写法:

source.replace("\\*", "C")

这种写法会导致字符串无法替换。

还有个小问题,如果我只想替换第一个匹配的字符串该怎么办?

这时可以使用replaceFirst方法:

source.replaceFirst("A", "B")

2. Integer不能用==判断相等?

不知道你在项目中有没有见过,有些同事对Integer类型的两个参数使用==比较是否相等?

反正我见过的,那么这种用法对吗?

我的回答是看具体场景,不能说一定对,或不对。

有些状态字段,比如:orderStatus有:-1(未下单),0(已下单),1(已支付),2(已完成),3(取消),5种状态。

这时如果用==判断是否相等:

  Integer orderStatus1 = new Integer(1);  Integer orderStatus2 = new Integer(1);  System.out.println(orderStatus1 == orderStatus2);

返回结果会是true吗?

答案:是false

有些同学可能会反驳,Integer中不是有范围是:-128-127的缓存吗?

为什么是false

先看看Integer的构造方法:java中那些让你傻傻分不清楚的小细节 它其实并没有用到缓存。

那么缓存是在哪里用的?

答案在valueOf方法中:java中那些让你傻傻分不清楚的小细节

如果上面的判断改成这样:

String orderStatus1 = new String("1");String orderStatus2 = new String("1");System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));

返回结果会是true吗?

答案:还真是true

我们要养成良好编码习惯,尽量少用==判断两个Integer类型数据是否相等,只有在上述非常特殊的场景下才相等。

而应该改成使用equals方法判断:

Integer orderStatus1 = new Integer(1);Integer orderStatus2 = new Integer(1);System.out.println(orderStatus1.equals(orderStatus2));

3. 使用BigDecimal就不丢失精度?

通常我们会把一些小数类型的字段(比如:金额),定义成BigDecimal,而不是Double,避免丢失精度问题。

使用Double时可能会有这种场景:

double amount1 = 0.02;double amount2 = 0.03;System.out.println(amount2 - amount1);

正常情况下预计amount2 - amount1应该等于0.01

但是执行结果,却为:

0.009999999999999998

实际结果小于预计结果。

Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。

常识告诉我们使用BigDecimal能避免丢失精度。

但是使用BigDecimal能避免丢失精度吗?

答案是否定的。

为什么?

BigDecimal amount1 = new BigDecimal(0.02);BigDecimal amount2 = new BigDecimal(0.03);System.out.println(amount2.subtract(amount1));

这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。

结果:

0.0099999999999999984734433411404097569175064563751220703125

不科学呀,为啥还是丢失精度了?

jdk中BigDecimal的构造方法上有这样一段描述:

java中那些让你傻傻分不清楚的小细节

大致的意思是此构造函数的结果可能不可预测,可能会出现创建时为0.1,但实际是0.1000000000000000055511151231257827021181583404541015625的情况。

由此可见,使用BigDecimal构造函数初始化对象,也会丢失精度。

那么,如何才能不丢失精度呢?

BigDecimal amount1 = new BigDecimal(Double.toString(0.02));BigDecimal amount2 = new BigDecimal(Double.toString(0.03));System.out.println(amount2.subtract(amount1));

使用Double.toString方法对double类型的小数进行转换,这样能保证精度不丢失。

其实,还有更好的办法:

BigDecimal amount1 = BigDecimal.valueOf(0.02);BigDecimal amount2 = BigDecimal.valueOf(0.03);System.out.println(amount2.subtract(amount1));

使用BigDecimal.valueOf方法初始化BigDecimal类型参数,也能保证精度不丢失。在新版的阿里巴巴开发手册中,也推荐使用这种方式创建BigDecimal参数。

4. 字符串拼接不能用String?

String类型的字符串被称为不可变序列,也就是说该对象的数据被定义好后就不能修改了,如果要修改则需要创建新对象。

String a = "123";String b = "456";String c = a + b;System.out.println(c);

在大量字符串拼接的场景中,如果对象被定义成String类型,会产生很多无用的中间对象,浪费内存空间,效率低。

这时,我们可以用更高效的可变字符序列:StringBuilderStringBuffer,来定义对象。

那么,StringBuilderStringBuffer有啥区别?

StringBuffer对各主要方法加了synchronized关键字,而StringBuilder没有。所以,StringBuffer是线程安全的,而StringBuilder不是。

其实,我们很少会出现需要在多线程下拼接字符串的场景,所以StringBuffer实际上用得非常少。一般情况下,拼接字符串时我们推荐使用StringBuilder,通过它的append方法追加字符串,它只会产生一个对象,而且没有加锁,效率较高。

String a = "123";String b = "456";StringBuilder c = new StringBuilder();c.append(a).append(b);System.out.println(c);

接下来,关键问题来了:字符串拼接时使用String类型的对象,效率一定比StringBuilder类型的对象低?

答案是否定的。

为什么?

使用javap -c StringTest命令反编译:

java中那些让你傻傻分不清楚的小细节

从图中能看出定义了两个String类型的参数,又定义了一个StringBuilder类的参数,然后两次使用append方法追加字符串。

如果代码是这样的:

String a = "123";String b = "789";String c = a + b;System.out.println(c);

使用javap -c StringTest命令反编译的结果会怎样呢?

java中那些让你傻傻分不清楚的小细节

我们会惊讶的发现,同样定义了两个String类型的参数,又定义了一个StringBuilder类的参数,然后两次使用append方法追加字符串。跟上面的结果是一样的。

其实从jdk5开始,java就对String类型的字符串的+操作做了优化,该操作编译成字节码文件后会被优化为StringBuilder的append操作。

5. isEmpty和isBlank的区别

我们在对字符串进行操作的时候,需要经常判断该字符串是否为空。如果没有借助任何工具,我们一般是这样判断的:

if (null != source && !"".equals(source)) {    System.out.println("not empty");}

但是如果每次都这样判断,会有些麻烦,所以很多jar包都对字符串判空做了封装。目前市面上主流的工具有:

  • spring中的StringUtils

  • jdbc中的StringUtils

  • apache common3中的StringUtils

不过spring中的StringUtils类只有isEmpty方法,没有isNotEmpty方法。

jdbc中的StringUtils类只有isNullOrEmpty方法,也没有isNotNullOrEmpty方法。

所以在这里强烈推荐一下apache common3中的StringUtils类,它里面包含了很多实用的判空方法:isEmpty、isBlank、isNotEmpty、isNotBlank等,还有其他字符串处理方法。

问题来了,isEmptyisBlank有啥区别?

使用isEmpty方法判断:

 StringUtils.isEmpty(null)      = true StringUtils.isEmpty("")        = true StringUtils.isEmpty(" ")       = false StringUtils.isEmpty("bob")     = false StringUtils.isEmpty("  bob  ") = false

使用isBlank方法判断:

StringUtils.isBlank(null)      = trueStringUtils.isBlank("")        = trueStringUtils.isBlank(" ")       = trueStringUtils.isBlank("bob")     = falseStringUtils.isBlank("  bob  ") = false

两个方法关键的区别在于这种" "空字符串的情况,isNotEmpty返回false,而isBlank返回true。

6. mapper查询结果要判空?

有次代码review的时候,当时有个同事说这里的判空可以去掉,让我记忆犹新:

List<User> list = userMapper.query(search);if(CollectionUtils.isNotEmpty(list)) {    List<Long> idList = list.stream().map(User::getId).collect(Collectors.toList());}

因为按常理,一般调用方法查询出来的集合,可能为null,需要判空的。但是,这里比较特殊,我查了一下mybatis的源码,这个判空的代码还真的可以去掉。

怎么回事呢?

mybatis的查询方法最终都会调到DefaultResultSetHandler类的handleResultSets方法:java中那些让你傻傻分不清楚的小细节

java中那些让你傻傻分不清楚的小细节 该方法会返回一个multipleResultsList集合对象,在方法刚开始就new出来了,肯定是不会为空。

所以,如果你在项目的代码中看到有人直接使用查询出的结果,不判空也不要惊讶:

List<User> list = userMapper.query(search);List<Long> idList = list.stream().map(User::getId).collect(Collectors.toList());

因为mapper底层已经处理过的,它不会出现空指针异常。

7. indexOf方法的正确用法

有次在review别人代码的时候,看到有个地方indexOf使用了这种写法,让我印象比较深刻:

String source = "#ATYSDFA*Y";if(source.indexOf("#") > 0) {    System.out.println("do something");}

你们说这段代码会打印出do something吗?

答案是否定的。

为什么呢?

jdk官方说了不存在的情况会返回-1java中那些让你傻傻分不清楚的小细节 indexOf方法返回的是指定元素在字符串中的位置,从0开始。而上面的例子#在字符串的第一个位置,所以调用indexOf方法后的值其实是0。所以,条件是false,不会打印do something

如果想通过indexOf判断某个元素是否存在时,要用:

if(source.indexOf("#") > -1) {    System.out.println("do something");}

其实,还有更优雅的contains方法:

if(source.contains("#")) {   System.out.println("do something");}

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

 个人公众号

java中那些让你傻傻分不清楚的小细节

 个人微信

java中那些让你傻傻分不清楚的小细节

本文分享自微信公众号 - 苏三说技术(gh_9f551dfec941)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
技术小男生 技术小男生
4个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi/etc/profile2:按字母键i进入编辑模式,在最底部添加内容:JAVAHOME/opt/jdk1.8.0152CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jarPATH$JAVAHOME/bin:$PATH3:生效配置
光头强的博客 光头强的博客
4个月前
Java面向对象试题
1、请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
1年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
Wesley13 Wesley13
1年前
Java爬虫之JSoup使用教程
title:Java爬虫之JSoup使用教程date:201812248:00:000800update:201812248:00:000800author:mecover:https://imgblog.csdnimg.cn/20181224144920712(https://www.oschin
Wesley13 Wesley13
1年前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序selectfromtable_nameorderiddesc;2.按照指定(多个)字段排序selectfromtable_nameorderiddesc,statusdesc;3.按照指定字段和规则排序selec
Stella981 Stella981
1年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Stella981 Stella981
1年前
Angular material mat
IconIconNamematiconcode_add\_comment_addcommenticon<maticonadd\_comment</maticon_attach\_file_attachfileicon<maticonattach\_file</maticon_attach\
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
helloworld_34035044 helloworld_34035044
7个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为