Java进阶之函数式编程
Java8开始,支持函数式编程,什么是函数式编程?
函数式编程(Functional Programming, FP)是一种编程范式,它强调使用函数来处理数据的计算。这个名字来源于这种范式对函数的重视,其中“函数”指的是数学意义上的函数,即一种关系,在这种关系中,每个输入值都对应一个唯一的输出值,而且同样的输入总是会产生同样的输出。即函数的确定性。意思就是函数的每个输入值(自变量)映射到唯一的输出值(因变量)。类似于接口的幂等性,不过幂等性指的是同样的数据经过一段操作过程,执行的结果不会发生改变,强调的是过程,而函数的确定性指的是一个数学的概念。
一、什么是Lambda表达式?
Lambda表达式是一种简洁、可传递的匿名函数,它可以作为参数传递给方法或存储在变量中。
目的是解决面向对象开发中繁琐的代码处理。用简洁、可传递的匿名函数语法而简化代码开发。
Lambda表达式的基本语法如下:
接口名 变量名 = Lambda表达式
无参数的: ()->{}
有参数的:(parameters) -> expression 或 (parameters)->{statements; }
parameters:Lambda表达式的参数列表,可以省略类型声明,如果只有一个参数且类型可以推导,则圆括号也可以省略。
expression:如果Lambda表达式只包含一个表达式,则可以直接返回该表达式的值。
statements:如果Lambda表达式包含多个语句,则需要用花括号括起来,并显式包含 return 语句(如果需要返回值)。
Lambda表达式的几种示例:
示例 1:无参数的 Lambda表达式
Runnable run = () -> System.out.println("Hello, world!");
new Thread(run).start();
示例 2:带一个参数的 Lambda表达式
ActionListener listener = event -> System.out.println("Button clicked!");
button.addActionListener(listener);
示例 3:带多个参数的 Lambda表达式
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(1, 2)); // 输出 3
示例 4:带类型声明的 Lambda表达式
BinaryOperator<Integer> multiply = (Integer a, Integer b) -> a * b;
System.out.println(multiply.apply(2, 3)); // 输出 6
示例 5:带多个语句的 Lambda表达式
BinaryOperator<Integer> subtract = (a, b) -> {
int result = a - b;
return result;
};
System.out.println(subtract.apply(5, 3)); // 输出 2
Lambda表达式可以用于任何需要函数对象的地方,例如,在实现接口的方法时,或者在创建线程时。
// 使用 Lambda表达式实现 Runnable
Runnable run = () -> System.out.println("Hello, world!");
new Thread(run).start();
二、函数式接口
什么是函数式接口?
函数接口(Functional Interface)是一个只包含一个抽象方法的接口。这样的接口可以用来表示 Lambda 表达式,方法引用或者构造器引用。
Java8引入了函数接口的概念,以支持Lambda表达式和流(Stream)等新特性。
函数接口的定义
一个接口如果满足以下条件之一,那么它就是一个函数接口:
仅包含一个抽象方法。
包含多个默认方法(default methods),但只有一个抽象方法。
包含静态方法(static methods),但只有一个抽象方法。
包含继承自 Object 类的方法(如 toString()、equals() 等),但只有一个抽象方法。
Java8增加的接口default和static方法也是为了函数式编程而准备。
函数接口的标记
一般我们在接口名上方会增加@FunctionalInterface注解来标识这是一个函数式接口。当然不是必须的,Java编译器会自动识别一个接口是否是函数接口。
示例
以下是一个简单的函数接口示例:
@FunctionalInterface
interface MyFunctionalInterface {
void doSomething();
}
这个接口只有一个抽象方法 doSomething(),因此它是一个函数接口。
传统写法实现:
class MyFunctionalInterfaceImpl implements MyFunctionalInterface {
@Override
public void doSomething() {
System.out.println("Doing something!");
}
}
public class Main {
public static void main(String[] args) {
MyFunctionalInterface myFunctionalInterface = new MyFunctionalInterfaceImpl();
myFunctionalInterface.doSomething(); // 输出 "Doing something!"
}
}
使用Lambda表达式来实现这个接口:
public class Main {
public static void main(String[] args) {
MyFunctionalInterface myFunctionalInterface = () -> System.out.println("Doing something!");
myFunctionalInterface.doSomething(); // 输出 "Doing something!"
}
}
由此可见,Lambda表达式确实大大减少了面向对象式编程代码开发的繁琐程度,使代码更加简洁、易读,并且提高了开发效率。
Java中内置的函数式接口有哪些?
Java8在java.util.function包中提供了一些内置的函数接口,如:
Predicate<T>:接受一个参数并返回一个布尔值。断言型
Function<T, R>:接受一个参数并返回一个结果。功能型
Consumer<T>:接受一个参数但不返回任何结果。消费型
Supplier<T>:不接受参数,但返回一个结果。供给型
T:表示函数的输入类型。这个参数类型是泛型,意味着你可以将Function<T, R>接口应用于任何类型的输入数据。
R:表示函数的输出类型。这个返回类型也是泛型,意味着函数可以返回任何类型的结果。
这些内置的函数接口可以用于各种场景,使得 Lambda 表达式和流操作更加方便和灵活。
比如使用 Function<T, R> 接口:
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<Integer, String> converter = (num) -> "The number is: " + num;
String result = converter.apply(42); // 输出 "The number is: 42"
System.out.println(result);
}
}
不用自己去定义函数式接口了,直接使用各种内置的就可以。此时才想起当初看李老师写线程池那里的不同参数用不同接口的这个写法,才懂啊。
通过与其他Java 8特性(如Stream API、方法引用等)结合使用,Lambda表达式可以发挥更大的作用。
三、Stream API
Lambda表达式与Stream API使用
Stream API可以让人以声明式方式处理数据集合。
通过Stream API可以用更简洁的代码对数据执行复杂的查询操作。
在java.util.stream包下有stream的一系列类文件。
集合的根接口Collection里的stream()可以创建创建一个顺序流来对数据一次遍历,遍历过程中你可以对数据进行过滤、排序、聚合等操作。
Stream API 的常用方法
中间操作,因为这些方法返回的都是流本身,所以可以串联调用。
filter - 通过特定的条件过滤流中的元素。
map - 将流中的每个元素转换成其他形式。
flatMap - 将流中的每个元素转换成流,然后将这些流扁平化成一个流。
sorted - 对流中的元素进行排序。
distinct - 去除流中重复的元素。
limit - 限制流中的元素数量。
skip - 跳过流中的前几个元素。
终端操作
forEach - 遍历流中的每个元素。
collect - 将流中的元素收集到集合中。
toArray - 将流中的元素转换成数组。
reduce - 通过一个起始值,反复利用BinaryOperator来处理和累积流中的元素。
min - 找出流中的最小值。
max - 找出流中的最大值。
count - 计算流中元素的数量。
anyMatch - 检查是否至少有一个元素匹配给定的谓词。
allMatch - 检查是否所有元素都匹配给定的谓词。
noneMatch - 检查是否没有元素匹配给定的谓词。
findFirst - 返回流中的第一个元素。
findAny - 返回流中的任意一个元素。
示例
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamApiExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Arya", "Sansa", "Robb", "Sansa");
// 使用 Stream API 过滤、转换、排序并收集结果
List<String> filteredAndSortedNames = names.stream()
.filter(name -> name.startsWith("S")) // 过滤以"S"开头的名字
.map(String::toUpperCase) // 将名字转换为大写
.sorted() // 对结果进行排序
.distinct() // 去除重复元素
.collect(Collectors.toList()); // Collectors可以用来收集结果,还有一些其他常用的 Collectors,比如 Collectors.toSet()、Collectors.toMap() 等
System.out.println(filteredAndSortedNames); // 输出:[SANS, ROBB]
// 使用 Stream API 计算列表中元素的数量
long count = names.stream().count();
System.out.println(count); // 输出:5
// 使用 Stream API 检查是否有元素以"A"开头
boolean anyMatch = names.stream().anyMatch(name -> name.startsWith("A"));
System.out.println(anyMatch); // 输出:true
}
}
上例中创建了一个包含名字的列表 names。然后使用stream()方法创建了一个流。使用 filter()、map()、sorted()、distinct()和collect()方法来过滤、转换、排序、去除重复元素,并将结果收集到一个新的列表中。
最后使用 count() 和 anyMatch() 方法来进行计数和匹配检查。
Stream API与Lambda表达式结合使用,可以让你轻松地实现复杂的集合数据处理操作。
在处理大量数据时,Java中还提供了并行流list.parallelStream()。
并行流是指将一个流的数据分成多个数据块,并使用多个线程并行处理这些数据块的流。在处理大数据集时,使用并行流可以显著提高程序的运行效率。
并行流和 Collectors 的结合使用可以让你更高效地处理数据。但在使用并行流时,需要注意线程安全问题,确保并行操作不会导致数据不一致。
你可以通过调用流的parallel()方法,将一个普通流转换为并行流。
对于基本数据类型,尽可能地使用IntStream、LongStream 和 DoubleStream这些特定类型的流,以避免不必要的装箱成本。
此外还有很多用于特定类型的数据处理的流:
Optional流:Optional类似于一个可能包含也可能不包含非空值的容器对象。你可以通过Optional的stream()方法从Optional对象中创建一个流,但如果Optional是空的,这个流也将是空的。
Iterate流:Iterate是一个用于生成无限顺序无序流的方法,它从一个初始元素开始,并应用一个函数来生成序列中的下一个元素。
Generate流:Generate是一个用于生成无限连续流的方法,每次生成一个由提供的Supplier函数生成的元素。
Primitivespecializedstreams:除了IntStream、LongStream和DoubleStream,Java8还提供了BigIntegerStream和BigDecimalStream,这些流用于处理大数字和精确的小数点计算。
File流:Files类提供了一些方法来创建代表文件内容的流,例如Files.lines()方法可以创建一个包含文件所有行的Stream<String>。
Array流:Arrays类也提供了一些方法来创建流,例如Arrays.stream(Object[])可以创建一个对象流,而Arrays.stream(int[])、Arrays.stream(long[])和Arrays.stream(double[])分别可以创建基本类型int、long和double的流。
Buffered流:在Java的I/O包中,BufferedInputStream、BufferedOutputStream、BufferedReader和BufferedWriter等类提供了缓冲功能,以提高I/O操作的效率。
四、方法引用
方法引用是Lambda表达式的一种特殊的更加简化的语法形式。
当Lambda表达式的实现只是调用一个已经存在的方法时,可以使用方法引用来简化代码。
方法引用可以看作是一种“短路的Lambda表达式”,它可以直接引用方法,而不是编写方法的实现。
方法引用的四种形式
静态方法引用: 使用ClassName::staticMethodName来引用一个静态方法。
例如,Integer::parseInt可以引用Integer类的parseInt静态方法。
// 使用静态方法引用将字符串转换为整数
Function<String, Integer> stringToInteger = Integer::parseInt;
Integer number = stringToInteger.apply("123");
System.out.println(number); // 输出:123
实例方法引用: 使用instanceReference::instanceMethodName来引用一个实例方法。
例如,如果有一个实例变量list,那么list::add可以引用List接口的add方法。
// 使用实例方法引用添加元素到列表
List<String> list = new ArrayList<>();
list.add("Item 1");
list.add("Item 2");
// 将实例方法引用赋值给一个函数接口
java.util.function.Consumer<String> listAdd = list::add;
listAdd.accept("Item 3"); // 添加 "Item 3" 到列表
System.out.println(list); // 输出:[Item 1, Item 2, Item 3]
类型方法引用: 使用ClassName::instanceMethodName来引用一个类型的方法。这种形式通常用于引用对象的实例方法,
例如,String::length可以引用String类的length方法
// 使用类型方法引用获取字符串的长度
Function<String, Integer> stringLength = String::length;
Integer length = stringLength.apply("Hello");
System.out.println(length); // 输出:5
构造器引用: 使用ClassName::new来引用一个类的构造器。例如,ArrayList::new可以引用ArrayList类的无参构造器。
// 使用构造器引用创建ArrayList实例
Supplier<ArrayList<String>> arrayListSupplier = ArrayList::new;
ArrayList<String> list = arrayListSupplier.get();
list.add("Item 1");
System.out.println(list); // 输出:[Item 1]
所以,方法引用就是一种简化的Lambda表达式的方式,可以直接引用已有方法。
Java 函数式编程除了Lambda表达式、Stream API、函数接口、方法引用、并行流还有
Optional类:用于减少空指针异常的风险,表示一个可能存在也可能不存在的值。
CompletableFuture类:用于异步编程,可以代表一个异步计算的结果。
不研究了。
END