Java笔记18 - 函数式编程
函数式编程
-
一个大型程序调用若干底层函数, 这些函数又可以调用其他函数
-
大任务被一层层拆解并执行
-
函数是面向过程的程序设计的基本单元
-
Java不支持单独定义函数, 静态方法视为独立的函数
-
函数式编程归结为面向过程的程序设计
-
计算机: CPU执行计算代码, 条件判断还有调整等指令代码, 汇编是最贴近计算机的语言
-
计算: 数学意义上的计算, 越是抽象的计算, 离计算机硬件越近
-
编程语言约低级, 越解决计算机, 抽象程度低, 执行效率高. C语言
-
编程语言越高级, 越贴近计算, 抽象程度高, 执行效率低. Python
-
函数式编程是抽象程度很高的编程范式, 纯粹的函数式编程语言编写的函数没有任何的变量.
-
因此, 任一函数, 只要输入确定, 输出就是一定确定的. 称为没有副作用
-
允许函数本身作为参数传入另一个函数, 还可以返回一个函数
lambda基础
- java的实例方法和静态方法, 本质上都相当于过程式语言的函数. 例如C函数:
char* strcpy(char* dest, char* src)
- 只不过java的实例方法隐含地传入了一个
this
变量 - 函数式编程把函数作为基本运算单位, 可以接受函数, 可以返回函数.
- 支持函数式编程的编码风格称为
Lambda
表达式
Lambda表达式
- 参数类型和返回值类型都是由编译器自动推断
FunctionalInterface
- 单方法接口称之为
FunctionalInterface
, 用注解@FunctionalInterface
标记
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
@FunctionalInterface
public interface Compare<T> {
int compare(T o1, T o2); // 唯一一个抽象方法, 其他都是`default`和`static`方法
boolean equals(Object obj); // 这是`Object`定义的方法
default Comparator<T> reversed() {
return Collections.reverseOrder();
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
// ...
}
// ...
}
方法引入
除了Lambda表达式, 可以直接传入方法使用
public static void main(String[] args) {
String[] array = new String[] {"Apple", "Orange", "Banana", "Lemon"};
Arrays.sort(array, Main::cmp); // 直接传入静态方法
/**
* String compareTo(String o), 在实际方法调用的时候为
* `public static int compareTo(this, String o)`
*/
Arrays.sort(array, String::compareTo); // 直接传入静态方法
System.out.println(String.join(", ", array));
}
static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
- 方法引用: 某个方法签名和接口恰好一致, 就可以直接传入方法引用
- 签名一致: 方法参数一致, 返回类型相同, 两个方法签名一致
构造方法引用
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
// List<Person> persons = new ArrayList<>();
// for (String name: names) {
// persons.add(new Person(name));
// }
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
System.out.println(persons);
}
static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person" + this.name;
}
}
- 构造方法的引用写法:
类名::new
- 构造方法隐式的返回
this
总结
FunctionalInterface
允许传入- 接口实现类
- Lambda表达式(只需要列出参数名, 其他由编译器推断类型)
- 符合方法签名的静态实例
- 符合方法签名的实例方法 (实例类型被看作第一个参数类型)
- 符合方法签名的构造方法 (实例方法被看作返回类型)
FunctionalInterface
不强制继承关系, 不需要方法名称相同, 只要求方法参数(类型和数量)与方法返回类型相同, 即认为方法签名相同
使用Stream
-
这个
Stream
代表任意Java对象序列 -
区别: java.io / java.util.stream
- 存储: 顺序读写的
byte
或char
/ 顺序输出的任意Java对象实例 - 用途: 序列化至文件或网络 / 内存计算或者业务逻辑
- 存储: 顺序读写的
-
List
用来操作一组已存在的Java对象,Stream
输出的元素可能没有预先存储再内存中, 而是实时计算出来的 -
区别: java.util.List / java.util.stream
- 元素: 已分配并存储在内存中 / 可能未分配, 实时计算
- 用途: 操作一组存在的Java对象 / 惰性计算
-
总结:
- 可以"存储"有限或者无限个元素, 可能全部存储在内存, 也有根据需要实时计算出来的
- 一个
Stream
可以轻易转化为另一个Stream
, 不会修改原Stream
本身
-
惰性计算的特定: 一个
Stream
转另一个Stream
时, 只存储了转换规则, 没有任何计算 -
真正的计算通常发生在最后结果的获取
创建Stream
Stream.of()
创建Stream
最简单的方法, 直接使用Stream.of()
, 传入可变参数即创建一个能确定输出确定元素的Stream
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()相当于内部循环调用
// 可以传入符合Consumer接口的 void accept(T t)方法引用
stream.forEach(System.out::println);
基于数组或Collection
Stream<String> stream1 = Arrays.stream(new String[] {"A", "B", "C"});
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
-
数组使用
Arrays.stream()
方法 -
Collection
直接使用stream()
方法即可 -
上述方法都是现有序列变成stream, 元素是固定的
基于Supplier
- 使用
Stream.generate()
方法, 传入一个Supplier
对象 Stream
会不断调用Supplier.get()
方法来不断产生下一个元素Stream
保存的是算法, 可以表示无限队列
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
// 注意: 无限序列必须先编程有限序列再打印
natual.limit(20).forEach(System.out::println);
}
}
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
- 无限循环直接调用
forEach()
或者count()
求值, 会进入死循环
其他方法
创建Stream
的第三种方法是通过一些API接口, 直接获得Stream
// 文件读取
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
// 每个元素, 表示一行内容
}
// 正则
Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brow fox jumps over the lazy dog");
s.forEach(System.out::println);
基本类型
- 泛型不支持基本类型, 无法进行
Stream<int>
, 会编译错误. - 提供
IntStream
,LongStream
,DoubleStream
, 使用基本类型的Stream
- 使用方法和泛型
Stream
, 并无大碍. - 目的是: 为了避免使用
Stream<Integer>
的运行效率低
IntStream is = Arrays.stream(new int[] {1, 2, 4});
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
使用map
- 将一个
Stream
转换成另一个Stream
map
操作, 把一个Stream
的每一个元素一一对应到应用了目标函数的结果上- 参数接收一个
Function
对象, 并定义一个apply()
, 负责把一个T
类型转换成R
类型 - 对任何Java对象都有效
public class Main {
public static void main(String[] args) {
List.of("Apple ", " pear", "ORANGE", "BanaNa ")
.stream()
.map(String::trim) // 去除空格
.map(String::toLowerCase) // 变成小写
.forEach(System.out::println); // 打印
}
}
String[] array = new String[] { " 2019-12-31 ", "2020 - 01-09 ", "2020- 05 - 01 ", "2022 - 02 - 01",
" 2025-01 -01" };
// 请使用map把String[]转换为LocalDate并打印:
Arrays.stream(array)
.map(n -> n.replace(" ", ""))
.map(LocalDate::parse)
.forEach(System.out::println);
使用filter
Stream
的转换方法, 对每个元素一一测试, 过滤不满足条件的元素- 接受
Predicate
方法, 定义一个test()
方法, 判断元素是否符合条件 - 可以应用与任何Java对象
public class Main {
public static void main(String[] args) {
Stream.generate(new LocalDateSupplier())
.limit(31)
.filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
.forEach(System.out::println);
}
}
class LocalDateSupplier implements Supplier<LocalDate> {
LocalDate start = LocalDate.of(2020, 1, 1);
int n = -1;
@Override
public LocalDate get() {
n++;
return start.plusDays(n);
}
}
使用reduce
- reduce是一个聚合方法, 可以把Stream的所有元素按照函数聚合成一个结果
- 传入
BinaryOperator
接口, 定义了一个apply()
方法, 负责把上次累加的结果和本地的元素进行运算. 并返回累加的结果. - 如果去掉初始值, 并stream为空, 会出现返回
Optional
对象的情况, 所以我们需要进一步进行判断
public static void main(String[] args) {
// 按行读取配置文件:
List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
// 把k=v转换为Map[k]=v:
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
// 把所有聚合到一个Map
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
// 打印结果
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
}
输出集合
Stream
操作分成两种- 转换操作: 并不会触发任何计算
- 聚合操作: 真正的计算从这里开始, 因为要获取具体元素
public class Main {
public static void main (String[] args) {
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
Long s5 = s4.reduce((long) 0, (acc, n) -> acc + n);
System.out.println(s5);
}
}
class NatualSupplier implements Supplier<Long> {
long n = 0;
public Long get() {
System.out.println("调用get()");
n++;
return n;
}
}
-
s1 -> s3的过程不会有任何计算, 只保留计算过程
-
s4的聚合开始真正的获取元素, 一直从s1开始计算
-
不进行s5操作的话, 不会调用
get()
方法
输出为List
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
Stream
的每个元素收集到List
方法是调用collect()
方法, 并传入Collectors.toList()
对象.- 使用
collect(Collectors.toSet())
可以把Stream
每个元素收集到Set
中
输出为数组
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
String[] array = list.stream().toArray(String[]::new);
- 传入的"构造方法"是
String[]::new
输出为Map
- 需要指定两个映射函数, 分别把元素映射为key和value
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream.collect(Collectors.toMap(
s -> s.substring(0, s.indexOf(':')),
s -> s.substring(s.indexOf(':') + 1)
));
System.out.println(map);
分组输出
List<String> list = List.of("Apple", "Banana", "Blackberry", "Cocount");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
- 使用
groupingBy()
进行分组输出, 第一个参数, 表示key
, 第二个参数表示value
其他操作
排序
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
- 如果需要自定义排序, 传入
Comparator
即可
去重
List<String> list = List.of("A", "A", "b", "B")
.stream()
.distinct()
.collect(Collectors.toList());
截取
List list = List.of("a", "b", "c", "d", "e")
.stream()
.skip(2) // 跳过多少个元素
.limit(3) // 截取3个
.collect(Collectors.toList());
System.out.println(list);
合并
Stream<String> s1 = List.of("a", "b", "c").stream();
Stream<String> s2 = List.of("d", "c").stream();
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList()));
flatMap
flatMap
把Stream
中的每个元素, 都变成Stream
, 然后合并成一个Stream
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
Stream<Integer> i = s.flatMap(list -> list.stream());
System.out.println(i.collect(Collectors.toList()));
并行
Stream<String> s = List.of("a", "b", "c", "d").stream();
String[] r = s.parallel() // 此处变为并行处理stream, 提高处理效率
.sorted()
.toArray(String[]::new);
for (String i : r) {
System.out.println(i);
}
System.out.println(r);
其他聚合方法
- 其他的聚合方法:
count()
: 返回stream
中元素个数max(Comparator<? super T> cp)
: 找出最大元素
- 针对
IntStream
,LongStream
和DoubleStream
:sum()
: 求和操作average()
: 对所有元素求平均数
- 测试
Stream
的元素是否满足条件boolean allMatch(Predicate<? super T>)
: 测试所有元素是否满足测试条件boolean anyMatch(Predicate<? super T>
: 测试至少有一个元素满足条件
forEach()
循环处理每一个元素
s.forEach(System.out::println);
困惑
String[]::new
到底是个什么
- 相当于
size -> new String[size]