传统命令式编程风格与编程功能风格之间的差异

时间:2015-08-21 07:34:14

标签: java performance java-8 java-stream

我在这里有一个问题陈述 我需要做什么迭代列表找到第一个大于3的整数,然后只是加倍并返回它。

这些是检查执行操作的方法

public static boolean isGreaterThan3(int number){
        System.out.println("WhyFunctional.isGreaterThan3 " + number);
        return number > 3;
    }
    public static boolean isEven(int number){
        System.out.println("WhyFunctional.isEven " + number);
        return number % 2 == 0;
    }
    public static int doubleIt(int number){
        System.out.println("WhyFunctional.doubleIt " + number);
        return number << 1;
    }

使用java 8流我可以像

那样做
List<Integer> integerList = Arrays.asList(1, 2, 3, 5, 4, 6, 7, 8, 9, 10);
integerList.stream()
           .filter(WhyFunctional::isGreaterThan3)
           .filter(WhyFunctional::isEven)
           .map(WhyFunctional::doubleIt)
           .findFirst();

,输出

WhyFunctional.isGreaterThan3 1
WhyFunctional.isGreaterThan3 2
WhyFunctional.isGreaterThan3 3
WhyFunctional.isGreaterThan3 5
WhyFunctional.isEven 5
WhyFunctional.isGreaterThan3 4
WhyFunctional.isEven 4
WhyFunctional.doubleIt 4
Optional[8]

总共8次操作。

使用命令式样式或在java8之前,我可以将其编码为

for (Integer integer : integerList) {
            if(isGreaterThan3(integer)){
                if(isEven(integer)){
                    System.out.println(doubleIt(integer));
                    break;
                }
            }
        }

,输出

WhyFunctional.isGreaterThan3 1
WhyFunctional.isGreaterThan3 2
WhyFunctional.isGreaterThan3 3
WhyFunctional.isGreaterThan3 5
WhyFunctional.isEven 5
WhyFunctional.isGreaterThan3 4
WhyFunctional.isEven 4
WhyFunctional.doubleIt 4
8

和操作相同。所以我的问题是,如果我使用流而不是传统的循环,它会有什么不同。

5 个答案:

答案 0 :(得分:7)

Stream API引入了流的新概念,允许您以新的方式解耦任务。例如,根据您的任务,您可能希望使用大于三的偶数偶数来执行不同的操作。在某些地方你想要找到第一个,在其他地方你需要10个这样的数字,在第三个地方你想要应用更多的过滤。您可以封装查找此类数字的算法,如下所示:

static IntStream numbers() {
    return IntStream.range(1, Integer.MAX_VALUE)
                    .filter(WhyFunctional::isGreaterThan3)
                    .filter(WhyFunctional::isEven)
                    .map(WhyFunctional::doubleIt);
}

在这里。您刚刚创建了一个算法来生成这样的数字(不生成它们),您不关心它们将如何使用。一个用户可能会打电话:

int num = numbers().findFirst().get();

其他用户可能需要获得10个这样的数字:

int[] tenNumbers = numbers().limit(10).toArray();

第三个用户可能想要找到第一个匹配的数字,该数字也可被7整除:

int result = numbers().filter(n -> n % 7 == 0).findFirst().get();

将算法封装在传统的命令式风格中会更加困难。

通常,Stream API与性能无关(尽管并行流可能比传统解决方案更快)。这是关于代码的表达能力。

答案 1 :(得分:6)

命令式样式使用用于实现它的机制(迭代)来补充计算逻辑。另一方面,功能样式是 decomplects 两者。您针对您提供逻辑的API进行编码,API可以自由选择 以及 时应用它。

特别是,Streams API有两种方式如何应用逻辑:顺序或并行。后者实际上是将lambdas和Streams API本身引入Java的驱动力。

执行计算时选择的自由会导致懒惰:而在命令式样式中,您拥有一个具体的数据集合,在功能样式中您可以拥有一个集合与逻辑,以转换它。当您实际使用数据时,可以及时应用逻辑#34;这进一步允许您扩展计算的构建:每个方法可以接收流并在其上应用进一步的计算步骤,或者它可以以不同的方式使用它(通过收集到列表,通过仅查找第一个项目和从不将计算应用于其余部分,但计算总值等)。

作为懒惰提供的新机会的一个特例,我能够编写一个Spring MVC控制器,它返回一个Stream,其数据源是一个数据库 - 当我返回流时,数据仍然在数据库中。只有View层才会提取数据,隐式应用它不知道的转换逻辑,永远不必在内存中保留多个流元素。这将经典地具有O(n)空间复杂度的解决方案转换为O(1),从而对结果集的大小变得不敏感。

答案 2 :(得分:3)

使用Stream API描述操作而不是实现它。允许Stream API实现操作的一个众所周知的优点是可以选择使用不同的执行策略,如并行执行(正如其他人已经说过的那样)。

另一个似乎有点被低估的特性是可能以命令式编程风格无法改变操作本身,因为这意味着修改代码:

IntStream is=IntStream.rangeClosed(1, 10).filter(i -> i > 4);
if(evenOnly) is=is.filter(i -> (i&1)==0);
if(doubleIt) is=is.map(i -> i<<1);
is.findFirst().ifPresent(System.out::println);

这里,在终端操作开始之前做出是否过滤掉奇数或加倍结果的决定。在命令式编程中,您必须重新检查循环中的标志或编码多个替代循环。应该提到的是,在循环中检查这样的条件在今天的JVM上并不是那么糟糕,因为优化器能够在运行时将它们移出循环,因此通常不需要编写多个循环。

但请考虑以下示例:

Stream<String> s = Stream.of("java8 streams", "are cool");
if(singleWords) s=s.flatMap(Pattern.compile("\\s")::splitAsStream);
s.collect(Collectors.groupingBy(str->str.charAt(0)))
 .forEach((k,v)->System.out.println(k+" => "+v));

由于flatMap相当于嵌套循环,因此我们有一个基于运行时值的简单循环或嵌套循环,因此在命令式样式中编码相同并不那么简单。通常,如果要在两种循环之间共享代码,则必须将代码拆分为多种方法。

我已经遇到过一个现实生活中的例子,其中复杂操作的组合有多个条件flatMap步骤。等效的命令式代码是疯狂的......

答案 3 :(得分:1)

1)功能方法允许更多的声明性编程方式:您只需提供要应用的函数列表,而不需要手动编写迭代,因此您的代码有时会更加健全。

2)如果切换到并行流(https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html),则可以自动将程序转换为并行并更快地执行。它是可能的,因为你没有显式地编写迭代代码,只列出要应用的函数,因此编译器/运行时可以将它并行。

答案 4 :(得分:1)

在这个简单的例子中,没有什么区别,JVM将尝试在每种情况下进行相同数量的工作。

你开始看到区别的是更复杂的例子,如

integerList.parallelStream()

使代码并发循环要困难得多。注意:你实际上不会这样做,因为开销会很高,你只需要第一个元素。

BTW第一个示例返回结果,第二个示例返回。