Java 8怎么了:局部套用vs闭包

简介: 【编者按】本文作者为专注于自然语言处理多年的 Pierre-Yves Saumont,Pierre-Yves 著有30多本主讲 Java 软件开发的书籍,自2008开始供职于 Alcatel-Lucent 公司,担任软件研发工程师。 本文主要介绍了 Java 8 中的闭包与局部套用功能,由国内 I

【编者按】本文作者为专注于自然语言处理多年的 Pierre-Yves Saumont,Pierre-Yves 著有30多本主讲 Java 软件开发的书籍,自2008开始供职于 Alcatel-Lucent 公司,担任软件研发工程师。

本文主要介绍了 Java 8 中的闭包与局部套用功能,由国内 ITOM 管理平台 OneAPM 编译呈现。

关于Java 8,存在着许多错误观念。譬如,认为Java 8给Java带来了闭包特性就是其中之一。这个想法是错的,因为闭包特性从Java诞生之初就已经存在了。然而闭包是有缺陷的。尽管Java 8似乎倾向于函数式编程,我们仍应尽力避免使用Java闭包。但是,Java 8并没有在此方面提供过多帮助。

我们知道,参数求值时间是使用方法和使用函数时的一个重大区别。在Java中,我们可以写一个带参数且有返回值的方法。但是,这可以被称作函数吗?当然不能。方法只可以通过调用进行操纵,这表示它的参数会在该方法执行前取值。这是Java中参数按值传递的结果。

函数则与之不同。操作函数时我们可以不计算参数,且对参数何时取值有绝对的控制权。而且,如果一个函数有多个参数,它们可以不同时取值。这一点通过局部套用就可以做到。但是首先,我们将考虑如何利用闭包进行实现。

闭包举例

对函数而言,闭包能够在封装的上下文中获取内容。在函数式编程中,一个函数的结果应当仅由其参数决定。很显然,闭包打破了这一准则。

请看Java 5/6/7中的示例:

private Integer b = 2; 
    List list = Arrays.asList(1, 2, 3, 4, 5); 
    System.out.println(calculate(list.stream(), 3).collect(toList())); 
    private Stream calculate(Stream stream, Integer a) { 
      return stream.map(new Function() { 
        @Override 
        public Integer apply(Integer t) { 
          return t * a + b; 
        } 
      }); 
    } 
    public interface Function<T, U> { 
      U apply(T t); 
    }

以上代码将产生如下结果:

[5, 8, 11, 14, 17]

所得结果是函数 f(x) = x 3 + 2 对于列 [1, 2, 3, 4, 5]的映射。到这一步都没什么问题。但是3和2可以用其他值替换吗?换句话说,它难道不是函数f(x, a, b) = x a + b 对于该列的映射吗?

是,也不是。不是的原因在于a和b都被隐性定义了final关键词,因此它们在函数取值时作为常数参与计算。但是当然,它们的值也会有变动。它们的final属性(在Java 8中隐性定义,在之前版本中则显性定义)只是编译器优化编译过程的一种方式。编译器并不在乎任何潜在的变动值。它只在乎引用有没有发生变动,也就是说,它想要确保Integer整数对象ab的引用不发生变化,但并不在意它们的取值。这个特性在以下代码中可以看出:

 private Integer b = 2; 
    private Integer getB() { 
      return this.b; 
    } 
    List list = Arrays.asList(1, 2, 3, 4, 5); 
    System.out.println(calculator.calculate(list.stream(), new Int(3)).collect(toList())); 
    private Streamcalculate00(Streamstream, final Int a) { 
      return stream.map(new Function() { 
        @Override 
        public Integer apply(Integer t) { 
          return t * a.value + getB(); 
        } 
      }); 
 
    } 
    - 
    static private class Int { 
      public int value; 
      public Int(int value) { 
        this.value = value; 
      } 
     }

在这里,我们使用了可变对象a(属于Int类,而不是不可变的Integer类),以及一个方法来获取b。现在,我们来模拟一个有三个变量的函数,但是仍旧使用仅有一个变量的函数,同时使用闭包来代替其他两个变量。很显然,这是非函数性的,因为它打破了仅依赖于函数参数的准则。

结果之一是,尽管有需要,我们也不能在别的地方重用这个函数,因为它依赖于上下文而不仅仅依赖于参数。我们要复制这些代码才能实现重用。另一个结果是,由于它需要上下文才能运行,我们也不能单独进行函数测试。

那么,我们应该使用带有三个参数的函数吗?我们可能会认为,这不可能实现。因为具体的实现过程与三个参数何时取值相关。它们都在不同的地方取值。如果我们刚才使用的是带有三个参数的函数,它们就必须同时取值。而映射方法只会映射带一个参数的函数到流,不可能映射带有三个参数的函数。因此,其余两个参数在函数绑定时(也即传递给映射时)必须已经取值。解决方法是先对其余两个参数取值。

我们也可以用闭包来实现这一功能,但是所得代码是不可测试的,且可能存在重叠。

使用Java 8 的句法(lambdas)也无法改变这一状况:

private Integer b = 2; 
    private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { 
      return stream.map(t -> t * a + b); 
 
    }

我们需要的是一种在不同时间获取三个参数的方法——Currying(局部套用,也称柯里化函数,尽管它其实是Moses Shönfinkel发明的)。

使用局部闭包

局部闭包就是逐一对函数参数取值,每一步都生成少一个参数的新函数。举例来看,如果我们有如下函数:

f(x, y, z) = x * y + z

我们可以同时取参数值为2,4,5,得到以下方程:

f(3, 4, 5) = 3 * 4 + 5 = 17

我们也可以只取一个参数为3,得到以下方程:

f(3, y, z) = g(y, z) = 3 * y + z

现在,我们得到了只有两个参数的新函数g。再对该函数进行局部套用,将4赋值给y:

g(4, z) = h(z) = 3 * 4 + z

给参数赋值的顺序对计算结果并无影响。此处,我们并不是在局部相加,(如果是局部相加,我们还得考虑运算符优先级。)而是在进行对函数的局部应用。

那么,我们如何在Java中实现这种方法呢?以下是在Java5/6/7中的应用:

private static List<Integer> calculate(List<Integer> list, Integer a) { 
      return list.map(new Function<Integer, Function<Integer, Function<Integer, Integer>>>() { 
        @Override 
        public Function<Integer, Function<Integer, Integer>> apply(final Integer x) { 
          return new Function<Integer, Function<Integer, Integer>>() { 
            @Override 
            public Function<Integer, Integer> apply(final Integer y) { 
              return new Function<Integer, Integer>() { 
                @Override 
                public Integer apply(Integer t) { 
                  return x + y * t; 
                } 
              }; 
            } 
          }; 
        } 
      }.apply(b).apply(a)); 
    }

以上代码完全可以实现所需功能,但是要想说服开发者,让他们用这种方式编写代码,恐怕非常困难!还好,Java 8的lambda句法提供了以下实现方式:

private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {

      return stream.map(((Function<Integer, Function<Integer, Function<Integer, Integer>>>) 
                           x -> y -> t -> x + y * t).apply(b).apply(a)); 
    }

怎么样?或者,是不是可以写得更简单一点:

private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { 
      return stream.map((x -> y -> t -> x + y * t).apply(b).apply(a)); 
    }

完全可以,但是Java 8不能自行判断参数类型,因此我们必须使用manifest类型来帮助确认(manifest在Java规范中的意思是explicit)。为了让代码看起来更整洁,我们可以使用一些小技巧:

interface F3 extends Function<Integer, Function<Integer, Function<Integer, Integer>>> {} 
    private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { 
      return stream.map(((F3) x -> y -> z -> x + y * z).apply(b).apply(a)); 
    }

现在,我们来为函数命名,并在必要时重用它:

private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { 
      F3 calculation = x -> y -> z -> x + y * z; 
      return stream.map(calculation.apply(b).apply(a)); 
    }

我们还可以声明计算函数为一个辅助类的静态成员,使用静态导入来进一步简化代码:

public class Functions { 
      static Function<Integer, Function<Integer, Function<Integer, Integer>>> calculation = 
           x -> y -> z -> x + y * z; 
        } 
        ... 
 
        import static Functions.calculation; 
        private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { 
          return stream.map(calculation.apply(b).apply(a)); 
        }

可惜,Java 8 鼓励的是使用闭包。不然,我会介绍更多能让局部套用的使用更为简便的功能性语法糖。比如,在Scala中,以上例子就可以这样改写:

stream.map(calculation(b)(a))

虽然在Java中我们没法这样写。可是,通过下面的静态方法,我们可以达到相似的效果:

static Function<Integer, Function<Integer, Function<Integer, Integer>>> calculation 
        = x -> y -> z -> x + y * z; 
    static Function<Integer, Integer> calculation(Integer x, Integer y) { 
      return calculation.apply(x).apply(y); 
    }

现在,我们可以写:

 private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { 
      return stream.map(calculation(b, a)); 
    }

请注意,calculation(b, a)不是带有两个参数的函数。它只是一个方法,在将两个参数逐一地局部调用至一个带有三个参数的函数之后,它会返回一个带有一个参数的函数,该函数便可传递给映射函数。

现在,calculation方法便可以单独测试了。

自动局部调用

在之前的例子中,我们已经亲手实践过局部调用了。然而,我们大可以编写程序来自动化调用过程。我们可以编写这样一个方法:它会接收带有两个参数的函数,并返回该函数的局部调用版本。写起来非常简单:

public <A, B, C> Function<A, Function<B, C>> curry(final BiFunction<A, B, C> f) { 
      return (A a) -> (B b) -> f.apply(a, b); 
    }

有必要的话,我们还可以写一个方法来颠倒这一过程。这个过程可以接受A的Function函数作为参数,返回一个可返回C的B的Function函数,最终返回一个返回C的A,B的BiFunction函数。

public <A, B, C> BiFunction<A, B, C> uncurry(Function<A, Function<B, C>> f) { 
      return (A a, B b) -> f.apply(a).apply(b); 
    }

局部调用的其他应用

局部调用的应用方式还有很多。最重要的应用是模拟多参数函数。在Java 8提供了单参数函数(java.util.functions.Function)以及双参数函数(java.util.functions.BiFunction)。但并未提供存在于其他语言中的三参数、四参数、五参数甚至更多参数的函数。其实,有没有这些函数并不重要。它们只是在特定情况下,需要同时对所有参数取值时应用的语法糖。实际上,这也是BiFunctin在Java 8中存在的原因:函数的常见使用方法就是模拟二元运算符,(请注意:在Java 8中有BinaryOperator接口,但它只用于两个参数以及返回值都属于同一类型的特殊情况。我们将在下一篇文章中讨论这一点。)

局部调用在函数的各个参数需要在不同地方取值时是非常好用的。通过局部调用,我们可以在某一组件中对一个参数取值,然后将计算结果传递到另一组件对其他参数取值,如此反复,直到所有参数值都被取到。

小结

Java 8并不是一种函数式语言(可能永远也不会是)。但是,我们仍可以在Java(甚至是Java 8之前的版本)中使用函数式范式。这样做的确会略有代价。但这种代价在Java 8中已经大幅减少了。尽管如此,想要写函数型代码的开发者还是得动动脑筋才能掌握这种范式。使用局部调用就是智力成果之一。

请记住:

(A, B, C) -> D

总是可以由如下方式替代:

A -> B -> C -> D

即便Java 8无法判断该表达方式的类型,你只要自行指定其类型就可以了。这就是局部调用,它总是比闭包更为稳妥。

编译自:https://dzone.com/articles/whats-wrong-java-8-currying-vs

相关文章
|
28天前
|
存储 缓存 安全
HashMap VS TreeMap:谁才是Java Map界的王者?
HashMap VS TreeMap:谁才是Java Map界的王者?
65 2
|
1月前
|
数据采集 缓存 Java
Python vs Java:爬虫任务中的效率比较
Python vs Java:爬虫任务中的效率比较
|
4月前
|
缓存 JavaScript Java
怎么在Java 16中编写C风格的局部静态变量
Java 16通过 JEP 395 放宽了内层类声明静态成员的限制, 允许声明静态成员, 如记录类成员. 这项改进使得可以在局部范围内使用类似 C 风格的静态变量, 即局部变量仅初始化一次并在多次调用间共享. 例如, 缓存正则表达式模式, 以前需要将其置于类命名空间中, 现在可以保持在方法范围内
|
27天前
|
安全 Java 程序员
Java集合之战:ArrayList vs LinkedList,谁才是你的最佳选择?
本文介绍了 Java 中常用的两个集合类 ArrayList 和 LinkedList,分析了它们的底层实现、特点及适用场景。ArrayList 基于数组,适合频繁查询;LinkedList 基于链表,适合频繁增删。文章还讨论了如何实现线程安全,推荐使用 CopyOnWriteArrayList 来提升性能。希望帮助读者选择合适的数据结构,写出更高效的代码。
52 3
|
5月前
|
Java C++ 开发者
【技术贴】if-else VS switch:谁才是Java条件判断的王者?
【6月更文挑战第14天】本文探讨了Java中if-else与switch语句的选择问题。if-else基于布尔逻辑,适合处理复杂逻辑,而switch在处理多分支特别是枚举类型时更高效。if-else在条件动态变化或复杂逻辑时更合适,switch则因其跳转表机制在固定选项中表现优秀。性能上,switch在大量选项时占优,但现代JVM优化后两者差异不大。选择时应考虑场景、可读性和维护性,灵活运用。理解两者特点,才能写出优雅高效的代码。
400 0
|
6月前
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
3月前
|
Java C++ 开发者
if-else VS switch:谁才是Java条件判断的王者?
if-else VS switch:谁才是Java条件判断的王者?
44 3
|
3月前
|
传感器 C# 监控
硬件交互新体验:WPF与传感器的完美结合——从初始化串行端口到读取温度数据,一步步教你打造实时监控的智能应用
【8月更文挑战第31天】本文通过详细教程,指导Windows Presentation Foundation (WPF) 开发者如何读取并处理温度传感器数据,增强应用程序的功能性和用户体验。首先,通过`.NET Framework`的`Serial Port`类实现与传感器的串行通信;接着,创建WPF界面显示实时数据;最后,提供示例代码说明如何初始化串行端口及读取数据。无论哪种传感器,只要支持串行通信,均可采用类似方法集成到WPF应用中。适合希望掌握硬件交互技术的WPF开发者参考。
68 0
|
3月前
|
C# Windows 开发者
当WPF遇见OpenGL:一场关于如何在Windows Presentation Foundation中融入高性能跨平台图形处理技术的精彩碰撞——详解集成步骤与实战代码示例
【8月更文挑战第31天】本文详细介绍了如何在Windows Presentation Foundation (WPF) 中集成OpenGL,以实现高性能的跨平台图形处理。通过具体示例代码,展示了使用SharpGL库在WPF应用中创建并渲染OpenGL图形的过程,包括开发环境搭建、OpenGL渲染窗口创建及控件集成等关键步骤,帮助开发者更好地理解和应用OpenGL技术。
246 0
|
3月前
|
开发者 C# 容器
【独家揭秘】当WPF邂逅DirectX:看这两个技术如何联手打造令人惊艳的高性能图形渲染体验,从环境搭建到代码实践,一步步教你成为图形编程高手
【8月更文挑战第31天】本文通过代码示例详细介绍了如何在WPF应用中集成DirectX以实现高性能图形渲染。首先创建WPF项目并使用SharpDX作为桥梁,然后在XAML中定义承载DirectX内容的容器。接着,通过C#代码初始化DirectX环境,设置渲染逻辑,并在WPF窗口中绘制图形。此方法适用于从简单2D到复杂3D场景的各种图形处理需求,为WPF开发者提供了高性能图形渲染的技术支持和实践指导。
221 0