数据来源:《Java实战第二版》、Chatgpt
1、Stream API
流:从支持数据处理操作的源生成的元素序列。关键元素:元素序列、源、数据处理操作、流水线、内部迭代。
流的使用包括三件事:
1、一个数据源(如集合)来执行一个查询
2、一个中间操作链,形成一条流的流水线
3、一个终端操作,执行流水线,并能生成结果
1.1 关键概念
-
及早求值: 会立即对流进行计算并生成新流或结果,如
sorted()
,collect()
. -
惰性求值: 只在终止操作时才执行计算.
-
中间操作: 可以连接起来的流操作。如:filter、map、limit、sorted、distinct.
-
终端操作: 关闭流的操作,并返回一个不是流的结果。 如:forEach、count、collect.
1.2 构建流
1.2.1 由值创建流
Stream<String> stream = Stream.of("1","2");
1.2.2 由可空对象创建流
Stream<String> stram = Stream.ofNullable(System.getProperty("home"));
1.2.3 由数组创建流
int[] numbers = {1,2,3};
int sum = Arrays.stream(numbers).sum();
1.2.4 由文件生成流
try (Stream<String> lines = Files.lines(Paths.get("filename.txt"))) {
lines.filter(line -> line.contains("hello"))
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
1.2.5 由函数生成无限流
Stream.iterate(0,n->n+2)
.limit(10)
.forEach(System.out:println);
1.2.6 创建空流
- 迭代
Stream.empty();
-
生成
Stream.generate(Math::random) .limit(5) .forEach(System.out::println);
1.3 API
1.3.1 筛选
- filter(): 该操作接受一个 Predicate 参数,返回一个新的流,其中包含原始流中所有符合条件的元素。
例如,以下代码使用 filter 操作从一个字符串列表中过滤出长度大于 5 的字符串:
List<String> list = Arrays.asList("apple", "banana", "watermelon", "orange", "pear");
Stream<String> stream = list.stream().filter(s -> s.length() > 5);
1.3.2 流切片
-
takeWhile(): 它可以从流中按顺序获取元素,直到遇到第一个不符合指定条件的元素。该操作接受一个 Predicate 参数,返回一个新的流,其中包含原始流中从开头开始满足条件的所有元素。
以下代码使用 takeWhile 操作从一个字符串列表中获取直到第一个长度小于 6 的字符串:
List<String> list = Arrays.asList("apple", "banana", "watermelon", "orange", "pear");
Stream<String> stream = list.stream().takeWhile(s -> s.length() >= 6);
-
dropWhile(): 从流中按顺序丢弃元素,直到遇到第一个不符合指定条件的元素。该操作接受一个 Predicate 参数,返回一个新的流,其中包含原始流中去掉开头所有不满足条件的元素后剩余的所有元素。
以下代码使用 dropWhile 操作从一个字符串列表中去掉前缀为"a"的所有字符串:
List<String> list = Arrays.asList("apple", "banana", "apricot", "orange", "avocado"); Stream<String> stream = list.stream().dropWhile(s -> s.startsWith("a"));
-
limit(): 用于限制从流中获取的元素数量。该操作接受一个 long 类型的参数,返回一个新的流,其中包含原始流中指定数量的元素。
以下代码使用 limit 操作从一个字符串列表中获取前三个元素:
List<String> list = Arrays.asList("apple", "banana", "watermelon", "orange", "pear"); Stream<String> stream = list.stream().limit(3);
-
skip(): 用于跳过流中的前 n 个元素。该操作接受一个 long 类型的参数,返回一个新的流,其中包含原始流中去掉开头的 n 个元素后剩余的所有元素。
以下代码使用 skip 操作从一个字符串列表中跳过前两个元素:
List<String> list = Arrays.asList("apple", "banana", "watermelon", "orange", "pear"); Stream<String> stream = list.stream().skip(2);
1.3.3 映射
-
map(): 用于将一个流中的每个元素通过指定的函数进行转换。该操作接受一个 Function 参数,返回一个新的流,其中包含所有经过指定函数处理后的元素。
以下代码使用 map 操作将一个字符串列表中的所有字符串转换为大写形式:
List<String> list = Arrays.asList("apple", "banana", "watermelon", "orange", "pear"); Stream<String> stream = list.stream().map(String::toUpperCase);
1.3.4 流的扁平化
-
flatmap(): 用于将一个流中的每个元素映射为一个流,并将这些流合并成为一个流。具体来说,它接收一个 Function 参数,这个函数将一个流中的元素映射为另一个流,最终返回的是所有映射后的流的合并后的结果。
以下代码使用 flatMap 操作从一个字符串列表中获取所有单词,并转换为大写形式:
List<String> list = Arrays.asList("Hello World", "I am Java Stream", "Goodbye");
Stream<String> stream = list.stream().flatMap(str -> Arrays.stream(str.split(" "))).map(String::toUpperCase);
- map()
- Arrays.stream()
1.3.5 查找和匹配
-
allMatch: 流中元素是否都能匹配给定的谓词
-
anyMatch: 流中是否有一个元素能匹配给定的谓词
-
noneMatch: 流中没有任何元素与给定的谓词匹配
-
findFirst: 返回流中的第一个元素
-
findAny: 返回当前流中的任意元素
1.3.6 归约
collectors提供的工厂方法创建的收集器.
-
将流元素归约和汇总成一个值
// 汇总 long howMangDishes = menu.stream().collect(Collectors.counting()); // 求和 int totalCalories = menu.collect().collect(summingInt(Dish::getCalories)); // 查找最大值 Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories); Optional<Dish> mostCalorieDish = menu.collect().collect(maxBy(dishCaloriesComparator)); // 求平均值 double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories)); // 求汇总结果: 总和、平均值、最大值、最小值 IntSummaryStatistics int[] arr = {1, 2, 3, 4, 5}; IntStream stream = Arrays.stream(arr); IntSummaryStatistics stats = stream.summaryStatistics(); System.out.println("Count: " + stats.getCount()); System.out.println("Min: " + stats.getMin()); System.out.println("Max: " + stats.getMax()); System.out.println("Sum: " + stats.getSum()); System.out.println("Average: " + stats.getAverage()); // 连接字符串 String shortMenu = menu.stream().map(Dish::getName).collect(joining(","));
-
reduce()
用于将流中的所有元素通过指定的操作(如加法、乘法等)进行归约,得到一个最终结果。这个方法可以带有一个初始值,也可以不带。
BigDecimal[] arr = {BigDecimal.valueOf(1), BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4), BigDecimal.valueOf(5)}; BigDecimal sum = Arrays.stream(arr).reduce(BigDecimal.ZERO, BigDecimal::add);
-
元素分组
用于对流中的元素进行分组,并把结果存储到一个
Map
对象中。1、一级分组
// 简单 Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType)); // 简单 mapping Map<Dish.Type, List<String>> dishNamesByType = menu.stream().collect(groupingBy(Dish::getType,mapping(Dish::getName,toList()))); // 简单 匿名方法 public enum ClaoricLevel{DIET,NORMAL,FAT} 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; } }) ); // 复杂 flatMap() Map<String, List<String>> dishTags = new HashMap<>(); dishTags.put("pork", asList("greasy","salty")); dishTags.put("beef", asList("salty","roasted")); Map<Dist.Type, Set<String>> dishNamesByType = menu.stream().collect( groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(),toSet())) );
2、多级分组
// 简单 Map<Dish.Type, Map<String, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect( groupingBy(Dish::getType, groupingBy(dish -> { if (dish.getCalories() <= 400){ return CaloricLevel.DIET; }else if(dish.getCalories() <= 700){ return CaloricLevel.NORMAL; }else{ return CaloricLevel.FAT; } }) ) ); // 把收集器的结果转换为另一种类型 Map<Dish.Type, Dish> mostCaloricByType = menu.stream().collect( groupingBy(Dish::getType, collectingAndThen(maxBy(comparingInt(Dish::getCalories)),Optional.get()) ) ); // 与groupingBy联合使用的其他收集器 Map<Dish.Type, Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType, sumingInt(Dish::getCalories));
-
元素分区
它用于将一个元素集合拆分为两个不同的分区。它接受一个
Predicate
参数作为过滤条件,该过滤条件将集合中的元素分为两个部分:符合条件的和不符合条件的。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Map<Boolean, List<Integer>> isEvenMap = numbers.stream() .collect(Collectors.partitioningBy(num -> num % 2 == 0));
1.4 数值流
数值流可以处理基本数据类型的计算和转换,避免了自动装箱(autoboxing)和拆箱(unboxing)的额外开销,从而提高了程序的性能。
- IntStream
- DoubleStream
- LongStream
2、Lambda 表达式
java Lambda 表达式是 Java 8 中引入的新特性,它可以让开发人员更快、更简洁地编写匿名函数和函数式接口的实现。
优点:匿名、函数、传递、简洁。
Lambda 表达式的语法非常简明,使用箭头符号 “->” 分隔参数列表和函数体,格式如下:
(parameter1, parameter2, ...) -> { function body }
2.1 关键概念
-
方法引用
方法引用(Method Reference)是Lambda表达式的一种简写形式,可以方便地重用已有的代码逻辑。方法引用可以看作是一个函数指针,它指向一个已经存在的方法,并且可以直接将其作为Lambda表达式传递。
方法引用有4种类型:
- 静态方法引用:类名::staticMethod
- 实例方法引用:实例名::instanceMethod 或者 类名::instanceMethod
- 构造方法引用:类名::new
- 数组构造方法引用:类型[]::new
-
行为参数化
行为参数化指的是将一个行为(behavior)传递给某个方法,使得该方法可以在执行时根据传递进来的行为来执行不同的操作。
-
类型推断
Java SE 8引入了类型推断机制,也称为局部变量类型推断(Local Variable Type Inference),它允许Java编译器自动推断出局部变量的数据类型,而无需显式地声明变量的类型。这一特性可以使得代码更加简洁、易读。
2.2 函数式接口
2.2.1 Predicate
Predicate是一个函数式接口,它代表一个可调用的布尔值测试。它只有一个抽象方法:
boolean test(T t);
2.2.2 Consumer
它代表一个接受单个输入参数并且不返回结果的操作,它经常用于在不返回任何结果的情况下修改或处理数据。
它只有一个抽象方法:
void accept(T t);
2.2.3 Function<T,R>
接受一个参数并返回一个结果。它的抽象方法如下所示:
R apply(T t);
其中,T表示输入类型,R表示输出类型。Function通常用于对数据进行转换或操作.
3、新日期时间 API
Java 8 中引入了全新的日期和时间 API,以解决之前 Java 日期时间库的不足。这个新的日期时间 API 位于 java.time 包中,包含许多新类和方法,如 LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Period、Duration 等等。
它的设计更加合理,易于使用,并提供了更多的功能,如时区处理、日期计算和格式化等。
3.1 LocalDate:表示年月日的类。
// 获取当前日期
LocalDate today = LocalDate.now();
System.out.println(today); // 输出格式为:2022-08-26
// 使用指定日期创建 LocalDate
LocalDate date = LocalDate.of(2022, 8, 15);
System.out.println(date); // 输出格式为:2022-08-15
// 获取年、月、日
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
3.2 LocalTime:表示时间的类。
// 获取当前时间
LocalTime now = LocalTime.now();
System.out.println(now); // 输出格式为:15:56:12.123456789
// 使用指定时间创建 LocalTime
LocalTime time = LocalTime.of(15, 30, 0);
System.out.println(time); // 输出格式为:15:30
3.3 LocalDateTime:表示年月日和时间的结合体。
// 获取当前日期和时间
LocalDateTime dt = LocalDateTime.now();
System.out.println(dt); // 输出格式为:2022-08-26T15:56:12.123456789
// 使用指定日期和时间创建 LocalDateTime
LocalDateTime dt2 = LocalDateTime.of(2022, Month.AUGUST, 26, 15, 30);
System.out.println(dt2); // 输出格式为:2022-08-26T15:30
3.4 ZonedDateTime:表示带时区的日期和时间。
// 获取当前带时区的日期和时间
ZonedDateTime zdt = ZonedDateTime.now();
System.out.println(zdt); // 输出格式为:2022-08-26T15:56:12.123456789+08:00[Asia/Shanghai]
3.5 Period 和 Duration:分别表示日期间隔和时间间隔。
// 计算两个日期之间的天数
LocalDate date1 = LocalDate.of(2022, 8, 15);
LocalDate date2 = LocalDate.of(2022, 8, 26);
Period period = Period.between(date1, date2);
System.out.println(period.getDays()); // 输出 11
// 计算两个时间之间的秒数
LocalTime time1 = LocalTime.of(8, 0, 0);
LocalTime time2 = LocalTime.of(17, 0, 0);
Duration duration = Duration.between(time1, time2);
System.out.println(duration.getSeconds()); // 输出 32400
4、接口默认方法
它是指在接口中定义一个带有实现的方法,这样实现该接口的类即使没有实现该方法,也可以通过默认方法获得该方法的实现。接口默认方法的具体特点如下:
- 默认方法使用 default 关键字进行定义,可以包含方法体。
- 接口默认方法可以被实现该接口的类继承或重写。
- 如果一个类实现了多个接口,而这些接口拥有相同名称的默认方法,则该类必须覆盖这些方法。
- 接口默认方法可以继承其他接口的默认方法和抽象方法。
默认方法的好处在于可以在不破坏现有的代码结构的情况下,向 Java 中的旧接口添加新的方法。另外,默认方法可以提高代码的可用性和灵活性。
以下是一个简单的接口定义示例,其中包含一个默认方法:
public interface MyInterface {
// 抽象方法
void abstractMethod();
// 默认方法
default void defaultMethod() {
System.out.println("This is a default method.");
}
}
如果一个类实现了该接口,但没有实现默认方法 defaultMethod()
,则该类会自动继承 defaultMethod()
的实现。
public class MyClass implements MyInterface {
// 实现抽象方法
public void abstractMethod() {
System.out.println("This is a abstract method.");
}
// 不需要实现默认方法 defaultMethod()
MyClass obj = new MyClass();
obj.abstractMethod(); // 输出 This is a abstract method.
obj.defaultMethod(); // 输出 This is a default method.
如上所示,在实现接口时,如果没有提供默认方法的实现,则会自动使用接口中的默认方法。
5、Optional
Optional是null的进阶,关键在于语义的区别:Optional是可以允许变量缺失的,null则只能依赖对业务模型的理解。针对null的检查只会掩盖问题,并未真正地修复问题。引入Optional类的意图并非要消除每一个null的引用,与此相反,它的目标是帮助你更好地设计出普适的API,让程序员看到方法签名,就能了解它是否接受一个null。
5.1 创建Optional对象
- Optional.empty()
- Optional.of(obj): obj,非空值
- Optional.ofNullable(obj): obj,可空值
5.2 用途
-
用Optional封装可能为null的值
-
异常处理
6、并行数据处理
6.1 并行流
并行流:一个把内容拆分成多个数据块,用不同线程分别处理每个数据块的流。
并行流的背后是Java7引入的分支|合并框架。
6.1.1 使用
-
parallel(): 把顺序流变为并行流,之后的操作并行。
-
sequential(): 把并行流变为顺序流,之后的操作顺序。
stream.parallel() .filter(...) .sequential() .map(...) .parallel() .reduce();
6.1.2 注意事项
- 不一定比顺序流效率更高,有时甚至违反直觉。
- 注意装箱。自动装箱和自动拆箱会大大降低性能,使用IntStream、DoubleStream来避免。
- 有些操作本身在并行流上的性能就比顺序流差。比如limit、findFirst这类依赖顺序的操作。
- 考虑流的操作流水线的总计算成本。
- 要考虑流背后的数据结构是否易于分解,留意中间的操作修改流的操作。
- 考虑终端合并操作中合并步骤的代价是大是小。如Collector中的combiner方法。
6.2 分支|合并框架
RecursiveTask: 以递归的方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。
较复杂,不作介绍
6.3 Spliterator
可分迭代器:用于遍历数据源中的元素,专为并行而设计。
较复杂,不作介绍
7、DSL
较复杂,不作介绍