尚硅谷Java8 新特性学习笔记

Java 8

一、介绍

1、生态

  • Lambda 表达式
  • 函数式接口
  • 方法引用 / 构造器引用
  • Stream API
  • 接口中的默认方法 / 静态方法
  • 新时间日期 API
  • 其他新特性

2、新特性

  • 速度更快
  • 代码更少
  • 强大的 Stream API
  • 便于并行
  • 最大化减少空指针异常 Optional (Kotlin ?)

3、温故而知新

  • Hashmap 底层结构/原理 老话题不再阐述 …

  • 并发hashmap …

  • Java虚拟机 …

  • Java内存模型 …

二、Lambda 表达式

Lambda是一个匿名函数,可以理解为一段可以传递的代码(将代码像数据一样传递);可以写出更简洁、更灵活的代码;作为一种更紧凑的代码风格,是Java语言表达能力得到提升

1、从匿名类到 Lambda 的转换

演变过程:

  • 垃圾代码 --> 策略模式 --> 匿名内部类 --> Lambda表达式

Employee 实体类:

package com.yyff.java8;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Employee {

    private Integer id;
    private String name;
    private Integer age;
    private Double salary;

    public String show() {
        return "测试方法引用!";
    }
}

自定义函数式接口:

@FunctionalInterface
public interface MyPredicate<T> {

	public boolean test(T t);
	
}

过滤工资大于5000的实现类:

public class FilterEmployeeForSalary implements MyPredicate<Employee> {

	@Override
	public boolean test(Employee t) {
		return t.getSalary() >= 5000;
	}

}

过滤出工资大于5000的员工实现类:

public class FilterEmployeeForSalary implements MyPredicate<Employee> {
    /**
     * 过滤出工资大于5000的员工
     * @param employee
     * @return
     */
    @Override
    public boolean test(Employee employee) {
        return employee.getSalary() > 5000;
    }
}

测试类:

public class TestLambda1 {
	
	//原来的匿名内部类
	@Test
	public void test1(){
		Comparator<String> com = new Comparator<String>(){
			@Override
			public int compare(String o1, String o2) {
				return Integer.compare(o1.length(), o2.length());
			}
		};
		
		TreeSet<String> ts = new TreeSet<>(com);
		
		TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>(){
			@Override
			public int compare(String o1, String o2) {
				return Integer.compare(o1.length(), o2.length());
			}
			
		});
	}
	
	//现在的 Lambda 表达式
	@Test
	public void test2(){
		Comparator<String> com = (x, y) -> Integer.compare(x.length(), y.length());
		TreeSet<String> ts = new TreeSet<>(com);
	}
	
	List<Employee> emps = Arrays.asList(
			new Employee(101, "张三", 18, 9999.99),
			new Employee(102, "李四", 59, 6666.66),
			new Employee(103, "王五", 28, 3333.33),
			new Employee(104, "赵六", 8, 7777.77),
			new Employee(105, "田七", 38, 5555.55)
	);

	//需求:获取公司中年龄小于 35 的员工信息
	public List<Employee> filterEmployeeAge(List<Employee> emps){
		List<Employee> list = new ArrayList<>();
		
		for (Employee emp : emps) {
			if(emp.getAge() <= 35){
				list.add(emp);
			}
		}
		
		return list;
	}
	
	@Test
	public void test3(){
		List<Employee> list = filterEmployeeAge(emps);
		
		for (Employee employee : list) {
			System.out.println(employee);
		}
	}
	
	//需求:获取公司中工资大于 5000 的员工信息
	public List<Employee> filterEmployeeSalary(List<Employee> emps){
		List<Employee> list = new ArrayList<>();
		
		for (Employee emp : emps) {
			if(emp.getSalary() >= 5000){
				list.add(emp);
			}
		}
		
		return list;
	}
	
	//优化方式一:策略设计模式
	public List<Employee> filterEmployee(List<Employee> emps, MyPredicate<Employee> mp){
		List<Employee> list = new ArrayList<>();
		
		for (Employee employee : emps) {
			if(mp.test(employee)){
				list.add(employee);
			}
		}
		
		return list;
	}
	
	@Test
	public void test4(){
		List<Employee> list = filterEmployee(emps, new FilterEmployeeForAge());
		for (Employee employee : list) {
			System.out.println(employee);
		}
		
		System.out.println("------------------------------------------");
		
		List<Employee> list2 = filterEmployee(emps, new FilterEmployeeForSalary());
		for (Employee employee : list2) {
			System.out.println(employee);
		}
	}
	
	//优化方式二:匿名内部类
	@Test
	public void test5(){
		List<Employee> list = filterEmployee(emps, new MyPredicate<Employee>() {
			@Override
			public boolean test(Employee t) {
				return t.getId() <= 103;
			}
		});
		
		for (Employee employee : list) {
			System.out.println(employee);
		}
	}
	
	//优化方式三:Lambda 表达式
	@Test
	public void test6(){
		List<Employee> list = filterEmployee(emps, (e) -> e.getAge() <= 35);
		list.forEach(System.out::println);
		
		System.out.println("------------------------------------------");
		
		List<Employee> list2 = filterEmployee(emps, (e) -> e.getSalary() >= 5000);
		list2.forEach(System.out::println);
	}
	
	//优化方式四:Stream API
	@Test
	public void test7(){
		emps.stream()
			.filter((e) -> e.getAge() <= 35)
			.forEach(System.out::println);
		
		System.out.println("----------------------------------------------");
		
		emps.stream()
			.map(Employee::getName)
			.limit(3)
			.sorted()
			.forEach(System.out::println);
	}
}

2、Lambda 表达式语法

Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。它将 Lambda 分为两个部分:

  • 左侧:指定了 Lambda 表达式需要的所有参数
  • 右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能

口诀:

  • 写死小括号,拷贝右箭头,落地大括号
  • 左右遇一括号省
  • 左侧推断类型省

3、类型推断

上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”

4、几种语法格式

1、无参数,无返回值

语法格式:

无参数,无返回值:() -> System.out.println(“Hello Lambda!”);

例如 Runnable接口:

 public class Test02 {
     int num = 10; //jdk 1.7以前 必须final修饰
     
     @Test
     public void test01(){
         //匿名内部类
         new Runnable() {
             @Override
             public void run() {
                 //在局部类中引用同级局部变量
                 //只读
                 System.out.println("Hello World" + num);
             }
         };
     }
 
     @Test
     public void test02(){
         //语法糖
         Runnable runnable = () -> {
             System.out.println("Hello Lambda");
         };
     }
 }

2、有一个参数,并且无返回值

(x) -> System.out.println(x)

若只有一个参数,小括号可以省略不写 x -> System.out.println(x)

 @Test
 public void test03(){
     Consumer<String> consumer = (a) -> System.out.println(a);
     consumer.accept("我觉得还行!");
 }

3、有两个及以上的参数,有返回值,并且 Lambda 体中有多条语句

 @Test
 public void test04(){
     Comparator<Integer> comparator = (a, b) -> {
         System.out.println("比较接口");
         return Integer.compare(a, b);
     };
 }

4、有两个及以上的参数,有返回值,并且 Lambda 体中只有1条语句 (大括号 与 return 都可以省略不写)

 @Test
 public void test04(){
     Comparator<Integer> comparator = (a, b) -> Integer.compare(a, b);
 }

5、若 Lambda 体中只有一条语句, return 和 大括号都可以省略不写

 Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

6、Lambda 表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,即“类型推断”

 (Integer x, Integer y) -> Integer.compare(x, y);

5、函数式接口

接口中只有一个抽象方法的接口 @FunctionalIterface

  • 定义一个函数式接口:
@FunctionalInterface
public interface MyFun {
    Integer count(Integer a, Integer b);
}
  • 用一下:
@Test
public void test05(){
    MyFun myFun1 = (a, b) -> a + b;
    MyFun myFun2 = (a, b) -> a - b;
    MyFun myFun3 = (a, b) -> a * b;
    MyFun myFun4 = (a, b) -> a / b;
}
  • 再用一下:
public Integer operation(Integer a, Integer b, MyFun myFun){
    return myFun.count(a, b);
}

@Test
public void test06(){
    Integer result = operation(1, 2, (x, y) -> x + y);
    System.out.println(result);
}

6、案例

案例一:调用 Collections.sort() 方法,通过定制排序 比较两个 Employee (先按照年龄比,年龄相同按照姓名比),使用 Lambda 表达式作为参数传递

  • 定义实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
    
    private Integer id;
    private String name;
    private Integer age;
    private Double salary;
}
  • 定义 List 传入数据
List<Employee> emps = Arrays.asList(
    new Employee(101, "Z3", 19, 9999.99),
    new Employee(102, "L4", 20, 7777.77),
    new Employee(103, "W5", 35, 6666.66),
    new Employee(104, "Tom", 44, 1111.11),
    new Employee(105, "Jerry", 60, 4444.44)
);
  • @Test
@Test
public void test01(){
    Collections.sort(employeesList,(e1,e2)->{
        if (e1.getAge().equals(e2.getAge())) {
            return e1.getName().compareTo(e2.getName());
        }else {
            return Integer.compare(e1.getAge(),e2.getAge());
        }
    });

    for (Employee employee : employeesList) {
        System.out.println(employee);
    }
}

案例二:声明函数式接口,接口中声明抽象方法,String getValue(String str); 声明类 TestLambda,类中编写方法使用接口作为参数,将一个字符串转换成大写,并作为方法的返回值;再将一个字符串的第二个和第四个索引位置进行截取字串

@FunctionalInterface
public interface MyFun02<T> {
    /**
     * 测试案例2
     * @param t
     * @return
     */
    T getValue(T t);
}
/**
 * 案例二:声明函数式接口,接口中声明抽象方法,String getValue(String str);
 * 声明类 TestLambda,类中编写方法使用接口作为参数,将一个字符串转换成大写,并作为方法的返回值;
 * 再将一个字符串的第二个和第四个索引位置进行截取字串
 */
@Test
public void test02() {
    String str = "helloworld";
    
    // s -> s.toUpperCase() 可以写成 String::toUpperCase
    String toUpperString = toUpperString(str, s -> s.toUpperCase());
    System.out.println("toUpperString = " + toUpperString);
}

public String toUpperString(String string, MyFun02<String> myFun02) {
    return myFun02.getValue(string);
}

案例三:声明一个带两个泛型的函数式接口,泛型类型为<T, R> T 为参数,R 为返回值;接口中声明对应的抽象方法;在 TestLambda 类中声明方法,使用接口作为参数,计算两个 Long 类型参数的和;在计算两个 Long 类型参数的乘积

三、函数式接口

Java内置四大核心函数式接口

函数式接口参数类型返回类型用途
Consumer 消费型接口Tvoid对类型为T的对象应用操作:void accept(T t)
Supplier 提供型接口T返回类型为T的对象:T get()
Function<T, R> 函数型接口TR对类型为T的对象应用操作,并返回结果为R类型的对象:R apply(T t)
Predicate 断言型接口Tboolean确定类型为T的对象是否满足某约束,并返回boolean值:boolean test(T t)

1、消费型接口

/**
 * Consumer<T> 消费型接口 :
 */
@Test
public void consumerTest() {
    happy(2999.99, m -> System.out.println("此次消费了:" + m + "元"));
}

public void happy(Double money, Consumer<Double> consumer) {
    consumer.accept(money);
}

2、提供型接口

/**
 * Supplier<T> 供给型接口 :
 */
@Test
public void supplierTest() {
    Random random = new Random();
    List<Integer> numList = getNumList(10, () -> random.nextInt(100));
    numList.forEach(System.out::println);
}

/**
 * 需求:产生指定个数的整数,并放入集合中
 *
 * @param num
 * @param sup
 * @return
 */
public List<Integer> getNumList(int num, Supplier<Integer> sup) {
    List<Integer> list = new ArrayList<>();

    for (int i = 0; i < num; i++) {
        Integer n = sup.get();
        list.add(n);
    }

    return list;
}

3、函数型接口

/**
 * Function<T, R> 函数型接口
 */
@Test
public void functionTest() {
    // s -> s.trim() 可替换成 String::trim
    String newStr = strHandler("\t\t\t 威武   ", s -> s.trim());
    System.out.println(newStr);

    String subStr = strHandler("  威武呀", s -> s.substring(2, 5));
    System.out.println(subStr);
}

/**
 * 需求:用于处理字符串
 *
 * @param str
 * @param fun
 * @return
 */
public String strHandler(String str, Function<String, String> fun) {
    return fun.apply(str);
}

4、断言型接口

/**
 * Predicate<T> 断言型接口:
 */
@Test
public void predicateTest(){
    List<String> list = Arrays.asList("Hello", "yxj", "Lambda", "www", "ok");
    List<String> strings = filterStr(list, p -> p.length() > 3);
    strings.forEach(System.out::println);
}

/**
 * 需求:将满足条件的字符串,放入集合中
 *
 * @param list
 * @param pre
 * @return
 */
public List<String> filterStr(List<String> list, Predicate<String> pre) {
    List<String> strList = new ArrayList<>();

    for (String str : list) {
        if (pre.test(str)) {
            strList.add(str);
        }
    }

    return strList;
}

5、其他接口

20201207110028521

四、引用

1、方法引用

若 Lambda 体中的功能,已经有方法提供了实现,可以使用方法引用(可以将方法引用理解为 Lambda 表达式的另外一种表现形式)

语法格式:

  • 对象 :: 实例方法
  • 类名 :: 静态方法
  • 类名 :: 实例方法

注意:

  • 方法引用所引用的方法的参数列表与返回值类型,需要与函数式接口中抽象方法的参数列表和返回值类型保持一致!
  • 若Lambda 的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,格式: ClassName::MethodName
① 对象的引用 :: 实例方法名
/**
 * 对象::实例方法
 */
@Test
public void test01() {
    PrintStream printStream = System.out;
    Consumer<String> consumer = s -> printStream.println(s);
    consumer.accept("aaa");

    Consumer<String> consumer2 = printStream::println;
    consumer2.accept("bbb");
}

@Test
public void test02(){
    Employee emp = new Employee(101, "张三", 18, 9999.99);
    Supplier<String> supplier = ()->emp.getName();
    System.out.println(supplier.get());

    Supplier<String> supplier2 = emp::getName;
    System.out.println(supplier2.get());
}

@Test
public void test03(){
    BiFunction<Double, Double, Double> fun = (x, y) -> Math.max(x, y);
    Double apply = fun.apply(99.99, 199.99);
    System.out.println(apply);

    BiFunction<Double, Double, Double> fun2 = Math::max;
    Double apply1 = fun2.apply(88.88, 188.88);
    System.out.println(apply1);
}

**注意:**Lambda 表达实体中调用方法的参数列表、返回类型必须和函数式接口中抽象方法保持一致

② 类名 :: 静态方法名
/**
 * 类名 :: 静态方法名
 */
@Test
public void test02() {
    Comparator<Integer> comparator = (a, b) -> Integer.compare(a, b);
    System.out.println(comparator.compare(1, 2));

    Comparator<Integer> comparator2 = Integer::compare;
    System.out.println(comparator2.compare(10, 20));
}
③ 类名 :: 实例方法名
/**
 * 类名 :: 实例方法名
 */
@Test
public void test05() {
    BiPredicate<String, String> bp = (x, y) -> x.equals(y);
    boolean test = bp.test("hello word", "hello future");

    BiPredicate<String, String> bp2 = String::equals;
    boolean test2 = bp2.test("hello word", "hello future");

    System.out.println("-----------------------------------------");

    Function<Employee, String> function = e -> e.show();
    String apply = function.apply(new Employee());
    System.out.println(apply);

    Function<Employee, String> function2 = Employee::show;
    String apply1 = function2.apply(new Employee());
    System.out.println(apply1);
}

**条件:**Lambda 参数列表中的第一个参数是方法的调用者,第二个参数是方法的参数时,才能使用 ClassName :: Method

2、构造器引用

/**
 * 构造器引用
 */
@Test
public void test06() {
    Supplier<Employee> supplier = () -> new Employee();
    Employee employee = supplier.get();
    System.out.println(employee);

    Supplier<Employee> supplier1 = Employee::new;
    Employee employee1 = supplier1.get();
    System.out.println(employee1);
}

@Test
public void test07(){
    Supplier<List> supplier = ()->new ArrayList<Integer>();
    List list = supplier.get();
    list.add(1);
    list.add(2);
    System.out.println(list);

    Supplier<List> supplier1 = ArrayList::new;
    List list1 = supplier1.get();
    System.out.println(list1);
}

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

3、数组引用

/**
 * 数组引用
 */
@Test
public void test08(){
    Function<Integer, String[]> fun = (args)->new String[args];
    String[] apply = fun.apply(10);
    System.out.println("apply.length = " + apply.length);

    System.out.println("--------------------------");

    Function<Integer, Employee[]> fun2 = Employee[]::new;
    Employee[] apply1 = fun2.apply(20);
    System.out.println("apply1.length = " + apply1.length);
}
apply.length = 10
--------------------------
apply1.length = 20

Process finished with exit code 0

4、Lambda 表达式中的闭包问题

这个问题我们在匿名内部类中也会存在,如果我们把注释放开会报错,告诉我 num 值是 final 不能被改变。这里我们虽然没有标识 num 类型为 final,但是在编译期间虚拟机会帮我们加上 final 修饰关键字。

import java.util.function.Consumer;
public class Main {
    public static void main(String[] args) {

        int num = 10;

        Consumer<String> consumer = ele -> {
            System.out.println(num);
        };

        //num = num + 2;
        consumer.accept("hello");
    }
}

五、强大的 Stream API

注意:产生一个全新的流,和原来的数据源没有关系(数据源不受影响)!!!

1、什么是 Stream 流

流(Stream) 到底是什么呢?是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算!

注意:

Stream 自己不会存储元素

Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream

Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行

2、操作 Stream 流

Stream 的操作三个步骤

  • 创建 Stream
    • 一个数据源(如:集合、数组),获取一个流
  • 中间操作
    • 一个中间操作链,对数据源的数据进行处理
  • 终止操作(终端操作)
    • 一个终止操作,执行中间操作链,并产生结果
image-20220108170026154
① 创建 Stream 流

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

  • default Stream stream() : 返回一个顺序流(串行)
  • default Stream parallelStream() : 返回一个并行流
/**
* 创建流
*/
@Test
public void test01(){
    /**
    * 集合流
    *  - Collection.stream() 串行流
    *  - Collection.parallelStream() 并行流
    */
    List<String> list = new ArrayList<>();
    Stream<String> stream1 = list.stream();

    //数组流
    //Arrays.stream(array)
    String[] strings = new String[10];
    Stream<String> stream2 = Arrays.stream(strings);

    //Stream 静态方法
    //Stream.of(...)
    Stream<Integer> stream3 = Stream.of(1, 2, 3);

    //无限流
    //迭代
    Stream<Integer> stream4 = Stream.iterate(0, (i) -> i+2);
    stream4.forEach(System.out::println);

    //生成
    Stream.generate(() -> Math.random())
        .limit(5)
        .forEach(System.out::println);
}
image-20220108171037080
② 中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。

a、筛选 / 切片
方法描述
filter接收 Lambda ,从流中排除某些元素
limit截断流,使其元素不超过给定数量
skip(n)跳过元素,返回一个舍弃了前n个元素的流;若流中元素不足n个,则返回一个空流;与 limit(n) 互补
distinct筛选,通过流所生成的 hashCode() 与 equals() 取除重复元素

filter 操作

   //2. 中间操作
   List<Employee> emps = Arrays.asList(
           new Employee(102, "李四", 59, 6666.66),
           new Employee(101, "张三", 18, 9999.99),
           new Employee(103, "王五", 28, 3333.33),
           new Employee(104, "赵六", 8, 7777.77),
           new Employee(104, "赵六", 8, 7777.77),
           new Employee(104, "赵六", 8, 7777.77),
           new Employee(105, "田七", 38, 5555.55)
   );

   //内部迭代:迭代操作 Stream API 内部完成
   @Test
   public void test2(){
       //所有的中间操作不会做任何的处理
       Stream<Employee> stream = emps.stream()
               .filter((e) -> {
                   System.out.println("测试中间操作");
                   // 加 return 原因:因为lambda表达式中,如果有多条语句,需要用大括号括起来,最后一句为返回语句,要加return
                   return e.getAge() <= 35;
               });

       //只有当做终止操作时,所有的中间操作会一次性的全部执行,称为“惰性求值”
       stream.forEach(System.out::println);
   }

   //外部迭代
   @Test
   public void test3(){
       Iterator<Employee> it = emps.iterator();

       while(it.hasNext()){
           System.out.println(it.next());
       }
   }

内部迭代:

测试中间操作
测试中间操作
Employee{id=101, name='张三', age=18, salary=9999.99}
测试中间操作
Employee{id=103, name='王五', age=28, salary=3333.33}
测试中间操作
Employee{id=104, name='赵六', age=8, salary=7777.77}
测试中间操作
Employee{id=104, name='赵六', age=8, salary=7777.77}
测试中间操作
Employee{id=104, name='赵六', age=8, salary=7777.77}
测试中间操作

Process finished with exit code 0

limit 操作

@Test
public void test4(){
    emps.stream()
            .filter((e) -> {
                System.out.println("短路!"); // &&  ||
                return e.getSalary() >= 5000;
            }).limit(3)
            .forEach(System.out::println);
}
短路!
Employee{id=102, name='李四', age=59, salary=6666.66}
短路!
Employee{id=101, name='张三', age=18, salary=9999.99}
短路!
短路!
Employee{id=104, name='赵六', age=8, salary=7777.77}

Process finished with exit code 0

skip(n) 操作

@Test
public void test5() {
    emps.parallelStream()
            .filter((e) -> e.getSalary() >= 5000)
            .skip(2)
            .forEach(System.out::println);
}
Employee{id=104, name='赵六', age=8, salary=7777.77}
Employee{id=105, name='田七', age=38, salary=5555.55}
Employee{id=104, name='赵六', age=8, salary=7777.77}
Employee{id=104, name='赵六', age=8, salary=7777.77}

Process finished with exit code 0

distinct 操作

@Test
public void test6() {
    emps.stream()
            .distinct()
            .forEach(System.out::println);
}
Employee{id=102, name='李四', age=59, salary=6666.66}
Employee{id=101, name='张三', age=18, salary=9999.99}
Employee{id=103, name='王五', age=28, salary=3333.33}
Employee{id=104, name='赵六', age=8, salary=7777.77}
Employee{id=104, name='赵六', age=8, salary=7777.77}
Employee{id=104, name='赵六', age=8, salary=7777.77}
Employee{id=105, name='田七', age=38, salary=5555.55}

Process finished with exit code 0

发现结果并没有去重,因为 distinct 操作是通过流所生成的 hashCode() 与 equals() 取除重复元素,所以要重新 hashCode() 和 equals() 方法

重写 hashCode() 和 equals() 方法后

Employee{id=102, name='李四', age=59, salary=6666.66}
Employee{id=101, name='张三', age=18, salary=9999.99}
Employee{id=103, name='王五', age=28, salary=3333.33}
Employee{id=104, name='赵六', age=8, salary=7777.77}
Employee{id=105, name='田七', age=38, salary=5555.55}

Process finished with exit code 0
b、映射
方法描述
map(Function f)接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
mapToDouble(ToDoubleFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream
mapTolnt(TolntFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream
mapToLong(ToLongFunction f)接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream
flatMap(Function f)接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

map:

image-20220108232233968

@Test
public void test01() {
    List<String> strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");
    strList.stream().map(String::toUpperCase).forEach(System.out::println);

    System.out.println("-----------------");

    Stream<Stream<Character>> streamStream = strList.stream().map(StreamTest02::filterCharacter);
    streamStream.forEach(sm->sm.forEach(System.out::println));
}

/**
 * 把 String 类型字符串转换成 Character 类型的流
 * @param str
 * @return
 */
public static Stream<Character> filterCharacter(String str) {
    List<Character> list = new ArrayList<>();

    for (char c : str.toCharArray()) {
        list.add(c);
    }
    return list.stream();
}
AAA
BBB
CCC
DDD
EEE
-----------------
a
a
a
b
b
b
c
c
c
d
d
d
e
e
e

Process finished with exit code 0

我们发现下面这种写法太麻烦了

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

所以我们可以用 flatMap(Function f)

@Test
public void test02(){
    List<String> strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");
    Stream<Character> stream = strList.stream().flatMap(StreamTest02::filterCharacter);
    stream.forEach(System.out::println);
}
a
a
a
b
b
b
c
c
c
d
d
d
e
e
e

Process finished with exit code 0

那么 map() 和 flatMap() 有什么区别呢?

  • map():{{a,a,a},{b,b,b},{c,c,c}}
  • flatMap():{a,a,a,b,b,b,c,c,c}

有点类似于集合中 add() 和 addAll() 方法的区别

add():

@Test
public void test03(){
    List<String> strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");

    List<Object> list = new ArrayList<>();
    list.add("111");
    list.add("222");
    list.add(strList);
    System.out.println(list);
}
[111, 222, [aaa, bbb, ccc, ddd, eee]]

Process finished with exit code 0

addAll():

@Test
public void test03(){
    List<String> strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");

    List<Object> list = new ArrayList<>();
    list.add("111");
    list.add("222");
    ///对比使用 add 和 addAll
    // list.add(strList);
    list.addAll(strList);
    System.out.println(list);
}
[111, 222, aaa, bbb, ccc, ddd, eee]

Process finished with exit code 0
c、排序
方法描述
sorted()自然排序
sorted(Comparator c)定制排序

Comparable:自然排序

@Test
public void test04(){
    List<Integer> list = Arrays.asList(1,2,3,4,5);
    list.stream()
        .sorted() //comparaTo()
        .forEach(System.out::println);
}

Comparator:定制排序

@Test
public void test05(){
    emps.stream()
        .sorted((e1, e2) -> { //compara()
            if (e1.getAge().equals(e2.getAge())){
                return e1.getName().compareTo(e2.getName());
            } else {
                return e1.getAge().compareTo(e2.getAge());
            }
        })
        .forEach(System.out::println);
}
③ 终止操作
a、查找 / 匹配
方法描述
allMatch检查是否匹配所有元素
anyMatch检查是否至少匹配一个元素
noneMatch检查是否没有匹配所有元素
findFirst返回第一个元素
findAny返回当前流中的任意元素
count返回流中元素的总个数
max返回流中最大值
min返回流中最小值
forEach(Consumer c)内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代它有你把送代做了)

给 Employee 加一个状态

public class Employee {
    private long id;
    private String name;
    private int age;
    private double salary;
    private status status;
    
    // 略
    
    public enum status {
        // 空闲
        FREE,

        // 忙碌
        BUSY,

        // 休假
        VOCATION
    }
}

测试:

List<Employee> emps = Arrays.asList(
        new Employee(102, "李四", 59, 6666.66, Employee.status.FREE),
        new Employee(101, "张三", 18, 9999.99, Employee.status.BUSY),
        new Employee(103, "王五", 28, 3333.33, Employee.status.VOCATION),
        new Employee(104, "赵六", 8, 7777.77, Employee.status.FREE),
        new Employee(104, "赵六", 8, 7777.77, Employee.status.FREE),
        new Employee(104, "赵六", 8, 7777.77, Employee.status.FREE),
        new Employee(105, "田七", 38, 5555.55, Employee.status.BUSY)
);

@Test
public void test04() {
    boolean b = emps.stream().allMatch(e -> e.getStatus().equals(Employee.status.BUSY));
    System.out.println(b);

    System.out.println("--------------");

    /// 可替换成 Comparator.comparingDouble(Employee::getSalary)
    Optional<Employee> first = emps.stream()
        .sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()))
        .findFirst();
    Employee employee = first.get();
    System.out.println(employee);
}
false
--------------
Employee{id=103, name='王五', age=28, salary=3333.33, status=VOCATION}

Process finished with exit code 0
findAny:

如果用串行流 stream 的话结果可能是一样的,但是并行流 parallelStream 结果就是随机的

/**
 * 从所有员工中任意找出一个状态为 FREE 的员工
 */
@Test
public void test05(){
    Optional<Employee> any = emps.parallelStream().filter(e -> e.getStatus().equals(Employee.status.FREE)).findAny();
    System.out.println(any.get());
}
max/min:
@Test
public void test06(){
    /// 获取工资最高的员工信息
    System.out.println(emps.stream().max(Comparator.comparingDouble(Employee::getSalary)).get());

    System.out.println("----------");

    /// 获取员工的最高工资
    System.out.println(emps.stream()
                       .map(Employee::getSalary) /// 员工工资提取到 map 中
                       .max(Double::compare).get()); /// 从 map 中选出符合条件的
}
Employee{id=101, name='张三', age=18, salary=9999.99, status=BUSY}
----------
9999.99

Process finished with exit code 0
b、归约 / 收集

归约

方法描述
reduce(T identity, BinaryOperator b)可以将流中的数据反复结合起来,得到一个值。返回 T
reduce(BinaryOperator b)可以将流中的数据反复结合起来,得到一个值。返回 Optional

备注:map 和 reduce 的连接通常称为map-reduce 模式,因 Google 用它来进行网络搜索而出名

@Test
public void test07(){
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    Integer reduce = list.stream().reduce(0, (x, y) -> x + y);
    System.out.println(reduce);

    System.out.println("-----------");

    Optional<Double> optional = emps.stream().map(Employee::getSalary).reduce(Double::sum);
    System.out.println(optional.get());
}
55
-----------
33333.355

Process finished with exit code 0

reduce 运算逻辑:

image-20220109165449851

返回值类型不同:

image-20220109170507868

收集

方法描述
collect(Collector c)将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法

Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

image-20220109174122062

image-20220109174142199

toList …:
/**
 * 把员工的名字收集到指定的集合中
 */
@Test
public void test08(){
    List<String> list = emps.stream().map(Employee::getName).collect(Collectors.toList());
    list.forEach(System.out::println);

    System.out.println("--------------");

    Set<String> set = emps.stream().map(Employee::getName).collect(Collectors.toSet());
    set.forEach(System.out::println);

    System.out.println("--------------");

    Map<Long, String> collect = emps.stream().collect(Collectors.toMap(Employee::getId, Employee::getName));
    System.out.println(collect);
}
李四
张三
王五
赵六
田七
--------------
李四
张三
王五
赵六
田七
--------------
{101=张三, 102=李四, 103=王五, 104=赵六, 105=田七}

Process finished with exit code 0
总数、平均值…:
@Test
public void test09() {
    /// 总数
    // 等价于 Long count = emps.stream().count()
    // 还等价于 Long count = (long) emps.size(); 
    Long count = emps.stream().collect(Collectors.counting());
    System.out.println(count);

    //平均值
    Double avg = emps.stream()
            .collect(Collectors.averagingDouble(Employee::getSalary));
    System.out.println(avg);

    /// 总和
    // 等价于 Double sum = emps.stream().mapToDouble(Employee::getSalary).sum();
    Double sum = emps.stream().collect(Collectors.summingDouble(Employee::getSalary));
    System.out.println(sum);

    //最大值
    Optional<Employee> max = emps.stream()
            .collect(Collectors.maxBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
    System.out.println(max.get());

    //最小值
    Optional<Double> min = emps.stream()
            .map(Employee::getSalary)
            .collect(Collectors.minBy(Double::compare));
    System.out.println(min.get());
}
分组:
/**
 * 分组
 */
@Test
public void test10() {
    Map<Employee.status, List<Employee>> collect = emps.stream().collect(Collectors.groupingBy(Employee::getStatus));
    collect.forEach((key, value) -> System.out.println(key + ":" + value));
}
VOCATION:[Employee{id=103, name='王五', age=28, salary=3333.33, status=VOCATION}]
BUSY:[Employee{id=101, name='张三', age=18, salary=9999.99, status=BUSY}, Employee{id=105, name='田七', age=38, salary=5555.55, status=BUSY}]
FREE:[Employee{id=102, name='李四', age=59, salary=6666.66, status=FREE}, Employee{id=104, name='赵六', age=8, salary=7777.77, status=FREE}]

Process finished with exit code 0
多级分组:
/**
 * 多级分组
 */
@Test
public void test11() {
    Map<Employee.status, Map<Object, List<Employee>>> collect = emps.stream()
            .collect(Collectors.groupingBy(Employee::getStatus, Collectors.groupingBy(e -> {
        if (e.getAge() < 35) {
            return "青年";
        } else if (e.getAge() < 50) {
            return "中年";
        }else {
            return "老年";
        }
    })));
    collect.forEach((key, value) -> System.out.println(key + ":" + value));
}
BUSY:{青年=[Employee{id=101, name='张三', age=18, salary=9999.99, status=BUSY}], 中年=[Employee{id=105, name='田七', age=38, salary=5555.55, status=BUSY}]}
VOCATION:{青年=[Employee{id=103, name='王五', age=28, salary=3333.33, status=VOCATION}]}
FREE:{青年=[Employee{id=104, name='赵六', age=8, salary=7777.77, status=FREE}], 老年=[Employee{id=102, name='李四', age=59, salary=6666.66, status=FREE}]}

Process finished with exit code 0
分区:
/**
 * 分区
 */
@Test
public void test12(){
    Map<Boolean, List<Employee>> collect = emps.stream().collect(Collectors.partitioningBy(e -> e.getSalary() > 8000));
    collect.forEach((key, value) -> System.out.println(key + ":" + value));
}
false:[Employee{id=102, name='李四', age=59, salary=6666.66, status=FREE}, Employee{id=103, name='王五', age=28, salary=3333.33, status=VOCATION}, Employee{id=104, name='赵六', age=8, salary=7777.77, status=FREE}, Employee{id=105, name='田七', age=38, salary=5555.55, status=BUSY}]
true:[Employee{id=101, name='张三', age=18, salary=9999.99, status=BUSY}]

Process finished with exit code 0
summing:
/**
 * summing
 */
@Test
public void test13(){
    DoubleSummaryStatistics collect = emps.stream().collect(Collectors.summarizingDouble(Employee::getSalary));
    double max = collect.getMax();
    double min = collect.getMin();
    long count = collect.getCount();
    double average = collect.getAverage();
    double sum = collect.getSum();
}
joining:
/**
 * joining
 */
@Test
public void test14(){
    String collect = emps.stream().map(Employee::getName).collect(Collectors.joining(",","start ==","== end"));
    System.out.println(collect);
}
start ==李四,张三,王五,赵六,田七== end

Process finished with exit code 0

3、Stream API 练习

案例一:给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?

如:给定【1,2,3,4,5】,返回【1,4,9,16,25】

/**
 * 案例一:给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?
 * 如:给定【1,2,3,4,5】,返回【1,4,9,16,25】
 */
@Test
public void test01(){
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> stream = list.stream().map(e -> e * e);
    stream.forEach(System.out::println);

    System.out.println("-----------------------");

    Integer[] integer = new Integer[]{1, 2, 3, 4, 5};
    Arrays.stream(integer).map(e->e*e).forEach(System.out::println);
}

案例二:怎样使用 map 和 reduce 数一数流中有多少个 Employee 呢?

/**
 * 案例二:怎样使用 map 和 reduce 数一数流中有多少个 Employee 呢?
 */
@Test
public void test02(){
    Optional<Integer> reduce = emps.stream().map(e -> 1).reduce(Integer::sum);
    System.out.println(reduce.get());
}

案例三:

Trader 交易员类:

public class Trader {
    private String name;
    private String city;
}

Transaction 交易类:

public class Transaction {
    private Trader trader;
    private int year;
    private int value;
}

TransactionTest 测试类:

public class TransactionTest {
    List<Transaction> transactions = null;

    @Before
    public void before() {
        Trader raoul = new Trader("Raoul", "Cambridge");
        Trader mario = new Trader("Mario", "Milan");
        Trader alan = new Trader("Alan", "Cambridge");
        Trader brian = new Trader("Brian", "Cambridge");

        transactions = Arrays.asList(
                new Transaction(brian, 2011, 300),
                new Transaction(raoul, 2012, 1000),
                new Transaction(raoul, 2011, 400),
                new Transaction(mario, 2012, 710),
                new Transaction(mario, 2012, 700),
                new Transaction(alan, 2012, 950)
        );
    }
}
  1. 找出2011年发生的所有交易, 并按交易额排序(从低到高)
/**
 * 1. 找出2011年发生的所有交易, 并按交易额排序(从低到高)
 */
@Test
public void test01(){
    int targetYear = 2011;
    /// 可替换成 
    // Stream<Transaction> sorted = transactions.stream().filter(e -> e.getYear() == targetYear).sorted(Comparator.comparingInt(Transaction::getValue));
    Stream<Transaction> sorted = transactions.stream().filter(e -> e.getYear() == targetYear).sorted((e1, e2) -> {
        return Integer.compare(e1.getValue(), e2.getValue());
    });
    sorted.forEach(System.out::println);
}
Transaction{trader=Trader{name='Brian', city='Cambridge'}, year=2011, value=300}
Transaction{trader=Trader{name='Raoul', city='Cambridge'}, year=2011, value=400}

Process finished with exit code 0
  1. 交易员都在哪些不同的城市工作过?
/**
 * 2. 交易员都在哪些不同的城市工作过?
 */
@Test
public void test02(){
    transactions.stream().map(e->e.getTrader().getCity()).distinct().forEach(System.out::println);
}
Cambridge
Milan

Process finished with exit code 0
  1. 查找所有来自剑桥的交易员,并按姓名排序
/**
 * 3. 查找所有来自剑桥的交易员,并按姓名排序
 */
@Test
public void test03(){
    String targetCity = "Cambridge";
    transactions.stream()
            .filter(e->e.getTrader().getCity().equals(targetCity))
            .map(Transaction::getTrader)
            // 可替换成 .sorted(Comparator.comparing(Trader::getName))
            .sorted((t1,t2)->t1.getName().compareTo(t2.getName()))
            .distinct()
            .forEach(System.out::println);
}
Trader{name='Alan', city='Cambridge'}
Trader{name='Brian', city='Cambridge'}
Trader{name='Raoul', city='Cambridge'}

Process finished with exit code 0
  1. 返回所有交易员的姓名字符串,按字母顺序排序
/**
 * 4. 返回所有交易员的姓名字符串,按字母顺序排序
 */
@Test
public void test04(){
    System.out.println("=====按姓名字母顺序排=====");
    transactions.stream().map(e->e.getTrader().getName()).sorted().distinct().forEach(System.out::println);

    System.out.println("=====先排序后拼接=====");
    String reduce = transactions.stream().map(e -> e.getTrader().getName()).sorted().distinct().reduce("", String::concat);
    System.out.println(reduce);

    System.out.println("=====把名字拆成一个个字母然后按字母排序(忽略大小写)=====");
    transactions.stream()
            .map(e -> e.getTrader().getName())
            .flatMap(TransactionTest::filterCharacter)
            /// 可替换成 .sorted(String::compareToIgnoreCase)
            .sorted((e1,e2)->e1.compareToIgnoreCase(e2))
            .forEach(System.out::print);
}

/**
 * 把 String 字符串中的字符一个个提取出来转成一个个字符串,然后再转换成 Stream 流
 * @param str
 * @return
 */
public static Stream<String> filterCharacter(String str){
    List<String> list = new ArrayList<>();

    for (Character c : str.toCharArray()) {
        list.add(c.toString());
    }
    return list.stream();
}
=====按姓名字母顺序排=====
Alan
Brian
Mario
Raoul
=====先排序后拼接=====
AlanBrianMarioRaoul
=====把名字拆成一个个字母然后按字母排序(忽略大小写)=====
aaaaaAaBiiilllMMnnoooorRRrruu
    
Process finished with exit code 0
  1. 有没有交易员是在米兰工作的?
/**
 * 5. 有没有交易员是在米兰工作的?
 */
@Test
public void test05(){
    String targetCity = "Milan";
    boolean b = transactions.stream().anyMatch(e -> e.getTrader().getCity().equals(targetCity));
    System.out.println(b);
}
true

Process finished with exit code 0
  1. 打印生活在剑桥的交易员的所有交易额
/**
 * 6. 打印生活在剑桥的交易员的所有交易额
 */
@Test
public void test06(){
    String targetCity = "Cambridge";
    Optional<Integer> sum = transactions.stream()
            .filter(e -> e.getTrader().getCity().equals(targetCity))
            .map(Transaction::getValue)
            .reduce(Integer::sum);
    System.out.println(sum.get());
}
2650

Process finished with exit code 0
  1. 所有交易中,最高的交易额是多少

注意审题:要的是最高交易额,而不是最高交易额的交易

/**
 * 7. 所有交易中,最高的交易额是多少
 */
@Test
public void test07(){
    Optional<Integer> max = transactions.stream().map(Transaction::getValue).max(Integer::compare);
    System.out.println(max.get());

    System.out.println("=====注意审题:要的是最高交易额,而不是最高交易额的交易=====");
    System.out.println("=====以下的返回的是最高交易额的交易=====");
    Optional<Transaction> transaction = transactions.stream()
        .max(Comparator.comparingInt(Transaction::getValue));
    System.out.println(transaction.get());
}
1000
=====注意审题:要的是最高交易额,而不是最高交易额的交易=====
=====以下的返回的是最高交易额的交易=====
Transaction{trader=Trader{name='Raoul', city='Cambridge'}, year=2012, value=1000}

Process finished with exit code 0
  1. 找到交易额最小的交易
/**
 * 8. 找到交易额最小的交易
 */
@Test
public void test08(){
    Optional<Transaction> min = transactions.stream().min(Comparator.comparingInt(Transaction::getValue));
    System.out.println(min.get());
}
Transaction{trader=Trader{name='Brian', city='Cambridge'}, year=2011, value=300}

Process finished with exit code 0

六、并行流与串行流

听不懂…略

七、Optional

**定义:**Optional 类 (java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在用 Optional 可以更好的表达这个概念;并且可以避免空指针异常

常用方法:

  • Optional.of(T t):创建一个 Optional 实例
  • Optional.empty(T t):创建一个空的 Optional 实例
  • Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则空实例
  • isPresent():判断是否包含某值
  • orElse(T t):如果调用对象包含值,返回该值,否则返回 t
  • orElseGet(Supplier s):如果调用对象包含值,返回该值,否则返回 s 获取的值
  • map(Function f):如果有值对其处理,并返回处理后的 Optional,否则返回 Optional.empty()
  • flatmap(Function mapper):与 map 相似,要求返回值必须是 Optional

Optional.of(T t):

@Test
public void test01(){
    Optional<Employee> employee = Optional.of(new Employee());
    System.out.println(employee.get());

    System.out.println("==========");

    Optional<Object> o = Optional.of(null);
    System.out.println(o.get());
}
Employee{id=0, name='null', age=0, salary=0.0, status=null}
==========

java.lang.NullPointerException
	at java.util.Objects.requireNonNull(Objects.java:203)
	at java.util.Optional.<init>(Optional.java:96)
	at java.util.Optional.of(Optional.java:108)
	at lambda.OptionalTest.test01(OptionalTest.java:23)

Optional.empty(T t):

@Test
public void test02(){
    Optional<Object> empty = Optional.empty();
    System.out.println(empty.get());
}
java.util.NoSuchElementException: No value present

	at java.util.Optional.get(Optional.java:135)
	at lambda.OptionalTest.test02(OptionalTest.java:30)

Optional.ofNullable(T t):

@Test
public void test03(){
    Optional<Employee> employee = Optional.ofNullable(null);
    System.out.println(employee.get());
}
java.util.NoSuchElementException: No value present

	at java.util.Optional.get(Optional.java:135)
	at lambda.OptionalTest.test03(OptionalTest.java:36)
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Man {
    private Goddess goddess;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Goddess {
    private String name;
}

以前:

@Test
public void test04(){
    Man man = new Man();
    String goddessName = getGoddessName(man);
    System.out.println(goddessName);
}

/**
 * 获取一个男人心中的女神的名字
 * @param man
 * @return
 */
public String getGoddessName(Man man){
    return man.getGoddess().getName();
}
java.lang.NullPointerException
	at lambda.OptionalTest.getGoddessName(OptionalTest.java:53)
	at lambda.OptionalTest.test04(OptionalTest.java:43)

解决:

@Test
public void test04(){
    Man man = new Man();
    String goddessName = getGoddessName(man);
    System.out.println(goddessName);
}

/**
 * 获取一个男人心中的女神的名字
 * @param man
 * @return
 */
public String getGoddessName(Man man){
    /// 判空,防止空指针
    // return man.getGoddess().getName();
    if (man != null){
        Goddess goddess = man.getGoddess();
        if (goddess != null){
            return goddess.getName();
        }
    }
    return "苍老师";
}
苍老师

Process finished with exit code 0

那如果一层套一层,男人心中有女神,女神心中有她的男神,男神又有他的女神…此时如果再用 if 判空就很麻烦了

现在:

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class NewMan {
    private Optional<Goddess> goddess = Optional.empty();
}
@Test
public void test05(){
    Optional<Goddess> goddess = Optional.ofNullable(new Goddess("深田"));
    Optional<NewMan> newMan = Optional.ofNullable(new NewMan(goddess));
    String name1 = getGoddessName2(newMan);
    System.out.println(name1);

    System.out.println("==========");

    Optional<NewMan> optional = Optional.ofNullable(null);
    String name2 = getGoddessName2(optional);
    System.out.println(name2);
}

public String getGoddessName2(Optional<NewMan> newMan){
    return newMan.orElse(new NewMan())
            .getGoddess()
            .orElse(new Goddess("三上悠亚"))
            .getName();
}
深田
==========
三上悠亚

Process finished with exit code 0
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值