【JavaSE 第二十二天】

【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 :
  1. 测试方法注意事项
  • 必须是 public 修饰的,没有返回值,没有参数
  • 必须使注解 @Test 修饰
  1. 运行测试方法
  • 选中方法名 → 右键 → Run ‘测试方法名’ 运行选中的测试方法
  • 选中测试类类名 → 右键 → Run ‘测试类类名’ 运行测试类中所有测试方法
  • 选中模块名 → 右键 → Run ‘All Tests’ 运行模块中的所有测试类的所有测试方法
  1. 查看测试结果
  • 绿色:表示测试通过
  • 红色:表示测试失败,有问题

简单使用:

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);
}

下面进行测试:三种方法

  1. 第一种方法:
    实现接口,重写两个方法:
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());
    }
}
  1. 第二种方法:
    匿名内部类的写法,不必写实现类:
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;
            }
        });
    }
}
  1. 第三种方法:
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
接口名抽象方法描述
Consumervoid accept(T t)接收一个对象用于完成功能
BiConsumer<T,U>void accept(T t, U u)接收两个对象用于完成功能
DoubleConsumervoid accept(double value)接收一个double值
IntConsumervoid accept(int value)接收一个 int 值
LongConsumervoid accept(long value)接收一个 long 值
ObjDoubleConsumervoid accept(T t, double value)接收一个对象和一个 double 值
ObjIntConsumervoid accept(T t, int value)接收一个对象和一个 int 值
ObjLongConsumervoid 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);
        });
    }
}
Ⅱ、 供给型接口
  • 供给型接口的抽象方法特点:无参,但是有返回值
接口名抽象方法描述
SupplierT get()返回一个对象
BooleanSupplierboolean getAsBoolean()返回一个 boolean 值
DoubleSupplierdouble getAsDouble()返回一个 double 值
IntSupplierint getAsInt()返回一个 int 值
LongSupplierlong 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 结果
接口名抽象方法描述
Predicateboolean test(T t)接收一个对象
BiPredicate<T,U>boolean test(T t, U u)接收两个对象
DoublePredicateboolean test(double value)接收一个 double 值
IntPredicateboolean test(int value)接收一个 int 值
LongPredicateboolean 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 类型对象结果
UnaryOperatorT apply(T t)接收一个 T 类型对象,返回一个 T 类型对象结果
DoubleFunctionR apply(double value)接收一个 double 值,返回一个 R 类型对象
IntFunctionR apply(int value)接收一个 int 值,返回一个 R 类型对象
LongFunctionR apply(long value)接收一个 long 值,返回一个 R 类型对象
ToDoubleFunctiondouble applyAsDouble(T value)接收一个 T 类型对象,返回一个 double 结果
ToIntFunctionint applyAsInt(T value)接收一个 T 类型对象,返回一个 int
ToLongFunctionlong applyAsLong(T value)接收一个 T 类型对象,返回一个 long 结果
DoubleToIntFunctionint applyAsInt(double value)接收一个 double 值,返回一个 int 结果
DoubleToLongFunctionlong applyAsLong(double value)接收一个 double 值,返回一个 long 结果
IntToDoubleFunctiondouble applyAsDouble(int value)接收一个 int 值,返回一个 double 结果
IntToLongFunctionlong applyAsLong(int value)接收一个 int 值,返回一个 long 结果
LongToDoubleFunctiondouble applyAsDouble(long value)接收一个 long 值,返回一个 double 结果
LongToIntFunctionint applyAsInt(long value)接收一个 long 值,返回一个 int 结果
DoubleUnaryOperatordouble applyAsDouble(double operand)接收一个 double 值,返回一个 double 结果
IntUnaryOperatorint applyAsInt(int operand)接收一个 int 值,返回一个 int 结果
LongUnaryOperatorlong applyAsLong(long operand)接收一个 long 值,返回一个 long 结果
BiFunction<T,U,R>R apply(T t, U u)接收一个 T 类型和一个 U 类型对象,返回一个 R 类型对象结果
BinaryOperatorT 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 结果
DoubleBinaryOperatordouble applyAsDouble(double left, double right)接收两个 double 值,返回一个 double 结果
IntBinaryOperatorint applyAsInt(int left, int right)接收两个 int 值,返回一个 int 结果
LongBinaryOperatorlong 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 中增加了 StreamAPIjava.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 的操作三个步骤:

  1. 创建 Stream:通过一个数据源(如:集合、数组),获取一个流
  2. 中间操作:中间操作是个操作链,对数据源的数据进行n次处理,但是在终结操作前,并不会真正执行
  3. 终止操作:一旦执行终止操作,就执行中间操作链,最终产生结果并结束 Stream

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 循环)依次进行以
下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
  2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
  3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
  4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
  5. 将两个队伍合并为一个队伍;存储到一个新集合中。
  6. 根据姓名创建 Person 对象;存储到一个新集合中。
  7. 打印整个队伍的 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 对象
  1. static <T> Optional<T> empty() :用来创建一个空的 Optional
@Test
public void testCreate01(){
    Optional<String> optional01 = Optional.empty();
    System.out.println(optional01);
}
  1. 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);
}
  1. 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 容器中取出所包装的对象
  1. T get() :要求 Optional 容器必须非空
@Test
public void testGetValue01(){
    String str = "hello";
    Optional<String> opt = Optional.of(str);
    System.out.println(opt.get());
}
  1. 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);
}
  1. 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);
}
  1. <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);
}
  • 其他方法
  1. boolean isPresent() :判断 Optional 容器中是否有值
@Test
public void testGetValue05(){
    Optional<String> opt = Optional.of("hello");
    boolean present = opt.isPresent();
    System.out.println(present);
}
  1. 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);
}
  1. <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");  
    }  
}  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值