Java8新特性

1.Lambda 表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性,Lambda表达式是表示可传递匿名函数的一种简洁方式,Lambda表达式没有名称,但是有参数列表、函数主体、返回类型,还可能有一个可以抛出的异常列表,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑。

语法

(parameters) -> expression
或
(parameters) ->{ statements; }

##以下是lambda表达式的重要特征

可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。
Lambda 表达式实例
Lambda 表达式的简单例子:

// 1. 不需要参数,返回值为 5  
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)
public class Java8Tester {
   public static void main(String args[]){
      Java8Tester tester = new Java8Tester();
      // 类型声明
      MathOperation addition = (int a, int b) -> a + b;
        	
      // 不用类型声明
      MathOperation subtraction = (a, b) -> a - b;
        
      // 大括号中的返回语句
      MathOperation multiplication = (int a, int b) -> { return a * b; };
        
      // 没有大括号及返回语句
      MathOperation division = (int a, int b) -> a / b;
        
      System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
      System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
      System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
      System.out.println("10 / 5 = " + tester.operate(10, 5, division));
        
      // 不用括号
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括号
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
        
      greetService1.sayMessage("Runoob");
      greetService2.sayMessage("Google");
   }
    
   interface MathOperation {
      int operation(int a, int b);
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
    
   private int operate(int a, int b, MathOperation mathOperation){
      return mathOperation.operation(a, b);
   }
}

结果:

10 + 5 = 15
10 - 5 = 5
10 x 5 = 50
10 / 5 = 2
Hello Runoob
Hello Google

使用 Lambda 表达式需要注意以下两点:

Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。

变量作用域

lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
在 Java8Tester.java 文件输入以下代码:

public class Java8Tester {
 
   final static String salutation = "Hello! ";
   
   public static void main(String args[]){
      GreetingService greetService1 = message -> 
      System.out.println(salutation + message);
      greetService1.sayMessage("Runoob");
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
}

结果:
Hello! Runoob
我们也可以直接在 lambda 表达式中访问外层的局部变量:

public class Java8Tester {
    public static void main(String args[]) {
        final int num = 1;
        Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
        s.convert(2);  // 输出结果为 3
    }
 
    public interface Converter<T1, T2> {
        void convert(int i);
    }
}

lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;  
//报错信息:Local variable num defined in an enclosing scope must be final or effectively 
 final

在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

String first = "";  
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());  //编译会出错 

Lambda表达式的组成

在这里插入图片描述
参数列表:本例中是两个Mask对象的参数,采用的是Comparator接口中compare方法的参数。
箭头:->把参数列表和主体分隔为两个部分。
主体:本例中是把比较口罩品牌的表达式作为Lambda表达式的返回。主体可以修改成另外一种写法,含义是一样的:

maskList.sort((Mask o1, Mask o2) -> {
    return o1.getBrand().compareTo(o2.getBrand());
});

从上面的例子中的两个种写法中,可以看出Lambda表达式有两种基本语法,分别如下:
(参数列表) -> 表达式
(参数列表) -> { 多条语句 }

Lambda表达式示例
当主体是一个表达式时,不需要return语句,隐含return该表达式的返回值。

(Mask o1, Mask o2) -> o1.getBrand().compareTo(o2.getBrand())

参数列表中仅有一个Mask类型的参数,返回的是一个String类型,是该Mask对象的品牌信息。

(Mask mask) -> mask.getBrand()

参数列表中仅有一个Mask类型的参数,返回的是一个boolean类型,是该Mask对象的类型是否为N95。

(Mask mask) -> mask.getType() == "N95"

参数列表中没有任何参数,返回的是一个int类型。

() -> 996

参数列表中有两个int类型的参数,但是没有返回值(void)。在主体中可以写多条语句,不过记住要用{和}将其包裹。

(int x, int y) -> {
    System.out.println("万猫学社想对你说:");
    System.out.println("第一个参数是:" + x);
    System.out.println("第二个参数是:" + y);
    System.out.println("两数之和是:" + (x + y));
}

观察以下lambda写法:

() -> {} //正确,这个Lambda表达式没有参数,也没有任何返回。
() -> “返回一个字符串” //正确,这个Lambda表达式没有参数,主体是一个表达式,返回String类型。
() -> {“返回一个没有大括号的字符串”} //错误,"返回一个没有大括号的字符串"是一个表达式,不是一个语句,不能使用{ }将其包裹,可以修改为() -> “返回一个没有大括号的字符串”。
() -> {return ‘含有return的返回’} //正确,这个Lambda表达式没有参数,主体是一个语句,使用{ }将其包裹,返回String类型。
() -> return “不含有return的返回” // 错误,return “万猫学社”;是一个语句,不是一个表达式,必须使用{ }将其包裹,可以修改为() -> { return “万猫学社”; }。

lambda与匿名函数比较

/**
 * 口罩
 */
 @Data
public class Mask {
    /**
     * 品牌
     */
    private String brand;
    /**
     * 类型
     */
    private String type;

创建对象

List<Mask> maskList = new ArrayList<>();
maskList.add(new Mask("3M", "KN95"));
maskList.add(new Mask("3M", "FFP2"));
maskList.add(new Mask("Honeywell", "KN95"));
maskList.add(new Mask("Honeywell", "N95"));

java8之前,匿名函数进行排序

maskList.sort(new Comparator<Mask>() {
    @Override
    public int compare(Mask o1, Mask o2) {
        return o1.getBrand().compareTo(o2.getBrand());
    }
});

java使用lambda表达式,使用lambda使代码看起来更加简洁

maskList.sort((Mask o1, Mask o2) -> o1.getBrand().compareTo(o2.getBrand()));

2.方法引用

方法引用通过方法的名字来指向一个方法。

方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

方法引用使用一对冒号 ::

当你需要方法引用时,目标引用放在分隔符::前,方法的名称放在分隔符::后。比如,上面的Mask::getBrand,就是引用了Mask中定义的getBrand方法。方法名称后不需要加括号,因为我们并没有实际调用 它。方法引用提高了代码的可读性,也使逻辑更加清晰

1. 静态方法
指向静态方法的引用,语法:类名::静态方法名,类名放在分隔符::前,:静态方法名放在分隔符::后。比如:

(String str) -> Integer.parseInt(str)

使用方法引用以后,可以简写为:

Integer::parseInt

2. 内部对象的实例方法
指向Lambda表达式内部对象的实例方法的引用,**语法:类名::实例方法名,**类名放在分隔符::前,:实例方法名放在分隔符::后。比如:

(Mask mask) -> mask.getBrand() 

使用方法引用以后,可以简写为:

Mask::getBrand

3. 外部对象的实例方法
指向Lambda表达式外部对象的实例方法的引用,语法:实例名::实例方法名,类名放在分隔符::前,:实例方法名放在分隔符::后。比如:

String type = "N95";
Predicate<String> predicate = (String str) -> type.equals(str);
System.out.println(predicate.test("N95"));

其中,type是一个Lambda表达式外部的局部变量,使用方法引用以后,可以简写为:

String type = "N95";
Predicate<String> predicate = type::equals;
System.out.println(predicate.test("N95"));

4. 构造方法
指向构造方法的引用,语法:**类名::new,**类名放在分隔符::前,new放在分隔符::后。比如:

String brand, String type) -> new Mask(brand, type)

使用方法引用以后,可以简写为:

Mask::new

String strings=new String(array);
(char[] array) -> new String(array)是一个构造方法的Lambda表达式,此种方法引用的语法是:类名::new,(char[] array)->String::new。
(String str) -> str.length()是一个内部对象的实例方法的Lambda表达式,此种方法引用的语法是:类名::实例方法名,(String str)>String::length
(String type) -> mask.setType(type)中的mask是一个Mask对象的局部变量,它是一个包含外部对象的Lambda表达式,此种方法引用的语法是:实例名::实例方法名,(String type)->mask::setType。
(String str) -> System.out.println(str)是一个静态方法的Lambda表达式,语法是:类名::静态方法,System.out::println

3.函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
Lambda 表达式和方法引用(实际上也可认为是Lambda表达式)上。

有且仅有一个抽象方法的接口叫做函数式接口,Lambda表达式可以直接作为函数式接口的实例,函数式接口的抽象方法的签名和Lambda表达式的签名必须一致。

编译器可以通过函数式接口推断出Lambda表达式的参数类型,所以在编写Lambda表达式时,可以省略参数类型。比如:

Comparator<Mask> comparator = (Mask o1, Mask o2) -> o1.getBrand().compareTo(o2.getBrand());

简写成:

Comparator<Mask> comparator = (o1, o2) -> o1.getBrand().compareTo(o2.getBrand()); 

另外,当Lambda表达式只有一个参数的时候,不仅可以省略参数类型,还可以省略到参数名称两边的括号,比如:

Predicate<Mask> predicate = (Mask mask) -> mask.getType() == "N95";

简写为:

Predicate<Mask> predicate = mask -> mask.getType() == "N95";

常用的函数式接口

Supplier接口
Supplier接口是对象实例的提供者,定义了一个名叫get的抽象方法,它没有任何入参,并返回一个泛型T对象,具体源码如下:

@Data
@@AllArgsConstructor
public class Mask {
    /**
     * 品牌
     */
    private String brand;
    /**
     * 类型
     */
    private String type;

使用Lambda表达式声明一个Supplier的实例:

Supplier<Mask> supplier = () -> new Mask("3M", "N95");
//使用supplier创建实例
Mast mast=supplier.get()
//特别需要注意的是,本例中每一次调用get方法都会创建新的对象。
接口名称方法名称方法签名
Supplierget() -> T
BooleanSuppliergetAsBoolean() -> boolean
DoubleSuppliergetAsDouble() -> double
IntSuppliergetAsInt() -> int
LongSuppliergetAsLong() -> long

Consumer接口
Consumer接口是一个类似消费者的接口,定义了一个名叫accept的抽象方法,它的入参是一个泛型T对象,没有任何返回(void),主要源码如下:

package java.util.function;

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

使用Lambda表达式声明一个Supplier的实例,它是用来创建品牌为3M、类型为N95的Mask实例;再使用Lambda表达式声明一个Consumer的实例,它是用于打印出Mask实例的相关信息;最后Consumer消费了Supplier生产的Mask

Supplier<Mask> supplier = () -> new Mask("3M", "N95");
Consumer<Mask> consumer = (Mask mask) -> {
    System.out.println("Brand: " + mask.getBrand() + ", Type: " + mask.getType());
};
consumer.accept(supplier.get());

Consumer复合

Consumer接口中,有一个默认方法andThen,它的入参还是Consumer接口的实例。做完上一个Consumer的操作以后,再做当前Consumer的操作,就像工厂的流水线一样,比如:


        User userTest=new User();
        Consumer<User> name = (user -> user.setName("yaoyazhou"));
        Consumer<User> age = (user -> user.setAge(18));
        Consumer<User> address = (user -> user.setAddress("杭州"));
        name.andThen(age).andThen(address).accept(userTest);
        结果:
        User(name=yaoyazhou, age=18, address=杭州)

Consumer相关的接口

接口名称方法名称方法签名
Consumeraccept(T) -> void
DoubleConsumeraccept(double) -> void
IntConsumeraccept(int) -> void
LongConsumeraccept(long) -> void
ObjDoubleConsumeraccept(T, double) -> void
ObjIntConsumeraccept(T, int) -> void
ObjLongConsumeraccept(T, long) -> void

Predicate接口

Predicate接口是判断是与否的接口,定义了一个名叫test的抽象方法,它的入参是一个泛型T对象,并返回一个boolean类型,主要源码如下:

package java.util.function;

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
Supplier<Mask> supplier = () -> new Mask("3M", "N95");
Predicate<Mask> n95 = (Mask mask) -> "N95".equals(mask.getType());
Predicate<Mask> kn95 = (Mask mask) -> "KN95".equals(mask.getType());
System.out.println("是否为N95口罩:" + n95.test(supplier.get()));
System.out.println("是否为KN95口罩:" + kn95.test(supplier.get()));

Predicate复合
Predicate接口一共有3个默认方法:negate、and和or,用它们可以创建更加复杂的Predicate接口实例。

   
        User user1=new User("yaoyazhou",2,"hangzhou");
        User user2=new User("demo",3,"beijing");
        User user3=new User("shenzhen",4,"java");
        
        Predicate<User> predicate=user -> user.getAge()==2;
        Predicate<User> predicate1=user -> user.getName()=="yaoyazhou";
        Predicate<User> predicate2=user -> user.getAddress()=="hangzhou";
        
        User user=new User("yaoyazhou",2,"hangzhou");
        
        boolean test = predicate.and(predicate1).and(predicate2).test(user);
        boolean test1 = predicate.and(predicate2).or(predicate1).test(user);
        boolean test2 = predicate.and(predicate1).negate().test(user);
     结果:
     true
	 true
	 false

Predicate相关的接口
在这里插入图片描述

Function接口

Function接口是对实例进行处理转换的接口,定义了一个名叫apply的抽象方法,它的入参是一个泛型T对象,并返回一个泛型R对象,主要源码如下:

package java.util.function;

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Supplier<Mask> supplier = () -> new Mask("3M", "N95");
Function<Mask, String> brand = (Mask mask) -> mask.getBrand();
Function<Mask, String> type = (Mask mask) -> mask.getType();
System.out.println("口罩品牌:" + brand.apply(supplier.get()));
System.out.println("口罩类型:" + type.apply(supplier.get()));

Function复合
Function接口一共有2个默认方法,分别是:andThen和compose,用它们可以创建更加复杂的Function接口实例。

  Function<Integer,Integer> function=x->x+2;
        Function<Integer,Integer> function1=y->y-4;
        Integer apply = function.andThen(function1).apply(10);
//compose方法 Function接口的compose方法,和andThen方法相反的,先做当前Function的操作,然后再做上一个Function的操作
  Integer apply1 = function.compose(function1).apply(9);

在这里插入图片描述

BiFunction接口

package java.util.function;

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
BiFunction<String,String,Mask> biFunction = (String brand, String type) -> new Mask(brand, type);
Mask mask = biFunction.apply("3M", "N95");
System.out.println("Brand: " + mask.getBrand() + ", Type: " + mask.getType());

基本数据类型
以上介绍的几个常用的函数式接口入参和返回,都是泛型对象的,也就是必须为引用类型。当我们传入或获取的是基本数据类型时,将会发生自动装箱和自动拆箱,带来不必要的性能损耗,比如:

Supplier supplier = () -> System.currentTimeMillis();
long timeMillis = supplier.get();

在上面例子里,发生了一次自动装箱(long被装箱为Long)和一次自动拆箱(Long被拆箱为long),如何避免这种不必要的性能损耗呢?JDK为我们提供相应的函数式接口,如LongSupplier接口,定义了一个名叫getAsLong的抽象方法,签名是() -> long。上面的例子可以优化为:

LongSupplier supplier = () -> System.currentTimeMillis();
long timeMillis = supplier.getAsLong();

Comparator的使用

//通过使用comparator年龄正序排序
 User user1=new User("hangzhou",2);
        User user2=new User("shanghai",3);
        User user3=new User("beijing",4);
        List<User> list=new ArrayList<>();
        list.add(user1);
        list.add(user2);
        list.add(user3);
        list.sort(Comparator.comparing(User::getAge));
        list.forEach(System.out::println);
//使用comparator逆序排列reversed()
 list.sort(Comparator.comparing(User::getAge).reversed());
//比较器链排序,thenComparing,如果年龄一样,使用名字排序
 list.sort(Comparator.comparing(User::getAge).reversed().thenComparing(User::getName));

//thenComparingDouble(),double类型的排序
//thenComparingInt(),int类型的排序
//thenComparingLong(),long类型
list.sort(Comparator.comparing(User::getAge).reversed().thenComparingDouble(User::getPrice))

在这里插入图片描述

4.接口的默认方法

简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。

我们只需在方法名前面加个 default 关键字即可实现默认方法。

为什么要有这个特性?

首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
}

多个默认方法
一个接口有默认方法,考虑这样的情况,一个类实现了多个接口,且这些接口有相同的默认方法,以下实例说明了这种情况的解决方法:

public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
}
 
public interface FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮车!");
   }
}

第一个解决方案是创建自己的默认方法,来覆盖重写接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮汽车!");
   }
}

第二种解决方案可以使用 super 来调用指定接口的默认方法:

public class Car implements Vehicle, FourWheeler {
   public void print(){
      Vehicle.super.print();
   }
}
**```
**静态默认方法**
Java 8 的另一个特性是接口可以声明(并且可以提供实现)静态方法。例如:

```java
public interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
    // 静态方法
   static void blowHorn(){
      System.out.println("按喇叭!!!");
   }
}

默认方法实例
我们可以通过以下代码来了解关于默认方法的使用,可以将代码放入 Java8Tester.java 文件中:

public class Java8Tester {
   public static void main(String args[]){
      Vehicle vehicle = new Car();
      vehicle.print();
   }
}
 
interface Vehicle {
   default void print(){
      System.out.println("我是一辆车!");
   }
    
   static void blowHorn(){
      System.out.println("按喇叭!!!");
   }
}
 
interface FourWheeler {
   default void print(){
      System.out.println("我是一辆四轮车!");
   }
}
 
class Car implements Vehicle, FourWheeler {
   public void print(){
      Vehicle.super.print();
      FourWheeler.super.print();
      Vehicle.blowHorn();
      System.out.println("我是一辆汽车!");
   }
}
结果:
我是一辆车!
我是一辆四轮车!
按喇叭!!!
我是一辆汽车!

5.Stream

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

+--------------------+       +------+   +------+   +---+   +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+       +------+   +------+   +---+   +-------+
List<Integer> transactionsIds = 
widgets.stream()
             .filter(b -> b.getColor() == RED)
             .sorted((x,y) -> x.getWeight() - y.getWeight())
             .mapToInt(Widget::getWeight)
             .sum();

流的创建:

1.1 使用Collection下的 stream() 和 parallelStream() 方法

List<String> list=new ArrayList<>()
Stream<String> stream = list.stream(); //获取一个顺序流
Stream<String> parallelStream = list.parallelStream(); //获取一个并行流

1.2 使用Arrays 中的 stream() 方法,将数组转成流

Integer[] nums=new Integer[10];
Stream<Integer> stream=Arrays.stream(nums);

1.3 使用Stream中的静态方法:of()、iterate()、generate()

Stream<Integer> stream = Stream.of(1,2,3,4,5,6);

Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 2).limit(6);
stream2.forEach(System.out::println); // 0 2 4 6 8 10

Stream<Double> stream3 = Stream.generate(Math::random).limit(2);
stream3.forEach(System.out::println);

1.4 使用 BufferedReader.lines() 方法,将每行内容转成流

BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream<String> lineStream = reader.lines();
lineStream.forEach(System.out::println);

1.5 使用 Pattern.splitAsStream() 方法,将字符串分隔成流

Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);

2.1 筛选与切片
filter:过滤流中的某些元素
limit(n):获取n个元素
skip(n):跳过n元素,配合limit(n)可实现分页
distinct:通过流中元素的 hashCode() 和 equals() 去除重复元素

Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);

Stream<Integer> newStream = stream.filter(s -> s > 5) //6 6 7 9 8 10 12 14 14
        .distinct() //6 7 9 8 10 12 14 去重
        .skip(2) //9 8 10 12 14,索引跳过
        .limit(2); //9 8,得到前两个元素
newStream.forEach(System.out::println);

2.2 映射
map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

List<String> list = Arrays.asList("a,b,c", "1,2,3");

//将每个元素转成一个新的且不带逗号的元素
Stream<String> s1 = list.stream().map(s -> s.replaceAll(",", ""));
s1.forEach(System.out::println); // abc  123

Stream<String> s3 = list.stream().flatMap(s -> {
    //将每个元素转换成一个stream
    String[] split = s.split(",");
    Stream<String> s2 = Arrays.stream(split);
    return s2;
});
s3.forEach(System.out::println); // a b c 1 2 3

2.3 排序
sorted():自然排序,流中元素需实现Comparable接口
sorted(Comparator com):定制排序,自定义Comparator排序器

List<String> list = Arrays.asList("aa", "ff", "dd");
//String 类自身已实现Compareable接口
list.stream().sorted().forEach(System.out::println);// aa dd ff
 
Student s1 = new Student("aa", 10);
Student s2 = new Student("bb", 20);
Student s3 = new Student("aa", 30);
Student s4 = new Student("dd", 40);
List<Student> studentList = Arrays.asList(s1, s2, s3, s4);
 
//自定义排序:先按姓名升序,姓名相同则按年龄升序
studentList.stream().sorted(
        (o1, o2) -> {
            if (o1.getName().equals(o2.getName())) {
                return o1.getAge() - o2.getAge();
            } else {
                return o1.getName().compareTo(o2.getName());
            }
        }
).forEach(System.out::println);

结果::
User(name=aa, age=10)
User(name=aa, age=30)
User(name=bb, age=20)
User(name=dd, age=40)
User(name=aa, age=10)
User(name=aa, age=30)
User(name=bb, age=20)
User(name=dd, age=40)

2.4 消费
peek:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。

Student s1 = new Student("aa", 10);
Student s2 = new Student("bb", 20);
List<Student> studentList = Arrays.asList(s1, s2);

studentList.stream()
        .peek(o -> o.setAge(100))
        .forEach(System.out::println);   

//结果:
Student{name='aa', age=100}
Student{name='bb', age=100}       

3流的终止操作*

1 匹配、聚合操作

​ allMatch:接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
​ noneMatch:接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
​ anyMatch:接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
​ findFirst:返回流中第一个元素
​ findAny:返回流中的任意元素
​ count:返回流中元素的总个数
​ max:返回流中元素最大值
​ min:返回流中元素最小值

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

boolean allMatch = list.stream().allMatch(e -> e > 10); //false
boolean noneMatch = list.stream().noneMatch(e -> e > 10); //true
boolean anyMatch = list.stream().anyMatch(e -> e > 4);  //true

Integer findFirst = list.stream().findFirst().get(); //1
Integer findAny = list.stream().findAny().get(); //1

long count = list.stream().count(); //5
Integer max = list.stream().max(Integer::compareTo).get(); //5
Integer min = list.stream().min(Integer::compareTo).get(); //1

2 规约操作

​ Optional reduce(BinaryOperator accumulator):第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。
​ T reduce(T identity, BinaryOperator accumulator):流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素。
U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator combiner):在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行规约。

//经过测试,当元素个数小于24时,并行时线程数等于元素个数,当大于等于24时,并行时线程数为16

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24);

Integer v = list.stream().reduce((x1, x2) -> x1 + x2).get();
System.out.println(v);   // 300

Integer v1 = list.stream().reduce(10, (x1, x2) -> x1 + x2);
System.out.println(v1);  //310

Integer v2 = list.stream().reduce(0,
        (x1, x2) -> {
            System.out.println("stream accumulator: x1:" + x1 + "  x2:" + x2);
            return x1 - x2;
        },
        (x1, x2) -> {
            System.out.println("stream combiner: x1:" + x1 + "  x2:" + x2);
            return x1 * x2;
        });
System.out.println(v2); // -300

Integer v3 = list.parallelStream().reduce(0,
        (x1, x2) -> {
            System.out.println("parallelStream accumulator: x1:" + x1 + "  x2:" + x2);
            return x1 - x2;
        },
        (x1, x2) -> {
            System.out.println("parallelStream combiner: x1:" + x1 + "  x2:" + x2);
            return x1 * x2;
        });
System.out.println(v3); //197474048

3 收集操作

​ collect:接收一个Collector实例,将流中元素收集成另外一个数据结构。
​ Collector<T, A, R> 是一个接口,有以下5个抽象方法:
​ Supplier supplier():创建一个结果容器A
​ BiConsumer<A, T> accumulator():消费型接口,第一个参数为容器A,第二个参数为流中元素T。
​ BinaryOperator combiner():函数接口,该参数的作用跟上一个方法(reduce)中的combiner参数一样,将并行流中各 个子进程的运行结果(accumulator函数操作后的容器A)进行合并。
​ Function<A, R> finisher():函数式接口,参数为:容器A,返回类型为:collect方法最终想要的结果R。
​ Set characteristics():返回一个不可变的Set集合,用来表明该Collector的特征。有以下三个特征:
​ CONCURRENT:表示此收集器支持并发。(官方文档还有其他描述,暂时没去探索,故不作过多翻译)
​ UNORDERED:表示该收集操作不会保留流中元素原有的顺序。
​ IDENTITY_FINISH:表示finisher参数只是标识而已,可忽略。
​ 注:如果对以上函数接口不太理解的话,可参考我另外一篇文章:Java 8 函数式接口

3.3.1 Collector 工具库:Collectors

Student s1 = new Student("aa", 10,1);
Student s2 = new Student("bb", 20,2);
Student s3 = new Student("cc", 10,3);
List<Student> list = Arrays.asList(s1, s2, s3);

//装成list
List<Integer> ageList = list.stream().map(Student::getAge).collect(Collectors.toList()); // [10, 20, 10]

//转成set
Set<Integer> ageSet = list.stream().map(Student::getAge).collect(Collectors.toSet()); // [20, 10]

//转成map,注:key不能相同,否则报错
Map<String, Integer> studentMap = list.stream().collect(Collectors.toMap(Student::getName, Student::getAge)); // {cc=10, bb=20, aa=10}

//字符串分隔符连接
String joinName = list.stream().map(Student::getName).collect(Collectors.joining(",", "(", ")")); // (aa,bb,cc)

//聚合操作
//1.学生总数
Long count = list.stream().collect(Collectors.counting()); // 3
//2.最大年龄 (最小的minBy同理)
Integer maxAge = list.stream().map(Student::getAge).collect(Collectors.maxBy(Integer::compare)).get(); // 20
//3.所有人的年龄
Integer sumAge = list.stream().collect(Collectors.summingInt(Student::getAge)); // 40
//4.平均年龄
Double averageAge = list.stream().collect(Collectors.averagingDouble(Student::getAge)); // 13.333333333333334
// 带上以上所有方法
DoubleSummaryStatistics statistics = list.stream().collect(Collectors.summarizingDouble(Student::getAge));
System.out.println("count:" + statistics.getCount() + ",max:" + statistics.getMax() + ",sum:" + statistics.getSum() + ",average:" + statistics.getAverage());

//分组
Map<Integer, List<Student>> ageMap = list.stream().collect(Collectors.groupingBy(Student::getAge));
//多重分组,先根据类型分再根据年龄分
Map<Integer, Map<Integer, List<Student>>> typeAgeMap = list.stream().collect(Collectors.groupingBy(Student::getType, Collectors.groupingBy(Student::getAge)));

//分区
//分成两部分,一部分大于10岁,一部分小于等于10岁
Map<Boolean, List<Student>> partMap = list.stream().collect(Collectors.partitioningBy(v -> v.getAge() > 10));

//规约
Integer allAge = list.stream().map(Student::getAge).collect(Collectors.reducing(Integer::sum)).get(); //40
3.3.2 Collectors.toList() 解析

//toList 源码
public static <T> Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
            (left, right) -> {
                left.addAll(right);
                return left;
            }, CH_ID);
}

//为了更好地理解,我们转化一下源码中的lambda表达式
public <T> Collector<T, ?, List<T>> toList() {
    Supplier<List<T>> supplier = () -> new ArrayList();
    BiConsumer<List<T>, T> accumulator = (list, t) -> list.add(t);
    BinaryOperator<List<T>> combiner = (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    };
    Function<List<T>, List<T>> finisher = (list) -> list;
    Set<Collector.Characteristics> characteristics = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
 return new Collector<T, List<T>, List<T>>() {
    @Override
    public Supplier supplier() {
        return supplier;
    }
 
    @Override
    public BiConsumer accumulator() {
        return accumulator;
    }
 
    @Override
    public BinaryOperator combiner() {
        return combiner;
    }
 
    @Override
    public Function finisher() {
        return finisher;
    }
 
    @Override
    public Set<Characteristics> characteristics() {
        return characteristics;
    }
};

生成流

在 Java 8 中, 集合接口有两个方法来生成流:

  • stream() − 为集合创建串行流。

  • parallelStream() − 为集合创建并行流。

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
List<String> collect = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

结果:
[abc, bc, efg, abcd, jkl]

map

map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);

//获取平方数,返回一个List
List<Integer> collect = numbers.stream().map(integer -> integer * integer).collect(Collectors.toList());
结果:
[9, 4, 4, 9, 49, 9, 25]
 
//去重排序,返回一个set集合
Set<Integer> collect = numbers.stream().map(integer -> integer + 2).collect(Collectors.toSet());
结果:
[4, 7, 7, 9]

//获取对象的List集合
 List<User> users = Arrays.asList(new User().setId("A").setName("张三").setPerson(new Person("1")),
                new User().setId("B").setName("李四").setPerson(new Person("2")),
                new User().setId("C").setName("王五").setPerson(new Person("3")));
                
List<Person> collect = users.stream().map(user -> user.getPerson()).collect(Collectors.toList());       
结果:
Person(id=1)
Person(id=2)
Person(id=3)

    
      List<Privilege> collect = PrivilegeItems.stream().map(privilegeItem -> {
            Privilege privilege = new Privilege();
            privilege.setPrivilegeType(privilegeItem.getPrivilegeType().toString());
            privilege.setPrivilegeStatus(privilegeItem.getPrivilegeStatus().toString());
            return privilege;
    //要有返回值
        }).collect(Collectors.toList());
    

//获取对象的map集合
Map<String, String> collect = users.stream().collect(Collectors.toMap(User::getName, User::getId));
结果:
{李四=B, 张三=A, 王五=C}


 ListMap(Collectors.toMap):

       List<User> users = Arrays.asList(new User().setId("A").setName("张三").setPerson(new Person("1")),
                new User().setId("A").setName("李四").setPerson(new Person("2")),
                new User().setId("C").setName("王五").setPerson(new Person("3")));

        //Map<String, User> collect = users.stream().collect(Collectors.toMap(User::getId, t -> t));

        //Map<String, String> collect = users.stream().collect(Collectors.toMap(User::getId, User::toString));

        Map<String, User> collect = users.stream().collect(Collectors.toMap(User::getId, Function.identity()));
        
结果:
    
{A=User(name=张三, id=A, person=Person(id=1)), B=User(name=李四, id=B, person=Person(id=2)), C=User(name=王五, id=C, person=Person(id=3))}

{A=User(name=张三, id=A, person=Person(id=1)), B=User(name=李四, id=B, person=Person(id=2)), C=User(name=王五, id=C, person=Person(id=3))}

{A=User(name=张三, id=A, person=Person(id=1)), B=User(name=李四, id=B, person=Person(id=2)), C=User(name=王五, id=C, person=Person(id=3))}
 Map<String, User> collect = users.stream().collect(Collectors.toMap(User::getId, Function.identity()));



还是用上面的例子,如果 List 中 userId 有相同的,使用上面的写法会抛异常:
    
java.lang.IllegalStateException: Duplicate key User(name=张三, id=A, person=Person(id=1))
        
        
Map<String, String> collect = users.stream().collect(Collectors.toMap(User::getId, User::getName, (n1, n2) -> n1 + n2));

结果:
{A=张三李四, C=王五}



比如我们希望返回的 Map 是根据 Key 排序的,可以使用如下写法:
    
    
List<User> userList = Arrays.asList(
        new User().setId("B").setName("张三"),
        new User().setId("A").setName("李四"),
        new User().setId("C").setName("王五")
);
TreeMap<String, String> collect = userList.stream().collect(
        Collectors.toMap(User::getId, User::getName, (n1, n2) -> n1, TreeMap::new)
);

结果:
{A=李四, B=张三, C=王五}

filter

filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤出空字符串:

   List<User> userList = Arrays.asList(
                new User().setId("1").setName("张三").setPerson(new Person("1")),
                new User().setId("4").setName("李四").setPerson(new Person("2")),
                new User().setId("7").setName("王五").setPerson(new Person("3"))
        );

        long count = userList.stream().map(User::getPerson).filter(person -> person.getId() =="1").count();
        
        结果:
        1
        
        
   List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
        // 获取空字符串的数量
        long count = strings.stream().filter(string -> string.isEmpty()).count();
        
        结果:
        2

sorted

  List<User> userList = Arrays.asList(
                new User().setId("1").setName("张三").setPerson(new Person("1")),
                new User().setId("8").setName("李四").setPerson(new Person("2")),
                new User().setId("7").setName("王五").setPerson(new Person("3"))
        );

        List<String> collect = userList.stream().map(User::getId).sorted().collect(Collectors.toList());
        
        结果:
        [1, 7, 8]

并行(parallel)程序

   List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
        // 获取空字符串的数量
        long count = strings.parallelStream().filter(string -> string.isEmpty()).count();
        
        结果:
        2

Collectors

Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors 可用于返回列表或字符串:

List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
        List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
       

        System.out.println("筛选列表: " + filtered);
        String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
        System.out.println("合并字符串: " + mergedString);
        
        结果:
        筛选列表: [abc, bc, efg, abcd, jkl]
        合并字符串: abc, bc, efg, abcd, jkl

统计

另外,一些产生统计结果的收集器也非常有用。它们主要用于int、double、long等基本类型上,它们可以用来产生类似如下的统计结果。

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
 
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
 
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());

6 optional

Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
Optional 类的引入很好的解决空指针异常。
类声明
以下是一个 java.util.Optional 类的声明:

public final class Optional<T>
extends Object
序号方法 & 描述
1static Optional empty()返回空的 Optional 实例
2boolean equals(Object obj)判断其他对象是否等于 Optional
3Optional filter(Predicate<? super predicate)如果值存在,并且这个值匹配给定的 predicate,返回一个Optional用以描述这个值,否则返回一个空的Optional
4 Optional flatMap(Function<? super T,Optional> mapper)如果值存在,返回基于Optional包含的映射方法的值,否则返回一个空的Optional
5T get()如果在这个Optional中包含这个值,返回值,否则抛出异常:NoSuchElementException
6int hashCode()返回存在值的哈希码,如果值不存在 返回 0。
7void ifPresent(Consumer<? super T> consumer)如果值存在则使用该值调用 consumer , 否则不做任何事情。
8boolean isPresent()如果值存在则方法会返回true,否则返回 false。
9Optional map(Function<? super T,? extends U> mapper)如果有值,则对其执行调用映射函数得到返回值。如果返回值不为 null,则创建包含映射返回值的Optional作为map方法返回值,否则返回空Optional。
10static Optional of(T value)返回一个指定非null值的Optional。
11static Optional ofNullable(T value)如果为非空,返回 Optional 描述的指定值,否则返回空的 Optional。
12T orElse(T other)如果存在该值,返回值, 否则返回 other。
13T orElseGet(Supplier<? extends T> other)如果存在该值,返回值, 否则触发 other,并返回 other 调用的结果。
14 T orElseThrow(Supplier<? extends X> exceptionSupplier)如果存在该值,返回包含的值,否则抛出由 Supplier 继承的异常
15String toString()返回一个Optional的非空字符串,用来调试

注意: 这些方法是从 java.lang.Object 类继承来的。

//返货空的optional实例
 Optional<User> empty = Optional.empty();
//of()不能为空,ofNullable可以为空,isPresent()对象是否存在
 User user = new User("yy", 22, "hangzhou");
 User user1=null;
 Optional<User> opt = Optional.ofNullable(user);
 if(opt.isPresent()){
     System.out.println(opt.get());
 }
 //有值返回值,没有返回user2
  User user3 = Optional.ofNullable(user).orElse(user2);
//orElseGet()有值的时候返回值,如果没有值,它会执行作为参数传入的 Supplier(供应者) 函数式接口,并将返回其执行结果:
  User user = Optional.ofNullable(user1).orElseGet(()->user2);
//orElse和orElseGet的区别是,如果user1为非空,orElse会执行,orElseGet不会执行,在执行较密集的调用时,比如调用 Web 服务或数据查询,这个差异会对性能产生重大影响。
 User user4 = Optional.ofNullable(user1).orElse(createUser());
 User user5 = Optional.ofNullable(user1).orElseGet(()->createUser());
//orElseThrow() 对象为空的时候抛出异常
 User result = Optional.ofNullable(user).orElseThrow( () -> new IllegalArgumentException());
//  map方法接受一个映射函数参数,返回一个被Optional包装的结果。若结果为空,则返回 空Optional。flatMap方法接受一个返回值为Optional的映射函数参数,该返回值亦是flatMap方法的返回值。若结果为空,则返回 空Optional。

//User内定义的的方法,
@Data
public Optional<String> getPosition() {
    private String name;
    private int age;
    private String address;
     public Optional<String> getPosition() {
        return Optional.ofNullable(name);
    }
  }
 String map = Optional.ofNullable(user1).map(user -> user.getName()).orElse("heell");
 //flatMap()j
 String flatMap = Optional.ofNullable(user1).flatMap(user -> user.getPosition()).orElse("hello");

 //若现在需求变了,需要将list的每个字符串按照“ ”分割,分别处理每个单词(list共4个单词)。这里我们仍然使用map进行测试。
*[该代码存在语法错误,第二个map提示所需参数为Function<? super Stream, ? extend R> mapper。这说明第一个map产生了多个流(hello流和world流),这个时候就需要使用到flatMap了。jdk8对flatMap的解释为
Returns a stream consisting of the results of replacing each element of this stream with the contents of a mapped stream produced by applying the provided mapping function to each element.
意思很明确,流的每个元素都会产生一个新的流。这多个流会合并为一个流,作为flatMap的返回结果]
 list.stream().map(s -> Stream.of(s.split(" "))).map(String::toUpperCase).forEach();//报错
 list.stream().flatMap(s -> Stream.of(s.split(" "))).map(String::toUpperCase).forEach(s -> System.out.println(s+"9 "));

日期时间 API

非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。

设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。

时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。

Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:

Local(本地) − 简化了日期时间的处理,没有时区的问题。

Zoned(时区) − 通过制定的时区处理日期时间。

新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。

public class Java8Tester {
   public static void main(String args[]){
      Java8Tester java8tester = new Java8Tester();
      java8tester.testLocalDateTime();
   }
    
   public void testLocalDateTime(){
    
      // 获取当前的日期时间
      LocalDateTime currentTime = LocalDateTime.now();
      System.out.println("当前时间: " + currentTime);
        
      LocalDate date1 = currentTime.toLocalDate();
      System.out.println("date1: " + date1);
        
      Month month = currentTime.getMonth();
      int day = currentTime.getDayOfMonth();
      int seconds = currentTime.getSecond();
        
      System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);
        
      LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
      System.out.println("date2: " + date2);
        
      // 12 december 2014
      LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
      System.out.println("date3: " + date3);
        
      // 22 小时 15 分钟
      LocalTime date4 = LocalTime.of(22, 15);
      System.out.println("date4: " + date4);
        
      // 解析字符串
      LocalTime date5 = LocalTime.parse("20:15:30");
      System.out.println("date5: " + date5);
   }
}
结果:
当前时间: 2016-04-15T16:55:48.668
date1: 2016-04-15: APRIL,: 15,: 48
date2: 2012-04-10T16:55:48.668
date3: 2014-12-12
date4: 22:15
date5: 20:15:30

使用时区的日期时间API

public class Java8Tester {
   public static void main(String args[]){
      Java8Tester java8tester = new Java8Tester();
      java8tester.testZonedDateTime();
   }
    
   public void testZonedDateTime(){
    
      // 获取当前时间日期
      ZonedDateTime date1 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");
      System.out.println("date1: " + date1);
        
      ZoneId id = ZoneId.of("Europe/Paris");
      System.out.println("ZoneId: " + id);
        
      ZoneId currentZone = ZoneId.systemDefault();
      System.out.println("当期时区: " + currentZone);
   }
}
结果:
date1: 2015-12-03T10:15:30+08:00[Asia/Shanghai]
ZoneId: Europe/Paris
当期时区: Asia/Shanghai
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值