JDK8特性與使用手記

新特性總覽

  • lambda表達式
  • Stream操作數組
  • Optional取代null
  • 簡潔的并發編程
  • LocalDateTime新的時間API

Lambda表達式

  • 概念:Lambda表達式是一個匿名函數,Lambda表達式基于數學中的λ演算得名,直接對應其中的Lambda抽象(lambda abstraction),是一個匿名函數,既沒有函數名的函數。Lambda表達式可以表示閉包(注意和數學傳統意義的不同)。你也可以理解為,簡潔的表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出異常的列表。

  • 作用:既然是匿名函數,那就類比于匿名內部類的用法咯,哪些地方,會用到一些代碼量大但實際邏輯不算復雜的方法調用,就可以用到它。

    什么是函數式接口?

    答:僅僅只有一個抽象方法的接口。

  • 語法:

    • () -> 表達式
    • () -> {語句;}
    • () -> 對象
    • (Class)() -> {語句;}【指定對象類型】
      public static void doSomething(Runnable r) { r.run(); }
      public static void doSomething(Task a) { a.execute(); }
      ....
      doSomething(() -> System.out.println("DDDD"));
      // 為了避免隱晦的方法調用,嘗試顯式地類型轉換
      doSomething((Runnable)() -> System.out.println("DDDD"));
      
  • 使用場景(很多特殊場景都包含在內)
    總的來說,只有在接受函數式接口的地方才可以使用Lambda表達式。


    image.png
  • 一些Jdk8的lambda語法糖:

    Lambda:(Apple a) -> a.getWeight() 
    方法引用:Apple::getWeight
    
    Lambda:() -> Thread.currentThread().dumpStack() 
    方法引用:Thread.currentThread()::dumpStack
      
    Lambda:(str, i) -> str.substring(i)
    方法引用:String::substring
      
    Lambda:(String s) -> System.out.println(s)
    方法引用:System.out::println
    
  • 構造函數引用

    • 無參構造器
      Supplier<Apple> c1 = Apple::new;
      Apple apple = c1.get();
      // 等價于
      Supplier<Apple> c2 = () -> new Apple();
      
    • 一參構造器
      Function<Integer, Apple> c1 = Apple::new;
      Apple apple = c1.apply(123);
      
    • 兩參構造器
      BiFunction<Integer, String, Apple> c1 = Apple::new;
      Apple apple = c2.apply(120, "red");
      
  • 簡化的數組排序

    apples.sort(comparing(Apple::getWeight));
    // 其中:
    // ArrayList.sort() since:1.2
    // Comparator.comparing(Function<Apple, Integer>) since: 1.8
    
  • 更復雜的數組排序

    • 倒序
      apples.sort(Comparator.comparing(Apple::getWeight).reversed());
      
    • 多條件排序
      apples.sort(Comparator.comparing(Apple::getWeight).reversed()
                  .thenComparing(Apple::getCountry));
      
  • Predicate的復合

    以下三個基本謂詞,可以配合已有的謂詞(Predicate),來制造出更加復雜的謂詞。

    • negate:“非”
      private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
          List<T> result = new ArrayList<>();
          for (T t : list) {
              if (predicate.test(t)) {
                  result.add(t);
              }
          }
          return result;
      }
      // 紅蘋果
      Predicate<Apple> redApplePd = a -> "red".equals(a.getColor());
      // 不是紅蘋果
      Predicate<Apple> notRedApplePd = a -> redApplePd.negate();
      // 過濾
      List<Apple> redApple = filter(rawApples, redApplePd);
      List<Apple> notRedApple = filter(rawApples, notRedApplePd);
      
    • and:“與”
      Predicate<Apple> redHeavyApplePd = ((Predicate<Apple>) apple -> apple.color.equals("red")).and(apple -> false);
      
    • or:“或”(同理)
  • Function的復合

    • andThen
      f.andThen(g)相當于 g(f()),先執行f(),后執行g()
      Function<Integer, Integer> dbSelf = x -> x * 2;
      Function<Integer, Integer> oppositeNum = x -> -1 * x;
      Function<Integer, String> toStr = String::valueOf;
      String result = dbSelf.andThen(oppositeNum).andThen(toStr).apply(1); // "-2"
      
    • compose
      f.compose(g)相當于 f(g()),先執行g(),后執行f()
      Function<Integer, String> toStr = x -> "ToString:" + x;
      Function<String, String> split = x -> x.split(":")[1];
      Function<String, Integer> toInt = Integer::valueOf;
      int result = toInt.compose(split.compose(toStr)).apply(123); // 123
      

Stream

  • 概念:流是Java API新成員,它允許你以聲明性方式處理數據集合。
  • 特點:
    • 流水線:類似“鏈式調用”,一步接一步。
    • 內部迭代:與使用迭代器顯式迭代的集合不同,流的迭代操作是在背后進行的。
  • 作用:
    • 減少for循環
    • 減少數組操作中可能聲明的垃圾變量的數量
    • 直觀、提高可讀性
  • 流和集合的區別:
    • 類似于看視頻,無論點到視頻的哪一段,它都能很快加載出來,這就是流。而集合相當于我要把整部電影down下來,才能點哪看哪。
    • 集合是內存中的數據結構,它包含數據結構中目前所有的值,集合中每個元素都需要事先計算好,才被放入集合。
    • 流是在概念上固定的數據結構,其元素時按需計算的(懶加載)。需要多少就給多少。換一個角度,流像是一個延遲創建的集合:只有在消費者要求的時候才會生成值。
  • 看一個例子
    public List<Apple> filterApple(List<Apple> apples, Predicate<Apple> criteria) {
          List<Apple> result = new ArrayList<>();
          apples.forEach(apple -> {
              if (criteria.test(apple)) {
                  result.add(apple);
              }
          });
          return result;
      }
    
    如果只用Lambda表達式,那操作數組起來,也還是需要一些for循環的加持。
    而有了Stream,寫起code就簡單很多了。
    List<Apple> redHeavyApples = apples.stream()
      .filter(apple -> "red".equals(apple.color))
      .filter(apple -> apple.weight > 120)
      .collect(Collectors.toList());
    
  • 相關包、類、方法
    • 包:java.util.stream
    • 接口:
      • BaseStream<T, S extends BaseStream<T, S>> extends AutoCloseable
      • Stream<T> extends BaseStream<T, Stream<T>>
      • DoubleStream extends BaseStream<Double, DoubleStream>
      • IntStream extends BaseStream<Integer, IntStream>
      • LongStream extends BaseStream<Long, LongStream>
    • 方法:
      • filter(Predicate<? super T>):Stream<T>
      • map(Function<? super T, ? extends R>):Stream<R>
      • mapToInt(ToIntFunction<? super T>):LongStream
      • mapToDouble(ToDoubleFunction<? super T>):DoubleStream
      • mapToLong
      • flatMap(Function<? super T, ? super Stream<? extends R>>):Stream<R>
      • flatMapToInt()
      • flatMapToLong()
      • flatMapToDouble()
      • distinct():Stream<T>
      • sorted():Stream<T>
      • sorted(Comparator<? super T>):Stream<T>
      • peek(Consumer<? super T>):Stream<T>
      • limit(long):Stream<T>
      • skip(long):Stream<T>
      • forEach(Consumer<? super T>):void
      • forEachOrdered()
      • toArray():Object[]
      • toArray(IntFunction<A[]>):Object[]
      • reduce(T, BinaryOperator<T>):T
      • reduce(BinaryOperator<T>):Optional<T>
      • reduce(U, BiFunction<U, ? super T, U>, BinaryOperator<U>):U
      • collect(Supplier<R>, BiConsumer<R, ? super T>, BiConsumer<R,R>):R
      • collect(Collector<? super T,A,R>):R
      • min(Comparator<? super T>):Optional<T>
      • max
      • count():long
      • anyMatch(Predicate<? super T>):boolean
      • allMatch
      • noneMatch
      • findFirst():Optional<T>
      • findAny
      • builder():Builder<T>
      • empty():Stream<T>
      • of(T...):Stream<T>
      • iterate(T, UnaryOperator<T>):Steram<T>
      • generate(Supplier<T>):Stream<T>
      • concat(Stream<? extends T>, Stream<? extends T>):Stream<T>
  • 例子:取重的綠蘋果,然后升序排序,取它的重量
    List<Integer> greenHeavyAppleWeight = apples.stream()
          .filter(apple -> apple.weight > 120)
          .filter(apple -> "green".equals(apple.color))
          .sorted(comparing(Apple::getWeight)) // Comparator.comparing()
          .map(apple -> apple.weight)
          .collect(toList()); // Collectors.toList()
    
  • 流只能被消費一次
    List<String> names = Arrays.asList("Java8", "Lambdas", "In", "Action");
      Stream<String> s = names.stream();
      s.forEach(System.out::println);
      // 再繼續執行一次,則會拋出異常
      s.forEach(System.out::println);
    
  • flatMap()實現流的扁平化
    String[] words = {"Hello", "World"};
    Stream<String> streamOfWords = Arrays.stream(words);
    
    // 沒有打平,是兩個 String[],我需要兩個嵌套for 循環來打印內容
    List<String[]> a = streamOfWords.map(w -> w.split("")).collect(toList());
    for (String[] itemStrings: a) {
        System.out.println("item.length: " + itemStrings.length);
        for (String item: itemStrings) {
            System.out.print(item);
        }
        System.out.println();
    }
    
    沒有打平,出來的是兩個String[],我需要兩個嵌套for 循環來打印內容。而如果用flatMap:
    String[] words = {"Hello", "World"};
    Stream<String> streamOfWords = Arrays.stream(words);
    
    // 打平,一個for循環就搞定
    List<String> chars = streamOfWords
            .map(w -> w.split(""))
            .flatMap(Arrays::stream)
            .collect(toList());
    for (String item: chars) {
        System.out.print(item + "-");
    }
    

打平之后,直接操作一個數組就好。

  • 例子:求最大最小值
    List<Integer> numbers = Arrays.asList(2, 5, 3, 4, 1, 6, 3, 5);
    // way 1
    Integer max = numbers.stream().max(Integer::compareTo).orElse(null);
    // way 2
    max = numbers.stream().reduce(Integer::max).orElse(null);
    
  • 原始類型流的特化(Stream轉IntStream/LongStream/DoubleStream)

    作用:直接特化為原始類型:int、long、double,避免暗含的裝箱成本。以及,有了一些額外的計算方法。

    • 映射到數值流:mapToInt/mapToLong/mapToDouble
    • 轉回對象流:boxed
  • 特化流的一下額外方法
    // 獲得 1到100 的所有偶數
    IntStream.rangeClosed(1, 100).filter(num -> num%2 == 0).forEach(System.out::println);
    
  • 構建流
    • 方式一:由值創建流
      Stream<String> stream = Stream.of("Java8", "Lambda", "In");
      stream.map(String::toUpperCase).forEach(System.out::println);
      
    • 方式二:由數組創建流
      int[] nums = {2,4,6,7,8,12};
      int sum = Arrays.stream(nums).sum();
      
    • 方式三:由集合創建流
      List<Integer> nums = Arrays.asList(1,2,3,4,5);
      Stream<Integer> numStream = nums.stream();
      Stream<Integer> parallelStream = nums.parallelStream();
      
    • 方式四:文件 + NIO 創建流

      利用 java.nio.file.Files中的一些靜態方法(靜態方法 since JDK1.8)都返回一個流。Filessince JDK1.7

      一個很有用的方法是 Files.lines ,它會返回一個由指定文件中的各行構成的字符串流。
      long uniqueWords; try (Stream<String> lines = Files.lines(Paths.get(ClassLoader.getSystemResource("data.txt").toURI()), Charset.defaultCharset())) { uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) .distinct() .count(); System.out.println("uniqueWords:" + uniqueWords); } catch (IOException e) { e.fillInStackTrace(); } catch (URISyntaxException e) { e.printStackTrace(); }

    • 方式五:由函數生成流
      以下兩個操作,可以創建“無限流”,一般配合limit 使用
      Stream.iterate(<初始值>, <值的變化函數>)
      Stream.generate(Supplier<? extends Object> s)
      
      不像從固定集合創建的流那樣有固定大小的流。由 iterate和 generate 產生的流會用給定的函數按需創建值,因此可以無窮無盡地計算下去。
      // 迭代:每次返回前一個元素加2的值
      Stream.iterate(0, n -> n + 2)
          .limit(10)
          .forEach(System.out::println);
      
      // 生成:接收一個Supplier類型的函數(有出無入的函數)
      Stream.generate(Math::random)
          .limit(5)
          .forEach(System.out::println);
      
      看下面這個例子,對比Lambda表達式(匿名函數)和匿名內部類:
      // lambda
      IntStream twos = IntStream.generate(() -> 2);
      // 匿名內部類
      IntStream twos = IntStream.generate(new IntSupplier() {
          @Override
          public int getAsInt() {
              return 2;
          }
      });
      
      如果這個IntSupplier中不存在成員變量,那么,兩者等價。總的來說,匿名內部類更加靈活,而且其output值不一定唯一不變,較為靈活。
  • 收集器的用法
    • 接口:java.util.stream.Collector
    • 作用:對Stream的處理結果做收集。
    • 例子:
      • 分組

        List<Apple> apples = Arrays.asList(
              new Apple(130, "red"),
              new Apple(22, "red"),
              new Apple(60, "green"),
              new Apple(162, "green"),
              new Apple(126, "green"),
              new Apple(142, "green"),
              new Apple(117, "green")
        );
        // 根據顏色分組
        Map<String, List<Apple>> colorAppleMap = apples.stream().collect(groupingBy(apple -> apple.color));
        
      • 多級分組

        Map<Dish.Type, Map<Dish.CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
        menu.stream().collect(
                groupingBy(Dish::getType,
                        groupingBy(dish -> {
                            if (dish.getCalories() <= 400) {
                                return Dish.CaloricLevel.DIET;
                            } else if (dish.getCalories() <= 700) {
                                return Dish.CaloricLevel.NORMAL;
                            } else {
                                return Dish.CaloricLevel.FAT;
                            }
                        })
                )
        );
        
      • 分組的一些配合操作

        Stream.collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
        Stream.collect(groupingBy(Dish::getType, mapping(...)));
        // 如:
        Map<Dish.Type, Set<Dish.CaloricLevel>> caloricLevelsByType =
        menu.stream().collect(
             groupingBy(Dish::getType, mapping(
                  dish -> {
                      if (dish.getCalories() <= 400) {
                          return Dish.CaloricLevel.DIET;
                      } else if (dish.getCalories() <= 700) {
                          return Dish.CaloricLevel.NORMAL;
                      } else {
                          return Dish.CaloricLevel.FAT;
                      }
                  },
                  toSet())));
        
      • 計數

        long count = apples.size();
        long count = apples.stream().collect(Collectors.counting());
        long count = apples.stream().count();
        
      • 匯總

        // 求和
        int totalWeight = apples.stream().collect(summingInt(Apple::getWeight));
        totalWeight = apples.stream().mapToInt(Apple::getWeight).sum();
        
        // 平均數
        double avgWeight = apples.stream().collect(averagingDouble(Apple::getWeight));
        avgWeight = apples.stream().mapToDouble(Apple::getWeight).average().orElse(-1);
        
        // 匯總
        IntSummaryStatistics appleStatistics = apples.stream().collect(summarizingInt(Apple::getWeight));
        System.out.println(appleStatistics.getMax());
        System.out.println(appleStatistics.getMin());
        System.out.println(appleStatistics.getAverage());
        System.out.println(appleStatistics.getCount());
        System.out.println(appleStatistics.getSum());
        
      • 連接字符串

        String[] strs = {"Hello", "World"};
        String result = Arrays.stream(strs).collect(joining([分隔符]));
        
      • 分區

        根據Predicate條件,分成true和false兩部分集合。

        Map<Boolean, List<Apple>> partitionApples = apples.stream().collect(partitioningBy(apple -> "green".equals(apple.color)));
        

        看似沒有什么特點,但是其實和grouping類似,有一個downStream:Collector的一個第二參數,這就厲害了,擴展性很強。

        // 先劃分了素食和非素食,然后,每一類里面,去熱量最高的一個。
        Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = menu.stream().collect(
          partitioningBy(Dish::isVegetarian, collectingAndThen(
                  maxBy(comparingInt(Dish::getCalories)),
                  Optional::get
          )));
        
  • 自定義流(這個就再說咯)
  • 并行流

    相對于stream(),用parallelStream()就能把集合轉換為并行流。

    • 概念:并行流就是一個把內容分成多個數據塊,并用不同線程分別處理每個數據塊的流。
    • 方法:
      切換為并行流:Stream.parallel()
      切換為順序流:Stream.sequential()
      // 注意,誰最后調用,流就apply誰。
      
    • 用并行流之前,要測試性能(如果遇到iterator裝包解包等的情況,實際上并行鎖會更加慢,能用特化流就盡量用特化流)
    • Spliterator 定義了并行流如何拆分它要遍歷的數據

Optional

  • class:java.util.Optional
  • 作用:解決和避免NPE異常
  • Optional對象的方法
    • get():不推薦使用。如果變量存在,它直接返回封裝的變量值,否則就拋出一個NPE或者NoSuchElementException異常。
      Optional方法 調用get()后拋出異常
      Optional.of(null) java.lang.NullPointerException
      Optional.ofNullable(null) java.util.NoSuchElementException
    • orElse(T other)
    • orElseGet(Supplier<? extends T> other)orElse的延遲調用版,Supplier方法只有在Optional對象不含值時才執行。
      • 適用場景:
        • 創建默認值是耗時的工作。
        • 或者需要十分確定某個方法僅在Optional為空時才調用。
      Person p1 = null;
      Optional<Person> optP1 = Optional.ofNullable(p1);
      Person resultP = optP1.orElseGet(() -> {
          Person p = new Person();
          p.firstName = "Fang";
          return p;
      });
      System.out.println("resultP.firstName: " + resultP.firstName);
      
    • orElseThrow(Supplier<? extends X> exceptionSupplier):定制拋出的異常。
    • ifPresent(Consumer<? super T>):當變量值存在時執行一個作為參數傳入的方法,否則不做任何操作。
      Person p1 = new Person();
      Optional<Person> optP1 = Optional.ofNullable(p1);
      optP1.ifPresent(person -> System.out.println("Haha"));
      
    • filter
      Person p1 = new Person();
      p1.firstName="Fang";
      p1.lastName="Hua";
      Person resP = Optional.ofNullable(p1).filter(person -> "Hua".equals(person.lastName)).orElseGet(() -> {
          Person newP = new Person();
          newP.firstName= "Ming";
          return newP;
      });
      System.out.println(resP.firstName); // Ming
      
    • 當然,還有一些方法與Stream接口相似,如mapflatMap
  • 例子:
    • 對象嵌套取值
      • old
        @Test
        public void test_optional_1() {
            Person person = new Person();
            // 當然從重構角度來看,這里是不對的,我們知道太多這個類內部的東西,是需要重構的
            String name = person.getCar().getInsurance().getName();
        }
        public class Person {
            private Car car;
            public Car getCar() { return car; }
        }
        public class Car {
            private Insurance insurance;
            public Insurance getInsurance() { return insurance; }
        }
        public class Insurance {
            private String name;
            public String getName() { return name; }
        }
        
      • new
        public String getCarInsuranceName(Person person) {
            return Optional.ofNullable(person).flatMap(Person::getCar)
                    .flatMap(Car::getInsurance)
                    .map(Insurance::getName)
                    .orElse("Unknown");
        }
        
        public class Person {
            private String sex;
            private String firstName;
            private String lastName;
            private Optional<Car> car = Optional.empty();
        
            public Optional<Car> getCar() {
                return car;
            }
        }
        
        public class Car {
            private Optional<Insurance> insurance = Optional.empty();
        
            public Optional<Insurance> getInsurance() {
                return insurance;
            }
        }
        
        public class Insurance {
            private String name;
        
            public String getName() {
                return name;
            }
        }
        
    • 封裝可能為空的值
      Object value = map.get("key");
      // 加上 Optional
      Optional<Object> value = Optional.ofNullable(map.get("key"));
      
      // 如: Map<String, Person> map
      String valA = Optional.ofNullable(map.get("A")).orElse(new Person()).firstName;
      
    • 異常與Optional 去替代 if-else判斷
      public static Optional<Integer> stringToInt(String s) {
          try {
              return Optional.of(Integer.parseInt(s));
          } catch (NumberFormatException e) {
              return Optional.empty();
          }
      }
      
    • 兩個Optional對象的組合
      public Insurance findBestInsurance(Person person, Car car) {
          Insurance insurance = new Insurance();
          insurance.name = person.firstName + person.lastName + " --insurance 01";
          return insurance;
      }
      
      public Optional<Insurance> nullSafeFindBestInsurance(Optional<Person> person) {
          if (person.isPresent() && person.get().getCar().isPresent()) {
              Car car = person.get().getCar().get();
              return Optional.of(findBestInsurance(person.get(), car));
          } else {
              return Optional.empty();
          }
      }
      
  • 原理:
    • 變量存在時,Optional類只是對類簡單封裝。
    • 變量不存在時,缺失的值會被建模成一個“空”的Optional對象,由方法Optional.empty()返回。
    • Optional.empty()是一個靜態工廠方法,

函數式編程(Stream+Lambda)

函數式編程 VS 命令式編程

  • 命令式編程關注怎么做,而函數式編程關注做什么
  • 函數式編程 可讀性強,但運行速度不見得更快。

例子

1. for循環取數組最小值

int[] nums = {1,3,-1,6,-20};
int min = Integer.MAX_VALUE;
for (int i:nums) {
    if(i < min) {
        min = i;
    }
}

變成

int min2 = IntStream.of(nums).parallel().min().getAsInt();

2. 接口的實現/匿名內部類轉Lambda

/// 接口實現
Object target = new Runnable() {
    @Override
    public void run() {
        System.out.println("新建一個線程");
    }
};
new Thread((Runnable) target).start();

/// 匿名內部類
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("BBB");
    }
}).start();

變成

Object target2 = (Runnable)() -> System.out.println("新建一個線程2");
Runnable target3 = () -> System.out.println("新建一個線程3");
System.out.println("target2 == target3 :" + (target2 == target3)); // false
new Thread((Runnable) target2).start();

new Thread(() -> System.out.println("BBB")).start()

3. Lambda創建自定義接口的實例對象

必備條件:

  1. 該接口中只能有一個抽象方法
  2. 在接口上加上@FunctionalInterface注解(可選:為了編譯器的校驗,有這個注解的接口,當存在多個抽象方法時,是會編譯報錯的。)

JDK8 中,接口中可以定義靜態方法和默認方法。

@FunctionalInterface
interface Interface1 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }

    static int sub(int x, int y) {
        return x - y;
    }
}

@FunctionalInterface
interface Interface2 {
    int doubleNum(int i);

    default int add(int x, int y) {
        return x + y;
    }
}

@FunctionalInterface
interface Interface3 extends Interface1, Interface2 {
    @Override
    default int add(int x, int y) {
        return Interface1.super.add(x, y);
    }
}


@Test
public void test_lambda_1() {
    Interface1 i1 = (i) -> i * 2;
    System.out.println("Interface1.sub(10, 3): " + Interface1.sub(10, 3));
    System.out.println("i1.add(3,7):" + i1.add(3, 7));
    System.out.println("i1.doubleNum(20):" + i1.doubleNum(20));

    Interface2 i2 = i -> i * 2;
    Interface3 i3 = (int i) -> i * 2;
    Interface3 i4 = (int i) -> {
        System.out.println(".....");
        return i * 2;
    };
}

4. Lambda與Function

String cityName= "HongKong";
int stateCode=237;
String street = "東岸村黃皮樹下街1號";

String locationID = "";

Function<String, String> locationIDBuilder = locId -> locId + cityName; // Step 1
locationID = locationIDBuilder
        .andThen(locId -> locId + ",區號:" + stateCode) // Step 2
        .andThen(locId -> locId+",街道:" + street).apply(locationID); // Step 3
System.out.println("locationID:" + locationID);

JDK 1.8 API包含了很多內建的函數式接口,在老Java中常用到的比如Comparator或者Runnable接口,這些接口都增加了@FunctionalInterface注解以便能用在lambda上

name type description
Consumer Consumer< T > 接收T對象,不返回值
Predicate Predicate< T > 接收T對象并返回boolean
Function Function< T, R > 接收T對象,返回R對象
Supplier Supplier< T > 提供T對象(例如工廠),不接收值
UnaryOperator UnaryOperator 接收T對象,返回T對象
BinaryOperator BinaryOperator 接收兩個T對象,返回T對象

Lambda 與 設計模式

策略模式

interface ValidationStrategy {
    boolean execute(String s);
}

static class IsAllLowerCase implements ValidationStrategy {
    @Override
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}

static class IsNumeric implements ValidationStrategy {
    @Override
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}

static class Validator {
    private final ValidationStrategy validationStrategy;

    public Validator(ValidationStrategy validationStrategy) {
        this.validationStrategy = validationStrategy;
    }

    public boolean validate(String s) {
        return validationStrategy.execute(s);
    }
}

正常來說,要new一些策略來當參數傳。

IsNumeric isNumeric = new IsNumeric();
IsAllLowerCase isAllLowerCase = new IsAllLowerCase();

Validator validatorA = new Validator(isNumeric);
Validator validatorB = new Validator(isAllLowerCase);

使用lambda,讓new盡量少出現在code中。

Validator validatorA = new Validator(s -> s.matches("\\d+"));
Validator validatorB = new Validator(s -> s.matches("[a-z]+"));

模板模式(抽象類的應用)

public abstract class AbstractOnlineBank {
    public void processCustomer(int id) {
        Customer customer = Database.getCustomerWithId(id);
        makeCustomerHappy(customer);
    }

    abstract void makeCustomerHappy(Customer customer);

    static class Customer {}

    static class Database {
        static Customer getCustomerWithId(int id) {
            return new Customer();
        }
    }
}
...
AbstractOnlineBank bank = new AbstractOnlineBank() {
    @Override
    void makeCustomerHappy(Customer customer) {
        System.out.println("Hello!");
    }
};
bank.processCustomer(1);
bank.processCustomer(2);

用了Lambda,抽象方法都用不著了

public class AbstractOnlineBank {
    public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
        Customer customer = Database.getCustomerWithId(id);
        makeCustomerHappy.accept(customer);
    }

    static class Customer {}

    static class Database {
        static Customer getCustomerWithId(int id) {
            return new Customer();
        }
    }
}
...
AbstractOnlineBank bank = new AbstractOnlineBank();
bank.processCustomer(1, customer -> System.out.println("Hello"));
bank.processCustomer(2, customer -> System.out.println("Hi"));

觀察者模式

interface Observer{
        void inform(String tweet);
    }

private static class NYTimes implements Observer {

    @Override
    public void inform(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("Breaking news in NY!" + tweet);
        }
    }
}

private static class Guardian implements Observer {

    @Override
    public void inform(String tweet) {
        if (tweet != null && tweet.contains("queen")) {
            System.out.println("Yet another news in London... " + tweet);
        }
    }
}

private static class LeMonde implements Observer {

    @Override
    public void inform(String tweet) {
        if(tweet != null && tweet.contains("wine")){
            System.out.println("Today cheese, wine and news! " + tweet);
        }
    }
}

interface Subject {
    void registerObserver(Observer o);

    void notifyObserver(String tweet);
}

private static class Feed implements Subject {
    private final List<Observer> observers = new ArrayList<>();

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void notifyObserver(String tweet) {
        observers.forEach(o -> o.inform(tweet));
    }
}

好的,看到了有一個觀察者接口,并且只有一個方法inform,那么,我們是不是就可以少聲明這幾個實現類呢?

Feed feedLambda = new Feed();
feedLambda.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("money")) {
        System.out.println("Breaking news in NY!" + tweet);
    }
});

feedLambda.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("queen")) {
        System.out.println("Yet another news in London... " + tweet);
    }
});

feedLambda.notifyObserver("Money money money, give me money!");

責任鏈模式

private static abstract class AbstractProcessingObject<T> {
    protected AbstractProcessingObject<T> successor;

    public void setSuccessor(AbstractProcessingObject<T> successor) {
        this.successor = successor;
    }

    public T handle(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handle(r);
        }
        return r;
    }

    protected abstract T handleWork(T input);
}

一看到,又是抽象類加不同的實現,那就想到是不是可以用匿名函數實現,而這里,我們使用UnaryOperator

// 流程A
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
// 流程B
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
// A --> B
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result2 = pipeline.apply("Aren't labdas really sexy?!!");

工廠模式

private interface Product {
}

private static class ProductFactory {
    public static Product createProduct(String name) {
        switch (name) {
            case "loan":
                return new Loan();
            case "stock":
                return new Stock();
            case "bond":
                return new Bond();
            default:
                throw new RuntimeException("No such product " + name);
        }
    }
}

static private class Loan implements Product {
}

static private class Stock implements Product {
}

static private class Bond implements Product {
}

簡單情況下,可以用Supplier去實現引用方法式的構造器調用,并且減少switch。

private static class ProductFactory {
    private static final Map<String, Supplier<Product>> map = new HashMap<>();

    static {
        map.put("loan", Loan::new);
        map.put("stock", Stock::new);
        map.put("bond", Bond::new);
    }

    public static Product createProduct(String name) {
        Supplier<Product> productSupplier = map.get(name);
        if (productSupplier != null) {
            return productSupplier.get();
        }

        throw new RuntimeException("No such product " + name);
    }
}

當然,如果工廠方法 createProduct 需要接收多個傳遞給產品構造方法的參數,這種方式的擴展性不是很好。

默認方法

Jdk8開始支持的東西

Jdk8中的接口支持在聲明方法的同時提供實現。

  • Jdk8支持接口中有靜態方法。
    interface IBoy {
          static String getCompany() {
              return "OOL";
          }
      }
    
  • Jdk8支持接口中有默認方法。【jdk8 新功能】
    // List.sort()
    default void sort(Comparator<? super E> c) {
          Object[] a = this.toArray();
          Arrays.sort(a, (Comparator) c);
          ListIterator<E> i = this.listIterator();
          for (Object e : a) {
              i.next();
              i.set((E) e);
          }
      }
    // Collection.stream()
    default Stream<E> stream() {
          return StreamSupport.stream(spliterator(), false);
      }
    

作用

  • 為接口定義默認的方法實現:默認方法提供了接口的這個方法的默認實現,那么,在使用Lambda匿名地構造接口實現時,就不需要顯示重寫接口的這個方法,默認方法自動就會繼承過來。
  • 新增的接口,只要加上default修飾符,就可以不被顯式繼承,因此,別的地方的代碼是不用改動的!!

    看個例子:jdk8以前的Iterator接口,實際上用戶是不在乎remove()方法的,而且當時是沒有forEachRemaining()方法的,那么,當時的做法,就是每次實現這個接口的時候,都需要顯式繼承并重寫remove()方法,很煩。

    到了jdk8, 有了default修飾符,那么,我們不想顯式重寫的remove()方法就可以不用重寫了,然后,jdk8新增了forEachRemaining()方法,也不需要其他實現類再去修改code了,因為它壓根不需要你顯式重寫。

解決繼承鏈上的沖突的規則

public interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}
public interface B extends A {
    default void hello() {
        System.out.println("Hello from B");
    }
}
public class C implements A, B {
    public static void main(String[] args) {
        // 猜猜打印的是什么?
        new C().hello();
    }
}

以上問題,就是菱形繼承問題,如果父子接口有同名的default方法,那么,以上代碼編譯不通過。
我們需要重寫這個方法:

public class C implements A, B {
    public static void main(String[] args) {
        new C().hello();
    }

    @Override
    public void hello() {
       A.super.hello();
    }

    OR

    @Override
    public void hello() {
       B.super.hello();
    }

    OR

    @Override
    public void hello() {
       System.out.println("Hello from C!");
    }
}

組合式異步編程

  • 術語: CompletableFuture
  • 背景:
    • Jdk 7 中引入了“并行/合并框架”

      注意:并行和并發的區別:

      • 并行(parallellism):
        • 并行指的是同一個時刻,多個任務確實真的在同時運行。多個任務不搶對方資源。
        • 例子:兩個人,各自一邊吃水果,吃完就吃pizza。【有多個CPU內核時,每個內核各自不占對方的資源,各做各的事,是可以達到真正意義上的“同時”的。】
      • 并發(concurrency):
        • 并發是指在一段時間內宏觀上多個程序同時運行。多個任務互相搶資源。
        • 例子:一個人,同時做多件事情。【單核計算器中,是不可能“同時”做兩件事的,只是時間片在進程間的切換很快,我們感覺不到而已。】
    • Jdk 8 中引入了并行流
    • Jdk 8 中改進了Future接口,并且,新增了CompletableFuture接口。
    • 如果我們現在有一個大的耗時任務要處理,我們可以將其拆分為多個小任務,讓其并行處理,最終再將處理的結果統計合并起來, 那么,我們可以結合 并行/合并框架 + 并行流 來快速實現。

RecursiveTask(JDK 1.7)

例子:實現一個100000個自然數的求和。

class SumTask extends RecursiveTask<Long> {
public static final int Flag = 50;
long[] arr;
int start;
int end;

public SumTask(long[] arr, int start, int end) {
    this.arr = arr;
    this.start = start;
    this.end = end;
}

public SumTask(long[] arr) {
    this.arr = arr;
    this.start = 0;
    this.end = arr.length;
}

@Override
protected Long compute() {
    // 如果不能進行更小粒度的任務分配
    int length = end - start;
    if (length <= Flag) {
        return processSequentially();
    }
    //分治
    int middle = (start + end) / 2;
    SumTask sumTaskOne = new SumTask(arr, start, middle);
    SumTask sumTaskTwo = new SumTask(arr, middle, end);
    invokeAll(sumTaskOne, sumTaskTwo);
    Long join1 = sumTaskOne.join();
    Long join2 = sumTaskTwo.join();
    return join1 + join2;
}

// 小任務具體是做什么
private long processSequentially() {
    long sum = 0;
    for (int i = start; i < end; i++) {
        sum += arr[i];
    }
    return sum;
}
}

@Test
public void test() {
  long[] arr = new long[1000];
  for (int i = 0; i < arr.length; i++) {
      arr[i] = (long) (Math.random() * 10 + 1);
  }
  // 線程池(since 1.7)
  ForkJoinPool forkJoinPool = new ForkJoinPool(5);
  ForkJoinTask<Long> forkJoinTask = new SumTask(arr);
  long result = forkJoinPool.invoke(forkJoinTask);
  System.out.println(result);
}
  • 總結: 分支/合并框架讓你得以用遞歸方式將可以并行的任務拆分成更小的任務,在不同的線程上執行,然后將各個子任務的結果合并起來生成整體結果。

Spliterator(可分迭代器 JDK 1.8)

(一)背景

  • jdk 1.8 加入,和Iterator一樣,也用于遍歷數據源元素,但它是為了并行執行而設計的。
  • jdk8已經為集合框架中的所有數據結構提供了一個默認的Spliterator實現。
  • 目的:
    • 為了優化在并行流做任務處理時的數據源拆分遍歷時,使用Iterator的裝包和解包的性能開銷。
    • 配合并行流更快地遍歷和處理元素。
  • 內部方法:
    public interface Spliterator<T> {
      // 如果還有元素要遍歷, 返回true
      boolean tryAdvance(Consumer<? super T> action);
      // 把一些元素拆分給第二個Spliterator,不斷對 Spliterator 調用 trySplit直到它返回 null ,表明它處理的數據結構不能再分割
      Spliterator<T> trySplit();
      // 估計剩下多少元素要遍歷
      long estimateSize();
      // 用于影響開分過程的配置參數
      int characteristics();
    }
    
  • 延遲綁定的Spliterator:Spliterator可以在第一次遍歷、第一次拆分或第一次查詢估計大小時綁定元素的數據源,而不是在創建時就綁定。這種情況下,它稱為延遲綁定(late-binding)的 Spliterator 。

(二)例子

例子:一個自定義的并行迭代器,用于處理單詞數量統計

// 首先,我們有一個英文句子,我們要統計它的單詞數量
public static final String SENTENCE =
        " Nel   mezzo del cammin  di nostra  vita " +
                "mi  ritrovai in una  selva oscura" +
                " che la  dritta via era   smarrita ";
  • 方法一:通常手段,寫一個方法,直接實現統計邏輯
    public static int countWords1(String s) {
          int counter = 0;
          boolean lastSpace = true;
          for (char c : s.toCharArray()) {
              if (Character.isWhitespace(c)) {
                  lastSpace = true;
              } else {
                  if (lastSpace) {
                      counter ++;
                  }
                  lastSpace = Character.isWhitespace(c);
              }
          }
          return counter;
      }
    
  • 方法二:利用jdk8 stream的函數聲明式來簡化代碼,但是要實現輔助對象了
    private static class WordCounter {
          private final int counter;
          private final boolean lastSpace;
    
          public WordCounter(int counter, boolean lastSpace) {
              this.counter = counter;
              this.lastSpace = lastSpace;
          }
    
          // 如何改變WordCounter的屬性狀態
          public WordCounter accumulate(Character c) {
              if (Character.isWhitespace(c)) {
                  return lastSpace ? this : new WordCounter(counter, true);
              } else {
                  return lastSpace ? new WordCounter(counter + 1, false):this;
              }
          }
    
          // 調用此方法時,會把兩個子counter的部分結果進行匯總。
          // 其實就是 把內部計數器相加
          public WordCounter combine(WordCounter wordCounter) {
              return new WordCounter(counter + wordCounter.counter, wordCounter.lastSpace);
          }
    
          public int getCounter() {
              return counter;
          }
      }
    
    實現了輔助對象后,實現一個傳stream處理的方法,去調用
    public static int countWords2(Stream<Character> stream) {
          WordCounter wordCounter = stream.reduce(new WordCounter(0, true),WordCounter::accumulate, WordCounter::combine);
          return wordCounter.getCounter();
      }
    
    用以上這種方式,就只能實現串行的處理
    @Test
    public void test_2() {
         Stream<Character> stream = IntStream.range(0, SENTENCE.length())
                .mapToObj(SENTENCE::charAt);
        System.out.println("Found " + countWords2(stream) + " words");
    }
    
  • 方法三:使用jdk8中的可分迭代器,模擬jdk7的拆分/合并模式去實現并行迭代處理過程:
    public class WordCounterSpliterator implements Spliterator<Character> {
          private final String string;
          private int currentChar = 0;
      
          public WordCounterSpliterator(String string) {
              this.string = string;
          }
      
          /**
           * 把String中當前位置的char 傳給 Consumer,并讓其位置+1,
           * 作為參數傳遞的Consumer是一個java內部類,在遍歷流時將要處理的char傳給一系列要對其執行的函數。
           * 這里只有一個歸約函數,即 WordCounter 類的 accumulate方法。
           * 如果新的指針位置小于 String 的總長,且還有要遍歷的 Character ,則tryAdvance 返回 true 。
           */
          @Override
          public boolean tryAdvance(Consumer<? super Character> action) {
              action.accept(string.charAt(currentChar++));
              return currentChar < string.length();
          }
      
          @Override
          public Spliterator<Character> trySplit() {
              // 像 RecursiveTask 的 compute 方法一樣(分支/合并框架的使用方式)
      
              int currentSize = string.length() - currentChar;
      
              // 定義不再拆分的界限(不斷拆分,直到返回null)
              if (currentSize < 10) {
                  return null;
              }
              for (int splitPos = currentSize / 2 + currentChar;
                   splitPos < string.length(); splitPos++) {
                  if (Character.isWhitespace(string.charAt(splitPos))) {
                      // 類似RecursiveTask那樣,遞歸拆分
                      Spliterator<Character> spliterator =
                              new WordCounterSpliterator(string.substring(currentChar,
                                      splitPos));
                      currentChar = splitPos;
                      return spliterator;
                  }
              }
              return null;
          }
      
          @Override
          public long estimateSize() {
              return string.length() - currentChar;
          }
      
          @Override
          public int characteristics() {
              return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
          }
    }
    
    有了自定義的可分迭代器,我們就可以用并行的處理方式了:
    @Test
    public void test_3() {
          Spliterator<Character> spliterator = new WordCounterSpliterator(SENTENCE);
          Stream<Character> stream = StreamSupport.stream(spliterator, true);
          System.out.println("Found " + countWords2(stream.parallel()) + " words");
    }
    

CompletableFuture(Jdk 1.8)

  • 翻譯:可完備的Future
  • 簡單來說,就是寫法更靈活、code可讀性更好的Future。
  • 是除了并行流之外的另一種并行方式,只是使用場景不同。

(一)一個商店商品報價的例子

普通的方法去寫一個同步的計算報價的方法:

private static class Shop {
    private final String name;
    private final Random random;

    public Shop(String name) {
        this.name = name;
        random = new Random(name.charAt(0) * name.charAt(1) * name.charAt(2));
    }

    public double getPrice(String product) {
        return calculatePrice(product);
    }
    
    private double calculatePrice(String product) {
        delay(1000);
        return random.nextDouble()*product.charAt(0) + product.charAt(1);
    }
}
......
@Test
public void test_1() {
    Shop nike = new Shop("nike");
    Shop adidas = new Shop("adidas");
    System.out.println("nike Kobe1 price : "+ nike.getPrice("Kobe1"));
    System.out.println("nike Rose3 price : "+ adidas.getPrice("Rose3"));
}

我們先用CompletableFuture來讓計算變成異步。

  • 首先,用一個線程去執行對應邏輯,并且返回一個CompletableFuture實例:
    private static class Shop {
         ............
          public Future<Double> getPriceAsync(String product) {
              CompletableFuture<Double> futurePrice = new CompletableFuture<>();
              new Thread(() -> {
                  double price = calculatePrice(product);
                  futurePrice.complete(price);
              }).start();
              return futurePrice;
          }
          ............
    }
    
  • 好,然后我們在執行此方法時,就實現了異步。
    @Test
    public void test_2() throws ExecutionException, InterruptedException {
          Shop nike = new Shop("nike");
          Shop adidas = new Shop("adidas");
          Future<Double> price1 = nike.getPriceAsync("Kobe1");
          Future<Double> price2 = adidas.getPriceAsync("Kobe1");
          System.out.printf("nike Kobe1 price :%.2f%n",price1.get());
          System.out.printf("nike Kobe1 price :%.2f%n", price2.get());
    }
    

(二)Future的局限性

看到上面,我們發現,其實,和用Future去handle結果返回,好像差不多。

  • 我們可以用Future.get(timeout, TimeUnit) 來防止一直拿不到值而等待的情況。
  • 可以用Future.isDone()來判斷當前任務是否跑完,然后做不同的handle邏輯。
@Test
public void test_1() throws InterruptedException, ExecutionException {
    ExecutorService executor = Executors.newCachedThreadPool();
    Future<Double> future = executor.submit(this::doSthAsync);
    System.out.println("好,任務起來了,我去干別的先了");
    for (int i = 0; i < 2; i++) {
        System.out.println("主線程正在干活。。");
        Thread.sleep(500L);
    }
    try {
        System.out.println("異步任務返回了: " + future.get(2, TimeUnit.SECONDS));
    } catch (TimeoutException e) {
        System.out.println("異步任務出了異常!!!");
        e.printStackTrace();
    }
}

這里,就要說一下Future局限性了。

  1. 我們很難表述Future結果之間的依賴性。比如這樣一個案例:“當長時間計算任務完成時,請將該計算的結果通知到另一個長時間運行的計算任務,這兩個計算任務都完成后,將計算的結果與另一個查詢操作結果合并”。
  2. 以下場景,Future都難以表述:
    • 將兩個異步計算合并為一個——這兩個異步計算之間相互獨立,同時第二個又依賴于第一個的結果。
    • 等待Future集合中的所有任務都完成。
    • 僅等待Future集合中最快結束的任務完成(有可能因為它們試圖通過不同的方式計算同一個值),并返回它的結果。
    • 通過編程方式完成一個Future任務的執行(即以手工設定異步操作結果的方式)。
    • 應對Future的完成事件(即當Future的完成事件發生時會收到通知,并能使用Future計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果)。

(三)CompetableFuture與Future間的關系

CompetableFuture之于Future,相當于Stream之于Collection

(四)CompetableFuture的一些用法

  • CompetableFuture的一些靜態方法,直接簡化創建Thread的邏輯:

    public Future<Double> getPriceAsync(String product) {
          CompletableFuture<Double> futurePrice = new CompletableFuture<>();
          new Thread(() -> {
              double price = calculatePrice(product);
              futurePrice.complete(price);
          }).start();
          return futurePrice;
    }
    

    直接變成一句話

    public Future<Double> getPriceAsync(String product) {
        return CompletableFuture.supplyAsync(() -> calculatePrice(product));
    }
    

    supplyAsync方法接受一個生產者(Supplier)作為參數,返回一個CompletableFuture對象,該對象完成異步執行后會讀取調用生產者方法的返回值。生產者方法會交由ForkJoinPool池中的某個執行線程(Executor)運行,但是你也可以使用supplyAsync方法的重載版本,傳遞第二個參數指定不同的執行線程執行生產者方法。

  • 再來一個例子,之前是一家Shop做異步處理,這還不能發揮此接口的最大效用,所以,這次,來一打Shops,比較最佳售價。

    private final List<Shop> shops = Lists.newArrayList(new Shop("BestPrice"),
          new Shop("LetsSaveBig"),
          new Shop("MyFavoriteShop"),
          new Shop("BuyItAll"));
    

    一般情況下,用并行流和CompletableFuture的異步效果是半斤八兩的。

    /// 并行流的方式
    public List<String> findPricesParallel(String product) {
          return shops.parallelStream()
                  .map(shop -> String.format("%s 價格 %.2f", shop.getName() , shop.getPrice(product)))
                  .collect(toList());
    }
    
    // CompletableFuture的方式
    public List<String> findPricesFuture(String product) {
          List<CompletableFuture<String>> completableFutures = shops.stream()
                      .map(shop -> CompletableFuture.supplyAsync(() -> String.format("%s 價格 %.2f", shop.getName(), shop.getPrice(product))))
                      .collect(toList());
          return completableFutures
                  .stream()
                  .map(CompletableFuture::join)
                  .collect(toList());
    }
    

    默認CompletableFuture.supplyAsyn()內部使用的線程池和ParallelStream使用的是同一個線程池,是默認的固定線程數量的線程池,這個線程數由CPU、JVM配置等決定。

    具體線程數取決于Runtime.getRuntime().availableProcessors()的返回值

    但是,CompletableFuture是可以配置supplyAsyn()中使用的ThreadFactory的,而ParallelStream是不能的

    // 使用自定義的線程池
      private final Executor executor = Executors.newFixedThreadPool(100, new ThreadFactory() {
          @Override
          public Thread newThread(Runnable runnable) {
              Thread t = new Thread(runnable);
              t.setDaemon(true);
              return t;
          }
      });
    ...
    ...
    public List<String> findPricesFuture(String product) {
          return shops.stream()
                  .map(shop -> CompletableFuture.supplyAsync(() -> "" + shop.getPrice(), executor))
                  .collect(toList())
                  .stream()
                  .map(CompletableFuture::join)
                  .collect(toList());
      }
    

并發,用并行流還是CompletableFuture?

情況 推薦 原因
如果你進行的是計算密集型的操作,并且沒有I/O Stream 實現簡單
如果你并行的工作單元還涉及等待I/O的操作(包括網絡連接等待) CompletableFuture 高靈活性

新的日期和時間API

背景

  • Java 1.0
    • 特點:只有java.util.Date類
    • 缺點:這個類無法表示日期,只能以毫秒的精度表示時間。而且,易用性差,如:Date date = new Date(114, 2, 18);居然表示2014年3月18日。
  • Java 1.1
    • 更新:Date類中的很多方法被廢棄了,取而代之的是java.util.Calendar類
    • 缺點:Calendar類同樣很難用,比如:
      • 月份依舊是從0開始計算(不過,至少Calendar類拿掉了由1900年開始計算年份這一設計)
      • DateFormat方法也有它自己的問題,它不是線程安全的。

LocalDate和LocalTime

相關類:

  • java.time.LocalDate
  • java.time.LocalTime
  • java.time.LocalDateTime

三個類都實現了各種基本計算方法、parse方法、比較方法, 以及各種靜態方法。

LocalDate localDate = LocalDate.of(2018, 11, 25);
int year = localDate.getYear();// 2018
Month month = localDate.getMonth(); // 11
int day = localDate.getDayOfMonth(); // 25
DayOfWeek dow = localDate.getDayOfWeek(); // SUNDAY
int len = localDate.lengthOfMonth(); // 本月總天數: 30
boolean leap = localDate.isLeapYear(); // 是不是閏年: false

LocalDate localDate = LocalDate.now();
int year = localDate.get(ChronoField.YEAR);
int month = localDate.get(ChronoField.MONTH_OF_YEAR);
int day = localDate.get(ChronoField.DAY_OF_MONTH);

LocalTime localTime = LocalTime.now();
int hour = localTime.get(ChronoField.HOUR_OF_DAY);
int minute = localTime.get(ChronoField.MINUTE_OF_HOUR);
int second = localTime.get(ChronoField.SECOND_OF_MINUTE);

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime); // 2018-11-25T22:10:08.721
System.out.println(localDateTime.atZone(ZoneId.of("GMT"))); // 2018-11-25T22:11:08.778Z[GMT]
System.out.println(localDateTime.atOffset(ZoneOffset.UTC)); // 2018-11-25T22:11:44.362Z

機器的日期和時間格式

從計算機的角度來看,建模時間最自然的格式是表示一個持續時間段上某個點的單一大整型數。

  • 相關類:

    • java.time.Instant
  • 建模方式:以Unix元年時間(傳統的設定為UTC時區1970年1月1日午夜時分)開始所經歷的秒數進行計算。

  • 作用:適用于計算機做高精度運算。

  • 用法實例

    • Instant.ofEpochSecond(秒/long, 納秒/long)
      Instant.ofEpochSecond(3); // 1970-01-01T00:00:03Z
      Instant.ofEpochSecond(3, 0);
      Instant.ofEpochSecond(2, 1_000_000_000); // 2 秒之后再加上100萬納秒(1秒)
      Instant.ofEpochSecond(4, -1_000_000_000); // 4秒之前的100萬納秒(1秒)
      

Duration/Period

Duration

  • 作用:Duration類主要用于以秒和納秒衡量時間的長短
  • 注意:不要用機器時間相關API來計算Duration,你看不懂
    LocalTime time1 = LocalTime.of(21, 50, 10);
    LocalTime time2 = LocalTime.of(22, 50, 10);
    LocalDateTime dateTime1 = LocalDateTime.of(2018, 11, 17, 21, 50, 10);
    LocalDateTime dateTime2 = LocalDateTime.of(2018, 11, 17, 23, 50, 10);
    Instant instant1 = Instant.ofEpochSecond(1000 * 60 * 2);
    Instant instant2 = Instant.ofEpochSecond(1000 * 60 * 3);
    
    // 可用工廠方法定義
    Duration threeMinutes = Duration.ofMinutes(3);
    Duration fourMinutes = Duration.of(4, ChronoUnit.MINUTES);
    
    Duration d1 = Duration.between(time1, time2);
    Duration d2 = Duration.between(dateTime1, dateTime2);
    Duration d3 = Duration.between(instant1, instant2);
    // PT1H 相差1小時
    System.out.println("d1:" + d1);
    // PT2H 相差2小時
    System.out.println("d2:" + d2);
    // PT16H40M 相差16小時40分鐘
    System.out.println("d3:" + d3);
    
    而且,不要試圖在這兩類對象之間創建duration,會觸發一個DateTimeException異常。而且,不要放一個LocalDate對象作為參數,不合適。

Period

  • 作用:以年、月或者日的方式對多個時間單位建模
  • 用法:
    // 可用工廠方法定義
    Period tenDay = Period.ofDays(10);
    Period threeWeeks = Period.ofWeeks(3);
    Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
    
    Period period = Period.between(LocalDate.of(2018, 11, 7), LocalDate.of(2018, 11, 17));
    System.out.println("Period between:" + period); // P10D 相差10天
    

修改、構造時間

簡單來說,API給我們劃分了讀取和修改兩類方法:

  • 讀:get
  • 修改:with

下面這些方法都會生成一個新的時間對象,不會修改源對象:

// 2018-11-17
LocalDate date1 = LocalDate.of(2018, 11, 17);
// 2019-11-17
LocalDate date2 = date1.withYear(2019);
// 2019-11-25
LocalDate date3 = date2.withDayOfMonth(25);
// 2019-09-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);

特別:用TemporalAdjuster實現復雜操作

利用重寫各種withXX方法,并自定義TemporalAdjuster參數,就能實現復雜的時間操作:

// 2018-11-17
LocalDate date1 = LocalDate.of(2018, 11, 17);
// 2018-11-19
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
// 2018-11-30
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());

我們看看這個用TemporalAdjuster接口,其實要自定義很簡單,因為它只有一個接口:

@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

Format

  • 相關類:
    • java.time.format.DateTimeFormatter
    • java.time.format.DateTimeFormatterBuilder:用于實現更加復雜的格式化
  • 特點:
    • 所有的DateTimeFormatter實例都是線程安全的。(所以,你能夠以單例模式創建格式器實例,就像DateTimeFormatter所定義的那些常量,并能在多個線程間共享這些實例。)
// Date 轉 String
LocalDate date1 = LocalDate.of(2018, 11, 17);
String s1 = date1.format(DateTimeFormatter.BASIC_ISO_DATE); // 20181117
String s2 = date1.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2018-11-17
// String 轉 Date
LocalDate date2 = LocalDate.parse("20181117", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date3 = LocalDate.parse("2018-11-17", DateTimeFormatter.ISO_LOCAL_DATE);

DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date5 = LocalDate.of(2018, 11, 16);
// 16. novembre 2018
String formattedDate2 = date5.format(italianFormatter);
// 2018-11-16
LocalDate date6 = LocalDate.parse(formattedDate2, italianFormatter);

DateTimeFormatterBuilder實現細粒度格式化控制:

DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
                .appendText(ChronoField.DAY_OF_MONTH)
                .appendLiteral(". ")
                .appendText(ChronoField.MONTH_OF_YEAR)
                .appendLiteral(" ")
                .appendText(ChronoField.YEAR)
                .parseCaseInsensitive()
                .toFormatter(Locale.ITALIAN);

LocalDate now = LocalDate.now();
// 17. novembre 2018
String s1 = now.format(italianFormatter);

處理不同的時區和歷法

  • 相關類:
    • java.time.ZoneId
    • java.time.ZoneOffset
  • 特點:
    • 新的java.time.ZoneId類是老版java.util.TimeZone的替代品。
    • 更容易處理日光時(Daylight Saving Time,DST)這種問題。
 // 地區ID都為“{區域}/{城市}”的格式
ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");

LocalDate date = LocalDate.of(2018, 11, 17);
ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone);

LocalDateTime dateTime = LocalDateTime.of(2018, 11, 27, 18, 13, 15);
ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone);

Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(shanghaiZone);


// LocalDateTime 轉 Instant
LocalDateTime dateTime2 = LocalDateTime.of(2018, 11, 17, 18, 45);
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
Instant instantFromDateTime = dateTime2.toInstant(newYorkOffset);

// 通過反向的方式得到LocalDateTime對象
Instant instant2 = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant2, shanghaiZone);

// OffsetDateTime,它使用ISO-8601的歷法系統,以相對于UTC/格林尼治時間的偏差方式表示日期時間。
LocalDateTime dateTime3 = LocalDateTime.of(2018, 11, 17, 18, 45);
OffsetDateTime offsetDateTime = OffsetDateTime.of(dateTime3, newYorkOffset);
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容