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


117

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

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而言,那应该是最好的选择。它具有范围函数,这是我到目前为止发现的。
Marko Topolnik

1
@MarkoTopolnik Streams类已被删除(更准确地说,它已在其他几个类中拆分,并且某些方法已被完全删除)。
assylias 2013年

3
您将for循环称为冗长!在Cobol时代,你不在身边是一件好事。在Cobol中使用了10条声明性语句来显示升序数字。这些天,年轻人不欣赏他们的成就。
吉尔伯特·勒布朗克

1
@GilbertLeBlanc的详细程度与它无关。循环是不可组合的,流是不可组合的。循环导致不可避免的重复,而流允许重用。因此,从数量上讲,Stream是比循环更好的抽象,应该首选。
阿兰·奥德

2
@GilbertLeBlanc,我们不得不在雪中赤脚编程。
达伍德·伊本·卡里姆

Answers:


154

对于此特定示例,您可以执行以下操作:

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 2013年

8
@jwenting对于具有FP经验的人来说,围绕高阶函数进行旋转的代码绝对是赢家。对于没有经验的人来说,是时候提高您的技能了-否则就有落伍的危险。
Marko Topolnik

2
@MarkoTopolnik您可能想要使用Javadoc的较新版本(您指向的是内部版本78,最新版本是内部版本105:download.java.net/lambda/b105/docs/api/java/util/stream/…
马克·罗特维(Mark Rotteveel),2013年

1
@GraemeMoss您仍然可以使用相同的模式(IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());),但是它使IMO感到困惑,在这种情况下,似乎指示了循环。
assylias 2013年

65

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

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对于这种情况,该方法也有问题,因为每个调用都需要前一个的结果。因此,使用generate和的流iterate在生成重复常数方面效果不佳。

相对较差的性能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!来实现虚拟列表流。我曾期望有必要创建一个专门的分离器,以使其快速运行,但是看起来已经相当不错了。


6
经验不足的开发人员在得知nCopies实际上并没有复制任何内容并且“复制”全部指向该单个对象时,可能会感到困惑或陷入困境。如果该对象是不可变的,则总是安全的,例如本例中的装箱原语。您在“一次装箱”语句中提到了这一点,但是最好在此处明确指出警告,因为这种行为并非特定于自动装箱。
威廉·普赖斯

1
因此,这意味着LongStream.range要比慢得多IntStream.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倍。
塔吉尔·瓦列夫

1
过早优化FTW
拉斐尔·布加杰斯基

1
为了完整起见,很高兴看到一个基准,将这两种技术与一个普通的旧for循环进行比较。虽然您的解决方案比Stream代码更快,但我猜想,for循环会大大击败其中任何一个。
typeracer

35

为了完整性,也因为我无法帮助自己:)

仅在Java级别的冗长程度下,生成有限的常量序列与在Haskell中所看到的非常接近。

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

() -> 1只会产生1,这是故意的吗?因此输出将是1 1 1 1 1 1 1 1
Christian Ullenboom 2014年

4
是的,按照OP的第一个Haskell示例take 8 (repeat 1)。亚述几乎涵盖了所有其他情况。
clstrfsck 2014年

3
Stream<T>它还有一种通用generate方法来获取其他类型的无限流,可以用相同的方式来限制它。
zstewart

11

一旦将重复功能定义为

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

2
这个很棒。但是,通过将更Runnable改为Function<Integer, ?>,然后使用,我对其进行了修改以提供迭代次数f.apply(i)
Fons

0

这是我实现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!");
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.