《Java 8实战》学习笔记 -持续更新

《Java 8实战》学习笔记

** 第一部分 **

第三章、Lambda表达式

1、基础概念

 

image-20200925101009761
  • Before
Comparator<Apple> byWeight = new Comparator<Apple>() {
     public int compare(Apple a1, Apple a2){
     return a1.getWeight().compareTo(a2.getWeight());
     }
};
  • After
Comparator<Apple> byWeight =
 					(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • java中有效的Lambda表达式
//第一个Lambda表达式具有一个String类型的参数并返回一个int。Lambda没有return语句,因为已经隐含了return
(String s) -> s.length()
//第二个Lambda 表达式有一个 Apple 类型的 参数并返回一 个boolean(苹 果的重量是否 超过150克)
(Apple a) -> a.getWeight() > 150
//第三个Lambda表达式具有两个int类型的参数而没有返回值(void返回)。注意Lambda表达式可以包含多行语句,这里是两行
(int x, int y) -> {
 System.out.println("Result:");
 System.out.println(x+y);
}
//第四个Lambda表达式没有参数,返回一个int
() -> 42
//第五个Lambda表达式具有两个Apple类型的参数,返回一个int:比较两个Apple的重量
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
    
    
() -> {}
() -> "Raoul"
() -> {return "Mario";}
  • 使用案例
//布尔表达式 
(List<String> list) -> list.isEmpty()
//创建对象 
() -> new Apple(10)
//消费一个对象 
(Apple a) -> {
 	System.out.println(a.getWeight());
}
//从一个对象中选择/抽取 
(String s) -> s.length()
//组合两个值 
(int a, int b) -> a * b
//比较两个对象 
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

2、函数式接口

  • 一言以蔽之,函数式接口就是只定义一个抽象方法的接口。
  • 比如

 

image-20200925102310159
  • @FunctionalInterface注解是标注这个类是函数式接口类,通常只做标识提醒用,无任何其他实质性的作用,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override 标注表示方法被重写了。

  • 为了总结关于函数式接口和Lambda的讨论,表总结了一些使用案例、Lambda的例子,以 及可以使用的函数式接口。

 

image-20200925142452842

** 第二部分 **

第四章 引入流(stream)

1、基础概念

 

image-20200925144337955

1)流和集合

  • 流是按需加载的,就像流媒体视频一样,是按需下载播放的
  • 集合是急切创建的,所有都放在内存

 

image-20200925150035836

2)流只能遍历一次

  • 遍历完之后,我们就说这个流已经被消费掉了。
  • 例如,以下代码会抛出一个异常,说流已被消 费掉了:

 

image-20200925150243946

集合和流的另一个关键区别在于它们遍历数据的方式

3)外部迭代和内部迭代

  • 外部迭代(用for-each)

 

image-20200925151322993
  • 流:内部迭代

 

image-20200925151350666
List<String> names = menu.stream()
                         .map(Dish::getName)
                         .collect(toList());
  • 帮助理解区别

  • 以下是for-each迭代

    • 你:“索菲亚,我们把玩具收起来吧。地上还有玩具吗?”
      索菲亚:“有,球。”
      你:“好,把球放进盒子里。还有吗?”

      索菲亚:“有,那是我的娃娃。”
      你:“好,把娃娃放进盒子里。还有吗?”
      索菲亚:“有,有我的书。”
      你:“好,把书放进盒子里。还有吗?”
      索菲亚:“没了,没有了。”
      你:“好,我们收好啦。”

  • 以下是流迭代

    • 你:“索菲亚,我们把玩具收起来吧。把你身边的玩具都给我”
      索菲亚:“好的。”
      你:“好,我们收好啦。”

4)流操作

 

image-20200925152119327
  • filter、map和limit可以连成一条流水线;
  • collect触发流水线执行并关闭它。关闭流的操作称为终端操作。

 

image-20200925152202955

第五章 使用流

1、筛选与切片

1)谓词筛选-filter

  • Streams接口支持filter方法(你现在应该很熟悉了)。该操作会接受一个谓词(一个返回 boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

  • 例如

List<Dish> vegetarianMenu = menu.stream()
							 .filter(Dish::isVegetarian)
 							 .collect(toList());

2)筛选各异的元素(其实就是去重)-distinct

  • 例如-筛选出列表中所有的偶数,并确保没有 重复
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
         .filter(i -> i % 2 == 0)
         .distinct()
         .forEach(System.out::println);

3)截短流(其实就类似于mysql的分页)-limit

  • 例如,选出了符合谓词的头三个元素, 然后就立即返回了结果。
List<Dish> dishes = menu.stream()
                         .filter(d -> d.getCalories() > 300)
                         .limit(3)
                         .collect(toList());

4)跳过元素-skip- 和limit互补

  • skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一 个空流。请注意,limit(n)和skip(n)是互补的!
  • 例如,下面的代码将跳过超过300卡路里的头 两道菜,并返回剩下的
List<Dish> dishes = menu.stream()
                         .filter(d -> d.getCalories() > 300)
                         .skip(2)
                         .collect(toList());

2、映射

1)流的扁平化-flatMap

  • 使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所 有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流
List<String> uniqueCharacters =
     words.stream()
             .map(w -> w.split(""))
             .flatMap(Arrays::stream)
             .distinct()
             .collect(Collectors.toList());
  • 和map对比图

 

image-20200929190754854
  • flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接 起来成为一个流。

3、查找和匹配

1)anyMatch-流中是否有一个元素能匹配给定的谓词

if(menu.stream().anyMatch(Dish::isVegetarian)){
	 System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

2)allMatch-看流中的元素是否都能匹配给定的谓词

3)noneMatch-和allMatch相对,即都不能匹配

4)findAny-返回当前流中的任意元素

  • 它可以与其他流操作结合使用。
  • 可以结合使用filter和findAny方法来实现查询
Optional<Dish> dish =
                     menu.stream()
                     .filter(Dish::isVegetarian)
                     .findAny();
  • Optional返回值

 isPresent()将在Optional包含值的时候返回true, 否则返回false。

 ifPresent(Consumer block)会在值存在的时候执行给定的代码块。它让你传递一个接收T类型参数,并返回void的Lambda 表达式。

 T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。

 T orElse(T other)会在值存在时返回值,否则返回一个默认值。

 

image-20201010094604740

5)findFirst-查找第一个元素

  • 给定一个数字列表,下面的代码能找出第一个平方 能被3整除的数:
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
            someNumbers.stream()
                        .map(x -> x * x)
                        .filter(x -> x % 3 == 0)
                        .findFirst(); // 9

4、归约-reduce

1)元素求和

  • Java7 写法
int sum = 0;
for (int x : numbers) {
 sum += x;
}
  • java8写法
private static void useReduce() {
        logger.info("[useReduce]用法");
        List<Integer> nums = Arrays.asList(1,2,3,6);
        //int sum = nums.parallelStream().reduce(0,(a,b) -> a + b);
  			int sum = nums.parallelStream().reduce(0,Integer::sum);
        int product = nums.parallelStream().reduce(1,(a,b) -> a * b);
        System.out.println(sum);
        System.out.println(product);
    }

 

image-20201208095548744

2) 最大值和最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max); 
Optional<Integer> min = numbers.stream().reduce(Integer::min);

 

image-20201208100049593

3) 算出集合中有多少元素

Integer count = nums.parallelStream()
                .map(d -> 1)
                .reduce(0, Integer::sum);
        System.out.println(count);
  • 可以把流中每个元素都映射成数字1,然后用reduce求和

4) 总结

 

image-20201208100703665

5、付诸实践

  • 练习

(1) 找出2011年发生的所有交易,并按交易额排序(从低到高)。

(2) 交易员都在哪些不同的城市工作过?

(3) 查找所有来自于剑桥的交易员,并按姓名排序。

(4) 返回所有交易员的姓名字符串,按字母顺序排序。

(5) 有没有交易员是在米兰工作的?

(6) 打印生活在剑桥的交易员的所有交易额。

(7) 所有交易中,最高的交易额是多少? (8) 找到交易额最小的交易。

  • Traders和Transactions列表
Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario","Milan");
Trader alan = new Trader("Alan","Cambridge");
Trader brian = new Trader("Brian","Cambridge");
List<Transaction> transactions = Arrays.asList(
 new Transaction(brian, 2011, 300),
 new Transaction(raoul, 2012, 1000),
 new Transaction(raoul, 2011, 400),
 new Transaction(mario, 2012, 710),
 new Transaction(mario, 2012, 700),
 new Transaction(alan, 2012, 950)
);
  • Trader和Transaction类
public class Trader{
   private final String name;
   private final String city;
   public Trader(String n, String c){
       this.name = n;
       this.city = c;
   }
   public String getName(){
   		return this.name;
   }
   public String getCity(){
   		return this.city;
   } 
    public String toString(){
   		return "Trader:"+this.name + " in " + this.city;
   }
}
public class Transaction{
 private final Trader trader;
 private final int year;
 private final int value;
 public Transaction(Trader trader, int year, int value){
     this.trader = trader;
     this.year = year;
     this.value = value;
 }
 public Trader getTrader(){
 		return this.trader;
 }
 public int getYear(){
 		return this.year;
 }
 public int getValue(){
 		return this.value;
 }
 public String toString(){
     return "{" + this.trader + ", " +
     "year: "+this.year+", " +
     "value:" + this.value +"}";
     }
}

 

image-20201209190132006

 

image-20201209190159500

 

image-20201209190222480

 

image-20201209190302596

 

image-20201209190314608

 

image-20201209190326078

 

image-20201209190344549

 

image-20201209190355697

 

image-20201209190411445

6、数值流

6.1 原始类型流特化

1、映射到数值流

 

image-20201209191337650

这里,mapToInt会从每道菜中提取热量(用一个Integer表示),并返回一个IntStream (而不是一个Stream)。然后你就可以调用IntStream接口中定义的sum方法,对卡 路里求和了!请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如 max、min、average等。

2、转换回对象流

一旦有了数值流,你可能会想把它转换回非特化流。例如,IntStream上的操作只能 产生原始整数: IntStream 的 map 操作接受的 Lambda 必须接受 int 并返回 int (一个 IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish。为此,你需要访问Stream 接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int都会装箱成一个 Integer),可以使用boxed方法

 

image-20201209191651427

3、默认值OptionalInt

求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算IntStream中的最 大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢? Optional可以用 Integer、String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类 型特化版本:OptionalInt、OptionalDouble和OptionalLong。

  • 要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = menu.stream()
 .mapToInt(Dish::getCalories)
 .max(); 
//如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:
int max = maxCalories.orElse(1);

6.2 数值范围

假设你想要生成1和100之间的所有数字。Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围: range和rangeClosed。

 

image-20201209192254220

6.3 数值流应用:勾股数

Stream<int[]> pythagoreanTriples =
                 IntStream.rangeClosed(1, 100).boxed()
                           .flatMap(a ->
                                    IntStream.rangeClosed(a, 100)
                                   					 .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
                                    					.mapToObj(b ->
                                              				new int[]{a, b, (int)Math.sqrt(a * a + b * b)})
       																);

pythagoreanTriples.limit(5)
                  .forEach(t ->
                 					System.out.println(t[0] + ", " + t[1] + ", " + t[2]));

7、构建流

7.1 由值创建流-Stream.of/empty

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println); 

Stream<String> emptyStream = Stream.empty();

7.2 由数组创建流-Arrays.stream

int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
//这里stream里面必须放int[]才能调出sum()方法

7.3 由文件生成流java.nio.file.Files

  • 看一个文件中有多少各不相同的词
long uniqueWords = 0;
        try (Stream<String> lines =
                     Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
            uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                    .distinct()
                    .count();
        } catch (IOException e) {

        }

你可以使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行。然后,你 可以对line调用split方法将行拆分成单词。应该注意的是,你该如何使用flatMap产生一个扁 平的单词流,而不是给每一行生成一个单词流。最后,把distinct和count方法链接起来,数 数流中有多少各不相同的单词。

7.4 由函数生成流:创建无限流

1、迭代-Stream.iterate

Stream.iterate(0, n -> n + 2)
 .limit(10)
 .forEach(System.out::println);
  • 一般来说,在需要依次生成一系列值的时候应该使用iterate,比如一系列日期:1月31日, 2月1日,依此类推。

  • 测验5.4:斐波纳契元组序列

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55…

Stream.iterate(new int[]{0, 1},
                t -> new int[]{t[1],t[0] + t[1]})
                .limit(10)
                .map(t -> t[0])
                .forEach(System.out::println);

2、生成-Stream.generate

Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);

第六章 用流收集数据

1、 toList()

  • 用法
List<Transaction> transactions =
 				transactionStream.collect(Collectors.toList());

2、归约和汇总

1)count/counting

long howManyDishes = menu.stream().collect(Collectors.counting());
//这还可以写得更为直接:
long howManyDishes = menu.stream().count();

2)找流中的最大值和最小值 --maxBy

  • Collectors.maxBy
Comparator<Dish> dishCaloriesComparator =
 					Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
                 menu.stream()
                 			.collect(maxBy(dishCaloriesComparator));

3)汇总/平均数Collectors.summingInt等

  • Collectors.summingInt 求出菜单列表的总热量:
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

Collectors.summingLong和Collectors.summingDouble方法的作用完全一样,可以用 于求和字段为long或double的情况。

  • Collectors.averagingInt,连同对应的averagingLong和averagingDouble可以计算数值的平均数
double avgCalories =
 			menu.stream().collect(averagingInt(Dish::getCalories));
  • 用summarizingInt工厂 方法返回的收集器 , 通过一次summarizing操作你可以就数出菜单中元素的个数,并得 到菜肴热量总和、平均值、最大值和最小值:
IntSummaryStatistics menuStatistics =
			 menu.stream().collect(summarizingInt(Dish::getCalories));
  • 结果

IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}

4) 连接字符串 -- joining

String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
  • 结果

pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon

3、分组-- Collectors.groupingBy

Map<Dish.Type, List<Dish>> dishesByType =
 			menu.stream().collect(groupingBy(Dish::getType));
  • 结果

{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}

  • 复杂一点的
public enum CaloricLevel { DIET, NORMAL, FAT }
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
         Collectors.groupingBy(dish -> {
         	if (dish.getCalories() <= 400) return CaloricLevel.DIET;
         	else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
         	else return CaloricLevel.FAT;
 		} ));

1)多级分组

  • 要进 行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流 中项目分类的二级标准
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
            menu.stream().collect(
             Collectors.groupingBy(Dish::getType,
                     Collectors.groupingBy(dish -> {
                     if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                     else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; 
                     else return CaloricLevel.FAT;
 									} )
 						)
				);
  • 结果

{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]}, FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}

一般来说,把groupingBy看作“桶”比较容易明白。第一个groupingBy给每个键建立了 一个桶。然后再用下游的收集器去收集每个桶中的元素,以此得到n级分组。

 

1607848004762

4、 分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函 数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以 分为两组——true是一组,false是一组。

Map<Boolean, List<Dish>> partitionedMenu =
				 menu.stream().collect(partitioningBy(Dish::isVegetarian));
List<Dish> vegetarianDishes =
 				menu.stream().filter(Dish::isVegetarian).collect(toList());

{false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}

1) 分区的优势

  • partitioningBy 工厂方法有一个重载版本,可以像下面这样传递第二个收集器:
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
                  menu.stream().collect(
                                    partitioningBy(Dish::isVegetarian,
                                                   groupingBy(Dish::getType)));

{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, true={OTHER=[french fries, rice, season fruit, pizza]}}

  • 找到素食和非素 食中热量最高的菜:
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
        menu.stream().collect(
                     partitioningBy(Dish::isVegetarian,
                     collectingAndThen(
                               maxBy(comparingInt(Dish::getCalories)),
                               Optional::get)));

{false=pork, true=pizza}

*** Collectors类静态工厂方法的表格 ***

 

image-20201217094750459

 

image-20201217094854828

5、收集器接口

1) 自己的ToListCollector方法

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }
    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.indentity();
    }
    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(
                IDENTITY_FINISH, CONCURRENT));
    }
}

第7章 并行数据处理与性能

1、将顺序流转换为并行流

1)parallel方法:

public static long parallelSum(long n) {
         return Stream.iterate(1L, i -> i + 1)
                 .limit(n)
                 .parallel()
                 .reduce(0L, Long::sum);
}
  • 转换成并行流之后的图解

 

image-20201219144739822

2) 当然也可以转回来-sequential方法

stream.parallel()
     .filter(...)
     .sequential()
     .map(...)
     .parallel()
     .reduce();

并行流内部使用了默认的ForkJoinPool(7.2节会进一步讲到分支/合并框架),它默认的 线程数量就是你的处理器数量,这个值是由 Runtime.getRuntime().availableProcessors()得到的。 但是你可以通过系统属性 java.util.concurrent.ForkJoinPool.common. parallelism来改变线程池大小,如下所示: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12"); 这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个 并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值。

* 并行处理的性能-流的数据源和可分解性

按照可分解性总结了一些流数据源适不适于并行。

 

image-20201219150640347
  • 调并行流背后使用的基础架构是Java 7中引入的分支/合并框架。

2、分支/合并框架-Java7的框架

> 分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任 务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把子任务分配给 线程池(称为ForkJoinPool)中的工作线程。

1) 使用 RecursiveTask

要把任务提交到这个池,必须创建RecursiveTask的一个子类,其中R是并行化任务(以 及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当 然它可能会更新其他非局部机构)。

  • 要定义RecursiveTask,只需实现它唯一的抽象方法 compute:
protected abstract R compute();

上面的方法实现大概如下:

if (任务足够小或不可分) {
 顺序计算该任务
} else {
 将任务分成两个子任务
 递归调用本方法,拆分每个子任务,等待所有子任务完成
 合并每个子任务的结果
}
  • Java7的框架暂不做记录,本篇将专注Java8

3、Spliterator接口

Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable iterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行 而设计的。

Java 8已经为集合框架中包含的所有数据结构提供了一个 默认的Spliterator实现。集合实现了Spliterator接口,接口提供了一个spliterator方法。 这个接口定义了若干方法,如下面的代码清单所示。

  • Spliterator接口
public interface Spliterator<T> {
     boolean tryAdvance(Consumer<? super T> action);
     Spliterator<T> trySplit();
     long estimateSize();
     int characteristics();
}

1) 拆分过程

将Stream拆分成多个部分的算法是一个递归过程,第一步是对第一个 Spliterator调用trySplit,生成第二个Spliterator。第二步对这两个Spliterator调用 trysplit,这样总共就有了四个Spliterator。这个框架不断对Spliterator调用trySplit 直到它返回null,表明它处理的数据结构不能再分割,如第三步所示。最后,这个递归拆分过 程到第四步就终止了,这时所有的Spliterator在调用trySplit时都返回了null。

 

image-20201219152149118

** 第三部分 **

第八章 高效Java8编程

1、为改善可读性和灵活性重构代码

1) 改善代码的可读性

通常的理解是,“别人理解这段代码的难易程度”

利用Lambda表达式、方法引用以及Stream改善程序代码的 可读性:

  • 重构代码,用Lambda表达式取代匿名类

  • 用方法引用重构Lambda表达式

  • 用Stream API重构命令式的数据处理

2) 从匿名类到 Lambda 表达式的转换

i、 Runnable

  • 创建 Runnable对象的匿名类

这段代码及其对应的Lambda表达式实现如下:

//传统方法
Runnable r1 = new Runnable(){
 public void run(){
 	System.out.println("Hello");
 }
};

//Java8
Runnable r2 = () -> System.out.println("Hello");
  • 请注意,Lambda形式的匿名类里面不可使用与外部同名的局部变量,否则会报错

如下:

int a = 10;
Runnable r1 = () -> {
     int a = 2;
     System.out.println(a);
};

3) 从 Lambda 表达式到方法引用的转换

Lambda表达式非常适用于需要传递代码片段的场景。不过,为了改善代码的可读性,也请 尽量使用方法引用。因为方法名往往能更直观地表达代码的意图。

i、提取方法

  • 比如按照食物的热量级别对菜肴进行分类:groupingBy
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
                 menu.stream()
                 .collect(
                 groupingBy(dish -> {
                             if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                         else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                         else return CaloricLevel.FAT;
 }));
  • 可以将if提取出来作为方法,这样调用更为直观,比如:
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
 menu.stream().collect(groupingBy(Dish::getCaloricLevel));
public class Dish{
     …
     public CaloricLevel getCaloricLevel(){
         if (this.getCalories() <= 400) return CaloricLevel.DIET;
         else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
         else return CaloricLevel.FAT;
     }
}

ii、静态辅助方法,比如comparing、maxBy。

比如

  • 原来代码
inventory.sort(
 	(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
  • 可以写成
inventory.sort(Comparator.comparing(Apple::getWeight));

再比如,

很多通用的归约操作,比如sum、maximum,都有内建的辅助方法可以和方法引用结 合使用。比如,在我们的示例代码中,使用Collectors接口可以轻松得到和或者最大值

  • 原来代码
int totalCalories =
     menu.stream().map(Dish::getCalories)
     .reduce(0, (c1, c2) -> c1 + c2);
  • 可以写成
int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));

4) 从命令式的数据处理切换到 Stream

比如

  • 原来代码
List<String> dishNames = new ArrayList<>();
    for(Dish dish: menu){
         if(dish.getCalories() > 300){
         dishNames.add(dish.getName());
     }
}
  • 可以改成
menu.parallelStream()
     .filter(d -> d.getCalories() > 300)
     .map(Dish::getName)
     .collect(toList());

2、使用 Lambda 重构面向对象的设计模式

对设计经验的归纳总结被称为设计模式①。设计软件时,如果你愿意,可以复用这些方式方 法来解决一些常见问题。这看起来像传统建筑工程师的工作方式,对典型的场景(比如悬挂桥、 拱桥等)都定义有可重用的解决方案。

  • 例如,访问者模式常用于分离程序的算法和它的操作对象。
  • 单例模式一般用于限制类的实例化,仅生成一份对象。

1) 策略模式

  • 策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。

策略模式包含三部分内容:

  • 一个代表某个算法的接口(它是策略模式的接口)。
  • 一个或多个该接口的具体实现,它们代表了算法的多种实现(比如,实体类ConcreteStrategyA或者ConcreteStrategyB)。
  • 一个或多个使用策略对象的客户。

 

image-20201219162409419

假设希望验证输入的内容是否根据标准进行了恰当的格式化(比如只包含小写字母或 数字)。

  • 你可以从定义一个验证文本(以String的形式表示)的接口入手:
public interface ValidationStrategy {
 	boolean execute(String s);
}
  • 其次,定义该接口的一个或多个具体实现
public class IsAllLowerCase implements ValidationStrategy {
     public boolean execute(String s){
        return s.matches("[a-z]+");
     }
}
public class IsNumeric implements ValidationStrategy {
     public boolean execute(String s){
        return s.matches("\\d+");
     }
}
  • 验证策略
public class Validator{
     private final ValidationStrategy strategy;
    
     public Validator(ValidationStrategy v){
     	this.strategy = v; 
     }
     public boolean validate(String s){
        return strategy.execute(s);
     }
}
//使用
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator(new IsAllLowerCase ());
boolean b2 = lowerCaseValidator.validate("bbbb");

其实ValidationStrategy已经是一个函数接口,与Predicate<String>具有同样的函数描述

  • 所以可以将上面的代码修改成:
Validator numericValidator =
		 new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator =
 		new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");
  • 这个地方,其实就是省掉了impl接口实现方法了,如果用实现方法的话,万一需要大量的策略就得创建大量的实现方法,这其实也是策略模式的弊端,这时候直接用Lambda表达式来代替实现方法,Lambda表达式避免了采用策略设计模式时僵化的模板代码。

2) 模板方法

模板方法模式在你“希望使用这个算法,但是需要对其中的某些行进行改进,才能达到希望的效果” 时是非常有用的。

​ 假设需要编写一个简单的在线银行 应用。通常,用户需要输入一个用户账户,之后应用才能从银行的数据库中得到用户的详细信息, 最终完成一些让用户满意的操作。不同分行的在线银行应用让客户满意的方式可能还略有不同, 比如给客户的账户发放红利,或者仅仅是少发送一些推广文件。你可能通过下面的抽象类方式来 实现在线银行应用:

abstract class OnlineBanking {
     public void processCustomer(int id){
         Customer c = Database.getCustomerWithId(id);
         makeCustomerHappy(c);
     } 
    abstract void makeCustomerHappy(Customer c);
}
  • Lambda表达式方式

向processCustomer方法引入了第二个参数,它是一个Consumer类型的参数,与前文定义的makeCustomerHappy的特征保持一致:

public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
     Customer c = Database.getCustomerWithId(id);
     makeCustomerHappy.accept(c);
}
  • 现在可以通过传递Lambda表达式,直接插入不同的行为,不再需要继承 OnlineBanking类了:
new OnlineBankingLambda().processCustomer(1337, (Customer c) ->
									 System.out.println("Hello " + c.getName());

3) 观察者模式

观察者模式是一种比较常见的方案,某些事件发生时(比如状态转变),如果一个对象(通 常我们称之为主题)需要自动地通知其他多个对象(称为观察者),就会采用该方案。

 

image-20201220153238266

好几家报纸机构,比如《纽约时报》《卫报》以及《世 界报》都订阅了新闻,他们希望当接收的新闻中包含他们感兴趣的关键字时,能得到特别通知。

下面是原始的方式实现新闻通知,根据新闻的内容通知不同的观察者(新闻机构)

  • 首先定义一个观察者接口
interface Observer {
	 void notify(String tweet);
}
  • 再定义不同的观察者实现上面的接口
class NYTimes implements Observer{
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("money")){
            System.out.println("Breaking news in NY! " + tweet);
        }
    }
}
class Guardian implements Observer {
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("queen")){
            System.out.println("Yet another news in London... " + tweet);
        }
    }
}
class LeMonde implements Observer{
    public void notify(String tweet) {
        if(tweet != null && tweet.contains("wine")){
            System.out.println("Today cheese, wine and news! " + tweet);
        }
    }
}
  • 然后再定义一个接口主题接口,用来提供注册观察者和实现通知
interface Subject{
     void registerObserver(Observer o);
     void notifyObservers(String tweet);
}
  • 实现上面的接口
class Feed implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    public void registerObserver(Observer o) {
        this.observers.add(o);
    }
    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}
  • 最后就可以进行注册并通知
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Java 8 in Action!");

这个时候Guardian会收到消息,即打印内容
以上是观察者模式的一个完整的步骤,但是应该也能发现,依然需要定义很多的方法去实现观察者接口,所以可以用Lambda表达式来简化他们。

  • 比如
f.registerObserver((String tweet) -> {
    if(tweet != null && tweet.contains("money")){
        System.out.println("Breaking news in NY! " + tweet);
    }
});
f.registerObserver((String tweet) -> {
    if(tweet != null && tweet.contains("queen")){
        System.out.println("Yet another news in London... " + tweet);
    }
});
  • 当然,以上不的意思并不是希望让所有的观察者模式都简化成Lambda表达式这样,因为当观察者的逻辑有可能十分复杂时,它们可能还持有状态,抑或定义了多个方法,在这些情形下,还是应该继续使用类的方式。

4) 责任链模式

责任链模式是一种创建处理对象序列(比如操作序列)的通用方案。一个处理对象可能需要 在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处 理对象,以此类推。

 

image-20201220153255220

代码示例

  • 先定义一个抽象类,来控制
public abstract class ProcessingObject<T> {
    protected ProcessingObject<T> successor;
    public void setSuccessor(ProcessingObject<T> successor){
        this.successor = successor;
    }
    public T handle(T input){
        T r = handleWork(input);
        if(successor != null){
            return successor.handle(r);
        }
        return r;
    }
    abstract protected T handleWork(T input);
}

handle方法提供了如何进行 工作处理的框架。不同的处理对象可以通过继承ProcessingObject类,提供handleWork方法 来进行创建。

  • 创建两个处理对象,它们的功能是进行一些文 本处理工作。
public class HeaderTextProcessing extends ProcessingObject<String> {
    public String handleWork(String text){
        return "From Raoul, Mario and Alan: " + text;
    }
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
    public String handleWork(String text){
        return text.replaceAll("labda", "lambda");
    }
}
  • 将这两个处理对象结合起来,构造一个操作序列
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle("Aren't labdas really sexy?!!");
System.out.println(result);

打印结果为:打印输出“From Raoul, Mario and Alan: Aren't lambdas really sexy?!!”

请注意labdas已经变化为lambdas

  • 接下来使用Lambda表达式--andthen方法
UnaryOperator<String> headerProcessing =
                (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing =
    (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline =
    headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!");

5) 工厂模式

使用工厂模式,你无需向客户暴露实例化的逻辑就能完成对象的创建。

比如,我们假定你为 一家银行工作,他们需要一种方式创建不同的金融产品:贷款、期权、股票,等等。 通常,你会创建一个工厂类,它包含一个负责实现不同对象的方法,如下所示:

public 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);
        }
    }
} 

Product p = ProductFactory.createProduct("loan");
  • 使用Lambda表达式

引用贷款 (Loan)构造函数的示例:

Supplier<Product> loanSupplier = Loan::new;
Loan loan = loanSupplier.get();

所以通过这种方式,就可以将最上面的代码重构,创建一个Map,将产品名映射到对应的构造函数:

final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
    map.put("loan", Loan::new);
    map.put("stock", Stock::new);
    map.put("bond", Bond::new);
}

像之前使用工厂设计模式那样,利用这个Map来实例化不同的产品:

public static Product createProduct(String name){
    Supplier<Product> p = map.get(name);
    if(p != null) return p.get();
    throw new IllegalArgumentException("No such product " + name);
}
  • 当然如果工厂方法createProduct需要接收多个传递给产品构造方法的参数,这种方式的扩展性不是很 好。

为了完成这个任务,你需要创建一个特殊的函数接口TriFunction。最终 的结果是Map变得更加复杂。

public interface TriFunction<T, U, V, R>{
    R apply(T t, U u, V v);
}
Map<String, TriFunction<Integer, Integer, String, Product>> map
    = new HashMap<>();

3、调试Lambda 表达式--peek

peek的设计初衷就是在流的每个元素恢复运行之 前,插入执行一个动作。但是它不像forEach那样恢复整个流的运行,而是在一个元素上完成操 作之后,它只会将操作顺承到流水线中的下一个操作。

  • 使用peek输出了Stream流水线操作之前和操作之后的中间值:
List<Integer> result =
    numbers.stream()
    .peek(x -> System.out.println("from stream: " + x))
    .map(x -> x + 17)
    .peek(x -> System.out.println("after map: " + x))
    .filter(x -> x % 2 == 0)
    .peek(x -> System.out.println("after filter: " + x))
    .limit(3)
    .peek(x -> System.out.println("after limit: " + x))
    .collect(toList());

 

image-20201220163653038

第十章 用Optional取代null

2、Optional 类入门

防止出现NullPointerException

  • 原始代码-有个人,人有车,车有保险
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; }
}

调用

public String getCarInsuranceName(Person person) {
 	return person.getCar().getInsurance().getName();
}

这个时候,如果一个人没有车呢,那么在get车保险的时候指定会报出NullPointerException。

很多时候可能会这么做:

public String getCarInsuranceName(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}

但真的不够友好

  • 所以需要改动,就可以用Optional 了
public class Person {
 private Optional<Car> car;
 public Optional<Car> getCar() { return car; }
}
public class Car {
 private Optional<Insurance> insurance;
 public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
 private String name;
 public String getName() { return name; }
}

3、应用 Optional 的几种模式

1) 创建Optional 对象

就以上改动后的代码,进行操作

a.声明一个空的Optional- empty()

Optional<Car> optCar = Optional.empty();

b.依据一个非空值创建Optional- of()

Car car = new Car();
Optional<Car> optCar = Optional.of(car);

这时候如果car是空,就会直接抛出NullPointerException

c.可接受null的Optional- ofNullable()

Optional<Car> optCar = Optional.ofNullable(car);

如果car是null,那么得到的Optional对象就是个空对象。

2) 使用 map 从 Optional 对象中提取和转换值

比如,你可能想要从insurance公司对象中提取 公司的名称。

提取名称之前,你需要检查insurance对象是否为null,代码如下:

String name = null;
if(insurance != null){
 	name = insurance.getName();
}
  • 为了支持这种模式,Optional提供了一个map方法。(这里的map方法,从概念上讲和前面stream的map相差无几,因为其实说到底Optional也是流)
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

如果Optional包含一个值,那函数就将该值作为参数传递给map,对该值进行转换。如 果Optional为空,就什么也不做。

 

image-20201220232011807

3) 使用 flatMap 链接 Optional 对象

 

image-20201220233602966

a.使用Optional获取car的保险公司名称

public String getCarInsuranceName(Optional<Person> person) {
     return person.flatMap(Person::getCar)
         .flatMap(Car::getInsurance)
         .map(Insurance::getName)
         .orElse("Unknown");
    }

b.使用Optional解引用串接的Person/Car/Insurance对象

 

image-20201220233925639

4) 默认行为及解引用 Optional 对象

Optional类提供了多种方法读取 Optional实例中的变量值。

  • get()是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量 值,否则就抛出一个NoSuchElementException异常。所以,除非你非常确定Optional 变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于 嵌套式的null检查,也并未体现出多大的改进。
  • orElse(T other)是我们在代码清单10-5中使用的方法,正如之前提到的,它允许你在 Optional对象不包含值时提供一个默认值。
  • orElseGet(Supplier other)是orElse方法的延迟调用版,Supplier 方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作, 你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在 Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。
  • orElseThrow(Supplier exceptionSupplier)和get方法非常类似, 它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希 望抛出的异常类型。
  • ifPresent(Consumer)让你能在变量值存在时执行一个作为参数传入的 方法,否则就不进行任何操作。

5) 两个 Optional 对象的组合

假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外 部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:

public Insurance findCheapestInsurance(Person person, Car car) {
     // 不同的保险公司提供的查询服务
     // 对比所有数据
     return cheapestCompany;
}

还假设你想要该方法的一个null-安全的版本,它接受两个Optional对象作为参数, 返回值是一个Optional对象,如果传入的任何一个参数值为空,它的返回值亦为空.

public Optional<Insurance> nullSafeFindCheapestInsurance(
     Optional<Person> person, Optional<Car> car) {
     if (person.isPresent() && car.isPresent()) { 
         return Optional.of(findCheapestInsurance(person.get(), car.get()));
     } else {
         return Optional.empty();
     }
}
  • 这么写也不是不可以,但是如果真的这么写的话,和之前大量的!=null又有什么区别呢

  • 所以要做改进,把flatMap用到实处

  • 以不解包的方式组合两个Optional对象

public Optional<Insurance> nullSafeFindCheapestInsurance(
     Optional<Person> person, Optional<Car> car) {
     return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}

6) 使用 filter 剔除特定的值

比如,你可能需要检查保险公司的名 称是否为“Cambridge-Insurance”。为了以一种安全的方式进行这些操作,你首先需要确定引用指 向的Insurance对象是否为null,之后再调用它的getName方法,

Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
 	System.out.println("ok");
}
  • 如果调用filter方法,接受一个谓词作为参数。
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
     "CambridgeInsurance".equals(insurance.getName()))
     .ifPresent(x -> System.out.println("ok"));

分类和概括 - 表格

 

image-20201221001544168

 

image-20201221001605937

4、使用 Optional 的实战示例

1) 用 Optional 封装可能为 null 的值

假设你有一个Map方法,访问由key索引的值时,如果map 中没有与key关联的值,该次调用就会返回一个null。

Object value = map.get("key");

这个时候如果get为空就会报错,可以采用Optional.ofNullable方法:

Optional<Object> value = Optional.ofNullable(map.get("key"));

2) 异常与 Optional 的对比

使用静态方法Integer.parseInt(String),将 String转换为int。在这个例子中,如果String无法解析到对应的整型,该方法就抛出一个 NumberFormatException。

  • 可以做一些优化,做一个工具方法

将String转换为Integer,并返回一个Optional对象

public static Optional<Integer> stringToInt(String s) {
     try {
        return Optional.of(Integer.parseInt(s));
     } catch (NumberFormatException e) {
        return Optional.empty();
     }
}

第十二章 新的日期和时间API

1、LocalDate、LocalTime、Instant、Duration 以及 Period

1) 使用LocalDate和LocalTime

  • 创建一个LocalDate对象并读取其值
LocalDate date = LocalDate.of(2014, 3, 18);
//2014-03-18
int year = date.getYear();
//2014
Month month = date.getMonth();
//MARCH
int day = date.getDayOfMonth();
//18
DayOfWeek dow = date.getDayOfWeek();
//TUESDAY
int len = date.lengthOfMonth();
//31.查看一个月一共多少天
boolean leap = date.isLeapYear(); 
//false(不是闰年)


LocalDate today = LocalDate.now(); 
//获取当前时间
  • 当然也可以传递一个TemporalField参数给get方法拿到同样的信息。
  • TemporalField是一个接口,它定 义了如何访问temporal对象某个字段的值。ChronoField枚举实现了这一接口,所以你可以很 方便地使用get方法得到枚举元素的值。
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
  • 创建LocalTime并读取其值
LocalTime time = LocalTime.of(13, 45, 20);
//13:45:20
int hour = time.getHour();
//13
int minute = time.getMinute();
//45
int second = time.getSecond();
//20
  • LocalDate和LocalTime都可以通过parse去将字符串解析成日期时间
LocalDate date = LocalDate.parse("2014-03-18");
LocalTime time = LocalTime.parse("13:45:20");

2) 合并日期和时间-LocalDateTime

  • LocalDateTime,是LocalDate和LocalTime的合体。
// 2014-03-18T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
  • 当然也可以从LocalDateTime中提取LocalDate或者LocalTime
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();

3) 机器的日期和时间格式 -Instant

  • 这个是方便机器来读时间的,返回的是时间戳

通过向静态工厂方法ofEpochSecond传递一个代表秒数的值创建一个该类的实例。静 态工厂方法ofEpochSecond还有一个增强的重载版本,它接收第二个以纳秒为单位的参数值,对 传入作为秒数的参数进行调整。重载的版本会调整纳秒参数,确保保存的纳秒分片在0到999 999 999之间。

  • 所以,下面四个实例相同
//3秒
Instant.ofEpochSecond(3);
//3秒+0纳秒
Instant.ofEpochSecond(3, 0);
//2秒+100万纳秒,即1秒
Instant.ofEpochSecond(2, 1_000_000_000);
//4秒-100万纳秒
Instant.ofEpochSecond(4, -1_000_000_000); 

//可以通过Instant.now()返回int型时间戳
int day = Instant.now();

4) 定义Duration或Period-持续时间或期间

上面的所有类都实现了Temporal接口,这个接口定义了如何读取和操纵为时间建模和对象的值

可以创建两个LocalTimes对象、两个LocalDateTimes对象,或者两个Instant对象之间的duration

Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);
  • 可能会发现上面的三行都是time的,因为Duration主要是用于计算以秒和纳秒衡量时间的长短,所以不能仅向between传递LocalDate对象作为参数
  • 当然,由于Local系列是方便人来读的,而Instant是方便机器读的时间戳,所以不能将两种参数同时传入between方法,否则会报个DateTimeException异常
  • 当然,如果要算两个日期之间的期间,可以用Period
Period tenDays = Period.between(LocalDate.of(2014, 3, 8),
 																LocalDate.of(2014, 3, 18));
  • 而且Duration和Period类都提供了很多非常方便的工厂类,直接创建对应的实例;
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
  • 以下是日期-时间类中表示时间间隔的通用方法

 

image-20201223100530453

 

image-20201223100541513

2、操纵、解析和格式化日期

ChronoUnit很重要,请记住这个枚举类

  • 如果已经有一个LocalDate对象,想要创建它的一个修改版,最直接也最简单的方法是使 用withAttribute方法。且不会修改对象而只修改属性.
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.withYear(2011);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
  • 也可以加减
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(3);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);
  • 除此之外,还有很多通用方法

 

image-20201224094424094

1) 使用TemporalAdjuster- 下周、下个工作日等

  • 比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。
  • 可以通过TemporalAdjuster类的静态工厂方法访问它们
import static java.time.temporal.TemporalAdjusters.*;

LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(lastDayOfMonth());

TemporalAdjuster类中的工厂方法

 

image-20201224094900711

2) 实现一个定制的TemporalAdjuster

  • 设计一个NextWorkingDay类,该类实现了TemporalAdjuster接口,能够计算明天 的日期,同时过滤掉周六和周日这些节假日。

  • 如果当天的星期介于周一至周五之间,日期向后移动一天;如果当天是周六或者周日,则 返回下一个周一。

  • NextWorkingDay类的实现如下:

public class NextWorkingDay implements TemporalAdjuster {
   @Override
   public Temporal adjustInto(Temporal temporal) {
     DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
     int dayToAdd = 1;
     if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
     else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
     return temporal.plus(dayToAdd, ChronoUnit.DAYS);
   }
}
  • 上面是类的形式,但是由于TemporalAdjuster是一个函数式接口, 只能以Lambda表达式的方式向该adjuster接口传递行为,需要这样用
date = date.with(temporal -> {
     DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
     int dayToAdd = 1;
     if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
     else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
     return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
  • 当然,这样写也依然很累赘,可以对它进行封装。
  • 使用TemporalAdjusters类的静态工厂方法ofDateAdjuster,它接受一个UnaryOperator
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
       temporal -> {
           DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
           int dayToAdd = 1;
           if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
           if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
           return temporal.plus(dayToAdd, ChronoUnit.DAYS);
   });

//使用
date = date.with(nextWorkingDay);

3) 打印输出及解析日期-时间对象(格式化)

  • 新的 java.time.format包就是特别为这个目的而设计的。这个包中,最重要的类是DateTimeFormatter。

  • 所有的 DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。比如,下 面的这个例子中,我们使用了两个不同的格式器生成了字符串:

LocalDate date = LocalDate.of(2014, 3, 18);
//20140318
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
//2014-03-18
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
  • 当然,也可以通过解析代表日期或时间的字符串重新创建该日期对象。所有的日期和时间API 都提供了表示时间点或者时间段的工厂方法,你可以使用工厂方法parse达到重创该日期对象 的目的:
LocalDate date1 = LocalDate.parse("20140318",DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18",DateTimeFormatter.ISO_LOCAL_DATE);
  • DateTimeFormatter类还支持一个静态工厂方法,它可以按 照某个特定的模式创建格式器,代码清单如下。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);
  • 当然你也定义自己的DateTimeFormatter
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date.format(italianFormatter); // 18. marzo 2014
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);
  • 如果还需要更细化的控制,可以用DateTimeFormatterBuilder类
  • 可 以 通 过 DateTimeFormatterBuilder自己编程上个代码片段的italianFormatter
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
         .appendText(ChronoField.DAY_OF_MONTH)
         .appendLiteral(". ")
         .appendText(ChronoField.MONTH_OF_YEAR)
         .appendLiteral(" ")
         .appendText(ChronoField.YEAR)
         .parseCaseInsensitive()
         .toFormatter(Locale.ITALIAN);

3、处理不同时区的和历法

  • 新的java.time.ZoneId 类是老版java.util.TimeZone的替代品。
  • 如果之前用老版的java.util.TimeZone创建了时区,可以用toZoneId()方法转换成新的ZoneId类型
ZoneId zoneId = TimeZone.getDefault().toZoneId();

1) 为时间点添加时区信息

//日期
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
//日期加时间
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
//时间戳
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
  • 对ZonedDateTime的组成部分进行了说明,理解LocaleDate、 LocalTime、LocalDateTime以及ZoneId之间的差异。

 

image-20201224122412386
//通过ZoneId,你还可以将LocalDateTime转换为Instant:
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
//你也可以通过反向的方式得到LocalDateTime对象:
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

Q.E.D.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水豚少年的码农生活

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值