前言
Java 12 作为短期支持版本(非 LTS)于 2019 年 3 月 19 日正式发布。虽然其支持周期较短,但引入了一些非常有价值的新特性和改进。
1. Switch 表达式
Switch 表达式是 Java 12 引入的最重要的语法特性之一,它允许 switch 作为表达式使用,并返回值,使代码更加简洁易读。
传统 switch 语句的问题
String dayOfWeek = "MONDAY";
String typeOfDay;
switch (dayOfWeek) {
case "MONDAY":
case "TUESDAY":
case "WEDNESDAY":
case "THURSDAY":
case "FRIDAY":
typeOfDay = "工作日";
break;
case "SATURDAY":
case "SUNDAY":
typeOfDay = "周末";
break;
default:
typeOfDay = "无效日期";
}
传统 switch 语句的问题:
- 需要显式使用 break 防止 fall-through
- 冗长且容易出错
- 无法直接作为表达式使用
Java 12 Switch 表达式
// 需要添加--enable-preview参数编译运行
String dayOfWeek = "MONDAY";
String typeOfDay = switch (dayOfWeek) {
case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "工作日";
case "SATURDAY", "SUNDAY" -> "周末";
default -> "无效日期";
};
优点:
- 箭头语法(
->
)更加简洁 - 不需要 break 语句
- 可以在同一行处理多个 case
- 编译器会检查穷尽性(exhaustiveness),确保所有可能情况都被处理
2. 字符串新增方法
Java 12 为 String 类添加了几个新方法,使字符串处理更加方便。
indent()方法
String text = "Hello\nWorld";
System.out.println(text.indent(4));
// 输出:
// Hello
// World
String indented = " 缩进的文本";
System.out.println(indented.indent(-2));
// 减少缩进
transform()方法
transform()
方法允许对字符串应用一个函数,并返回函数的结果。这个方法的强大之处在于它可以将字符串转换为任何类型的值,而不仅仅限于字符串。
方法签名:
public <R> R transform(Function<? super String, ? extends R> f)
基本用法:
String result = "hello".transform(s -> s.toUpperCase())
.transform(s -> s + " World");
System.out.println(result); // 输出: HELLO World
高级用法:
- 链式处理字符串:
String processed = " 需要处理的文本 "
.transform(String::strip) // 去除前后空格
.transform(String::toUpperCase) // 转为大写
.transform(s -> s.replace("处理", "转换")) // 替换文本
.transform(s -> s + "!"); // 添加感叹号
System.out.println(processed); // 输出: 需要转换的文本!
- 字符串到数值的转换:
int number = "42".transform(Integer::parseInt);
double price = "19.99".transform(Double::parseDouble);
System.out.println("数字: " + number); // 输出: 数字: 42
System.out.println("价格: " + price); // 输出: 价格: 19.99
- 字符串到集合的转换:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
List<String> words = "apple,banana,orange"
.transform(s -> s.split(","))
.transform(Arrays::asList);
System.out.println("水果列表: " + words); // 输出: 水果列表: [apple, banana, orange]
List<Integer> lengths = "apple,banana,orange"
.transform(s -> s.split(","))
.transform(Arrays::stream)
.transform(stream -> stream.map(String::length))
.transform(stream -> stream.collect(Collectors.toList()));
System.out.println("单词长度: " + lengths); // 输出: 单词长度: [5, 6, 6]
- 字符串到复杂对象的转换:
class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Person person = "John:30"
.transform(s -> s.split(":"))
.transform(parts -> new Person(parts[0], Integer.parseInt(parts[1])));
System.out.println(person); // 输出: Person{name='John', age=30}
- 结合 Optional 使用:
import java.util.Optional;
String input = "42";
Optional<Integer> optionalValue = Optional.of(input)
.transform(s -> {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
});
optionalValue.ifPresent(n -> System.out.println("解析的数字: " + n));
与其他方法的比较:
transform()
方法与 Java 8 引入的 Stream API 中的map()
方法相似,但有以下区别:
transform()
直接在 String 对象上调用,不需要创建 Streamtransform()
可以返回任何类型,更加灵活transform()
简化了链式操作,代码更简洁
性能考虑:
transform()
方法本身几乎没有性能开销,性能取决于传入的函数。在处理大量数据时,可能需要考虑使用更专业的工具,如并行流。
3. 紧凑数字格式
Java 12 引入了紧凑数字格式,用于以更易读的方式显示大数字。
import java.text.NumberFormat;
import java.util.Locale;
public class CompactNumberFormatExample {
public static void main(String[] args) {
NumberFormat fmt = NumberFormat.getCompactNumberInstance(
Locale.CHINA, NumberFormat.Style.SHORT);
System.out.println(fmt.format(1000)); // 输出: 1千
System.out.println(fmt.format(1500)); // 输出: 2千
System.out.println(fmt.format(1000000)); // 输出: 1百万
System.out.println(fmt.format(1200000)); // 输出: 1百万
// 长格式
NumberFormat fmtLong = NumberFormat.getCompactNumberInstance(
Locale.CHINA, NumberFormat.Style.LONG);
System.out.println(fmtLong.format(1000)); // 输出: 1千
System.out.println(fmtLong.format(1000000)); // 输出: 1百万
System.out.println(fmtLong.format(1000000000)); // 输出: 10亿
}
}
4. 文件比较 API (Files.mismatch)
Java 12 引入了新的文件比较方法Files.mismatch()
,可以高效比较两个文件的内容。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileMismatchExample {
public static void main(String[] args) throws IOException {
// 创建临时文件用于示例
Path file1 = Files.createTempFile("file1", ".txt");
Path file2 = Files.createTempFile("file2", ".txt");
Path file3 = Files.createTempFile("file3", ".txt");
// 写入内容
Files.writeString(file1, "Hello Java 12");
Files.writeString(file2, "Hello Java 12");
Files.writeString(file3, "Hello Java 11");
// 比较文件
long mismatch1 = Files.mismatch(file1, file2);
System.out.println("文件1和文件2比较: " + mismatch1); // -1表示文件内容相同
long mismatch2 = Files.mismatch(file1, file3);
System.out.println("文件1和文件3比较: " + mismatch2); // 返回第一个不同字节的位置
// 清理临时文件
Files.delete(file1);
Files.delete(file2);
Files.delete(file3);
}
}
5. Teeing 收集器
Java 12 为 Stream API 添加了新的收集器Collectors.teeing()
,可以同时执行两个收集操作,然后合并结果。这个收集器的名称来源于 Unix/Linux 中的"tee"命令,该命令能够将输入同时发送到两个不同的输出。
基本概念
Collectors.teeing()
方法接受三个参数:
- 第一个下游收集器
- 第二个下游收集器
- 一个合并函数,用于合并两个收集器的结果
方法签名:
public static <T, R1, R2, R> Collector<T, ?, R> teeing(
Collector<? super T, ?, R1> downstream1,
Collector<? super T, ?, R2> downstream2,
BiFunction<? super R1, ? super R2, R> merger)
基本示例
import java.util.List;
import java.util.stream.Collectors;
public class TeeingBasicExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 同时计算总和和平均值
record SumAndAverage(double sum, double average) {}
SumAndAverage result = numbers.stream().collect(
Collectors.teeing(
Collectors.summingDouble(i -> i), // 第一个收集器:计算总和
Collectors.averagingDouble(i -> i), // 第二个收集器:计算平均值
(sum, avg) -> new SumAndAverage(sum, avg) // 合并函数:将结果组合成一个记录
)
);
System.out.println("总和: " + result.sum()); // 输出: 总和: 15.0
System.out.println("平均值: " + result.average()); // 输出: 平均值: 3.0
}
}
高级用例
- 同时计算最小值和最大值:
import java.util.List;
import java.util.stream.Collectors;
public class MinMaxExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(5, 2, 8, 1, 9, 3);
record MinMax(int min, int max) {}
MinMax result = numbers.stream().collect(
Collectors.teeing(
Collectors.minBy(Integer::compareTo),
Collectors.maxBy(Integer::compareTo),
(min, max) -> new MinMax(
min.orElseThrow(),
max.orElseThrow()
)
)
);
System.out.println("最小值: " + result.min()); // 输出: 最小值: 1
System.out.println("最大值: " + result.max()); // 输出: 最大值: 9
}
}
- 分组统计:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Employee {
private final String name;
private final String department;
private final double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
}
public class GroupingStatisticsExample {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("张三", "技术", 10000),
new Employee("李四", "市场", 8000),
new Employee("王五", "技术", 12000),
new Employee("赵六", "市场", 9000),
new Employee("钱七", "技术", 11000)
);
record DepartmentStats(
Map<String, Long> headcount,
Map<String, Double> totalSalary
) {}
DepartmentStats stats = employees.stream().collect(
Collectors.teeing(
// 按部门统计人数
Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
),
// 按部门统计总薪资
Collectors.groupingBy(
Employee::getDepartment,
Collectors.summingDouble(Employee::getSalary)
),
DepartmentStats::new
)
);
stats.headcount().forEach((dept, count) ->
System.out.println(dept + " 部门人数: " + count));
stats.totalSalary().forEach((dept, total) ->
System.out.println(dept + " 部门总薪资: " + total));
}
}
- 复杂数据分析:
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
class Order {
private final String id;
private final String customerId;
private final List<String> products;
private final double totalAmount;
public Order(String id, String customerId, List<String> products, double totalAmount) {
this.id = id;
this.customerId = customerId;
this.products = products;
this.totalAmount = totalAmount;
}
public String getId() { return id; }
public String getCustomerId() { return customerId; }
public List<String> getProducts() { return products; }
public double getTotalAmount() { return totalAmount; }
}
public class OrderAnalysisExample {
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("1", "C1", List.of("A", "B"), 200),
new Order("2", "C2", List.of("B", "C"), 150),
new Order("3", "C1", List.of("A", "C"), 300),
new Order("4", "C3", List.of("D"), 100)
);
record OrderAnalysis(
Map<String, Double> customerTotals,
Set<String> popularProducts
) {}
OrderAnalysis analysis = orders.stream().collect(
Collectors.teeing(
// 计算每个客户的总消费
Collectors.groupingBy(
Order::getCustomerId,
Collectors.summingDouble(Order::getTotalAmount)
),
// 找出所有产品
Collectors.flatMapping(
order -> order.getProducts().stream(),
Collectors.filtering(
product -> orders.stream()
.flatMap(o -> o.getProducts().stream())
.filter(p -> p.equals(product))
.count() > 1,
Collectors.toSet()
)
),
OrderAnalysis::new
)
);
System.out.println("客户消费统计:");
analysis.customerTotals().forEach((customer, total) ->
System.out.println(customer + ": " + total));
System.out.println("热门产品(出现1次以上): " + analysis.popularProducts());
}
}
- 结合 partitioningBy:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class PartitioningExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
record OddEvenStats(
Map<Boolean, List<Integer>> partitioned,
Map<Boolean, Double> averages
) {}
OddEvenStats stats = numbers.stream().collect(
Collectors.teeing(
// 按奇偶分组
Collectors.partitioningBy(n -> n % 2 == 0),
// 按奇偶计算平均值
Collectors.partitioningBy(
n -> n % 2 == 0,
Collectors.averagingDouble(Integer::doubleValue)
),
OddEvenStats::new
)
);
System.out.println("偶数: " + stats.partitioned().get(true));
System.out.println("奇数: " + stats.partitioned().get(false));
System.out.println("偶数平均值: " + stats.averages().get(true));
System.out.println("奇数平均值: " + stats.averages().get(false));
}
}
与其他 Collectors 的组合
teeing()
收集器可以与其他 Collectors API 结合使用,创建更复杂的数据处理管道:
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Product {
private final String name;
private final String category;
private final double price;
public Product(String name, String category, double price) {
this.name = name;
this.category = category;
this.price = price;
}
public String getName() { return name; }
public String getCategory() { return category; }
public double getPrice() { return price; }
}
public class AdvancedTeeingExample {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("笔记本电脑", "电子", 5999),
new Product("手机", "电子", 2999),
new Product("T恤", "服装", 99),
new Product("裤子", "服装", 199),
new Product("键盘", "电子", 299)
);
record CategoryStats(
Map<String, List<Product>> productsByCategory,
Map<String, Double> avgPriceByCategory
) {}
CategoryStats stats = products.stream().collect(
Collectors.teeing(
// 按类别分组产品
Collectors.groupingBy(Product::getCategory),
// 按类别计算平均价格
Collectors.groupingBy(
Product::getCategory,
Collectors.averagingDouble(Product::getPrice)
),
CategoryStats::new
)
);
// 打印分析结果
stats.productsByCategory().forEach((category, prods) -> {
System.out.println(category + " 类别产品:");
prods.forEach(p -> System.out.println(" - " + p.getName() + ": " + p.getPrice()));
});
System.out.println("\n类别平均价格:");
stats.avgPriceByCategory().forEach((category, avg) ->
System.out.println(category + ": " + avg));
}
}
性能考虑
teeing()
收集器通常会对同一个数据流进行两次独立的收集操作,这意味着数据会被遍历两次。对于大型数据集,这可能会导致性能问题。在性能关键的场景中,可能需要考虑其他优化策略。
总结
Java 12 虽然是非 LTS 版本,但引入了多项重要特性,特别是 Switch 表达式(后来在 Java 14 中正式发布)和 String 新方法等对开发者日常工作有帮助的改进。紧凑数字格式和 Teeing 收集器等特性也提供了更好的 API 使用体验。