【问题标题】:Does Java 8 provide a good way to repeat a value or function?Java 8 是否提供了一种重复值或函数的好方法?
【发布时间】:2013-08-30 12:03:21
【问题描述】:

在许多其他语言中,例如。 Haskell,很容易多次重复一个值或函数,例如。获取值 1 的 8 个副本的列表:

take 8 (repeat 1)

但是我在Java 8中还没有发现这个。Java 8的JDK中有这样的功能吗?

或者其他等价于范围的东西

[1..8]

这似乎可以替代 Java 中的冗长语句,例如

for (int i = 1; i <= 8; i++) {
    System.out.println(i);
}

拥有类似的东西

Range.from(1, 8).forEach(i -> System.out.println(i))

虽然这个特定的示例实际上看起来并不简洁...但希望它更具可读性。

【问题讨论】:

  • 你研究过Streams API吗?就 JDK 而言,这应该是您最好的选择。它有一个range 函数,这是我目前发现的。
  • @MarkoTopolnik Streams 类has been removed(更准确地说,它已被拆分为其他几个类,并且一些方法已被完全删除)。
  • 你调用 for 循环很冗长!幸好在 Cobol 时代你不在身边。 Cobol 中使用了 10 多个声明性语句来显示升序数字。现在的年轻人并不欣赏他们拥有的有多好。
  • @GilbertLeBlanc 冗长与此无关。循环是不可组合的,流是。循环导致不可避免的重复,而流允许重用。因此,流在数量上是比循环更好的抽象,应该是首选。
  • @GilbertLeBlanc,我们不得不在雪地里赤脚编码。

标签: java java-8


【解决方案1】:

对于这个具体的例子,你可以这样做:

IntStream.rangeClosed(1, 8)
         .forEach(System.out::println);

如果你需要一个不同于1的步骤,你可以使用一个映射函数,例如对于一个步骤2:

IntStream.rangeClosed(1, 8)
         .map(i -> 2 * i - 1)
         .forEach(System.out::println);

或者构建自定义迭代并限制迭代的大小:

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

【讨论】:

  • 闭包将彻底改变 Java 代码,变得更好。期待那一天……
  • @jwenting 这真的取决于 - 通常使用 GUI 的东西(Swing 或 JavaFX),由于匿名类而删除了 很多 样板。
  • @jwenting 对于任何有 FP 经验的人来说,围绕高阶函数编写的代码绝对是一种胜利。对于没有这种经验的人来说,是时候提升你的技能了——否则就有被抛在后面的风险。
  • @MarkoTopolnik 您可能想要使用稍新版本的 javadoc(您​​指向的是 build 78,最新的是 build 105:download.java.net/lambda/b105/docs/api/java/util/stream/…
  • @GraemeMoss 您仍然可以使用相同的模式 (IntStream.rangeClosed(1, 8).forEach(i -&gt; methodNoArgs());),但它会使 IMO 感到困惑,在这种情况下,似乎指示了一个循环。
【解决方案2】:

这是我前几天遇到的另一种技术:

Collections.nCopies(8, 1)
           .stream()
           .forEach(i -> System.out.println(i));

Collections.nCopies 调用会创建一个 List,其中包含您提供的任何值的 n 副本。在这种情况下,它是装箱的Integer 值1。当然,它实际上并没有创建带有n 元素的列表;它创建一个仅包含值和长度的“虚拟化”列表,并且在范围内对get 的任何调用都只会返回该值。 nCopies 方法自 JDK 1.2 中引入 Collections Framework 以来就一直存在。当然,在 Java SE 8 中添加了根据结果创建流的功能。

很重要,另一种方法可以在大约相同的行数中做同样的事情。

但是,这种技术比IntStream.generateIntStream.iterate 方法更快,而且令人惊讶的是,它也比IntStream.range 方法更快。

对于iterategenerate,结果可能并不令人惊讶。流框架(实际上,这些流的拆分器)是建立在 lambda 每次可能生成不同值的假设之上,并且它们将生成无限数量的结果。这使得并行拆分特别困难。对于这种情况,iterate 方法也存在问题,因为每次调用都需要前一次调用的结果。所以使用generateiterate 的流在生成重复常量方面表现不佳。

range 相对较差的表现令人惊讶。这也是虚拟化的,因此元素实际上并不都存在于内存中,并且大小是预先知道的。这应该是一个快速且易于并行化的拆分器。但令人惊讶的是,它并没有做得很好。也许原因是range 必须为范围内的每个元素计算一个值,然后在其上调用一个函数。但是这个函数只是忽略了它的输入并返回一个常量,所以我很惊讶它没有被内联和杀死。

Collections.nCopies 技术必须进行装箱/拆箱才能处理值,因为 List 没有原始特化。由于每次的值都相同,它基本上被装箱一次,并且该框由所有n 副本共享。我怀疑装箱/拆箱是高度优化的,甚至是内在化的,并且可以很好地内联。

代码如下:

    public static final int LIMIT = 500_000_000;
    public static final long VALUE = 3L;

    public long range() {
        return
            LongStream.range(0, LIMIT)
                .parallel()
                .map(i -> VALUE)
                .map(i -> i % 73 % 13)
                .sum();
}

    public long ncopies() {
        return
            Collections.nCopies(LIMIT, VALUE)
                .parallelStream()
                .mapToLong(i -> i)
                .map(i -> i % 73 % 13)
                .sum();
}

以下是 JMH 的结果:(2.8GHz Core2Duo)

Benchmark                    Mode   Samples         Mean   Mean error    Units
c.s.q.SO18532488.ncopies    thrpt         5        7.547        2.904    ops/s
c.s.q.SO18532488.range      thrpt         5        0.317        0.064    ops/s

ncopies 版本存在相当大的差异,但总体而言,它似乎比 range 版本快 20 倍。 (不过,我很愿意相信我做错了什么。)

nCopies 技术的效果让我感到惊讶。在内部,它并没有什么特别之处,虚拟列表的流只是使用IntStream.range 实现的!我原以为有必要创建一个专门的拆分器来让它快速运行,但它似乎已经很不错了。

【讨论】:

  • 经验不足的开发人员在得知 nCopies 实际上并没有复制任何东西并且“副本”都指向那个单一对象时可能会感到困惑或陷入困境.如果该对象是不可变的 总是安全的,例如本例中的盒装原语。您在“装箱一次”声明中提到了这一点,但在这里明确指出警告可能会很好,因为这种行为并非特定于自动装箱。
  • 这意味着LongStream.rangeIntStream.range 慢很多?因此,放弃了不提供IntStream(但对所有整数类型使用LongStream)的想法是一件好事。请注意,对于顺序用例,根本没有理由使用流:Collections.nCopies(8, 1).forEach(i -&gt; System.out.println(i));Collections.nCopies(8, 1).stream().forEach(i -&gt; System.out.println(i)); 相同,但效率更高的可能是 Collections.&lt;Runnable&gt;nCopies(8, () -&gt; System.out.println(1)).forEach(Runnable::run);
  • @Holger,这些测试是在干净的类型配置文件上执行的,因此它们与现实世界无关。可能LongStream.range 的性能更差,因为它有两个内部带有LongFunction 的映射,而ncopies 有三个带有IntFunctionToLongFunctionLongFunction 的映射,因此所有的lambda 都是单态的。在预污染类型配置文件(更接近实际情况)上运行此测试表明 ncopies 慢了 1.5 倍。
  • 过早优化 FTW
  • 为了完整起见,很高兴看到一个基准将这两种技术与一个普通的旧 for 循环进行比较。虽然您的解决方案比 Stream 代码更快,但我的猜测是 for 循环会大大击败其中任何一个。
【解决方案3】:

为了完整性,也因为我忍不住 :)

生成有限序列的常量与您在 Haskell 中看到的非常接近,只是具有 Java 级别的冗长性。

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);

【讨论】:

  • () -&gt; 1 只会生成 1,这是有意的吗?所以输出将是1 1 1 1 1 1 1 1
  • 是的,根据 OP 的第一个 Haskell 示例 take 8 (repeat 1)。 assylias 几乎涵盖了所有其他情况。
  • Stream&lt;T&gt; 也有一个通用的generate 方法,用于获取其他类型的无限流,可以用同样的方式进行限制。
【解决方案4】:

一旦重复函数在某处被定义为

public static BiConsumer<Integer, Runnable> repeat = (n, f) -> {
    for (int i = 1; i <= n; i++)
        f.run();
};

你可以不时这样使用它,例如:

repeat.accept(8, () -> System.out.println("Yes"));

得到和等价于Haskell的

take 8 (repeat 1)

你可以写

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));

【讨论】:

  • 这个太棒了。但是我修改它以提供迭代次数,方法是将Runnable 更改为Function&lt;Integer, ?&gt;,然后使用f.apply(i)
【解决方案5】:

另一种选择是使用Stream.generate() 方法。例如,下面的 sn-p 将创建一个包含 5 个 MyClass 实例的列表:

List<MyClass> timezones = Stream
    .generate(MyClass::createInstance)
    .limit(5)
    .collect(Collectors.toList());

来自 java 文档:

生成(供应商) 返回一个无限的顺序无序 流,其中每个元素由提供的供应商生成。

【讨论】:

    【解决方案6】:

    这是我实现时间功能的解决方案。我是一名大三学生,所以我承认这可能并不理想,如果这不是一个好主意,无论出于何种原因,我都很高兴。

    public static <T extends Object, R extends Void> R times(int count, Function<T, R> f, T t) {
        while (count > 0) {
            f.apply(t);
            count--;
        }
        return null;
    }
    

    以下是一些用法示例:

    Function<String, Void> greet = greeting -> {
        System.out.println(greeting);
        return null;
    };
    
    times(3, greet, "Hello World!");
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多