你好! 在 Java Syntax Pro 任务中,我们研究了 lambda 表达式,并表示它们只不过是函数式接口中函数式方法的实现。换句话说,这是某个匿名(未知)类的实现,其未实现的方法。如果在课程讲座中我们深入研究了 lambda 表达式的操作,那么现在我们将考虑另一面:即这些接口。Java 的第八版引入了函数式接口的概念。这是什么?具有一个未实现(抽象)方法的接口被认为是功能性的。许多开箱即用的接口都属于这个定义,例如前面讨论的接口。还有我们自己创建的接口,例如: 谓词
消费者
供应商
功能
一元运算符
Comparator
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
我们有一个接口,其任务是将一种类型的对象转换为另一种类型的对象(一种适配器)。该注释@FunctionalInterface
并不是超级复杂或重要的东西,因为它的目的是告诉编译器该接口是函数式的并且不应包含超过一个方法。如果带有此注释的接口具有多个未实现(抽象)方法,则编译器将不会跳过该接口,因为它会将其视为错误代码。没有这个注解的接口可以被认为是功能性的并且可以工作,但这 @FunctionalInterface
无非是额外的保险。我们回去上课吧Comparator
。如果你查看它的代码(或文档),你会发现它有不止一种方法。然后你会问:那么,如何才能将其视为函数式接口呢?抽象接口可以具有不在单个方法范围内的方法:
- 静止的
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
收到这个方法后,编译器没有抱怨,这意味着我们的接口仍然有效。
- 默认方法
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
}
再次,我们看到编译器没有开始抱怨,我们也没有超出函数式接口的限制。
- 对象类方法
Object
。这不适用于接口。但是,如果我们在接口中有一个抽象方法,该方法的签名与类的某些方法相匹配Object
,那么这样的方法(或多个方法)将不会打破我们的功能接口限制:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
boolean equals(Object obj);
}
同样,我们的编译器不会抱怨,因此该接口Converter
仍然被认为是函数式的。现在的问题是:为什么我们需要将自己限制在函数式接口中的一个未实现的方法?然后我们就可以使用 lambda 来实现它。让我们看一个例子Converter
。为此,我们创建一个类Dog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
还有一只类似的Raccoon
(浣熊):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
假设我们有一个对象Dog
,并且需要根据其字段创建一个对象Raccoon
。也就是说,Converter
它将一种类型的对象转换为另一种类型。它会是什么样子:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
当我们运行它时,我们会在控制台上得到以下输出:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
这意味着我们的方法工作正常。
基本 Java 8 函数式接口
好吧,现在让我们看一下 Java 8 为我们带来的几个函数式接口,它们与 Stream API 结合使用。谓词
Predicate
— 用于检查是否满足特定条件的功能接口。如果满足条件,则返回true
,否则返回 - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
作为示例,考虑创建一个Predicate
将检查多种类型的奇偶校验的Integer
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
控制台输出:
true
false
消费者
Consumer
(来自英语 - “consumer”) - 一个函数接口,它将类型 T 的对象作为输入参数,执行一些操作,但不返回任何内容:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
例如,考虑,其任务是使用传递的字符串参数将问候语输出到控制台: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
控制台输出:
Hello Elena !!!
供应商
Supplier
(来自英语 - 提供程序) - 一个不接受任何参数但返回 T 类型对象的函数式接口:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
例如,考虑Supplier
,它将从列表中生成随机名称:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
如果我们运行它,我们将看到控制台中名称列表的随机结果。
功能
Function
— 此函数接口接受参数 T 并将其转换为类型 R 的对象,该对象作为结果返回:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
举个例子,它将数字从字符串格式 ( ) 转换为数字格式 ( ): Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
当我们运行它时,我们会在控制台上得到以下输出:
678
PS:如果我们不仅向字符串中传递数字,还向字符串中传递其他字符,则会抛出异常- NumberFormatException
。
一元运算符
UnaryOperator
— 一个函数式接口,它接受类型 T 的对象作为参数,对其执行一些操作,并以相同类型 T 的对象的形式返回操作结果:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
,它使用它的方法apply
来计算数字的平方:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
控制台输出:
81
我们研究了五个功能接口。从 Java 8 开始,这并不是我们可用的全部 - 这些是主要接口。其余可用的都是其复杂的类似物。完整的列表可以在 Oracle 官方文档中找到。
Stream 中的函数接口
如上所述,这些功能接口与 Stream API 紧密耦合。你问如何?因此,许多方法Stream
专门与这些功能接口一起工作。让我们看看如何在Stream
.
带谓词的方法
例如,让我们采用类方法Stream
-filter
它接受参数Predicate
并Stream
仅返回满足条件的元素Predicate
。在 -a 的上下文中Stream
,这意味着它仅传递在接口方法true
中使用时返回的那些元素。这就是我们的示例的样子,但对于 中的元素过滤器: test
Predicate
Predicate
Stream
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
因此,列表evenNumbers
将包含元素 {2, 4, 6, 8}。而且,正如我们所记得的,collect
它将所有元素收集到某个集合中:在我们的例子中,收集到List
。
与消费者的方法
Stream
中使用函数式接口的方法之一Consumer
是peek
. 这就是我们的Consumer
in示例Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
控制台输出:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
但由于该方法peek
适用于,因此不会发生Consumer
对字符串的修改,而是会返回原始元素:与它们到达时相同。因此,该列表将包含元素“Elena”、“John”、“Alex”、“Jim”、“Sara”。还有一种常用的方法,与方法类似,但不同的是它是final-terminal。Stream
peek
Stream
peopleGreetings
foreach
peek
与供应商的方法
Stream
使用函数接口的方法的一个示例Supplier
是generate
,它根据传递给它的函数接口生成无限序列。让我们使用我们的示例Supplier
将五个随机名称打印到控制台:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
这是我们在控制台中得到的输出:
John
Elena
Elena
Elena
Jim
这里我们用 方法limit(5)
对 method 进行了限制generate
,否则程序会无限期地将随机名称打印到控制台。
方法与函数
Stream
带参数的方法的一个典型示例Function
是map
采用一种类型的元素、对它们执行某些操作并将它们传递出去的方法,但这些元素已经可以是不同类型的元素。Function
in的示例可能如下所示Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
结果,我们得到了一个数字列表,但是在Integer
.
使用 UnaryOperator 的方法
作为用作参数的方法UnaryOperator
,我们采用类方法Stream
- iterate
。此方法与 方法类似generate
:它也生成无限序列,但有两个参数:
- 第一个是序列生成开始的元素;
- 第二个是
UnaryOperator
,表示从第一个元素生成新元素的原理。
UnaryOperator
,但在方法中iterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
当我们运行它时,我们会在控制台上得到以下输出:
9
81
6561
43046721
也就是说,我们的每个元素都与其自身相乘,对于前四个数字依此类推。就这样!如果读完这篇文章后,您距离理解和掌握 Java 中的 Stream API 又近了一步,那就太好了!
GO TO FULL VERSION