Java 8中10个不易察觉的错误

标签: java 错误 | 发表时间:2014-06-17 01:14 | 作者:
出处:http://it.deepinmind.com
  1. 不小心重用了流

我敢打赌,每人至少都会犯一次这样的错误。就像现有的这些“流”(比如说InputStream),你也只能对它们消费一次。下面的代码是无法工作的:

   IntStream stream = IntStream.of(1, 2);
stream.forEach(System.out::println);

// That was fun! Let's do it again!
stream.forEach(System.out::println);

你会碰到一个这样的错误:

   java.lang.IllegalStateException: 
  stream has already been operated upon or closed

因此使用流的时候应当格外注意。它只能消费一次。

  1. 不小心创建了一个”无限"流

你可能一不留神就创建了一个无限流。就拿下面这个例子来说:

   IntStream.iterate(0, i -> i + 1)
         .forEach(System.out::println);

流的问题就在于它有可能是无限的,如果你的确是这样设计的话。唯一的问题就是,这并不是你真正想要的。因此,你得确保每次都给流提供一个适当的大小限制:

   // That's better
IntStream.iterate(0, i -> i + 1)
         .limit(10)
         .forEach(System.out::println);
  1. 不小心创建了一个“隐藏的”无限流

这个话题是说不完的。你可能一不小心就真的创建了一个无限流。比如说下面的这个:

   IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .distinct()
         .limit(10)
         .forEach(System.out::println);

这样做的结果是:

  • 我们生成了0和1的交替数列
  • 然后只保留不同的数值,比如说,一个0和一个1
  • 然后再将流的大小限制为10
  • 最后对流进行消费

好吧,这个distinct()操作它并不知道iterate()所调用的这个函数生成的只有两个不同的值。它觉得可能还会有别的值。因此它会不停地从流中消费新的值,而这个limit(10)永远也不会被调用到。不幸的是,你的应用程序会崩掉。

  1. 不小心创建了一个”隐藏”的并行无限流

我还是想继续提醒下你,你可能真的一不小心就消费了一个无限流。假设你认为distinct()操作是会并行执行的。那你可能会这么写:

   IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .parallel()
         .distinct()
         .limit(10)
         .forEach(System.out::println);

现在我们可以知道的是,这段代码会一直执行下去。不过在前面那个例子中,你至少只消耗了机器上的一个CPU。而现在你可能会消耗四个,一个无限流的消费很可能就会消耗掉你整个系统的资源。这可相当不妙。这种情况下你可能得去重启服务器了。看下我的笔记本在最终崩溃前是什么样的:

  1. 操作的顺序

为什么我一直在强调你可能一不小心就创建了一个无限流?很简单。因为如果你把上面的这个流的limit()和distinct()操作的顺序掉换一下,一切就都OK了。

   IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .limit(10)
         .distinct()
         .forEach(System.out::println);

现在则会输出:

   0
1

为什么会这样?因为我们先将无限流的大小限制为10个值,也就是(0 1 0 1 0 1 0 1 0 1),然后再在这个有限流上进行归约,求出它所包含的不同值,(0,1)。

当然了,这个在语义上就是错误的了。因为你实际上想要的是数据集的前10个不同值。没有人会真的要先取10个随机数,然后再求出它们的不同值的。

如果你是来自SQL背景的话,你可能不会想到还有这个区别。就拿SQL Server 2012举例来说,下面的两个SQL语句是一样的:

   -- Using TOP
SELECT DISTINCT TOP 10 *
FROM i
ORDER BY ..

-- Using FETCH
SELECT *
FROM i
ORDER BY ..
OFFSET 0 ROWS
FETCH NEXT 10 ROWS ONLY

因此,作为一名SQL用户,你可能并不会注意到流操作顺序的重要性。

  1. 还是操作顺序

既然说到了SQL,如果你用的是MySQL或者PostgreSQL,你可能会经常用到LIMIT .. OFFSET子句。SQL里全是这种暗坑,这就是其中之一。正如SQL Server 2012中的语法所说明的那样,OFFSET子名会优先执行。

如果你将MySQL/PostgreSQL方言转化成流的话,得到的结果很可能是错的:

   IntStream.iterate(0, i -> i + 1)
         .limit(10) // LIMIT
         .skip(5)   // OFFSET
         .forEach(System.out::println);

上面的代码会输出:

   5
6
7
8
9

是的,它输出9后就结束了,因为首先生效的是limit(),这样会输出(0 1 2 3 4 5 6 7 8 9)。其次才是skip(),它将流缩减为(5 6 7 8 9)。而这并不是你所想要的。

警惕LIMIT .. OFFSET和OFFSET .. LIMIT的陷阱!

  1. 使用过滤器来遍历文件系统

这个问题我们之前已经讲过了。使用过滤器来遍历文件系统是个不错的方式:

   Files.walk(Paths.get("."))
     .filter(p -> !p.toFile().getName().startsWith("."))
     .forEach(System.out::println);

看起来上面的这个流只是遍历了所有的非隐藏目录,也就是不以点号开始的那些目录。不幸的是,你又犯了错误五和错误六了。walk()方法已经生成一个当前目录下的所有子目录的流。虽然是一个惰性流,但是也包含了所有的子路径。现在的这个过滤器可以正确过滤掉所有名字以点号开始的那些目录,也就是说结果流中不会包含.git或者.idea。不过路径可能会是:..git\refs或者..idea\libraries。而这并不是你实际想要的。

你可别为了解决问题而这么写:

   Files.walk(Paths.get("."))
     .filter(p -> !p.toString().contains(File.separator + "."))
     .forEach(System.out::println);

虽然这么写的结果是对的,但是它会去遍历整个子目录结构树,这会递归所有的隐藏目录的子目录。

我猜你又得求助于老的JDK1.0中所提供的File.list()了。不过好消息是, FilenameFilter和FileFilter现在都是函数式接口了。

  1. 修改流内部的集合

当遍历列表的时候,你不能在迭代的过程中同时去修改这个列表。这个在Java 8之前就是这样的,不过在Java 8的流中则更为棘手。看下下面这个0到9的列表:

   // Of course, we create this list using streams:
List<Integer> list = 
IntStream.range(0, 10)
         .boxed()
         .collect(toCollection(ArrayList::new));

现在,假设下我们在消费流的时候同时去删除元素:

   list.stream()
    // remove(Object), not remove(int)!
    .peek(list::remove)
    .forEach(System.out::println);

有趣的是,其中的一些元素中可以的删除的。你得到的输出将会是这样的:

   0
2
4
6
8
null
null
null
null
null
java.util.ConcurrentModificationException

如果我们捕获异常后再查看下这个列表,会发现一个很有趣的事情。得到的结果是:

   [1, 3, 5, 7, 9]

所有的奇数都这样。这是一个BUG吗?不,这更像是一个特性。如果你看一下JDK的源码,会发现在ArrayList.ArraListSpliterator里面有这么一段注释:

/* * If ArrayLists were immutable, or structurally immutable (no * adds, removes, etc), we could implement their spliterators * with Arrays.spliterator. Instead we detect as much * interference during traversal as practical without * sacrificing much performance. We rely primarily on * modCounts. These are not guaranteed to detect concurrency * violations, and are sometimes overly conservative about * within-thread interference, but detect enough problems to * be worthwhile in practice. To carry this out, we (1) lazily * initialize fence and expectedModCount until the latest * point that we need to commit to the state we are checking * against; thus improving precision. (This doesn't apply to * SubLists, that create spliterators with current non-lazy * values). (2) We perform only a single * ConcurrentModificationException check at the end of forEach * (the most performance-sensitive method). When using forEach * (as opposed to iterators), we can normally only detect * interference after actions, not before. Further * CME-triggering checks apply to all other possible * violations of assumptions for example null or too-small * elementData array given its size(), that could only have * occurred due to interference. This allows the inner loop * of forEach to run without any further checks, and * simplifies lambda-resolution. While this does entail a * number of checks, note that in the common case of * list.stream().forEach(a), no checks or other computation * occur anywhere other than inside forEach itself. The other * less-often-used methods cannot take advantage of most of * these streamlinings. */

现在来看下如果我们对这个流排序后会是什么结果:

   list.stream()
    .sorted()
    .peek(list::remove)
    .forEach(System.out::println);

输出的结果看起来是我们想要的:

   0
1
2
3
4
5
6
7
8
9

而流消费完后的列表是空的:

   []

也就是说所有的元素都正确地消费掉并删除了。sorted()操作是一个“带状态的中间操作”,这意味着后续的操作不会再操作内部的那个集合了,而是在一个内部的状态上进行操作。现在你可以安全地从列表里删除元素了!

不过,真的是吗这样?我们来试一下带有parallel(), sorted()的删除操作:

   list.stream()
    .sorted()
    .parallel()
    .peek(list::remove)
    .forEach(System.out::println);

这个会输出 :

   7
6
2
5
8
4
1
0
9
3

现在列表里包含:

   [8]

唉呀。居然没有删完所有的元素?!谁能解决这个问题,我免费请他喝酒!

这些行为看起来都是不确定的,我只能建议你在使用流的时候不要去修改它内部的数据集合。这样做是没用的。

  1. 忘了去消费流

你觉得下面这个流在做什么?

   IntStream.range(1, 5)
         .peek(System.out::println)
         .peek(i -> { 
              if (i == 5) 
                  throw new RuntimeException("bang");
          });

看完这段代码,你觉得应该会输出(1 2 3 4 5)然后抛出一个异常。不过并不是这样。它什么也不会做。这个流并没有被消费掉,它只是静静的待在那里。

正如别的流API或者DSL那样,你可能会忘了调用这个终止操作。当你使用peek()的时候也是这样的,因为peek有点类似于forEach()。

在jOOQ中也存在这样的情况,如果你忘了去调用 execute()或者fetch():

   DSL.using(configuration)
   .update(TABLE)
   .set(TABLE.COL1, 1)
   .set(TABLE.COL2, "abc")

   .where(TABLE.ID.eq(3));

杯具。忘了调用execute方法了。

  1. 并行流死锁

终于快讲完了~

如果你没有正确地进行同步的话,所有的并发系统都可能碰到死锁。现实中的例子可能不那么明显,不过如果你想自己创造一个场景的话倒是很容易。下面这个parallel()流肯定会造成死锁:

   Object[] locks = { new Object(), new Object() };

IntStream
    .range(1, 5)
    .parallel()
    .peek(Unchecked.intConsumer(i -> {
        synchronized (locks[i % locks.length]) {
            Thread.sleep(100);

            synchronized (locks[(i + 1) % locks.length]) {
                Thread.sleep(50);
            }
        }
    }))
    .forEach(System.out::println);

注意这里Unchecked.intConsumer()的使用,它把IntConsumer接口转化成了 org.jooq.lambda.fi.util.function.CheckedIntConsumer,这样你才可以抛出已检查异常。

好吧。这下你的机器倒霉了。这些线程会一直阻塞下去:-)

不过好消息就是,在Java里面要写出一个这种教科书上的死锁可不是那么容易。

想进一步了解的话,可以看下Brian Goetz在StackOverflow上的 一个回答

结论

引入了流和函数式编程之后,我们开始会碰到许多新的难以发现的BUG。这些BUG很难避免,除非你见过并且还时刻保持警惕。你必须去考虑操作的顺序,还得注意流是不是无限的。

流是一个非常强大的工具,但也是一个首先得去熟练掌握的工具。

原创文章转载请注明出处: Java 8中10个不易察觉的错误 英文原文链接

相关 [java 错误] 推荐:

Java开发者易犯错误Top10

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

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

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

Java 程序员容易犯的10个SQL错误

- - ITeye博客
Java程序员编程时需要混合面向对象思维和一般命令式编程的方法,能否完美的将两者结合起来完全得依靠编程人员的水准:. 技能(任何人都能容易学会命令式编程). 模式(有些人用“模式-模式”,举个例子,模式可以应用到任何地方,而且都可以归为某一类模式). 心境(首先,要写个好的面向对象程序是比命令式程序难的多,你得花费一些功夫).

Java 8中10个不易察觉的错误

- - Java译站
我敢打赌,每人至少都会犯一次这样的错误. 就像现有的这些“流”(比如说InputStream),你也只能对它们消费一次. 你会碰到一个这样的错误:. 因此使用流的时候应当格外注意. 不小心创建了一个”无限"流. 你可能一不留神就创建了一个无限流. 流的问题就在于它有可能是无限的,如果你的确是这样设计的话.

Java开发者写SQL时常犯的10个错误

- - ImportNew
我十分惊讶的发现,我最近的一篇文章——《 Java开发者写SQL时常犯的10个错误》——最近在 我的博客和 我的合作伙伴DZone上非常的受欢迎. (这篇博客)的流行程度说明了几件事:. SQL在专业的Java开发中多么重要. 基本的SQL知识被忘掉(的情况)普遍存在. 通过 embracing SQL,你就能了解像.

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

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

Java程序员们最常犯的3个集合错误

- - JavaRanger - 专注JAVA高性能程序开发、JVM、Mysql优化、算法
将数组转化为一个列表时,程序员们经常这样做:. Arrays.asList() 会返回一个ArrayList对象,ArrayList类是Arrays的一个私有静态类,而不是java.util.ArrayList 类,java.util.Arrays.ArrayList类有set()、get()、contains()方法,但是没有增加元素的方法,所以它的大 小是固定的,想要创建一个真正的ArrayList类,你应该这样做:.

Java中的锁(Locks in Java)

- - 并发编程网 - ifeve.com
原文链接 作者:Jakob Jenkov 译者:申章 校对:丁一. 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂. 因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字( 译者注:这说的是Java 5之前的情况).

Oracle错误代码

- - 数据库 - ITeye博客
ORA-00001: 违反唯一约束条件 (.). ORA-00017: 请求会话以设置跟踪事件. ORA-00018: 超出最大会话数. ORA-00019: 超出最大会话许可数. ORA-00020: 超出最大进程数 (). ORA-00021: 会话附属于其它某些进程;无法转换会话. ORA-00022: 无效的会话 ID;访问被拒绝.

Java PaaS 对决

- 呆瓜 - IBM developerWorks 中国 : 文档库
本文为 Java 开发人员比较了三种主要的 Platform as a Service (PaaS) 产品:Google App Engine for Java、Amazon Elastic Beanstalk 和 CloudBees RUN@Cloud. 它分析了每种服务独特的技术方法、优点以及缺点,而且还讨论了常见的解决方法.