Java8新特性

153 阅读33分钟

新特性

  • Lambda表达式− Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。

  • 方法引用− 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

  • 默认方法− 默认方法就是一个在接口里面有了一个实现的方法。

  • Stream−新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。

  • Optional类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。

  • Time api− 加强对日期与时间的处理。

Lambda表达式

Lambda的标准格式

( 参数类型 参数名称 ) -> {    代码体;}

格式说明:

  • ( 参数类型 参数名称 ) :参数列表

  • { 代码体 }:方法体

  • -> :箭头,分隔参数列表和方法体

Lambda与方法的对比 匿名内部类

public void run() {     System.out.println("aa"); }

Lambda

() -> System.out.println("bb!")

无参数无返回值的Lambda

interface Swimmable {    public abstract void swimming(); }

public class Demo02LambdaUse {    public static void main(String[] args) {         goSwimming(new Swimmable() {            @Override public void swimming() {                System.out.println("匿名内部类游泳");             }         });        goSwimming(() -> { System.out.println("Lambda游泳"); });    }        public static void goSwimming(Swimmable swimmable) {         swimmable.swimming();     }

有参数有返回值的Lambda

传统写法 如果使用传统的代码对 ArrayList 集合进行排序,写法如下:

public class Person {     private String name;    private int age;    private int height; // 省略其他 }

public class Demo03LambdaUse {     public static void main(String[] args) {        ArrayList<Person> persons = new ArrayList<>();         persons.add(new Person("刘德华", 58, 174));        persons.add(new Person("张学友", 58, 176));         persons.add(new Person("刘德华", 54, 171));         persons.add(new Person("黎明", 53, 178));        Collections.sort(persons, new Comparator<Person>() {            @Override public int compare(Person o1, Person o2) {                return o1.getAge() - o2.getAge(); } });        for (Person person : persons) {            System.out.println(person);         }    }}

Lambda

public class Demo03LambdaUse {     public static void main(String[] args) {        ArrayList<Person> persons = new ArrayList<>();        persons.add(new Person("刘德华", 58, 174));         persons.add(new Person("张学友", 58, 176));         persons.add(new Person("刘德华", 54, 171));        persons.add(new Person("黎明", 53, 178));        Collections.sort(persons, (o1, o2) -> o1.getAge() - o2.getAge());        for (Person person : persons) {             System.out.println(person);         }        System.out.println("-----------------");         List<Integer> list = Arrays.asList(11, 22, 33, 44);         list.forEach(new Consumer<Integer>() {            @Override public void accept(Integer integer) {                 System.out.println(integer);             }         });        System.out.println("-----------------");        list.forEach(s ->  System.out.println(s));    } }

Lambda的实现原理

// 编译前
public class Demo04LambdaImpl { 
    public static void main(String[] args) {
        goSwimming(() -> { System.out.println("Lambda游泳"); }); 
    }
    public static void goSwimming(Swimmable swimmable) { 
        swimmable.swimming(); 
    } 
}
// 编译后
public class Demo04LambdaImpl { 
    public static void main(String[] args) {
        goSwimming(new Swimmable() {
            public void swimming() {
                Demo04LambdaImpl.lambda$main$0(); 
            } 
        }); 
    }private static void lambda$main$0() { 
        System.out.println("Lambda表达式游泳"); 
    }public static void goSwimming(Swimmable swimmable) {
        swimmable.swimming(); 
    } 
}

匿名内部类在编译的时候会一个class文件 Lambda在程序运行的时候形成一个类

  1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码

  2. 还会形成一个匿名内部类,实现接口,重写抽象方法

  3. 在接口的重写方法中会调用新生成的方法

Lambda的前提条件

Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意

  1. 方法的参数或局部变量类型必须为接口才能使用Lambda

  2. 接口中有且仅有一个抽象方法

    public interface Flyable { public abstract void flying(); }

    public class Demo05LambdaCondition { public static void main(String[] args) { test01(() -> { }); Flyable s = new Flyable() { @Override public void flying() { } }; Flyable s2 = () -> { }; }public static void test01(Flyable fly) { fly.flying(); } }

小结 Lambda表达式的前提条件: 1. 方法的参数或变量的类型是接口 2. 这个接口中只能有一个抽象方法

函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口。 函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以 适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。 FunctionalInterface注解 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注 解可用于一个接口的定义上:

@FunctionalInterface 
public interface Operator {
    void myMethod(); 
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即 使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

Lambda和匿名内部类对比

了解Lambda和匿名内部类在使用上的区别

  1. 所需的类型不一样 匿名内部类,需要的类型可以是类,抽象类,接口 Lambda表达式,需要的类型必须是接口

  2. 抽象方法的数量不一样 匿名内部类所需的接口中抽象方法的数量随意 Lambda表达式所需的接口只能有一个抽象方法

  3. 实现原理不同 匿名内部类是在编译后会形成class Lambda表达式是在程序运行的时候动态生成class

静态方法

jdk8以前的接口

interface 接口名 {
    静态常量; 
    抽象方法; 
}

JDK 8对接口的增强,接口还可以有默认方法静态方法 JDK 8的接口:

interface 接口名 {
    静态常量; 
    抽象方法;
    默认方法;
    静态方法;
}

接口默认方法的定义格式

interface 接口名 { 
    修饰符 default 返回值类型 方法名() { 
        代码; 
    }
}

接口默认方法的使用

方式一:实现类直接调用接口默认方法 方式二:实现类重写接口默认方法

public class Demo02UserDefaultFunction { 
    public static void main(String[] args) { 
        BB b = new BB(); // 方式一:实现类直接调用接口默认方法 
        b.test02(); 
        CC c = new CC(); // 调用实现类重写接口默认方法 
        c.test02(); 
    } 
}

interface AA {
    public abstract void test1(); 
    public default void test02() { 
        System.out.println("AA test02"); 
    } 
}

class BB implements AA { 
    @Override public void test1() {
        System.out.println("BB test1");
    }
}

class CC implements AA { 
    @Override public void test1() { 
        System.out.println("CC test1");
    }
    // 方式二:实现类重写接口默认方法
    @Override public void test02() { 
        System.out.println("CC实现类重写接口默认方法");
    } 
}

接口静态方法的使用

直接使用接口名调用即可:接口名.静态方法名();

public class Demo04UseStaticFunction { 
    public static void main(String[] args) { 
        // 直接使用接口名调用即可:接口名.静态方法名();
        AAA.test01(); 
    }
}

interface AAA { 
    public static void test01() { 
        System.out.println("AAA 接口的静态方法"); 
    } 
}

class BBB implements AAA { 
    /* @Override 静态方法不能重写 
    public static void test01() { 
    System.out.println("AAA 接口的静态方法"); 
    }*/ 
}

接口默认方法和静态方法的区别

  1. 默认方法通过实例调用,静态方法通过接口名调用。

  2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。

  3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。

常用的函数式接口

常用内置函数式接口介绍

它们主要在 java.util.function 包中。下面是最常用的几个接口。 1.Supplier

@FunctionalInterface 
public interface Supplier<T> { 
    public abstract T get(); 
}
  1. Consumer接口

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

3.Function接口

@FunctionalInterface 
public interface Function<T, R> { 
    public abstract R apply(T t); 
}

4.Predicate接口

@FunctionalInterface 
public interface Predicate<T> { 
    public abstract boolean test(T t); 
}
Predicate接口用于做判断,返回boolean类型的值

Supplier接口

java.util.function.Supplier 接口,它意味着"供给" , 对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。 使用Lambda表达式返回数组元素最大值 使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。

public class Demo05Supplier { 
    public static void main(String[] args) { 
        printMax(() -> { 
            // 先排序,最后就是最大的 Arrays.sort(arr);
            int[] arr = {10, 20, 100, 30, 40, 50};
            // 最后就是最大的 
       		return arr[arr.length - 1]; 
    	});
    }
    private static void printMax(Supplier<Integer> supplier) {
        int max = supplier.get(); 
        System.out.println("max = " + max);
    } 
}

Consumer接口

java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛 型参数决定。 使用Lambda表达式将一个字符串转成大写和小写的字符串 Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口。基本使用如:

public class Demo06Consumer {
    public static void main(String[] args) { 
        // Lambda表达式 
        test( s ->  System.out.println(s.toLowerCase()));
    }
    public static void test(Consumer<String> consumer) { 
        consumer.accept("HelloWorld");
    } 
}

默认方法:andThen 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操 作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:

default Consumer<T> andThen(Consumer<? super T> after) { 
    Objects.requireNonNull(after); 
    return (T t) -> { accept(t); after.accept(t); }; 
}

要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况:

public class Demo07ConsumerAndThen { 
    public static void main(String[] args) { 
        // Lambda表达式
        test((String s) -> { System.out.println(s.toLowerCase()); },
             (String s) -> { System.out.println(s.toUpperCase()); });
        // Lambda表达式简写 
        test(s -> System.out.println(s.toLowerCase()), 
             s -> System.out.println(s.toUpperCase())); 
    }
    public static void test(Consumer<String> c1, Consumer<String > c2) {
        String str = "Hello World"; 
        // c1.accept(str); 
        // 转小写 
        // c2.accept(str);
        // 转大写 
        // c1.andThen(c2).accept(str);
        c2.andThen(c1).accept(str);
    } 
}

Function接口

java.util.function.Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件, 后者称为后置条件。有参数有返回值。 使用Lambda表达式将字符串转成数字 Function转换型接口,对apply方法传入的T类型数据进行处理,返回R类型的结果,有参有返回的接口。使用的场景 例如:将 String 类型转换为 Integer 类型。

public class Demo08Function { 
    public static void main(String[] args) { 
        // Lambda表达式 
        test((String s) -> { return Integer.parseInt(s); }); 
    }
    
    public static void test(Function<String, Integer> function) { 
        Integer in = function.apply("10"); 
        System.out.println("in: " + (in + 5));
    } 
}

默认方法:andThen Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { 
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t)); 
}

该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:

public class Demo09FunctionAndThen { 
    public static void main(String[] args) { 
        // Lambda表达式 
        test((String s) -> { return Integer.parseInt(s); }, 
             (Integer i) -> { return i * 10; }); 
    }
   
    public static void test(Function<String, Integer> f1, Function<Integer, Integer> f2) {
        // 
        Integer in = f1.apply("66"); 
        // 将字符串解析成为int数字 
        // Integer in2 = f2.apply(in);
        // 将上一步的int数字乘以10 
        Integer in3 = f1.andThen(f2).apply("66"); 
        System.out.println("in3: " + in3); 
        // 660 
    }
}

第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过 andThen 按照前后顺序组合到了一起。

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用 java.util.function.Predicate 接口。 使用Lambda判断一个人名如果超过3个字就认为是很长的名字 对test方法的参数T进行判断,返回boolean类型的结果。用于条件判断的场景:

public class Demo10Predicate { 
    public static void main(String[] args) {
        test(s -> s.length() > 3, "迪丽热巴");
    }
    private static void test(Predicate<String> predicate, String str) { 
        boolean veryLong = predicate.test(str); 
        System.out.println("名字很长吗:" + veryLong);
    } 
}

条件判断的标准是传入的Lambda表达式逻辑,只要名称长度大于3则认为很长。 默认方法:and 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实 现“并且”的效果时,可以使用default方法 and 。其JDK源码为:

default Predicate<T> and(Predicate<? super T> other) { 
    Objects.requireNonNull(other); 
    return (t) -> test(t) && other.test(t); 
}

使用Lambda表达式判断一个字符串中即包含W,也包含H 使用Lambda表达式判断一个字符串中包含W或者包含H 使用Lambda表达式判断一个字符串中即不包含W 如果要判断一个字符串既要包含大写“H”,又要包含大写“W”,那么:

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> { return s.contains("H");}, 
             (String s) -> {return s.contains("W"); }); 
    }
    
    public static void test(Predicate<String> p1, Predicate<String> p2) { 
        String str = "HelloWorld"; 
        boolean b1 = p1.test(str); 
        // 判断包含大写“H” 
        boolean b2 = p2.test(str); 
        // 判断包含大写“W” 
        // if (b1 && b2) { 
        // System.out.println("即包含W,也包含H");
        // } 
        boolean bb = p1.and(p2).test(str);
        if (bb) {
            System.out.println("即包含W,也包含H"); 
        } 
    }
}

默认方法:or 使用Lambda表达式判断一个字符串中包含W或者包含H 与 and 的“与”类似,默认方法 or 实现逻辑关系中的“”。JDK源码为:

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other); 
    return (t) -> test(t) || other.test(t);
}

如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可,其他都不变:

public class Demo10Predicate_And_Or_Negate {
    public static void main(String[] args) { 
    // Lambda表达式 
    test((String s) -> { return s.contains("H"); },
         (String s) -> { return s.contains("W"); }); 
    }
    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str); 
        // 判断包含大写“H”
        boolean b2 = p2.test(str); 
        // 判断包含大写“W” 
        // if (b1 || b2) { 
        // System.out.println("有H,或者W");
        // } 
        boolean bbb = p1.or(p2).test(str); 
        if (bbb) { 
            System.out.println("有H,或者W");
        }
	}
}                                     

默认方法:negate 使用Lambda表达式判断一个字符串中即不包含W “与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法 negate 的JDK源代码为:

default Predicate<T> negate() {
    return (t) -> !test(t);
}

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在 test 方法调用之前调 用 negate 方法,正如 and 和 or 方法一样:

public class Demo10Predicate_And_Or_Negate { 
    public static void main(String[] args) {
        // Lambda表达式 
        test((String s) -> { return s.contains("H"); }, 
             (String s) -> { return s.contains("W"); });
    }
    public static void test(Predicate<String> p1, Predicate<String> p2) {
        String str = "HelloWorld";
        boolean b1 = p1.test(str); 
        // 判断包含大写“H” 
        boolean b2 = p2.test(str); 
        // 判断包含大写“W”
        // 没有H,就打印 
        // if (!b1) {
        // System.out.println("没有H");
        // } 
        boolean test = p1.negate().test(str);
        if (test) {
            System.out.println("没有H");
        }
    } 
}

方法引用

对象名::引用成员方法

这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代 码为:

// 对象::实例方法 
@Test 
public void test01() {
    Date now = new Date();
    Supplier<Long> supp = () -> { return now.getTime(); };
    System.out.println(supp.get());
    Supplier<Long> supp2 = now::getTime; 
    System.out.println(supp2.get()); 
}

方法引用的注意事项 1. 被引用的方法,参数要和接口中抽象方法的参数一样 2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值

类名::引用静态方法

由于在 java.lang.System 类中已经存在了静态方法 currentTimeMillis ,所以当我们需要通过Lambda来调用该 方法时,可以使用方法引用 , 写法是:

// 类名::静态方法 
@Test 
public void test02() {
    Supplier<Long> supp = () -> { return System.currentTimeMillis(); };
    System.out.println(supp.get()); 
    Supplier<Long> supp2 = System::currentTimeMillis; 
    System.out.println(supp2.get());
}

类名::引用实例方法

Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。

// 类名::实例方法 
@Test 
public void test03() { 
    Function<String, Integer> f1 = (s) -> { return s.length(); };
    System.out.println(f1.apply("abc"));
    Function<String, Integer> f2 = String::length;
    System.out.println(f2.apply("abc"));
    BiFunction<String, Integer, String> bif = String::substring; 
    String hello = bif.apply("hello", 2); 
    System.out.println("hello = " + hello); 
}

类名::new引用构造器

由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。首先是一个简单的 Person 类:

public class Person { 
    private String name;
    public Person(String name) { 
    	this.name = name; 
    }
    public String getName() { 
        return name; 
    } 
}

要使用这个函数式接口,可以通过方法引用传递:

// 类名::new
@Test 
public void test04() { 
    Supplier<Person> sup = () -> { return new Person(); };
    System.out.println(sup.get()); 
    Supplier<Person> sup2 = Person::new; System.out.println(sup2.get());
    BiFunction<String, Integer, Person> fun2 = Person::new; 
    System.out.println(fun2.apply("张三", 18));
}

数组::new 引用数组构造器

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。

// 类型[]::new
@Test 
public void test05() { 
    Function<Integer, String[]> fun = (len) -> { return new String[len]; };
    String[] arr1 = fun.apply(10); 
    System.out.println(arr1 + ", " + arr1.length);
    Function<Integer, String[]> fun2 = String[]::new; 
    String[] arr2 = fun.apply(5);
    System.out.println(arr2 + ", " + arr2.length); 
}

小结 方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为 Lambda表达式的缩写形式 , 不过要注意的是方法引用只能"引用"已经存在的方法!

Stream流

stream流的案例

获取Stream流的两种方式

获取一个流非常简单,有以下几种常用的方式:

  • 所有的 Collection 集合都可以通过 stream 默认方法获取流;

  • Stream 接口的静态方法 of 可以获取数组对应的流

方式1 : 根据Collection获取流 首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。

public interface Collection { 
    default Stream<E> stream()
}

public class Demo04GetStream { 
    public static void main(String[] args) { 
        // 集合获取流
        // Collection接口中的方法: default Stream<E> stream() 获取流 
        List<String> list = new ArrayList<>(); 
        // ... 
        Stream<String> stream1 = list.stream(); 
        Set<String> set = new HashSet<>(); 
        // ... 
        Stream<String> stream2 = set.stream(); 
        Vector<String> vector = new Vector<>();
        // ...
        Stream<String> stream3 = vector.stream(); 
    } 
}

java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:

public class Demo05GetStream { 
    public static void main(String[] args) {
        // Map获取流 
        Map<String, String> map = new HashMap<>(); 
        // ... 
        Stream<String> keyStream = map.keySet().stream();
        Stream<String> valueStream = map.values().stream(); 
        Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
    } 
}

方式2 : Stream中的静态方法of获取流 由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:

public class Demo06GetStream {
    public static void main(String[] args) {
        // Stream中的静态方法: static Stream of(T... values) 
        Stream<String> stream6 = Stream.of("aa", "bb", "cc");
        String[] arr = {"aa", "bb", "cc"};
        Stream<String> stream7 = Stream.of(arr);
        Integer[] arr2 = {11, 22, 33};
        Stream<Integer> stream8 = Stream.of(arr2);
        // 注意:基本数据类型的数组不行 
        int[] arr3 = {11, 22, 33};
        Stream<int[]> stream9 = Stream.of(arr3);
    }
}

小结 学习了两种获取流的方式: 1. 通过Collection接口中的默认方法Stream stream() 2. 通过Stream接口中的静态of方法

Stream常用方法和注意事项

Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

方法名

方法作用

返回值类型

方法种类

count

统计个数

long

终结

forEach

逐一处理

void

终结

fifilter

过滤

Stream

函数拼接

limit

取用前几个

Stream

函数拼接

skip

跳过前几个

Stream

函数拼接

map

映射

Stream

函数拼接

concat

组合

Stream

函数拼接

  • 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法。

  • 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结 方法。)

Stream注意事项(重要) 1. Stream只能操作一次 2. Stream方法返回的是新的流 3. Stream不调用终结方法,中间的操作不会执行

Stream流的forEach方法

forEach 用来遍历流中的数据

void forEach(Consumer<? super T> action);

该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。例如:

@Test
public void testForEach() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子"); 
    /*one.stream().forEach((String s) -> { 
		System.out.println(s); 
	});*/
    // 简写 
    // one.stream().forEach(s -> System.out.println(s)); 
    one.stream().forEach(System.out::println);
}

Stream流的count方法

Stream流提供 count 方法来统计其中的元素个数:

long count();

该方法返回一个long值代表元素个数。基本使用:

@Test
public void testCount() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
    System.out.println(one.stream().count());
}

Stream流的fifilter方法

fifilter用于过滤数据,返回符合过滤条件的数据。可以通过 filter 方法将一个流转换成另一个子集流。方法声明:

Stream filter(Predicate<? super T> predicate);

该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。 Stream流中的 filter 方法基本使用的代码如:

@Test 
public void testFilter() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
    one.stream().filter(s -> s.length() == 2).forEach(System.out::println);
}
// 在这里通过Lambda表达式来指定了筛选的条件:姓名长度为2个字。

Stream流的limit方法

limit 方法可以对流进行截取,只取用前n个。方法签名:

Stream limit(long maxSize);

参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作。基本使用:

@Test 
public void testLimit() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子"); 
    one.stream().limit(3).forEach(System.out::println); 
}

Stream流的skip方法

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

Stream skip(long n);

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

@Test
public void testSkip() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");
    one.stream().skip(2).forEach(System.out::println);
}

Stream流的map方法

如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名:

Stream map(Function<? super T, ? extends R> mapper);

该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。 Stream流中的 map 方法基本使用的代码如:

@Test 
public void testMap() { 
    Stream<String> original = Stream.of("11", "22", "33"); 
    Stream<Integer> result = original.map(Integer::parseInt); 
    result.forEach(s -> System.out.println(s + 10)); 
}

这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)

Stream流的sorted方法

如果需要将数据排序,可以使用 sorted 方法。方法签名:

Stream sorted(); Stream sorted(Comparator<? super T> comparator);

基本使用 Stream流中的 sorted 方法基本使用的代码如:

@Test 
public void testSorted() { 
    // sorted(): 根据元素的自然顺序排序 
    // sorted(Comparator<? super T> comparator): 根据比较器指定的规则排序 
    Stream.of(33, 22, 11, 55) 
        .sorted() 
        .sorted((o1, o2) -> o2 - o1) 
        .forEach(System.out::println); 
}

这段代码中, sorted 方法根据元素的自然顺序排序,也可以指定比较器排序。

Stream流的distinct方法

如果需要去除重复数据,可以使用 distinct 方法。方法签名:

Stream distinct();

基本使用 Stream流中的 distinct 方法基本使用的代码如:

@Test 
public void testDistinct() { 
    Stream.of(22, 33, 22, 11, 33) 
        .distinct() 
        .forEach(System.out::println); 
}

如果是自定义类型如何是否也能去除重复的数据呢?

@Test 
public void testDistinct2() { 
    Stream.of(
        new Person("刘德华", 58), 
        new Person("张学友", 56), 
        new Person("张学友", 56), 
        new Person("黎明", 52)) 
        .distinct() 
        .forEach(System.out::println); 
}

public class Person {
    private String name;
    private int age; // 省略其他 
}

自定义类型是根据对象的hashCode和equals来去除重复元素的。

Stream流的match方法

如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。方法签名:

boolean allMatch(Predicate<? super T> predicate); boolean anyMatch(Predicate<? super T> predicate); boolean noneMatch(Predicate<? super T> predicate);

基本使用 Stream流中的 Match 相关方法基本使用的代码如:

@Test 
public void testMatch() { 
    boolean b = Stream.of(5, 3, 6, 1) 
        // .allMatch(e -> e > 0); // allMatch: 元素是否全部满足条件 
        // .anyMatch(e -> e > 5); // anyMatch: 元素是否任意有一个满足条件 
        .noneMatch(e -> e < 0); // noneMatch: 元素是否全部不满足条件 
    System.out.println("b = " + b); 
}

Stream流的find方法

如果需要找到某些数据,可以使用 find 相关方法。方法签名:

Optional findFirst(); Optional findAny();

基本使用 Stream流中的 find 相关方法基本使用的代码如:

@Test 
public void testFind() { 
    Optional<Integer> first = Stream.of(5, 3, 6, 1).findFirst(); 
    System.out.println("first = " + first.get()); 
    Optional<Integer> any = Stream.of(5, 3, 6, 1).findAny(); 
    System.out.println("any = " + any.get()); 
}

Stream流的max和min方法

如果需要获取最大和最小值,可以使用 max 和 min 方法。方法签名:

Optional max(Comparator<? super T> comparator); Optional min(Comparator<? super T> comparator);

基本使用 Stream流中的 max 和 min 相关方法基本使用的代码如:

@Test 
public void testMax_Min() { 
    Optional<Integer> max = Stream.of(5, 3, 6, 1).max((o1, o2) -> o1 - o2); 
    System.out.println("first = " + max.get()); 
    Optional<Integer> min = Stream.of(5, 3, 6, 1).min((o1, o2) -> o1 - o2); 
    System.out.println("any = " + min.get()); 
}

Stream流的reduce方法

如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法。方法签名:

T reduce(T identity, BinaryOperator accumulator);

基本使用 Stream流中的 reduce 相关方法基本使用的代码如:

@Test 
public void testReduce() { 
    int reduce = Stream.of(4, 5, 3, 9) 
        .reduce(0, (a, b) -> { 
            System.out.println("a = " + a + ", b = " + b); 
            return a + b; 
        }); 
    // reduce: 
    // 第一次将默认做赋值给x, 取出第一个元素赋值给y,进行操作 
    // 第二次,将第一次的结果赋值给x, 取出二个元素赋值给y,进行操作 
    // 第三次,将第二次的结果赋值给x, 取出三个元素赋值给y,进行操作 
    // 第四次,将第三次的结果赋值给x, 取出四个元素赋值给y,进行操作 
    System.out.println("reduce = " + reduce); 
    int reduce2 = Stream.of(4, 5, 3, 9) 
        .reduce(0, (x, y) -> { 
            return Integer.sum(x, y); 
        }); 
    int reduce3 = Stream.of(4, 5, 3, 9).reduce(0, Integer::sum);
    int max = Stream.of(4, 5, 3, 9) 
        .reduce(0, (x, y) -> { 
            return x > y ? x : y; 
        }); 
    System.out.println("max = " + max); 
}

Stream流的map和reduce组合使用

@Test 
public void testMapReduce() { 
    // 求出所有年龄的总和 
    int totalAge = Stream.of( 
        new Person("刘德华", 58), 
        new Person("张学友", 56), 
        new Person("郭富城", 54), 
        new Person("黎明", 52)) 
        .map((p) -> p.getAge()) 
        .reduce(0, (x, y) -> x + y); 
    System.out.println("totalAge = " + totalAge); 
    // 找出最大年龄 
    int maxAge = Stream.of( 
        new Person("刘德华", 58), 
        new Person("张学友", 56), 
        new Person("郭富城", 54),
        new Person("黎明", 52)) 
        .map((p) -> p.getAge()) 
        .reduce(0, (x, y) -> x > y ? x : y); 
    System.out.println("maxAge = " + maxAge); 
    // 统计 数字2 出现的次数 
    int count = Stream.of(1, 2, 2, 1, 3, 2) 
        .map(i -> { 
            if (i == 2) { 
                return 1; 
            } else { 
                return 0; 
            } 
        })
        .reduce(0, Integer::sum); 
    System.out.println("count = " + count); 
}

Stream流的mapToInt

如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法。方法签名:

IntStream mapToInt(ToIntFunction<? super T> mapper);

基本使用 Stream流中的 mapToInt 相关方法基本使用的代码如:

@Test 
public void test1() { 
    // Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱 
    Stream<Integer> stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5}); 
    // 把大于3的和打印出来 
    // Integer result = stream 
    // .filter(i -> i.intValue() > 3)
    // .reduce(0, Integer::sum); 
    // System.out.println(result); 
    // 先将流中的Integer数据转成int,后续都是操作int类型 
    IntStream intStream = stream.mapToInt(Integer::intValue); 
    int reduce = intStream 
        .filter(i -> i > 3) 
        .reduce(0, Integer::sum); 
    System.out.println(reduce); 
    // 将IntStream转化为Stream<Integer> 
    IntStream intStream1 = IntStream.rangeClosed(1, 10); 
    Stream<Integer> boxed = intStream1.boxed(); 
    boxed.forEach(s -> System.out.println(s.getClass() + ", " + s)); 
}

Stream流的concat方法

如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :

static Stream concat(Stream<? extends T> a, Stream<? extends T> b)

该方法的基本使用代码如:

@Test 
public void testContact() { 
    Stream<String> streamA = Stream.of("张三"); 
    Stream<String> streamB = Stream.of("李四"); 
    Stream<String> result = Stream.concat(streamA, streamB); 
    result.forEach(System.out::println); 
}

Stream综合案例 现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,进行以下 若干操作步骤: 1. 第一个队伍只要名字为3个字的成员姓名; 2. 第一个队伍筛选之后只要前3个人; 3. 第二个队伍只要姓张的成员姓名; 4. 第二个队伍筛选之后不要前2个人; 5. 将两个队伍合并为一个队伍; 6. 根据姓名创建 Person 对象; 7. 打印整个队伍的Person对象信息。

@Test
void test04() {
    String[] arr = {"迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公"};
    String[] arr2 = {"古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三"};
    String[] arr = {"迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公"};
    String[] arr2 = {"古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三"};
    List<String> one = Arrays.asList(arr);
    List<String> two = Arrays.asList(arr2);
    //  第一个队伍只要名字为3个字的成员姓名; 
    one.stream().filter(s -> s.length()==3).forEach(System.out::println);
    System.out.println("------------------------------------");
    // 第一个队伍筛选之后只要前3个人;
    one.stream().limit(3).forEach(System.out::println);
    // 第二个队伍只要姓张的成员姓名;
    System.out.println("------------------------------------");
    // 第二个队伍只要姓张的成员姓名; 
    two.stream().filter(s -> s.startsWith("张")).forEach(System.out::println);
    System.out.println("------------------------------------");
    // 第二个队伍筛选之后不要前2个人;
    two.stream().skip(2).forEach(System.out::println);
    System.out.println("------------------------------------");
    // 根据姓名创建 Person 对象; 
    // 打印整个队伍的Person对象信息。
    Stream<String> concat = Stream.concat(one.stream(), two.stream());
    concat.map(Person::new).forEach(System.out::println);
}

收集Stream流中的结果

Stream流中的结果到集合中

Stream流提供 collect 方法,其参数需要一个 java.util.stream.Collector<T,A, R> 接口对象来指定收集到哪 种集合中。java.util.stream.Collectors 类提供一些方法,可以作为 Collector`接口的实例: public static Collector<T, ?, List> toList() :转换为 List 集合。 public static Collector<T, ?, Set> toSet() :转换为 Set 集合。 下面是这两个方法的基本使用代码:

// 将流中数据收集到集合中 
@Test 
public void testStreamToCollection() { 
    Stream<String> stream = Stream.of("aa", "bb", "cc"); 
    // List<String> list = stream.collect(Collectors.toList()); 
    // Set<String> set = stream.collect(Collectors.toSet()); 
    ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new)); 
    HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new)); 
}

Stream流中的结果到数组中

Stream提供 toArray 方法来将结果放到一个数组中,返回值类型是Object[]的。其使用场景如:

@Test 
public void testStreamToArray() { 
    Stream<String> stream = Stream.of("aa", "bb", "cc"); 
    // Object[] objects = stream.toArray(); 
    // for (Object obj : objects) { 
    // System.out.println(); 
    // } 
    String[] strings = stream.toArray(String[]::new); 
    for (String str : strings) { 
        System.out.println(str); 
    } 
}

对流中数据进行聚合计算

当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值,获取最小 值,求总和,平均值,统计数量。 maxBy,minBy,summingInt,averagingInt,counting

@Test 
public void testStreamToOther() { 
    Stream<Student> studentStream = Stream.of( 
        new Student("赵丽颖", 58, 95), 
        new Student("杨颖", 56, 88), 
        new Student("迪丽热巴", 56, 99), 
        new Student("柳岩", 52, 77)); 
    //获取最大值 用完需重新获取!
        Optional<Student> collect = studentStream.
            collect(Collectors.maxBy((o1, o2) -> o1.getSocre() - o2.getSocre())); 
    //获取最小值 
        Optional<Student> collect = studentStream.
            collect(Collectors.minBy((o1, o2) ->  o1.getSocre() - o2.getSocre())); 
        System.out.println(collect.get()); 
    //求总和 
        int sumAge = studentStream.collect(Collectors.summingInt(s -> s.getAge())); 
        System.out.println("sumAge = " + sumAge); 
    // 平均值 
        double avgScore = studentStream.
            collect(Collectors.averagingInt(s -> s.getSocre())); 
        System.out.println("avgScore = " + avgScore); 
    // 统计数量 
        Long count = studentStream.collect(Collectors.counting()); 
        System.out.println("count = " + count); 
}

对流中数据进行分组

当我们使用Stream流处理数据后,可以根据某个属性将数据分组:groupingBy

// 分组
@Test 
public void testGroup() { 
    Stream<Student> studentStream = Stream.of( 
        new Student("赵丽颖", 52, 95), 
        new Student("杨颖", 56, 88), 
        new Student("迪丽热巴", 56, 55), 
        new Student("柳岩", 52, 33)); 
    // Map<Integer, List<Student>> map = 
    studentStream.collect(Collectors.groupingBy(Student::getAge)); 
    // 将分数大于60的分为一组,小于60分成另一组 
    Map<String, List<Student>> map = studentStream.collect(Collectors.groupingBy((s) ->{ 
        if (s.getSocre() > 60) { 
            return "及格"; 
        } else { 
            return "不及格"; 
        } 
    })); 
    map.forEach((k, v) -> { 
        System.out.println(k + "::" + v); 
    }); 
} 

效果:

不及格::[Student{name='迪丽热巴', age=56, socre=55},Student{name='柳岩', age=52, socre=33}] 
及格::[Student{name='赵丽颖', age=52, socre=95},Student{name='杨颖', age=56, socre=88}]

对流中数据进行多级分组

还可以对数据进行多级分组:

// 多级分组 
@Test 
public void testCustomGroup() { 
    Stream<Student> studentStream = Stream.of( 
        new Student("赵丽颖", 52, 95), 
        new Student("杨颖", 56, 88), 
        new Student("迪丽热巴", 56, 99), 
        new Student("柳岩", 52, 77)); 
    Map<Integer, Map<String, List<Student>>> map = 
        studentStream.
        collect(Collectors.groupingBy(s -> s.getAge(), Collectors.groupingBy(s -> { 
            if (s.getSocre() >= 90) { 
                return "优秀"; 
            } else if (s.getSocre() >= 80 && s.getSocre() < 90) { 
                return "良好"; 
            } else if (s.getSocre() >= 80 && s.getSocre() < 80) { 
                return "及格"; 
            } else { 
                return "不及格"; 
            } 
        }))); 
    map.forEach((k, v) -> { 
        System.out.println(k + " == " + v); 
    }); 
}

效果:

52 == {不及格=[Student{name='柳岩', age=52, socre=77}], 
			优秀=[Student{name='赵丽颖', age=52, socre=95}]} 
56 == {优秀=[Student{name='迪丽热巴', age=56, socre=99}], 
			良好=[Student{name='杨颖', age=56, socre=88}]}

对流中数据进行分区

// 分区 
@Test 
public void testPartition() { 
    Stream<Student> studentStream = Stream.of( 
        new Student("赵丽颖", 52, 95), 
        new Student("杨颖", 56, 88), 
        new Student("迪丽热巴", 56, 99), 
        new Student("柳岩", 52, 77)); 
    // partitioningBy会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。 
    Map<Boolean, List<Student>> map = studentStream.
        collect(Collectors.partitioningBy(s -> s.getSocre() > 90)); 
    map.forEach((k, v) -> { 
        System.out.println(k + " == " + v); 
    }); 
} 

效果:

false==[Student{name='杨颖', age=56, socre=88}, Student{name='柳岩', age=52, socre=77}] 
tru ==[Student{name='赵丽颖', age=52, socre=95}, Student{name='迪丽热巴', age=56, socre=99}]

对流中数据进行拼接

Collectors.joining 会根据指定的连接符,将所有元素连接成一个字符串。

// 拼接 
@Test 
public void testJoining() { 
  Stream<Student> studentStream = Stream.of( 
    new Student("赵丽颖", 52, 95), 
    new Student("杨颖", 56, 88), 
    new Student("迪丽热巴", 56, 99), 
    new Student("柳岩", 52, 77)); 
  String collect = studentStream 
    .map(Student::getName) 
    .collect(Collectors.joining(">_<", "^_^", "^v^")); 
  System.out.println(collect); 
}

效果:

^_^赵丽颖>_<杨颖>_<迪丽热巴>_<柳岩^v^

小结 收集Stream流中的结果 到集合中: Collectors.toList()/Collectors.toSet()/Collectors.toCollection() 到数组中: toArray()/toArray(int[]::new) 聚合计算: Collectors.maxBy/Collectors.minBy/Collectors.counting/Collectors.summingInt/Collectors.averagingInt 分组: Collectors.groupingBy 分区: Collectors.partitionBy 拼接: Collectors.joinging

并行的Stream流

串行的Stream流 目前我们使用的Stream流是串行的,就是在一个线程上执行。 并行的Stream流 parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。

获取并行Stream流的两种方式

  1. 直接获取并行的流 2. 将串行流转成并行流

    @Test public void testgetParallelStream() { ArrayList list = new ArrayList<>(); // 直接获取并行的流 Stream stream = list.parallelStream(); // 将串行流转成并行流 Stream stream = list.stream().parallel(); }

小结 获取并行流有两种方式: 直接获取并行流: parallelStream() 将串行流转成并行流: parallel()

parallelStream线程安全问题

解决方法: 加锁、使用线程安全的集合或者调用Stream的 toArray() / collect() 操作就是满足线程安全。

Fork/Join案例

需求:使用Fork/Join计算1-10000的和,当一个任务的计算数量大于3000时拆分任务,数量小于3000时计算。

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask; 
import java.util.concurrent.RecursiveTask;

public class Demo07ForkJoin { 
    public static void main(String[] args) { 
        long start = System.currentTimeMillis(); 
        ForkJoinPool pool = new ForkJoinPool(); 
        SumRecursiveTask task = new SumRecursiveTask(1, 10000L); 
        Long result = pool.invoke(task); 
        System.out.println("result = " + result); 
        long end = System.currentTimeMillis(); 
        System.out.println("消耗的时间为: " + (end - start)); 
    }
}
class SumRecursiveTask extends RecursiveTask<Long> { 
    private static final long THRESHOLD = 3000L; 
    private final long start; 
    private final long end; 
    public SumRecursiveTask(long start, long end) { 
        this.start = start; 
        this.end = end; 
    }
    @Override 
    protected Long compute() { 
        long length = end - start; 
        if (length <= THRESHOLD) { 
            // 任务不用再拆分了.可以计算了 
            long sum = 0; 
            for (long i = start; i <= end; i++) { 
                sum += i; 
            }
            System.out.println("计算: " + start + " -> " + end + ",结果为: " + sum); 
            return sum; 
        } else { 
            // 数量大于预定的数量,任务还需要再拆分 
            long middle = (start + end) / 2; 
            System.out.println("拆分: 左边 " + start + " -> " + middle + 
                               ", 右边 " + (middle + 1) + " -> " + end); 
            SumRecursiveTask left = new SumRecursiveTask(start, middle); 
            left.fork(); 
            SumRecursiveTask right = new SumRecursiveTask(middle + 1, end); 
            right.fork(); 
            return left.join() + right.join(); 
        } 
    } 
}

小结 1. parallelStream是线程不安全的 2. parallelStream适用的场景是CPU密集型的,只是做到别浪费CPU,假如本身电脑CPU的负载很大,那还到处用 并行流,那并不能起到作用 3. I/O密集型 磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少消耗CPU资源,一般并行流中不适用于I/O密集 型的操作,就比如使用并流行进行大批量的消息推送,涉及到了大量I/O,使用并行流反而慢了很多 4. 在使用并行流的时候是无法保证元素的顺序的,也就是即使你用了同步集合也只能保证元素都正确但无法保证 其中的顺序

Optional

以前对null的处理方式

@Test 
public void test01() { 
    String userName = "凤姐"; 
    // String userName = null; 
    if (userName != null) { 
        System.out.println("用户名为:" + userName); 
    } else { 
        System.out.println("用户名不存在"); 
    } 
}

Optional的基本使用

Optional.of(T t) : 创建一个 Optional 实例 Optional.empty() : 创建一个空的 Optional 实例 Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例

Optional类的常用方法:

isPresent() : 判断是否包含值,包含值返回true,不包含值返回false get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值 map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()

@Test 
public void test02() { 
    // Optional<String> userNameO = Optional.of("凤姐"); 
    // Optional<String> userNameO = Optional.of(null); 
    // Optional<String> userNameO = Optional.ofNullable(null); 
    Optional<String> userNameO = Optional.empty(); 
    // isPresent() : 判断是否包含值,包含值返回true,不包含值返回falseif (userNameO.isPresent()) { 
        // get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException。 
        String userName = userNameO.get(); 
        System.out.println("用户名为:" + userName); 
    } else { 
        System.out.println("用户名不存在"); 
    } 
}

Optional的高级使用

@Test 
public void test03() { 
    Optional<String> userNameO = Optional.of("凤姐"); 
    // Optional<String> userNameO = Optional.empty(); 
    // 存在做的什么 
    // userNameO.ifPresent(s -> System.out.println("用户名为" + s)); 
    // 存在做的什么,不存在做点什么 
    userNameO.ifPresentOrElse(s -> System.out.println("用户名为" + s) 
                              , () -> System.out.println("用户名不存在")); 
}
@Test 
public void test04() { 
    // Optional<String> userNameO = Optional.of("凤姐"); 
    Optional<String> userNameO = Optional.empty(); 
    // 如果调用对象包含值,返回该值,否则返回参数t 
    System.out.println("用户名为" + userNameO.orElse("null"));
    // 如果调用对象包含值,返回该值,否则返回参数Supplier得到的值 
    String s1 = userNameO.orElseGet(() -> {return "未知用户名";}); 
    System.out.println("s1 = " + s1); 
}

Time

新日期时间 API介绍

JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于java.time 包 中,下面是一些关键类。 LocalDate :表示日期,包含年月日,格式为 2019-10-16 LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300 LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750 DateTimeFormatter :日期时间格式化类。 Instant:时间戳,表示一个特定的时间瞬间。 Duration:用于计算2个时间(LocalTime,时分秒)的距离 Period:用于计算2个日期(LocalDate,年月日)的距离 ZonedDateTime :包含时区的时间

JDK 8的日期和时间类

日期时间的获取

LocalDate、LocalTime、LocalDateTime类的实例是不可变的对象,分别表示使用 ISO-8601 日历系统的日期、时 间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。

// LocalDate:获取日期时间的信息。格式为 2019-10-16 
@Test 
public void test01() { 
    // 创建指定日期 
    LocalDate fj = LocalDate.of(1985, 9, 23); 
    System.out.println("fj = " + fj); // 1985-09-23 
    // 得到当前日期 
    LocalDate nowDate = LocalDate.now(); 
    System.out.println("nowDate = " + nowDate); // 2019-10-16 
    // 获取日期信息 
    System.out.println("年: " + nowDate.getYear()); 
    System.out.println("月: " + nowDate.getMonthValue()); 
    System.out.println("日: " + nowDate.getDayOfMonth()); 
    System.out.println("星期: " + nowDate.getDayOfWeek()); 
}
// LocalTime类: 获取时间信息。格式为 16:38:54.158549300 
@Test 
public void test02() { 
    // 得到指定的时间 
    LocalTime time = LocalTime.of(12,15, 28, 129_900_000); 
    System.out.println("time = " + time); 
    // 得到当前时间 
    LocalTime nowTime = LocalTime.now(); 
    System.out.println("nowTime = " + nowTime); 
    // 获取时间信息 
    System.out.println("小时: " + nowTime.getHour()); 
    System.out.println("分钟: " + nowTime.getMinute()); 
    System.out.println("秒: " + nowTime.getSecond()); 
    System.out.println("纳秒: " + nowTime.getNano()); 
}
// LocalDateTime类: 获取日期时间信息。格式为 2018-09-06T15:33:56.750 
@Test 
public void test03() { 
    LocalDateTime fj = LocalDateTime.of(1985, 9, 23, 9, 10, 20); 
    System.out.println("fj = " + fj); // 1985-09-23T09:10:20 
    // 得到当前日期时间 
    LocalDateTime now = LocalDateTime.now(); 
    System.out.println("now = " + now); // 2019-10-16T16:42:24.497896800 
    System.out.println(now.getYear()); 
    System.out.println(now.getMonthValue()); 
    System.out.println(now.getDayOfMonth());
    System.out.println(now.getHour());
    System.out.println(now.getMinute());
    System.out.println(now.getSecond());
    System.out.println(now.getNano()); 
}

日期时间的修改

对日期时间的修改,对已存在的LocalDate对象,创建它的修改版,最简单的方式是使用withAttribute方法。 withAttribute方法会创建对象的一个副本,并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象,他们不会影响原来的对象.

// LocalDateTime类: 对日期时间的修改 
@Test 
public void test05() { 
    LocalDateTime now = LocalDateTime.now(); 
    System.out.println("now = " + now); 
    // 修改日期时间 
    LocalDateTime setYear = now.withYear(2078); 
    System.out.println("修改年份: " + setYear); 
    System.out.println("now == setYear: " + (now == setYear)); 
    System.out.println("修改月份: " + now.withMonth(6)); 
    System.out.println("修改小时: " + now.withHour(9)); 
    System.out.println("修改分钟: " + now.withMinute(11)); 
    // 再当前对象的基础上加上或减去指定的时间 
    LocalDateTime localDateTime = now.plusDays(5); 
    System.out.println("5天后: " + localDateTime); 
    System.out.println("now == localDateTime: " + (now == localDateTime)); 
    System.out.println("10年后: " + now.plusYears(10)); 
    System.out.println("20月后: " + now.plusMonths(20)); 
    System.out.println("20年前: " + now.minusYears(20)); 
    System.out.println("5月前: " + now.minusMonths(5)); 
    System.out.println("100天前: " + now.minusDays(100)); 
}

日期时间的比较

// 日期时间的比较 
@Test 
public void test06() { 
    // 在JDK8中,LocalDate类中使用isBefore()、isAfter()、
    //isEqual()方法来比较两个日期,可直接进行比较。 
    LocalDate now = LocalDate.now(); 
    LocalDate date = LocalDate.of(2018, 8, 8); 
    System.out.println(now.isBefore(date)); // false 
    System.out.println(now.isAfter(date)); // true 
}

JDK 8的时间格式化与解析通过

通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。

// 日期格式化 
@Test 
public void test04() { 
    // 得到当前日期时间 
    LocalDateTime now = LocalDateTime.now(); 
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 
    // 将日期时间格式化为字符串 
    String format = now.format(formatter); 
    System.out.println("format = " + format); 
    // 将字符串解析为日期时间 
    LocalDateTime parse = LocalDateTime.parse("1985-09-23 10:12:22", formatter); 
    System.out.println("parse = " + parse); 
}

JDK 8的 Instant 类

Instant 时间戳/时间线,内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。

// 时间戳 
@Test 
public void test07() { 
    Instant now = Instant.now(); 
    System.out.println("当前时间戳 = " + now); 
    // 获取从1970年1月1日 00:00:00的秒 
    System.out.println(now.getNano()); 
    System.out.println(now.getEpochSecond()); 
    System.out.println(now.toEpochMilli()); 
    System.out.println(System.currentTimeMillis()); 
    Instant instant = Instant.ofEpochSecond(5); 
    System.out.println(instant); 
}

JDK 8的计算日期时间差类

Duration/Period类: 计算日期时间差。 1. Duration:用于计算2个时间(LocalTime,时分秒)的距离 2. Period:用于计算2个日期(LocalDate,年月日)的距离

// Duration/Period类: 计算日期时间差 
@Test 
public void test08() { 
    // Duration计算时间的距离 
    LocalTime now = LocalTime.now(); 
    LocalTime time = LocalTime.of(14, 15, 20); 
    Duration duration = Duration.between(time, now); 
    System.out.println("相差的天数:" + duration.toDays()); 
    System.out.println("相差的小时数:" + duration.toHours()); 
    System.out.println("相差的分钟数:" + duration.toMinutes()); 
    System.out.println("相差的秒数:" + duration.toSeconds()); 
    // Period计算日期的距离 
    LocalDate nowDate = LocalDate.now(); 
    LocalDate date = LocalDate.of(1998, 8, 8); 
    // 让后面的时间减去前面的时间 
    Period period = Period.between(date, nowDate); 
    System.out.println("相差的年:" + period.getYears()); 
    System.out.println("相差的月:" + period.getMonths()); 
    System.out.println("相差的天:" + period.getDays()); 
}

JDK 8的时间校正器

有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。 TemporalAdjuster : 时间校正器。 TemporalAdjusters : 该类通过静态方法提供了大量的常用TemporalAdjuster的实现

// TemporalAdjuster类:自定义调整时间 
@Test 
public void test09() { 
    LocalDateTime now = LocalDateTime.now(); 
    // 得到下一个月的第一天 
    TemporalAdjuster firsWeekDayOfNextMonth = temporal -> { 
        LocalDateTime dateTime = (LocalDateTime) temporal; 
        LocalDateTime nextMonth = dateTime.plusMonths(1).withDayOfMonth(1); 
        System.out.println("nextMonth = " + nextMonth); 
        return nextMonth; 
    };
    LocalDateTime nextMonth = now.with(firsWeekDayOfNextMonth); 
    System.out.println("nextMonth = " + nextMonth); 
}

JDK 8设置日期时间的时区

Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别 为:ZonedDate、ZonedTime、ZonedDateTime。 其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。 ZoneId:该类中包含了所有的时区信息。

// 设置日期时间的时区 
@Test 
public void test10() { 
    // 1.获取所有的时区ID 
    // ZoneId.getAvailableZoneIds().forEach(System.out::println); 
    // 不带时间,获取计算机的当前时间 
    // 中国使用的东八区的时区.比标准时间早8个小时 
    LocalDateTime now = LocalDateTime.now(); 
    System.out.println("now = " + now); 
    // 2.操作带时区的类 
    // now(Clock.systemUTC()): 创建世界标准时间 
    ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC()); 
    System.out.println("bz = " + bz); 
    // now(): 使用计算机的默认的时区,创建日期时间 
    ZonedDateTime now1 = ZonedDateTime.now(); 
    System.out.println("now1 = " + now1);
    // 2019-10- 19T16:19:44.007153500+08:00[Asia/Shanghai] 
    // 使用指定的时区创建日期时间 
    ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Vancouver")); 
    System.out.println("now2 = " + now2); 
    // 2019-10-19T01:21:44.248794200- 07:00[America/Vancouver] 
}

小结 详细学习了新的日期是时间相关类,LocalDate表示日期,包含年月日,LocalTime表示时间,包含时分 秒,LocalDateTime = LocalDate + LocalTime,时间的格式化和解析,通过DateTimeFormatter类型进行. 学习了Instant类,方便操作秒和纳秒,一般是给程序使用的.学习Duration/Period计算日期或时间的距离,还使用时间调 整器方便的调整时间,学习了带时区的3个类ZoneDate/ZoneTime/ZoneDateTime JDK 8新的日期和时间 API的优势:

  1. 新版的日期和时间API中,日期和时间对象是不可变的。操纵的日期不会影响老值,而是新生成一个实例。

  2. 新的API提供了两种不同的时间表示方式,有效地区分了人和机器的不同需求。

  3. TemporalAdjuster可以更精确的操纵日期,还可以自定义日期调整器。

  4. 是线程安全的