1.函数式编程思想
最近工作中遇到很多函数式编程使用的实例,Stream流之类的操作,但是我由于我很久没接触到,没使用这一特性导致代码阅读起来有点吃力,故借周末补充一下相关的知识点,作此笔记供下次查阅。
1.1 概念:
面向对象思想需要关注用什么对象完成什么事情。而函数式编程思想就类似于我们数学中的函数。主要关注对数据进行了什么操作。
1.2 优点 :
- 代码简洁,开发快速
- 接近自然语言,易于理解
- 易于并发编程
2. Lambda 表达式
2.1 概述:
Lambda是JDB8中一个新的语法糖。它可以对某些匿名内部类的写法进行简化。他是函数式编程思想的一个重要体现。他让我们不用关注是什么对象,而是更关注我们对数据进行什么操作。
2.2 核心原则:
可推导可省略
2.3 基本格式
(参数列表) -> {方法体}
2.4 小例子
例一
如果此匿名内部类是一个接口且只有一个方法则可用Lambda代替
例一:
new Thread(new Runnable(){
@Override
public void run() {
.....
}
}.start();
例二:
System.out.println(typeConver(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
}).getClass());//class java.lang.Integer
public static <R> R typeConver(Function<String,R> function){
String str="1235";
R result=function.apply(str);
return result;
}
)
我们可以使用Lambda表达式对其进行修改。如下:
例一:
new Thread( () ->{
...
}).start();
例二:
System.out.println(typeConver(s -> Integer.valueOf(s)).getClass());
2.5 Lambda表达式省略规则
- 参数类型可以省略
- 方法体只有一句代码时大括号 return 和此行代码的分号可以省略
- 方法只有一个参数时 小括号也是可以省略的
- 以上规则记不住也可以省略不记 ^.^
3. Stream 流
3.1 开端
Java8的Stream流使用的是函数式编程模式,如同他的名字一样,它可以被用来对集合或数组进行链状流式操作。可以更方便我们的开发过程。(这也是我目前知识欠缺的地方 😺)
案例准备:
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode //后期去重使用
public class Author {
private Long id;
private String name;
private Integer age;
private String intro;
private List<Book> books;
}
public class Book {
private Long id;
private String name;
private String category;
private Integer score;
private String intro;
}
public class TestStream{
public static List<Author> getAuthors(){
Author author = new Author(1L, "梦多", 33, "描述1", null);
Author author2 = new Author(2L, "梦多2", 15, "描述2", null);
Author author3 = new Author(3L, "梦多3", 14, "描述3", null);
Author author4 = new Author(4L, "梦多4", 14, "描述4", null);
List<Book> books1=new ArrayList<>();
List<Book> books2=new ArrayList<>();
List<Book> books3=new ArrayList<>();
books1.add(new Book(1L,"书名1","分类1",88,"描述1"));
books1.add(new Book(2L,"书名2","分类2",99,"描述2"));
books2.add(new Book(3L,"书名3","分类3,分类2",85,"描述3"));
books2.add(new Book(3L,"书名3","分类3",85,"描述3"));
books2.add(new Book(4L,"书名4","分类4",56,"描述4"));
books3.add(new Book(5L,"书名5","分类5",56,"描述5"));
books3.add(new Book(6L,"书名6","分类6",100,"描述6"));
books3.add(new Book(6L,"书名6","分类6",100,"描述6"));
author.setBooks(books1);
author2.setBooks(books2);
author3.setBooks(books3);
author4.setBooks(books3);
return new ArrayList<>(Arrays.asList(author,author2,author3,author4));
}
}
3.2 实操
3.2.1需求
我们可以调用getAuthors方法获取所有作家的集合,现在需要打印所有年龄小于18的作家的名字,且要注意去重
3.2.2实现
List<Author> authors = getAuthors();
authors.stream()//把集合转换成流
.distinct()//去重 去掉流中重复的元素
.filter((author)->author.getAge()<18)//过滤 加断言(判断)
.forEach(author -> System.out.println(author.getName()));//打印姓名
3.3 常用操作
3.3.1 创建流
单列集合:集合对象.stream()
List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();//把集合转换成流
数组: Arrays.stream(数组) 或者使用 Stream.of 来创建
Integer[] arr={1,2,3,4,5};
Stream<Integer> stream = Arrays.stream(arr);
Stream<Integer> arr1 = Stream.of(arr);
双列集合: 转换成单列集合后再创建。
HashMap<String, Integer> map = new HashMap<>();
map.put("Test1",19);
map.put("Test2",17);
map.put("Test3",18);
Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();
3.3.2 中间操作
filter
可以对流中操作进行条件过滤,符合条件的才能继续留在流中。
authors.stream()//把集合转换成流
.filter((author)->author.getAge()<18);//过滤 加断言(判断)
map
可以把流中的元素进行计算或转换
输出所有的作家姓名
authors.stream()//把集合转换成流
.map(author -> author.getName())//流中元素进行类型转换 Author-->String
.forEach(author -> System.out.println(author));//打印
distinct 去重
打印所有作家的姓名,但不能有重复元素。 注: distinct方法时依赖Object中的equals方法来判断是否是相同对象的。所以要注意重写equals方法
sorted 排序
对流中元素按照年龄排序,且要求不能有重复的元素
authors.stream()//把集合转换成流
.distinct()
.sorted((o1, o2) -> o2.getAge()-o1.getAge())
.forEach(author -> System.out.println(author));//打印
limit 设置流的最大长度,超出部分将被抛弃
skip 跳过流中前n个元素
flatMap
map只能把一个对象转换成另一个对象来作为流中的元素。而flatmap可以把一个对象转换成多个对象作为流中的元素。
- 打印所有书籍的名字。去重
authors.stream()//把集合转换成流
.flatMap((Function<Author, Stream<?>>) author -> author.getBooks().stream())
.distinct()
.forEach(book -> System.out.println(book));//打印
- 打印现有数据的所有分类。要求对分类进行去重,不能出现这种格式: 哲学,爱情
authors.stream()//把集合转换成流
.flatMap(author -> author.getBooks().stream())
.distinct()
.flatMap(book -> Arrays.stream(book.getCategory().split(",")))
.forEach(cata-> System.out.println(cata));
3.3.3 终结操作
forEach 对流中元素进行遍历操作
count 计数
打印作家所出书籍树木,且去重
long count = authors.stream()//把集合转换成流
.flatMap(author -> author.getBooks().stream())
.distinct()
.count();
System.out.println(count);
min&max 获取流中最小值&最大值
分别获取这些作家所出书籍的最高分和最低分
Optional<Book> max = authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.max((o1, o2) -> o1.getScore() - o2.getScore());
System.out.println(max);
Optional<Book> min = authors.stream()
.flatMap(author -> author.getBooks().stream())
.distinct()
.min((o1, o2) -> o1.getScore() - o2.getScore());
System.out.println(min);
collect
把当前流转成一个集合
- 获取一个存放所有作者名字的List集合
List<String> collect = authors.stream()
.distinct()
.map(author -> author.getName())
.collect(Collectors.toList());
System.out.println(collect);
- 获取一个所有书名的Set集合
Set<String> collect = authors.stream()
.distinct()
.flatMap(author -> author.getBooks().stream())
.map(book -> book.getName())
.collect(Collectors.toSet());
System.out.println(collect);
- 获取一个Map集合
Map<String, List<Book>> collect = authors.stream()
.distinct()
.collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()
));
System.out.println(collect);
查找与匹配
anyMatch
存在符合任意条件的元素
boolean b = authors.stream()
.distinct()
.anyMatch(author -> author.getAge() > 14);
System.out.println(b);
allMatch
全部元素都匹配才true
boolean b = authors.stream()
.distinct()
.allMatch(author -> author.getAge() > 18);
System.out.println(b);
noneMatch
所有元素都不符合条件才true
boolean b = authors.stream()
.distinct()
.noneMatch(author -> author.getAge() > 33);
System.out.println(b);
findAny
获取流中任意一个元素。没法保证取得一定是流中第一个元素
findFirst
获取第一个元素
reduce 归并
对流中的数据按照指定的计算方式计算出一个结果
reduce的作用就是把stream中的元素组合起来,我们可以传入一个初始值,他会按照我们的计算方式依次拿流中的元素和在初始化值的基础上进行计算,计算结果再和后面的元素进行计算。
**reduce两个参数的重载形式**内部的计算方式是:
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;
其中Identity就是我们可以通过方法参数传入的初始值,accumulator的apply具体进行什么计算,也是我们通过方法参数来确定的。
1.使用reduce求所有作者年龄的和
Integer reduce = authors.stream()
.distinct()
.map(author -> author.getAge())//Author 转成 Integer
.reduce(0, (integer, integer2) -> integer + integer2);
System.out.println(reduce);
2.使用reduce求所有作者中年龄的最大值
Integer reduce = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(Integer.MIN_VALUE,(integer, integer2) -> integer>integer2?integer:integer2);
System.out.println(reduce);
3.使用reduce求所有作者中年龄的最小值(使用一个参数)
Optional<Integer> reduce = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer result, Integer element) {
return result > element ? element : result;
}
});
reduce.ifPresent(integer -> System.out.println(integer));
reduce一个参数的重载形式内部计算
boolean foundAny = false;
T result = null;
for(T element: this stream){
if(!foundAny){
foundAny=true;
result=element;
}else{
result = accumulator.apply(result, element);
}
}
return foundAny ? Optional.of(result):Optional.empty();
注意事项:
- 惰性求值 (如果没有终结操作,没有中间操作是不会得到执行的)
- 流是一次性的(一旦一个流对象经过一个终结操作之后,这个流就不能再被使用)
- 不会影响原数据(我们在流中可以对数据做很多处理。但是正常情况下是不会对原来集合中的元素造成影响,这也是我们所期望的。)
4. Optional
我们在编写代码的时候出现最多的就是空指针异常。所以在很多情况下我们需要做各种非空判断。
尤其是对象中的属性还是一个对象的情况下,这种判断会更多。
而过多的判断语句会让我们的代码显得臃肿不堪。
所以在JDK8中引入了Optional,养成使用Optional 的习惯后你可以写出更优雅的代码来避免空指针异常。
并且在很多函数式编程相关的API中也都用到了Optional,如果不会使用Optinal也会对函数式编程的学习造成影响。
4.1 使用
4.1.1 创建对象
Optional就好像是包装类,可以把我们的具体数据封装在Optional对象内部,然后我们去使用Optional中封装好的方法对封装进去的数据进行操作,就可以非常优雅地避免空指针异常。
我们一般使用 Optional的静态方法ofNullable来把数据封装成一个Optional对象。无论传入的参数是否为null都不会出现问题。
Author author = getAuthor();
Optional<Author> authorOptional = Optional.ofNullable(author);
你可能会觉得还要加一行代码来封装数据很麻烦,但是如果改造下getAuthor方法,让其返回的值就是Optional的话,我们在使用时就会方便很多。
而且实际开发中我们的数据很多是从数据库中获取的。Mybatis从3.5版本也已经支持Optional了。我们可以直接把dao方法的返回值类型定义为Optional类型,Mybatis会自己把数据封装成Optional对象返回。封装的过程也不需要我们自己操作。
如果你确定一个对象不是空的则可以使用Optional的静态方法of来把数据封装成Optional对象
Author author = getAuthor();
Optional<Author> authorOptional = Optional.of(author);
但是一定要注意,如果使用of,则传入的参数必须不为null!!
如果一个方法的返回值类型是Optional类型。而如果我们经判断发现某次计算得到的返回值为null,这个时候就需要把null封装成Optional对象返回。这是则可以使用 Optional 的静态方法 empty来进行封装
Optional.empty();
4.1.2 安全消费值
我们获取到一个Optional对象后肯定要对其中的数据进行使用,这时候我们可以使用ifPresent方法来消费其中的值。
这个方法会判断其内部的数据是否是空,不为空才会执行具体的消费代码。这样使用起来就更加安全了。
例如,以下写法可优雅地避免空指针异常。
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
anthorOptional.ifPresent(author -> sout(author.getName()));
4.1.3 安全获取值
如果我们期望安全的获取值。我们不推荐使用get方法,而是使用Optional提供的以下方法。
- orElseGet
获取数据并且设置数据为空时的默认值。如果数据不为空就能获得到该数据。如果为空则根据传入的参数来创建对象作为默认值的返回。 - orElseThorw
获取数据,如果数据不为空就能获取到该数据。如果为空则根据传入参数来创建异常抛出。
4.1.4 过滤
我们可以使用filter方法对数据进行过滤。如果原本是有数据的,但是不符合判断,也会变成一个无数据的Optional对象。
4.1.5 判断
我们可以使用isPresent方法对是否存在进行判断。如果空返回值为false,如果不为空,返回true。但是这种方法并不能体现Optional的好处,更推荐使用ifPresent方法。
4.1.6 数据转换
Optional还提供了map可以让我们对数据进行转换,并且转换得到的数据也还是被Optional包装好的,保证了我们的使用安全。 例如我们想获取作家的书籍集合
Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
Optional<List<Book>> books = authorOptional.map(author -> author.getBooks());
5. 函数式接口
只有一个抽象方法的接口成为函数接口
JDK的函数接口上都加了 @FunctionalInterface 注解进行标识。但是无论是否加上该注解只要接口中只有一个抽象方法,都是函数式接口。
5.1 常见函数式接口
- Consumer 消费接口 有入参无返回值 根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数进行消费。
public interface Consumer<T> {
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
- Function 计算转换接口 有出入参 可以在方法中对入参操作并有返回值
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
- Predicate 判断接口
- Supplier 生产型接口 无入参有返回 可以在方法中创建对象,把创建好的对象返回
