21 防止死循环

有些小伙伴看到这个标题,可能会感到有点意外,代码中不是应该避免死循环吗?为啥还是会产生死循环?

殊不知有些死循环是我们自己写的,例如下面这段代码:


while(true) {if(condition) {break;}System.out.println("do samething");
}

这里使用了 while(true) 的循环调用,这种写法在 CAS 自旋锁中使用比较多。

当满足 condition 等于 true 的时候,则自动退出该循环。

如果 condition 条件非常复杂,一旦出现判断不正确,或者少写了一些逻辑判断,就可能在某些场景下出现死循环的问题。

出现死循环,大概率是开发人员人为的 bug 导致的,不过这种情况很容易被测出来。

还有一种隐藏的比较深的死循环,是由于代码写的不太严谨导致的。如果用正常数据,可能测不出问题,但一旦出现异常数据,就会立即出现死循环。

其实,还有另一种死循环无限递归。

如果想要打印某个分类的所有父分类,可以用类似这样的递归方法实现:

public void printCategory(Category category) {if(category == null || category.getParentId() == null) {return;} System.out.println("父分类名称:"+ category.getName());Category parent = categoryMapper.getCategoryById(category.getParentId());printCategory(parent);
}

正常情况下,这段代码是没有问题的。

但如果某次有人误操作,把某个分类的 parentId 指向了它自己,这样就会出现无限递归的情况。导致接口一直不能返回数据,最终会发生堆栈溢出。

建议写递归方法时,设定一个递归的深度,比如:分类最大等级有4级,则深度可以设置为4。然后在递归方法中做判断,如果深度大于4时,则自动返回,这样就能避免无限循环的情况。

22 注意 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 参数。

23 尽可能复用代码

Ctrl + C 和 Ctrl + V 可能是程序员使用最多的快捷键了。

没错,我们是大自然的搬运工。哈哈哈。

在项目初期,我们使用这种工作模式,确实可以提高一些工作效率,可以少写(实际上是少敲)很多代码。

但它带来的问题是,会出现大量的代码重复。例如:


@Service
@Slf4j
public class TestService1 {public void test1()  {addLog("test1");}private void addLog(String info) {if (log.isInfoEnabled()) {log.info("info:{}", info);}}
}@Service
@Slf4j
public class TestService2 {public void test2()  {addLog("test2");}private void addLog(String info) {if (log.isInfoEnabled()) {log.info("info:{}", info);}}
}@Service
@Slf4j
public class TestService3 {public void test3()  {addLog("test3");}private void addLog(String info) {if (log.isInfoEnabled()) {log.info("info:{}", info);}}
}

在 TestService1、TestService2、TestService3 类中,都有一个 addLog 方法用于添加日志。

本来该功能用得好好的,直到有一天,线上出现了一个事故服务器磁盘满了。

原因是打印的日志太多,记了很多没必要的日志,比如查询接口的所有返回值,大对象的具体打印等。

没办法,只能将 addLog 方法改成只记录 debug 日志。

于是乎,你需要全文搜索,addLog 方法去修改,改成如下代码:


private void addLog(String info) {if (log.isDebugEnabled()) {log.debug("debug:{}", info);}
}

这里是有三个类中需要修改这段代码,但如果实际工作中有三十个、三百个类需要修改,会让你非常痛苦。改错了,或者改漏了,都会埋下隐患,把自己坑了。

为何不把这种功能的代码提取出来,放到某个工具类中呢?


@Slf4j
public class LogUtil {private LogUtil() {throw new RuntimeException("初始化失败");}public static void addLog(String info) {if (log.isDebugEnabled()) {log.debug("debug:{}", info);}}
}

然后,在其他的地方,只需要调用。


@Service
@Slf4j
public class TestService1 {public void test1()  {LogUtil.addLog("test1");}
}

如果哪天 addLog 的逻辑又要改了,只需要修改 LogUtil 类的 addLog 方法即可。你可以自信满满的修改,不需要再小心翼翼了。

我们写的代码,绝大多数是可维护性的代码,而非一次性的。所以,建议在写代码的过程中,如果出现重复的代码,尽量提取成公共方法。千万别因为项目初期一时的爽快,而给项目埋下隐患,后面的维护成本可能会非常高。

24 foreach 循环中不 remove 元素

我们知道在 Java 中,循环有很多种写法,比如 while、for、foreach 等。


public class Test2 {public static void main(String[] args) {List<String> list = Lists.newArrayList("a","b","c");for (String temp : list) {if ("c".equals(temp)) {list.remove(temp);}}System.out.println(list);}
}

执行结果:


Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)at java.util.ArrayList$Itr.next(ArrayList.java:851)at com.sue.jump.service.test1.Test2.main(Test2.java:24)

这种在 foreach 循环中调用 remove 方法删除元素,可能会报 ConcurrentModificationException 异常。

如果想在遍历集合时,删除其中的元素,可以用 for 循环,例如:


public class Test2 {public static void main(String[] args) {List<String> list = Lists.newArrayList("a","b","c");for (int i = 0; i < list.size(); i++) {String temp = list.get(i);if ("c".equals(temp)) {list.remove(temp);}}System.out.println(list);}
}

执行结果:

[a, b]

25 避免随意打印日志

在我们写代码的时候,打印日志是必不可少的工作之一。

因为日志可以帮我们快速定位问题,判断代码当时真正的执行逻辑。

但打印日志的时候也需要注意,不是说任何时候都要打印日志,比如:


@PostMapping("/query")
public List<User> query(@RequestBody List<Long> ids) {log.info("request params:{}", ids);List<User> userList = userService.query(ids);log.info("response:{}", userList);return userList;
}

对于有些查询接口,在日志中打印出了请求参数和接口返回值。

咋一看没啥问题。

但如果ids中传入值非常多,比如有 1000 个。而该接口被调用的频次又很高,一下子就会打印大量的日志,用不了多久就可能把磁盘空间打满。

如果真的想打印这些日志该怎么办?


@PostMapping("/query")
public List<User> query(@RequestBody List<Long> ids) {if (log.isDebugEnabled()) {log.debug("request params:{}", ids);}List<User> userList = userService.query(ids);if (log.isDebugEnabled()) {log.debug("response:{}", userList);}return userList;
}

使用 isDebugEnabled 判断一下,如果当前的日志级别是 debug 才打印日志。生产环境默认日志级别是 info,在有些紧急情况下,把某个接口或者方法的日志级别改成 debug,打印完我们需要的日志后,又调整回去。

方便我们定位问题,又不会产生大量的垃圾日志,一举两得。

26 比较时把常量写前面

在比较两个参数值是否相等时,通常我们会使用 == 号,或者 equals 方法。

我在第 15 个技巧中说过,使用 == 号比较两个值是否相等时,可能会存在问题,建议使用 equals 方法做比较。

反例:


if(user.getName().equals("苏三")) {System.out.println("找到:"+user.getName());
}

在上面这段代码中,如果 user 对象,或者 user.getName() 方法返回值为 null,则都报 NullPointerException 异常。

那么,如何避免空指针异常呢?

正例:


private static final String FOUND_NAME = "苏三";
...
if(null == user) {return;
}if(FOUND_NAME.equals(user.getName())) {System.out.println("找到:"+user.getName());
}

在使用 equals 做比较时,尽量将常量写在前面,即 equals 方法的左边。

这样即使 user.getName() 返回的数据为 null,equals 方法会直接返回 false,而不再是报空指针异常。

27 名称要见名知意

Java 中没有强制规定参数、方法、类或者包名该怎么起名。但如果我们没有养成良好的起名习惯,随意起名的话,可能会出现很多奇怪的代码。

27.1 有意义的参数名

有时候,我们写代码时为了省事(可以少敲几个字母),参数名起得越简单越好。假如同事 A 写的代码如下:


int a = 1;
int b = 2;
String c = "abc";
boolean b = false;

一段时间之后,同事 A 离职了,同事 B 接手了这段代码。

他此时一脸懵逼,a 是什么意思,b 又是什么意思,还有 c.. .然后心里一万匹草泥马。

给参数起一个有意义的名字,是非常重要的事情,避免给自己或者别人埋坑。

正解:


int supplierCount = 1;
int purchaserCount = 2;
String userName = "abc";
boolean hasSuccess = false;

27.2 见名知意

光起有意义的参数名还不够,我们不能就这点追求。我们起的参数名称最好能够见名知意,不然就会出现这样的情况:


String yongHuMing = "苏三";
String 用户Name = "苏三";
String su3 = "苏三";
String suThree = "苏三";

这几种参数名看起来是不是有点怪怪的?

为啥不定义成国际上通用的(地球人都能看懂)英文单词呢?


String userName = "苏三";
String susan = "苏三";

上面的这两个参数名,基本上大家都能看懂,减少了好多沟通成本。

所以建议在定义不管是参数名、方法名、类名时,优先使用国际上通用的英文单词,更简单直观,减少沟通成本。少用汉子、拼音,或者数字定义名称。

27.3 参数名风格一致

参数名其实有多种风格,列如:


//字母全小写
int suppliercount = 1;//字母全大写
int SUPPLIERCOUNT = 1;//小写字母 + 下划线
int supplier_count = 1;//大写字母 + 下划线
int SUPPLIER_COUNT = 1;//驼峰标识
int supplierCount = 1;

如果某个类中定义了多种风格的参数名称,看起来是不是有点杂乱无章?

所以建议类的成员变量、局部变量和方法参数使用 supplierCount,这种驼峰风格,即:第一个字母小写,后面的每个单词首字母大写。例如:

int supplierCount = 1;

此外,为了好做区分,静态常量建议使用 SUPPLIER_COUNT,即大写字母 + 下划线分隔的参数名。例如:


private static final int SUPPLIER_COUNT = 1;

28 SimpleDateFormat 线程不安全

在 Java8 之前,我们对时间的格式化处理,一般都是用的 SimpleDateFormat 类实现的。

例如:


@Service
public class SimpleDateFormatService {public Date time(String time) throws ParseException {SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return dateFormat.parse(time);}
}

如果你真的这样写,是没问题的。

就怕哪天抽风,你觉得 dateFormat 是一段固定的代码,应该要把它抽取成常量。

于是把代码改成下面的这样:


@Service
public class SimpleDateFormatService {private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public Date time(String time) throws ParseException {return dateFormat.parse(time);}
}

dateFormat 对象被定义成了静态常量,这样就能被所有对象共用。

如果只有一个线程调用 time 方法,也不会出现问题。

但 Serivce 类的方法,往往是被 Controller 类调用的,而 Controller 类的接口方法,则会被 Tomcat 的线程池调用。换句话说,可能会出现多个线程调用同一个 Controller 类的同一个方法,也就是会出现多个线程会同时调用 time 方法。

而 time 方法会调用 SimpleDateFormat 类的 parse 方法:

@Override
public Date parse(String text, ParsePosition pos) {...Date parsedDate;try {parsedDate = calb.establish(calendar).getTime();...} catch (IllegalArgumentException e) {pos.errorIndex = start;pos.index = oldStart;return null;}return parsedDate;
}

该方法会调用 establish 方法:


Calendar establish(Calendar cal) {...//1.清空数据cal.clear();//2.设置时间cal.set(...);//3.返回return cal;
}

其中的步骤 1、2、3 是非原子操作。

但如果 cal 对象是局部变量还好,坏就坏在 parse 方法调用 establish 方法时,传入的 calendar 是 SimpleDateFormat 类的父类 DateFormat 的成员变量:

public abstract class DateFormat extends Forma {....protected Calendar calendar;...
}

这样就可能会出现多个线程,同时修改同一个对象即 dateFormat,它的同一个成员变量即 Calendar 值的情况。

这样可能会出现,某个线程设置好了时间,又被其他的线程修改了,从而出现时间错误的情况。

那么,如何解决这个问题呢?

  • SimpleDateFormat 类的对象不要定义成静态的,可以改成方法的局部变量。

  • 使用 ThreadLocal 保存 SimpleDateFormat 类的数据。

  • 使用Java8 的 DateTimeFormatter 类。

29 少用 Executors 创建线程池

我们都知道 JDK5 之后,提供了 ThreadPoolExecutor 类,用它可以自定义线程池。

线程池的好处有很多,下面主要说说这 3 个方面。

  • 降低资源消耗:避免了频繁的创建线程和销毁线程,可以直接复用已有线程。而我们都知道,创建线程是非常耗时的操作。

  • 提供速度:任务过来之后,因为线程已存在,可以拿来直接使用。

  • 提高线程的可管理性:线程是非常宝贵的资源,如果创建过多的线程,不仅会消耗系统资源,甚至会影响系统的稳定。使用线程池,可以非常方便的创建、管理和监控线程。

当然 JDK 为了我们使用更便捷,专门提供了 Executors 类,给我们快速创建线程池。

该类中包含了很多静态方法:

  • newCachedThreadPool:创建一个可缓冲的线程,如果线程池大小超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newFixedThreadPool:创建一个固定大小的线程池,如果任务数量超过线程池大小,则将多余的任务放到队列中。

  • newScheduledThreadPool:创建一个固定大小,并且能执行定时周期任务的线程池。

  • newSingleThreadExecutor:创建只有一个线程的线程池,保证所有的任务安装顺序执行。

在高并发的场景下,如果大家使用这些静态方法创建线程池,会有一些问题。那么,我们一起看看有哪些问题?

  • newFixedThreadPool:允许请求的队列长度是 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

  • newSingleThreadExecutor:允许请求的队列长度是 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

  • newCachedThreadPool:允许创建的线程数是 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

那我们该怎办呢?

优先推荐使用 ThreadPoolExecutor 类,我们自定义线程池。

具体代码如下:


ExecutorService threadPool = new ThreadPoolExecutor(8, //corePoolSize线程池中核心线程数10, //maximumPoolSize 线程池中最大线程数60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue(500), //队列new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略

顺便说一下,如果是一些低并发场景,使用 Executors 类创建线程池也未尝不可,也不能完全一棍子打死。在这些低并发场景下,很难出现 OOM 问题,所以我们需要根据实际业务场景选择。

30 Arrays.asList 转换的集合别修改

在我们日常工作中,经常需要把数组转换成 List 集合。

因为数组的长度是固定的,不太好扩容,而 List 的长度是可变的,它的长度会根据元素的数量动态扩容。

在 JDK 的 Arrays 类中提供了asList 方法,可以把数组转换成 List。

正例:


String [] array = new String [] {"a","b","c"};
List<String> list = Arrays.asList(array);
for (String str : list) {System.out.println(str);
}

在这个例子中,使用 Arrays.asList 方法将 array 数组,直接转换成了 list。然后在 for 循环中遍历 list,打印出它里面的元素。

如果转换后的 list,只是使用,没新增或修改元素,不会有问题。

反例:


String[] array = new String[]{"a", "b", "c"};
List<String> list = Arrays.asList(array);
list.add("d");
for (String str : list) {System.out.println(str);
}

执行结果:


Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at com.sue.jump.service.test1.Test2.main(Test2.java:24)

会直接报 UnsupportedOperationException 异常。

为什么呢?

答:使用 Arrays.asList 方法转换后的 ArrayList,是 Arrays 类的内部类,并非 java.util 包下我们常用的 ArrayList。

Arrays 类的内部 ArrayList 类,它没有实现父类的 add 和 remove 方法,用的是父类 AbstractList 的默认实现。

我们看看 AbstractList 是如何实现的:


public void add(int index, E element) {throw new UnsupportedOperationException();
}public E remove(int index) {throw new UnsupportedOperationException();
}

该类的 add 和 remove 方法直接抛异常了,因此调用 Arrays 类的内部 ArrayList 类的 add 和 remove 方法,同样会抛异常。

说实话,Java 代码优化是一个比较大的话题,它里面可以优化的点非常多,我没办法一一列举完。在这里只能抛砖引玉,介绍一下比较常见的知识点,更全面的内容,需要小伙伴们自己去思考和探索。

31. 相关文章

一、Java 中代码优化的 30 个小技巧(上)

二、Java 中代码优化的 30 个小技巧(中)

三、Java 中代码优化的 30 个小技巧(下)

Java 中代码优化的 30 个小技巧(下)相关推荐

  1. Java 中代码优化的 30 个小技巧(中)

    11 位运算效率更高 如果你读过 JDK 的源码,比如 ThreadLocal.HashMap 等类,你就会发现,它们的底层都用了位运算. 为什么开发 JDK 的大神们,都喜欢用位运算? 答:因为位运 ...

  2. Java 中代码优化的 30 个小技巧(上)

    前言 今天我们一起聊聊 Java 中代码优化的 30 个小技巧,希望会对你有所帮助. 1 用 String.format 拼接字符串 不知道你有没有拼接过字符串,特别是那种有多个参数,字符串比较长的情 ...

  3. 聊聊我们Java中代码优化的30个小技巧

    今天我们一起聊聊Java中代码优化的30个小技巧,希望会对你有所帮助. 1.用String.format拼接字符串 不知道你有没有拼接过字符串,特别是那种有多个参数,字符串比较长的情况. 比如现在有个 ...

  4. Java中代码优化的30个小技巧

    1.用String.format拼接字符串 String.format方法拼接url请求参数,日志打印等字符串. 但不建议在for循环中用它拼接字符串,因为它的执行效率,比使用+号拼接字符串,或者使用 ...

  5. Java中不可或缺的59个小技巧,贼好用!

    来源:https://blog.dogchao.cn/?p=70 <Effective JavaJava>名著,必读.如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提 ...

  6. Java中char 转化为int小技巧

    char a = '3'; int b = a; 上图这样是可以将char类型强转为int类型,但是的是ASCII值,并不是我们想要的int类型值 char a = '3'; int b = a - ...

  7. java 有趣注释_Java8 中有趣酷炫的小技巧

    https://mp.weixin.qq.com/s/ZlbcfT-fUoVEctSqBeZWcg Java8 中有趣酷炫的小技巧 执行注释 大多数开发人员认为 注释 永远不会在程序中执行,并用于帮助 ...

  8. Java内存管理的9个小技巧

    1.最基本的建议是尽早释放无用对象的引用.如:  ...  A a = new A();  //应用a对象  a = null; //当使用对象a之后主动将其设置为空  -.  注:如果a 是方法的返 ...

  9. echart 数据视图_关于数据可视化图表的制作,你需要关注的30个小技巧

    优秀的数据可视化图表只是罗列.总结数据吗?当然不是!数据可视化其真正的价值是设计出可以被读者轻松理解的数据展示,因此在设计过程中,每一个选择,最终都应落脚于读者的体验,而非图表制作者个人. 今天就给大 ...

最新文章

  1. Go语言连接 zookeeper
  2. 整理一周的Python全品类资料包含100本电子书,还有独家实战项目源码公开!
  3. php 张开收缩显示,js实现可以点击收缩或张开的悬浮窗
  4. 深度学习加持的工业AI质检
  5. Hadoop入门进阶步步高(三-配置Hadoop
  6. 北京小学 计算机派位,北京小升初电脑派位原理详解!和对口直升入学有什么区别?...
  7. 把collections.Counter的计数结果转为pandas的DataFrame
  8. Node.js 环境性能监控探究
  9. RFB-Net论文解读
  10. iPad和iPhone开发的异同
  11. Mplayer播放器程序设计Linux,linux下源码安装mplayer播放器
  12. 红色警戒常用的快捷键
  13. 关于PS CC 不能直接拖图片的问题
  14. abb机器人常见维故障现象
  15. Halcon椭圆测量
  16. 2021届Java开发求职-------面试实战之Vivo提前批
  17. matlab出图时汉字都变成方框,linux下Matlab 2020中文字体方框问题解决方法
  18. Lego Boost打算把所有乐高玩具变成可编程机器人
  19. android glide面试题,Android面试:80%的面试官关于Glide都会问这几个问题!【建议收藏】...
  20. 沐神《动手学深度实战Kaggle比赛:狗的品种识别(ImageNet Dogs)

热门文章

  1. 影响未来的十大网络技术
  2. 三极管与恒流源充放电电路
  3. 小程序下拉刷新没有三个小圆点的加载动画
  4. MATLAB快速读取STL文件
  5. 勇立潮头,推动国产数据库产业崛起—“金兰生态 仓起辉煌·2020人大金仓生态大会暨新战略发布会”在京成功召开...
  6. 2017-2018-2 20179215《密码与安全新技术》第1周作业
  7. 831数据结构与c语言试题,2018年广东工业大学计算机院831数据结构与C语言[专硕]之C程序设计考研核心题库...
  8. Python3.9的保留字彩蛋__peg_parser__
  9. 服务器购买之后要做什么(一)
  10. 利用python进行数据分析-数据聚合与分组运算2