Java 12 新特性详解与实践

45 阅读7分钟

前言

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

高级用法:

  1. 链式处理字符串:
String processed = "  需要处理的文本  "
    .transform(String::strip)        // 去除前后空格
    .transform(String::toUpperCase)  // 转为大写
    .transform(s -> s.replace("处理""转换")) // 替换文本
    .transform(s -> s + "!");        // 添加感叹号

System.out.println(processed); // 输出: 需要转换的文本!
  1. 字符串到数值的转换:
int number = "42".transform(Integer::parseInt);
double price = "19.99".transform(Double::parseDouble);

System.out.println("数字: " + number);      // 输出: 数字: 42
System.out.println("价格: " + price);       // 输出: 价格: 19.99
  1. 字符串到集合的转换:
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]
  1. 字符串到复杂对象的转换:
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}
  1. 结合 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 对象上调用,不需要创建 Stream
  • transform()可以返回任何类型,更加灵活
  • 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()方法接受三个参数:

  1. 第一个下游收集器
  2. 第二个下游收集器
  3. 一个合并函数,用于合并两个收集器的结果

方法签名:

public static <TR1R2R> Collector<T, ?, R> teeing(
    Collector<? super T, ?, R1> downstream1,
    Collector<? super T, ?, R2> downstream2,
    BiFunction<? super R1, ? super R2R> merger)

基本示例

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

public class TeeingBasicExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(12345);

        // 同时计算总和和平均值
        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
    }
}

高级用例

  1. 同时计算最小值和最大值:
import java.util.List;
import java.util.stream.Collectors;

public class MinMaxExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(528193);

        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
    }
}
  1. 分组统计:
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));
    }
}
  1. 复杂数据分析:
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<StringgetProducts() 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());
    }
}
  1. 结合 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(12345678910);

        record OddEvenStats(
            Map<BooleanList<Integer>> partitioned,
            Map<BooleanDoubleaverages
        ) {}

        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 使用体验。