函数式接⼝、Stream流、⽅法引⽤

1 函数式接⼝


前章回顾

lambda表达式:

  1. 匿名内部类实现 函数式 接口
  2. 有上下文的推导环境

  可推导, 可省略

格式:

  (函数式接口中方法的参数) -> {要实现的方法体}

  1. 参数类型 可以省略
  2. 只有一个参数, () 可以省略
  3. 方法中只有一句话, return + ; + {} 可以省略

1.1 概念

函数式接⼝在 Java 中是指: 有且仅有⼀个抽象⽅法的接⼝
函数式接⼝,即适⽤于函数式编程场景的接⼝。⽽ Java 中的函数式编程体现就是 Lambda ,所以函数式接⼝就是可以适⽤于 Lambda 使⽤的接⼝。只有确保接⼝中有且仅有⼀个抽象⽅法, Java 中的 Lambda 才能顺利 地进⾏推导。
备注: 语法糖 是指使⽤更加⽅便,但是原理不变的代码语法。例如在遍历集合时使⽤的 for-each 法,其实底层的实现原理仍然是迭代器,这便是 语法糖 。从应⽤层⾯来讲, Java 中的 Lambda 可以 被当做是匿名内部类的 语法糖 ,但是⼆者在原理上是不同的。

1.2 格式

只要确保接⼝中有且仅有⼀个抽象⽅法即可:
修饰符 interface 接⼝名称 {
 public abstract 返回值类型 ⽅法名称(可选参数信息);
 // 其他⾮抽象⽅法内容
}

 由于接⼝当中抽象⽅法的 public abstract 是可以省略的,所以定义⼀个函数式接⼝很简:

public interface MyFunctionalInterface {
 void myMethod();
}

1.3 @FunctionalInterface注解

@Override(重写) 注解的作⽤类似, Java 8 中专⻔为函数式接⼝引⼊了⼀个新的注
解: @FunctionalInterface 。该注解可⽤于⼀个接⼝的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
 void myMethod();
}
⼀旦使⽤该注解来定义接⼝,编译器将会强制检查该接⼝是否确实有且仅有⼀个抽象⽅法,否则将会报错。需要 注意 的是,即使不使⽤该注解,只要满⾜函数式接⼝的定义,这仍然是⼀个函数式接⼝,使⽤起 来都⼀样。

1.4 ⾃定义函数式接⼝

对于刚刚定义好的 MyFunctionalInterface 函数式接⼝,典型使⽤场景就是作为⽅法的参数:
public class Demo09FunctionalInterface {
 // 使⽤⾃定义的函数式接⼝作为⽅法参数
 private static void doSomething(MyFunctionalInterface inter) {
 inter.myMethod(); // 调⽤⾃定义的函数式接⼝⽅法
 }
 public static void main(String[] args) {
 // 调⽤使⽤函数式接⼝的⽅法
 doSomething(() -> System.out.println("Lambda执⾏啦!"));
 }
}

2 函数式编程


在兼顾⾯向对象特性的基础上, Java 语⾔通过 Lambda 表达式与⽅法引⽤等,为开发者打开了函数式编程的 ⼤⻔。下⾯我们做⼀个初探。

2.1 Lambda的延迟执⾏

有些场景的代码执⾏后,结果不⼀定会被使⽤,从⽽造成性能浪费。⽽ Lambda 表达式是延迟执⾏的,这正好可以作为解决⽅案,提升性能。
性能浪费的⽇志案例
注:⽇志可以帮助我们快速的定位问题,记录程序运⾏过程中的情况,以便项⽬的监控和优化
⼀种典型的场景就是对参数进⾏有条件使⽤,例如对⽇志消息进⾏拼接后,在满⾜条件的情况下进⾏打印输出:
public class Demo01Logger {
 private static void log(int level, String msg) {
 if (level == 1) {
 System.out.println(msg);
 }
 }
 public static void main(String[] args) {
 String msgA = "Hello";
 String msgB = "World";
 String msgC = "Java";
 log(1, msgA + msgB + msgC);
 }
}
这段代码存在问题:⽆论级别是否满⾜要求,作为 log ⽅法的第⼆个参数,三个字符串⼀定会⾸先被拼接并传⼊⽅法内,然后才会进⾏级别判断。如果级别不符合要求,那么字符串的拼接操作就⽩做了,存在性 能浪费。
备注: SLF4J 是应⽤⾮常⼴泛的⽇志框架,它在记录⽇志时为了解决这种性能浪费的问题,并不推荐⾸先进⾏字符串的拼接,⽽是将字符串的若⼲部分作为可变参数传⼊⽅法中,仅在⽇志级别满⾜要求的情况下才会进⾏字符串拼接。例如: LOGGER.debug(" 变量 {} 的取值为 {} ", "os", "macOS") ,其中的⼤括号 {} 为占位符。如果满⾜⽇志级别要求,则会将 “os” “macOS” 两个字符串依次拼接到⼤括号的位置;否则不会进⾏字符串拼接。这也是⼀种可⾏解决⽅案,但 Lambda 可以做 到更好。
体验 Lambda 的更优写法
使⽤ Lambda 必然需要⼀个函数式接⼝:
@FunctionalInterface
public interface MessageBuilder {
 String buildMessage();
}
然后对 log ⽅法进⾏改造:
public class Demo02LoggerLambda {
 private static void log(int level, MessageBuilder builder) {
 if (level == 1) {
 System.out.println(builder.buildMessage());
 }
 }
 public static void main(String[] args) {
 String msgA = "Hello";
 String msgB = "World";
 String msgC = "Java";
 log(1, () -> msgA + msgB + msgC );
 }
}
这样⼀来,只有当级别满⾜要求的时候,才会进⾏三个字符串的拼接 ; 否则三个字符串将不会进⾏拼接。
证明 Lambda 的延迟
下⾯的代码可以通过结果进⾏验证:
public class Demo03LoggerDelay {
 private static void log(int level, MessageBuilder builder) {
 if (level == 1) {
 System.out.println(builder.buildMessage());
 }
 }
 public static void main(String[] args) {
 String msgA = "Hello";
 String msgB = "World";
 String msgC = "Java";
 log(2, () -> {
 System.out.println("Lambda执⾏!");
 return msgA + msgB + msgC;
 });
 }
}
从结果中可以看出,在不符合级别要求的情况下, Lambda 将不会执⾏。从⽽达到节省性能的效果。
扩展:实际上使⽤内部类也可以达到同样的效果,只是将代码操作延迟到了另外⼀个对象当中通过调⽤⽅法来完成。⽽是否调⽤其所在⽅法是在条件判断之后才执⾏的。

2.2 使⽤Lambda作为参数和返回值

如果抛开实现原理不说, Java 中的 Lambda 表达式可以被当作是匿名内部类的替代品。如果⽅法的参数是⼀个函数式接⼝类型,那么就可以使⽤ Lambda 表达式进⾏替代。使⽤ Lambda 表达式作为⽅法参数,其实就 是使⽤函数式接⼝作为⽅法参数。
例如 java.lang.Runnable 接⼝就是⼀个函数式接⼝,假设有⼀个 startThread ⽅法使⽤该接⼝作为参数,那么就可以使⽤ Lambda 进⾏传参。这种情况其实和 Thread 类的构造⽅法参数为 Runnable 没有本质 区别。
public class Demo04Runnable {
 private static void startThread(Runnable task) {
 new Thread(task).start();
 }
 public static void main(String[] args) {
 startThread(() -> System.out.println("线程任务执⾏!"));
 }
}
类似地,如果⼀个⽅法的返回值类型是⼀个函数式接⼝,那么就可以直接返回⼀个 Lambda 表达式。当需要通过⼀个⽅法来获取⼀个 java.util.Comparator 接⼝类型的对象作为排序器时,就可以调该⽅法获 取。
import java.util.Arrays;
import java.util.Comparator;
public class Demo06Comparator {
 private static Comparator<String> newComparator() {
 return (a, b) -> b.length() - a.length();
 }
 public static void main(String[] args) {
 String[] array = { "abc", "ab", "abcd" };
 System.out.println(Arrays.toString(array));
 Arrays.sort(array, newComparator());
 System.out.println(Arrays.toString(array));
 }
}
其中直接 return ⼀个 Lambda 表达式即可。

3 常⽤函数式接⼝


JDK 提供了⼤量常⽤的函数式接⼝以丰富 Lambda 的典型使⽤场景,它们主要在 java.util.function 中被提供。下⾯是最简单的⼏个接⼝及使⽤示例。

3.1 Supplier接⼝

java.util.function.Supplier<T> (生产者)接⼝仅包含⼀个⽆参的⽅法: T get() 。⽤来获取⼀个泛型参数 指定类型的对象数据。由于这是⼀个函数式接⼝,这也就意味着对应的Lambda 表达式需要 对外提供 ⼀个符合泛型类型的对象数据。
import java.util.function.Supplier;
public class Demo08Supplier {
 private static String getString(Supplier<String> function) {
 return function.get();
 }
 public static void main(String[] args) {
 String msgA = "Hello";
 String msgB = "World";
 System.out.println(getString(() -> msgA + msgB));
 }
}

3.2 练习:求数组元素最⼤值

题⽬
使⽤ Supplier 接⼝作为⽅法参数类型,通过 Lambda 表达式求出 int 数组中的最⼤值。提示:接⼝的泛型请使⽤ java.lang.Integer 类。
解答
public class Demo02Test {
 // 定⼀个⽅法,⽅法的参数传递Supplier,泛型使⽤Integer
 public static int getMax(Supplier<Integer> sup) {
 return sup.get();
 }
 public static void main(String[] args) {
 int arr[] = { 2, 3, 4, 52, 333, 23 };
 // 调⽤getMax⽅法,参数传递Lambda
 int maxNum = getMax(() -> {
 // 计算数组的最⼤值
 int max = arr[0];
 for (int i : arr) {
 if (i > max) {
 max = i;
 }
 }
 return max;
 });
 System.out.println(maxNum);
 }
}

3.3 Consumer接⼝

java.util.function.Consumer<T> (消费者) 接⼝则正好与 Supplier 接⼝相反,它不是⽣产⼀个数据,⽽是 消费 ⼀个数据,其数据类型由泛型决定。
抽象⽅法:accept
Consumer 接⼝中包含抽象⽅法 void accept(T t) ,意为消费⼀个指定泛型的数据。基本使⽤如:
import java.util.function.Consumer;
public class Demo09Consumer {
 private static void consumeString(Consumer<String> function) {
 function.accept("Hello");
 }
 public static void main(String[] args) {
 consumeString(s -> System.out.println(s));
 }
}
当然,更好的写法是使⽤⽅法引⽤。
默认⽅法: andThen
如果⼀个⽅法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,⾸先做⼀个操作,然后再做⼀个操作,实现组合。⽽这个⽅法就是 Consumer 接⼝中的 default ⽅法 andThen 。下⾯ JDK 的源代码:
default Consumer<T> andThen(Consumer<? super T> after) {
 Objects.requireNonNull(after);
 return (T t) -> { accept(t); after.accept(t); };
}
备注: java.util.Objects requireNonNull 静态⽅法将会在参数为 null 时主动抛出
NullPointerException 异常。这省去了重复编写 if 语句和抛出空指针异常的麻烦。
要想实现组合,需要两个或多个 Lambda 表达式即可,⽽ andThen 的语义正是 ⼀步接⼀步 操作。例如两个步骤组合的情况:
import java.util.function.Consumer;
public class Demo10ConsumerAndThen {
 private static void consumeString(Consumer<String> one, Consumer<String> two) {
 one.andThen(two).accept("Hello");
 }
 public static void main(String[] args) {
 consumeString(
 s -> System.out.println(s.toUpperCase()),
 s -> System.out.println(s.toLowerCase()));
 }
}
运⾏结果将会⾸先打印完全⼤写的 HELLO ,然后打印完全⼩写的 hello 。当然,通过链式写法可以实现更多步骤的组合。

3.4 练习:格式化打印信息

题⽬
下⾯的字符串数组当中存有多条信息,请按照格式 姓名 :XX 。性别 :XX 的格式将信息打印出来。要求将打印姓名的动作作为第⼀个 Consumer 接⼝的 Lambda 实例,将打印性别的动作作为第⼆个 Consumer 接⼝ Lambda 实例,将两个 Consumer 接⼝按照顺序 拼接 到⼀起。
public static void main(String[] args) {
 String[] array = { "迪丽热巴,⼥", "古⼒娜扎,⼥", "⻢尔扎哈,男" };
}
解答
import java.util.function.Consumer;
public class DemoConsumer {
 public static void main(String[] args) {
 String[] array = { "迪丽热巴,⼥", "古⼒娜扎,⼥", "⻢尔扎哈,男" };
 printInfo(s -> System.out.print("姓名:" + s.split(",")[0]),
 s -> System.out.println("。性别:" + s.split(",")[1] + "。"),
 array);
 }
 private static void printInfo(Consumer<String> one, Consumer<String> two,
String[] array) {
 for (String info : array) {
 one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:⼥。
 }
 }
}

3.5 Predicate接⼝

有时候我们需要对某种类型的数据进⾏判断,从⽽得到⼀个 boolean 值结果。这时可以
使 java.util.function.Predicate<T> 接⼝。
抽象⽅法: test
Predicate 接⼝中包含⼀个抽象⽅法: boolean test(T t) 。⽤于条件判断的场景:
import java.util.function.Predicate;
public class Demo15PredicateTest {
 private static void method(Predicate<String> predicate) {
 boolean veryLong = predicate.test("HelloWorld");
 System.out.println("字符串很⻓吗:" + veryLong);
 }
 
 public static void main(String[] args) {
 method(s -> s.length() > 5);
 }
}
条件判断的标准是传⼊的 Lambda 表达式逻辑,只要字符串⻓度⼤于 5 则认为很⻓。
默认⽅法: and
既然是条件判断,就会存在与、或、⾮三种常⻅的逻辑关系。其中将两个 Predicate 条件使⽤ 逻辑连接起来实现 并且 的效果时,可以使⽤ default ⽅法 and 。其 JDK 源码为:
default Predicate<T> and(Predicate<? super T> other) {
 Objects.requireNonNull(other);
 return (t) -> test(t) && other.test(t);
}
如果要判断⼀个字符串既要包含⼤写 “H” ,⼜要包含⼤写 “W” ,那么:
import java.util.function.Predicate;
public class Demo16PredicateAnd {
 private static void method(Predicate<String> one, Predicate<String> two) {
 boolean isValid = one.and(two).test("Helloworld");
 System.out.println("字符串符合要求吗:" + isValid);
 }
 
 public static void main(String[] args) {
 method(s -> s.contains("H"), s -> s.contains("W"));
 }
}
默认⽅法: or
and 类似,默认⽅法 or 实现逻辑关系中的 JDK 源码为:
default Predicate<T> or(Predicate<? super T> other) {
 Objects.requireNonNull(other);
 return (t) -> test(t) || other.test(t);
}
如果希望实现逻辑 字符串包含⼤写 H 或者包含⼤写 W” ,那么代码只需要将 “and” 修改为 “or” 名称即可,其他都不变:
import java.util.function.Predicate;
public class Demo16PredicateAnd {
 private static void method(Predicate<String> one, Predicate<String> two) {
 boolean isValid = one.or(two).test("Helloworld");
 System.out.println("字符串符合要求吗:" + isValid);
 }
 
 public static void main(String[] args) {
 method(s -> s.contains("H"), s -> s.contains("W"));
 }
}
默认⽅法: negate
已经了解了,剩下的 (取反)也会简单。默认⽅法 negate JDK 源代码为:
default Predicate<T> negate() {
 return (t) -> !test(t);
}
从实现中很容易看出,它是执⾏了 test ⽅法之后,对结果 boolean 值进⾏ “!” 取反⽽已。⼀定要在 test ⽅法调⽤之前调⽤ negate ⽅法,正如 and or ⽅法⼀样:
import java.util.function.Predicate;
public class Demo17PredicateNegate {
 private static void method(Predicate<String> predicate) {
 boolean veryLong = predicate.negate().test("HelloWorld");
 System.out.println("字符串很⻓吗:" + veryLong);
 }
 
 public static void main(String[] args) {
 method(s -> s.length() < 5);
 }
}
3.6 练习:集合信息筛选
题⽬
数组当中有多条 姓名 + 性别 的信息如下,请通过 Predicate 接⼝的拼装将符合要求的字符串筛选到集合 ArrayList 中,需要同时满⾜两个条件:
1. 必须为⼥⽣;
2. 姓名为 4 个字。
public class DemoPredicate {
 public static void main(String[] args) {
 String[] array = { "迪丽热巴,⼥", "古⼒娜扎,⼥", "⻢尔扎哈,男", "赵丽颖,⼥" };
 }
}
解答
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class DemoPredicate {
 public static void main(String[] args) {
 String[] array = { "迪丽热巴,⼥", "古⼒娜扎,⼥", "⻢尔扎哈,男", "赵丽颖,⼥" };
 List<String> list = filter(array,
 s -> "⼥".equals(s.split(",")[1]),
 s -> s.split(",")[0].length() == 4);
 System.out.println(list);
 }
 private static List<String> filter(String[] array, Predicate<String> one,
 Predicate<String> two) {
 List<String> list = new ArrayList<>();
for (String info : array) {
 if (one.and(two).test(info)) {
 list.add(info);
 }
 }
 return list;
 }
}

3.7 Function接⼝

java.util.function.Function<T,R> 接⼝⽤来根据⼀个类型的数据得到另⼀个类型的数据,前者称为
前置条件,后者称为后置条件。
抽象⽅法: apply
Function 接⼝中最主要的抽象⽅法为: R apply(T t) ,根据类型 T 的参数获取类型 R 的结果。
使⽤的场景例如:将 String 类型转换为 Integer 类型。
import java.util.function.Function;
public class Demo11FunctionApply {
 private static void method(Function<String, Integer> function) {
 int num = function.apply("10");
 System.out.println(num + 20);
 }
 
 public static void main(String[] args) {
 method(s -> Integer.parseInt(s));
 }
}
当然,最好是通过⽅法引⽤的写法。
默认⽅法: andThen
Function 接⼝中有⼀个默认的 andThen ⽅法,⽤来进⾏组合操作。 JDK 源代码如:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
 Objects.requireNonNull(after);
 return (T t) -> after.apply(apply(t));
}
该⽅法同样⽤于 先做什么,再做什么 的场景,和 Consumer 中的 andThen 差不多:
import java.util.function.Function;
public class Demo12FunctionAndThen {
 private static void method(Function<String, Integer> one, Function<Integer,
Integer> two) {
 int num = one.andThen(two).apply("10");
 System.out.println(num + 20);
 }
 
 public static void main(String[] args) {
 method(str->Integer.parseInt(str)+10, i -> i *= 10);
 }
}
第⼀个操作是将字符串解析成为 int 数字,第⼆个操作是乘以 10 。两个操作通过 andThen 按照前后顺序组合到了⼀起。
请注意, Function 的前置条件泛型和后置条件泛型可以相同。

3.8 练习:⾃定义函数模型拼接

题⽬
请使⽤ Function 进⾏函数模型的拼接,按照顺序需要执⾏的多个函数操作为:
String str = " 赵丽颖 , 20";
1. 将字符串截取数字年龄部分,得到字符串;
2. 将上⼀步的字符串转换成为 int 类型的数字;
3. 将上⼀步的 int 数字累加 100 ,得到结果 int 数字。
解答
import java.util.function.Function;
public class DemoFunction {
 public static void main(String[] args) {
 String str = "赵丽颖, 20";
 int age = getAgeNum(str, s -> s.split(",")[1],
 s ->Integer.parseInt(s),
 n -> n += 100);
 System.out.println(age);
 }
 private static int getAgeNum(String str, Function<String, String> one,
 Function<String, Integer> two,
 Function<Integer, Integer> three) {
 return one.andThen(two).andThen(three).apply(str);
 }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sion. Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值