问候! :)
离开几个月后,我决定恢复风格:)。 我注意到我以前有关新的Date / Time API的一篇文章非常受欢迎,因此这次我将把本篇文章专门介绍Java 8的另一个新功能: Lambda Expressions 。
功能编程
Lambda表达式是Java编程语言最终实现函数式编程细微差别的方式。
函数式编程的定义充满争议。 以下是维基百科告诉我们的内容:
“在计算机科学中,函数式编程是一种编程范式,一种构建计算机程序的结构和元素的方式,将计算视为对数学函数的评估,并避免了状态和可变数据”
总而言之, lambda表达式将允许将行为,函数作为方法调用中的参数进行传递。 这是一个与Java程序员习惯不同的范例,因为一直以来,我们只编写了将对象作为参数的方法,而没有其他方法!
Java平台在这次聚会上实际上有点晚了。 诸如Scala,C#,Python甚至Javascript之类的其他语言已经这样做了相当长的时间。 有人认为,即使lambda使“事半功倍”成为可能,但它会损害代码的可读性。 那些反对在Java编程语言中添加lambda的人经常使用此指控。 马丁·福勒本人曾说过:
任何傻瓜都可以编写计算机可以理解的代码。 好的程序员编写人类可以理解的代码。”
除了争议之外,至少有一个很好的理由支持lambda表达式 :并行性。 随着多核CPU的激增,必须编写易于使用并行处理的代码。 在Java 8之前,还没有一种简单的方法可以轻松并行地迭代大量对象。 正如我们将进一步看到的那样,使用Streams将使我们能够做到这一点。
Lambdas与匿名内部类
对于那些无法抑制您的兴奋的人,这里是第一个口味。 所谓的“经典”使用lambda会发生在通常选择匿名类的地方。 如果您想到它,那就是我们想要传递“行为”而不是状态(对象)的确切位置。
例如,我将使用大多数人可能已经知道的Swing API。 实际上,在任何需要处理用户事件的GUI API中,此类情况几乎都是相同的:JavaFX,Apache Wicket,GWT等。
使用Swing ,如果希望在用户单击按钮时执行某些操作,则可以执行以下操作:
上图显示的是我们在Java中处理事件的最常用方式之一。 但是请注意,我们的真正意图只是将行为传递给addActionListener()方法(按钮动作)。 我们最终要做的是传递一个对象(状态)作为参数,即匿名ActionListener 。
以及如何使用lambda来完成完全相同的事情? 像这样:
如我之前所说,我们可以“事半功倍”。 我们仅将我们真正想完成的动作(仅行为)作为参数传递给addActionListener方法。 创建匿名类所需的所有麻烦事就消失了。 语法细节将在以后进行探讨,但是上面代码中的lambda表达式可以归结为:
(event) -> System.out.println("Button 2 clicked!")
我知道我知道。 你们中有些人可能在想:
“等一下! 自从《 地牢与龙》第一集问世以来,我一直是一名摇摆式程序员,但我从未见过仅用一行代码就能处理事件!
冷静,年轻的绝地武士。 也可以用“ n”行代码编写lambda 。 但是再说一遍,代码越大,我们获得的可读性就越少:
就个人而言,我仍然是那些认为即使使用多个语句,使用lambda的代码也比使用匿名类的代码更干净的人的一部分。 如果我们忽略缩进,那么所有语法要求就是将大括号加起来作为块定界符,并且每个语句都有自己的“;”:
(event) -> {System.out.println("First"); System.out.println("Second");}
但是,不要失去所有希望。 当您有多个语句时,仍然存在使用lambda处理事件的更简洁的方法。 只需看下面的代码摘录:
public class MyFrame extends Frame {public MyFrame() {//create the buttonJButton button5 = new JButton("Button 5");//"buttonClick()" is a private method of this very classbutton5.addActionListener(e -> buttonClick(e));//etc etc etc}private void buttonClick(ActionEvent event) {//multiple statements here}
}
看到? 就那么简单。
@FunctionalInterface
要编写lambda表达式,您首先需要一个所谓的“功能接口” 。 “功能接口”是具有完全一种抽象方法的Java接口。 不要忘记这一部分,“一种抽象方法”。 这是因为现在在Java 8中可以在接口内部具有具体的方法实现: 默认方法和静态方法 。
就规范而言,您在接口中可能拥有的所有默认方法和静态方法均不计入功能接口配额。 如果您有9个默认或静态方法,并且只有一个抽象方法,那么从概念上讲,它仍然是一个函数接口 。 为了使情况更清楚一点,有一个功能丰富的注释 @FunctionalInterface,其唯一作用是将接口标记为“功能性”。 请注意,与@Override一起发生时,它的用途仅是在编译时演示意图。 尽管它是可选的,但我强烈建议您使用它。
ps:以前使用的ActionListener接口只有一个抽象方法,这使其成为完整的功能接口。
让我们创建一个简单的示例,以增强lambda表达式的语法。 想象一下,我们想创建一个API,一个类,用作两个Double类型操作数的计算器。 也就是说,一个java类具有加,减,除等方法,两个类型为Double的对象:
public class Calculator {public static Double sum(Double a, Double b) {return a + b;}public static Double subtract(Double a, Double b) {return a - b;}public static Double multiply(Double a, Double b) {return a * b;}//etc etc etc...
}
为了“直接脱离NASA”使用此计算器,API的客户端只需调用任何静态方法即可:
Double result = Calculator.sum(200, 100); //300
但是,这种方法存在一些问题。 实际上不可能对Double类型的两个对象之间的所有可能操作进行编程。 很快,我们的客户将需要较少的通用操作,例如平方根或其他。 而您(此API的所有者)将永远被奴役。
如果我们的计算器足够灵活以使客户自己知道他们想使用哪种数学运算,那不是很好吗? 为了实现这个目标,我们首先创建一个称为DoubleOperator的功能接口 :
@FunctionalInterface
public interface DoubleOperator {public Double apply(Double a, Double b);}
我们的接口定义了一个合约,通过该合约对两个Double类型的对象进行操作,该对象还返回一个Double。 确切的操作将留给客户决定。
现在, Calculator类仅需要一个方法,将两个Double操作数作为参数和一个lambda表达式 ,这些表达式将使我们的客户可以知道他们想要的操作:
public class Calculator {public static Double calculate(Double op1, Double op2, DoubleOperator operator) {return operator.apply(op1, op2); //delegate to the operator}}
最后,这是我们的客户如何在新API上调用方法:
//sum
Double result1 = Calculator.calculate(30d, 70d, (a, b) -> a + b);
System.out.println(result1); //100.0//subtract
Double result2 = Calculator.calculate(200d, 50d, (a, b) -> a - b);
System.out.println(result2); // 150.0//multiply
Double result3 = Calculator.calculate(5d, 5d, (a, b) -> a * b);
System.out.println(result3); // 25.0//find the smallest operand using a ternary operator
Double result4 = Calculator.calculate(666d, 777d, (a, b) -> a > b ? b : a);
System.out.println(result4); //666.0
现在的天空是极限。 客户可以使用任何想到的方法调用calculate()方法。 他们需要做的就是提供一个有效的lambda表达式 。
Lambda必须以字符“->”分隔。 左侧部分仅用于参数声明。 右侧部分代表方法实现本身:
请注意,左侧部分仅具有参数声明,该参数声明对应于DoubleOperator.apply(Double a,Double b)签名。 该参数的类型可以由编译器推断,并且在大多数情况下无需通知。 同样,参数变量的名称可以是我们想要的任何名称,而不必像我们的功能接口的签名一样是“ a”和“ b” :
//sum with explicit types
Double result1 = Calculator.calculate(30d, 70d, (Double x, Double y) -> x + y); //another way
OperadorDouble operator = (Double op1, Double op2) -> op1 + op2;
Double result2 = Calculator.calculate(30d, 70d, operador);
当功能接口的方法签名没有任何参数时,您要做的就是放置一个空的“()” 。 这可以在Runnable界面的帮助下看到:
/* The r variable can be passed to any method that takes a Runnable */
Runnable r = () -> System.out.println("Lambda without parameter");
出于好奇,我将展示一种替代语法,该语法也可用于声明lambda ,即方法参考 。 我不会深入探讨细节,否则我将需要一整本书来撰写这篇文章。 当您的所有表达式想要进行方法调用时,它提供了一种更简洁的方法:
JButton button4 = new JButton("Button 4");//this
button4.addActionListener(ActionEvent::getSource); //is equivalent to this
button4.addActionListener((event) -> event.getSource());
不要重新发明轮子
在继续之前,让我们快速停下来以记住我们都知道的这个旧术语。 这意味着在Java的8 API中,我们日常工作中可能已经需要大量的功能接口 。 其中包括一个可以完全消除我们的DoubleOperator接口的接口。
所有这些接口都位于java.util.function包内,主要的接口是:
名称 | 参量 | 返回 | 例 |
---|---|---|---|
BinaryOperator <T> | (T,T) | Ť | 在相同类型的两个对象之间进行任何类型的操作。 |
消费者<T> | Ť | 虚空 | 打印一个值。 |
函数<T,R> | Ť | [R | 以Double类型的对象并将其作为String返回。 |
谓词<T> | Ť | 布尔值 | 对作为参数传递的对象进行任何类型的测试:oneString.endsWith(“ suffix”) |
供应商<T> | – | Ť | 进行不带任何参数但具有返回值的操作。 |
不是吗 所有其他内容只是上述内容的变体。 足够快地,当我们看到Streams的使用时,我们将有机会看到大多数Streams的实际应用,并且更容易适应整个画面。 不过,我们可以重构我们的计算器类,并通过在JDK中,已经提供了一个代替古老DoubleOperator接口BinaryOperator :
public class Calculator {public static <T> T calculate(T op1, T op2, BinaryOperator<T> operator) {return operator.apply(op1, op2);}}
对于我们的客户,除了BinaryOperator接口具有参数化类型, 泛型之外,几乎没有什么改变,现在我们的计算器更加灵活,因为我们可以在任何类型的两个对象之间进行数学运算,而不仅仅是Doubles :
//sum integers
Integer result1 = Calculator.calculate(5, 5, (x, y) -> x + y);
集合和流
作为开发人员,我们可能会浪费大部分时间使用第三方API,而不是自己开发。 这就是到目前为止,我们已经完成了这些工作,了解了如何在自己的API中使用lambda 。
现在是时候分析对核心Java API所做的一些更改了,这些更改使我们可以在处理集合时使用lambda 。 为了说明我们的示例,我们将使用一个简单的类Person ,它具有名称 , 年龄和性别 (“ M”代表男性,“ F”代表女性):
public class Person {private String name;private Integer age;private String sex; //M or F//gets and sets
}
前面的所有示例都需要对象集合,因此,假设我们有一个Person类型的对象集合:
List<Person> persons = thisMethodReturnsPersons();
我们从添加到Collection接口的新方法stream()开始。 由于所有集合都“扩展” Collection ,因此所有Java集合都继承了此方法:
List<Person> persons = thisMethodReturnsPersons();
Stream<Person> stream = persons.stream(); //a stream of person objects
尽管它看来, 流接口不只是一个更经常类型的集合。 Stream更多地是一种“数据流”抽象,使我们能够转换或操纵其数据。 与我们已经知道的集合不同, Stream不允许直接访问其元素(我们需要将Stream转换回Collection )。
为了进行比较,让我们看看如果我们必须计算人员集合中有多少个女性对象,我们的代码会是什么样子。 首先,没有流 :
long count = 0;
List<Person> persons = thisMethodReturnsPersons();
for (Person p : persons) {if (p.getSex().equals("F")) {count++; }
}
使用for循环,我们创建一个计数器,该计数器在每次遇到女性时都会增加。 这样的代码我们已经完成了数百次。
现在使用流同样的事情:
List<Person> persons = thisMethodReturnsPersons();
long count = persons.stream().filter(person -> person.getSex().equals("F")).count();
清洁得多,不是吗? 这一切都始于调用stream()方法,所有其他调用都链接在一起,因为Stream接口中的大多数方法都是在考虑到Builder模式的情况下设计的。 对于那些不习惯使用这种方法进行链接的用户,可能更容易这样可视化:
List<Person> persons = thisMethodReturnsPersons();
Stream<Person> stream = persons.stream();
stream = stream.filter(person -> person.getSex().equals("F"));
long count = stream.count();
让我们将注意力集中在我们使用的Stream的两种方法中, filter()和count() 。
filter()采用条件来过滤集合。 这个条件由一个lambda表达式表示,该表达式带有一个参数并返回一个布尔值 :
person -> person.getSex().equals("F")
并非偶然,用于表示该表达式的功能接口 (filter()方法的参数)是谓词接口。 她只有一种抽象方法boolean test(T t) :
@FunctionalInterface
public interface Predicate<T> {boolean test(T t);//non abstract methods here
}
参数化类型T表示流元素的类型,即Person对象。 这样就好像我们的lambda表达式实现了test()方法一样:
boolean test(Person person) {if (person.getSex().equals("F")) {return true;} else {return false;}
}
过滤之后,剩下的就是调用count()方法。 没什么大不了的,它只是计算过滤发生后我们流中还剩下多少个对象(除了过滤之外,我们还可以有更多的东西)。 count()方法被视为“终端操作”,在调用该方法后,该流被称为“已消耗”且无法再使用。
让我们看一下Stream接口的其他一些方法。
收集()
通常使用collect()方法对流执行可变还原 (有关详细信息,请参见链接)。 这通常意味着将流转换回普通集合。 注意,与count()方法一样, collect()方法也是“终端操作” !
假设最后一个示例有一个小的变体,我们只想从人的集合中过滤掉女性对象。 但是,这一次我们不只是过滤女性( filter() ),然后对它们进行计数( count() )。 我们将以物理方式将所有女性对象分离到一个完全不同的集合中,该集合仅包含女性:
List<Person> persons = thisMethodReturnsPersons();//creating a List with females only
List<Person> listFemales = persons.stream().filter(p -> p.getSex().equals("F")).collect(Collectors.toList());//creating a Set with females only
Set<Person> setFemales = persons.stream().filter(p -> p.getSex().equals("F")).collect(Collectors.toSet());
过滤部分保持不变,唯一的不同是最后对collect()的调用。 如我们所见,此调用接受一个参数和Collector类型的对象。
要构建类型为Collector的对象需要花费一些时间,因此幸运的是,有一个类允许我们以更方便的方式构建它们,并与Collectors (plural)类会面。 如Collectors.toList()和Collectors.toSet()所示 。 一些有趣的例子:
//We can choose the specific type of collection we want
//by using Collectors.toCollection().//another way for building a Stream
Stream<String> myStream = Stream.of("a", "b", "c", "d"); //transforming into a LinkedList (using method reference)
LinkedList<String> linkedList = myStream.collect(Collectors.toCollection(LinkedList::new));//transforming into a TreeSet
Stream<String> s1 = Stream.of("a", "b", "c", "d");
TreeSet<String> t1 = s1.collect(Collectors.toCollection( () -> new TreeSet<String>() ));//using method reference, the same would be accomplished like this
Stream<String> s2 = Stream.of("a", "b", "c", "d");
TreeSet<String> t2 = s2.collect(Collectors.toCollection( TreeSet::new ));
请注意Collectors.toCollection()方法如何采用类型Supplier的lambda表达式 。
功能接口 Supplier提供了一个抽象方法T get() ,该方法不接受任何参数并返回一个对象。 这就是为什么我们的表达式只是对我们要使用的集合构造函数的调用:
() -> new TreeSet<String>()
地图()
map()方法非常简单。 当您要在其他某种类型的对象中转换一个集合的每个元素时,可以使用它,这意味着将一个集合的每个元素映射到另一种类型的元素。
让我们的示例更进一步,让我们尝试以下情形:给定Person对象的集合,让我们获得一个完全不同的集合,该集合仅包含女性对象名称(如Strings),全部使用大写字母。 概括起来,除了使用filter()和collect()来将我们所有的女性对象分离在自己的集合中之外,我们还将使用map()方法将每个女性Person对象转换为其String表示形式(名称用大写字母表示) ):
这是代码:
List<Person> persons = thisMethodReturnsPersons();List<String> names = persons.stream().filter(p -> p.getSex().equals("F")).map(p -> p.getName().toUpperCase()).collect(Collectors.toList());
用作map()方法的参数的功能接口是Function ,其唯一的抽象方法R apply(T t)将一个对象作为参数,并返回不同类型的对象。 这就是map()的确切含义:拿一个Person并变成String 。
forEach()和forEachOrdered()
也许最简单的方法是forEach()和forEachOrdered()提供访问流中每个元素的方法,例如在遇到控制台时打印每个元素。 两者之间的主要区别是,第一个不保证“相遇顺序”,第二个不保证。
流是否具有“相遇顺序”取决于它的始发集合以及在其中执行的中介操作。 源自列表的 流具有预期的定义顺序。
这次功能接口是Consumer ,其抽象方法void accept(T t)接受单个参数,并且不返回任何内容:
List<Person> persons = thisMethodReturnsPersons();//print without any "encounter order" guarantee
persons.stream().forEach(p -> System.out.println(p.getName()));//print in the correct order if possible
persons.stream().forEachOrdered(p -> System.out.println(p.getName()));
请记住, forEach()和forEachOrdered() 也是终端操作 ! (您不需要内心地知道这一点,只需在需要时在javadocs中进行查找即可)
min()和max()
使用lambda表达式查找集合的最小和最大元素也变得容易得多 。 使用常规算法,这是一种既简单又令人讨厌的例程。
让我们获取Person对象的集合,并在其中找到最年轻和最老的人:
List<Person> persons = thisMethodReturnsPersons();//youngest using min()
Optional<Person> youngest = persons.stream().min((p1, p2) -> p1.getAge().compareTo(p2.getAge()));//oldest using max()
Optional<Person> oldest = persons.stream().max((p1, p2) -> p1.getAge().compareTo(p2.getAge()));//printing their ages in the console
System.out.println(youngest.get().getAge());
System.out.println(oldest.get().getAge());
min()和max()方法也采用一个功能接口作为参数,只有这一点并不新鲜: Comparator 。 ( ps :如果您正在阅读本文,但不知道“比较器”是什么,我建议您退后一步,尝试使用Java基础知识,然后再使用lambdas。)
上面的代码还有一些我们之前从未见过的东西,即Optional类。 这也是Java 8中的新功能,我不会详细介绍它。 如果您感到好奇,请点击此链接。
使用新的静态方法Comparator.comparing()可以达到相同的结果,该方法采用一个Function并充当创建比较器的实用程序:
//min()
Optional<Person> youngest = persons.stream().min(Comparator.comparing(p -> p.getAge()));//max()
Optional<Person> oldest = persons.stream().max(Comparator.comparing(p -> p.getAge()));
关于collect()和Collector的更多信息
使用方法collect()使我们能够进行一些非常有趣的操作,以及一些内置的Collector的帮助 。
例如,可以计算所有Person对象的平均年龄:
List<Person> persons = thisMethodReturnsPersons();Double average = persons.stream().collect(Collectors.averagingDouble(p -> p.getAge()));System.out.println("A average is: " + average);
Collector类中有3种方法可以朝这个方向提供帮助,每种方法特定于一种数据类型:
- Collectors.averagingInt() (整数)
- Collectors.averagingLong() (longs)
- Collectors.averagingDouble() (双精度)
所有这些方法都返回一个有效的收集器 ,该收集器可以作为参数传递给collect() 。
另一个有趣的可能性是能够将一个集合stream划分为两个值集合。 当我们专门为女性“ Person”对象创建新集合时,我们已经做过类似的事情,但是我们的原始集合仍然将男性和女性对象混合在一起。 如果我们想将原始收藏划分为两个新收藏,一个仅包含男性,另一个则包含女性,该怎么办?
为了实现这一点,我们将使用Collectors.partitioningBy() :
List<Person> persons = thisMethodReturnsPersons();//a Map Boolean -> List<Person>
Map<Boolean, List<Person>> result = persons.stream().collect(Collectors.partitioningBy(p -> p.getSex().equals("M")));//males stored with the 'true' key
List<Person> males = result.get(Boolean.TRUE);//females stored with the 'false' key
List<Person> females = result.get(Boolean.FALSE);
上面显示的Collectors.partitioningBy()方法通过创建一个包含两个元素的Map来工作,一个元素存储为键“ true” ,另一个存储为“ false”键。 由于它采用Predicate类型的功能接口 ,其返回值是一个boolean值 ,因此其表达式的值为“ true”的元素将进入“ true”集合,而那些值为“ false”的元素将进入“ false”集合。
为了解决这个问题,让我们假设另一个场景,其中我们可能希望按年龄对所有Person对象进行分组。 看起来就像我们对Collectors.partitioningBy()所做的一样,只是这次不是一个简单的true / false条件,而是我们由年龄决定的条件。
小菜一碟,我们只使用Collectors.groupingBy() :
//Map "Age" -> "List<Person>"
Map<Integer, List<Person>> result = persons.stream().collect(Collectors.groupingBy(p -> p.getAge()));
如果没有lambda,您将如何做? 考虑一下让我头疼。
性能与并行性
在本文的开头,我提到使用lambda表达式的优点之一是能够并行处理集合,这就是我接下来要说明的内容。 令人惊讶的是,没有什么可显示的。 为了使所有先前的代码成为“并行处理”,我们需要做的就是更改一个方法调用:
List<Person> persons = thisMethodReturnsPersons();//sequential
Stream<Person> s1 = persons.stream();//parallel
Stream<Person> s2 = persons.parallelStream();
而已。 只需将对stream()的调用更改为parallelStream()即可进行并行处理。 所有其他链接的方法调用均保持不变。
为了演示使用并行处理的区别,我使用最后一个代码示例进行了测试,该示例按年龄将所有Person对象分组。 考虑到2000万个对象的测试数据,这是我们得到的:
如果将不带lambda的“老派”方法与顺序lambda处理stream()进行比较 ,可以说是平局。 另一方面, parallelStream()的速度似乎快三倍。 只有4秒。 那是300%的差异。
注意:这绝不意味着您应该并行进行所有处理!
除了显而易见的事实,即我的测试过于简单以至于不能盲目地考虑之外,在选择并行处理之前还必须考虑到并行性存在固有的开销:将集合分解为多个集合,然后再次合并以形成最终结果,这一点很重要。 。
话虽这么说,如果没有相对大量的元素,那么并行处理的成本可能不会得到回报。 在不加选择地使用parallelStream()之前,请仔细分析。
好吧,我想这就是全部。 当然涵盖所有内容是不可能的,需要整本书,但是我认为这里显示了很多相关方面。 如果您有什么话要发表评论。
编码愉快!
翻译自: https://www.javacodegeeks.com/2015/03/java-8-lambda-expressions-tutorial.html