【问题标题】:Why functions considered as first-class citizens are so important? [closed]为什么被视为一等公民的功能如此重要? [关闭]
【发布时间】:2015-08-21 11:32:20
【问题描述】:

Java 8 提供了一堆函数式接口,我们可以使用 lambda 表达式来实现这些接口,它允许将函数视为 first-class citizen(作为参数传递,从方法返回等...)。

例子:

Stream.of("Hello", "World").forEach(str->System.out.println(str));

为什么被视为一等公民的职能如此重要?有什么例子可以证明这种力量吗?

【问题讨论】:

  • Java 对待函数的方式仍然与它在 loooooonnnng 时期的处理方式几乎相同。例如。 Comparator 接口自 java 1.2 以来就已经存在,现在添加了 @FunctionalInterface 并没有太大变化。但是,您现在可以使用 lambda 表达式和方法引用来编写更短的代码这一事实很好。
  • 一流的函数允许您将行为视为数据。这反过来又使您能够更自然地对行为进行抽象,进而使您能够编写更具表现力的高级 API(如 Streams)。这反过来又会导致更多富有表现力、更易读、更不容易出错的应用程序代码。
  • 另一种表述方式(基本上是@BrianGoetz 所说的)是一流函数允许您定义可以很好地堆叠、组合和链接行为的高阶函数总而言之,所有这些都以声明性、简洁和可读的方式进行。网上有很多关于函数式编程及其好处的资源。我喜欢这类问题,但我怀疑 SO 会允许它,因为它非常广泛。

标签: java interface lambda functional-programming java-8


【解决方案1】:

这是一个简短的节目,展示了(可以说)主要的差异化因素。

public static void main(String[] args) {
  List<Integer> input = Arrays.asList(10, 12, 13, 15, 17, 19);

  List<Integer> list = pickEvensViaLists(input);
  for (int i = 0; i < 2; ++i) 
    System.out.println(list.get(i));

  System.out.println("--------------------------------------------");
  pickEvensViaStreams(input).limit(2).forEach((x) -> System.out.println(x));
}      

private static List<Integer> pickEvensViaLists(List<Integer> input) {
  List<Integer> list = new ArrayList<Integer>(input);
  for (Iterator<Integer> iter = list.iterator(); iter.hasNext(); ) {
    int curr = iter.next();
    System.out.println("processing list element " + curr);
    if (curr % 2 != 0) 
      iter.remove();
  }
  return list;
}

private static Stream<Integer> pickEvensViaStreams(List<Integer> input) {
  Stream<Integer> inputStream = input.stream();
  Stream<Integer> filtered = inputStream.filter((curr) -> { 
    System.out.println("processing stream element " + curr);
    return curr % 2 == 0; 
  });
  return filtered;
}

这个程序接受一个输入列表并从中打印前两个偶数。它这样做了两次:第一次使用带有手写循环的列表,第二次使用带有 lambda 表达式的流。

在两种方法中必须编写的代码量方面存在一些差异,但这不是(在我看来)重点。不同之处在于评估的方式:

在基于列表的方法中,pickEvensViaLists() 的代码会遍历整个列表。它将从列表中删除所有奇数值,然后才会返回到main()。因此,它返回给main() 的列表将包含四个值:10, 12, 20, 30main() 将只打印前两个。

在基于流的方法中,pickEvensViaStreams() 的代码实际上并不迭代任何东西。它返回一个可以从输入流中计算出来的流,但它还没有计算任何一个。只有当main() 开始迭代(通过forEach())时,返回的流的元素才会被一一计算。由于main() 只关心前两个元素,因此实际上只计算了返回流的两个元素。换句话说:使用流,您会得到惰性评估:流只在需要时迭代。

我们来看看这个程序的输出:

--------------------------------------------
list-based filtering:
processing list element 10
processing list element 12
processing list element 13
processing list element 15
processing list element 17
processing list element 19
processing list element 20
processing list element 30
10
12
--------------------------------------------
stream-based filtering:
processing stream element 10
10
processing stream element 12
12

使用列表迭代整个输入(因此有八个“处理列表元素”消息)。使用流时,实际上只从输入中提取了两个元素,导致只有两个“处理流元素”消息。

【讨论】:

    【解决方案2】:

    这是一个表现力的问题。您不必这样做,但在许多实际情况下,它会使您的代码更具可读性和简洁性。例如,获取您的代码:

    public class Foo {
        public static void main(String[] args) {
            Stream.of("Hello", "World").forEach(str->System.out.println(str));
        }
    }
    

    并将其与我能想到的最简洁的 Java 7 实现进行比较:

    interface Procedure<T> {
        void call(T arg);
    }
    
    class Util {
        static <T> void forEach(Procedure<T> proc, T... elements) {
            for (T el: elements) {
                proc.call(el);
            }
        }
    }
    
    public class Foo {
        static public void main(String[] args) {
            Util.forEach(
                new Procedure<String>() {
                    public void call(String str) { System.out.println(str); }
                },
                "Hello", "World"
            );
        }
    }
    

    结果是一样的,行数少了一点:) 另请注意,为了支持具有不同数量参数的Procedure 实例,您将需要每个接口或(更实用)将所有参数传递为单个 Parameters 对象。通过在Procedure 实现中添加一些字段,可以以类似的方式进行闭包。这是很多样板。

    事实上,像一流的“函子”和(非可变)闭包这样的东西已经使用anonymous classes 存在了很长时间,但它们需要大量的实现工作。 Lambda 只是让事情变得更容易读写(至少,在大多数情况下)。

    【讨论】:

      【解决方案3】:

      我认为问题的第二部分已经得到很好的解决。但我想尝试回答第一个问题。

      根据定义,一等公民函数可以做的事情更多。一等公民函数可以:

      1. 由变量命名
      2. 作为参数传递
      3. 作为另一个函数的结果返回
      4. 作为成员数据类型参与数据结构(例如,数组或列表)

      这些是“头等舱”的特权。

      【讨论】:

      • 但是,您也可以使用旧的单方法匿名类的所有 4 个项目。 Java 8 只是让事情变得更简洁。 Lambda 不会引入任何没有它们就无法完成的事情。
      【解决方案4】:

      这个想法是能够将行为作为参数传递。例如,这在实现Strategy pattern 时很有用。

      Streams API 是一个很好的例子,说明了将行为作为参数传递是如何有用的:

      people.stream()
        .map(person::name)
        .map(name->new GraveStone(name, Rock.GRANITE)
        .collect(Collectors.toSet())
      

      它还允许程序员从函数式编程而不是面向对象编程的角度进行思考,这对于很多任务来说很方便,但在答案中涵盖的内容相当广泛。

      【讨论】:

        猜你喜欢
        • 2011-07-07
        • 2015-09-18
        • 2013-06-20
        • 2015-08-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多