【总结】函数式编程仅关注:被操作的数据(方法的入参)、对数据进行的操作(方法体)。
函数式编程的优点:
- 代码简洁。
- 接近自然语言,易于理解。
- 易于实现并发编程。
1 Lambda表达式
Lambda表达式是JDK8的一个新特性,可以用来简化某些匿名内部类的开发,是函数式编程的一种形式。
【总结】应用场景是:将方法的入参定义为函数式接口,对应同一函数式接口,在不同的具体场景下,通过匿名内部类或Lambda表达式的方式实现该接口,根据具体的业务场景重写接口中的抽象方法。即同一个接口在不同的地方对应着不同的实现(重写同一抽象方法的不同版本的方法体)。
1.1 Lambda表达式的适用范围
当通过匿名内部类的方式创建某接口的实现类,且该接口为函数式接口(有且仅有一个抽象方法)需要被重写的情况下,就可以运用Lambda表达式来简化开发代码。
1.2 Lambda表达式的语法格式(示例)
1.2.1 示例1 - 实现Runnable创建线程(方法入参为接口)
Lambda表达式示例1,相关的代码示例如下:
- 非Lambda表达式-匿名内部类的Java代码:
// 非Lambda表达式,匿名内部类的常规写法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("非Lambda表达式的线程被创建并执行了。。。");
}
}).start();
- 对应地,Lambda表达式的Java代码:
// Lambda表达式,简化匿名内部类的开发代码
new Thread(() -> {
System.out.println("Lambda表达式的线程被创建并执行了。。。");
}).start();
1.2.2 示例2 - 模拟计算器(方法入参为接口)
【补充】IntBinaryOperator类是JDK中的一个函数式接口,该接口中有且仅有一个抽象方法
int applyAsInt(int left, int right);
,后续根据具体地应用场景创建具体类实现该接口重写该抽象方法,实现方法入参的两个数值的加减乘除或各种运算,返回运算结果。
Lambda表达式示例2,将函数式接口IntBinaryOperator作为方法入参,实现了一个灵活定义两数具体运算的计算器,相关的代码示例如下:
- 首先,定义灵活计算器的方法(只是示例 因此这里参与运算的数a、b硬编码了),将函数式接口作为方法入参,代码示例如下:
// 定义示例2(函数式接口多种不同的实现)的测试方法
private static int calculateNum(IntBinaryOperator operator){
int a = 25;
int b = 5;
return operator.applyAsInt(a, b);
}
- 其次,通过非Lambda表达式 - 匿名内部类的方式实现该函数式接口,重写其抽象方法根据不同的具体场景定义不同的两数相加、两数相减的具体运算逻辑,代码示例如下:
// 示例2:非Lambda表达式-匿名内部类的方式
int addResult = calculateNum(new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
});
System.out.println("实现函数式接口重写方法为两数相加的运算逻辑,运算结果为:"+ addResult);
int subtractResult = calculateNum(new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left - right;
}
});
System.out.println("实现函数式接口重写方法为两数相减的运算逻辑,运算结果为:"+ subtractResult);
- 最后,相应地,通过Lambda表达式的方式实现该函数式接口,重写其抽象方法根据不同的具体场景定义不同的两数相乘、两数相除的具体运算逻辑,代码示例如下:
// 示例2:Lambda表达式的方式
int multiplyResult = calculateNum((a, b) -> {
return a * b;
});
System.out.println("实现函数式接口重写方法为两数相乘的运算逻辑,运算结果为:"+ multiplyResult);
int divideResult = calculateNum((m, n) -> {
return m / n;
});
System.out.println("实现函数式接口重写方法为两数相除的运算逻辑,运算结果为:"+ divideResult);
1.2.3 示例3 - Function<T, R>函数式接口(方法入参为接口)
【补充】Function<T, R>接口是JDK中的一个常用的函数式接口,有且仅有一个抽象方法
R apply(T t);
,该抽象方法的入参类型为泛型T
,该抽象方法的返回值类型为泛型R
。
- 首先,定义Function<T, R>函数式接口的测试方法(只是示例 因此这里入参的值硬编码了),将函数式接口作为方法入参,代码示例如下:
// 定义示例3的测试方法 - Funtion<T, R>函数式接口,T为其抽象方法apply的入参类型、R为抽象方法apply的返回值类型
private static <R> R typeConvert(Function<String ,R> function){
String string = "123456";
R result = function.apply(string);
return result;
}
- 其次,通过非Lambda表达式 - 匿名内部类的方式实现该函数式接口,重写其抽象方法,代码示例如下:
// 示例3:非Lambda表达式-匿名内部类
final Integer convertResult = typeConvert(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
});
System.out.println(convertResult);
- 最后,相应地,通过Lambda表达式的方式实现该函数式接口,重写其抽象方法,代码示例如下:
// 示例3:Lambda表达式
final Integer convertRes = typeConvert((s) -> {
return Integer.valueOf(s);
});
System.out.println(convertRes);
1.3 IDEA中自动转换匿名内部类为Lambda表达式
首先,针对函数式接口编写好非Lambda表达式-匿名内部类形式的代码,在IDEA中,光标置于改代码块,按下快捷键"Alt + Enter",选中“Replace with lambda”
即可自动地转换为对应的最简化的Lambda表达式形式。如下图所示:
相应地,通过
“Replace lambda with anonymous class”
,IDEA也可以实现将Lambda表达式转换为匿名内部类的形式。
1.4 Lambda表达式省略规则(实现最简化开发)
-
实现函数式接口并重写抽象方法时,方法入参的类型可以忽略不写。
-
重写的抽象方法中,若方法体仅包含一句代码时,大括号
{}
、return
、该一句代码结尾的分号;
都可以省略不写。 -
重写的抽象方法仅包含一个入参时,Lambda表达式入参的一对小括号
()
可以省略不写。代码示例如下:
// 示例3:Lambda表达式-最简化开发
final Integer convertResult = typeConvert(s -> Integer.valueOf(s));
System.out.println(convertResult);
2 Stream流
Stream流作为JDK8的新特性,通过函数式编程的形式可以更高效地处理 集合、数组。
2.1 测试实体类及测试方法
- 首先,定义所需的实体类Author与Book,代码示例如下:
实体类代码中,借助“Lombok”依赖通过注解快速生成实体类的Getter、Setter、toString、equals、hashcode方法、全参构造方法、无参构造方法等。
@Data
@NoArgsConstructor
@AllArgsConstructor
// 通过Lombok注解快速重写该实体类的equals()和Hashcode()方法,用于集合元素的去重。
@EqualsAndHashCode
// Stream流练习-测试实体类1
public class Author {
private Long id;
private String name;
private Integer age;
// 作者简介
private String introduce;
// 作者的作品
private List<Book> books;
}
@Data
// 通过Lombok注解快速重写该实体类的equals()和Hashcode()方法,用于集合元素的去重。
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
// Stream流练习-测试实体类2
public class Book {
private Long id;
private String name;
// 作品的分类,若某一个作品同时属于多种分类,则该成员变量值为“分类名称1,分类名称2,分类名称3”
private String category;
// 作品的评分
private Integer score;
// 作品的简介
private String introduce;
}
- 其次,定义用于获取所有作者的测试方法getAuthor(),代码示例如下:
// Stream流练习-测试方法
private static List<Author> getAuthors(){
//数据初始化
Author author = new Author (1L,"蒙多",33,"一个从菜刀中明悟哲理的祖安人",null);
Author author2 = new Author (2L,"亚拉索" , 15, "狂风也追逐不上他的思考速度" ,null);
Author author3 = new Author (3L,"易" ,14,"是这个世界在限制他的思维" ,null);
Author author4 = new Author(3L, "易" ,14,"是这个世界在限制他的思维" ,null);
//作品集合
List <Book> books1 = new ArrayList<>();
List<Book> books2 = new ArrayList<>();
List<Book> books3 = new ArrayList<>();
books1.add(new Book (1L ,"刀的两侧是光明与黑暗" ,"哲学,爱情" ,88,"用一把刀划分了爱恨"));
books1.add(new Book(2L,"一个人 不能死在同一把刀下","个人成长,爱情" ,99,"讲述如何从失败中明悟真理"));
books2.add(new Book(3L, "那风吹不到的地方" ,"哲学",85 ,"带你用思维去领略世界的尽头"));
books2.add(new Book (3L, "那风吹不到的地方","哲学" ,85 , "带你用思维去领略世界的尽头"));
books2.add(new Book (4L, "吹或不吹" , "爱情,个人传记" ,56,"-个哲学家的恋爱观注定很难把他所在的时代理解"));
books3.add(new Book (5L, "你的剑就是我的剑" , "爱情" ,56 ,"无法想象一个武者能对他的伴侣这么的宽容"));
books3.add(new Book (6L, "风与剑" , "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢? "));
books3.add(new Book(6L,"风与剑","个人传记",100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢? "));
author.setBooks(books1) ;
author2.setBooks(books2);
author3. setBooks(books3);
author4. setBooks(books3);
List<Author> authorList = new ArrayList<>(Arrays.asList(author, author2, author3, author4));
return authorList;
}
2.2 Stream流的语法格式(示例)
2.2.1 示例1 - 去重+过滤+消费(控制台打印)
- 首先,通过匿名内部类的方式,结合Steam流处理集合,对集合进行去重、过滤、消费(控制台打印)的一系列操作,代码示例如下:
// 示例1-匿名内部类形式:调用getAuthors()方法获取所有作者对象,现需要控制台打印年龄在18岁以内的所有作者姓名,且去重。
getAuthors().stream() // 将List集合转换为Stream流,后续就可以进行流式编程操作集合元素。
.distinct() // 集合元素去重。
.filter(new Predicate<Author>() { // 依据指定条件进行集合元素过滤,匿名内部类实现函数式接口Predicate,重写其抽象方法test,方法体中自定义过滤条件,Stream流底层会将集合中的每一个元素依次地调用该test方法,仅保留test方法返回值为true的集合元素,其余元素被过滤。
@Override
public boolean test(Author author) {
return author.getAge() <= 18;
}
})
.forEach(new Consumer<Author>() { // 遍历集合元素进行指定操作,匿名内部类实现函数式接口Consumer,重写其抽象方法accept,方法体中自定义遍历每一个集合元素所进行的消费操作(这里的控制台打印每一个作者姓名)。
@Override
public void accept(Author author) {
System.out.println(author.getName());
}
});
- 其次,通过Lambda表达式的方式,结合Steam流处理集合,对集合进行去重、过滤、消费(控制台打印)的一系列操作,代码示例如下:
// 示例1-Lambda表达式形式:调用getAuthors()方法获取所有作者对象,现需要控制台打印年龄在18岁以内的所有作者姓名,且去重。
getAuthors().stream()
.distinct()
.filter(author -> author.getAge() <= 18)
.forEach(author -> System.out.println(author.getName()));
}
2.3 IDEA Debug - Stream流的调试支持
IDEA Debug - Stream流的调试支持,如下图所示:
2.4 流生成
2.4.1 单列集合 - 生成流
单列集合生成流,单列集合生成流的语法格式:[集合对象].stream()
。
生成对应的流对象之后,就可以通过该流对象调用Stream流的各种方法操作该单列集合的各个元素了。代码示例如下:
// 示例 - 单列集合生成流
List<Author> authorList = getAuthors();
Stream<Author> authorStream = authorList.stream();
2.4.2 双列集合 - 生成流
双列集合生成流,是两步走:
- 先将双列集合转换为单列集合:
[双列集合对象].entrySet();
。 - 单列集合(Set集合)生成流的语法格式:
[集合对象].stream();
。
双列集合 - 生成流:双列集合 —> 单列集合 —> Stream
双列集合生成流,并通过流的过滤 + 消费操作,将双列集合中所有的年龄大于18岁的个人信息打印到控制台,代码示例如下:
// 示例 - 双列集合生成流
Map<String, Integer> map = new HashMap<>();
map.put("zhangsan", 18);
map.put("lisi", 24);
map.put("wangwu", 30);
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
Stream<Map.Entry<String, Integer>> entryStream = entrySet.stream();
entryStream.filter(entry -> entry.getValue() > 18)
.forEach(entry -> System.out.println(entry.getKey() + "的年龄为:" + entry.getValue()));
2.4.3 数组 - 生成流
方式一 - 利用Arrays类
利用Arrays类,数组生成流的语法格式:Arrays.stream([数组名称]);
。
数组生成流,并通过流的过滤 + 消费操作,将数组中所有的奇数打印到控制台,代码示例如下:
// 示例 - 数组生成流(利用Arrays类)
int[] arr = {1, 2, 3, 4, 5, 6};
IntStream intStream = Arrays.stream(arr);
intStream.filter(a -> a % 2 == 1)
.forEach(value -> System.out.println(value));
方式二 - 利用Stream类
利用Stream类,数组生成流的语法格式:Stream.of([数组名称]);
。
数组生成流,并通过流的过滤 + 消费操作,将数组中所有的长度大于5的名字打印到控制台,代码示例如下:
// 示例 - 数组生成流(利用Stream类)
String[] names = {"zs", "zhangsan", "ls", "lisi", "ww", "wangwu"};
Stream<String> stringStream = Stream.of(names);
stringStream.filter(name -> name.length() > 5)
.forEach(name -> System.out.println(name));
2.5 流中间操作
2.5.1 filter
filter方法作为流的中间操作,它依据重写抽象方法自定义的过滤条件,对流中的每一个元素进行数据的过滤,返回true的元素才会被保留在原集合 / 数组中,其余元素被过滤。
filter方法的入参为一个函数式接口,通过匿名内部类或者Lambda表达式的形式实现该函数式接口,根据具体业务场景重写其抽象方法public boolean test([流中的每一个元素]) {}
,在重写的方法体中定义具体的过滤条件,返回true / false。
关于filter方法的代码示例,详见上述“生成流”部分的示例。
2.5.2 map
map方法作为流的中间操作,主要是用来对流中的每一个元素进行数据的计算或者数据类型的转换。
map方法的入参为一个函数式接口Function<T, R>
,通过匿名内部类或者Lambda表达式的形式实现该函数式接口,根据具体业务场景重写其抽象方法 {}
,在重写的方法体中定义具体的计算逻辑或者数据类型转换的逻辑,入参类型为Function<T, R>
自定义指定的第一个泛型(第一个泛型必须与流中的元素类型保持一致),返回值类型为自定义指定的第二个泛型。
利用流中间操作 map ,实现数据类型的转换,将所有作者的姓名打印到控制台,代码示例如下(匿名内部类形式、Lambda表达式形式):
【注意】不熟悉需要实现的函数式接口以及需要重写的抽象方法的情况下,可以先通过IDEA的提示功能编写匿名内部类的形式,然后再转换为Lambda表达式(详见上述“ IDEA中自动转换匿名内部类为Lambda表达式”)即可。
// 示例(匿名内部类形式) - 中间操作 map 实现数据类型的转换(Author ---> String)
getAuthors().stream()
.map(new Function<Author, String>() {
@Override
public String apply(Author author) {
return author.getName();
}
}).forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
// 示例(Lambda表达式形式) - 中间操作 map 实现数据类型的转换(Author ---> String)
getAuthors().stream()
.map(author -> author.getName())
.forEach(s -> System.out.println(s));
利用流中间操作 map ,实现数据的计算,将所有作者的姓名打印到控制台,代码示例如下(Lambda表达式形式):
// 示例 - 中间操作 map 实现数据计算(Author ---> Integer ---> Integer计算)
getAuthors().stream()
.map(author -> author.getAge()) // map 实现 数据类型的转换
.map(age -> age + 1) // map 实现 数据的计算
.forEach(age -> System.out.println(age));
2.5.3 distinct
distinct方法作为流的中间操作,用来对流中的各个元素进行数据的去重,重复的元素在集合 / 数组中仅保留一份。
【注意】流的中间操作distinct依赖于元素所在类的equals方法进行对象是否相等重复的判断,因此流的元素所对应的类必须重写了Object类的equals方法(某类的两个对象所有的成员变量值均相等才算相等);否则就走Object的原生equals方法,通过地址值来判断是否相等了。
关于distinct方法的代码示例,详见上述“示例1 - 去重+过滤+消费(控制台打印)”部分的示例。
2.5.4 sorted
sorted方法作为流的中间操作,用来对流中的各个元素进行数据的排序。
【补充】
Comparable<T>
接口是定义比较能力的接口。
若想要某个类拥有比较能力,则让该类实现Comparable<T>
接口,根据具体的比较逻辑重写接口中的抽象方法public int compareTo(Author o) {}
即可。
方式一:sorted([Comparator函数式接口])
代码示例如下:
// 示例(匿名内部类形式) - 中间操作 sorted([比较器接口])方法 实现元素排序
getAuthors().stream()
.distinct()
.sorted(new Comparator<Author>() {
@Override
public int compare(Author o1, Author o2) {
return o1.getAge() - o2.getAge();
}
})
.forEach(author -> System.out.println("作家" + author.getName() + "的年龄为:" + author.getAge()));
// 示例(Lambda表达式形式) - 中间操作 sorted([比较器接口])方法 实现元素排序
getAuthors().stream()
.distinct()
.sorted((o1, o2) -> o1.getAge() - o2.getAge())
.forEach(author -> System.out.println("作家" + author.getName() + "的年龄为:" + author.getAge()));
方式二:sorted()空参方法
【注意】流中间操作进行集合元素的排序时,若调用sorted()空参方法进行排序,前提是集合元素所在的类实现了
Comparable<T>
接口,并根据具体的比较逻辑重写接口中的抽象方法public int compareTo (Author o){}
。
代码示例如下:
- 首先,将集合元素所在的类实现
Comparable<T>
接口,重写其抽象方法public int compareTo (Author o){}
。代码示例如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
// 通过Lombok注解快速重写该实体类的equals()和Hashcode()方法,用于集合元素的去重。
@EqualsAndHashCode
// Stream流练习-测试实体类1
public class Author implements Comparable<Author> {
private Long id;
private String name;
private Integer age;
// 作者简介
private String introduce;
// 作者的作品
private List<Book> books;
@Override
public int compareTo(Author o) {
// 将当前对象的年龄与入参对象的年龄进行比较(二者相减,compareTo方法会根据返回值的正、负、0进行比较)
return this.getAge() - o.getAge();
}
}
- 其次,编写流操作代码,流中间操作直接调用
sorted()
空参方法即可,代码示例如下:
// 示例 - 中间操作 sorted()空参方法 实现元素排序(前提:元素所在类实现Comparable接口)
getAuthors().stream()
.distinct()
.sorted()
.forEach(author -> System.out.println("作家" + author.getName() + "的年龄为:" + author.getAge()));
2.5.5 limit
limit方法作为流的中间操作,用来对流中的元素个数进行限制,实现数据个数的限制(流的截取),limit(n)方法的入参设置流的最大长度,当集合 / 数组中元素个数超出限制时,会直接依据最大长度对流进行截取,仅保留前n个元素。
代码示例如下:
// 示例 - 中间操作 limit(n) 方法 实现流截取
getAuthors().stream()
.distinct()
.sorted((o1, o2) -> o1.getAge() - o2.getAge())
.limit(1)
.forEach(author -> System.out.println("目前最年轻的作家为:" + author.getName() + ",ta的年龄为:" + author.getAge()));
2.5.6 skip
skip方法作为流的中间操作,用来对流中的元素进行跳过,实现数据的跳过(排除),skip(n)方法的入参设置跳过前n个元素,从第n + 1个元素开始进行后续流操作。
代码示例如下:
// 示例 - 中间操作 skip(n) 方法 实现数据的排除
getAuthors().stream()
.distinct()
.sorted()
.skip(1)
.limit(1)
.forEach(author -> System.out.println("目前次年长的作家为:" + author.getName() + ",ta的年龄为:" + author.getAge()));
2.5.7 flatMap
2.6 流终结操作
持续更新中。。。
声明:本文是个人学习笔记,来源“三更草堂”。