学习笔记_java8新特性

学习笔记_java8新特性

相信关于java8的新特性大家都有所了解(包括编译器、类库、工具类、JVM等,本来也只是局限于了解,扫地生在此前个人开发中很少使用。最近在实习中发现,利用这些特性在一定程度上是可以简化处理过程的。有人可能会觉得使用lambda开发不是为难后续维护的人吗?我认为存在即合理。

1 Lambda API

1.1 简介

什么是Lambda表达式?

  Lambda是一个匿名函数,可以理解为是一段可以传递的代码,可以将代码像传递参数、传递数据一样进行传输。使用Lambda表达式,可以写出更加紧凑、更加简洁、更加灵活的代码。

使用Lambda的限制条件

Lambda并不是任何地方都可以使用,Lambda表达式需要“函数式接口”的支持。

什么是函数式接口?

接口中只有一个抽象方法的接口,称为函数式接口,可以用@FunctionalInterface修饰一下,这里需要注意的是:未使用 @FunctionalInterfaces注解的接口未必就不是函数式接口,一个接口是不是函数式接口的条件只有一条,即接口中只有一个抽象方法的接口(Object类中的方法不算)。而使用@FunctionalInterface注解修饰了的接口就一定是函数式接口,添加@FunctionalInterface注解可以帮助我们检查是否是函数式接口。

JDK中常见的函数式接口有:

@FunctionalInterface
public interface Runnable {     
    void run(); 
}
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

以下接口中虽然有两个方法,但因hashCode()是Object类中的方法,因此该接口也是函数式接口:

@FunctionalInterface
public interface FuncInterface {
    void doSomething();
    int hashCode();  // Object类中的方法
}

1.2 Lambda表达式示例

需求一:开启一个线程,在线程中打印出"Hello World"

未使用Lambda表达式时的写法:

@Test
public void test1() {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World");
        }
    });
    thread.start();
}

使用Lambda时的写法:

@Test
public void test2() {
    new Thread(() -> System.out.println("Hello World")).start();
}

需求二:模拟一个计算器,使其可以进行简单的加、减、乘操作

  1. 计算器操作函数式接口
package top.saodisheng.lambdaapi;

/**
 * description:
 * 模拟计算机操作的函数式接口
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public interface Calculator<T> {
    T operation(T t1, T t2);
}
  1. 具体操作
package top.saodisheng.lambdaapi;

import org.junit.Test;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class CalculatorTest {
    public Integer operator(Integer v1, Integer v2, Calculator<Integer> calculator) {
        return calculator.operation(v1, v2);
    }

    public Integer add(Integer v1, Integer v2) {
        // (x, y) -> x + y 是对Calculator接口方法的实现
        return operator(v1, v2, (x, y) -> x + y);
    }

    public Integer subtr(Integer v1, Integer v2) {
        return operator(v1, v2, (x, y) -> x - y);
    }

    public Integer multi(Integer v1, Integer v2) {
        return operator(v1, v2, (x, y) -> x * y);
    }

    @Test
    public void test1() {
        Integer add = add(1, 2);
        Integer subtr = subtr(100, 50);
        Integer multi = multi(10, 2);
        System.out.println("add(1, 2) -> " + add);
        System.out.println("subtr(100, 50) -> " + subtr);
        System.out.println("multi(10, 2) -> " + multi);
    }
}

  运行结果:

image-20210723103232871

从需求一中,我们可以看出,使用Lambda比使用匿名内部类代码更加简洁,同时,也可以理解为什么Lambda必须需要函数式接口的支持。

从需求二的例子中,我们可以更加理解“一段可以传递的代码”这句话的含义。对数据的操作方法定义在Calculator接口中,而加、减、乘的具体实现代码在各自的方法中,并将这些实现作为参数传递给CalculatorTest类的operator()方法,最终返回操作结果。

1.3 Lambda表达式的语法

1.3.1 Lambda表达式的语法结构

(参数列表——对应的是接口中对应的抽象方法的参数列表) -> {对抽象方法的实现}

Lambda表达式语法分为3个部分:

  1. 左边的参数列表,对应的是函数式接口中抽象方法的参数列表;
  2. 中间的符号:->,为固定写法;
  3. 右边大括号内对函数接口抽象方法的实现。

Lambda表达式的在具体场景下可以有简略写法。

1.3.2 语法格式一:无参数,无返回值

@Test
public void test3() {
    // Runnable runnable = () -> {System.out.println("Hello World")};
    // 函数实现主体只有一行代码时,{}可以省略
    Runnable runnable = () -> System.out.println("Hello World");
    runnable.run();
}

1.3.3 语法格式二:有一个参数,无返回值

public void print(Consumer<String> msg) {
    System.out.println(msg);
}

public void doPrint(String msg) {
    // print((str) -> System.out.println(msg));
    // 此时,左边的()可以省略。简写为
    print(str -> System.out.println(msg));
}

 

1.3.4 语法格式三:Lambda体内只有一条语句,且有返回值,return可省略

public Integer subtr(Integer v1,Integer v2) {
    return operator(v1, v2, (x, y) -> x - y);
}

1.3.5 语法格式四:有两个以上参数,且Lambda体中有多条语句

public Integer add(Integer v1,Integer v2) {
    return operator(v1,v2,(x,y) -> {
        System.out.println("进行加法运算");
        return x + y;
    });
}

1.3.6 语法格式五:Lambda表达式的数据类型可以省略不写

JVM编译器通过上下文可以推断出数据类型,但要注意的是,当多个参数时,要么都写,要么都不写,不能有的写,有的不写

public Integer subtr(Integer v1,Integer v2) {
    // 错误,参数类型不全写
    return operator(v1, v2, (Integer x, y) -> x - y);
}

public Integer subtr(Integer v1, Integer v2) {
    // 正确,参数类型全写
    return operator(v1, v2, (Integer x, Integer y) -> x - y);
}

public Integer subtr(Integer v1,Integer v2) {
    // 正确,参数类型全不写
    return operator(v1, v2, (x, y) -> x - y); 
}

1.4 Java8四大内置核心函数式接口

Consumer<T> : 消费型接口(无返回值,有去无回)
    void accept(T t);

Supplier<T> : 供给型接口
    T get();         

Function<T,R> : 函数型接口
    R apply(T t);
        
Predicate<T> : 断言型接口
    boolean test(T t);
        
四大核心接口的-->扩展子接口

示例:

package top.saodisheng.lambdaapi;

import lombok.Data;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
@Data
public class User {
    private String userName;
    private int age;
}

-----------------------------------------------------------------
package top.saodisheng.lambdaapi;

import org.junit.Test;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class InnerInterface {
    /**
     * print
     * 打印User信息
     * @param user user
     * @param userConsumer userConsumer
     * @return void
     * @author saodisheng_liuxingwu
     * @date 2021/7/23
    */
    public void print(User user, Consumer<User> userConsumer) {
        userConsumer.accept(user);
    }

    /**
     * getUser
     * 返回一个User
     * @param userSupplier userSupplier
     * @return top.saodisheng.lambdaapi.User
     * @author saodisheng_liuxingwu
     * @date 2021/7/23
    */
    public User getUser(Supplier<User> userSupplier) {
        return userSupplier.get();
    }

    /**
     * transformUser
     * 转换一个User
     * @param user user
     * @param function function
     * @return top.saodisheng.lambdaapi.User
     * @author saodisheng_liuxingwu
     * @date 2021/7/23
    */
    public User transformUser(User user, Function<User, User> function) {
        return function.apply(user);
    }

    /**
     * checkUser
     * 检验User是否合法
     * @param user user
     * @param predicate predicate
     * @return boolean
     * @author saodisheng_liuxingwu
     * @date 2021/7/23
    */
    public boolean checkUser(User user, Predicate<User> predicate) {
        return predicate.test(user);
    }

    @Test
    public void test1() {
        User userObj = new User();
        userObj.setUserName("技术扫地生");
        userObj.setAge(3);

        // 测试Consumer
        InnerInterface mainInst = new InnerInterface();
        mainInst.print(userObj, user -> System.out.println(user));

        // 测试 Supplier
        User user1 = mainInst.getUser(() -> {
            User user = new User();
            user.setUserName("楼上老刘");
            user.setAge(3);
            return user;
        });
        System.out.println(user1);

        // 将技术扫地生的年龄改为10岁
        User user2 = mainInst.transformUser(userObj, user -> {
            user.setAge(10);
            return user;
        });
        System.out.println(user2);

        // 判断User是否为技术扫地生
        boolean checkUser = mainInst.checkUser(userObj, user -> "技术扫地生".equals(user.getUserName()));
        System.out.println(checkUser);
    }
}

运行结果:

image-20210723113058747

以上四大核心内置接口是我们日常开发中经常要用到的,同时,它们还有一些变种,如:

BiConsumer,Consumer的增强版,接受两个参数:

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
}

BiFunction类似,Function的增强版,接受两个参数,返回一个参数:

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

其他的类似,这些函数式接口都在java.util.function包下,读者可去这个包下去查询。

2 方法引用

2.1 方法引用的使用场景

我们用Lambda表达式来实现匿名方法。但有些情况下,我们用Lambda表达式(切记必须在lambda表达式中使用)仅仅是调用一些已经存在的方法,除了调用动作外,没有其他任何多余的动作,在这种情况下,我们倾向于通过方法名来调用它,而Lambda表达式可以帮助我们实现这一要求,它使得Lambda在调用那些已经拥有方法名的方法代码更简洁、更容易理解。方法引用可以理解为Lambda表达式的另外一种表现形式。

2.2 方法引用的分类

类型 语法 对应的Lambda表达式
静态方法引用 类名::staticMethod (args) -> 类名.staticMethod(args)
实例方法引用 inst::instMethod (args) -> inst.instMethod(args)
对象方法引用 类名::instMethod (inst,args) -> 类名.instMethod(args)
构建方法引用 类名::new (args) -> new 类名(args)

2.3 方法引用举例

2.3.1 静态方法引用

有一个Person类,如下所示:

package top.saodisheng.methodreference;

import lombok.Data;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
@Data
public class Person {
    private String name;
    private Integer age;
    private LocalDateTime birthday;
    public static int compareByAge(Person a, Person b) {
        return a.age.compareTo(b.age);
    }
}

现假设,一个部门有30人,把他们存放在一个数组中,并按年龄排序,通常我们可以自己写一个比较器,代码如下:

Person[] rosterAsArray = new Person[30];
// 添加数组元素省略

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}       
Arrays.sort(rosterAsArray, new PersonAgeComparator());

Arrays.sort的声明为:public static void sort(T[] a, Comparator<? super T> c),比较器参数Comparator为一个函数式接口,利用上一节Lambda表达式所学知识,可以改写为以下代码:

Person[] rosterAsArray = new Person[30];
// 添加数组元素省略

Arrays.sort(rosterAsArray, (a,b) -> a.getAge().compareTo(b.getAge()));

然而,你会发现,Perdon类中已经有了一个静态方法的比较器:compareByAge,因此,我们改用Person类已经提供的比较器:

Person[] rosterAsArray = new Person[30];
// 添加数组元素省略

Arrays.sort(rosterAsArray, (a,b) -> Person.compareByAge(a,b));

以上代码,因为Lambda表达式调用了一个已经存在的静态方法,根据我们第2节表格中的语法,上面的代码可以最终改写成静态方法引用:

Person[] rosterAsArray = new Person[30];
// 添加数组元素省略

Arrays.sort(rosterAsArray, Person::compareByAge);

下面这个例子更简单:

public class Test {
    public static void main(String[] args) { 
        List<Integer> list = Arrays.asList(82,22,34,50,9);
        list.sort(Integer::compare);
        System.out.println(list); 
    }
}

引用的方式来调用 ,运行结果为:

[9, 22, 34, 50, 82]

2.3.2 实例方法引用

实例方法引用,顾名思义就是调用已经存在的实例的方法,与静态方法引用不同的是类要先实例化,静态方法引用类无需实例化,直接用类名去调用

package top.saodisheng.instancereference;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
@Data
@AllArgsConstructor
public class User {
    private String Name;
    private int age;
}

---------------------------------------------------------------
package top.saodisheng.instancereference;

import org.junit.Test;

import java.util.function.Supplier;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class InstanceReferenceTest {
    @Test
    public void test1() {
        User user = new User("扫地生", 32);
        Supplier<String> supplier = () -> user.getName();
        System.out.println("Lambda表达式输出结果:" + supplier.get());
        
        Supplier<String> supplier2 = user::getName;
        System.out.println("实例方法引用输出结果:" + supplier2.get());
    }
}

输出结果:

image-20210723134425171

2.3.3 对象方法引用

若Lambda参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数时,可以使用对象方法引用。

String的equals()方法:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i]) {
                    return false;
                }
                i++;
            }
            return true;
        }
    }
    return false;
}
public static void main(String[] args) {
    // x为实例方法(String的equals方法)的调用者,y为实例方法的参数
   BiPredicate<String,String> bp = (x, y) -> x.equals(y);
   BiPredicate<String,String> bp1 = String::equals;

   boolean test = bp1.test("xy", "xx");
   System.out.println(test);
}

BiPredicate的test()方法接受两个参数,x和y,具体实现为x.equals(y),满足Lambda参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数,因此可以使用对象方法引用。

2.3.4 构造方法引用

注意:需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表保持一致。

如:要获取一个空的User列表:

Supplier<List<User>> userSupplier = () -> new ArrayList<>();
List<User> user = userSupplier.get();
Supplier<List<User>> userSupplier2 = ArrayList<User>::new;    // 构造方法引用写法
List<User> user2 = userSupplier.get();

3 Stream API

Stream 是Java8中处理集合的关键抽象概念,它可以对集合进行非常复杂的查找、过滤、筛选等操作,以上的操作在实际开发中有很大的用途。好在新版的JPA中,也已经加入了Stream。如:

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

Stream API给我们操作集合带来了强大的功用,同时Stream API操作简单,容易上手。

3.1 Stream的操作步骤

Stream有如下三个操作步骤:

  1. 创建Stream

    从一个数据源,如集合、数组中获取流。

  2. 中间操作

    一个操作的中间链,对数据源的数据进行操作(起初就是因为这点,各种链式编程让我觉得眼花缭乱的。好家伙,第一印象不是很好呀。但是一切“困难”都是纸老虎)。

  3. 终止操作

    一个终止操作,执行中间操作链,并产生结果。

要注意的是,对流的操作完成后需要进行关闭操作(或者用JAVA7的try-with-resources)。

举个简单的例子:

假设有一个Person类和一个Person列表,现在有两个需求:

  • 找到年龄大于4岁的人并输出;
  • 找出所有中国人的数量。
package top.saodisheng.streamapi;

import lombok.Builder;
import lombok.Data;
/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/22
 */
@Data
@Builder
public class Person {
    private String name;
    private Integer age;
    private String country;
    private String sex;
}

在JDK8以前,我们可以通过遍历列表来完成。但是在有了Stream API后,可以这样来实现:

package top.saodisheng.streamapi;

import com.blinkfox.minitable.MiniTable;
import com.sun.istack.internal.Nullable;
import de.vandermeer.asciitable.AsciiTable;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/22
 */
public class StreamAPITest {
    private static List<Person> personList = new ArrayList<>();


    @BeforeClass
    public static void initPersonList(){
        personList.add(Person.builder().name("zhangsan").age(3).country("china").sex("F").build());
        personList.add(Person.builder().name("lisi").age(5).country("pakistan").sex("M").build());
        personList.add(Person.builder().name("wangwu").age(4).country("russin").sex("M").build());
        personList.add(Person.builder().name("zhaoliu").age(7).country("china").sex("F").build());
        personList.add(Person.builder().name("sunqi").age(1).country("china").sex("F").build());
        personList.add(Person.builder().name("nico").age(2).country("usa").sex("M").build());
        personList.add(Person.builder().name("nico").age(2).country("usa").sex("M").build());
        showInMiniTable();
    }
    
    @Nullable
    public static void showInMiniTable() {
        final MiniTable miniTable = new MiniTable("PersonList").addHeaders("name", "age", "country", "sex");
        personList.forEach(p -> {
            miniTable.addDatas(p.getName(), p.getAge(), p.getCountry(), p.getSex());
        });
        System.out.println(miniTable.render());
    }
    
    @Test
    public void test() {
        System.out.println("找出年龄大于4岁的人并输出");
        personList.stream().filter((p) -> p.getAge() > 4).forEach(System.out::println);
        System.out.println("找出中国人的数量");
        long count = personList.stream().filter((p) -> "china".equals(p.getCountry())).count();
        System.out.println("中国人的数量为:" + count);
    }
}

输出结果:

image-20210722201653671

在这个例子中,personList.stream()是创建流,filter()属于中间操作,forEach、count()是终止操作。

3.2 Stream中间操作--筛选与切片

  • filter:接收Lambda,从流中过滤一些数据,得到符合指定条件的内容(最终得到的数据单元仍和原始一样);
  • limit:截断流,使其元素不超过给定对象
  • skip(n):跳过元素,返回一个扔掉了前n个元素的流,若流中元素不足n个,则返回一个空流,与limit(n)互补
  • distinct:筛选,通过流所生成元素的hashCode()和equals()去除重复元素。

3.2.1 limit举例

需求,从Person列表中取出两个女性。

@Test
public void test2() {
    System.out.println("从PersonList取出两个女性");
    System.out.println("personList.stream().filter(p -> "F".equals(p.getSex())).limit(2).forEach(System.out::println)");
    System.out.println("结果如下");
    personList.stream().filter(p -> "F".equals(p.getSex())).limit(2).forEach(System.out::println);
}

输出结果为:

image-20210722201742314

3.2.2 skip举例

从Person列表中从第2个女性开始,取出所有的女性。

@Test
public void test3() {
    System.out.println("从personList列表中第2个女性开始,取出所有女性");
    System.out.println("personList.stream().filter(p -> "F".equals(p.getSex())).skip(1).forEach(System.out::println)");
    System.out.println("结果如下");
    personList.stream().filter(p -> "F".equals(p.getSex())).skip(1).forEach(System.out::println);
}

image-20210722201822502

3.2.3 distinct举例

从列表中取出所有男性,并去重

@Test
public void test4() {
    System.out.println("从personList列表中取出所有男性,并去重");
    System.out.println("personList.stream().filter(p -> "M".equals(p.getSex())).distinct().forEach(System.out::println)");
    System.out.println("结果如下");
    personList.stream().filter(p -> "M".equals(p.getSex())).distinct().forEach(System.out::println);
}

image-20210722202024282

3.3 Stream中间操作--映射

  • map--接收Lambda,将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素(新的流),最终以多个流的结果返回
  • flatMap--接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

3.3.1 map举例

例1:比如,我们用一个PersonCountry类来接收所有的国家信息:

@Data
public class PersonCountry {
    private String country;
}

@Test
public void test5() {
    System.out.println("用一个PersonCountry类来接受所有国家的信息");
    personList.stream().map(p -> {
        PersonCountry countryNames = new PersonCountry();
        countryNames.setCountry(p.getCountry());
        return countryNames;
    }).distinct().forEach(System.out::println);
}

image-20210722203859725

例2:假如有一个字符列表,需要提出每一个字符

List<String> list = Arrays.asList("aaa","bbb","ccc","ddd","ddd");

代码如下:

public static Stream<Character> getCharacterByString(String str) {
    List<Character> characterList = new ArrayList<>();
    for (Character character : str.toCharArray()) {
        characterList.add(character);
    }
    return characterList.stream();
}
@Test
public void test6() {
    List<String> list = Arrays.asList("aaa","bbb","ccc","ddd","ddd");
    // 结果是流中流
    final Stream<Stream<Character>> streamStream
            = list.stream().map(StreamAPITest::getCharacterByString); 
    // 结果输出的是每个流的逻辑地址
    streamStream.forEach(System.out::println);
}

运行结果:

image-20210722205933773

从输出结果及返回结果类型(Stream<Stream>)可以看出这是一个流中流,要想打印出我们想要的结果,需要对流中的每个流进行打印:

streamStream.forEach(sm -> sm.forEach(System.out::print));

image-20210722210204516

但我们希望的是返回的是一个流,而不是一个包含了多个流的流,而flatMap可以帮助我们做到这一点。

3.3.2 flatMap举例

改写上面的方法,将map改成flatMap:

@Test
public void test7() {
    List<String> list = Arrays.asList("aaa","bbb","ccc","ddd","ddd");
    // 结果是一个流
    final Stream<Character> streamStream = list.stream().flatMap(StreamAPITest::getCharacterByString);
    streamStream.forEach(System.out::print);
}

运行结果为:

image-20210722224614172

3.3.3 map和flatMap的图解

map图解:

img

map在接收到流后,直接将Stream放入到一个Stream中,最终整体返回一个包含了多个Stream的Stream。

flatMap图解:

img

flatMap在接收到Stream后,会将接收到的Stream中的每个元素取出来放入一个Stream中,最后将一个包含多个元素的Stream返回。

3.4 Stream中间操作--排序

  • sorted()--自然排序(Comparable)
  • sorted(Comparator com)--定制排序(Comparator)

自然排序比较好理解,这里只讲一下定制排序,对前面的personList按年龄从小到大排序,年龄相同,则再按姓名排序:

@Test
public void test8() {
    System.out.println("对personList按年龄从小到大排序,年龄相同,则再按姓名排序");
    personList.stream().sorted((p1, p2) -> {
        if (p1.getAge().equals(p2.getAge())) {
            return p1.getName().compareTo(p2.getName());
        } else {
            return p1.getAge().compareTo(p2.getAge());
        }
    }).forEach(System.out::println);
}

运行结果:

image-20210722230641569

3. 5 终止操作--查找与匹配

  • allMatch--检查是否匹配所有元素
  • anyMatch--检查是否至少匹配一个元素
  • noneMatch--检查是否没有匹配所有元素
  • findFirst--返回第一个元素
  • findAny--返回当前流中的任意元素
  • count--返回流中元素的总个数
  • max--返回流中最大值
  • min--返回流中最小值

这些方面在Stream类中都有说明,这里不一一举例,只对allMatch、max各举一例进行说明。

3.5.1 allMatch

@Test
public void test9() {
    System.out.print("判断personList中是否都是大于等于1岁的小孩子:");
    System.out.println(personList.stream().allMatch(p -> p.getAge() >= 1));
    System.out.print("判断personList中是否都是中国人:");
    System.out.println(personList.stream().allMatch(p -> "china".equals(p.getCountry())));
}

运行结果:

image-20210723000108321

3.5.1 max min

@Test
public void test10() {
    System.out.println("找到personList中年龄最小的孩子,其信息如下");
    Optional<Person> maxAge = personList.stream().max((p1, p2) -> p1.getAge().compareTo(p2.getAge()));
    System.out.println(maxAge.get());
    System.out.println("找到personList中年龄最大的孩子,其信息如下");
    Optional<Person> minAge = personList.stream().min((p1, p2) -> p1.getAge().compareTo(p2.getAge()));
    System.out.println(minAge.get());
}

运行结果:

image-20210723000606186

3.6 归约

Stream API的归约操作可以将流中元素反复结合起来,得到一个值,有:

Optional<T> reduce(BinaryOperator<T> accumulator);

T reduce(T identity, BinaryOperator<T> accumulator);

<U> U reduce(U identity,
             BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner);

3.6.1 求一个1到100的和

@Test
public void test11() {
    System.out.println("求1到100的和");
    List<Integer> integerList = new ArrayList<>();
    for (int i = 1; i <= 100; i++) {
        integerList.add(1);
    }
    final Integer reduce = integerList.stream().reduce(0, (x, y) -> x + y);
    System.out.println("结果为:" + reduce);
}

image-20210723085130580

这个例子用到了reduce第二个方法:T reduce(T identity, BinaryOperator accumulator)

把这个动作拆解一下,其运算步骤模拟如下:

0  (1,2) -> 1 + 2 + 0     
3  (3,4) -> 3 + 4 + 3
10 (5,6) -> 5 + 6 + 10
.
.
.

其运算步骤是,每次将列表的两个元素相加,并将结果与前一次的两个元素的相加结果进行累加,因此,在开始时,将identity设为0,因为第1个元素和第2个元素在相加的时候,前面还没有元素操作过。

3.6.2 求所有人的年龄之和

@Test
public void test12() {
    System.out.println("求personList中所有年龄的总和");
    final Optional<Integer> reduce = personList.stream().map(Person::getAge).reduce(Integer::sum);
    System.out.println("结果为:" + reduce);
}

image-20210723085719140

3.7 收集

collect:将流转换为其他形式,接收一个Collector接口实现 ,用于给Stream中汇总的方法

<R, A> R collect(Collector<? super T, A, R> collector);

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

collect不光可以将流转换成其他集合等形式,还可以进行归约等操作,具体实现也很简单,主要是与Collectors类搭配使用。

3.7.1 改写3.3.1 map举例中的的例子,将国家收集起来转换成List

@Test
public void test13() {
    List<String> collect = personList.stream().map(p -> p.getCountry()).distinct().collect(Collectors.toList());
    System.out.println(collect);
}

输出结果:

image-20210723090354439

3.7.2 计算出平均年龄

@Test
public void test14() {
    final Double collect = personList.stream().collect(Collectors.averagingInt(p -> p.getAge()));
    System.out.println("平均年龄为:" + collect);
}

输出结果:

image-20210723091035799

3.7.3 找出最小年龄、最大年龄

@Test
public void test15() {
    final Optional<Integer> maxAge = personList.stream().map(Person::getAge).collect(Collectors.maxBy(Integer::compareTo));
    System.out.println("最大年龄为:" + maxAge);
    final Optional<Integer> minAge = personList.stream().map(Person::getAge).collect(Collectors.minBy(Integer::compareTo));
    System.out.println("最大年龄为:" + minAge);
}

image-20210723091453544

还有其他很操作,可以参考java.util.stream.Collectors。

3.8 完整测试代码

package top.saodisheng.streamapi;

import com.blinkfox.minitable.MiniTable;
import com.sun.istack.internal.Nullable;
import de.vandermeer.asciitable.AsciiTable;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/22
 */
public class StreamAPITest {
    private static List<Person> personList = new ArrayList<>();
    @Test
    public void test1() {
        System.out.println("找出年龄大于4岁的人并输出");
        personList.stream().filter((p) -> p.getAge() > 4).forEach(System.out::println);
        System.out.println("找出中国人的数量");
        long count = personList.stream().filter((p) -> "china".equals(p.getCountry())).count();
        System.out.println("中国人的数量为:" + count);
    }

    @Test
    public void test2() {
        System.out.println("从PersonList取出两个女性");
        System.out.println("personList.stream().filter(p -> "F".equals(p.getSex())).limit(2).forEach(System.out::println)");
        System.out.println("结果如下");
        personList.stream().filter(p -> "F".equals(p.getSex())).limit(2).forEach(System.out::println);
    }

    @Test
    public void test3() {
        System.out.println("从personList列表中第2个女性开始,取出所有女性");
        System.out.println("personList.stream().filter(p -> "F".equals(p.getSex())).skip(1).forEach(System.out::println)");
        System.out.println("结果如下");
        personList.stream().filter(p -> "F".equals(p.getSex())).skip(1).forEach(System.out::println);
    }

    @Test
    public void test4() {
        System.out.println("从personList列表中取出所有男性,并去重");
        System.out.println("personList.stream().filter(p -> "M".equals(p.getSex())).distinct().forEach(System.out::println)");
        System.out.println("结果如下");
        personList.stream().filter(p -> "M".equals(p.getSex())).distinct().forEach(System.out::println);
    }

    @Test
    public void test5() {
        System.out.println("用一个PersonCountry类来接受所有国家的信息");
        personList.stream().map(p -> {
            PersonCountry countryNames = new PersonCountry();
            countryNames.setCountry(p.getCountry());
            return countryNames;
        }).distinct().forEach(System.out::println);
    }

    public static Stream<Character> getCharacterByString(String str) {
        List<Character> characterList = new ArrayList<>();
        for (Character character : str.toCharArray()) {
            characterList.add(character);
        }
        return characterList.stream();
    }
    @Test
    public void test6() {
        List<String> list = Arrays.asList("aaa","bbb","ccc","ddd","ddd");
        final Stream<Stream<Character>> streamStream
                = list.stream().map(StreamAPITest::getCharacterByString);
//        streamStream.forEach(System.out::println);
        streamStream.forEach(sm -> sm.forEach(System.out::print));
    }

    @Test
    public void test7() {
        List<String> list = Arrays.asList("aaa","bbb","ccc","ddd","ddd");
        final Stream<Character> streamStream = list.stream().flatMap(StreamAPITest::getCharacterByString);
        streamStream.forEach(System.out::print);
    }

    @Test
    public void test8() {
        System.out.println("对personList按年龄从小到大排序,年龄相同,则再按姓名排序");
        personList.stream().sorted((p1, p2) -> {
            if (p1.getAge().equals(p2.getAge())) {
                return p1.getName().compareTo(p2.getName());
            } else {
                return p1.getAge().compareTo(p2.getAge());
            }
        }).forEach(System.out::println);
    }

    @Test
    public void test9() {
        System.out.print("判断personList中是否都是大于等于1岁的小孩子:");
        System.out.println(personList.stream().allMatch(p -> p.getAge() >= 1));
        System.out.print("判断personList中是否都是中国人:");
        System.out.println(personList.stream().allMatch(p -> "china".equals(p.getCountry())));
    }

    @Test
    public void test10() {
        System.out.println("找到personList中年龄最小的孩子,其信息如下");
        Optional<Person> maxAge = personList.stream().max((p1, p2) -> p1.getAge().compareTo(p2.getAge()));
        System.out.println(maxAge.get());
        System.out.println("找到personList中年龄最大的孩子,其信息如下");
        Optional<Person> minAge = personList.stream().min((p1, p2) -> p1.getAge().compareTo(p2.getAge()));
        System.out.println(minAge.get());
    }

    @Test
    public void test11() {
        System.out.println("求1到100的和");
        List<Integer> integerList = new ArrayList<>();
        for (int i = 1; i <= 100; i++) {
            integerList.add(1);
        }
        final Integer reduce = integerList.stream().reduce(0, (x, y) -> x + y);
        System.out.println("结果为:" + reduce);
    }

    @Test
    public void test12() {
        System.out.println("求personList中所有年龄的总和");
        final Optional<Integer> reduce = personList.stream().map(Person::getAge).reduce(Integer::sum);
        System.out.println("结果为:" + reduce);
    }

    @Test
    public void test13() {
        final List<String> collect = personList.stream().map(p -> p.getCountry()).distinct().collect(Collectors.toList());
        System.out.println(collect);
    }

    @Test
    public void test14() {
        final Double collect = personList.stream().collect(Collectors.averagingInt(p -> p.getAge()));
        System.out.println("平均年龄为:" + collect);
    }

    @Test
    public void test15() {
        final Optional<Integer> maxAge = personList.stream().map(Person::getAge).collect(Collectors.maxBy(Integer::compareTo));
        System.out.println("最大年龄为:" + maxAge);
        final Optional<Integer> minAge = personList.stream().map(Person::getAge).collect(Collectors.minBy(Integer::compareTo));
        System.out.println("最大年龄为:" + minAge);
    }


    @BeforeClass
    public static void initPersonList(){
        personList.add(Person.builder().name("zhangsan").age(3).country("china").sex("F").build());
        personList.add(Person.builder().name("lisi").age(5).country("pakistan").sex("M").build());
        personList.add(Person.builder().name("wangwu").age(4).country("russin").sex("M").build());
        personList.add(Person.builder().name("zhaoliu").age(7).country("china").sex("F").build());
        personList.add(Person.builder().name("sunqi").age(1).country("china").sex("F").build());
        personList.add(Person.builder().name("nico").age(2).country("usa").sex("M").build());
        personList.add(Person.builder().name("nico").age(2).country("usa").sex("M").build());
        showInMiniTable();
    }

    @Nullable
    public static void showInMiniTable() {
        final MiniTable miniTable = new MiniTable("PersonList").addHeaders("name", "age", "country", "sex");
        personList.forEach(p -> {
            miniTable.addDatas(p.getName(), p.getAge(), p.getCountry(), p.getSex());
        });
        System.out.println(miniTable.render());
    }
}

---------------------------------对象类-----------------------------------=
    package top.saodisheng.streamapi;

import lombok.Builder;
import lombok.Data;
/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/22
 */
@Data
@Builder
public class Person {
    private String name;
    private Integer age;
    private String country;
    private String sex;
}
package top.saodisheng.streamapi;

import lombok.Data;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/22
 */
@Data
public class PersonCountry {
    private String country;
}

4 接口默认方法和静态方法

在JDK1.8以前,接口(interface)没有提供任何具体的实现,在《JAVA编程思想》中是这样描述的:“interface这个关键字产生了一个完全抽象的类,它根本就没有提供任何具体的实现。它允许创建者确定方法名、参数列表和返回类型,但是没有任何方法体。接口只提供了形式,而未提供任何具体实现”。

但是这一限制在JDK1.8中被打破了,JDK1.8开始,接口允许定义默认方法和静态方法。

接口默认方法的语法很简单,即:

default关键字 methodName(参数列表) { // 实现体 }

接口静态方法语法与类的静态方法类似,不同的是接口静态方法的修饰符只能是public。

4.1 默认方法

为了提高代码的可重用性。接口的默认方法有助于在扩展系统功能的同时,不对现有的继承关系及类库产生很大的影响。例如在JDK1.8中,Java集合框架的Collection接口增加了stream()等默认方法,这些默认方法即增强了集合的功能,又能保证对低版本的JDK的兼容。

举个简单的例子,假如有一个Animal接口其中有fly()和swim()方法,有一个鸟类Bird和一个鱼类Fish同时实现这个接口,代码如下:

Animal接口

package top.saodisheng.defaultandstaticinstance;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public interface Animal {
    void fly();
    void swim();
}

Bird.java

package top.saodisheng.defaultandstaticinstance;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class Bird implements Animal{
    @Override
    public void fly() {
        System.out.println("飞得更高");
    }

    @Override
    public void swim() {
        System.out.println("就不会,我骄傲了吗");
    }
}

Fish.java

package top.saodisheng.defaultandstaticinstance;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class Fish implements Animal{
    @Override
    public void fly() {
        System.out.println("飞鱼吗?不存在的");
    }

    @Override
    public void swim() {
        System.out.println("游泳?老司机了");
    }
}

从上代码可以看到,因为Animal中定义了fly()和swim()方法,所以所有实现它的类都要覆写这两个方法,在Bird类中,鸟会飞,不会游泳,但是又必须要实现swim()方法,Fish类不会飞,但是又必须要实现fly()方法。代码出现冗余。

假如现在又有了新的需求,需要在Animal接口中再增加一个cry()方法,那么之前所有实现了Animal接口的方法势必都在再覆写cry()方法,整个系统中可能会有很多地方需要同步修改,而此时,default方法和静态方法就显得尤为必要了。

改写上面的例子:

Animal.java

package top.saodisheng.defaultandstaticinstance;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public interface Animal {
    default void fly() {
        System.out.println("飞鸟专属该方法");
    };
    default void swim() {
        System.out.println("鱼儿专属喔");
    };
}

Bird.java

package top.saodisheng.defaultandstaticinstance;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class Bird implements Animal{
}

Fish.java

package top.saodisheng.defaultandstaticinstance;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class Fish implements Animal{
}

测试类:

@Test
public void test1() {
    Bird bird = new Bird();
    bird.fly();
    Fish fish = new Fish();
    fish.swim();
}

运行结果:

image-20210723141900081

从修改后代码可以看出,代码得到了复用,Animal实现类中也没有了冗余。

4.2 静态方法

假如有一个Animal工厂接口,该接口中有一个静态方法create()专门生产不同的Animal,在JDK1.8后由于引入了Lambda表达式,使子类不用覆写该接口的create()方法也可以生产任意的Animal,代码如下:

package top.saodisheng.defaultandstaticinstance;

import java.util.function.Supplier;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class AnimalFactory {
    static Animal create(Supplier<Animal> supplier) {
        return supplier.get();
    }
}

测试类:

@Test
public void test2() {
    // 生产一只鸟
    Animal bird = AnimalFactory.create(Bird::new);
    bird.fly();
    // 生产一条鱼
    Animal fish = AnimalFactory.create(Fish::new);
    fish.swim();
}

4.3 接口静态方法的“类优先”原则

如果一个接口实现类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略,如改写之前的Bird类:

package top.saodisheng.defaultandstaticinstance;

/**
 * description:
 *
 * @author 扫地生_saodisheng
 * @date 2021/7/23
 */
public class Bird implements Animal{
    @Override
    public void fly() {
        System.out.println("鸟儿自个的方法");
    }
}

测试类:

@Test
public void test3() {
    Animal bird = AnimalFactory.create(Bird::new);
    bird.fly();
}

image-20210723142726464

可见,调用的是Bird类中自己的fly()方法而不是Animal接口中的默认方法。

4.4 接口冲突

假如一个类实现了两个接口,两个接口中都有同样的默认方法,哪个是有效的?

答案是:两个都无效!

该类必须要覆该方法来解决冲突,否则编译器将会报错。

5 Optional

NullPointerException相信每个JAVA程序员都不陌生,是JAVA应用程序中最常见的异常。之前,Google Guava项目曾提出用Optional类来包装对象从而解决NullPointerException。受此影响,JDK8的类中也引入了Optional类,在新版的SpringData Jpa和Spring Redis Data中都已实现了对该方法的支持。

5.1 Optional类

/**
 * A container object which may or may not contain a non-null value.
 * If a value is present, {@code isPresent()} will return {@code true} and
 * {@code get()} will return the value.
 *
 * @since 1.8
 */
public final class Optional<T> {
    /**
     * Common instance for {@code empty()}.
     */
    private static final Optional<?> EMPTY = new Optional<>();

    /**
     * If non-null, the value; if null, indicates no value is present
     */
    private final T value;

   // 其他省略
}

该方法的注释大致意思是:Optional是一个容器对象,它可能包含空值,也可能包含非空值。当属性value被设置时,isPesent()方法将返回true,并且get()方法将返回这个值。

该类支持泛型,即其属性value可以是任何对象的实例。

5.2 Optional类的方法

序号 方法 方法说明
1 private Optional() 无参构造,构造一个空Optional
2 private Optional(T value) 根据传入的非空value构建Optional
3 public static Optional empty() 返回一个空的Optional,该实例的value为空
4 public static Optional of(T value) 根据传入的非空value构建Optional,与Optional(T value)方法作用相同
5 public static Optional ofNullable(T value) 与of(T value)方法不同的是,ofNullable(T value)允许你传入一个空的value,当传入的是空值时其创建一个空Optional,当传入的value非空时,与of()作用相同
6 public T get() 返回Optional的值,如果容器为空,则抛出NoSuchElementException异常
7 public boolean isPresent() 判断当前Optional是否已设置了值
8 public void ifPresent(Consumer<? super T> consumer) 判断当前Optional是否已设置了值,如果有值,则调用Consumer函数式接口进行处理
9 public Optional filter(Predicate<? super T> predicate) 如果设置了值,且满足Predicate的判断条件,则返回该Optional,否则返回一个空的Optional
10 public Optional map(Function<? super T, ? extends U> mapper) 如果Optional设置了value,则调用Function对值进行处理,并返回包含处理后值的Optional,否则返回空Optional
11 public Optional flatMap(Function<? super T, Optional> mapper) 与map()方法类型,不同的是它的mapper结果已经是一个Optional,不需要再对结果进行包装
12 public T orElse(T other) 如果Optional值不为空,则返回该值,否则返回other
13 public T orElseGet(Supplier<? extends T> other) 如果Optional值不为空,则返回该值,否则根据other另外生成一个
14 public T orElseThrow(Supplier<? extends X> exceptionSupplier)throws X 如果Optional值不为空,则返回该值,否则通过supplier抛出一个异常

5.3 Optional类的方法举例

《Java 8 Features Tutorial – The ULTIMATE Guide》中给出了两个简单的例子,我们从这两个例子入手来简单了解一下Optional容器的使用。

示例一:

@Test
public void test1() {
    Optional<Object> fullName = Optional.ofNullable(null);
    System.out.println("Full name is set? " + fullName.isPresent());
    System.out.println("Full name : " + fullName.orElseGet(() -> "[none]"));
    System.out.println(fullName.map(s -> "Hey" + s + "!").orElse("Hey Stranger!"));
}

运行结果:

Full Name is set? false
Full Name: [none]
Hey Stranger!  

说明:

ifPresent()方法当Optional实例的值非空时返回true,否则返回false;

orElseGet()方法当Optional包含非空值时返回该值,否则通过接收的function生成一个默认的;

map()方法转换当前Optional的值,并返回一个新的Optional实例;

orElse()方法与orElseGet方法相似,不同的是orElse()直接返回传入的默认值。

示例二:修改示例一,使其生成一个非空值的Optional实例

@Test
public void test2() {
    Optional<Object> fullName = Optional.ofNullable("Tom");
    System.out.println("Full name is set? " + fullName.isPresent());
    System.out.println("Full name : " + fullName.orElseGet(() -> "[none]"));
    System.out.println(fullName.map(s -> "Hey" + s + "!").orElse("Hey Stranger!"));
}

输出结果:

First Name is set? true
First Name: Tom
Hey Tom!

 

可以清晰地看出与示例一的区别。这不但简洁了我们的代码,而且使我们的代码更便于阅读。

下面看一下例子中使用到的几个方法的源码:

of

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

isPresent

public boolean isPresent() {
    return value != null;
}

orElseGet

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

orElse

public T orElse(T other) {
    return value != null ? value : other;
}

其他方法源码,读者可以去Optional源码中查看。

5.4 使用Optional避免空指针

在我们日常开发过程中不可避免地会遇到空指针问题,在以前,出现空指针问题,我们通常需要进行调试等方式才能最终定位到具体位置,尤其是在分布式系统服务之间的调用,问题更难定位。在使用Optional后,我们可以将接受到的参数对象进行包装,比如,订单服务要调用商品服务的一个接口,并将商品信息通过参数传入,这时候,传入的商品参数可能直接传入的就是null,这时,商品方法可以使用Optional.of(T)对传入的对象进行包装,如果T为空,则会直接抛出空指针异常,我们看到异常信息就能立即知道发生空指针的原因是参数T为空;或者,当传入的参数为空时,我们可以使用Optional.orElse()或Optional.orElseGet()方法生成一个默认的实例,再进行后续的操作。

下面再看个具体例子:在User类中有个Address类,在Address类中有个Street类,Street类中有streetName属性,现在的需求是:根据传入的User实例,获取对应的streetName,如果User为null或Address为null或Street为null,返回“nothing found”,否则返回对应的streetName。

实现一:

@Data
public class User {
    private String name;
    private Integer age;
    private Address address;
}
@Data
public class Address {
    private Street street;
}
@Data
public class Street {
    private String streetName;
    private Integer streetNo;
}
public String getUserSteetName(User user) {

    if(null != user) {

        Address address = user.getAddress();

        if(null != address) {

            Street street = address.getStreet();

            if(null != street) {
                return street.getStreetName();
            }
        }
    }

    return "nothing found";
}

实现二,使用Optional:

在实现一中明显的问题是if判断层级太深,下面复用Optional改写:

@Data
public class User {
    private String name;
    private Integer age;
    private Optional<Address> address = Optional.empty();
}
@Data
public class Address {
    private Optional<Street> street = Optional.empty();
}
@Data
public class Street {
    private String streetName;
    private Integer streetNo;
}
public String getUserSteetName(User user) {

    Optional<User> userOptional = Optional.ofNullable(user);
    final String streetName = userOptional.orElse(new User()).getAddress().orElse(new Address()).getStreet().orElse(new Street()).getStreetName();
    return StringUtils.isEmpty(streetName) ? "nothing found" : streetName;
}

利用orElse()方法给定默认值的方式确保不会报空指针问题问题,同时也能实现需求。

6 学习素材

学习素材

向大神看齐
原文地址:https://www.cnblogs.com/Liu-xing-wu/p/15054151.html