一. Lambda表达式
1. 函数式编程思想概述
在数学中函数就是有输入量 , 输出量的一套计算方案, 也就是拿什么东西做什么事情. 相对而言, 面向对象过分强调"必须通过对象的形式来做事情", 而函数式 思想则尽量忽略面向对象的复杂语法, 更加强调做什么,而不是以什么形式做
面向对象的思想 :
做一件事情, 找一个能解决这个事情的对象, 调用对象的方法, 完成事情.
函数式编程思想 :
只要能获取到结果, 谁去做的, 怎么做的都不重要. 重视的是结果, 不重视过程
2. 冗余的Runnable代码
当需要启动一个线程去完成任务时, 通常会通过java.lang.Runnable
接口来定义任务内容, 并使用java.lang.Thread
类来启动该线程. 代码如下 :
public static RunnableTest {
public static void main(String[] args) {
Runnable task = new Runnable() {
@Override
public void run() {//重写抽象方法
System.out.println("多线程任务执行!");
}
};
new Thread(task).start();//启动线程
//体验Lambda表达式
new Thread(() -> System.out.println(Thread.currentThread().getName()+"执行了")).start();
}
}
本着面向对象的思想, 先创建一个Runnable接口的匿名内部类对象, 来指定任务内容, 在将其交给一个线程来启动
代码分析
对于Runnable
的匿名内部类用法, 可以分析出几点内容 :
-
Thread
类需要Runnbale
接口作为参数, 其中抽象run
方法是用来指定线程任务内容的核心. - 为了指定
run
的方法体, 不得不需要Runnable
接口的实现类 - 为了省去定义一个
RunnableImpl
实现类的麻烦, 不得不用匿名内部类 - 必须覆盖重写
抽象run
方法, 所以方法名称, 方法参数, 方法返回值不得不再重写一遍, 且不能写错. - 而实际上, 似乎只有方法体才是关键所在
3. 编程思想转换
3.1 做什么, 而不是怎么做
我们真的希望创建一个匿名内部类对象吗 ? 不, 我们只是为了做这件事情 而不得不创建一个对象, 我们真正希望做的是 : 将run方法体内的代码传递给Thread类知晓.
传递一段代码---------这才是我们真正的目的, 而创建对象只是受限于面向对象语法而不得不采取的一种手段方式, 那有没有更简单的办法呢 ? 如果我们将关注点从 " 怎么做 " 回归到" 做什么 "的本质上, 就会发现只要能够更好地达到目的, 过程与形式其实并不重要.
JDK8
以后, Java中加入了==Lambda表达式==的重量级新特性, 为我们打开了新世界的大门
3.2 体验Lambda的更优写法
借助Java 8 的全新语法, 上述Runnable
接口的匿名内部类写法可以通过更简单的Lambda表达式来达到等效 :
public class LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多线程任务执行!")).start();//启动线程
}
}
这段代码和刚才执行的效果是完全一样的, 可以在JDK8
以上的版本下编译通过, 从代码语义可以看出, 我们启动了一个新线程, 而线程任务的内容以一种更加简洁的形式被指定
这让我们不再有不得不创建接口对象的束缚, 不再有抽象方法覆盖重写的负担 !
3.3 Lambda表达式标准格式
由三部分组成:
- 一些参数
- 一个箭头
- 一段代码
格式:
( 参数类型 参数名称 ) -> {
方法体;
return 返回值;
}
解释说明格式:
- 小括号中的参数和之前方法的参数写法一样,可以写任意个参数,如果多个参数,要使用逗号隔开。
- ->是一个运算符,表示指向性动作。
- 大括号中的方法体以及return返回值的写法和之前方法的大括号中的写法一样。
Lambda表达式是函数式编程思想
函数式编程: 可推导, 就是可省略
因为在Thread构造方法中需要Runnable类型的参数, 所以可以省略new Runnable
因为Runnable中只有一个抽象方法run, 所以重写的必然是这个run方法, 所以可以省略run方法的声明部分.
匿名内部类与Lambda对比
//匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程执行!");
}
}).start();
----------------------------------------------
//Lambda
() -> System.out.println("多线程任务执行!")
仔细分析该代码中, Runnable 接口只有一个run 方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个方法):
- 无参数:不需要任何条件即可执行该方案。
- 无返回值:该方案不产生任何结果。
- 代码块(方法体):该方案的具体执行步骤。
同样的语义体现在Lambda 语法中,要更加简单
Lambda :
- 前面的一对小括号即run 方法的参数(无),代表不需要任何条件;
- 中间的一个箭头代表将前面的参数传递给后面的代码;
- 后面的输出语句即业务逻辑代码。
3.4 参数和返回值
下面演示java.util.Comparator<T>
接口的使用场景代码, 其中抽象方法定义为 :
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时, Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则, 假设有一个Person
类, 含有String name
和int age
两个成员变量 :
public class Person {
private String name;
private int age;
//省略构造器, toString和get set方法
}
以前学的写法 :
//使用传统代码对Person[] 数组进行排序
public class Demo05Comparator {
public static void main(String[] args) {
// 本来年龄乱序的对象数组
Person[] array = { new Person("古力娜扎", 19), new Person("迪丽热巴",
18), new Person("马尔扎哈", 20) };
// 匿名内部类
Comparator<Person> comp = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
};
Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例
for (Person person : array) {
System.out.println(person);
}
}
}
只有参数和方法体才是关键, 其他都是多余的
Lambda写法 :
public class Demo06ComparatorLambda {
public static void main(String[] args) {
Person[] array = {
new Person("古力娜扎", 19),
new Person("迪丽热巴", 18),
new Person("马尔扎哈", 20) };
Arrays.sort(array, (Person a, Person b) -> {
return a.getAge() - b.getAge();
});
for (Person person : array) {
System.out.println(person);
}
}
}
3. 5 Lambda表达式省略格式
省略规则 :
在Lambda标准格式的基础上, 使用省略写法的规则为 :
- 小括号内参数的类型可以省略
- 如果小括号内有且仅有一个参, 则小括号可以省略
- 如果大括号内有且仅有一个语句, 则无论是否有返回值, 都可以省略大括号, return关键字及语句分号
原则就是 : 可推导出来的就可以省略
Lambda强调的是"做什么"而不是"怎么做", 所以凡是可以推导得出的信息都可以省略, 例如上例还可以使用Lambda的省略写法.
//Runnable接口简化:
() -> System.out.println("多线程任务执行!");
//Comparator接口简化:
Arrays.sort(array, (a, b)) -> a.getAge() - b.getAge());
4. Lambda的前提条件
- 使用Lambda必须具有接口, 且要求接口中有且仅有一个抽象方法, 无论是JDK内置的Runnable, Comparator接口还是自定义的接口, 只有当接口中的抽象方法存在且唯一时, 才可以使用Lambda.
- 使用Lambda必须具有接口作为方法参数, 也就是方法的参数或局部变量必须为Lambda对应的接口类型, 才能使用Lambda作为该接口的实例.
- 必须支持上下文推导,要能够推导出来Lambda表达式表示的是哪个接口中的内容。 可以使用接口当做参数,然后传递Lambda表达式(常用) 也可以将Lambda表达式赋值给一个接口类型的变量。
函数式接口.
public class Demo05BeforeLambda {
//使用接口当做参数
public static void method(MyInterface m) {//m = s -> System.out.println(s)
m.printStr("HELLO");
}
public static void main(String[] args) {
//使用接口当做参数,然后传递Lambda表达式。
//method(s -> System.out.println(s));
//使用匿名内部类方式创建对象
/*
MyInterface m = new MyInterface() {
@Override
public void printStr(String str) {
System.out.println(str);
}
};
*/
MyInterface m = str -> System.out.println(str);
m.printStr("Hello");
}
}
二. 函数式接口
1. 概述
函数式接口在Java中是指, 有且仅有一个抽象方法的接口.
函数式接口, 适用于函数式编程场景的接口, 而Java中的函数式编程体现就是Lambda, 所以函数式接口就是可以适用于Lambda使用的接口, 只有确保接口中有且仅有一个抽象方法, Java中的Lambda才能顺利的进行推导.
格式 :
只要确保接口中有且仅有一个抽象方法即可 :
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名 (参数列表);
//其他非抽象方法的内容
}
由于接口当中的抽象方法public abstract 是可以省略的, 所以定义一个函数式接口很简单,
public interface MyFunctionalInterface {
void myMethod();
}
2. 自定义函数式接口
对于刚刚定义好的MyFunctionalInterface
函数式接口, 典型的引用长久就是作为方法的参数 :
public class FunctionalInterfaceTest {
//使用自定义的函数式接口作为方法参数
private static void doSomething (MyFunctionalInterface inter) {
inter.myMethod();//调用自定义的函数式接口方法
}
public static void main(String[] args) {
//调用使用函数式接口的方法
doSomething (() -> System.out.println("Lambda执行啦"));
}
}
3. 接口注解
与@Override
注解的作用类似, Java8中专门为函数式接口引入了一个新的注解: @FunctionalInterface
. 该注解可用于一个接口的定义上 :
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
一旦使用该注解来定义接口, 编译器会强制检查该接口是否确实有且仅有一个抽象方法, 否则会报错. 不过, 就算不使用它, 只要满足函数式接口的定义, 这仍然是一个函数式接口, 使用起来是一样的.
4. 常用的函数式接口
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景, 他们主要在java.util.function
包中被提供, 以下介绍两个常用的函数式接口.
- Consumer接口
- Predicate接口
4.1 Consumer接口
在JDK8
的时候,提供java.util.function
包,这个包下有大量的函数式接口。
其中有一个接口叫做Consumer,这个接口可以看成一个消费者,可以去消费(使用)一个数据。
抽象方法:
void accept(T t): 对参数t进行使用
public class Demo01Consumer {
//定义方法,使用函数式接口Consumer当做方法参数
public static void method(Consumer<String> c) {
//调用accept方法,消费使用一个数据。
c.accept("hello");
}
public static void main(String[] args) {
//调用method方法
method(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//调用method方法,传递Lambda表达式。
method(s -> System.out.println(s));
}
}
4.2 Predicate接口
有时候我们需要对某种类型的数据进行判断, 从而得到一个boolean值结果, 这时候可以使用java.util.function.Predicate<T>
接口.
这个函数式接口可以对一个数据进行判断,判断是否符合要求抽象方法:
| 修饰符 | 返回值 | 方法名 | 参数列表 | 作用 | | --------------- | ------- | ------ | -------- | ------------ | | public abstract | boolean | test
| ( T t ) | 用于条件判断 |
判断参数t是否符合规则,如果符合规则返回true。
//1. 练习: 判断字符串长度是否大于5
//2. 练习: 判断字符串是否包含"H"
public class PredicateTest {
private static void method(Predicate<String> predicate, String str) {
boolean veryLong = predicate.test(str);
System.out.println("字符串很长吗" + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() > 5, "HelloWorld");
}
}
条件判断的标准是传入的Lambda表达式逻辑, 只要字符串长度大于5就认为很长.
三. Stream流
在Java8
中, 得益于Lambda所带来的函数式编程, 引入了一个全新的Stream概念, 用于解决已有集合类库既有的弊端.
1. Stream流的初体验
package cn.itcast.demo03_stream;
import java.util.ArrayList;
import java.util.List;
/*
要求:
1. 首先筛选所有姓张的人;
2. 然后筛选名字有三个字的人;
3. 最后进行对结果进行打印输出。
*/
@SuppressWarnings("all")
public class Demo01PrintList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
//1. 首先筛选所有姓张的人;
List<String> zhangList = new ArrayList<>();
for (String s : list) {
if(s.startsWith("张")) {
zhangList.add(s);
}
}
//2. 然后筛选名字有三个字的人;
List<String> threeList = new ArrayList<>();
for (String s : zhangList) {
if (s.length() == 3) {
threeList.add(s);
}
}
//3. 最后进行对结果进行打印输出。
for (String s : threeList) {
System.out.println(s);
}
System.out.println("=====================================");
//Stream流的初体验
list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(s -> System.out.println(s));
}
}
2. 获取流的方式
2.1 单列集合获取流的方式
在Java中,Stream表示流,Stream是一个接口类型,后面我们使用的流都是Stream这个接口的实现类。获取Stream流的两种方式:
- 通过Collection中的方法获取流(单列集合获取流的方式)
- 通过数组获取流
如果要通过单列集合获取流,那么可以调用集合的stream方法。
Stream stream():获取集合对应的流
// Stream<String> stream = list.stream();
public class Demo02CollectionGetStream {
public static void main(String[] args) {
//创建单列集合
List<String> list = new ArrayList<>();
//向集合添加元素
list.add("aa");
list.add("bb");
list.add("cc");
//调用集合的stream方法,获取流
Stream<String> stream = list.stream();
//将流调用toArray方法转成数组,再借助数组的工具类将数组的内容打印。
System.out.println(Arrays.toString(stream.toArray()));
}
}
2.2 通过数组获取流
如果要通过数组获取Stream流,有两种方式 :
- 静态方法of获取(记住)
static <T> Stream<T> of(T... values)
: 根据数组或多个元素获取流。 - 静态方法stream获取
static <T> Stream<T> stream(T[] array)
: 根据数组获取流。
public class Demo03ArrayGetStream {
public static void main(String[] args) {
//1. 通过Stream中的静态方法of获取
String[] strArr = {"aa", "bb", "cc"};
//static <T> Stream<T> of(T... values): 根据数组或多个元素获取流。
//Stream<String> stream = Stream.of(strArr);
Stream<String> stream = Stream.of("hello", "java", "world");
//将流进行输出
System.out.println(Arrays.toString(stream.toArray()));
//2. 通过Arrays工具类中的静态方法stream获取
Stream<String> stream2 = Arrays.stream(strArr);
//将流输出
System.out.println(Arrays.toString(stream2.toArray()));//[aa, bb, cc]
}
}
3. Stream中的方法
流模型的操作很丰富,这里介绍一些常用的API。
这些方法可以被分成两种:
- 终结方法:返回值类型不再是Stream 接口自身类型的方法,因此不再支持类似
StringBuilder
那样的链式调 用。本小节中,终结方法包括count
和forEach
方法。 - 非终结方法:返回值类型仍然是Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
以下方法, 方法种类为终结的方法不再支持链式调用.
| 修饰符 | 返回值 | 方法名 | 参数列表 | 作用 | 方法种类 | | ------------- | --------- | --------- | -------------------------------------------------- | ---------- | -------- | | public | void | forEach
| ( Consumer<? 1 super T> action
) | 逐一处理 | 终结 | | public | Stream | filter
| (Predicate<? super T> predicate
) | 过滤 | 终结 | | public | long | count
| ( ) | 统计个数 | 函数拼接 | | public | Stream | limit
| ( long maxSize
) | 取用前几个 | 函数拼接 | | public | Stream | skip
| ( long n
) | 跳过前几个 | 函数拼接 | | public static | Stream | concat
| ( Stream<? extends T> a, Stream<? extends T> b
) | 组合 | 函数拼接 |
3.1 forEach / forEachOrdered
forEach与增强 for不同, 该方法并不能保证元素的逐一消费动作在流中是被有效执行的
该方法接收一个Consumer
接口函数, 会将每一个流元素交给该函数进行处理.
package drafts.drafts2;
import java.util.HashSet;
import java.util.Set;
public class Drafts2 {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("捣乱黄");
set.add("捣乱靛");
set.add("捣乱灰");
set.add("捣乱红");
set.add("捣乱王");
set.stream().forEach((str) -> System.out.println(str));
}
}
捣乱靛
捣乱王
捣乱黄
捣乱红
捣乱灰
3.2 filter 过滤
可以通过filter
方法将一个流转换成另一个子集流.
该接口接收一个Predicate
函数式接口, 作为筛选条件.
package drafts.drafts3;
import java.util.stream.Stream;
public class Draft3 {
public static void main(String[] args) {
Stream<String> stream = Stream.of("孙悟空", "哪吒", "邋遢大王", "捣乱黄", "捣乱绿", "捣乱黑");
stream
.filter((str) -> str.length() == 3)
.forEach((str) -> System.out.println(str));
}
}
孙悟空
捣乱黄
捣乱绿
捣乱黑
3.3 统计个数
流中提供了count
方法来数一数其中元素的个数, 该方法是终结方法.
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
public class Drafts4 {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap();
map.put("张三", 23);
map.put("李四", 25);
map.put("王五", 25);
map.put("赵六", 26);
Set<Map.Entry<String, Integer>> set = map.entrySet();
Stream<Map.Entry<String, Integer>> stream = set.stream();
/* Stream<Map.Entry<String, Integer>> result =
stream //可以省略数据类型
.filter((Map.Entry<String, Integer> e) -> e.getValue() == 25);
System.out.println(result.count());
` */
System.out.println(stream.filter((Map.Entry<String, Integer> e) -> e.getValue() == 25).count());
}
}
3.4 取用 / 跳过
limit
方法可以对流进行截取, 得到只取用前n个元素的流
skip
方法可以跳过前几个元素, 得到截取后的流
package drafts.drafts5;
import java.util.stream.Stream;
public class Drafts5 {
public static void main(String[] args) {
Stream<Object> stream = Stream.of("捣乱王", "捣乱黑", "捣乱绿", "捣乱红");
stream.skip(1).limit(2).forEach((s) -> System.out.println(s));
}
}
捣乱黑
捣乱绿
3.5 concat 组合
如果有两个流, 希望合为一个流, 那么可以使用Stream
接口的==静态方法==concat
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
: 把参数列表中的 两个Stream流对象a和b,合并成一个新的Stream流对象
package drafts.drafts6;
import java.util.stream.Stream;
public class Drafts6 {
public static void main(String[] args) {
Stream<String> stream = Stream.of("捣乱王");
Stream<String> stream1 = Stream.of("捣乱绿");
Stream.concat(stream, stream1).forEach((s) -> System.out.println(s));
}
}
捣乱王
捣乱绿
Stream 综合案例
现在有两个ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下 若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;
- 第一个队伍筛选之后只要前3个人;
- 第二个队伍只要姓张的成员姓名;
- 第二个队伍筛选之后不要前2个人;
- 将两个队伍合并为一个队伍;
- 打印整个队伍的姓名信息。
两个队伍(集合)的代码如下:
public class Demo21ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("老子");
one.add("庄子");
one.add("孙子");
one.add("洪七公");
List<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("张三丰");
two.add("赵丽颖");
two.add("张二狗");
two.add("张天爱");
two.add("张三");
// ....
Stream<String> streamOne =
one.stream().filter(s -> s.length() == 3).limit(3);
Stream<String> streamTwo =
two.stream().filter(s ->s.startsWith("张")).skip(2);
Stream.concat(s1, s2).forEach((s) -> System.out.println(s));
}
}