Java 8提供了一种很好的重复值或函数的方法吗?

143

在许多其他语言中,例如 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))

虽然这个特定的例子看起来没有更加简洁,但希望它更易读。


2
你学过Streams API吗?在JDK方面,这应该是你最好的选择。它有一个range函数,这就是我目前找到的。 - Marko Topolnik
1
@MarkoTopolnik Streams类已被移除(更准确地说,它已被拆分为其他几个类,并且一些方法已被完全删除)。 - assylias
4
你称for循环啰嗦!如果你在Cobol时代出现,那就太糟了。在Cobol中,需要超过10个声明性语句才能显示升序数字。如今的年轻人不懂得珍惜他们拥有的好处。 - Gilbert Le Blanc
1
@GilbertLeBlanc 冗余性与此无关。循环不可重用,而流可以。循环导致无法避免的重复,而流允许重用。因此,流比循环更好地抽象化,应该优先考虑使用流。 - Alain O'Dea
2
@GilbertLeBlanc,我们不得不在雪地里赤脚编码。 - Dawood ibn Kareem
显示剩余2条评论
6个回答

186

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

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);

4
闭包将会完全改变 Java 代码,让它变得更好。期待那一天的到来... - Marko Topolnik
1
@jwenting 这真的取决于具体情况 - 通常在 GUI 方面(Swing 或 JavaFX),由于匿名类,这会消除 很多 样板代码。 - assylias
8
对于有函数式编程经验的人来说,以高阶函数为中心的代码是绝对优秀的。而对于没有这种经验的人来说,现在是提升技能的时候了——否则就可能落后于潮流。 - Marko Topolnik
2
@MarkoTopolnik,您可能想使用稍新的javadoc版本(您指向的是构建78,最新版本是构建105:http://download.java.net/lambda/b105/docs/api/java/util/stream/package-summary.html) - Mark Rotteveel
1
@GraemeMoss 您仍然可以使用相同的模式(IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());),但在我看来这会让事情变得混乱,在这种情况下,循环似乎是更合适的选择。 - assylias
显示剩余11条评论

80

这是我最近发现的另一种技术:

Collections.nCopies(8, 1)
           .stream()
           .forEach(i -> System.out.println(i));
Collections.nCopies 调用会创建一个包含 n 个给定值的 List,在这里是封装了整数值 1 的 Integer。它实际上并没有创建一个具有 n 个元素的列表,而是创建了一个"虚拟化"列表,只包含该值和长度,对范围内任何 get 调用都返回该值。自 JDK 1.2 引入 Collections Framework 以来,nCopies 方法一直存在。当然,从其结果创建流的能力是在 Java SE 8 中添加的。
这种技术比 IntStream.generateIntStream.iterate 方法更快,令人惊讶的是,它也比 IntStream.range 方法更快。
对于 iterategenerate 的结果可能并不令人感到意外。流框架(实际上是这些流的 Spliterators)是建立在 lambda 可能每次生成不同值的假设上,并且它们将生成数量不确定的结果。这使得并行拆分特别困难。对于这种情况,iterate 方法也存在问题,因为每个调用都需要前一个的结果。因此使用 generateiterate 的流不适合生成重复的常量。 range 的相对性能较差是令人惊讶的。它也是虚拟化的,因此实际上并没有所有元素都存在于内存中,并且大小是预先知道的。这应该使得它成为一个快速且容易并行的 Spliterator。但令人惊讶的是它表现并不好。也许原因在于 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来实现的!我原以为需要创建一个专门的spliterator才能使它跑得更快,但它已经表现得相当优秀了。


6
对于经验不太丰富的开发者来说,当他们学习到nCopies实际上并没有_复制_任何内容,而所有的“复制”都指向同一个对象时,可能会感到困惑或陷入麻烦。如果该对象是_不可变的_,比如这个例子中的装箱基元类型,那么就始终是安全的。你在“boxed once”语句中提及了这一点,但在这里明确指出这些注意事项可能更好,因为这种行为并不特定于自动装箱。 - William Price
1
那么这意味着LongStream.rangeIntStream.range慢得多?所以不提供IntStream(但对于所有整数类型使用LongStream)的想法已经被放弃是一件好事。请注意,对于顺序使用情况,根本没有理由使用流:Collections.nCopies(8, 1).forEach(i -> System.out.println(i));Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));相同,但更有效的可能是Collections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run); - Holger
1
@Holger,这些测试是在干净的类型配置文件上执行的,因此与实际情况无关。可能LongStream.range表现更差,因为它有两个包含LongFunction的映射,而ncopies有三个包含IntFunctionToLongFunctionLongFunction的映射,因此所有的lambda都是单态的。在预先污染的类型配置文件上运行此测试(更接近实际情况)显示ncopies慢了1.5倍。 - Tagir Valeev
1
过早优化必败 - Rafael Bugajewski
2
为了完整起见,能够看到将这两种技术与普通的 for 循环进行比较的基准测试将是不错的。虽然您的解决方案比“Stream”代码更快,但我猜测 for 循环会明显击败其中任何一种。 - typeracer
显示剩余3条评论

39

出于完整性考虑,也因为我忍不住 :)

生成一系列有限的常量与Haskell中看到的相当接近,只是Java会更啰嗦。

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

() -> 1 只会生成数字 1,这是预期的吗?因此输出将为 1 1 1 1 1 1 1 1 - Christian Ullenboom
4
是的,根据原帖中的第一个 Haskell 例子 take 8 (repeat 1)。assylias 已经覆盖了所有其他情况。 - clstrfsck
3
Stream<T> 还有一个泛型方法 generate,用于获取另一种类型的无限流,可以使用相同的方式进行限制。 - zstewart

16

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

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"));

3
这个很棒。不过我对它进行了修改,以便返回迭代的次数,我将Runnable改为了Function<Integer, ?>,然后使用了f.apply(i) - Fons

2

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

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

来自Java文档:

generate(Supplier s) 返回一个无限的顺序不确定流,其中每个元素都是由提供的Supplier生成的。


-1

这是我实现 times 函数的解决方案。我是一名初级程序员,所以我承认这可能不是最理想的方案,如果有任何不好的地方,我很乐意听取意见。

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!");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接