99%的Java程序员会踩的六个坑

千万小心,99%的Java程序员会踩这些坑 作为Java程序员的你,不知道有没有踩过一些基础知识的坑。有时候,某个bug查了半天,最后发现竟然是一个低级错误。有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了。有时候,你可能会看着某行代码目瞪口呆,心里想这行代码为什么会出错?今天跟大家一起聊聊99%的Java程序员踩过,或者即将踩的6个坑。如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。求一键三连关注公众号【苏三说技术】,在公众号中回复ttpshttps。... 阅读详情

前言

作为Java程序员的你,不知道有没有踩过一些基础知识的坑。

有时候,某个bug,你查了半天,最后发现竟然是一个非常低级的错误。

有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了。

有时候,你可能会看着某行代码目瞪口呆,心里想:这行代码为什么会出错?

今天跟大家一起聊聊99%的Java程序员踩过,或者即将踩的6个坑。

1、用==号比较的坑

不知道你在项目中有没有见过,有些同事对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的构造方法:

它其实并没有用到缓存。

那么缓存是在哪里用的?

答案在valueOf方法中:

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

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));

运行结果为true。

2、Objects.equals的坑

假设现在有这样一个需求:判断当前登录的用户,如果是我们指定的系统管理员,则发送一封邮件。系统管理员没有特殊的字段标识,他的用户id=888,在开发、测试、生产环境中该值都是一样的。

这个需求真的太容易实现了:

UserInfo userInfo = CurrentUser.getUserInfo();
if(Objects.isNull(userInfo)) {
   log.info("请先登录");
   return;
}
if(Objects.equals(userInfo.getId(),888L)) {
   sendEmail(userInfo):
}

从当前登录用户的上下文中获取用户信息,判断一下,如果用户信息为空,则直接返回。

如果获取到的用户信息不为空,接下来判断用户id是否等于888。

  • 如果等于888,则发送邮件。
  • 如果不等于888,则啥事也不干。

当我们用id=888的系统管理员账号登录之后,做了相关操作,满怀期待的准备收邮件的时候,却发现收了个寂寞。

后来,发现UserInfo类是这样定义的:

@Data
public class UserInfo {
    private Integer id;
    private String name;
    private Integer age;
    private String address;
}

此时,有些小伙伴可能会说:没看出什么问题呀。

但我要说的是这个代码确实有问题。

什么问题呢?

下面我们重点看看它的equals方法:

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

equals方法的判断逻辑如下:

  1. 该方法先判断对象a和b的引用是否相等,如果相等则直接返回true。
  2. 如果引用不相等,则判断a是否为空,如果a为空则返回false。
  3. 如果a不为空,调用对象的equals方法进一步判断值是否相等。

这就要从Integer的equals方法说起来了。

它的equals方法具体代码如下:

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

先判断参数obj是否是Integer类型,如果不是,则直接返回false。如果是Integer类型,再进一步判断int值是否相等。

而上面这个例子中b是long类型,所以Integer的equals方法直接返回了false。

也就是说,如果调用了Integer的equals方法,必须要求入参也是Integer类型,否则该方法会直接返回false。

除此之外,还有Byte、Short、Double、Float、Boolean和Character也有类似的equals方法判断逻辑。

常见的坑有:

  1. Long类型和Integer类型比较,比如:用户id的场景。
  2. Byte类型和Integer类型比较,比如:状态判断的场景。
  3. Double类型和Integer类型比较,比如:金额为0的判断场景。

如果你想进一步了解Objects.equals方法的问题,可以看看我的另一篇文章《​​Objects.equals有坑​​》。

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的构造方法上有这样一段描述:

大致的意思是此构造函数的结果可能不可预测,可能会出现创建时为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、Java8 filter的坑

对于Java8中的Stream用法,大家肯定再熟悉不过了。

我们通过对集合的Stream操作,可以实现:遍历集合、过滤数据、排序、判断、转换集合等等,N多功能。

这里重点说说数据的过滤。

在没有Java8之前,我们过滤数据一般是这样做的:

public List<User> filterUser(List<User> userList) {
    if(CollectionUtils.isEmpty(userList)) {
        return Collections.emptyList();
    }   
    List<User> resultList = Lists.newArrayList();
    for(User user: userList) {
        if(user.getId() > 1000 && user.getAge() > 18)   {
           resultList.add(user);
        }
    }
    return resultList;
}

通常需要另一个集合辅助完成这个功能。

但如果使用Java8的filter功能,代码会变得简洁很多,例如:

public List<User> filterUser(List<User> userList) {
    if(CollectionUtils.isEmpty(userList)) {
        return Collections.emptyList();
    }   
    return userList.stream()
    .filter(user -> user.getId() > 1000 && user.getAge() > 18)
    .collect(Collectors.toList());
}

代码简化了很多,完美。

但如果你对过滤后的数据,做修改了:

List<User> userList = queryUser();
List<User> filterList = filterUser(userList);
for(User user: filterList) {
   user.setName(user.getName() + "测试");
}
for(User user: userList) {
   System.out.println(user.getName());
}

你当时可能只是想修改过滤后的数据,但实际上,你会把元素数据一同修改了。

意不意外,惊不惊喜?

其根本原因是:过滤后的集合中,保存的是对象的引用,该引用只有一份数据。

也就是说,只要有一个地方,把该引用对象的成员变量的值,做修改了,其他地方也会同步修改。

如下图所示:

5、自动拆箱的坑

Java5之后,提供了自动装箱和自动拆箱的功能。

自动装箱是指:JDK会把基本类型,自动变成包装类型。

比如:

Integer integer = 1;

等价于:

Integer integer = new Integer(1);

而自动拆箱是指:JDK会把包装类型,自动转换成基本类型。

例如:

Integer integer = new Integer(2);
int sum = integer + 5;

等价于:

Integer integer = new Integer(2);
int sum = integer.intValue() + 5;

但实际工作中,我们在使用自动拆箱时,往往忘记了判空,导致出现​ ​NullPointerException​ ​异常。

(1)运算

很多时候,我们需要对传入的数据进行计算,例如:

public class Test2 {
    public static void main(String[] args) {
        System.out.println(add(new Integer(1), new Integer(2)));
    }
    private static Integer add(Integer a, Integer b) {
        return a + b;
    }
}

如果传入了null值:

System.out.println(add(null, new Integer(2)));

则会直接报错。

(2)传参

有时候,我们定义的某个方法是基本类型,但实际上传入了包装类,比如:

public static void main(String[] args) {
    Integer a = new Integer(1);
    Integer b = null;
    System.out.println(add(a, b));
}

private static Integer add(int a, int b) {
    return a + b;
}

如果出现add方法报NullPointerException异常,你可能会懵逼,int类型怎么会出现空指针异常呢?

其实,这个问题出在:Integer类型的参数,其实际传入值为null,JDK字段拆箱,调用了它的intValue方法导致的问题。

6、replace的坑

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

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

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

jdk的官方给出了答案。

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

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

replace有两个重载的方法。

  • 其中一个方法的参数:char oldChar 和 char newChar,支持字符的替换。
source.replace('A', 'B')
  • 另一个方法的参数是:CharSequence target 和 CharSequence replacement,支持字符串的替换。
source.replace("A", "B")

而replaceAll方法的参数是:String regex 和 String replacement,即基于正则表达式的替换。

例如对普通字符串进行替换:

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

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

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

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

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

小伙们看到看到二者的区别了没?使用replace方法无需对特殊字符进行转义。

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

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

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

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

这时可以使用replaceFirst方法:

VNC Viewer建立多个图形界面,来远程访问同一个服务器(Ubuntu) 如题。目前已经有了一个VNC viewer连接,那么如果再开一个图形界面呢? 思路主要是:新建一个用户,从这个用户新建一个vnc连接,然后访问图形界面。 阅读详情

相关推荐

halcon C++编程 第4讲 HObject/HImage Mat相互转换(tcy)

网上有很多相互转换程序,但并不知那个正确,本文对相互转换后的结果和原对象比较是否相同判断转换是否正确。经测试ok.本文包括转换代码及测试是否相同。 1.转换程序: /************************************ HObject转为Mat:time:2021/3/16 V1.0 *************************************/ cv::Mat HalconClass::HObjectToMat(const HalconCpp::HObject&..

tcy-阿春 4935

面试官:为什么Integer用==比较时127相等而128不相等?

这个几乎是Java 5引入自动装箱和自动拆箱后,很多人都会遇到(而且不止一次),而又完全摸不着头脑的坑。虽然已有很多文章分析了原因,但鉴于我这次还差点坑了同学,还是纪录下来长点记性。 问题描述 例一 来个简单点的例子 public static void main(String[] args) { for (int i = 0; i < 150; i++) { Integer a = i; Integer b = i; System.o

Jsono8的博客 1271

WIFI基础入门--802.11g--增强速率物理层ERP--15

WIFI基础入门--802.11--增强速率物理层--191.802.11g的组件保护机制 1.802.11g的组件 802.11g其实是将好几种物理层规范合而为一。虽然统称为增强速率物理层(Extended Rate PHY简称ERP),但是实际上ERP有好几种不同的方式: ERP-DSSS与ERP-CCK 这两种模式向下兼容于原始直接序列规范(1Mbps与2Mbps),以及所提到的802.11...

香蕉一号 4619

跳过这六个坑你就领先绝大部分Java程序员

作为Java程序员的你,不知道有没有踩过一些基础知识的坑。有时候,某个bug,你查了半天,最后发现竟然是一个非常低级的错误。有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了。有时候,你可能会看着某行代码目瞪口呆,心里想这行代码为什么会出错?今天跟大家一起聊聊99%的Java程序员踩过,或者即将踩的6个坑。...

m0_48922996的博客 1997

多目标跟踪(MOT)中的卡尔曼滤波(Kalman filter)和匈牙利(Hungarian)算法详解

多目标跟踪(MOT)中的卡尔曼滤波(Kalman filter)和匈牙利(Hungarian)算法详解 1. 概览 在开始具体讨论卡尔曼滤波和匈牙利算法之前,首先我们来看一下基于检测的目标跟踪算法的大致流程。 2. 卡尔曼滤波(Kalman filter) 目标跟踪中使用的就是最基础的 Kalman filter 算法。这里也仅仅讨论最基础的 Kalman filter 算法。 卡尔曼滤波算法的过程很简单,如下图所示。最核心的两个步骤就是预测和更新(下图的 correct)。 在目标跟踪任务中,目标的

生命在于折腾! 2万+

《LabVIEW ZYNQ实战指南》第6章>>案例解析:基于USB WiFi模块的ZYNQ无线通信部署与Linux RT开发

本文详细解析了如何为ZYNQ开发板部署USB WiFi模块,实现无线通信。通过LabVIEW Linux RT环境,利用封装好的驱动VI,开发者可轻松配置ZYNQ连接无线网络,摆脱网线束缚,实现程序无线部署与远程调试,为工业物联网应用奠定基础。

app77的博客 755

Java8 新特性踩坑篇,java.util.NoSuchElementException: No value present

今天测试老师在测试功能的时候,发现功能提交时居然报No value present错误了。而且还是在其它周边系统上,刚开始以为不是我们系统应该跟我们没关系,直到对方开发老师找过来查日志后,才发现是我写的代码有问题,哭了,还好是测试阶段;原因是我使用了Optional调用Get方法前没有先进行isPresent()判断是否为空,所以导致整个功能报废了;...

猪大肠的博客 6464

快速玩转 Llama2!机器学习 PAI 最佳实践(一)—低代码 Lora 微调及部署

本实践将采用阿里云机器学习平台PAI-快速开始模块针对 Llama-2-7b-chat 进行开发。PAI-快速开始支持基于开源模型的低代码训练、布署和推理全流程,适合想要快速开箱体验预训练模型的开发者。

阿里云云栖号 1767

new Integer(1) == new Integer(1) 是否相等、、、

为什么80%的码农都做不了架构师?>>> ...

weixin_34008933的博客 1709

Ubuntu服务器通过IPV6端口转发搭建幻兽帕鲁私服实战

本文详细介绍了在Ubuntu服务器上通过IPV6端口转发搭建幻兽帕鲁私服的完整教程。针对只有公网IPV6地址的服务器环境,提供了从硬件检查、端口转发工具配置到游戏服务器安装的全流程解决方案,帮助玩家轻松实现多人联机游戏体验。

weixin_30652491的博客 490

Codex入门24-效率倍增秘籍(终极总结:10个让你开发速度翻倍的Codex高级技巧)

专栏收官之作!经过前23篇的系统学习,你已经掌握了 Codex CLI 的核心用法。本文浓缩10个实战验证的高级技巧:管道化提示词将复杂任务拆成递进指令链,模板化 SKILL.md 打造个人技能库,Shell 别名让操作快如闪电,多模型切换省钱又高效……这不是理论课,每个技巧都配有可直接运行的代码。掌握全部10招,构建你的 AI 开发工作流闭环,让开发速度真正翻倍!

yueyeguzhuo的专栏 191

javastring类型占几个字节,讲的太透彻了

Java虚拟机内存模型 Java虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致! Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数)。更多信息我们会在后面的《深入JVM—JVM类执行机制中详细解说》。 在之前,我们也已经提到,JVM的逻辑内存模型如下: 我们现在来逐个的看下每个到底是做什么的! 1、程序计数器 程序计数器

m0_59183028的博客 2226

FlashBootloader01-基于UDS的Flash Bootloader【架构篇】

文章目录1 Flash Bootloader的依赖项2 Flash Bootloader中使用的UDS的主要功能3 Flash Bootloader更新固件3.1 初始化MCU3.2 刷写流程 1 Flash Bootloader的依赖项 通常Flash Bootloader由以下四方面组成: UDS协议 上位机「编写Bootloader Sequence」 通信驱动(如CAN ) Flash Driver 2 Flash Bootloader中使用的UDS的主要功能 1. 10 02服务 客户端

3592

99%的Java程序员会踩的6个坑

Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!作为Java程序员的你,不知道有没有踩过一些基础知识的坑。有时候,某个bug,你查了半天,最后发现竟然是一个非常低级的错误。有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了。有时候,你可能会看着某行代码目瞪口呆,心里想:这行代码为什么会出错?今天跟大家一起聊聊99%的Java程序员踩过,或者...

HollisChuang's Blog 664

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

90%的Java程序员会踩的6个坑

作为Java程序员的你,不知道有没有踩过一些基础知识的坑。有时候,某个bug,你查了半天,最后发现竟然是一个非常低级的错误。有时候,某些代码,这一批数据功能正常,但换了一批数据出现异常了。有时候,你可能会看着某行代码目瞪口呆,心里想:这行代码为什么会出错?今天跟大家一起聊聊99%的Java程序员踩过,或者即将踩的6个坑。...

吴名氏的博客 15万+

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

盘点一下,99%的Java程序员会踩的坑

作为Java程序员的你,不知道有没有踩过一些基础知识的坑。有时候,某个bug查了半天,最后发现竟然是一个低级错误。有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了。有时候,你可能会看着某行代码目瞪口呆,心里想这行代码为什么会出错?今天跟大家一起聊聊99%的Java程序员踩过,或者即将踩的6个坑。...

m0_67322837的博客 216

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

Java程序员容易踩中的6个坑(荣耀典藏版)

今天跟大家一起聊聊99%的Java程序员踩过,或者即将踩的6个坑。看看你有没有中招!!!!

人生若只初相见@的博客 2万+

大部分的Java程序员会踩的六个坑

作为Java程序员的你,不知道有没有踩过一些基础知识的坑。 有时候,某个bug,你查了半天,最后发现竟然是一个非常低级的错误。 有时候,某些代码,这一批数据功能正常,但换了一批数据就出现...

YYniannian的博客 291
上一篇: 看完拿捏秋招,368人已进入大厂,这份2022最新Java面试文
下一篇: Java 中的面向数据编程
程序员职业指南
博客等级 码龄4年 2138粉丝 960原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值