避坑手册(避坑指南pdf)

  本篇文章为你整理了避坑手册(避坑指南pdf)的详细内容,包含有避坑指南什么意思 避坑指南pdf 避坑表情包 避坑怎么解释 避坑手册,希望能帮助你了解 避坑手册。

  10余年软件开发与系统架构经验,一起聊聊软件开发技术、系统架构技术、以及程序员最真实可行的职场打怪技能,代码之外的生存软技能。

  
 

  JAVA编码中存在一些容易被人忽视的陷阱,稍不留神可能就会跌落其中,给项目的稳定运行埋下隐患。此外,这些陷阱也是面试的时候面试官比较喜欢问的问题。

  本文对这些陷阱进行了统一的整理,让你知道应该如何避免落入陷阱中,下面就一起来了解下吧。

  循环中操作目标list

  遍历List然后对list中符合条件的元素进行删除操作,这是项目里面非常常见的一个场景。

  先看下两种典型的错误写法:

  错误写法1:

  

 

 

  for (User user : userList) {

   if ("男".equals(user.getSex())) {

   userList.remove(user);

  

 

  错误原因:

  在循环或迭代时,会首先创建一个迭代实例,这个迭代实例的expectedModCount 赋值为集合的modCount。而每当迭代器使⽤ hashNext() / next() 遍历下⼀个元素之前,都会检测 modCount 变量与expectedModCount 值是否相等,相等的话就返回遍历;否则就抛出异常ConcurrentModificationException,终⽌遍历。

  如果在循环中添加或删除元素,是直接调用集合的add(),remove()方法,导致了modCount增加或减少,但这些方法不会修改迭代实例中的expectedModCount,导致在迭代实例中expectedModCount与 modCount的值不相等,抛出ConcurrentModificationException异常。

  错误写法2:

  

 

 

  for (int i = 0; i userList.size(); i++ ) {

   if ("男".equals(user.getSex())) {

   userList.remove(i);

  

 

  错误原因:

  删除元素之后,元素下标发生前移,但是指针是不变的,再处理下一个的时候,就可能会有部分元素被漏掉没有处理。

  那正确的方式应该如何处理呢?接着往下看。

  正确写法1:

  

 

 

  // 使用迭代器来实现

  Iterator iterator = userList.iterator();

  while (iterator.hasNext()) {

   if ("男".equals(user.getSex())) {

   iterator.remove();

  

 

  补充说明:
 

  与前面的错误写法1相对比,同样都是基于迭代器的逻辑,为什么这种写法就对了呢?
 

  这是因为迭代器中的remove(),add()方法,会在调用集合的remove(),add()方法后,将expectedModCount重新赋值为modCount,所以在迭代器中增加、删除元素是可以正常运行的。

  正确写法2:

  

 

 

  // 使用Lambda表达式实现

  userList.removeIf(user - "男".equals(user.getSex());

  

 

  正确写法3:

  

 

 

  // 使用removeAll实现

  List User maleUsers = new ArrayList ();

  for (User user : userList) {

   if ("男".equals(user.getSex())) {

   maleUsers.add(user);

  userList.removeAll(maleUsers);

  

 

  将对象作为参数传递并重新赋值

  看个例子:

  这里涉及到JAVA中一个值传递和引用传递的概念。
 

  对于一个引用类型而言,参数传递的时候,本质上是将一个引用对象对应内存地址传递过去,参数对象与实际对象指向同一个内存块。对于示例代码中的changeUser()方法,将入参重新赋值了一个新的对象,本质上其实是将user1对应指向的内存地址信息更改了,对于原始的user而言,并没有被改变。

  而对于chageAge()方法而言,对user2的操作,本质上是对user2所指向的具体内存对象进行操作,也即user对应的内存对象数据,所以这种情况下,变更是会生效的。

  所以呢,编码的时候,要注意不能在方法里面对入参进行重新赋值,可以采用返回值的方式返回个新的结果对象,然后进行赋值操作。

  字母L的使用

  先看一个例子:

  同样相同的三个数字的相乘,L标识的位置不同,得到的结果也不一样,那到底哪个是对的呢?

  很明显这个一个JAVA隐式类型转换的问题。

  第一个结果显然是不对的,因为三个int值相乘之后结果明显超出int长度范围,所以截断了。

  第二个结果,前面两个int相乘,在与第三个long型运算,结果会自动转换为long型,但是根据运算顺序,前面2个int值运算的中间结果也是int类型,且长度超出范围被截断了,截断后的结果与最后一位long进行的运算,所以第二个结果也是错的。

  第三个结果是对的,因为long型放在前面,所以前两位运算的时候就先转为long型,再与第三个int运算后,结果依旧是long型,不会出现溢出的情况。

  TIPS:
 

  int运算转long的时候,最好将第一个运算的数字标识为L(long)型,避免中途数据溢出。

  再看个例子:

  

 public static void main(String[] args) {

 

   System.out.println(2 * 2l);

  

 

  第一眼看完,想一下答案应该是几?是42吗?
 

  其实结果是4,为什么?因为第二个2后面的是个字母l。虽然这种写法对于程序而言没有问题,但是很容易让开发人员混淆,造成认知上的错误。

  TIPS:
 

  long数字标识的时候,使用大写字母L来表示。

  流/资源的释放

  打开的流或者连接,在用完之后需要可靠的退出。但是有一种循环中打开流的场景,需要特别注意,笔者在多年的代码review经历中发现,基本每个项目都会存在循环中打开的流没有全部可靠释放的问题。

  上面的示例代码中,虽然最后finally里面也有执行流的关闭操作,但是try分支中,inputStream是在一个for循环里面被多次创建了,而最终finally分支中仅关闭了最后一个,之前的流都处于未关闭状态,造成资源的泄漏。

  日期格式转换的并发场景

  很多的项目中都使用SimpleDateFormat来做日期的格式化操作,但是要注意SimpleDataFormat是非线程安全的,所以使用的时候需要注意。

  但是实际使用的时候,如果每次需要格式化的时候,都去new SimpleDateFormat()对象,这个成本开销有点大,会对整体的性能造成一定的影响。

  所以使用的时候可以采取一些措施,保证线程安全的同时也兼顾其处理性能:

  事务失效场景

  JAVA开发中,经常会使用Spring的@Transactional注解来指定事务回滚的相关策略,但是有时候会发现@Transactional并没有生效,下面介绍下可能的几种情况。

  finally分支的数据处理

  finally分支一般伴随着try...catch语句一起使用,用来当所有操作退出前执行一些收尾处理逻辑,比如资源释放、连接关闭等等。但是如果使用不当,也会造成我们的业务逻辑不按预期执行,所以使用的时候要注意。

  finally分支中对返回值重新修改

  先看下如下代码写法,在try...catch分支中都有return操作,然后再finally中进行返回值修改,最终返回结果并不会被finally中的逻辑修改:

  因为如果存在 finally 代码块,try...catch中的return语句不会立马返回调用者,而是记录下返回值的副本,待 finally代码块执行完毕之后再向调用者返回其值,然后即使在finally中修改了返回值,也不会返回修改后的值。

  再看另一种常见的写法:

  与上面的差异点在于,try...catch分支里面并没有return语句,而是finally外面统一执行返回操作,这种情况下就可以生效。其实也很好理解,try...catch...finally这个语句块里面没有return操作,所以也就不会有暂存return副本的逻辑了。

  finally分支中直接return

  在finally分支里面存在return语句是一个很不好的实践。一般的IDEA中也会智能提示finally里面存return分支。

  finally里面如果存在return分支,则finally里面的返回值会覆盖掉try...catch逻辑中处理后计划返回的结果,也即导致try...catch部分的逻辑失效,容易引起业务逻辑上的问题。

  finally分支中抛出异常

  一般的编码规范中,都会要求finally分支里面的处理逻辑要增加catch保护,防止其抛出异常。

  原因说明:

  相对而言,finally里面执行的都是一些资源释放类的操作,而try...catch部分则是业务维度的核心逻辑,人们更关心的是catch部分发生的业务层面的异常,如果finally里面抛出异常,会导致catch中原本应该要往外抛的异常被丢弃,可能会影响上层逻辑的后续处理。

  全局变量中的集合类

  全局类型的集合类,使用的时候需要注意两个关键点:

  注意下并发场景的线程安全性;

  注意下数据的最大范围、是否有数据淘汰机制,避免内存无限制增加,导致内存溢出。

  参考下redis之类的依赖内存的缓存中间件,都有一个绕不开的兜底策略,即数据淘汰机制。对于业务类编码实现的时候,如果使用Map等容器类来实现全局缓存的时候,应该要结合实际部署情况,确定内存中允许的最大数据条数,并提供超出指定容量时的处理策略。比如我们可以基于LinkedHashMap来定制一个基于LRU策略的缓存Map,来保证内存数据量不会无限制增长。

  

public class FixedLengthLinkedHashMap K, V extends LinkedHashMap K, V {

 

   private static final long serialVersionUID = 1287190405215174569L;

   private int maxEntries;

   public FixedLengthLinkedHashMap(int maxEntries, boolean accessOrder) {

   super(16, 0.75f, accessOrder);

   this.maxEntries = maxEntries;

   * 自定义数据淘汰触发条件,在每次put操作的时候会调用此方法来判断下

   protected boolean removeEldestEntry(Map.Entry K, V eldest) {

   return size() maxEntries;

  

 

  浮点数据计算

  看一段很简单的代码:

  

 

 

  public static void main(String[] args) {

   System.out.println(0.1 + 0.2);

  

 

  上面的执行后,输出结果应该是多少?很明显应该是0.3呀!然而实际上,运行之后,输出结果为:

  

0.30000000000000004

 

  

 

  这是因为浮点数是不精确的,因为浮点数是存在小数点位的,而十进制的0.1换算为二进制是一个无限循环小数,所以实际上存储的其实是一个近似0.1的值、而不是精确的0.1。

  也正是这个原因,一般实现中,判断两个float是否相等时,往往不用==,而是判断两个浮点数之差绝对值是否小于一个很小的数。

  对于一些需要精确计算的场景,显然是不能使用浮点数来运算的,比如一些银行金融领域涉及金钱数额相关的场景,是绝对不允许使用float或者double进行运算,而是推荐使用BigDecimal来替代。

  使用Object作为HashMap的key

  大家都知道在JAVA中,HashMap的key是不可以重复的,相同的key对应值会进行覆盖。但是,如果使用自定义对象作为HashMap的key,就要小心了,因为如果操作不当,很容易造成内存泄漏的问题。

  如果一定要使用,确保此Object一定是覆写了hashCode()和equals()方法,并且保证覆写的equals和hashCode方法中一定不能有频繁易变更的字段参与计算。

  好啦,关于JAVA中常见的十大陷阱,这里就给大家分享到这里。希望大家在实际编码的时候可以注意提防,避免踩坑。

  我是悟道,聊技术、又不仅仅聊技术~

  如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。

  期待与你一起探讨,一起成长为更好的自己。

  以上就是避坑手册(避坑指南pdf)的详细内容,想要了解更多 避坑手册的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: