EDUCBA-现代-Java-笔记-全-
EDUCBA 现代 Java 笔记(全)
001:课程简介
在本课程中,我们将学习Java 8、Java 9和Java 12版本中引入的一些新特性。这些特性旨在帮助开发者编写更简洁、高效的代码。我们将从Java 8的核心概念开始,逐步深入到后续版本的增强功能。
Java 8 涵盖主题
以下是Java 8部分我们将要学习的主要内容:
- Lambda表达式:一种简洁的匿名函数表示法。
- 方法引用:用于直接引用已有方法或构造函数的语法糖。
- 函数式接口:只包含一个抽象方法的接口。
- Java字符串增强:对String类的新增方法。
- 流过滤器:用于处理集合数据的声明式操作。
- 接口的默认方法与静态方法:接口中允许包含具体实现的方法。
- For-each循环增强:迭代操作的改进。
- Stream收集器类:用于将流转换为各种形式的汇总结果。
- StringJoiner类:用于构造由分隔符分隔的字符序列。
- Optional类:用于避免空指针异常的容器类。
- 并行数组排序:利用多核处理器进行高效排序。
Java 9 涵盖主题
上一节我们介绍了Java 8的主要特性,本节中我们来看看Java 9带来的新变化。
- 接口中的私有方法:允许在接口中定义私有辅助方法。
- Try-with-resources增强:资源自动管理语句的改进。
- 匿名内部类的钻石操作符:允许在匿名类中使用钻石操作符(
<>)。 - Stream API增强:增加了如
takeWhile、dropWhile等方法。 - Java 9模块系统:引入了模块化系统来管理代码依赖和封装。
Java 12 涵盖主题
在了解了Java 8和9之后,我们最后将探索Java 12中的一些新特性。
- G1 Stream API:为G1垃圾收集器新增的API。
- 紧凑数字格式:用于格式化数字的紧凑表示形式。
- 字符串新方法:如
indent()、transform()等。 - Switch表达式(预览特性):允许
switch语句有返回值并简化写法。
环境准备

要实践本课程的所有示例,你需要准备以下开发环境:
- 集成开发环境:你可以从提供的链接免费下载IntelliJ IDEA。
- Java开发工具包:你需要从提供的链接下载并安装JDK 13版本,该版本可在Windows、Mac或Linux操作系统上免费获取。
开始实践
现在,让我们开始本课程的实践部分。我们将首先创建Java 8的程序,然后逐步过渡到Java 9和Java 12的特性。
现代Java特性:02:Lambda表达式入门
在上一节课程简介中,我们概述了将要学习的各个Java版本特性。本节中,我们将深入探讨Java 8引入的第一个核心特性——Lambda表达式。
Lambda表达式是Java 8引入的一个新特性。它是一个匿名函数,即没有名称,也不属于任何类。Lambda表达式的概念最初源自LISP编程语言。
要创建一个Lambda表达式,我们在Lambda操作符(->)的左侧指定输入参数(如果有的话),并将表达式或语句块放在操作符的右侧。
方法与Lambda表达式对比
我们可以通过对比Java方法和Lambda表达式来理解其结构:
- Java方法主要包含以下部分:
- 名称
- 参数列表
- 方法体
- 返回类型
- Lambda表达式主要包含以下部分:
- 参数列表
- 方法体
- (没有名称,因为它是匿名的)
- (没有显式的返回类型,Java 8编译器能够通过检查代码推断出返回类型)
函数式接口
要使用Lambda表达式,你需要创建一个自己的函数式接口,或者使用Java提供的预定义函数式接口。函数式接口是指只包含一个抽象方法的接口。例如,Runnable、Callable、ActionListener都是函数式接口。
在Java 8之前,我们使用匿名内部类来实现函数式接口的唯一抽象方法。现在,让我们通过示例来学习如何创建Lambda表达式。
示例1:无参数的Lambda表达式
首先,我们创建一个无参数的Lambda表达式示例。
-
定义函数式接口:
@FunctionalInterface interface MyFunctionalInterface { String sayHello(); } -
使用Lambda表达式实现接口:
public class LambdaExample1 { public static void main(String[] args) { // 使用Lambda表达式实现接口的抽象方法 MyFunctionalInterface msg = () -> { return "Hello"; }; // 调用方法 System.out.println(msg.sayHello()); // 输出: Hello } }执行这段代码,将在控制台输出字符串“Hello”。
示例2:单个参数的Lambda表达式
上一节我们创建了无参数的Lambda表达式,本节中我们来看看如何创建带有单个参数的Lambda表达式。
-
定义函数式接口:
@FunctionalInterface interface MyFunctionalInterface1 { int incrementByFive(int a); } -
使用Lambda表达式实现接口:
public class LambdaExample2 { public static void main(String[] args) { // Lambda表达式接收一个参数a,并返回a+5 MyFunctionalInterface1 f = (a) -> a + 5; // 调用方法,传入参数22 System.out.println(f.incrementByFive(22)); // 输出: 27 } }执行此代码,将计算22 + 5并输出结果27。
示例3:多个参数的Lambda表达式
最后,我们来看一个传递多个参数的Lambda表达式示例。
-
定义函数式接口:
@FunctionalInterface interface StringConcat { String sConcat(String a, String b); } -
使用Lambda表达式实现接口:
public class LambdaExample3 { public static void main(String[] args) { // Lambda表达式接收两个字符串参数,并将它们连接起来 StringConcat s = (s1, s2) -> s1 + s2; // 调用方法,传入"Hello"和"World" String result = s.sConcat("Hello", "World"); System.out.println("Result is: " + result); // 输出: Result is: HelloWorld } }执行这段代码,将输出连接后的字符串“HelloWorld”。
总结

本节课中我们一起学习了Lambda表达式的基础知识。我们了解到Lambda表达式是Java 8引入的匿名函数,它使代码更加简洁。我们通过对比方法与Lambda表达式的结构理解了其组成,并明确了Lambda表达式需要与函数式接口(只有一个抽象方法的接口)配合使用。最后,我们通过三个由浅入深的实践示例,学会了如何创建无参数、单参数以及多参数的Lambda表达式。在接下来的课程中,我们将继续探索Java 8的其他现代特性。
002:Lambda表达式与方法引用



在本节课中,我们将继续学习Java 8的Lambda表达式,并深入了解方法引用这一简化Lambda表达式的强大工具。我们将通过多个实例,学习如何将Lambda表达式应用于集合迭代,以及如何使用四种不同类型的方法引用来编写更简洁的代码。


使用Lambda表达式迭代集合
上一节我们介绍了Lambda表达式的基本语法,本节中我们来看看如何将其应用于集合的迭代操作。


以下是一个使用forEach方法和Lambda表达式遍历列表的示例:

import java.util.*;
public class LambdaExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
list.add("Date");
list.forEach(str -> System.out.println(str));
}
}
执行上述代码,会依次打印出列表中的每个元素。通过这种方式,我们可以简洁地迭代Map或其他集合类。
方法引用简介
我们已经学习了多种创建Lambda表达式的方式。现在,让我们转向Java 8中的方法引用。方法引用本质上是Lambda表达式的一种简写形式,用于直接调用一个已存在的方法。
例如,一个打印字符串的Lambda表达式 str -> System.out.println(str) 可以替换为方法引用 System.out::println。这里的双冒号 :: 是方法引用操作符,用于分隔类(或对象)与方法名。
Java 8提供了四种主要的方法引用类型。
方法引用类型一:引用特定对象的实例方法


第一种类型是引用特定对象的实例方法,格式为 对象::实例方法名。


以下是实现此功能的步骤:

- 定义一个函数式接口。
- 创建一个包含目标实例方法的类。
- 在主类中创建该类的对象。
- 使用方法引用将该对象的方法赋值给接口引用。
- 通过接口引用调用方法。

// 1. 定义函数式接口
interface MyInterface {
void display();
}
// 2. 创建包含实例方法的类
class MyClass {
public void myMethod() {
System.out.println("Instance Method");
}
}



// 3. 主类中使用方法引用
public class MethodRefExample1 {
public static void main(String[] args) {
// 创建对象
MyClass obj = new MyClass();
// 使用方法引用
MyInterface ref = obj::myMethod;
// 调用方法
ref.display(); // 输出:Instance Method
}
}


方法引用类型二:引用类的静态方法
第二种类型是引用类的静态方法,格式为 类名::静态方法名。
以下是实现此功能的步骤:
- 创建一个包含静态方法的类。
- 使用函数式接口(如
BiFunction)来引用该静态方法。 - 通过接口的
apply方法传入参数并获取结果。

import java.util.function.BiFunction;

// 1. 创建包含静态方法的类
class Multiplication {
public static int multiply(int a, int b) {
return a * b;
}
}

// 2. 主类中使用静态方法引用
public class MethodRefExample2 {
public static void main(String[] args) {
// 引用静态方法
BiFunction<Integer, Integer, Integer> product = Multiplication::multiply;
// 应用参数并获取结果
int pr = product.apply(11, 5);
System.out.println("Product of given number is: " + pr); // 输出:55
}
}

方法引用类型三:引用特定类型的任意对象的实例方法


第三种类型是引用特定类型的任意对象的实例方法,格式为 类名::实例方法名。这在处理集合排序等操作时非常有用。

以下是实现此功能的步骤:

- 创建一个字符串数组。
- 使用
Arrays.sort方法并传入String::compareToIgnoreCase方法引用进行不区分大小写的排序。 - 遍历并打印排序后的数组。


import java.util.Arrays;
public class MethodRefExample3 {
public static void main(String[] args) {
String[] strArray = {"Steve", "Rick", "Aditya", "Lucy", "John"};
// 使用方法引用进行排序
Arrays.sort(strArray, String::compareToIgnoreCase);
// 打印排序后的数组
for (String str : strArray) {
System.out.println(str);
}
// 输出顺序为:Aditya, John, Lucy, Rick, Steve
}
}
方法引用类型四:引用构造函数
第四种类型是引用构造函数,格式为 类名::new。
以下是实现此功能的步骤:
- 定义一个函数式接口,其方法返回类型与目标构造函数匹配。
- 创建一个类,其构造函数包含所需逻辑。
- 使用方法引用将构造函数赋值给接口引用。
- 通过接口引用调用方法,从而触发构造函数。
// 1. 定义函数式接口
interface MyInterface1 {
Hello display(String s);
}


// 2. 创建类
class Hello {
// 构造函数
public Hello(String s) {
System.out.println(s);
}
}

// 3. 主类中使用构造函数引用
public class MethodRefExample4 {
public static void main(String[] args) {
// 引用构造函数
MyInterface1 ref = Hello::new;
// 调用方法,会创建Hello对象并执行其构造函数
ref.display("Hello World"); // 输出:Hello World
}
}

总结
本节课中我们一起学习了Lambda表达式在集合迭代中的应用,并深入探讨了Java 8方法引用的四种类型:
- 特定对象的实例方法引用 (
对象::实例方法) - 类的静态方法引用 (
类名::静态方法) - 特定类型的任意对象的实例方法引用 (
类名::实例方法) - 构造函数引用 (
类名::new)

方法引用提供了一种更简洁、更直接的方式来指向已有的方法或构造函数,是编写高效、清晰Java代码的重要工具。在接下来的课程中,我们将开始学习Java 8的另一个核心特性:Stream API。
003:流与接口

在本节课中,我们将学习Java 8引入的两个核心特性:流(Streams) 和接口的默认/静态方法。我们将通过对比传统代码与使用流的代码,直观地理解流如何使代码更简洁高效。同时,我们将探讨接口的新特性如何增强其灵活性。

流(Streams)简介


上一节我们介绍了Lambda表达式,本节中我们来看看如何将其与流(Streams)结合使用。流提供了一种声明式处理数据集合(如列表)的方式,可以极大地简化代码。

流的简单示例

我们将通过一个例子来对比传统循环和使用流的代码。目标是统计一个字符串列表中,长度小于6的字符串数量。

以下是使用传统循环的实现代码:

import java.util.ArrayList;
import java.util.List;
public class TraditionalExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("David");
names.add("Eve");
names.add("Steve");
long count = 0;
for (String name : names) {
if (name.length() < 6) {
count++;
}
}
System.out.println("There are " + count + " strings with length less than 6.");
}
}
现在,我们使用流和Lambda表达式来实现相同的功能:

import java.util.ArrayList;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("David");
names.add("Eve");
names.add("Steve");
long count = names.stream()
.filter(str -> str.length() < 6)
.count();
System.out.println("There are " + count + " strings with length less than 6.");
}
}
两个程序输出相同的结果,但使用流的代码更加简洁。这展示了流在减少代码行数方面的优势。
流的工作原理
现在我们来理解流在Java中是如何工作的。流的操作可以分为三个步骤:
- 创建流:从一个数据源(如集合)创建流。
- 中间操作:对初始流进行转换,生成一个新的流。可以链式调用多个中间操作。在上面的例子中,
filter就是一个中间操作。 - 终端操作:对最终的流执行操作,产生一个结果或副作用。在上面的例子中,
count就是一个终端操作。

以下是流的一些关键特性:

- 不存储元素:流本身不存储数据,它只是数据源的视图。
- 不修改源数据:对流进行的聚合操作(如
filter,map)不会改变原始数据源,它们总是返回一个新的流。 - 惰性求值:中间操作是惰性的,只有在终端操作需要时才会执行。例如,如果使用
limit(2),流在处理完前两个元素后就会停止。



流的操作示例
接下来,我们通过几个例子来熟悉流的常见操作。


示例1:迭代与筛选

这个例子展示如何生成一个整数流,筛选出能被3整除的数,并限制只取前6个结果。

import java.util.stream.Stream;

public class StreamIterateExample {
public static void main(String[] args) {
Stream.iterate(1, count -> count + 1)
.filter(number -> number % 3 == 0)
.limit(6)
.forEach(System.out::println);
}
}
示例2:合并两个流
这个例子展示如何使用 concat 方法将两个流合并成一个。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamConcatExample {
public static void main(String[] args) {
List<String> alphabets = Arrays.asList("A", "B", "C");
List<String> names = Arrays.asList("John", "Alice");
Stream<String> combinedStream = Stream.concat(alphabets.stream(), names.stream());
combinedStream.forEach(str -> System.out.print(str + " "));
}
}


示例3:使用 filter 操作
filter 是一个中间操作,它根据给定条件过滤流中的元素。
以下是使用 filter 筛选出长度大于6的名字:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;


public class StreamFilterExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Melcandry", "Sansa", "John", "Catelyn");
Stream<String> allNames = names.stream();
Stream<String> longNames = allNames.filter(str -> str.length() > 6);
longNames.forEach(str -> System.out.print(str + " "));
}
}

示例4:filter 与 collect 结合


我们可以将过滤后的流直接收集到一个新的列表中。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamFilterCollectExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Melcandry", "Sansa", "John", "Catelyn", "Tyrion");
List<String> longNames = names.stream()
.filter(str -> str.length() > 6)
.collect(Collectors.toList());
longNames.forEach(System.out::println);
}
}

示例5:filter 中使用多个条件
在 filter 中可以使用逻辑运算符组合多个条件。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;



public class StreamMultiFilterExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Sansa", "John", "Catelyn", "Joffrey");
List<String> longNames = names.stream()
.filter(str -> str.length() > 6 && str.length() < 8)
.collect(Collectors.toList());
longNames.forEach(System.out::println);
}
}
示例6:map 操作
map 操作将流中的每个元素映射成另一个元素。以下例子计算列表中每个数字的平方。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamMapExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(2, 3, 4, 5, 6);
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(squares);
}
}

Java 8 接口的增强
在进入接口新特性之前,我们先回顾一下流的使用。接下来,我们看看Java 8为接口带来的重要变化。

在Java 8之前,接口只能包含抽象方法。从Java 8开始,接口可以拥有 默认方法(Default Methods) 和 静态方法(Static Methods)。
引入默认方法的主要目的是允许开发者向接口添加新方法,而不会破坏已经实现了该接口的现有类。
默认方法示例

让我们创建一个包含默认方法的接口。


首先,定义接口 MyInterface:

public interface MyInterface {
// 默认方法
default void newMethod() {
System.out.println("Newly added default method");
}
// 抽象方法
void existingMethod(String str);
}

然后,创建一个类来实现这个接口:


public class MyClass implements MyInterface {
// 实现抽象方法
@Override
public void existingMethod(String str) {
System.out.println("String is: " + str);
}
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.newMethod(); // 调用接口的默认方法
obj.existingMethod("Java 8 is easy to learn");
}
}


运行 MyClass,可以看到默认方法 newMethod 被成功调用。

静态方法示例

接口也可以包含静态方法。我们在上面的接口中添加一个静态方法:
public interface MyInterface {
default void newMethod() {
System.out.println("Newly added default method");
}
// 静态方法
static void anotherNewMethod() {
System.out.println("Newly added static method");
}
void existingMethod(String str);
}

在实现类中,可以直接通过接口名调用静态方法:


public class MyClass implements MyInterface {
@Override
public void existingMethod(String str) {
System.out.println("String is: " + str);
}
public static void main(String[] args) {
MyInterface.anotherNewMethod(); // 调用接口的静态方法
MyClass obj = new MyClass();
obj.existingMethod("Hello World");
}
}

解决默认方法冲突


当一个类实现了多个接口,而这些接口有同名的默认方法时,就会发生冲突。实现类必须解决这个冲突。

定义两个接口:


interface MyInterface1 {
default void newMethod() {
System.out.println("Default method from MyInterface1");
}
void existingMethod(String str);
}
interface MyInterface2 {
default void newMethod() {
System.out.println("Default method from MyInterface2");
}
void disp(String str);
}
如果一个类同时实现这两个接口,编译器会报错,因为不知道应该继承哪个 newMethod。
// 这会编译错误
public class MyClass implements MyInterface1, MyInterface2 {
@Override
public void existingMethod(String str) { }
@Override
public void disp(String str) { }
}
解决方法是在实现类中重写这个冲突的默认方法:
public class MyClass implements MyInterface1, MyInterface2 {
@Override
public void existingMethod(String str) {
System.out.println("String is: " + str);
}
@Override
public void disp(String str) {
System.out.println("String is: " + str);
}
// 重写冲突的默认方法
@Override
public void newMethod() {
System.out.println("Implementation of default method in MyClass");
// 也可以选择调用某个父接口的默认方法
// MyInterface1.super.newMethod();
}
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.newMethod();
}
}
总结

本节课中我们一起学习了Java 8的两个强大特性。


首先,我们深入探讨了流(Streams)。我们了解到流通过 创建流 -> 中间操作 -> 终端操作 的流水线模式,以声明式风格处理数据。我们练习了 filter, map, limit, concat, collect 等关键操作,并理解了流不存储数据、不修改源和惰性求值的特点。与传统的循环和条件语句相比,流能让代码更简洁、更易读。
其次,我们学习了接口的增强。Java 8允许接口包含默认方法和静态方法。默认方法使接口能够向后兼容地扩展功能,而静态方法允许在接口中定义工具方法。我们还学习了当多个接口存在默认方法冲突时,需要在实现类中重写该方法以解决冲突。



掌握这些现代Java特性,将帮助你编写出更干净、更高效、更易于维护的代码。
004:Stream收集器与字符串连接器

在本节课中,我们将学习Java中两个强大的工具:Stream收集器(Collectors)和字符串连接器(StringJoiner)。我们将通过具体示例,了解如何使用它们来简化数据处理和字符串拼接任务。
概述



Stream收集器是java.util.stream.Collectors类提供的工具,用于将流(Stream)中的元素汇总成各种形式的结果,例如集合、映射或统计值。字符串连接器是Java 8引入的StringJoiner类,用于以指定的分隔符、前缀和后缀高效地连接多个字符串。

Stream收集器示例


上一节我们介绍了Stream API的基本概念,本节中我们来看看如何使用收集器对数据进行分组、转换和聚合。
示例1:使用groupingBy和counting进行分组计数
此示例演示如何使用Collectors.groupingBy方法对列表元素进行分组,并计算每个元素的出现次数。


以下是实现步骤:


- 导入必要的包:
java.util.*,java.util.function.Function,java.util.stream.Collectors。 - 创建一个字符串列表。
- 使用流的
collect方法,传入Collectors.groupingBy(Function.identity(), Collectors.counting())。 - 打印结果映射。

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class GroupingByExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Steve", "John", "Aji", "John", "Aji", "Aji");
Map<String, Long> result = names.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(result);
}
}
执行上述代码,输出结果为:{Steve=1, John=2, Aji=3}。


示例2:从对象列表中提取特定字段到新列表
这个例子展示如何从一个学生对象列表中,提取所有学生的姓名到一个新的字符串列表中。


以下是实现步骤:

- 创建一个
Student类,包含id、name、age字段。 - 创建一个
Student对象列表。 - 使用流的
map方法将每个Student对象转换为其name字段。 - 使用
Collectors.toList()将流转为一个新的列表。

import java.util.*;
import java.util.stream.Collectors;


class Student {
int id;
String name;
int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
public class MapExample {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student(11, "Jon", 22));
studentList.add(new Student(22, "Steve", 18));
studentList.add(new Student(33, "Lucy", 22));
studentList.add(new Student(44, "Sana", 23));
studentList.add(new Student(55, "Maggie", 18));
List<String> names = studentList.stream()
.map(n -> n.name)
.collect(Collectors.toList());
System.out.println(names);
}
}
输出结果为:[Jon, Steve, Lucy, Sana, Maggie]。
示例3:使用过滤器与收集器获取符合条件的对象集合



此示例在上一个例子的基础上,增加了过滤条件,只收集年龄大于22岁的学生,并将结果存入一个Set。


以下是实现步骤:

- 沿用之前的
Student类和列表。 - 使用流的
filter方法,筛选出id > 22的学生。 - 使用
Collectors.toSet()将结果收集到一个Set集合中。 - 遍历并打印
Set中的学生信息。

// ... Student 类定义同上
import java.util.Set;



public class FilterExample {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
// ... 添加学生对象同上
Set<Student> students = studentList.stream()
.filter(n -> n.id > 22)
.collect(Collectors.toSet());
for(Student stu : students) {
System.out.println(stu.id + " " + stu.name + " " + stu.age);
}
}
}
输出结果将显示id大于22的学生信息。

示例4:使用averagingInt计算平均值

这个例子演示如何使用Collectors.averagingInt方法计算学生列表的平均年龄。

以下是实现步骤:


- 创建一个新的
Student1类(或复用之前的Student类)。 - 创建学生列表并添加数据。
- 使用流的
collect方法,传入Collectors.averagingInt(s -> s.age)。 - 打印计算出的平均年龄。
import java.util.*;
import java.util.stream.Collectors;
class Student1 {
int id;
String name;
int age;
public Student1(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}


public class AverageExample {
public static void main(String[] args) {
List<Student1> studentList = new ArrayList<>();
studentList.add(new Student1(11, "Jon", 20));
studentList.add(new Student1(22, "Steve", 18));
studentList.add(new Student1(33, "Lucy", 22));
studentList.add(new Student1(44, "Sana", 23));
studentList.add(new Student1(55, "Maggie", 18));
Double avgAge = studentList.stream()
.collect(Collectors.averagingInt(s -> s.age));
System.out.println("The average age of students is: " + avgAge);
}
}
输出结果为:The average age of students is: 20.4。

字符串连接器示例
了解了Stream收集器后,我们再来看看另一个实用工具——字符串连接器(StringJoiner),它专门用于优雅地拼接字符串。
示例1:使用指定分隔符连接字符串



此示例展示如何使用StringJoiner,以连字符“-”作为分隔符来连接多个字符串。

以下是实现步骤:
- 导入
java.util.StringJoiner。 - 创建
StringJoiner实例,指定分隔符为“-”。 - 使用
add方法添加多个字符串。 - 打印最终连接好的字符串。

import java.util.StringJoiner;
public class StringJoinerDemo1 {
public static void main(String[] args) {
StringJoiner myString = new StringJoiner("-");
myString.add("Logan");
myString.add("Magneto");
myString.add("Rogue");
myString.add("Storm");
System.out.println(myString);
}
}
输出结果为:Logan-Magneto-Rogue-Storm。
示例2:添加前缀和后缀
这个例子在连接字符串的同时,为最终结果添加方括号作为前缀和后缀。
以下是实现步骤:




- 创建
StringJoiner实例时,依次传入分隔符“,”、前缀“[”和后缀“]”。 - 添加多个字符串。
- 打印结果。
import java.util.StringJoiner;
public class StringJoinerDemo2 {
public static void main(String[] args) {
StringJoiner myString = new StringJoiner(",", "[", "]");
myString.add("Logan");
myString.add("Magneto");
myString.add("Rogue");
myString.add("Storm");
System.out.println(myString);
}
}
输出结果为:[Logan,Magneto,Rogue,Storm]。


示例3:合并两个StringJoiner对象

此示例演示如何将两个StringJoiner对象的内容合并到一起。
以下是实现步骤:

- 创建第一个
StringJoiner对象myString,使用逗号分隔并添加前缀后缀。 - 创建第二个
StringJoiner对象myAnotherString,使用连字符分隔并添加不同的前缀后缀。 - 使用第一个对象的
merge方法合并第二个对象。 - 分别打印合并前后的字符串。



import java.util.StringJoiner;

public class StringJoinerDemo3 {
public static void main(String[] args) {
StringJoiner myString = new StringJoiner(",", "[", "]");
myString.add("Logan");
myString.add("Magneto");
myString.add("Rogue");
myString.add("Storm");
System.out.println("First String: " + myString);
StringJoiner myAnotherString = new StringJoiner("-", "P", "S");
myAnotherString.add("Sansa");
myAnotherString.add("Imp");
myAnotherString.add("Jon");
myAnotherString.add("Ned");
System.out.println("Second String: " + myAnotherString);
StringJoiner mergedString = myString.merge(myAnotherString);
System.out.println("Merged String: " + mergedString);
}
}
输出结果将显示两个独立的字符串以及合并后的新字符串。

总结

本节课中我们一起学习了Java Stream收集器(Collectors)和字符串连接器(StringJoiner)的核心用法。


- Stream收集器 提供了强大的终端操作,可以轻松实现分组、计数、映射、过滤和聚合(如求平均值)等复杂的数据转换操作,让流处理代码更加简洁高效。
- 字符串连接器 则提供了一种清晰、可控的方式来拼接多个字符串,支持自定义分隔符、前缀和后缀,避免了传统
StringBuilder手动处理分隔符的繁琐。


掌握这两个工具,能显著提升处理集合数据和字符串拼接任务的代码质量和开发效率。
005:数组并行排序与Java 9接口私有方法 🚀


在本节课中,我们将学习Java 8引入的数组并行排序功能,以及Java 9中接口私有方法的新特性。我们将通过具体示例来理解这些概念,并学习如何编写更简洁、高效的代码。


字符串合并器(StringJoiner)进阶用法


上一节我们介绍了StringJoiner的基本用法,本节中我们来看看它的其他实用方法,例如如何设置空值、获取长度以及转换为字符串。



以下是StringJoiner类中setEmptyValue、length和toString方法的示例:

import java.util.StringJoiner;

public class StringJoinerExample {
public static void main(String[] args) {
StringJoiner myString = new StringJoiner(",");
myString.setEmptyValue("这是一个默认字符串");
System.out.println("默认字符串: " + myString);
myString.add("Apple");
myString.add("Banana");
myString.add("Orange");
myString.add("Kiwi");
myString.add("Grapes");
System.out.println("添加值后的字符串: " + myString);
int length = myString.length();
System.out.println("StringJoiner的长度: " + length);
String s = myString.toString();
System.out.println("转换为字符串: " + s);
}
}
运行此代码,首先会输出设置的默认字符串,然后输出添加了所有水果名称的合并字符串。接着,程序会计算并打印StringJoiner对象的字符长度,最后展示通过toString方法转换后的标准字符串。
数组并行排序(Arrays.parallelSort)
现在,我们进入Java 8的核心新特性之一:数组并行排序。此方法位于java.util.Arrays类中,旨在利用多线程并行处理来加速大型数组的排序。
并行排序的原理是:将一个大数组递归地分割成多个子数组,直到每个子数组达到一个最小粒度。然后,多个线程并行地对这些子数组进行排序,最后使用归并排序算法将已排序的子数组合并起来。




示例1:对基本数据类型数组进行并行排序



让我们看一个对整型数组进行并行排序的简单例子。

import java.util.Arrays;



public class ParallelSortExample1 {
public static void main(String[] args) {
int[] numbers = {9, 2, 7, 1, 5, 8, 3, 6, 4};
Arrays.parallelSort(numbers);
Arrays.stream(numbers).forEach(n -> System.out.print(n + " "));
}
}


执行这段代码,输出结果将是按升序排列的数组:1 2 3 4 5 6 7 8 9。Arrays.parallelSort(numbers)方法完成了所有工作。


示例2:指定范围进行并行排序
parallelSort方法还允许我们只对数组的特定区间进行排序,通过指定起始索引(包含)和结束索引(不包含)来实现。


import java.util.Arrays;

public class ParallelSortExample2 {
public static void main(String[] args) {
int[] numbers = {9, 2, 7, 1, 5, 8, 3, 6, 4};
// 对索引从 1(第二个元素)到 5(第六个元素,不包含)的部分进行排序
Arrays.parallelSort(numbers, 1, 5);
Arrays.stream(numbers).forEach(n -> System.out.print(n + " "));
}
}
在这个例子中,只有原数组中索引1到4的元素(即值2, 7, 1, 5)被排序。输出结果为:9 1 2 5 7 8 3 6 4。第一个元素9和索引5之后的元素8, 3, 6, 4保持了原来的顺序。



Java 9 接口私有方法 🔒

Java 8允许在接口中定义default和static方法,以便向后兼容。Java 9在此基础上更进一步,引入了接口私有方法。
引入私有方法的主要目的是:消除多个default或static方法中的重复代码,通过私有方法来共享这些公共逻辑,从而使接口设计更清晰,代码更易维护。


示例1:Java 8中重复代码的问题


首先,我们看一个Java 8接口的例子,其中两个default方法包含了相同的代码块。


// Java 8 接口示例
interface MyInterfaceP1 {
default void method1() {
System.out.println("Starting method");
System.out.println("Doing something");
System.out.println("This is method 1");
}
default void method2() {
System.out.println("Starting method");
System.out.println("Doing something");
System.out.println("This is method 2");
}
}

class DemoClass26 implements MyInterfaceP1 {
public static void main(String[] args) {
DemoClass26 obj = new DemoClass26();
obj.method1();
obj.method2();
}
}
输出两段相同的“Starting method”和“Doing something”。这导致了代码冗余。




示例2:Java 9使用私有方法优化


现在,我们用Java 9的私有方法来重构上面的接口,将公共代码提取到一个私有方法中。

// Java 9 接口示例
interface MyInterfaceP2 {
default void method1() {
printCommonLines(); // 调用私有方法
System.out.println("This is method 1");
}
default void method2() {
printCommonLines(); // 调用同一个私有方法
System.out.println("This is method 2");
}
// Java 9 允许的私有实例方法
private void printCommonLines() {
System.out.println("Starting method");
System.out.println("Doing something");
}
}


class DemoClass27 implements MyInterfaceP2 {
public static void main(String[] args) {
DemoClass27 obj = new DemoClass27();
obj.method1();
obj.method2();
}
}
输出结果与之前相同,但接口内部的代码消除了重复,更加简洁。
示例3:接口中的私有静态方法


Java 9还支持私有静态方法,主要用于在接口的多个static方法之间共享代码。


// Java 9 接口示例(包含私有静态方法)
interface MyInterfaceP3 {
static void method1() {
printStaticCommonLines(); // 调用私有静态方法
System.out.println("This is static method 1");
}
static void method2() {
printStaticCommonLines(); // 调用同一个私有静态方法
System.out.println("This is static method 2");
}
// Java 9 允许的私有静态方法
private static void printStaticCommonLines() {
System.out.println("Starting static method");
System.out.println("Doing something static");
}
default void myMethods() {
method1(); // 调用静态方法
method2();
}
}


class DemoClass28 implements MyInterfaceP3 {
public static void main(String[] args) {
DemoClass28 obj = new DemoClass28();
obj.myMethods(); // 通过默认方法调用静态方法
}
}



在这个例子中,两个静态方法method1和method2共享了私有静态方法printStaticCommonLines中的代码。default方法myMethods则展示了如何在实例方法中调用这些接口静态方法。


总结 📚


本节课中我们一起学习了:
StringJoiner的进阶功能:包括设置空值、获取长度和转换为字符串。- Java 8 数组并行排序:使用
Arrays.parallelSort()方法高效排序大型数组,并可以指定排序范围。 - Java 9 接口私有方法:包括私有实例方法和私有静态方法。它们的主要价值在于消除接口中
default和static方法的代码冗余,提升代码的复用性和可读性,是编写整洁高效Java代码的重要工具。



通过掌握这些现代Java特性,你可以让代码更加模块化、高效,并易于维护。
006:try-with-resources增强与Java 9新特性
在本节课中,我们将要学习Java 9中对try-with-resources语句的增强,以及其他一些重要的新特性,包括菱形操作符的扩展、@SafeVarargs注解的增强以及Stream API的新增方法。我们将通过简单的代码示例来理解这些特性如何帮助我们编写更简洁、更高效的代码。
try-with-resources 增强



上一节我们介绍了Java 7中引入的try-with-resources语句。本节中我们来看看Java 9对其进行的重大增强。

try-with-resources语句的主要优势在于它能自动关闭所有资源(如文件、数据库连接、网络连接等),无需显式调用close()方法。这可以防止内存泄漏,并减少不必要的代码行,使代码更具可读性。
首先,我们回顾一下Java 7中的用法,然后看看Java 9如何解决了Java 7中存在的一些问题。

以下是Java 7中使用try-with-resources的示例代码:


import java.io.FileNotFoundException;
import java.io.FileOutputStream;





public class TryWithResourcesJava7 {
public static void main(String[] args) throws FileNotFoundException {
try (FileOutputStream fileOutputStream = new FileOutputStream("book.txt")) {
String myString = "Writing this line in the output file.";
byte[] byteArray = myString.getBytes();
fileOutputStream.write(byteArray);
System.out.println("The given string is written in the file successfully.");
} catch (Exception e) {
System.out.println(e);
}
}
}

Java 7中的try-with-resources语句存在一个问题:它不允许在语句或块的作用域之外声明资源。这导致在某些情况下需要编写冗余的代码。
以下是Java 7中一个会引发编译错误的示例:


import java.io.FileNotFoundException;
import java.io.FileOutputStream;

public class ProblemExample {
public static void main(String[] args) throws FileNotFoundException {
FileOutputStream fileOutputStream = new FileOutputStream("book.txt");
// 错误:资源必须在try-with-resources中声明
try (fileOutputStream) { // 编译错误
String myString = "Writing this line in the output file.";
byte[] byteArray = myString.getBytes();
fileOutputStream.write(byteArray);
System.out.println("The given string is written in the file successfully.");
} catch (Exception e) {
System.out.println(e);
}
}
}


为了解决上述错误,在Java 9之前,我们需要一种变通方法,即在try-with-resources中再次引用已存在的资源对象,这显得非常繁琐。

Java 9增强的try-with-resources允许我们使用在外部声明的资源,只要该资源是final或实际上是final的(即未被重新赋值)。这使得代码更加简洁。

以下是Java 9中的正确写法:
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
public class TryWithResourcesJava9 {
public static void main(String[] args) throws FileNotFoundException {
// 资源在外部声明
FileOutputStream fileOutputStream = new FileOutputStream("book.txt");
// Java 9允许在try-with-resources中使用已存在的final或effectively final变量
try (fileOutputStream) {
String myString = "Writing this line in the output file.";
byte[] byteArray = myString.getBytes();
fileOutputStream.write(byteArray);
System.out.println("The given string is written in the file successfully.");
} catch (Exception e) {
System.out.println(e);
}
}
}


通过这个增强,我们避免了创建冗余对象,代码逻辑更加清晰。
菱形操作符与匿名内部类



上一节我们介绍了资源管理的改进,本节中我们来看看Java 9对菱形操作符的扩展。


菱形操作符(<>)在Java 7中引入,其目的是通过省略表达式右侧的泛型类型来避免冗余代码。然而,在Java 7中,菱形操作符不能与匿名内部类一起使用。


首先,我们看一个Java 7中菱形操作符在普通类中正常工作的例子:

// 抽象类
abstract class MyCalc<T> {
abstract T add(T num1, T num2);
}
public class DiamondOperatorJava7 {
public static void main(String[] args) {
// 使用菱形操作符,编译器推断类型为Integer
MyCalc<Integer> obj = new MyCalc<>() {
@Override
Integer add(Integer x, Integer y) {
return x + y;
}
};
Integer sum = obj.add(100, 101);
System.out.println(sum); // 输出 201
}
}
但在Java 7中,如果将菱形操作符用于匿名内部类,则会导致编译错误。

以下是Java 7中会出错的示例:
abstract class MyCalc1<T> {
abstract T add(T num1, T num2);
}
public class DiamondOperatorError {
public static void main(String[] args) {
// 在Java 7中,这行会编译错误:cannot infer type arguments for anonymous inner classes
MyCalc1<Integer> obj = new MyCalc1<>() { // 错误!
@Override
Integer add(Integer x, Integer y) {
return x + y;
}
};
Integer sum = obj.add(100, 101);
System.out.println(sum);
}
}
Java 9改进了菱形操作符的使用,允许我们将其与匿名内部类一起使用。上面的代码在Java 9及更高版本中可以正常编译和运行。

@SafeVarargs 注解增强
接下来,我们看看Java 9中对@SafeVarargs注解的增强。
@SafeVarargs注解用于抑制由包含可变参数(varargs)的方法引发的“unsafe operation”警告。在Java 9之前,此注解只能用于final、static方法或构造器,因为这些方法不能被重写(重写的方法仍可能对其可变参数执行不安全的操作)。



Java 9扩展了@SafeVarargs注解的使用范围,现在它也可以用于private方法,因为私有方法同样不能被重写。

以下是使用@SafeVarargs注解的示例:
import java.util.ArrayList;
import java.util.List;

public class SafeVarargsExample {
// Java 9允许对私有方法使用@SafeVarargs
@SafeVarargs
private void printList(List<String>... names) {
for (List<String> nameList : names) {
for (String name : nameList) {
System.out.println(name);
}
}
}
public static void main(String[] args) {
SafeVarargsExample obj = new SafeVarargsExample();
List<String> list1 = new ArrayList<>();
list1.add("Kevin");
list1.add("Rick");
List<String> list2 = new ArrayList<>();
list2.add("Negan");
list2.add("Suhu");
// 传递可变数量的列表参数
obj.printList(list1, list2);
}
}
如果不使用@SafeVarargs注解,编译器可能会产生关于可变参数潜在堆污染的警告。使用该注解可以安全地消除这些警告。
Stream API 增强



最后,我们探讨Java 9中Stream API的几项重要增强。Java 8引入了Stream API,而Java 9为其添加了四个新方法:dropWhile、takeWhile、iterate和ofNullable。由于Stream是一个接口,这些新增的方法都是default或static方法。
dropWhile 方法


dropWhile方法会丢弃流中所有元素,直到给定的谓词(条件)返回false为止。


以下是dropWhile方法的示例:

import java.util.stream.Stream;
public class StreamDropWhileExample {
public static void main(String[] args) {
Stream<Integer> myStream = Stream.of(11, 22, 40, 60, 100);
// 丢弃所有小于50的元素
myStream.dropWhile(num -> num < 50)
.forEach(num -> System.out.println(num));
// 输出:60, 100
}
}
takeWhile 方法
takeWhile方法与dropWhile相反,它会获取流中的元素,直到给定的谓词返回false为止。


以下是takeWhile方法的示例:
import java.util.stream.Stream;
public class StreamTakeWhileExample {
public static void main(String[] args) {
Stream<Integer> myStream = Stream.of(17, 20, 30, 40, 60, 91, 120);
// 只获取小于50的元素
myStream.takeWhile(num -> num < 50)
.forEach(num -> System.out.println(num));
// 输出:17, 20, 30, 40
}
}
iterate 方法

Java 9为Stream.iterate方法添加了一个新的重载版本,它接受三个参数:初始值、谓词(迭代继续的条件)和更新函数(用于生成下一个值)。
以下是iterate方法的示例:
import java.util.stream.Stream;
public class StreamIterateExample {
public static void main(String[] args) {
// 参数:初始值1,条件n<200,更新函数n->n*3
Stream.iterate(1, n -> n < 200, n -> n * 3)
.forEach(num -> System.out.println(num));
// 输出:1, 3, 9, 27, 81
}
}
ofNullable 方法


ofNullable方法用于避免NullPointerException。如果传入的元素为null,则返回一个空的流;如果非空,则返回一个包含该单个元素的顺序流。


以下是ofNullable方法的示例:
import java.util.stream.Stream;
public class StreamOfNullableExample {
public static void main(String[] args) {
String possibleNullString = null;
// 安全地处理可能为null的值
Stream<String> myStream = Stream.ofNullable(possibleNullString);
myStream.forEach(str -> System.out.println(str)); // 无输出,流为空
String nonNullString = "Hello";
Stream<String> myStream2 = Stream.ofNullable(nonNullString);
myStream2.forEach(System.out::println); // 输出:Hello
}
}

总结
本节课中我们一起学习了Java 9中的几个关键特性增强:
- try-with-resources增强:允许使用在外部声明的
final或effectively final资源变量,简化了代码。 - 菱形操作符扩展:现在可以与匿名内部类一起使用,提高了泛型代码的简洁性。
- @SafeVarargs注解增强:可以应用于
private方法,更灵活地抑制可变参数警告。 - Stream API新方法:引入了
dropWhile、takeWhile、新的iterate重载和ofNullable方法,使得流操作更加强大和易于处理边界情况(如null值)。

这些特性共同助力开发者编写出更简洁、更健壮、更易维护的Java代码。
007:Java 9 模块 🧩
在本节课中,我们将要学习Java 9引入的一个重要新特性:模块系统。我们将探讨为什么需要模块、模块是什么,以及如何在Java中创建和使用模块。
为什么需要模块?
Java最重要的特性之一是代码的可重用性。它允许我们通过继承和接口来重用已创建的类。为了高效地重用这些类,Java将它们组织在包中,将相似类型的类放在同一个包里。
随着代码规模的增长,Java中的包数量也随之增加。在处理使用数百个包的大型程序时,很难理解哪个类使用了什么。包是组织类的好方法,但当我们需要在代码中使用多个包时,也需要一种方式来组织包本身。
此外,使类在包之间可重用的唯一方法是将其声明为public,但这意味着任何人都可以使用它。这个问题也需要解决。
什么是模块?
Java 9引入了一个名为“模块”的新特性来解决上述问题。模块是一组包的集合。模块不仅组织包,还负责控制可访问性。这样,我们希望被重用的模块部分可以被使用,而不希望被重用的部分则不能被外部访问。
模块的类型
模块中的包主要分为两种类型:
- 导出包:旨在模块外部使用。这意味着任何其他模块中的程序都可以使用这些包。
- 隐藏包:不打算在模块外部使用。它们是模块内部的,只能在模块内部使用。
为了更好地理解导出包和隐藏包的概念,我们可以以java.base模块为例。该模块有许多导出包(如java.util、java.time),也有一些隐藏包(如sun.security.provider)。
如何创建和使用模块

模块定义在名为module-info.java的文件中。要声明一个包为导出包,需要在exports关键字后指定包名。
例如,在module-info.java中定义java.base模块可能如下所示:
module java.base {
exports java.util;
exports java.time;
exports java.net;
exports java.lang;
}




接下来,我们通过一个具体示例来演示如何创建和使用模块。

示例:创建和使用模块

我们将创建两个模块:hello.world和test。在hello.world模块中创建一个类,然后在test模块中使用它。
第一步:创建 hello.world 模块
- 创建一个名为
hello.world的模块。 - 在该模块内,创建一个包
com.hello.world.app。 - 在该包中,创建一个名为
HelloWorld的类,其中包含一个静态方法。 - 在
hello.world模块的module-info.java文件中,导出com.hello.world.app包。

HelloWorld.java 内容:
package com.hello.world.app;
public class HelloWorld {
public static void sayHello() {
System.out.println("Welcome to module");
}
}
module-info.java (位于 hello.world 模块) 内容:
module hello.world {
exports com.hello.world.app;
}

第二步:创建 test 模块并使用 hello.world 模块
- 创建一个名为
test的模块。 - 在
test模块的module-info.java文件中,声明需要hello.world模块。 - 在
test模块内创建一个包(例如com.test)和一个主类。 - 在主类中,导入
com.hello.world.app.HelloWorld并调用其静态方法。

module-info.java (位于 test 模块) 内容:
module test {
requires hello.world;
}

TestMain.java 内容:
package com.test;
import com.hello.world.app.HelloWorld;
public class TestMain {
public static void main(String[] args) {
HelloWorld.sayHello(); // 输出:Welcome to module
}
}
执行TestMain类的main方法,将成功打印出“Welcome to module”。这个示例展示了如何在Java 9及更高版本中,通过模块系统来组织代码并控制包的可访问性。

现代Java特性:07:Java 12 新特性示例 🚀
上一节我们介绍了Java 9的模块系统,本节中我们来看看Java 12引入的一些新特性示例,首先是Stream API中Collectors的teeing方法。
Collectors.teeing 方法


Java 12为Collectors类添加了一个新的静态方法teeing。该方法接受两个收集器和一个合并函数,用于合并这两个收集器的结果。
以下是使用teeing方法计算数字列表平均值的示例。
示例1:计算平均值
import java.util.Arrays;
import java.util.List;
import static java.util.stream.Collectors.*;

public class TeeingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(10, 20, 30, 40);
// 传统方式计算平均值
double average = 0d;
for (Integer number : numbers) {
average += number.doubleValue() / numbers.size();
}
System.out.println("Average (traditional): " + average);
// 使用 teeing 方法计算平均值
average = numbers.stream()
.collect(teeing(
summingDouble(i -> i), // 第一个收集器:求和
counting(), // 第二个收集器:计数
(sum, n) -> sum / n)); // 合并函数:和/计数
System.out.println("Average (teeing): " + average);
}
}

运行上述代码,两种方式计算出的平均值结果是相同的。teeing方法提供了一种更函数式、更简洁的方式在一次流操作中组合多个收集结果。

结合过滤与下游收集器
teeing方法可以很好地与过滤等操作结合,进行更复杂的数据处理。




以下是筛选出亚洲地区且生活成本低于等于1000的城市示例。
示例2:筛选并统计城市
import java.util.stream.Stream;
import static java.util.stream.Collectors.*;

public class TeeingFilterExample {
public static void main(String[] args) {
var result = Stream.of(
new City("Colombo", "South Asia", 987),
new City("Delhi", "South Asia", 914),
new City("London", "Western Europe", 1334),
new City("Melbourne", "Australia", 3050),
new City("Shanghai", "East Asia", 1998),
new City("Algiers", "North Africa", 1067),
new City("Hanoi", "Southeast Asia", 1331))
.collect(teeing(
// 第一个下游收集器:过滤并映射出符合条件的城市名列表
filtering(c -> c.region.contains("Asia") && c.costOfLiving <= 1000,
mapping(City::name, toList())),
// 第二个下游收集器:计算符合条件的城市数量
filtering(c -> c.region.contains("Asia") && c.costOfLiving <= 1000,
counting()),
// 合并函数:将列表和计数组合成一个结果字符串
(list, count) -> "Cities: " + list + ", Count: " + count
));
System.out.println(result); // 输出:Cities: [Colombo, Delhi], Count: 2
}
static class City {
private final String name;
private final String region;
private final int costOfLiving;
public City(String name, String region, int costOfLiving) {
this.name = name;
this.region = region;
this.costOfLiving = costOfLiving;
}
public String name() { return name; }
public String region() { return region; }
public int costOfLiving() { return costOfLiving; }
}
}



在这个例子中,我们使用teeing同时执行了两个操作:一个是获取符合条件的城市名称列表,另一个是计算这些城市的数量。最后通过合并函数将两者结果组合输出。
本节课总结
本节课中我们一起学习了两个主要部分:
- Java 9 模块系统:了解了模块出现的背景、概念(导出包与隐藏包),并通过一个完整的示例掌握了如何创建模块、导出包以及在另一个模块中声明依赖并使用它。模块化是管理大型Java应用复杂性的关键特性。
- Java 12
Collectors.teeing方法:学习了这个新方法的作用——它允许在一次流操作中组合两个独立的收集器,并通过一个合并函数处理它们的结果。我们通过计算平均值和复杂过滤统计两个例子,演示了其用法,这有助于编写更简洁、表达力更强的流处理代码。



通过掌握这些现代Java特性,你可以编写出结构更清晰、更高效且易于维护的代码。
008:Java 12中的紧凑数字格式 📊

在本节课中,我们将要学习Java 12引入的一个新特性:紧凑数字格式。这是一种将十进制数字格式化为紧凑形式的工具,例如将“10000”显示为“10K”。我们将通过多个示例,了解如何创建和使用紧凑数字格式,以及如何控制其显示方式。

概述
紧凑数字格式是NumberFormat类的一个子类,它提供了一种将长数字(如千、百万、十亿)格式化为更短、更易读形式的方法。NumberFormat是所有数字格式的抽象基类,它提供了格式化和解析数字的接口。Java 12通过NumberFormat.getCompactNumberInstance()方法扩展了此功能。

创建基本的紧凑数字格式
上一节我们介绍了紧凑数字格式的基本概念,本节中我们来看看如何创建一个基本的紧凑数字格式实例。
以下是创建一个使用美国英语区域设置和短样式格式的示例代码:

import java.text.NumberFormat;
import java.util.Locale;

public class BasicCompactNumberExample {
public static void main(String[] args) {
// 创建短样式的紧凑数字格式实例
NumberFormat nfShort = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.println("Short Style:");
System.out.println("Result: " + nfShort.format(10000));
System.out.println("Result: " + nfShort.format(120300));
System.out.println("Result: " + nfShort.format(2120000));
System.out.println("Result: " + nfShort.format(1950000300L));
// 创建长样式的紧凑数字格式实例
NumberFormat nfLong = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
System.out.println("\nLong Style:");
System.out.println("Result: " + nfLong.format(10000));
System.out.println("Result: " + nfLong.format(120300));
System.out.println("Result: " + nfLong.format(2120000));
System.out.println("Result: " + nfLong.format(1950000300L));
}
}

运行此代码,短样式输出为“10K”、“120K”、“2M”、“2B”,而长样式输出为“10 thousand”、“120 thousand”、“2 million”、“2 billion”。
为不同区域设置应用格式



了解了基本用法后,我们可以探索如何为不同的区域设置(如法国、中国)创建紧凑数字格式。


以下是一个展示不同区域设置下数字格式差异的示例:




import java.text.NumberFormat;
import java.util.Locale;

public class LocaleCompactNumberExample {
private static void printCompactNumberFormat(long number) {
// 创建不同区域和样式的格式实例
NumberFormat nfDefault = NumberFormat.getCompactNumberInstance();
NumberFormat nfUSLong = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
NumberFormat nfFRShort = NumberFormat.getCompactNumberInstance(Locale.FRANCE, NumberFormat.Style.SHORT);
NumberFormat nfFRLong = NumberFormat.getCompactNumberInstance(Locale.FRANCE, NumberFormat.Style.LONG);
NumberFormat nfCHShort = NumberFormat.getCompactNumberInstance(Locale.CHINA, NumberFormat.Style.SHORT);
NumberFormat nfCHLong = NumberFormat.getCompactNumberInstance(Locale.CHINA, NumberFormat.Style.LONG);
System.out.println("Compact Formatting for " + number + ":");
System.out.println("Default: " + nfDefault.format(number));
System.out.println("US Long: " + nfUSLong.format(number));
System.out.println("FR Short: " + nfFRShort.format(number));
System.out.println("FR Long: " + nfFRLong.format(number));
System.out.println("CH Short: " + nfCHShort.format(number));
System.out.println("CH Long: " + nfCHLong.format(number));
System.out.println();
}
public static void main(String[] args) {
printCompactNumberFormat(23450000);
printCompactNumberFormat(123000000);
}
}

此示例展示了同一个数字在不同语言和文化环境下的不同紧凑表示形式。
获取可用的区域设置

有时,我们可能需要知道系统支持哪些区域设置。NumberFormat类提供了相应的方法。
以下是列出所有可用数字格式区域设置的代码:
import java.text.NumberFormat;
import java.util.Locale;

public class AvailableLocalesExample {
public static void main(String[] args) {
System.out.println("Number Format Locales:");
Locale[] numberLocales = NumberFormat.getAvailableLocales();
for (Locale locale : numberLocales) {
System.out.println(locale.toString());
}
}
}
控制小数位数

我们可以控制紧凑数字格式中小数部分的位数。这通过setMinimumFractionDigits和setMaximumFractionDigits方法实现。
以下是设置小数位数的示例:

import java.text.NumberFormat;
import java.util.Locale;


public class FractionDigitsExample {
public static void main(String[] args) {
NumberFormat nf = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
// 打印默认的最小和最大小数位数
System.out.println("Minimum: " + nf.getMinimumFractionDigits());
System.out.println("Maximum: " + nf.getMaximumFractionDigits());
// 使用默认设置格式化数字
System.out.println("\nResult: " + nf.format(123454));
System.out.println("Result: " + nf.format(5156835));
// 设置最大小数位数为1
nf.setMaximumFractionDigits(1);
System.out.println("\nResult: " + nf.format(123454));
System.out.println("Result: " + nf.format(5156835));
// 设置最大小数位数为2
nf.setMaximumFractionDigits(2);
System.out.println("\nResult: " + nf.format(123454));
System.out.println("Result: " + nf.format(5156835));
// 同时设置最小和最大小数位数为2
nf.setMinimumFractionDigits(2);
nf.setMaximumFractionDigits(2);
System.out.println("\nResult: " + nf.format(123454));
System.out.println("Result: " + nf.format(5156835));
}
}




默认情况下,最小和最大小数位数均为0。调整这些值可以控制输出数字的精度。
设置舍入模式

紧凑数字格式支持设置舍入模式。默认模式是HALF_EVEN。我们可以使用setRoundingMode方法进行更改。

以下是演示不同舍入模式效果的示例:




import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;
public class RoundingModeExample {
public static void main(String[] args) {
NumberFormat nf = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
// 无小数位格式化
System.out.println("\nResult: " + nf.format(123454));
System.out.println("Result: " + nf.format(56835));
// 设置小数位数为2
nf.setMinimumFractionDigits(2);
nf.setMaximumFractionDigits(2);
System.out.println("\nResult: " + nf.format(123454));
System.out.println("Result: " + nf.format(56835));
// 设置舍入模式为 UP
nf.setRoundingMode(RoundingMode.UP);
System.out.println("\nResult (UP): " + nf.format(123454));
System.out.println("Result (UP): " + nf.format(56835));
// 设置小数位数为1
nf.setMinimumFractionDigits(1);
nf.setMaximumFractionDigits(1);
System.out.println("\nResult: " + nf.format(123454));
System.out.println("Result: " + nf.format(56835));
// 设置舍入模式为 DOWN
nf.setRoundingMode(RoundingMode.DOWN);
System.out.println("\nResult (DOWN): " + nf.format(123454));
System.out.println("Result (DOWN): " + nf.format(56835));
}
}


解析紧凑数字字符串


除了格式化,NumberFormat还可以将紧凑数字字符串(如“1K”)解析回数字。这是通过parse方法实现的。
以下是使用parse方法的示例:


import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;


public class ParseExample {
public static void main(String[] args) throws ParseException {
// 短样式解析
NumberFormat nfShort = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.println("US Short Parsing:");
System.out.println(nfShort.parse("1K"));
System.out.println(nfShort.parse("1M"));
System.out.println(nfShort.parse("1B"));
// 长样式解析
NumberFormat nfLong = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
System.out.println("\nUS Long Parsing:");
System.out.println(nfLong.parse("1 thousand"));
System.out.println(nfLong.parse("1 million"));
System.out.println(nfLong.parse("1 billion"));
}
}
启用分组分隔符解析
默认情况下,解析紧凑数字字符串时不支持分组分隔符(如逗号)。可以通过setGroupingUsed(true)来启用。
以下是启用分组分隔符解析的示例:
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;


public class ParseWithGroupingExample {
public static void main(String[] args) throws ParseException {
NumberFormat nf = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.println("Without Grouping:");
System.out.println(nf.parse("1000K"));
System.out.println(nf.parse("1000M"));
System.out.println(nf.parse("1000B"));
// 启用分组
nf.setGroupingUsed(true);
System.out.println("\nWith Grouping:");
System.out.println(nf.parse("1,000K"));
System.out.println(nf.parse("1,000M"));
System.out.println(nf.parse("1,000B"));
}
}
总结

本节课中我们一起学习了Java 12引入的紧凑数字格式。我们了解了如何创建不同区域和样式的紧凑数字格式实例,如何控制小数位数和舍入模式,以及如何将紧凑数字字符串解析回数字。这个特性使得显示和解析大数字变得更加简洁和符合本地化习惯,是编写国际化应用程序时的有用工具。
009:字符串类的新方法 🆕
在本节课中,我们将学习Java 12及后续版本中为String类引入的几个新方法。这些方法旨在帮助开发者编写更简洁、更高效的代码。我们将逐一探讨indent、transform、describeConstable、resolveConstantDesc方法以及新的switch箭头表达式。
概述

Java 12为String类添加了多个实用方法,用于处理字符串的缩进、转换以及与JVM常量API的交互。此外,Java 12还引入了新的switch表达式语法,使代码更加简洁。本节将通过具体示例详细讲解这些新特性的用法。

1. indent 方法
indent方法用于调整字符串每一行的缩进。它接受一个整数参数n:
- 当
n > 0时,在每行开头插入n个空格。 - 当
n < 0时,尝试从每行开头移除最多n个空格。 - 当
n = 0时,保持原样,但会统一换行符。
该方法还会将字符串中的换行符统一为\n。
以下是使用indent方法的示例代码:



public static void main(String[] args) {
String s = "Life is too short to work so hard.";
System.out.println("Original String: " + s);
System.out.println("String length: " + s.length());
String sIndent = s.indent(5);
System.out.println("\nIndented String: " + sIndent);
System.out.println("Indented String length: " + sIndent.length());
}

执行上述代码,原始字符串长度为33。使用indent(5)后,每行开头添加了5个空格,因此新字符串的长度变为38(33个原字符 + 5个空格)。


2. 处理多行字符串的 indent
indent方法在处理包含换行符\n的多行字符串时尤为有用。我们可以创建一个辅助方法来统计字符串中特定字符的出现次数。

首先,定义一个统计字符的方法:

static int countChar(String s, char c) {
int cnt = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) {
cnt++;
}
}
return cnt;
}
接着,在主方法中使用多行字符串并应用indent:


public static void main(String[] args) {
String s = "Life is short,\nso I am careful where I put my time.\nI don't want to waste it.";
System.out.println("Original String:\n" + s);
System.out.println("String length: " + s.length());
System.out.println("Count of \\n: " + countChar(s, '\n'));
String sIndent = s.indent(5);
System.out.println("\nIndented String:\n" + sIndent);
System.out.println("Indented String length: " + sIndent.length());
System.out.println("Count of \\n after indent: " + countChar(sIndent, '\n'));
}
运行代码后,可以看到原始字符串有2个换行符。使用indent(5)后,字符串长度增加,并且indent方法统一了换行符,统计结果可能发生变化。

我们也可以尝试使用负值和零值参数:


String s2 = s.indent(-2); // 尝试移除每行开头的2个空格
String s3 = s.indent(0); // 不改变缩进,但统一换行符
System.out.println("String after indent(-2):\n" + s2);
System.out.println("String after indent(0):\n" + s3);

3. transform 方法


transform方法允许你对字符串应用一个函数(Function),并将该函数的返回值作为结果。这为链式字符串转换提供了更函数式的编程风格。

以下是一个示例,展示如何将字符串转换为大写,然后转换为字符数组:
import java.util.Arrays;


public static void main(String[] args) {
String str = "Life's too short";
var result = str
.transform(input -> input.concat(" to be sad."))
.transform(String::toUpperCase)
.transform(String::toCharArray);
System.out.println(Arrays.toString(result));
}

在这个例子中,我们首先将字符串连接一段话,然后将其转换为大写,最后转换为字符数组。transform方法使得这一系列操作可以流畅地链式调用。

4. describeConstable 与 resolveConstantDesc 方法


这两个方法与JVM的常量API相关,通常用于底层库开发,日常应用较少。



describeConstable(): 返回一个包含该字符串实例本身描述符的Optional对象。resolveConstantDesc(): 解析常量描述符,对于String类型,返回的就是字符串实例本身。
以下是它们的用法示例:
import java.util.Optional;
public static void main(String[] args) {
String str = "Life is short and we should respect every moment of it.";
// 使用 describeConstable
Optional<String> optStr = str.describeConstable();
optStr.ifPresent(value -> System.out.println("Value from describeConstable: " + value));
// 使用 resolveConstantDesc
String str2 = str.resolveConstantDesc(null);
System.out.println("str2.equals(str): " + str2.equals(str));
System.out.println("str2 == str: " + (str2 == str)); // 注意:可能为true,因为字符串驻留
}


describeConstable返回的Optional包含字符串自身。resolveConstantDesc方法将常量描述符解析为String实例,对于字符串常量,结果通常与原始实例相同。



5. 新的 switch 箭头表达式 (Java 12+)

Java 12引入了switch表达式,并使用->箭头符号简化了语法。它允许switch直接返回值或执行一个语句块,无需break。
以下是新旧switch用法的对比示例:

public static void main(String[] args) {
// 调用示例
getGrade('A');
getGrade('C');
getGrade('D');
getGrade('E');
getGrade('X');
}
// 使用新的 switch 表达式(箭头语法)
public static void getGrade(char grade) {
switch (grade) {
case 'A' -> System.out.println("Excellent");
case 'B' -> System.out.println("Good");
case 'C' -> System.out.println("Standard");
case 'D' -> System.out.println("Low");
case 'E' -> System.out.println("Very Low");
default -> System.out.println("Invalid");
}
getResult(grade);
}
// 另一个使用 switch 返回值的例子
public static void getResult(char grade) {
String result = switch (grade) {
case 'A', 'B', 'C' -> "Success";
case 'D', 'E' -> "Fail";
default -> "No Result";
};
System.out.println("Result: " + result);
}

新的switch语法更加简洁明了。在getGrade方法中,每个case直接通过箭头->指向要执行的语句。在getResult方法中,switch直接计算并返回一个值,并且支持在单个case中匹配多个值(如case 'A', 'B', 'C')。



总结
本节课我们一起学习了Java 12及后续版本中引入的多个现代String类方法和语言特性:




indent(int n):用于调整字符串的缩进,正数添加空格,负数移除空格,并统一换行符。transform(Function):以函数式风格对字符串进行链式转换。describeConstable()与resolveConstantDesc():用于JVM常量API交互,返回字符串的描述符或实例本身。- 新的
switch箭头表达式:使用->语法简化了switch结构,支持直接返回值和多值匹配,使代码更清晰、更安全(避免遗忘break)。



掌握这些新特性有助于你编写出更简洁、更易读且更高效的Java代码。

浙公网安备 33010602011771号