【JavaSE 第二十二天】
一、Junit (单元测试)
1. 学习目标
- 了解 Junit 的概述
- 掌握 Junit 的使用
2. 内容讲解
(1) Junit 的含义:
Junit 是 Java 语言编写的第三方单元测试框架
(2) 单元测试概念:
- 单元:在 Java 中,一个类就是一个单元
- 单元测试:程序员编写的一小段代码,用来对某个类中的某个方法进行功能测试或业务逻辑测试。
(3) Junit 单元测试框架的作用
作用:用来对类中的方法功能进行有目的的测试,以保证程序的正确性和稳定性,能够让方法独立运行起来。
(4) Junit单元测试框架的使用步骤
在模块中导入 jar 包,并关联到库,即可使用单元测试。(即使不写 main 方法,仍然可以运行)
①编写业务类,在业务类中编写业务方法。比如增删改查的方法
②编写测试类,在测试类中编写测试方法,在测试方法中编写测试代码来测试。
(测试类的命名规范:以 Test 开头,以业务类类名结尾,使用驼峰命名法,每一个单词首字母大写,称为大驼峰命名法,比如类名,接口名…从第二单词开始首字母大写,称为小驼峰命名法,比如方法命名)
Ⅰ、比如业务类类名:ProductDao,那么测试类类名就应该叫:TestProductDao ,测试方法的命名规则:以 test 开头,以业务方法名结尾
Ⅱ、 比如业务方法名为:save,那么测试方法名就应该叫:testSave
(5)使用入门级 Junit :
- 测试方法注意事项
- 必须是 public 修饰的,没有返回值,没有参数
- 必须使注解
@Test
修饰
- 运行测试方法
- 选中方法名 → 右键 → Run ‘测试方法名’ 运行选中的测试方法
- 选中测试类类名 → 右键 → Run ‘测试类类名’ 运行测试类中所有测试方法
- 选中模块名 → 右键 → Run ‘All Tests’ 运行模块中的所有测试类的所有测试方法
- 查看测试结果
- 绿色:表示测试通过
- 红色:表示测试失败,有问题
简单使用:
import org.junit.Test;
/**
* 使用单元测试:
* 1. 单元测试方法的要求:
* (1)必须是 public
* (2)必须是 void
* (3)必须没有参数
* (4)必须添加 @Test
*/
public class TestJunit {
@Test
public void test01(){
System.out.println("你好,单元测试!");
}
@Test
public void test02(){
int num=10/2;
System.out.println("num = " + num);
}
}
(6) Junit 常用注解( Junit4.xxxx 版本)(当前使用较多)
@Before
:用来修饰方法,该方法会在每一个测试方法执行之前执行一次。@After
:用来修饰方法,该方法会在每一个测试方法执行之后执行一次。@BeforeClass
:用来静态修饰方法,该方法会在所有测试方法之前执行一次。@AfterClass
:用来静态修饰方法,该方法会在所有测试方法之后执行一次。
public class TestJunit {
@Before // Before 注解添加在提前执行的方法上
public void init(){
System.out.println("在这个方法里面通常会做一些全局的初始化工作。。。");
}
@Test // Test 注解添加在要执行的单元测试方法上
public void test01(){
System.out.println("你好,单元测试!");
}
@Test
public void test02(){
int num=10/2;
System.out.println("num = " + num);
}
@After // After 注解添加在测试后执行的方法上
public void destroy(){
System.out.println("在这个方法里面通常会做一些全局的销毁、资源回收等等工作。。。");
}
}
(7) Junit 常用注解( Junit5.xxxx 版本)
@BeforeEach
:用来修饰方法,该方法会在每一个测试方法执行之前执行一次。@AfterEach
:用来修饰方法,该方法会在每一个测试方法执行之后执行一次。@BeforeAll
:用来静态修饰方法,该方法会在所有测试方法之前执行一次。@AfterAll
:用来静态修饰方法,该方法会在所有测试方法之后执行一次。
(8) Junit 的使用
示例代码:
public class Cacluate {
/**
* 业务方法1:求 a 和 b 之和
*/
public int sum(int a,int b){
return a + b;
}
/**
* 业务方法2:求 a 和 b 之差
*/
public int reduce(int a,int b){
return a - b;
}
}
public class TestJunit {
@Test
public void test03(){
// 判断加法运算是否符合说明
Cacluate cacluate=new Cacluate();
int sum = cacluate.sum(11,55);
// 判断运算结果是否符合预期
// 单元测试的断言
Assert.assertEquals(66,sum);
}
@Test
public void test04(){
Cacluate cacluate=new Cacluate();
int reduce=cacluate.reduce(77,11);
Assert.assertEquals(66,reduce);
}
}
/**
* 业务类:实现加减乘除运算
*/
public class Cacluate {
/**
* 业务方法1:求 a 和 b 之和
*/
public int sum(int a,int b){
return a + b + 10;
}
/**
* 业务方法2:求 a 和 b 之差
*/
public int sub(int a,int b){
return a - b;
}
}
public class TestCacluate {
static Cacluate c = null;
@BeforeClass // 用来静态修饰方法,该方法会在所有测试方法之前执行一次。
public static void init(){
System.out.println("初始化操作");
// 创建Cacluate对象
c = new Cacluate();
}
@AfterClass // 用来静态修饰方法,该方法会在所有测试方法之后执行一次。
public static void close(){
System.out.println("释放资源");
c = null;
}
@Test
public void testSum(){
int result = c.sum(1,1);
/**
* 断言:预习判断某个条件一定成立,如果条件不成立,则直接崩溃。
* assertEquals方法的参数
* (String message, double expected, double actual)
* message: 消息字符串
* expected: 期望值
* actual: 实际值
*/
// 如果期望值和实际值一致,则什么也不发生,否则会直接崩溃。
Assert.assertEquals("期望值和实际值不一致",12,result);
System.out.println(result);
}
@Test
public void testSub(){
// 创建Cacluate对象
// Cacluate c = new Cacluate();
int result = c.sub(1,1);
// 如果期望值和实际值一致,则什么也不发生,否则会直接崩溃。
Assert.assertEquals("期望值和实际值不一致",0,result);
System.out.println(result);
}
}
二、Lambda 表达式
1. 学习目标
- 能够理解函数式编程相对于面向对象的优点
- 能够掌握 Lambda 表达式的标准格式
- 能够使用 Lambda 标准格式
- 能够掌握 Lambda 表达式的省略格式与规则
- 能够通过 Lambda 使用自定义的接口(有且仅有一个抽象方法)
- 能够使用 Supplier 函数式接口
- 能够使用 Consumer 函数式接口
- 能够使用 Function 函数式接口
- 能够使用 Predicate 函数式接口
- 能够使用方法引用和构造器引用
2. 内容讲解
(1) 函数式编程的优势
① 函数式编程思想
在数学中,函数就是有输入量、输出量的一套计算方案,也就是 “拿什么东西做什么事情”。
编程中的函数,也有类似的概念,调用某个东西的时候,用实参为形参赋值,然后通过运行方法体,返回一个结果。对于调用者来做,关注这个方法具备什么样的功能。相对而言,面向对象过分强调 “必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
这种函数式编程思想依赖倒置性思想。
-
面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情
-
函数式编程思想:只要能获取到结果,谁去做的不重要,重视的是结果,不重视过程
Java8 引入了 Lambda 表达式之后,Java 也开始支持函数式编程。
- Lambda 表达式不是 Java 最早使用的,很多语言就支持 Lambda 表达式,例如:C++,C#,Python,Scala 等。如果有 Python 或者 Javascript 的语言基础,对理解 Lambda 表达式有很大帮助,可以这么说 Lambda 表达式其实就是实现 SAM 接口的语法糖,使得 Java 也算是支持函数式编程的语言。Lambda 的合理使用可以极大的减少代码冗余,同时可读性也好过冗长的匿名内部类。
备注:“语法糖” 是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的
for-each
语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java 中的 Lambda 可以被当做是匿名内部类的 “语法糖”,但是二者在原理上是不同的。
② 冗余的匿名内部类
当需要启动一个线程去完成任务时,通常会通过
java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。
示例代码如下:
public class R implements Runnable{
@Override
public void run() {
System.out.println("线程的做法!");
}
}
import org.junit.Test;
public class TestLambda {
@Test
public void testStart01(){
R r=new R();
// 线程的做法
new Thread(r).start();
}
@Test
public void testStart02(){
// 使用匿名内部类可以不用再写一个类,但是只能使用一次,针对只使用一次的类较好
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类的做法!");
}
}).start();
}
}
代码分析:
本着 “一切皆对象” 的思想,这种做法是无可厚非的:首先创建一个
Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
对于
Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;- 为了指定
run
的方法体,不得不需要Runnable
接口的实现类; - 为了省去定义一个
RunnableImpl
实现类的麻烦,不得不使用匿名内部类; - 必须覆盖重写抽象
run
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错; - 而实际上,似乎只有方法体才是关键所在。
③ 编程思想转换
重点转为:做什么,而不是谁来做,怎么做
创建一个匿名内部类对象只是为了做这件事情而不得不进行的操作。
我们真正希望做的事情是:将run
方法体内的代码传递给Thread
类知晓。
- 传递一段代码——这才是我们真正的目的。
- 而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。
- 2014年3月 Oracle 所发布的 Java 8(JDK 1.8)中,加入了 Lambda 表达式的重量级新特性。
④ Lambda 的更优写法:
借助 Java 8 的全新语法,上述
Runnable
接口的匿名内部类写法可以通过更简单的 Lambda 表达式达到等效:
一个接口中只有一个方法才能使用这种 Lambda 的更优写法,这样就用不着创建匿名内部类。
import org.junit.Test;
public class TestLambda {
@Test
public void testStart(){
new Thread(()->{ // 如果需要输入信息,即在小括号中写入信息
System.out.println("函数式编程!");
}).start();
}
}
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
这段代码和刚才的执行效果是完全一样的,可以在 JDK1.8 或更高的编译级别下通过。
从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
- 不用创建接口对象
- 不用抽象方法覆盖重写
(2) 函数式接口
Lambda 表达式其实就是实现 SAM 接口的语法糖,所谓 SAM 接口就是 Single Abstract Method ,即该接口中只有一个抽象方法需要实现,当然该接口可以包含其他非抽象方法。
其实只要满足 “SAM” 特征的接口都可以称为函数式接口,都可以使用 Lambda 表达式,但是如果要更明确一点,最好在声明接口时,加上 @FunctionalInterface 。一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。
之前学过的 SAM 接口中,标记了 @FunctionalInterface 的函数式接口的有:Runnable,Comparator,FileFilter。
Java 8 在 java.util.function 新增了很多函数式接口:主要分为四大类,消费型、供给型、判断型、功能型。基本可以满足我们的开发需求。当然你也可以定义自己的函数式接口。
① 自定义函数式接口
只要确保接口中有且仅有一个抽象方法即可
写法:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
其他非抽象方法内容
}
接口当中抽象方法的 public abstract 是可以省略的
例如:声明一个计算器 Calculator
接口,内含抽象方法 calculator()
可以对两个 int 数字进行计算,并返回结果:
public interface Calculator {
int calculator(int a,int b);
}
在测试类中,声明一个如下方法:
public static void invokeCalc(int a, int b, Calculator calculator) {
int result = calculator.calc(a, b);
System.out.println("结果是:" + result);
}
下面进行测试:三种方法
- 第一种方法:
实现接口,重写两个方法:
public class SumCalculate implements Calculator{
@Override
public int calculator(int a, int b) {
return a+b;
}
}
public class ReduceCalculate implements Calculator{
@Override
public int calculator(int a, int b) {
return a-b;
}
}
进行调用,测试返回值:
public class CalculateManager {
public static void invokeCalculate(int a,int b,Calculator calculator) {
int result = calculator.calculator(a, b);
System.out.println("result = " + result);
}
}
测试方法:
import org.junit.Test;
public class TestLambda {
@Test
public void testCalculator(){
CalculateManager.invokeCalculate(5,3,new SumCalculate());
}
}
- 第二种方法:
匿名内部类的写法,不必写实现类:
import org.junit.Test;
public class TestLambda {
@Test
public void testCalculator(){
// 匿名内部类的写法
CalculateManager.invokeCalculate(5,3, new Calculator() {
@Override
public int calculator(int a, int b) {
return a+b;
}
});
}
}
- 第三种方法:
import org.junit.Test;
public class TestLambda {
@Test
public void testCalculator(){
// Lambda 表达式的写法
CalculateManager.invokeCalculate(5,3,(int a,int b)->{return a+b;});
}
}
② 函数式接口的分类
除了我们可以自定义函数式接口之外,JDK 也给我们内置了一些函数式接口,具体分类如下:
Ⅰ、 消费型接口
- 消费型接口的抽象方法特点:有形参,但是返回值类型是void
接口名 | 抽象方法 | 描述 |
---|---|---|
Consumer | void accept(T t) | 接收一个对象用于完成功能 |
BiConsumer<T,U> | void accept(T t, U u) | 接收两个对象用于完成功能 |
DoubleConsumer | void accept(double value) | 接收一个double值 |
IntConsumer | void accept(int value) | 接收一个 int 值 |
LongConsumer | void accept(long value) | 接收一个 long 值 |
ObjDoubleConsumer | void accept(T t, double value) | 接收一个对象和一个 double 值 |
ObjIntConsumer | void accept(T t, int value) | 接收一个对象和一个 int 值 |
ObjLongConsumer | void accept(T t, long value) | 接收一个对象和一个 long 值 |
import java.util.function.Consumer;
public class LambdaManager {
public static void useConsumer(String str, Consumer<String> consumer){
consumer.accept(str);
}
}
import org.junit.Test;
public class TestLambda {
@Test
public void testConsumer(){
LambdaManager.useConsumer("消费性接口",(String s)->{
System.out.println("消费性接口的使用:"+s);
});
}
}
Ⅱ、 供给型接口
- 供给型接口的抽象方法特点:无参,但是有返回值
接口名 | 抽象方法 | 描述 |
---|---|---|
Supplier | T get() | 返回一个对象 |
BooleanSupplier | boolean getAsBoolean() | 返回一个 boolean 值 |
DoubleSupplier | double getAsDouble() | 返回一个 double 值 |
IntSupplier | int getAsInt() | 返回一个 int 值 |
LongSupplier | long getAsLong() | 返回一个 long 值 |
import java.util.function.Supplier;
public class LambdaManager {
public static void useSupplier(Supplier<String> supplier){
String result=supplier.get();
System.out.println("调用供给型接口获得到的返回值是:"+result);
}
}
import org.junit.Test;
public class TestLambda {
@Test
public void testSupplier(){
LambdaManager.useSupplier(()->{
return "供给型接口";
});
}
}
Ⅲ、 断言型接口
- 断言型接口的抽象方法特点:有参,但是返回值类型是 boolean 结果
接口名 | 抽象方法 | 描述 |
---|---|---|
Predicate | boolean test(T t) | 接收一个对象 |
BiPredicate<T,U> | boolean test(T t, U u) | 接收两个对象 |
DoublePredicate | boolean test(double value) | 接收一个 double 值 |
IntPredicate | boolean test(int value) | 接收一个 int 值 |
LongPredicate | boolean test(long value) | 接收一个 long 值 |
import java.util.function.Predicate;
public class LambdaManager {
public static void usePredicate(String name, Predicate<String> predicate){
boolean flag=predicate.test(name);
System.out.println("断言型接口:"+flag);
}
}
import org.junit.Test;
public class TestLambda {
@Test
public void testPredicate(){
LambdaManager.usePredicate("断言型接口",(String s)->{
// 判断传入的字符串是否一致
return s.equals("断言型接口");
});
}
}
Ⅳ、 功能型接口
- 功能型接口的抽象方法特点:既有参数又有返回值
接口名 | 抽象方法 | 描述 |
---|---|---|
Function<T,R> | R apply(T t) | 接收一个 T 类型对象,返回一个 R 类型对象结果 |
UnaryOperator | T apply(T t) | 接收一个 T 类型对象,返回一个 T 类型对象结果 |
DoubleFunction | R apply(double value) | 接收一个 double 值,返回一个 R 类型对象 |
IntFunction | R apply(int value) | 接收一个 int 值,返回一个 R 类型对象 |
LongFunction | R apply(long value) | 接收一个 long 值,返回一个 R 类型对象 |
ToDoubleFunction | double applyAsDouble(T value) | 接收一个 T 类型对象,返回一个 double 结果 |
ToIntFunction | int applyAsInt(T value) | 接收一个 T 类型对象,返回一个 int |
ToLongFunction | long applyAsLong(T value) | 接收一个 T 类型对象,返回一个 long 结果 |
DoubleToIntFunction | int applyAsInt(double value) | 接收一个 double 值,返回一个 int 结果 |
DoubleToLongFunction | long applyAsLong(double value) | 接收一个 double 值,返回一个 long 结果 |
IntToDoubleFunction | double applyAsDouble(int value) | 接收一个 int 值,返回一个 double 结果 |
IntToLongFunction | long applyAsLong(int value) | 接收一个 int 值,返回一个 long 结果 |
LongToDoubleFunction | double applyAsDouble(long value) | 接收一个 long 值,返回一个 double 结果 |
LongToIntFunction | int applyAsInt(long value) | 接收一个 long 值,返回一个 int 结果 |
DoubleUnaryOperator | double applyAsDouble(double operand) | 接收一个 double 值,返回一个 double 结果 |
IntUnaryOperator | int applyAsInt(int operand) | 接收一个 int 值,返回一个 int 结果 |
LongUnaryOperator | long applyAsLong(long operand) | 接收一个 long 值,返回一个 long 结果 |
BiFunction<T,U,R> | R apply(T t, U u) | 接收一个 T 类型和一个 U 类型对象,返回一个 R 类型对象结果 |
BinaryOperator | T apply(T t, T u) | 接收两个 T 类型对象,返回一个 T 类型对象结果 |
ToDoubleBiFunction<T,U> | double applyAsDouble(T t, U u) | 接收一个 T 类型和一个 U 类型对象,返回一个 double 结果 |
ToIntBiFunction<T,U> | int applyAsInt(T t, U u) | 接收一个 T 类型和一个 U 类型对象,返回一个 int 结果 |
ToLongBiFunction<T,U> | long applyAsLong(T t, U u) | 接收一个 T 类型和一个 U 类型对象,返回一个 long 结果 |
DoubleBinaryOperator | double applyAsDouble(double left, double right) | 接收两个 double 值,返回一个 double 结果 |
IntBinaryOperator | int applyAsInt(int left, int right) | 接收两个 int 值,返回一个 int 结果 |
LongBinaryOperator | long applyAsLong(long left, long right) | 接收两个 long 值,返回一个 long 结果 |
import java.util.function.Function;
public class LambdaManager {
public static void useFunction(String name, Function<String,String>function){
String result=function.apply(name);
System.out.println("功能型接口:"+result);
}
}
import org.junit.Test;
public class TestLambda {
@Test
public void testFunction(){
LambdaManager.useFunction("功能型接口",(String s)->{
s+=" + 功能型接口";
return s;
});
}
}
(3) Lambda 表达式语法及优化
Lambda 表达式是用来给【函数式接口】的变量或形参赋值用的
其实本质上,Lambda表达式是用于实现【函数式接口】的 “抽象方法”
Lambda 表达式语法格式:
(形参列表) -> {Lambda体}
说明:
(形参列表)
它就是你要赋值的函数式接口的抽象方法的形参列表,照抄{Lambda体}
就是实现这个抽象方法的方法体->
称为 Lambda 操作符(减号和大于号中间不能有空格,而且必须是英文状态下半角输入方式)
优化:Lambda 表达式可以精简
- 当
{Lambda体}
中只有一句语句时,可以省略{}
和{;}
- 当
{Lambda体}
中只有一句语句时,并且这个语句还是一个 return 语句,那么return
也可以省略,但是如果{;}
没有省略的话,return
是不能省略的 (形参列表)
的类型可以省略- 当
(形参列表)
的形参个数只有一个,那么可以把数据类型
和()
一起省略,但是形参名
不能省略 - 当
(形参列表)
是空参时,()
不能省略
示例代码:
public class TestLambdaGrammer {
@Test
public void test01(){
// 用 Lambda 表达式给 Runnable 接口的形参或变量赋值
/*
* 确定两件事,才能写好 Lambda 表达式
* (1)这个接口的抽象方法是否需要传入参数
* public void run()
* (2)这个抽象方法的实现要做什么
* 例如:我要打印 “hello lambda"
*/
Runnable r = () -> {System.out.println("hello lambda");};
}
@Test
public void test02(){
// Lambda 体省略了{;}
Runnable r = () -> System.out.println("hello lambda");
}
@Test
public void test03(){
String[] arr = {"hello","Hello","java","chai"};
// 为 arr 数组排序,但是,想要不区分大小写
/*
* public static <T> void sort(T[] a,Comparator<? super T> c)
* 这里要用 Lambda 表达式为 Comparator 类型的形参赋值
* 两件事:
* (1)这个接口的抽象方法: int compare(T o1, T o2)
* (2)这个抽象方法要做什么
* 例如:这里要对 String 类型的元素,不区分大小写的比较大小
*/
// 原样
// Arrays.sort(arr, (String o1, String o2) -> {return o1.compareToIgnoreCase(o2);});
// 省略了{return ;}
// Arrays.sort(arr, (String o1, String o2) -> o1.compareToIgnoreCase(o2));
//省略了两个String
Arrays.sort(arr, (o1, o2) -> o1.compareToIgnoreCase(o2));
for (String string : arr) {
System.out.println(string);
}
}
@Test
public void test04(){
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("java");
list.add("world");
/*
* JDK1.8 给 Collection 系列的集合,准确的讲是在 Iterable 接口中,增加了一个默认方法
* default void forEach(Consumer<? super T> action)
* 这个方法是用来遍历集合的。代替原来的 foreach 循环
* 这个方法的形参是 Consumer 接口类型,它是函数式接口中消费型接口的代表
* 现在调用这个方法,想要用 Lambda 表达式为 Consumer 接口类型形参赋值
* 两件事:
* (1)它的抽象方法: void accept(T t)
* (2)抽象方法的实现要完成的事是什么
* 例如:这里要打印这个 t
*/
// 原样
list.forEach((String t) -> {System.out.println(t);});
// 省略{;}
// list.forEach((String t) -> System.out.println(t));
// 省略String
// list.forEach((t) -> System.out.println(t));
// 可以省略形参()
list.forEach(t -> System.out.println(t));
}
}
(4) Lambda 表达式练习
① 无参无返回值形式
假如有自定义函数式接口 Call 如下:
public interface Call {
void shout();
}
在测试类中声明一个如下方法:
public static void callSomething(Call call){
call.shout();
}
在测试类的 main 方法中调用 callSomething 方法,并用 Lambda 表达式为形参 call 赋值:
public class TestLambda {
public static void main(String[] args) {
callSomething(()->System.out.println("张三"));
callSomething(()->System.out.println("李四"));
callSomething(()->System.out.println("王五"));
callSomething(()->System.out.println("赵六"));
}
}
② 消费型接口
代码示例:Consumer<T>
接口
- 在 JDK1.8 中 Collection 集合接口的父接口 Iterable 接口中增加了一个默认方法:
public default void forEach(Consumer<? super T> action)
遍历 Collection 集合的每个元素,执行 “xxx消费型” 操作。
- 在 JDK1.8 中 Map 集合接口中增加了一个默认方法:
public default void forEach(BiConsumer<? super K,? super V> action)
遍历 Map 集合的每对映射关系,执行 “xxx消费型” 操作。
代码示例:
(1)创建一个 Collection 系列的集合,,调用 forEach 方法遍历查看
(2)创建一个 Map 系列的集合,添加一些(key,value)键值对,例如,添加编程语言排名和语言名称,调用 forEach 方法遍历查看
import org.junit.Test;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
public class TestLambda {
@Test
public void testJDKConsumer01(){
// 创建一个 List 集合
List<Integer> list= Arrays.asList(1,2,3,4,5,6,7,8,9);
// 遍历出集合中的每一个元素
/* list.forEach((Integer integer)->{
System.out.println(integer);
}); */
// 优化为:
list.forEach((integer)-> System.out.println(integer));
}
@Test
public void testJDKConsumer02(){
HashMap<Integer,String> map = new HashMap<>();
map.put(1, "java");
map.put(2, "c");
map.put(3, "python");
map.put(4, "c++");
map.put(5, "VB");
map.put(6, "C#");
map.forEach((k,v) -> System.out.println(k+":"+v));
}
}
③ 供给型接口
代码示例:Supplier<T>
接口
- 在 JDK1.8 中增加了
StreamAPI
,java.util.stream.Stream<T>
是一个数据流。这个类型有一个静态方法:public static <T> Stream<T> generate(Supplier<T> s)
可以创建Stream的对象。- 又包含一个 forEach 方法可以遍历流中的元素:
public void forEach(Consumer<? super T> action)
。
代码示例:
调用 Stream 的 generate()
方法,来产生一个流对象,并调用 Math.random()
方法来产生数据,为 Supplier 函数式接口的形参赋值。最后调用 forEach 方法遍历流中的数据查看结果。
import org.junit.Test;
import java.util.stream.Stream;
public class TestLambda {
@Test
public void testJDKSupplier(){
// 无限流,操作提供的数据
Stream.generate(()->Math.random()).forEach(i-> System.out.println(i));
}
}
④ 功能型接口
代码示例:Funtion<T,R>
接口
- 在 JDK1.8 时 Map 接口增加了很多方法,例如:
public default void replaceAll(BiFunction<? super K,? super V,? extends V> function)
按照 Function 指定的操作替换 Map 中的 value 。public default void forEach(BiConsumer<? super K,? super V> action)
遍历 Map 集合的每对映射关系,执行 “xxx消费型” 操作。
代码示例:
import org.junit.Test;
import java.util.HashMap;
public class TestLambda02 {
@Test
public void testJDKFunction(){
HashMap<Integer,String> map = new HashMap<>();
map.put(1, "java");
map.put(2, "c");
map.put(3, "python");
map.put(4, "c++");
map.put(5, "VB");
map.put(6, "C#");
// 目标:将 java 学科的名字替换为 JavaWeb
// 传统写法:
/*for (Map.Entry<Integer,String> entry: map.entrySet()){
Integer k= entry.getKey();
String v= entry.getValue();
if (v.equals("java")){
map.put(k,"JavaWeb");
}
}*/
map.replaceAll((k,v)->{
if (v.equals("java")){
return "JavaWeb";
}
return v;
});
map.forEach((k,v)-> System.out.println(k+":"+v));
// 还可以使用流进行更简单的操作
}
}
案例:
(1)声明一个 Employee 员工类型,包含编号、姓名、薪资。
(2)添加 n 个员工对象到一个 HashMap<Integer,Employee>
集合中,其中员工编号为 key,员工对象为 value 。
(3)调用 Map 的 forEach 遍历集合
(4)调用 Map 的 replaceAll()
方法,将其中薪资低于10000元的,薪资设置为10000。
(5)再次调用 Map 的 forEach 遍历集合查看结果
Employee 类:
public class Employee{
private int id;
private String name;
private double salary;
public Employee(int id, String name, double salary) {
super();
this.id = id;
this.name = name;
this.salary = salary;
}
public Employee() {
super();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", salary=" + salary + "]";
}
}
测试类:
import org.junit.Test;
import java.util.HashMap;
@Test
public class TestOpEmp {
HashMap<Integer,Employee> map = new HashMap<>();
Employee e1 = new Employee(1, "张三", 8000);
Employee e2 = new Employee(2, "李四", 9000);
Employee e3 = new Employee(3, "王五", 10000);
Employee e4 = new Employee(4, "赵六", 11000);
Employee e5 = new Employee(5, "钱七", 12000);
map.put(e1.getId(), e1);
map.put(e2.getId(), e2);
map.put(e3.getId(), e3);
map.put(e4.getId(), e4);
map.put(e5.getId(), e5);
// 遍历打印每一个员工信息
map.forEach((k,v) -> System.out.println(k+"="+v));
System.out.println();
// 调用 Map 的 replaceAll 方法,将其中薪资低于10000元的,薪资设置为10000
map.replaceAll((k,v)->{
if(v.getSalary()<10000){
v.setSalary(10000);
}
return v;
});
// 再次遍历打印
map.forEach((k,v) -> System.out.println(k+"="+v));
}
⑤ 判断型接口
代码示例:Predicate<T>
接口
- JDK1.8时,
Collecton<E>
接口增加了一下方法,其中一个如下:public default boolean removeIf(Predicate<? super E> filter)
用于删除集合中满足 filter 指定的条件判断的。public default void forEach(Consumer<? super T> action)
遍历 Collection 集合的每个元素,执行 “xxx消费型” 操作。
案例:
(1)添加一些字符串到一个 Collection 集合中
(2)调用 forEach 遍历集合
(3)调用 removeIf()
方法,删除其中字符串的长度小于5的
(4)再次调用 forEach 遍历集合
import org.junit.Test;
import java.util.ArrayList;
public class TestLambda {
@Test
public void testJDKPredicate(){
ArrayList<String> list=new ArrayList<>();
list.add("hello");
list.add("world");
list.add("thanks");
list.add("ok");
list.add("yes");
// 遍历出集合中的元素
list.forEach(s -> System.out.println(s));
// 判断并删除字符串长度小于5的元素
list.removeIf(s->{
if (s.length()<5){
return true;
}
return false;
});
// 再次遍历打印
list.forEach(s -> System.out.println(s));
}
}
案例:
(1)声明一个 Employee 员工类型,包含编号、姓名、性别,年龄,薪资。
(2)声明一个 EmployeeSerice 员工管理类,包含一个 ArrayList<Employee>
集合的属性 all,在 EmployeeSerice 的构造器中,创建一些员工对象,为all集合初始化。
(3)在 EmployeeSerice 员工管理类中,声明一个方法:ArrayList<Employee> get(Predicate<Employee> p)
,即将满足 p 指定的条件的员工,添加到一个新的 ArrayList<Employee>
集合中返回。
(4)在测试类中创建 EmployeeSerice 员工管理类的对象,并调用 get()
方法,分别获取:
- 所有员工对象
- 所有年龄超过35的员工
- 所有薪资高于15000的女员工
示例代码:
Employee 类:
public class Employee{
private int id;
private String name;
private char gender;
private int age;
private double salary;
public Employee(int id, String name, char gender, int age, double salary) {
super();
this.id = id;
this.name = name;
this.gender = gender;
this.age = age;
this.salary = salary;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Employee() {
super();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", gender=" + gender + ", age=" + age + ", salary=" + salary + "]";
}
}
员工管理类:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class EmployeeManager {
private ArrayList<Employee> all;
public EmployeeManager(){
all = new ArrayList<Employee>();
all.add(new Employee(1, "张三", '男', 33, 8000));
all.add(new Employee(2, "李四", '女', 23, 18000));
all.add(new Employee(3, "王五", '男', 46, 8000));
all.add(new Employee(4, "赵六", '女', 23, 9000));
all.add(new Employee(5, "钱七", '男', 23, 15000));
all.add(new Employee(6, "周八", '男', 23, 11000));
}
// 第一种思路:
/*public List<Employee> find(Predicate<Employee> predicate){
List<Employee> newEmployList=new ArrayList<>();
// 判断员工是否匹配断言型接口的条件
for (Employee employee:all){
// 遍历出每一个员工
if (predicate.test(employee)) {
newEmployList.add(employee);
}
}
return newEmployList;
}*/
// 另一种思路:满足条件的就移除,传入的应该是要求的条件的反面
public List<Employee> find(Predicate<Employee> predicate){
all.removeIf(predicate);
return all;
}
}
测试类:
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class TestLambda {
@Test
public void testManagerEmployee(){
EmployeeManager employeeManager=new EmployeeManager();
// 测试第一种思路
// 1. 所有年龄超过35的员工(第一种思路下)
// List<Employee> employeeList=employeeManager.find(employee -> employee.getAge()>35);
// employeeList.forEach(employee -> System.out.println(employee));
// 2.所有薪资高于15000的女员工(第二种思路下)
// List<Employee> employeeList01=employeeManager.find(employee -> employee.getSalary()>15000&&employee.getGender()=='女');
// employeeList01.forEach(employee -> System.out.println(employee));
// 测试第二种思路
// 1. 所有年龄超过35的员工(第二种思路下)
//List<Employee> employeeList02=employeeManager.find(employee -> employee.getAge()<=35);
//employeeList02.forEach(employee -> System.out.println(employee));
// 2.所有薪资高于15000的女员工 排除 ①男员工 ②工资小于等于15000的女员工(第二种思路下)
List<Employee> employeeList03=employeeManager.find(employee -> employee.getGender()=='男'||employee.getSalary()<=15000);
employeeList03.forEach(employee -> System.out.println(employee));
}
}
(5) 方法引用与构造器引用
Lambda 表达式是可以简化函数式接口的变量与形参赋值的语法。而方法引用和构造器引用是为了简化 Lambda 表达式的。当 Lambda 表达式满足一些特殊的情况时,还可以再简化:
① 方法引用
(1)Lambda 体①只有一句语句,并且是②通过调用一个对象的类现有的方法来完成的
例如:
System.out 对象
,调用println()
方法来完成 Lambda 体- Math类,调用
random()
静态方法来完成 Lambda 体
(2)并且 Lambda 表达式的③形参正好是给该方法的实参
例如:
t->System.out.println(t)
() -> Math.random()
都是无参
代码示例:
import java.util.function.Consumer;
public class LambdaManager {
public static void useConsumer(String str, Consumer<String> consumer){
consumer.accept(str);
}
}
简化:
import org.junit.Test;
public class TestLambda {
@Test
public void testMethod(){
// 测试方法引用
LambdaManager.useConsumer("张三",s -> System.out.println(s));
// 使用方法引用进行简化:
LambdaManager.useConsumer("张三", System.out::println);
// 不能使用简化的情况:
new Thread(()-> System.out.println("Hello!")).start();
}
}
方法引用的语法格式:
实例对象名::实例方法
类名::静态方法
类名::实例方法
说明:
::
称为方法引用操作符(两个:
中间不能有空格,而且必须英文状态下半角输入)- Lambda 表达式的形参列表,全部在 Lambda 体中使用,要么是作为调用方法的对象,要么是作为方法的实参。
- 在整个 Lambda 体中没有额外的数据。
代码练习:
import org.junit.Test;
import java.util.Arrays;
import java.util.stream.Stream;
public class TestLambda {
@Test
public void testMethod01(){
// 练习使用方法的引用
String[] arr={"Hello","Big","little","java"};
// 排序:对数组中的元素进行排序,忽略大小写
// 简化之前的写法:
// Arrays.sort(arr,(s1,s2)->s1.compareToIgnoreCase(s2));
// 方法引用进行简化
Arrays.sort(arr,String::compareToIgnoreCase);
for (String s : arr) {
System.out.println(s);
}
}
@Test
public void testMethod02(){
// 优化之前的写法:
// Stream.generate(()->Math.random()).forEach(i-> System.out.println(i));
// 使用方法简化
Stream.generate(Math::random).forEach(System.out::println);
}
}
@Test
public void testMethod03(){
Runnable r = () -> System.out.println("hello lambda");
Runnable r = System.out::println; // 只会打印空行
// 不能简化方法引用,因为"hello lambda"这个无法省略
}
② 构造器引用
(1)当 Lambda 表达式是创建一个对象,并且满足 Lambda 表达式形参,正好是给创建这个对象的构造器的实参列表。
(2)当 Lambda 表达式是创建一个数组对象,并且满足 Lambda 表达式形参,正好是给创建这个数组对象的长度。
构造器引用的语法格式:
类名::new
数组类型名::new
示例代码:
import org.junit.Test;
import java.util.stream.Stream;
public class TestLambda {
@Test
public void testConstructor01(){
// 创建一个 Stream 流对象
Stream<Integer> stream=Stream.of(1,2,3,4);
// Lambda 的表达式
// Stream<int[]> stream01=stream.map(i->new int[i]);
// 构造器的使用
Stream<int[]> stream02=stream.map(int[]::new);
System.out.println(stream02);
}
@Test
public void testConstructor02() {
Stream<String> stream = Stream.of("1.0","2.3","4.4");
// Lambda 的表达式
// Stream<BigDecimal> stream2 = stream.map(num -> new BigDecimal(num));
// 构造器的使用
Stream<BigDecimal> stream2 = stream.map(BigDecimal::new);
}
@Test
public void testConstructor03(){
// Lambda 的表达式
// Supplier<String> s = () -> new String(); // 通过供给型接口,提供一个空字符串对象
// 构造器引用
Supplier<String> s = String::new;//通过供给型接口,提供一个空字符串对象
}
}
三、Stream 流
1. 学习目标
- 能够理解流与集合相比的优点
- 能够理解流的延迟执行特点
- 能够通过集合、映射或数组获取流
- 能够掌握常用的流操作
2. 内容讲解
(1) Stream 流的优势
Java8 中有两大最为重要的改变:第一个是 Lambda 表达式;另外一个则是 Stream API。
①
Stream API ( java.util.stream)
把真正的函数式编程风格引入到 Java 中。这是目前为止对 Java 类库最好的补充,因为Stream API
可以极大提高 Java 程序员的生产力,让程序员写出高效率、干净、简洁的代码。
②Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用
Stream API
对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用Stream API
来并行执行操作。简言之,Stream API
提供了一种高效且易于使用的处理数据的方式。
③Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合讲的是数据,负责存储数据, Stream 流讲的是计算,负责处理数据!
注意:
- Stream 自己不会存储元素
- Stream 不会改变源对象,每次处理都会返回一个持有结果的新 Stream
- Stream 操作是延迟执行的,这意味着他们会等到需要结果的时候才执行
(2) Stream 流的使用步骤
Stream 的操作三个步骤:
- 创建 Stream:通过一个数据源(如:集合、数组),获取一个流
- 中间操作:中间操作是个操作链,对数据源的数据进行n次处理,但是在终结操作前,并不会真正执行
- 终止操作:一旦执行终止操作,就执行中间操作链,最终产生结果并结束 Stream
(3) 创建 Stream
① 创建 Stream 方式一:通过集合
- Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
public default Stream<E> stream()
:返回一个顺序流public default Stream<E> parallelStream()
:返回一个并行流
@Test
public void test01(){
List<Integer> list = Arrays.asList(1,2,3,4,5);
// JDK1.8 中,Collection 系列集合增加了方法
Stream<Integer> stream = list.stream();
stream.forEach(System.out::println);
}
② 创建 Stream 方式二:通过数组
- Java8 中的 Arrays 的静态方法
stream()
可以获取数组流:public static <T> Stream<T> stream(T[] array)
:返回一个流
@Test
public void test02(){
String[] arr = {"hello","world"};
Stream<String> stream = Arrays.stream(arr);
stream.forEach(System.out::println);
}
@Test
public void test03(){
int[] arr = {1,2,3,4,5};
IntStream stream = Arrays.stream(arr);
stream.forEach(System.out::println);
}
- 重载形式,能够处理对应基本类型的数组:
public static IntStream stream(int[] array)
:返回一个整型数据流public static LongStream stream(long[] array)
:返回一个长整型数据流public static DoubleStream stream(double[] array)
:返回一个浮点型数据流
③ 创建 Stream方式三:通过 Stream 的 of()
静态方法
- 可以调用Stream类静态方法
of()
, 通过显示值创建一个流,它可以接收任意数量的参数。public static<T> Stream<T> of(T... values)
:返回一个顺序流
@Test
public void test04(){
Stream<Integer> stream = Stream.of(1,2,3,4,5);
stream.forEach(System.out::println);
}
④ 创建 Stream 方式四:创建无限流
- 可以使用静态方法
Stream.iterate()
和Stream.generate()
,创建无限流。public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
:返回一个无限流public static<T> Stream<T> generate(Supplier<T> s)
:返回一个无限流
@Test
public void test05(){
Stream<Double> stream = Stream.generate(Math::random);
stream.forEach(System.out::println);
}
@Test
public void test06(){
/*
* Stream<T> iterate(T seed, UnaryOperator<T> f)
* UnaryOperator 接口,SAM 接口,抽象方法:
* UnaryOperator<T> extends Function<T,T>
* T apply(T t)
*/
Stream<Integer> stream = Stream.iterate(1, num -> num+=2);
// stream = stream.limit(10);
stream.forEach(System.out::println);
}
(4) 中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。
① filer()
过滤
filter(Predicate p)
可以接收 Lambda , 从流中排除某些元素
@Test
public void testMiddle01(){
// 1. 创建 Stream
// Stream<Integer> stream=Stream.of(1,2,3,4,5,6);
// 2. 过滤掉非偶数,最后终结操作,例如:遍历
// stream.filter(i->i%2==0).forEach(System.out::println);
// 优化写法:
Stream.of(1,2,3,4,5,6).filter(i->i%2==0).forEach(System.out::println);
}
② distinct()
去重
distinct()
通过流所生成元素的equals()
去除重复元素
@Test
public void testMiddle02(){
// 去重操作:
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5).distinct().forEach(System.out::println);
// 去重并且只保留偶数
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5).distinct().filter(i->i%2==0).forEach(System.out::println);
}
③ limit()
截断
limit(long maxSize)
截断流,使其元素不超过给定数量
@Test
public void testMiddle03(){
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5).limit(3).forEach(System.out::println);
}
@Test
public void testMiddle04(){
Stream.of(1,2,2,3,3,4,4,5,2,3,4,5,6,7)
.distinct() //(1,2,3,4,5,6,7)
.filter(t -> t%2!=0) //(1,3,5,7)
.limit(3)
.forEach(System.out::println);
}
④ skip()
跳过
skip(long n)
跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与limit(n)
互补
@Test
public void testMiddle05(){
Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5).skip(5).forEach(System.out::println);
}
⑤ peek()
对每个元素进行 Lambda 操作
@Test
public void testMiddle06(){
long count = Stream.of(1,2,3,4,5,6,2,2,3,3,4,4,5)
.distinct()
.peek(System.out::println) // Consumer 接口的抽象方法 void accept(T t)
.count();
System.out.println("count="+count); // 统计新的元素个数(也是一种终结操作)
}
⑥ sorted()
排序
sorted()
产生一个新流,其中按自然顺序排序
@Test
public void testMiddle07(){
Stream.of(11,2,39,4,54,6,2,22,3,3,4,54,54)
.distinct() // 去重
.sorted() // 排序
.limit(3) // 截断个数
.forEach(System.out::println);
}
sorted(Comparator com)
产生一个新流,其中按比较器顺序排序
@Test
public void testMiddle08(){
// 希望能够找出前三个最大值,前三名最大的,不重复
Stream.of(11,2,39,4,54,6,2,22,3,3,4,54,54)
.distinct()
.sorted((n1,n2) -> n2 - n1) // 从小到大是 n1-n2 ,从大到小是 n2-n1
.limit(3)
.forEach(System.out::println);
}
⑦ map()
映射成新元素
map(Function f)
接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
@Test
public void testMiddle09(){
Stream.of(1,2,3,4,5)
.map(t -> t+=1) // Function<T,R> 接口抽象方法 R apply(T t)
.forEach(System.out::println); // 映射为 t+1
}
@Test
public void testMiddle10(){
// 目标:将数组中的元素全部变为大写字母
String[] arr = {"hello","world","java"}; // 原来数组中的元素不会被改变
Arrays.stream(arr)
.map(t->t.toUpperCase())
.forEach(System.out::println);
}
⑧ 所有中间操作方法列表
方 法 | 描 述 |
---|---|
filter(Predicate p) | 接收 Lambda , 从流中排除某些元素 |
distinct() | 筛选,通过流所生成元素的 equals() 去除重复元素 |
limit(long maxSize) | 截断流,使其元素不超过给定数量 |
skip(long n) | 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补 |
peek(Consumer action) | 接收 Lambda,对流中的每个数据执行 Lambda 体操作 |
sorted() | 产生一个新流,其中按自然顺序排序 |
sorted(Comparator com) | 产生一个新流,其中按比较器顺序排序 |
map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 |
mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream |
mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream |
mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream |
flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 |
(5) 终结操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void。流进行了终止操作后,不能再次使用。
① forEach()
迭代
@Test
public void testEnd01(){
Stream.of(1,2,3,4,5)
.forEach(System.out::println);
}
② count()
返回流中元素总数
@Test
public void testEnd02(){
long count = Stream.of(1,2,3,4,5)
.count();
System.out.println("count = " + count);
}
③ allMatch()
检查是否匹配所有元素
@Test
public void testEnd03(){
// 判断 Stream 流操作的元素是否全匹配某一个规则
boolean result = Stream.of(1,3,5,7,9)
.allMatch(t -> t%2!=0);
System.out.println(result);
}
④ anyMatch()
检查是否至少匹配一个元素
@Test
public void testEnd04(){
// 判断 Stream 流操作的元素是否有能匹配某一个规则的元素
boolean result = Stream.of(1,3,5,7,9)
.anyMatch(t -> t%2==0);
System.out.println(result);
}
⑤ findFirst()
返回第一个元素
@Test
public void testEnd05(){
Optional<Integer> first = Stream.of(1,3,5,7,9).findFirst();
System.out.println(first.get());
}
⑥ max()
返回流中最大值
@Test
public void testEnd06(){
Optional<Integer> max = Stream.of(1,2,4,5,7,8)
.max(Integer::compareTo);
System.out.println(max.get());
}
⑦ reduce()
可以将流中元素反复结合操作起来,得到一个值
T reduce(T iden, BinaryOperator b)
可以将流中元素反复结合起来,得到一个值。返回T
U reduce(BinaryOperator b)
可以将流中元素反复结合起来,得到一个值。返回Optional<T>
@Test
public void testEnd07(){
Optional<Integer> reduce01 = Stream.of(1, 2, 4, 5, 8).reduce((n1, n2) -> n1 + n2); // 反复加起来得到一个值
System.out.println(reduce01.get());
Integer reduce02 = Stream.of(1,2,4,5,7,8)
.reduce(0, (t1,t2) -> t1+t2); // 整体加完之后继续加上一个 “0”
System.out.println(reduce02);
}
@Test
public void testEnd08(){
Optional<Integer> max = Stream.of(1,2,4,5,7,8).reduce((t1,t2) -> t1>t2?t1:t2);
System.out.println(max.get());
Integer reduce = Stream.of(1, 2, 4, 5, 7, 8).reduce(0, (t1, t2) -> t1 > t2 ? t1 : t2);
System.out.println(reduce);
}
⑧ collect()
将流转换为其他形式(重要)
R collect(Collector c)
将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法
@Test
public void testEnd09(){
// 将所有的偶数单独放在一个新的集合中
List<Integer> list = Stream.of(1,2,4,5,7,8)
.filter(t -> t%2==0)
.collect(Collectors.toList());
System.out.println(list);
}
⑨ 所有终结操作的方法列表
方法 | 描述 |
---|---|
boolean allMatch(Predicate p) | 检查是否匹配所有元素 |
boolean anyMatch(Predicate p) | 检查是否至少匹配一个元素 |
boolean noneMatch(Predicate p) | 检查是否没有匹配所有元素 |
Optional findFirst() | 返回第一个元素 |
Optional findAny() | 返回当前流中的任意元素 |
long count() | 返回流中元素总数 |
Optional max(Comparator c) | 返回流中最大值 |
Optional min(Comparator c) | 返回流中最小值 |
void forEach(Consumer c) | 迭代 |
T reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T |
U reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 Optional<T> |
R collect(Collector c) | 将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法 |
Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。
(6) 练习
案例:
现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的 for 循环(或增强 for 循环)依次进行以
下若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
- 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
- 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
- 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
- 将两个队伍合并为一个队伍;存储到一个新集合中。
- 根据姓名创建 Person 对象;存储到一个新集合中。
- 打印整个队伍的 Person 对象信息。
Person 类的代码为:
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
两个队伍(集合)及后续操作的代码如下:
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TestStream {
/* 1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
5. 将两个队伍合并为一个队伍;存储到一个新集合中。
6. 根据姓名创建 Person 对象;存储到一个新集合中。
7. 打印整个队伍的Person对象信息。*/
@Test
public void testStreamAll(){
// 第一支队伍
List<String> one = new ArrayList<>();
one.add("张扛把子");
one.add("张老二");
one.add("张老三");
one.add("张老四");
one.add("张老五");
one.add("张老七");
one.add("张中");
one.add("张小");
// 第一支队伍只要三个字名字的人,再筛选只保留前三个人
List<String> newOne = one.stream().filter(s -> s.length() == 3).limit(3).collect(Collectors.toList());
// 第二支队伍
List<String> two = new ArrayList<>();
two.add("李老大");
two.add("王总");
two.add("张三");
two.add("张三丰");
two.add("张二狗");
two.add("埃斯奎");
two.add("尼古拉斯");
two.add("斯库鲁斯");
two.add("阿巴斯奇斯基");
// 第二支队伍只保留姓张的,然后再去掉前两个人
List<String> newTwo=two.stream().filter(s->s.startsWith("张")).skip(2).collect(Collectors.toList());
// 将两个队伍合并为一个队伍;存储到一个新的集合中
List<String> newAll = Stream.concat(newOne.stream(), newTwo.stream()).collect(Collectors.toList());
newAll.forEach(System.out::println);
// 从新的集合中取出每一个姓名,然后每一个姓名创建一个 Person 对象
// 传统做法:
/*List<Person> personList=new ArrayList<>();
for (String s : newAll) {
Person person = new Person(s);
personList.add(person);
}*/
// 优化写法:
List<Person> personList = newAll.stream().map(Person::new).collect(Collectors.toList());
personList.forEach(System.out::println);
}
}
四、Optional 类
到目前为止,臭名昭著的空指针异常是导致 Java 应用程序失败的最常见原因。以前,为了解决空指针异常,Google 公司著名的 Guava 项目引入了 Optional 类,Guava 通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到 Google Guava 的启发, Optional 类已经成为 Java8 类库的一部分。
Optional 实际上是个容器:它可以保存类型 T 的值,或者仅仅保存 null。Optional 提供很多有用的方法,这样我们就不用显式进行空值检测。
1. API
(1) 创建 Optional 对象
static <T> Optional<T> empty()
:用来创建一个空的 Optional
@Test
public void testCreate01(){
Optional<String> optional01 = Optional.empty();
System.out.println(optional01);
}
static <T> Optional<T> of(T value)
:用来创建一个非空的 Optional
@Test
public void testCreate02(){
String str = "hello";
Optional<String> optional02 = Optional.of(str);
System.out.println(optional02);
}
static <T> Optional<T> ofNullable(T value)
:用来创建一个可能是空,也可能非空的 Optional
@Test
public void testCreate03(){
String str = null;
Optional<String> optional03 = Optional.ofNullable(str);
System.out.println(optional03);
}
(2) 从 Optional 容器中取出所包装的对象
T get()
:要求 Optional 容器必须非空
@Test
public void testGetValue01(){
String str = "hello";
Optional<String> opt = Optional.of(str);
System.out.println(opt.get());
}
T orElse(T other)
:与orElse(T other) 与ofNullable(T value)
配合使用,如果 Optional 容器中非空,就返回所包装值,如果为空,就用orElse(T other)
,other
是指定的默认值(备胎)代替
@Test
public void testGetValue02(){
String str = "张三";
Optional<String> opt = Optional.ofNullable(str);
String string = opt.orElse("备胎");
System.out.println(string);
}
T orElseGet(Supplier<? extends T> other)
:如果 Optional 容器中非空,就返回所包装值,如果为空,就用 Supplier 接口的 Lambda 表达式提供的值代替
@Test
public void testGetValue03(){
String str = null;
Optional<String> opt = Optional.ofNullable(str);
String string = opt.orElseGet(()->new String("张三")); // 不能简化,形参与实参并不对应
System.out.println(string);
}
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
: 如果 Optional 容器中非空,就返回所包装值,如果为空,就抛出你指定的异常类型代替原来的 NoSuchElementException
@Test
public void testGetValue04(){
String str = null;
Optional<String> opt = Optional.ofNullable(str);
String string = opt.orElseThrow(()->new RuntimeException("Optional 容器为空。。。")); // 会抛出自定义的异常
System.out.println(string);
}
- 其他方法:
boolean isPresent()
:判断 Optional 容器中是否有值
@Test
public void testGetValue05(){
Optional<String> opt = Optional.of("hello");
boolean present = opt.isPresent();
System.out.println(present);
}
void ifPresent(Consumer<? super T> consumer)
:判断 Optional 容器中的值是否存在,如果存在,就对它进行 Consumer 指定的操作,如果不存在就不做
@Test
public void testGetValue06(){
Optional<String> opt = Optional.of("hello");
// 消费性接口只能消费使用 Optional 中的元素,但是不能改变 Optional 中的元素
opt.ifPresent(System.out::println);
}
<U> Optional<U> map(Function<? super T,? extends U> mapper)
: 判断 Optional 容器中的值是否存在,如果存在,就对它进行 Function 接口指定的操作,如果不存在就不做
@Test
public void testGetValue07(){
String str = "Hello";
Optional<String> opt = Optional.ofNullable(str);
// 判断是否是纯字母单词,如果是,转为大写,否则保持不变
// 使用正则表达式
Optional<String> newStrOptional = opt.filter(s->s.matches("[a-zA-Z]+"))
.map(s -> s.toUpperCase());
String newStr=newStrOptional.orElse("AAA");
System.out.println(newStr);
}
2. 练习
(1) 练习一
案例:
(1)声明一个 Girl 类型,包含姓名(String)属性
(2)声明一个 Boy 类型,包含姓名(String),女朋友(Girl)属性
(3)在测试类中,创建一个 Boy 对象,并:如果他有女朋友,显示他女朋友名称;如果他没有女朋友,他的女朋友默认为“嫦娥”,即只能欣赏“嫦娥”了
作为内部类:
class Girl{
private String name;
public Girl(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Girl [name=" + name + "]";
}
}
class Boy{
private String name;
private Girl girlFriend;
public Boy(String name, Girl girlFriend) {
super();
this.name = name;
this.girlFriend = girlFriend;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Girl getGirlFriend() {
return girlFriend;
}
public void setGirlFriend(Girl girlFriend) {
this.girlFriend = girlFriend;
}
@Override
public String toString() {
return "Boy [name=" + name + ", girlFriend=" + girlFriend + "]";
}
}
测试类:
@Test
public void testOptionalAll(){
Boy boy=new Boy("张三",new Girl("李四")); // 如果将李四改为 null 则会显示嫦娥
// 1. 简单做法:
Optional<Girl> girlFriend01=Optional.ofNullable(boy.getGirlFriend());
Girl girl=girlFriend01.orElse(new Girl("嫦娥"));
System.out.println(girl);
// 2. 复杂做法:
Optional<Girl> girlFriend02=Optional.ofNullable(boy.getGirlFriend());
// 判断女朋友是否是 null
Optional<Girl> optional=Optional.of(girlFriend02.orElse(new Girl("嫦娥")));
optional.ifPresent(System.out::println);
}
(2) 练习二
案例:
(1)声明学生类,包含姓名和年龄
(2)添加几个学生对象到一个 ArrayList<Student>
集合中
(3)对集合中的学生进行操作,找出年龄大于30岁的,并取出第一个学生,如果没有这样的学生,用无参构造 new 一个学生对象,打印学生信息
学生类示例代码:
作为内部类:
class Student{
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
测试类:
@Test
public void testStudent(){
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("张三", 23));
list.add(new Student("李四", 33));
list.add(new Student("王五", 43));
// 对集合中的学生进行操作,取出集合中第一个年龄大于30岁的学生
Optional<Student> optional=list.stream().filter(s->s.getAge()>30).findFirst();
// 如果没有,用无参构造创建一个学生对象,打印学生信息
Student student=optional.orElseGet(Student::new);
System.out.println(student);
}
五、JDK8 接口的新特性
1. JDK8 之前的接口
在 JDK8 之前,interface 之中可以定义变量和方法,变量必须是 public、static、final 修饰的,方法必须是 public、abstract 修饰的,由于这些修饰符都是默认的所以:以下写法等价
public interface JDK8BeforeInterface {
public static final int field1 = 0;
int field2 = 0;
public abstract void method1(int a) throws Exception;
void method2(int a) throws Exception;
}
2. JDK8 之后的接口
JDK8 及以后,允许我们在接口中定义 static 方法和 default 方法
- 接口中定义的静态 static 方法只能通过接口名直接调用,default 的方法需要用接口的实现类的对象来调用
- 接口中的 static 和 default 方法可以有函数体,其实现类不必要重写
- 其他的非 static 和非 default 的都是抽象方法,没有函数体,其实现类必须重写所有的抽象方法
- 如果子类(或实现类)继承的父类和其实现的接口定义了同名同参的方法,并且接口中的方法为 default 方法(都有函数体),那么该子类的对象调用该方法时(在子类没有重写该方法的情况下),默认是父类的方法(类优先性)
- 如果类实现了多个接口,而且多个接口中定义了同名同参数的 default 方法(有函数体),在该类没有重写的情况下,就会报错(接口冲突)。如果想解决这个问题,就必须在该类中重写此方法。
// 接口:
public interface JDK8Interface {
// static 修饰符定义静态方法
static void staticMethod() {
System.out.println("接口中的静态方法");
}
// default 修饰符定义默认方法
default void defaultMethod() {
System.out.println("接口中的默认方法");
}
}
// 实现接口:
public class JDK8InterfaceImpl implements JDK8Interface {
// 实现接口后,因为默认方法不是抽象方法,所以可以不重写,但是如果开发需要,也可以重写
}
// 测试调用:
public class Main {
public static void main(String[] args) {
// static 方法必须通过接口类调用
JDK8Interface.staticMethod();
//default 方法必须通过实现类的对象调用
new JDK8InterfaceImpl().defaultMethod();
}
}
// 实现接口:
public class AnotherJDK8InterfaceImpl implements JDK8Interface {
// 当然如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。
// 签名跟接口 default 方法一致,但是不能再加 default 修饰符
@Override
public void defaultMethod() {
System.out.println("接口实现类覆盖了接口中的default");
}
}