简介
什么特性
面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。
现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式我们都得学。
-
编写出更容易阅读的代码。
-
处理批量数据的并行类库。
-
在写回调函数和事件处理程序时,程序员不必再纠缠于匿名内部类的冗繁和可读性。
-
Java 8 还让集合类可以拥有一些额外的方法: default 方法。
什么是函数式编程
每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函
数,函数对一个值进行处理,映射成另一个值。
Lambda 表达式
一种紧凑的、传递行为的方式
lambda表达式本质上是一段匿名内部类,也可以是一段可以传递的代码
我们可以通过Lambda表达式实现接口的抽象方法,比之前简直简洁太多。之前的方法是写一个实现该接口的类,并写出实现体。具体的不写了,我们看看通过Lambda表达式的代码有多简洁。
Lambda表达式与匿名类对比
设计匿名内部类的目的,就是为了方便 Java 程序员将代码作为数据传递。不过,匿名内部
类还是不够简便。为了调用一行重要的逻辑代码,不得不加上 4 行冗繁的样板代码。若把
样板代码用其他颜色区分开来,就可一目了然:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("button clicked");
}
});
在 Java 8 中,上述代码可以写成一个Lambda 表达式
button.addActionListener(event -> System.out.println("button clicked"));
和传入一个实现某接口的对象不同,我们传入了一段代码块——一个没有名字的函数。
event 是参数名,和上面匿名内部类示例中的是同一个参数。 -> 将参数和 Lambda 表达式
的主体分开,而主体是用户点击按钮时会运行的一些代码。
和使用匿名内部类的另一处不同在于声明 event 参数的方式。使用匿名内部类时需要显式
地声明参数类型 ActionEvent event ,而在 Lambda 表达式中无需指定类型,程序依然可以
编译。这是因为 javac 根据程序的上下文( addActionListener 方法的签名)在后台推断出
了参数 event 的类型。这意味着如果参数类型不言而明,则无需显式指定。
注意:
尽管与之前相比,Lambda 表达式中的参数需要的样板代码很少,但是 Java 8
仍然是一种静态类型语言。为了增加可读性并迁就我们的习惯,声明参数时
也可以包括类型信息,而且有时编译器不一定能根据上下文推断出参数的
类型!
Lambda表达式基本形式
lambda 表达式的语法格式如下:
(parameters) -> expression
或
(parameters) ->{ statements; }
以下是lambda表达式的重要特征:
- **可选类型声明:**不需要声明参数类型,编译器可以统一识别参数值。
- **可选的参数圆括号:**一个参数无需定义圆括号,但多个参数需要定义圆括号。
- **可选的大括号:**如果主体包含了一个语句,就不需要使用大括号。
- **可选的返回关键字:**如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
口诀:左右遇一省括号,左侧推断类型省
注:当一个接口中存在多个抽象方法时,如果使用lambda表达式,并不能智能匹配对应的抽象方法,因此引入了函数式接口的概念
变量作用域
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
int num = 1;
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
String first = "";
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length()); //编译会出错
既成事实上的 final 是指只能给该变量赋值一次。换句话说,Lambda 表达式引用的是值,
而不是变量。在例 2-6 中, name 就是一个既成事实上的 final 变量。
String name = getUserName();
button.addActionListener(event -> System.out.println("hi " + name));
若匿名类的方法中使用外部变量,该变量必须是final。Java8中提供的lambda表达式虽然可以引用非final变量,但该变量在既成事实上必须是final,即必须是终态变量,否则,编译器会报错。Lambda表达式引用的是值,而不是变量。这也解释了为什么Lambda表达式被称为闭包。
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));
函数接口
Lambda表达式本身的类型为函数接口。使用只有一个方法的接口来表示某特定方法并反复使用。
函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型。接口中唯一方法的命名并不重要,只要方法签名和Lambda表达式的类型匹配即可,参数命名可以更有意义。
Lambda表达式和匿名内部类在JVM上的区别:
匿名内部类是一个不需要显示指定类名的类,编译器会为该类取名,匿名类生成.class文件。
Lambda表达式不会产生新的类,被封装成主类的一个私有方法,并通过invokedynamic指令进行调用。
为了引入Lambda表达式,java8新增了java.util.function包来包含常用的函数接口。
类型推断
Java 7 中程序员可省略构造函数的泛型类型,Java 8 更进一步,程序员可省略 Lambda 表达
式中的所有参数类型。
Javac只是根据Lambda表达式上下文来推断参数的正确类型,程序依然要经过类型检查来保证运行的安全性,只是不显式声明类型。
Predicate是用来判断真假的函数接口。
public interface Predicate<T>{
boolean test(T t);
}
下面是使用Predicate的例子。
Predicate<Integer> atLeast5 = x -> x>5;
x>5是表达式的主体,返回值就是表达式主体的值。
Lambda表达式是一个匿名方法,将行为像数据一样传递。
函数式接口
函数式接口的提出是为了给Lambda表达式的使用提供更好的支持。
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
如定义了一个函数式接口如下:
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
}
那么就可以使用Lambda表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):
GreetingService greetService1 = message -> System.out.println("Hello " + message);
函数式接口可以对现有的函数友好地支持 lambda。
java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有:
Predicate接口
输入一个参数,并返回一个Boolean值,其中内置许多用于逻辑判断的默认方法:
@Test
public void predicateTest() {
Predicate<Integer> predicate = (s) -> s > 0;
boolean test = predicate.test(2);
System.out.println("该数字大于0:" + test);
test = predicate.test(0);
System.out.println("该数字大于0:" + test);
test = predicate.negate().test(0);
System.out.println("该数字小于等于0:" + test);
}
Function接口
接收一个参数,返回单一的结果,默认的方法(andThen)可将多个函数串在一起,形成复合Funtion(有输入,有输出)结果:
@Test
public void functionTest() {
Function<Integer, Integer> add = (i) -> {
System.out.println("初始值:" + i);
return i+1;
};
Function<Integer, Integer> power = add.andThen((i) -> {
System.out.println("第一次运算:" + i);
return i * i;
});
Integer res = power.apply(2);
System.out.println("第二次运算:" + res);
}
Supplier接口
返回一个给定类型的结果,与Function不同的是,Supplier不需要接受参数(供应者,有输出无输入):
@Test
public void supplierTest() {
Supplier<String> supplier = () -> "有输出,无输入。";
String s = supplier.get();
System.out.println(s);
}
Consumer接口
代表了在单一的输入参数上需要进行的操作。和Function不同的是,Consumer没有返回值(消费者,有输入,无输出):
@Test
public void consumerTest() {
Consumer<Integer> add = (p) -> {
System.out.println("old value:" + p);
p = p + 1;
System.out.println("new value:" + p);
};
add.accept(1);
}
以上四个接口的用法代表了java.util.function包中四种类型,理解这四个函数式接口之后,其他的接口也就容易理解了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库