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

发表于:2014-7-16 10:03

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:有孚    来源:51Testing软件测试网采编

  现在我们可以知道的是,这段代码会一直执行下去。不过在前面那个例子中,你至少只消耗了机器上的一个CPU。而现在你可能会消耗四个,一个无限流的消费很可能就会消耗掉你整个系统的资源。这可相当不妙。这种情况下你可能得去重启服务器了。看下我的笔记本在最终崩溃前是什么样的:
  操作的顺序
  为什么我一直在强调你可能一不小心就创建了一个无限流?很简单。因为如果你把上面的这个流的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用户,你可能并不会注意到流操作顺序的重要性。
  还是操作顺序
  既然说到了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的陷阱!
  使用过滤器来遍历文件系统
  这个问题我们之前已经讲过了。使用过滤器来遍历文件系统是个不错的方式:
  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现在都是函数式接口了。
  修改流内部的集合
  当遍历列表的时候,你不能在迭代的过程中同时去修改这个列表。这个在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()操作是一个“带状态的中间操作”,这意味着后续的操作不会再操作内部的那个集合了,而是在一个内部的状态上进行操作。现在你可以安全地从列表里删除元素了!
32/3<123>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号