返回介绍

Java 8 lambda 最佳实践

发布于 2025-02-26 22:54:39 字数 8130 浏览 0 评论 0 收藏 0

原文出处: oschina-成熟的毛毛虫

Java 8 已经推出一段时间了,越来越多开发人员选择升级 JDK,这条 热门动弹 里面看出,JDK7 最多,其次是 6 和 8,这是好事!

在 8 里面 Lambda 是最火的主题,不仅仅是因为语法的改变,更重要的是带来了函数式编程的思想,我觉得优秀的程序员,有必要学习一下函数式编程的思想以开阔思路。所以这篇文章聊聊 Lambda 的应用场景,性能,也会提及下不好的一面。

Java 为何需要 Lambda

1996 年 1 月,Java 1.0 发布了,此后计算机编程领域发生了翻天覆地的变化。商业发展需要更复杂的应用,大多数程序都跑在更强大的装备多核 CPU 的机器上。带有高效运行期编译器的 Java 虚拟机(JVM)的出现,使得程序员将精力更多放在编写干净、易于维护的代码上,而不是思考如何将每一个 CPU 时钟、每一字节内存物尽其用。

多核 CPU 的出现成了“房间里的大象”,无法忽视却没人愿意正视。算法中引入锁不但容易出错,而且消耗时间。人们开发了 java.util.concurrent 包和很多第三方类库,试图将并发抽象化,用以帮助程序员写出在多核 CPU 上运行良好的程序。不幸的是,到目前为止,我们走得还不够远。

那些类库的开发者使用 Java 时,发现抽象的级别还不够。处理大数据就是个很好的例子,面对大数据,Java 还欠缺高效的并行操作。Java 8 允许开发者编写复杂的集合处理算法,只需要简单修改一个方法,就能让代码在多核 CPU 上高效运行。为了编写并行处理这些大数据的类库,需要在语言层面上修改现有的 Java:增加 lambda 表达式。

当然,这样做是有代价的,程序员必须学习如何编写和阅读包含 lambda 表达式的代码,但是,这不是一桩赔本的买卖。与手写一大段复杂的、线程安全的代码相比,学习一点新语法和一些新习惯容易很多。开发企业级应用时,好的类库和框架极大地降低了开发时间和成本,也扫清了开发易用且高效的类库的障碍。

如果你还未接触过 Lambda 的语法,可以看 这里

Lambda 的应用场景

你有必要学习下函数式编程的概念,比如 函数式编程初探 ,但下面我将重点放在函数式编程的实用性上,包括那些可以被大多数程序员理解和使用的技术,我们关心的如何写出好代码,而不是符合函数编程风格的代码。

1.使用() -> {} 替代匿名类

现在 Runnable 线程,Swing,JavaFX 的事件监听器代码等,在 java 8 中你可以使用 Lambda 表达式替代丑陋的匿名类。

//Before Java 8:
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Before Java8 ");
    }
}).start();

//Java 8 way:
new Thread(() -> System.out.println("In Java8!"));

// Before Java 8:
JButton show =  new JButton("Show");
show.addActionListener(new ActionListener() {
     @Override
     public void actionPerformed(ActionEvent e) {
           System.out.println("without lambda expression is boring");
        }
     });

// Java 8 way:
show.addActionListener((e) -> {
    System.out.println("Action !! Lambda expressions Rocks");
});

2.使用内循环替代外循环

外循环:描述怎么干,代码里嵌套 2 个以上的 for 循环的都比较难读懂;只能顺序处理 List 中的元素;

内循环:描述要干什么,而不是怎么干;不一定需要顺序处理 List 中的元素

//Prior Java 8 :
List features = Arrays.asList("Lambdas", "Default Method",
"Stream API", "Date and Time API");
for (String feature : features) {
   System.out.println(feature);
}

//In Java 8:
List features = Arrays.asList("Lambdas", "Default Method", "Stream API",
 "Date and Time API");
features.forEach(n -> System.out.println(n));

// Even better use Method reference feature of Java 8
// method reference is denoted by :: (double colon) operator
// looks similar to score resolution operator of C++
features.forEach(System.out::println);

Output:
Lambdas
Default Method
Stream API
Date and Time API

3.支持函数编程

为了支持函数编程,Java 8 加入了一个新的包 java.util.function,其中有一个接口 java.util.function.Predicate 是支持 Lambda 函数编程:

public static void main(args[]){
  List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

  System.out.println("Languages which starts with J :");
  filter(languages, (str)->str.startsWith("J"));

  System.out.println("Languages which ends with a ");
  filter(languages, (str)->str.endsWith("a"));

  System.out.println("Print all languages :");
  filter(languages, (str)->true);

   System.out.println("Print no language : ");
   filter(languages, (str)->false);

   System.out.println("Print language whose length greater than 4:");
   filter(languages, (str)->str.length() > 4);
}

 public static void filter(List names, Predicate condition) {
    names.stream().filter((name) -> (condition.test(name)))
        .forEach((name) -> {System.out.println(name + " ");
    });
 }

Output:
Languages which starts with J :
Java
Languages which ends with a
Java
Scala
Print all languages :
Java
Scala
C++
Haskell
Lisp
Print no language :
Print language whose length greater than 4:
Scala
Haskell

4.处理数据?用管道的方式更加简洁

Java 8 里面新增的 Stream API ,让集合中的数据处理起来更加方便,性能更高,可读性更好

假设一个业务场景:对于 20 元以上的商品,进行 9 折处理,最后得到这些商品的折后价格。

final BigDecimal totalOfDiscountedPrices = prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(price -> price.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO,BigDecimal::add);

System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);

想象一下:如果用面向对象处理这些数据,需要多少行?多少次循环?需要声明多少个中间变量?

关于 Stream API 的详细信息,可以查看我之前写的 文章

Lambda 的性能

Oracle 公司的性能工程师 Sergey Kuksenko 有一篇很好的性能比较的文档: JDK 8: Lambda Performance study , 详细而全面的比较了 lambda 表达式和匿名函数之间的性能差别。这里是 视频 。 16 页讲到最差(capture)也和 inner class 一样, non-capture 好的情况是 inner class 的 5 倍。

lambda 开发组也有一篇 ppt , 其中也讲到了 lambda 的性能(包括 capture 和非 capture 的情况)。看起来 lambda 最差的情况性能内部类一样, 好的情况会更好。

Java 8 Lambdas – they are fast, very fast 也有篇文章 (需要翻墙),表明 lambda 表达式也一样快。

Lambda 的阴暗面

前面都是讲 Lambda 如何改变 Java 程序员的思维习惯,但 Lambda 确实也带来了困惑

JVM 可以执行任何语言编写的代码,只要它们能编译成字节码,字节码自身是充分 OO 的,被设计成接近于 Java 语言,这意味着 Java 被编译成的字节码非常容易被重新组装。

但是如果不是 Java 语言,差距将越来越大,Scala 源码和被编译成的字节码之间巨大差距是一个证明,编译器加入了大量合成类 方法和变量,以便让 JVM 按照语言自身特定语法和流程控制执行。

我们首先看看 Java 6/7 中的一个传统方法案例:

// simple check against empty strings
public static int check(String s) {
    if (s.equals("")) {
        throw new IllegalArgumentException();
    }
    return s.length();
}

//map names to lengths

List lengths = new ArrayList();

for (String name : Arrays.asList(args)) {
    lengths.add(check(name));
}

如果一个空的字符串传入,这段代码将抛出错误,堆栈跟踪如下:

at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)

再看看 Lambda 的例子

Stream lengths = names.stream().map(name -> check(name));

at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)

这非常类似 Scala,出错栈信息太长,我们为代码的精简付出力代价,更精确的代码意味着更复杂的调试。

但这并不影响我们喜欢 Lambda!

总结

在 Java 世界里面,面向对象还是主流思想,对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式我们都得学。

这种新的抽象方式还有其他好处。很多人不总是在编写性能优先的代码,对于这些人来说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更多地表达了业务逻辑,而不是从机制上如何实现。易读的代码也易于维护、更可靠、更不容易出错。

在写回调函数和事件处理器时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,只有在真正需要的时候,才初始化变量的值。

总而言之,Java 更趋于完美了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文