您的位置 首页 java

Java 8 函数式接口

了解各种开箱即用的函数,如Consumer、Predicate和Supplier。

在本系列的第1部分中,我们了解到lambdas是一种功能接口——具有单个抽象方法的接口。 java API有许多单方法接口,如Runnable、Callable、Comparator、ActionListener等。它们可以使用匿名类语法实现和实例化。例如,以ITrade函数接口为例。它只有一个抽象方法,该方法接受一个交易对象并返回一个 布尔值 ——可能是检查交易状态或验证订单或其他条件。

@FunctionalInterface
public interface ITrade {
public boolean check(Trade t);
}
 

为了满足我们检查新交易的需求,我们可以使用上面的函数接口创建一个lambda表达式,如下图所示:

ITrade newTradeChecker = (Trade t) -> t.getStatus().equals("NEW");
// Or we could omit the input type se Tt ing:
ITrade newTradeChecker = (t) -> t.getStatus().equals("NEW");
 

表达式期望一个Trade实例作为输入参数,声明在箭头标记的左边(我们可以省略输入的类型)。表达式的右侧只是check方法的主体—检查在交易中传递的状态。返回类型是隐式布尔类型,因为检查方法返回布尔类型。当您开始创建大量表示真实行为函数的lambdas时,它的真正威力就来了。例如,除了我们已经看到的,这里是用于找出大交易(i)的lambda表达式。e,如果交易数量大于100万)或查看新创建的大型谷歌交易:

// Lambda for big trade
ITrade bigTradeLambda = (Trade t) -> t.getQuantity() > 10000000;
// Lambda that checks if the trade is a new large google trade
ITrade issuerBigNewTradeLambda = (t) -> {
return t.getIssuer().equals("GOOG") &&
t.getQuantity() > 10000000 &&
t.getStatus().equals("NEW");
};
 

然后可以将这些函数传递给一个方法(很可能是服务器端),该方法接受ITrade作为其参数之一。假设我们有一个交易集合,希望根据某个标准过滤掉一些交易。这个要求可以很容易地表达使用上述lambda传递给一个方法,它也接受一个交易列表:

// Method that takes in list of trades and applies the lambda behavior for each of the trade in the collection
private List<Trade> filterTrades(ITrade tradeLambda, List<Trade> trades) {
List<Trade> newTrades = new ArrayList<>();
for (Trade trade : trades) {
if (tradeLambda.check(trade)) {
newTrades.add(trade);
}
}
return newTrades;
}
 

如果输入交易满足lambda定义的先决条件,则将该交易添加到累积篮子中,否则将丢弃。这个方法的好处是,只要实现ITrade接口,它就可以接受任何lambda。方法如下所示,取不同的lambdas:

// Big trades function is passed
List<Trade> bigTrades =
client.filterTrades(bigTradeLambda,tradesCollection);
// "BIG+NEW+ISSUER"  Function  is passed
List<Trade> bigNewIssuerTrades =
client.filterTrades(issuerBigNewTradeLambda,tradesCollection);
// cancelled trades function is passed
List<Trade> bigNewIssuerTrades =
client.filterTrades(cancelledTradesLambda,tradesCollection);

 

预置的函数库

Java 8的设计者捕获了常用用例,并为它们创建了函数库。一个名为java.util的新包。创建函数来承载这些公共函数。在我们的ITrade案例中,我们所做的一切都是基于一个条件检查业务功能的布尔值。您可以在任何其他对象上测试条件(不仅仅是在trade上)。例如,您可以检查一个员工是否是一长串员工中的一员;从伦敦到巴黎的火车是否准点;不管今天是晴天还是晴天,等等。总的目标是检查一个条件,并在此基础上返回true或false。考虑到这些用例的共性,Java 8引入了一个名为Predicate的函数接口,它的功能与我们使用ITrade时的功能完全相同——检查输入是否正确(真/假)。我们可以使用定义良好的、多面功能接口的标准库,而不是编写自己的功能接口。在库中添加了一些这样的接口,我们将在下一节中讨论。

java.util.Predicate
 

Predicate 就是这样一个函数,它接受一个参数来计算布尔结果。它有一个返回布尔值的方法测试。参见下面的接口定义,它是一个接受任何类型T的泛型类型:

@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
From our knowledge of lambdas so far, we can deduce the lambda expression for this Predicate. Here are some example lambda expressions:
// A large or cancelled trade (this time using library function)!
Predicate<Trade> largeTrade = (Trade t) -> t.isBigTrade();
// Parenthesis and type are optional
Predicate<Trade> cancelledTrade = t -> t.isCancelledTrade();
// Lambda to check an empty string
Predicate<String> emptyStringChecker = s -> s.isEmpty();
// Lambda to find if the employee is an executive
Predicate<Employee> isExec = emp -> emp.isExec();
 

调用Predicate的test来检查true false,如下面的代码片段所示:

// Check to see if the trade has been cancelled
boolean cancelledTrade = cancelledTrade.test(t);
// Check to find if the employee is executive
boolean executive = isExec.test(emp);
 

java.util.Function

它接受类型T的参数,并通过apply方法在输入上应用指定的逻辑返回类型R的结果。界面定义如下:

// The T is the input argument while R is the return result
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
 

我们使用一个函数来进行转换,例如将温度从摄氏温度转换为华氏温度,将字符串转换为整数,等等:

// convert centigrade to fahrenheit
Function<Integer,Double> centigradeToFahrenheitInt = x -> new Double((x*9/5)+32);
// String to an integer
Function<String, Integer> stringToInt = x -> Integer.valueOf(x);
// tests
 System .out.println("Centigrade to Fahrenheit: "+centigradeToFahrenheitInt.apply(centigrade))
System.out.println(" String to Int: " + stringToInt.apply("4"));
 

这表示函数正在等待字符串并返回整数。更复杂一点的需求,比如为给定的交易列表聚合交易数量,可以表示为一个函数,如下所示:

// Function to calculate the aggregated quantity of all the trades - taking in a collection and returning an integer!
Function<List<Trade>,Integer> aggegatedQuantity = t -> {
int aggregatedQuantity = 0;
for (Trade t: t){
aggregatedQuantity+=t.getQuantity();
}
return aggregatedQuantity;
};
 

以上交易的聚合可以使用Stream API的“fluent”风格来完成:

// Using Stream and map and reduce functionality
aggregatedQuantity =
trades.stream()
.map((t) -> t.getQuantity())
.reduce(0, Integer:: sum );
// Or, even better
aggregatedQuantity =
trades.stream()
.map((t) -> t.getQuantity())
.sum();
 

其他函数

使用者接受单个参数,但不返回任何结果:

@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
 

这主要用于对参数执行操作,如持久化员工、调用内部维护操作、发送电子邮件时事通讯等。顾名思义,供应商为我们提供了结果;下面是方法签名:

@FunctionalInterface
public interface Supplier<T> {
T get();
}
 

例如,从数据库中获取配置值、加载引用数据、使用默认标识符创建学生列表等,都可以由supplier函数表示。在这些函数之上,Java 8还为特定用例提供了这些函数的专门版本。例如,扩展函数的UnaryOperator只对相同类型起作用。因此,如果我们知道输入和输出类型是相同的,我们可以使用UnaryOperator而不是Function。

// Here, the expected input and return type are exactly same.
// Hence we didn't use Function, but we are using a specialized sub-class
UnaryOperator<String> toLowerUsingUnary = (s) -> s.toLowerCase();
 

您注意到函数是用一种泛型类型声明的吗?在本例中,它们都是相同的字符串——因此,我们没有编写函数,而是使用+UnaryOperator进一步缩短了它。远一点,功能也提供给代表原始的专门化,DoubleUnaryOperator, LongUnaryOperator, IntUnaryOperator等等。他们基本上处理操作原语如双出双或长返回,等。就像函数的孩子适应特殊情况,所以做其他功能像IntPredicate或者LongConsumer BooleanSupplier,等。例如,IntPredicate代表一个整数值函数为基础,LongConsumer期望long值不返回结果,而BooleanSupplier提供布尔值。有太多这样的专门化,因此我强烈建议您详细阅读API以理解它们。

两个参数函数

到目前为止,我们已经处理了只接受单个输入参数的函数。有些用例可能必须对两个参数进行操作。例如,一个函数需要两个参数,但通过对这两个参数进行操作而产生结果。这种类型的功能适合于双参数函数桶,如BiPredicate、BiConsumer、BiFunction等。它们也非常容易理解,除了签名将具有额外的类型(两个输入类型和一个返回类型)。例如,双功能接口定义如下所示

@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
The above function has three types – T, U and R. The first two are are input types while the last one is the return result. For completeness, the following example provides a snippet of BiFunction usage, which accepts two Trades to produce sum of trade quantities. The input types are Trade and return type is Integer:
BiFunction<Trade,Trade,Integer> sumQuantities = (t1, t2) -> {
return t1.getQuantity()+t2.getQuantity();
};
 

使用两个参数函数的相同主题时,BiPredicate需要两个输入参数并返回一个布尔值(这并不奇怪!)

// Predicate expecting two trades to compare and returning the condition's output
BiPredicate<Trade,Trade> isBig = (t1, t2) -> t1.getQuantity() > t2.getQuantity();
 

正如您现在可能已经猜到的,这两个参数函数也有专门化。例如,对相同类型的操作数进行操作并发出相同类型的函数。这种功能是在BinaryOperator函数中捕获的。注意,extends BinaryOperator扩展了双函数。下面的例子展示了BinaryOperator的作用——它接受两个交易来生成一个合并的交易(它与输入参数的类型相同——请记住,BinaryOperator是双函数的一种特殊情况)。

BiFunction<Trade,Trade,Trade> tradeMerger2 = (t1, t2) -> {
// calling another method for merger
return merge(t1,t2);
};
// This method is called from the lambda.
// You can prepare a sophisticated algorithm to merge a trade in here
private Trade merge(Trade t1, Trade t2){
t1.setQuantity(t1.getQuantity()+t2.getQuantity());
return t1;
}
 

另外,您是否注意到实际的逻辑是由超函数双函数而不是二元运算符执行的?这是因为BinaryOperator扩展了双函数:

//BinaryOperator is a special case of BiFunction.
public interface BinaryOperator<T> extends BiFunction<T,T,T> { .. }
 

Now that we have seen the functions and functional interfaces, there is something I would like to reveal about interfaces. Until Java 8, the interfaces were abstract creatures – you certainly cannot add implementation to it, making it brittle in some ways. However, Java 8 re-engineered this, calling them virtual methods.

Virtual (default) methods

The journey of a Java library begins with a simple interface. Over time, these libraries are expected to evolve and grow in functionality. However, pre-Java 8 interfaces are untouchable! Once they are defined and declared, they are practically written in stone. For obvious reasons, backward compatibility being the biggest one, they cannot be changed after the fact. While it’s great that lambdas have been added to the language, there’s no point of having them if they cannot be used with existing APIs. In order to add support to absorb lambdas into our libraries, the interfaces needed to evolve. That is, we need to be able to add additional functionality to an already published API. The dilemma is how to embrace lambdas to create or enhance the APIs without losing backward compatibility? This requirement pushed Java designers to come up with yet another elegant feature – providing virtual extension methods or simply default methods to the interfaces. This means we can create concrete methods in our interfaces going forward. The virtual methods allow us to add newer functionality too. The collections API is one such example, where bringing lambdas into the equation has overhauled and enhanced the APIs. Let us walk through a simple example to demonstrate the default method functionality. Let’s assume that every component is said to have a name and creation date when instantiated. However, should the implementation doesn’t provide concrete implementation for the name and creation date, they would be inherited from the interface by default. For out example, the IComponent interface defines a set of default methods as shown below:

@FunctionalInterface
public interface IComponent {
// Functional method - note we must have one of these functional methods only
public void init();
// default method - note the keyword default
default String getComponentName(){
return "DEFAULT NAME";
}
// default method - note the keyword default
default Date getCreationDate(){
return new Date();
}
}
 

从上面的代码片段可以看出,接口现在有了一个实现,而不仅仅是抽象的。方法的前缀是一个关键字default,表示它们是默认方法

多重继承

多重继承对Java来说并不新鲜。Java从一开始就提供了多种类型的继承。如果我们有一个实现各种接口的对象层次结构,有一些规则可以帮助我们理解哪些实现应用于子类。基本规则是最接近子类的具体实现胜过其他继承的行为。直接的具体类型优先于任何其他类型。以下面的类层次结构为例。

// Person interface with a concrete implementation of name
interface Person{
default String getName(){
return "Person";
}
}
// Faculty interface extending Person but with its own name implementation
interface Faculty extends Person{
default public String getName(){
return "Faculty";
}
}
 

因此,Person和Faculty接口都提供了name的默认实现。但是,请注意,Faculty扩展Person,但是覆盖行为来提供它自己的实现。对于实现这两个接口的任何类,名称都是从Faculty继承的,因为它是最接近子类的子类型。因此,如果我有一个Student子类来实现Faculty(和Person),那么Student的getName()方法会打印出Faculty的名称:

// The Student inherits Faculty's name rather than Person
class Student implements Faculty, Person{ .. }
// the getName() prints Faculty
private void test() {
String name = new Student().getName();
System.out.println("Name is "+name);
}


output: Name is Faculty
 

然而,有一点需要注意。如果我们的教职工班根本不扩员怎么办?在这种情况下,Student类从两个实现中继承名称,从而使编译器发出呻吟。在这种情况下,为了使编译器满意,我们必须自己提供具体的实现。但是,如果希望继承超类型的行为之一,可以显式地继承,如下面的代码片段所示。

interface Person{ .. }
// Notice that the faculty is NOT implementing Person
interface Faculty { .. }
// As there's a conflict, out Student class must explicitly declare whose name it's going to inherit!
class Student implements Faculty, Person{
@Override
public String getName() {
return Person.super.getName();
}
}
 

有一种特殊的语法可以从超级接口获取方法– using super-interface.super.method: Person.super.getName().

方法引用

public class AddableTest {
// Add given two integers
private int addThemUp(int i1, int i2){
return i1+i2;
}
 

因为添加整数的方法已经存在,所以没有必要创建一个lambda表达式来做完全相同的事情。因此,当为可IAddable实现创建lambda时,我们通过方法引用(使用双冒号::)来引用这个现有的方法:

public class AddableTest {
// Lambda expression using existing method
IAddable addableViaMethodReference = this::addThemUp;
// Add given two integers
private int addThemUp(int i1, int i2){
return i1+i2;
}
}
 

注意上面代码中的this::addThemUp lambda表达式。this引用AddableTest类的一个实例,而双冒号后面的位是对已有方法的调用。另外,请注意方法引用没有在末尾添加大括号()。如果您有另一个类通过静态方法实现所需的功能,您可以通过使用方法引用的这个特性在lambda表达式中简单地使用它的方法。请看下面给出的例子:

// Class that provides the functionality via it's static method
public class AddableUtil {
public static int addThemUp(int i1, int i2){
return i1+i2;
}
}
// Test class
public class AddableTest {
// Lambda expression using static method on a separate class
IAddable addableViaMethodReference = AddableUtil::addThemUp;
...
}
 

我们还可以通过简单地调用类的构造函数Employee::new或Trade::new来使用构造函数引用。

总结

在这篇文章中,我们学习了更多关于Java功能接口和函数的知识。我们深入了解了各种开箱即用的函数,如Consumer、Predicate、Supplier等。我们还研究了虚拟方法和方法引用。在本系列的下一篇文章中,我们将详细讨论流API。

文章来源:智云一二三科技

文章标题:Java 8 函数式接口

文章地址:https://www.zhihuclub.com/189794.shtml

关于作者: 智云科技

热门文章

网站地图