[译] Java 中最常见的 5 个错误

标签: java | 发表时间:2015-07-24 14:36 | 作者:OneAPM蓝海讯通
出处:http://segmentfault.com/blogs

在编程时,开发者经常会遭遇各式各样莫名错误。近日,Sushil Das 在 Geek On Java上列举了 Java 开发中常见的 5 个错误,与君共「免」。

原文链接: Top 5 Common Mistake in Java

Java 中最常见的 5 个错误

以下为译文:

1. Null 的过度使用

避免过度使用 null 值是一个最佳实践。例如,更好的做法是让方法返回空的 array 或者 collection 而不是 null 值,因为这样可以防止程序抛出 NullPointerException。下面代码片段会从另一个方法获得一个集合:

  </>
List<String> accountIds = person.getAccountIds();
for (String accountId : accountIds) {
    processAccount(accountId);
}

当一个 person 没有 account 的时候,getAccountIds() 将返回 null 值,程序就会抛出 NullPointerException 异常。因此需要加入空检查来解决这个问题。如果将返回的 null 值替换成一个空的 list,那么 NullPointerException 也不会出现。而且,因为我们不再需要对变量 accountId 做空检查,代码将变得更加简洁。

当你想避免 null 值的时候,不同场景可能采取不同做法。其中一个方法就是使用 Optional 类型,它既可以是一个空对象,也可以是一些值的封装。

  </>
Optional<String> optionalString = Optional.ofNullable(nullableString);
if(optionalString.isPresent()) {
    System.out.println(optionalString.get());
}

事实上,Java8 提供了一个更简洁的方法:

  </>
Optional<String> optionalString = Optional.ofNullable(nullableString);
optionalString.ifPresent(System.out::println);

Java 是从 Java8 版本开始支持 Optional 类型,但是它在函数式编程世界早已广为人知。在此之前,它已经在 Google Guava 中针对 Java 的早期版本被使用。

2. 忽视异常

我们经常对异常置之不理。然而,针对初学者和有经验的 Java 程序员,最佳实践仍是处理它们。异常抛出通常是带有目的性的,因此在大多数情况下需要记录引起异常的事件。别小看这件事,如果必要的话,你可以重新抛出它,在一个对话框中将错误信息展示给用户或者将错误信息记录在日志中。至少,为了让其它开发者知晓前因后果,你应该解释为什么没有处理这个异常。

  </>
selfie = person.shootASelfie();
try {
    selfie.show();
} catch (NullPointerException e) {
    // Maybe, invisible man. Who cares, anyway?
}

强调某个异常不重要的一个简便途径就是将此信息作为异常的变量名,像这样:

  </>
try { selfie.delete(); } catch (NullPointerException unimportant) {  }

3. 并发修改异常

这种异常发生在集合对象被修改,同时又没有使用 iterator 对象提供的方法去更新集合中的内容。例如,这里有一个 hats 列表,并想删除其中所有含 ear flaps 的值:

  </>
List<IHat> hats = new ArrayList<>();
hats.add(new Ushanka()); // that one has ear flaps
hats.add(new Fedora());
hats.add(new Sombrero());
for (IHat hat : hats) {
    if (hat.hasEarFlaps()) {
        hats.remove(hat);
    }
}

如果运行此代码,ConcurrentModificationException 将会被抛出,因为代码在遍历这个集合的同时对其进行修改。当多个进程作用于同一列表,在其中一个进程遍历列表时,另一个进程试图修改列表内容,同样的异常也可能会出现。

在多线程中并发修改集合内容是非常常见的,因此需要使用并发编程中常用的方法进行处理,例如同步锁、对于并发修改采用特殊的集合等等。Java 在单线程和多线程情况下解决这个问题有微小的差别。

收集对象并在另一个循环中删除它们

直接的解决方案是将带有 ear flaps 的 hats 放进一个 list,之后用另一个循环删除它。不过这需要一个额外的集合来存放将要被删除的 hats。

  </>
List<IHat> hatsToRemove = new LinkedList<>();
for (IHat hat : hats) {
    if (hat.hasEarFlaps()) {
        hatsToRemove.add(hat);
    }
}
for (IHat hat : hatsToRemove) {
    hats.remove(hat);
}

使用 Iterator.remove 方法

这个方法更简单,同时并不需要创建额外的集合:

  </>
Iterator<IHat> hatIterator = hats.iterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.remove();
    }
}

使用 ListIterator 的方法

当需要修改的集合实现了 List 接口时,list iterator 是非常合适的选择。实现 ListIterator 接口的 iterator 不仅支持删除操作,还支持 addset 操作。ListIterator 接口实现了 Iterator 接口,因此这个例子看起来和 Iteratorremove 方法很像。唯一的区别是 hat iterator 的类型和我们获得 iterator 的方式——使用 listIterator() 方法。下面的片段展示了如何使用  ListIterator.removeListIterator.add 方法将带有 ear flaps 的 hat 替换成带有sombreros 的。

  </>
IHat sombrero = new Sombrero();
ListIterator<IHat> hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.remove();
        hatIterator.add(sombrero);
    }
}

使用 ListIterator,调用 removeadd 方法可替换为只调用一个 set 方法:

  </>
IHat sombrero = new Sombrero();
ListIterator<IHat> hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.set(sombrero); // set instead of remove and add
    }
}

使用Java 8中的 stream 方法

在 Java8 中,开发人员可以将一个 collection 转换为 stream,并且根据一些条件过滤 stream。这个例子讲述了 stream api 是如何过滤 hats 和避免 ConcurrentModificationException
hats = hats.stream().filter((hat -> !hat.hasEarFlaps()))

  </>
 .collect(Collectors.toCollection(ArrayList::new));

Collectors.toCollection 方法将会创建一个新的 ArrayList,它负责存放被过滤掉的 hats 值。如果过滤条件过滤掉了大量条目,这里将会产生一个很大的 ArrayList。因此,需要谨慎使用。

使用 Java 8 中的 List.removeIf 方法

可以使用 Java 8 中另一个更简洁明了的方法——  removeIf 方法:

  </>
hats.removeIf(IHat::hasEarFlaps);

在底层,它使用  Iterator.remove 来完成这个操作。

使用特殊的集合

如果在一开始就决定使用 CopyOnWriteArrayList 而不是 ArrayList,那就不会出现问题。因为  CopyOnWriteArrayList 提供了修改的方法(例如 set,add,remove),它不会去改变原始集合数组,而是创建了一个新的修改版本。这就允许遍历原来版本集合的同时进行修改,从而不会抛出  ConcurrentModificationException 异常。这种集合的缺点也非常明显——针对每次修改都产生一个新的集合。

还有其他适用于不同场景的集合,比如  CopyOnWriteSetConcurrentHashMap

关于另一个可能可能在并发修改集合时产生的错误是,从一个 collection 创建了一个 stream,在遍历 stream 的时候,同时修改后端的 collection。针对 stream 的一般准则是,在查询 stream 的时候,避免修改后端的 collection。接下来的例子将展示如何正确地处理 stream:

  </>
List<IHat> filteredHats = hats.stream().peek(hat -> {
    if (hat.hasEarFlaps()) {
        hats.remove(hat);
    }
}).collect(Collectors.toCollection(ArrayList::new));

peek 方法收集所有的元素,并对每一个元素执行既定动作。在这里,动作即为尝试从一个基础列表中删除数据,这显然是错误的。为避免这样的操作,可以尝试一些上面讲解的方法。

4. 违约

有时候,为了更好地协作,由标准库或者第三方提供的代码必须遵守共同的依赖准则。例如,必须遵守  hashCodeequals 的共同约定,从而保证 Java 集合框架中的一系列集合类和其它使用 hashCodeequals 方法的类能够正常工作。不遵守约定并不会产生 exception 或者破坏代码编译之类的错误;它很阴险,因为它随时可能在毫无危险提示的情况下更改应用程序行为。

错误代码可能潜入生产环境,从而造成一大堆不良影响。这包括较差的 UI 体验、错误的数据报告、较差的应用性能、数据丢失或者更多。庆幸的是,这些灾难性的错误不会经常发生。在之前已经提及了 hashCode 和equals 约定,它出现的场景可能是:集合依赖于将对象进行哈希或者比较,就像 HashMap 和 HashSet。简单来说,这个约定有两个准则:

  • 如果两个对象相等,那么 hash code 必须相等。
  • 如果两个对象有相同的 hash code,那么它们可能相等也可能不相等。

破坏约定的第一条准则,当你试图从一个 hashmap 中检索数据的时候将会导致错误。第二个准则意味着拥有相同 hash code 的对象不一定相等。

下面看一下破坏第一条准则的后果:

  </>
public static class Boat {
    private String name;

    Boat(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Boat boat = (Boat) o;

        return !(name != null ? !name.equals(boat.name) : boat.name != null);
    }

    @Override
    public int hashCode() {
        return (int) (Math.random() * 5000);
    }
}

正如你所见,Boat 类重写了 equalshashCode 方法。然而,它破坏了约定,因为 hashCode 针对每次调用的相同对象返回了随机值。下面的代码很可能在 hashset 中找不到一个名为 Enterprise 的boat,尽管事实上我们提前加入了这种类型的 boat:

  </>
public static void main(String[] args) {
    Set<Boat> boats = new HashSet<>();
    boats.add(new Boat("Enterprise"));

    System.out.printf("We have a boat named 'Enterprise' : %b\n", boats.contains(new Boat("Enterprise")));
}

另一个约定的例子是 finalize 方法。这里是官方 Java 文档关于它功能描述的引用:

finalize 的常规约定是:当 JavaTM 虚拟机确定任何线程都无法再通过任何方式访问指定对象时,这个方法会被调用,此后这个对象只能在某个其他(准备终止的)对象或类终结时被作为某个行为的结果。 finalize 方法有多个功能,其中包括再次使此对象对其他线程可用;不过 finalize 的主要目的是在不可撤消地丢弃对象之前执行清除操作。例如,表示输入/输出连接对象的 finalize 方法可执行显式 I/O 事务,以便在永久丢弃对象之前中断连接。

你可以决定在诸如文件处理器中使用 finalize 方法来释放资源,但是这种用法是很糟糕的。由于它是在垃圾回收期间被调用的,而 GC 的时间并不确定,因此 finalize 被调用的时间将无法保证。

5. 使用原始类型而不是参数化的

根据 Java 文档描述:原始类型要么是非参数化的,要么是类 R 的(同时也是非继承 R 父类或者父接口的)非静态成员。在 Java 泛型被引入之前,并没有原始类型的替代类型。Java 从1.5版本开始支持泛型编程,毫无疑问这是一个重要的功能提升。然而,由于向后兼容的原因,这里存在一个陷阱可能会破坏整个类型系统。着眼下例:

  </>
List listOfNumbers = new ArrayList();
listOfNumbers.add(10);
listOfNumbers.add("Twenty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));

这是一个由数字组成的列表被定义为原始的 ArrayList。由于它并没有指定类型参数,因此可以给它添加任何对象。但是最后一行将其包含的元素映射为 int 类型并乘以 2,打印出翻倍之后的数据到标准输出。

此代码编译时不会出错,但是一旦运行就会抛出运行时错误,因为这里试图将字符类型映射为整形。很显然,如果隐藏了必要信息,类型系统将不能帮助写出安全代码。

为了解决这个问题,需要为存入集合中的对象指定具体类型:

  </>
List<Integer> listOfNumbers = new ArrayList<>();

listOfNumbers.add(10);
listOfNumbers.add("Twenty");

listOfNumbers.forEach(n -> System.out.println((int) n * 2));

与之前代码的唯一差别即是定义集合的那一行:

  </>
List<Integer> listOfNumbers = new ArrayList<>();

修改之后的代码编译不可能被通过,因为这里试图向只期望存储整形的集合中添加字符串。编译器将会显示错误信息,并指向试图向列表中添加 Twenty 字符的那一行。参数化泛型类型是个不错的主意。这样的话,编译器就能够检查所有可能的类型,从而由于类型不一致而导致的运行时异常几率将大大降低。

本文系 OneAPM 工程师编译整理。想阅读更多技术文章,请访问 OneAPM 官方博客

相关 [java 常见 错误] 推荐:

[译] Java 中最常见的 5 个错误

- - SegmentFault 最新的文章
在编程时,开发者经常会遭遇各式各样莫名错误. 近日,Sushil Das 在 Geek On Java上列举了 Java 开发中常见的 5 个错误,与君共「免」. 原文链接: Top 5 Common Mistake in Java. 避免过度使用 null 值是一个最佳实践. 例如,更好的做法是让方法返回空的 array 或者 collection 而不是 null 值,因为这样可以防止程序抛出 NullPointerException.

Spring MVC的常见错误

- - Java译站
10年前我开始自己的职业生涯的时候,Struts还是市场上的主流标准. 然而多年过后,我发现Spring MVC已经越来越流行了. 对我而言这并不意外,因为它能和Spring容器无缝集成,同时它还提供了灵活性及扩展性. 从我迄今为止对Spring的经验来看,我发现有不少人在配置Spring的时候经常会犯一些常见的错误.

Java开发者易犯错误Top10

- - CSDN博客编程语言推荐文章
摘要:在Java中,有些事物如果不了解的话,很容易就会用错,如数组转换为数组列表、元素删除、Hashtable和HashMap、ArrayList和LinkedList、Super和Sub构造函数等,如果这些对你来说是陌生的,你可以在本文中了解它们. 本文总结了Java开发者经常会犯的前十种错误列表.

locale错误导致Java中文乱码错误的总结

- - Java - 编程语言 - ITeye博客
线上执行MapReduce任务计算时,经过排查发现了某些服务器计算的数据出现中文乱码问题,但是服务器的配置是完全一致的. 由于我们使用的key可能包含中文,中文乱码问题体现在每次合并map记录的时候计算数据的随机性,每次执行的结果都不一样(由于Map任务执分配的随机性). (注:此文章大部分都参考了同事查找到的问题解决方法.

Linux下的常见错误配置

- - FreeBuf.COM
    经过对大量客户的配置审计与渗透测试,我们总结出了一些Linux系统下的常见配置错误. 我们相信总结、回顾这些常见错误可以在以后为我们节省更多时间与资源,更重要的是可以帮助系统管理员,使其服务器更加安全可靠.     五个常见配置错误如下:. 1、用户/home目录的权限 2、系统中的getgid与setuid程序 3、全局可读/可写的文件/目录 4、使用包含漏洞的服务 5、默认的NFS挂载选项或不安全的导出选项.

varnish常见错误的解决方法

- - 开心平淡对待每一天。热爱生活
这是因为从2.0.6以后,obj.ttl 已经变更为beresp.ttl. 2. beresp.cacheable的含义是什么. 官方的解释:beresp.cacheable. A response is considered cacheable if HTTP status code is 200, 203, 300, 301, 302, 404 or 410 and pass wasn’t called in vcl_recv.

Mark Lutz:Python程序员的常见错误

- - 博客 - 伯乐在线
译注: Mark Lutz 是《Learning Python | 学习Python》的作者之一. 在这篇文章中,我将总结新老Python程序员常犯的一些错误,以帮助你们在自己的工作避免犯同样或类似错误. 首先我要说明一下的是,这些都是来源于第一手的经验. 我以讲授Python的知识为生. 在过去的7年里,我已经给上千名学生讲授上百堂Python的课程,同时看着这些学生们犯同样的错.

SEO中常见的七个错误

- - Java译站
SEO作为一个战略营销服务多年来它的效果一直都是非常不错的. 他们通过给网站进行深度优化从而使网站流量得到了质的提升. 品牌的曝光度和知名度的提升. 这些理由足以让营销人员将SEO作为主要的在线营销策略了. 然而,需要仔细调整SEO来适应客户企业以及目标用户的分布特征. 许多SEO服务提供商在替客户取得满意的效果前都经历了许多常见的错误的方法.

hadoop常见错误及解决办法!

- - 企业架构 - ITeye博客
转: http://p-x1984.iteye.com/blog/989577 1:Shuffle Error: Exceeded MAX_FAILED_UNIQUE_FETCHES; bailing-out Answer: 程序里面需要打开多个文件,进行分析,系统一般默认数量是1024,(用ulimit -a可以看到)对于正常使用是够了,但是对于程序来讲,就太少了.

Java常见疑惑和陷阱(PPT)

- water - BlogJava-首页技术区
本来是打算小范围内讨论的,话题也比较小,后来听说人多了,临时拼凑些材料. 话题过大后重点就放在讲解上,其实这里面讲解的东东还是挺多的. 以后有时间会将并发完整整理一次. xylz 2010-12-03 16:13 发表评论.