欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 创投人物 > Java之函数式接口、lambda表达式、stream流操作、Optional容器、方法引用

Java之函数式接口、lambda表达式、stream流操作、Optional容器、方法引用

2025/5/22 12:03:31 来源:https://blog.csdn.net/weixin_43281875/article/details/148117262  浏览:    关键词:Java之函数式接口、lambda表达式、stream流操作、Optional容器、方法引用

目录

    • 1. lambda表达式介绍及基本语法
      • 1.1 为什么要使用lambda?
      • 1.2 lambda基本语法
      • 1.3 函数式接口
        • 1.3.1 默认方法
        • 1.3.2 静态方法
      • 1.4 lambda表达式和匿名内部类的区别
    • 2. lambda表达式使用案例
      • 2.1 数据流stream
        • 2.1.1 filter过滤
        • 2.1.2 map映射
        • 2.1.3 排序sorted
        • 2.1.4 聚合reduce
        • 2.1.5 收集collect
      • 2.2 Optional
        • 2.2.1 创建Optional
        • 2.2.2 访问Optional
        • 2.2.3 处理空值
        • 2.2.4 转换操作
    • 3 方法引用
      • 3.1 方法引用类别
      • 3.2 一些区别和注意点

1. lambda表达式介绍及基本语法

1.1 为什么要使用lambda?

lambda表达式是Java 8引入的一种新特性,用于简化函数式接口的实现能够替代函数形参中需要实现的匿名内部类,使得代码更加清晰简洁,此外,在Java集合、数据流中也常常能见到它的身影。

比如若要创建一个线程,需要在new出线程的同时实现参数Runnable接口,这就是一个实现了Runnable接口的匿名内部类:

Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println(" runing ...");}
});

而如果使用lambda表达式,上述代码可以简化成:

Thread thread = new Thread(() -> { System.out.println(" runing... "); });

可以很直观的看到lambda表达式省略了大量非重要代码(样板代码),只保留具体实现内容。

1.2 lambda基本语法

lambda表达式是一种匿名函数,没有名称,但需要提供参数列表、函数主体和返回值,其基本语法为:

// 0. 基本语法,params为0~n个参数,body为函数体,逻辑代码主体
(params) -> {body}
// 1. 无参数,单行body实现代码,body的大括号可省略,参数的小括号不可省略:
() -> System.out.println(" test ...");
// 2. 单参数,多行代码,参数小括号可以省略:
param -> {System.out.println(" test1 ...");System.out.println(" test2 ...");
}
// 3.1 多参数,一行返回值:
(param1, param2) -> { param1 + param2; }
// 3.2 多参数,多行代码需要返回值,必须使用代码块和return关键字
(param1, param2) -> {int ans = param1 + param2;return ans;
}

可以发现,在上述lambda表达式例子中,参数都未声明类型,它怎么知道我传的param1param2是int还是double呢?

因为lambda表达式是对匿名内部类的简化,是实现了函数式接口的实例,而接口方法的参数列表早已规定好,lambda表达式只是补充了方法内容,并不是就在它出现的地方直接执行了,会在调用函数式接口的方法时才会执行lambda表达式body代码因此当真正执行到lambda表达式时会根据函数式接口方法声明的参数列表(上下文)来推断参数类型,故不需要显示声明。那么,函数式接口具体有哪些要求呢?

1.3 函数式接口

函数式接口是指仅有一个抽象方法的接口,接口中可以包含默认方法或静态方法,函数式接口使得方法可以作为参数传递。java 8后可以使用@FunctionalInterface注解标记函数式接口,如果在该注解标记的接口中声明多个抽象方法,编译器会报错,因为这样补充的方法体不知道具体实现的是哪个方法。

接口在设计之初仅能有抽象方法,而Java 8之后为了支持接口的演化,适应实际开发需求,引入了默认方法和静态方法。

1.3.1 默认方法
  • 为什么引入?接口设计按照开闭原则,对修改关闭,对扩展开放。但是若随着业务发展必须扩展某个接口为其添加新的方法,而它又已经被大量类实现时,所有实现类都会因为缺少对新方法的实现而报错。这时就需要将这个接口新增方法定义成默认实现,如此接口实现类可以保持原样,不受到影响。
  • 怎么使用?使用default关键字修饰,拥有方法体,用来提供接口的默认实现,实现接口的实例类可以选择覆盖它,也可以不覆盖。
  • 是否覆盖?如果实现类没覆盖默认方法,那么它会继承接口的默认方法,可以直接调用;如果已覆盖,那么调用的是实现类的方法。
  • 多接口冲突?Java支持多接口实现,如果某个实现类继承多个接口,而这些接口拥有同名默认方法,则必须处理,否则会报错,可以通过以下两种方式解决:
    a. 实现类重写默认方法,方法体内提供实现类自己的代码;
    b. 若要调用接口默认方法,则必须在重写方法中指定父接口明确调用哪个接口方法。
1.3.2 静态方法
  • 为什么引入?如果希望在接口中实现类似工具类的方法,比如一个校验接口,要完成输入文本的字符串内容、日期格式、数据类型等基本校验要求(可能还需要其他针对具体业务的校验),这时会希望这些通用功能可以集成在接口内被直接调用,而业务校验留在实现类中。在java 8之前,由于接口只能定义抽象方法,所以只能通过定义工具校验类并实现通用校验功能,然后在实现类中调用工具类来辅助实现功能:
interface Validator{boolean validate(String text);
}class ValidatorUtil{static boolean checkDate(String text){// 核对日期相关代码...return true;}static boolean checkNum(String text){// 核对数据类型相关代码...return true;}
}
class MyValidator implements Validator{@Overridepublic boolean validate(String text) {if (!ValidatorUtil.checkDate(text)) return  false;if (!ValidatorUtil.checkNum(text)) return  false;// 具体业务核对代码...return true;}
}

上面这种写法可以实现校验功能,但工具类与接口的联系不紧密,两者呈现独立关系,若中间层再使用一个抽象类来代替工具类,那么实现类只能单继承这个抽象类,丧失了灵活性。因此Java 8引入了接口静态方法,将工具方法和接口定义集中到同一接口中,让工具方法和接口紧密相连,使得接口不仅具有基础的“契约”或“模板”定义,还能成为一个功能模块。

  • 怎么使用? 使用static关键字修饰,只能通过接口名调用,被static修饰的方法不能被重写。

为了支持函数式编程,Java提供了一组通用的函数式接口:

  • Predicate接口:抽象方法为boolean test(T t),接收单个参数并返回布尔值,streamfilter方法参数是Predicate类型;
  • Function<T,R>接口:抽象方法为R apply(T t),接收单个参数并返回一个结果,通常用作转换,streammap方法参数是Function类型;
  • Consumer接口: 抽象方法为void accept(T t),接收单个参数但不返回输出,可用于日志打印;
  • Supplier接口:抽象方法为T get(),无参但有输出,可用于生成随机数(但这种一般直接写一行代码就行了);
  • UnaryOperator接口: 抽象方法为T apply(T t),其实就是Function接口的特殊版;
  • BinaryOperator、BiPredicate<T,U>、Function<T,U,R>:原接口的双参数版本。

1.4 lambda表达式和匿名内部类的区别

lambda表达式是对匿名内部类实现的简化,本质上仍旧和匿名内部类一样是一个函数式接口的实例,但是它们仍有一些区别:

  1. 作用域不同
    匿名内部类:会新建一个新的作用域,内部可以重新定义变量,可以使用外部的final定义的变量或有效最终变量(未使用final标记,但在它整个生命周期中只读、并未被重新赋值/改变引用地址的变量,通常集合对象都是有效最终变量);
    lambda表达式:与外部共用一个作用域,因此不能定义与外部同样的变量名,但对外部变量的访问和匿名内部类一致。
  2. 字节码生成方式不同
    匿名内部类:编译时会生成一个单独的.class文件,名称一般是‘OuterClassName\$1.class’OuterClassName是外部主类名,运行时JVM会通过类加载机制加载这个类;
    lambda表达式:不会生成额外的类文件,而是通过生成invokedynamic字节码指令和JVM的动态方法调用机制实现,在运行时动态生成实例。

总体来说,lambda表达式不管是在书写还是运行上都更加轻量级。

2. lambda表达式使用案例

lambda表达式用在函数式接口实例化中,只要定义的接口满足函数式接口的定义即可,也可以使用@FunctionalInterface注解来规范定义,编译器会帮忙检查,但这个注解并不强制使用。给出一些常用的例子:

2.1 数据流stream

将集合、数组等类型的数据转成stream之后再使用lambda表达式来进一步处理是开发中的高频场景,stream API常见的用法有过滤filter、映射map、排序sorted、聚合reduced、收集collect

2.1.1 filter过滤

filter方法用于根据条件筛选元素,下面以filter为例看看lambda表达式是如何使用的:

//list是Person列表,Person包含name(string)、age(int)、valid(boolean)字段,existNames为name列表
list.stream().filter(item -> existNames.contains(item.getName()));

filter里可以访问有效最终变量existNames,因为列表existNames的引用并未改变,而filter本身的声明在Steam接口中:

Stream<T> filter(Predicate<? super T> predicate);

它需要传入一个Predicate(谓词)类型的函数接口,该接口要求补充能够返回布尔值的表达式,参数为泛型,且和流中元素类型一致(因此Predicatetest参数必须是变成stream之前集合或列表中的元素类型,这一步在流操作中会自动根据上下文信息填充)。Predicate的定义是:

@FunctionalInterface
public interface Predicate<T> {boolean test(T t);//还有一些默认函数
}

filter的具体实现在实现了Stream接口的类ReferencePipeline中定义,filter方法中会调用predicatetest方法(lambda中只是实例化,predicate真实调用在ReferencePipeline中),因此当我们在使用streamfilter方法时,只需要人为填充好谓词类型要求的参数(集合中元素),方法体给出能够返回布尔值的表达式即可(但如果是多行代码,需要显式使用return返回判断结果)。当代码执行到predicate.test时,就会自动调用我们在lambda表达式中书写的参数item和代码块。

还有另外一种方法引用写法:

//list是Person列表,Person包含name(string)、age(int)、valid(boolean)字段
list.stream().filter(Person::getValid)

乍一眼看都不符合lambda表达式规范了,但是这种写法是可以的,只是必须满足两个条件才能使得方法签名兼容:

a. 流中元素和方法引用前的类型必须一致,即Person必须是list里的元素类型,这样流元素与Predicate参数类型才匹配,满足上下文关系;
b. 方法必须是无参方法,并且返回类型是boolean类型;

这是因为filter需要一个Predicate,它的抽象方法test需要一个参数并返回布尔值,在流操作中,test需要的参数类型和流元素类型一致,因此它依赖于流元素实例,而Person::getValid就是一个依赖Person实例(流中元素)且返回布尔值的方法,因此等价于Predicate<Person>item->item.getValid()。这个方法引用将被应用于流中每个元素,当filter方法调用test方法时,它会将每个Person实例传递getValid方法。后续再对方法引用做详细说明。

2.1.2 map映射

map方法用于将流中元素转换为其他元素,使用的函数接口为FunctionT是流中元素类型,R是转换后元素类型:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

实例代码:

//提取person集合的姓名
List<String> names= person.stream().map(Person::getName).collect(Collectors.toList());

上述代码同样使用方法引用将getName方法绑定到流元素Person实例上,这样能将接口函数R apply(T t)填充为:

String apply(Person item){return item.getName();
}
2.1.3 排序sorted

sorted方法用于对流中元素进行排序,如果不带任何参数,则按照自然顺序(数字按升序、字符串按字母顺序)进行排序,如果是带参数版本的,则是:

Stream<T> sorted(Comparator<? super T> comparator);

需要传递一个Comparator接口,这个函数式接口包含的抽象方法为compare,并且提供许多默认方法及静态方法来生成Comparator实例,支持多个比较器比较(使用thenComparing方法)。以一个例子来说明这些用法:一个Person包含年龄age和姓名name,现在要对Person列表按照

  1. 年龄降序,名字长度升序排序;
  2. 年龄降序,名字降序排列,那么代码为:
//peple为Person列表
// 1)年龄降序,名字长度升序排序
List<Person> sortedPeople = people.stream().sorted(Comparator.comparingInt(Person::getAge).reversed().thenComparing(Comparator.comparingInt(person -> person.getName().length()))).collect(Collectors.toList());
// 2)年龄降序,名字降序排列
List<Person> sortedPeople = people.stream().sorted(Comparator.comparingInt(person -> -person.getAge()).thenComparing(Comparator.comparing(Person::getName))).collect(Collectors.toList());

上述代码中sorted需要的Comparator接口参数都通过Comparator提供的默认函数生成,当需要使用数字比较大小时,可以使用Comparator.comparingInt方法:

public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)

这个方法需要传入一个ToIntFunction接口:

@FunctionalInterface
public interface ToIntFunction<T> {int applyAsInt(T value);
}

这个接口是Function接口的变种,将一个任意对象转换为一个整数,在流操作中,它的意义是将流元素T转换为一个整数,后续再在comparingInt默认方法中使用整型的compare方法根据转换值获得对象的大小关系。

当需要按照对象整型属性逆序排序时,可以使用reverse()方法,或者在lambda表达式中返回值前添加一个负号(上述代码直接写在代码前,因为只有一行代码默认为返回代码),这样能让对象间转换值大小关系反转,实现逆序排序。

当需要使用字符串进行排序时,可使用Comparator.comparing方法,它的参数是Function接口,用以将流元素转换为另一个对象类型R对象R必须实现Comparable接口,上述代码是使用方法引用将流元素转换为String(R=String),再利用R实现Comparable接口的compare方法进行对比获得对象间大小关系。

如果要直接实例化Comparator接口,则代码是:

// 年龄降序,名字降序排列
List<Person> sortedPeople = people.stream().sorted((p1, p2) -> {int pare1 = Integer.compare(p1.getAge(), p2.getAge());return pare1 !=0 ? pare1 : p2.getName().compareTo(p1.getName());}).collect(Collectors.toList());
2.1.4 聚合reduce

reduce用于对流中元素进行聚合操作,可以将流元素合并为一个单一的值,比如求总和、乘积、最大值、最小值。reduce拥有重载函数:

//有初始值版本,流中的运算从初始值identity开始
T reduce(T identity, BinaryOperator<T> accumulator);
//无初始值版本
Optional<T> reduce(BinaryOperator<T> accumulator);

当不使用初始值时,reduce返回Optional对象,需要使用orElse()处理流为空的情况同时获得值,另外从上面的函数声明可以看出,在使用reduce函数时参数和返回值为同样的泛型,因此如果要求对象集合的某个属性聚合值,比如Person列表的年龄总和,需要先通过map获取年龄流,再使用reduce计算总和:

// 有初始值计算,从0开始
int totalAge = people.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
// 无初始值,orElse获得值,且同时处理流为空无初始值情况,流为空返回50
int totalAge = people.stream().map(Person::getAge).reduce(Integer::sum).orElse(50);
2.1.5 收集collect

collectstream中用作终结操作,用于将流元素收集到集合、数组、哈希表中,使用collect函数时通常用Collectors类提供的静态方法来获得它需要的Collector接口参数,常用方法如下:

// 1) toList(),将流中元素收集到List中
List<String> names = people.stream().map(Person::getName).collect(Collectors.toList());
// 2) toSet(),将流中元素收集到Set中,如果最后是生成引用对象类型的Set,那么需要先对这个对象重写hashCode()和equals()方法
Set<String> namesSet = people.stream().map(Person::getName).collect(Collectors.toSet());
// 3) toMap(),收集到哈希表,需要提供key和value两个函数
Map<String,Integer> namesMap = people.stream().map(Person::getName).collect(Collectors.toMap(Person::getName, Person::getAge));
// 4) joining(),将流中字符串(字符串流)连接成单一字符串
String allName = people.stream().map(people::getName).collect(Collectors.joining(","));
// 5) 根据某个属性将流元素分类
Map<Integer, List<Person>> groups = people.stream().collect(Collectors.groupingBy(Person::getAge));
……

2.2 Optional

Optional是一个容器对象,使用Optional能够减少空指针异常的风险,因为它可以处理可能为null的值。Optional操作里面也会用到函数式接口。

2.2.1 创建Optional
// 1) Optional.of(T value),传参必须非空,否则抛出空指针错误
// 2) Optional.ofNullable(T value),传参可以为空,为空则返回空的Optional
Optional<String> optional = Optional.ofNullable("test");
// 3) Optional.empty(),创建一个空的Optional
2.2.2 访问Optional

1)isPresent():使用isPresent()方法来检查Optional对象是否包含值,这个用法等价于用“==null”

if(optional.isPresent()){//其他代码}

2)ifPresent():更常用的是ifPresent():

public void ifPresent(Consumer<? super T> consumer) {if (value != null)consumer.accept(value);
}

ifPresent()常与ofNullable()方法一起使用,它需要实例化Consumer接口,接口参数就是Optional容器里放的元素,因此Optional.ofNullable(xxx).ifPresent(xxx → xxxx)的含义是如果xxx不为空则执行xxxx操作。

Optional.ofNullable(dto.getTotalFee()).ifPresent(totalFee -> {beforeInfo.fluentPut(InsureFieldEnum.TOTAL_FEE.fieldName, tbshareinsure.getTotalFee());afterInfo.fluentPut(InsureFieldEnum.TOTAL_FEE.fieldName, dto.getTotalFee());
});

3)get():若Optional为空,则抛出空指针异常

2.2.3 处理空值

由于Optional支持为空,因此它也有配套的空值处理操作来避免异常抛出:

  1. orElse()stream.reduce无初始值版本配合使用
int totalAge = people.stream().map(Person::getAge).reduce(Integer::sum).orElse(50);
  1. T orElseGet(Supplier<? extends T> other),使用supplier接口提供
Optional.ofNullable(null).orElseGet(() -> "defaultValue");
  1. 如果Option为空,也可以自定义抛出异常,使用orElseThrow方法,需要提供能返回异常类型对象的Supplier实例
//<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X
Optional.ofNullable(null).orElseThrow(() ->new RuntimeException("ERROR!"));
2.2.4 转换操作

1)map映射,需要提供Function接口实例,返回的是Optional对象,因此如果要获取容器的值最好再配合orElse使用:

//函数原式为Optional<U> map(Function<? super T, ? extends U> mapper),这里的用法和stream.map类似,person为Person实例
Optional.ofNullable(person).map(Person::getName).orElse(null);

2)fitler,与stream.filter用法一样,返回Optional对象。

3 方法引用

lambda表达式实例化函数式接口其实是通过现场填充方法体来实现的,而方法引用是直接将另一个方法引用到函数式接口,用被引用的方法填充原抽象方法。

方法引用是用来引用一个方法,并不是调用方法,所以不能在方法中指明具体的参数(即使方法带有参数),只能使用方法的名字。如果一定要传递参数,则需要使用lambda表达式语法。

在使用方法引用来实例化函数式接口时,必须注意引用方法的参数签名/返回类型和接口抽象方法的参数签名/返回类型一致,这样才能达到“用引用方法替换接口方法”的目的。

3.1 方法引用类别

  • 静态方法引用:引用类的静态方法,在没有实例的情况下调用,语法为:ClassName::staticMethod
  • 实例方法引用-任意对象:引用某个对象的实例方法,语法为ClassName::method,一般用于流中调用某一个类的实例方法,不特指某个实例对象;
  • 实例方法引用-特定对象:引用某个特定实例的方法,语法为instance::methodinstance是一个具体的实例;
  • 构造方法引用:引用类的构造方法,用于创建对象,语法为ClassName::New

3.2 一些区别和注意点

任意对象和特定对象的实例方法引用区别是什么?——任意对象突出”实例“而非静态,指某个类的一群实例,特定对象突出”特定“,特指某一个实例。

任意对象实例引用多用于流操作,流需要对集合中每一个实例操作方法,比如:

list.stream().filter(Person::getValid)

因为Person::getValid在上述流操作中(泛型匹配)直接与Person实例挂钩,因此getValid参数为空是可以的。

特定对象实例引用需要先生成一个实例,再引用实例方法:

// 实例举例
Class PrintMsg{public void print(String msg){log.info(msg);}
}//实例方法引用
PrintMsg pm = new PrintMsg();
Consumer<String> printer = pm::print;  
printer.accept("test");

此处print方法签名(String类型的参数,空返回)和Consumer接口抽象方法accept的签名(泛型参数-此处为String,空返回)一致,如果泛型用了其他类,那么print方法里也要相应类的实例作为参数。

构造方法引用需要注意传递的参数与构造方法的参数匹配,比如如下例子是错误的:

Class Person{private String name;Person(String name){this.name = name;}
}//构造方法引用
Supplier<Person> provider = Person::new;
Person person = provider.get();

因为Supplier抽象函数get的参数列表为空,而Person只提供了有参构造,因此上述代码签名是不匹配,如果要正常运行代码,Person需要提供一个无参构造。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词