什么是函数
在数学中,函数是这样定义的:它是给定一个数集A,假设其中的元素为x,对A中的元素x施加对应法则f,记作f(x),得到另一数集B,假设B中的元素为y,则y与x之间的等量关系可以用y=f(x)表示。
Java中的函数与咱们学的数学中的函数有异曲同工之妙。函数,经常被我们叫做方法,它是执行特定任务或操作的代码块
。
函数是Java类的一部分,用于执行特定的任务。它们通常有一个返回类型和一个或多个参数
。
比如我们使用两个整数参数返回它们的和:
public class xiaowei { public int add(int a, int b) { return a + b; } }
函数对象
函数对象是通过Lambda表达式
创建的,它表示一个匿名函数。Lambda表达式可以用于实现函数接口,所以可以作为参数传递给其他方法或赋值给变量。
比如我们现在有几种水果组成的列表,把它们的英文名称转化为大写,代码就可以这样写:
List<String> list = Arrays.asList("Apple", "Banana", "Cherry"); list.forEach(s -> System.out.println(s.toUpperCase()));
s -> System.out.println(s.toUpperCase())就是一个Lambda表达式,它表示一个匿名函数,接受一个字符串参数s,然后将它转换为大写后打印出来。这个Lambda表达式实现了Consumer函数接口。接下来我们讲一下什么是函数接口!
函数接口
函数接口是只有一个抽象方法的接口。Java 8版本中引入的许多内置函数接口(这个见下面详细的表格)。Lambda表达式可以被用来实现这些接口,所以才允许我们将函数作为参数传递。
常见的Java 8内置函数接口及其用途:
接口名称 | 描述 | 示例 |
---|---|---|
Function<T, R> | 将一个输入参数T 转换为结果R | Function<Integer, String> toStringFunction = i -> String.valueOf(i); |
Predicate<T> | 对输入参数T 进行断言,返回结果是布尔值 | Predicate<String> isEmptyPredicate = s -> s.isEmpty(); |
Consumer<T> | 消费输入参数T ,不返回结果(void) | Consumer<String> printConsumer = s -> System.out.println(s); |
Supplier<T> | 无需输入参数,提供(生成)一个结果T ,和上面那行相反 | Supplier<String> defaultSupplier = () -> "Default"; |
UnaryOperator<T> | 对一个输入参数T 进行一元操作,返回同类型结果 | UnaryOperator<Integer> incrementOperator = i -> i + 1; |
BinaryOperator<T> | 对两个同类型输入参数T 进行二元操作,返回同类型结果 | BinaryOperator<Integer> additionOperator = (a, b) -> a + b; |
BiFunction<T, U, R> | 接受两个输入参数T 和U ,返回一个结果R | BiFunction<Integer, Integer, Integer> multiplicationFunction = (a, b) -> a * b; |
这些函数接口都只有一个抽象方法,与Lambda表达式结合使用,可以实现各种功能。
比如,我们可以使用Function
接口将一个列表中的每个元素转换为另一种形式,使用Predicate
接口过滤列表中的元素,使用Consumer
接口对列表中的每个元素执行某种操作,等等。
举个栗子:
我们创建一个包含整数的列表。然后,我们使用Function,Predicate,Consumer接口等接口来演示如何使用它们。代码来咯:
import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; public class XiaoWeiDemo1 { public static void main(String[] args) { // 创建一个整数列表 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 使用Function接口将整数转换为字符串 Function<Integer, String> toStringFunction = number -> String.valueOf(number); List<String> numberStrings = numbers.stream() .map(toStringFunction) .collect(Collectors.toList()); System.out.println("Number strings: " + numberStrings); // 使用Predicate接口过滤出偶数 Predicate<Integer> isEvenPredicate = number -> number % 2 == 0; List<Integer> evenNumbers = numbers.stream() .filter(isEvenPredicate) .collect(Collectors.toList()); System.out.println("Even numbers: " + evenNumbers); // 使用Consumer接口打印每个偶数 Consumer<Integer> printConsumer = number -> System.out.println("Even number: " + number); evenNumbers.forEach(printConsumer); } }
这就是Java 8函数接口的强大之处,它们可以让代码更加模块化、可重用和易于测试。我们只需要灵活地传递代码块作为参数,就可以需要的地方执行它们。
我们这个栗子里面用到了Stream流的几个API,比如过滤(filter),收集(collect)这些,这些API会在后续的文章里再和大家一起详细学习。
方法引用
方法引用也是Java 8中引入的一个特性,它允许我们直接引用已经存在的方法,而不是在Lambda表达式中重新编写方法体。方法引用主要有四种类型,每种类型都有其特定的使用方式:
静态方法引用
:当引用的方法是静态方法时,我们可以使用类名来进行引用。例如,对于Math类中的max方法,我们可以使用Math::max来进行引用。这种方式适用于不需要访问对象的实例成员的情况。
实例方法引用
:当引用的方法是实例方法时,我们可以使用对象名来进行引用。例如,假设我们有一个String对象str,并且想要引用它的length方法,那么可以使用str::length。这种方式允许我们直接引用特定对象的实例方法。
构造方法引用
:当引用的方法是构造方法时,我们可以使用类名来引用。例如,要引用ArrayList类的构造方法,我们可以使用ArrayList::new。这种方式在需要创建对象实例时特别有用,尤其是在结合Stream API进行集合操作时。
数组构造方法引用
:当引用的方法是数组构造方法时,我们可以使用数组类型来引用。例如,要引用int数组的构造方法,我们可以使用int[]::new。这种方式在需要动态创建数组时非常有用。
计算两个数之和案例
我们定义一个函数接口来计算两个数的和,然后使用Lambda表达式、方法引用等方式来实现它。
首先,我们定义一个函数接口BinaryOperator,它接受两个参数并返回一个结果。(见上文表格详情)这个接口类似于Java 8中的java.util.function.BinaryOperator
接口,但为了演示如何使用,我们重新实现一个:
@FunctionalInterface public interface BinaryOperator<T> { T apply(T a, T b); }
这里需要注意一下@FunctionalInterface注解:
@FunctionalInterface 是 Java 8 引入的一个注解,用来指示一个接口是函数式接口。函数式接口是指只包含一个抽象方法的接口(但是可以包含多个默认方法或者静态方法,但但是只能有一个抽象方法)。
接下来,我们创建一个类Calculator,它包含使用BinaryOperator接口的方法:
public class Calculator { // 使用函数接口作为参数的方法 public static <T> T calculate(T a, T b, BinaryOperator<T> operator) { return operator.apply(a, b); } // 一个静态方法,用于计算两个数的和,可以作为方法引用 public static int add(int a, int b) { return a + b; } public static void main(String[] args) { // 使用Lambda表达式实现函数接口 BinaryOperator<Integer> sumLambda = (a, b) -> a + b; int sumUsingLambda = calculate(5, 3, sumLambda); System.out.println("Sum using Lambda: " + sumUsingLambda); // 使用方法引用实现函数接口 BinaryOperator<Integer> sumMethodRef = Calculator::add; int sumUsingMethodRef = calculate(5, 3, sumMethodRef); System.out.println("Sum using Method Reference: " + sumUsingMethodRef); // 直接使用函数对象(在这种情况下是静态方法) int sumUsingStaticMethod = Calculator.add(5, 3); System.out.println("Sum using Static Method: " + sumUsingStaticMethod); } }
在main方法中,我们使用三种函数接口的方式:
第一种:使用Lambda表达式:我们创建了一个Lambda表达式(a, b) -> a + b,它实现了BinaryOperator接口,所有我们可以直接作为参数传递给calculate方法。
第二种:使用方法引用:我们通过Calculator::add引用了Calculator类中的静态方法add,这个方法同样实现了BinaryOperator接口的功能。方法引用是Lambda表达式的一种简写形式,当Lambda表达式的体只是调用一个已存在的方法时,可以使用方法引用。
第三种:直接使用函数对象:在这种情况下,我们直接调用了静态方法Calculator.add(5, 3),这不是通过函数接口调用的,但它展示了如何直接调用实现特定功能的函数对象(在这个例子中是静态方法)。
运行这段代码,会输出:
Sum using Lambda: 8
Sum using Method Reference: 8
Sum using Static Method: 8
方法引用与Lambda表达式转换
虽然Lambda表达式和方法引用在语法上有所不同,但在某些情况下,它们可以相互转换。这主要取决于函数式接口的抽象方法的签名和你想要引用的方法的签名是否匹配。
从Lambda表达式到方法引用:如果我们的Lambda表达式仅仅是调用了一个已存在的方法,并且这个方法的签名与函数式接口的抽象方法的签名完全匹配,那么就可以将这个Lambda表达式转换为一个方法引用。
例如,下面Lambda表达式:
Function<String, String> toUpperCaseLambda = s -> s.toUpperCase();
可以转换为下面的方法引用:
Function<String, String> toUpperCaseMethodRef = String::toUpperCase;
从方法引用到Lambda表达式:反过来,如果我们已经有一个方法引用,并且想要更明确地表达正在做什么,或者需要添加一些额外的逻辑,我们就可以将它转换为一个Lambda表达式。
比如,方法引用为:
Function<String, String> toUpperCaseMethodRef = String::toUpperCase;
可以转换为下面的Lambda表达式:
Function<String, String> toUpperCaseLambda = s -> { String upperCaseString = s.toUpperCase(); // 我们可以在这里添加额外的逻辑,如日志记录、错误处理等。 return upperCaseString; };
文章到这里就先结束了,后续会继续分享相关的知识点。