概念
函数式接口在Java中是指:有且仅有一个抽象方法的接口
函数式接口,即适用于函数式编程场景的接口,而Java中的函数式编程的体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口,只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能正确的推导
格式
只要确保接口中有且仅有一个抽象方法即可
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数);
//非抽象方法内容(默认 静态 私有)
}
@FunctionalInterface注解
与@Override
注解的作用类似,Java8中专门为函数式接口引入了一个新的注解@FunctionalInterface
标识检测一个接口为函数式接口
Lambda表达式的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行,正好可以作为解决方案,提升性能
常用函数式接口
JDK提供了大量的函数式接口来丰富Lambda的使用场景,它们主要在java.util.function
包中被提供
Supplier接口
java.util.function.Supplier<T>
接口仅包含一个无参的方法:T get()
用来获取一个泛型参数指定类型的对象数据
由于这是一个函数式接口,也就意味着对应的Lambda表达式需要对外提供一个符合泛型类型对象的数据
public class Main {
public static String getString(Supplier<String> supplier){
return supplier.get();
}
public static void main(String[] args) {
String message = getString(() -> "message");
}
}
其他都相同…可自行翻阅官方给出的java.util.function
包中的所有函数式接口
Stream流
说到Stream流就很容易想到IO流,而实际上,谁规定流就一定是IO流呢?在Java8中,得益于Lambda表达式所带来的函数式编程
引入了一个全新的Stream概念,用于解决已有集合类库即有的弊端
传统集合的多步遍历代码
几乎所有的集合(如Collection
或者Map
接口等)都支持直接的或者间接的遍历操作,当我们需要对集合中的元素进行操作时候,除了必要的添加,删除,获取外,最典型的就是集合遍历
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
for (String s : list) {
System.out.println(s);
}
}
这是一段非常简单的集合遍历操作,对集合中的每一个字符串都进行打印输出等操作
循环遍历的弊端
Java8的Lambda让我们更加专注于什么,而不是怎么做,现在我们体会一下代码,可以发现:
- for循环的语法就是做什么
- for循环的循环体才是怎么做
为什么要使用循环?因为要进行遍历,但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是手段
试想一下,如果希望对集合中的元素进行筛选过滤
1.将集合A根据条件一过滤为子集B
2.然后再根据条件二过滤为子集C
那么怎么办?在Java8之前的做法可能为:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
list.add("angel");
List<String> list1 = new ArrayList<>();
//过滤 只要a开头的
for (String name : list){
if (name.startsWith("a")){
list1.add(name);
}
}
List<String> list2 = new ArrayList<>();
//过滤 只要长度为3的
for (String name : list1){
if (name.length() == 3){
list2.add(name);
}
}
for (String name : list2) {
System.out.println(name);
}
}
上述代码有三个循环用来过滤和输出
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环循环再循环,这是理所当然的吗?不是。循环是做事情的方式,不是目的。另一方面,循环只能遍历一次,如果希望再次遍历,就只能再起一次循环
那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?
Stream的更优写法
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
list.add("and");
list.stream()
.filter((name) -> name.startsWith("a"))
.filter((name) -> name.length() == 3)
.forEach((name) -> System.out.println(name));
}
流式思想概述
拼接流式模型:建立一个生产线,按照生产线来生产商品
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能以及遍历性,我们应该首先拼接好一个模型步骤方案然后执行
Stream流是来自数据源的元素队列
- 元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算
- **数据源 **流的来源,可以是集合数组等
和以前的Collection操作不同,Stream操作还有两个基础特征
- Pipelinling:中间操作都会返回流本身,这样多个操作可以串联成一个管道,如同流式风格,可对操作优化如延迟执行和短路
- 内部迭代:以前对集合的遍历都是Iterator或者for循环,显式的在集合外部进行迭代,这叫做外部迭代。Stream提供了内部迭代方式
当使用一个流的时候,通常包括基本的三个步骤:获取一个数据源 -> 数据转换 -> 执行操作获取想要结果,每次转换原有Stream对象不改变,返回一个新的Stream对象(可有多次转换),这就允许对其操作可以像链条一样排列,形成一个管道
获取流
java.util.stream.Stream<T>
是Java8新加入的最常用的流接口(非函数式接口)
获取一个流非常简单,有以下几种常用的方式:
- 所有
Collection
集合都可以通过stream
默认方法获取流 Stream
接口的静态方法of
可以获取数组对应的流
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<String,String> map = new HashMap();
Stream<String> stream1 = list.stream();
Stream<String> stream2 = set.stream();
Set<String> keySet = map.keySet();
Stream<String> stream3 = keySet.stream();
Collection<String> values = map.values();
Stream<String> stream4 = values.stream();
Set<Map.Entry<String, String>> entries = map.entrySet();
Stream<Map.Entry<String, String>> stream5 = entries.stream();
//数组转换为Stream流
Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
String[] arr = {"aaa","bbb","ccc"};
Stream<String> stream7 = Stream.of(arr);
}
常用方法
流模型的操作很丰富,这里介绍一些常用的API,这些方法可以被分为两种
- 延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用(除了终结方法外的所有其余方法)
- 终结方法:返回值类型不再是Stream接口自身类型的方法,包括count方法和foreach方法
逐一处理:foreach
虽然方法名字叫foreach
但是与for循环的for-each昵称不同
void forEach(Consumer<? super T> action);
该方法接收一个Consumer
接口函数,会将每一个流元素交给该函数进行处理
java.util.function.Consumer<T>
接口是一个消费型接口
其中抽象方法accept
消费一个指定泛型的数据
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
使用:
public static void main(String[] args) {
Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
stream.forEach((key) -> System.out.println(key));
}
过滤:filter
可以通过filter方法将一个流过滤为子集流
Stream<T> filter(Predicate<? super T> predicate);
该方法接收一个Predicate
函数式接口参数作为筛选引用
java.util.function.Predicate<T>
接口是判断
其中抽象方法accept
消费一个指定泛型的数据,方法返回一个boolean,为true则Stream
流的filter
方法会留用元素
使用:
public static void main(String[] args) {
Stream<String> stream = Stream.of("aaa", "bbbb", "cccc");
stream.filter((key) -> key.length() == 3);
}
映射:map
如果需要将流中的数据映射到另一个流中,可以使用map
方法
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该方法需要一个Function
函数式接口参数,可以将当前流中T类型的数据转换成另一种R类型的流
java.util.function.Function
中唯一的抽象方法
R apply(T t);
这可以将T类型转换成R类型,而这种转换的动作就称为映射
使用:
public static void main(String[] args) {
Stream<String> stream = Stream.of("1", "2", "3");
Stream<Integer> stream1 = stream.map((key) -> Integer.valueOf(key));
}
统计个数:count
正如旧集合Collection
中的size
方法一样,流提供count
方法来数一数元素个数
long count();
基本使用:
public static void main(String[] args) {
Stream<String> stream = Stream.of("1", "2", "3");
System.out.println(stream.count());
}
取用前几个:limit
limit
方法可以对流进行截取,只取用前n个
Stream<T> limit(long maxSize);
参数是一个long类型,如果集合当前长度大于参数则进行截取,否则不进行操作
public static void main(String[] args) {
Stream<String> stream = Stream.of("1", "2", "3");
Stream<String> stream1 = stream.limit(2);
System.out.println(stream1.count());
}
跳过前几个:skip
如果希望跳过前几个元素,可以使用skip
方法截取一个新流
Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个,否则会得到一个长度为0的空流
public static void main(String[] args) {
Stream<String> stream = Stream.of("1", "2", "3");
Stream<String> stream1 = stream.skip(2);
System.out.println(stream1.count());
}
组合:concat
如果有两个流,希望合并为一个流,那么可以使用Stream
接口的静态方法concat
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {}
使用:
public static void main(String[] args) {
Stream<String> stream1 = Stream.of("1", "2", "3");
Stream<String> stream2 = Stream.of("4", "5", "6");
Stream<String> stream = Stream.concat(stream1, stream2);
}
Stream流的特点
Stream属于管道流,只能被消费(使用)一次
第一个Stream流调用完毕方法,数据就会流转到下一个Stream流上
而这时第一个Stream流已经使用完毕,就会关闭了
所以第一个Stream流就不能再调用方法了