专业的编程技术博客社区

网站首页 > 博客文章 正文

Java17 纪录类(java17官方下载官网)

baijin 2024-10-23 08:42:15 博客文章 6 ℃ 0 评论

记录类在以下情况下特别适用:

  1. 数据传输对象(DTO):记录类非常适合用作数据传输对象,用于在不同层之间传输数据。它们简洁、不可变,并自动提供了 equals()、hashCode() 和 toString() 方法,使得数据传输更加方便。
  2. 数据持久化:记录类可用于表示数据库表中的数据行或其他持久化数据。它们可以很好地与ORM(对象关系映射)框架集成,并提供简洁的数据模型。
  3. API响应对象:记录类可以用于表示API的响应对象,尤其是在使用JSON或其他序列化格式时。它们的简洁性和不可变性使得API的定义和使用更加清晰和简单。
  4. 值对象:记录类适用于表示值对象,即没有标识符的对象,其相等性只由其字段的值决定。这些对象通常用于表示简单的实体或概念,如日期、时间、坐标等。
  5. 模式匹配:Java 17 引入了模式匹配,记录类与模式匹配结合使用可以更加方便地进行模式匹配操作,例如使用instanceof运算符匹配记录类的类型和提取字段值。

使用记录类时需要注意以下几点:

  1. 不可变性:记录类的实例是不可变的,一旦创建就不能修改。因此,在设计记录类时应该考虑如何确保不可变性,并避免在记录类中提供可变的字段或方法。
  2. 继承性:记录类是final的,不能被继承。因此,不应该将记录类设计为需要扩展的基类,而应该将其设计为独立的数据传输对象或值对象。
  3. 可读性:由于记录类的字段和方法是自动生成的,并且记录类本身具有简洁的语法,因此应该确保记录类的名称和字段名称能够清晰地表达其用途和含义,以提高代码的可读性。
  4. 与模式匹配的结合使用:如果使用模式匹配操作记录类,需要注意确保模式匹配的覆盖情况,以避免出现意外的情况。

记录类是一种特殊类型的类,用于以比普通类更少的仪式来建模简单的数据集合。

有关记录类的背景信息,请参阅JEP 395。

记录类声明在头部指定了其内容的描述;适当的访问器、构造函数、equals、hashCode 和 toString 方法会自动生成。记录类的字段是final的,因为该类旨在作为一个简单的“数据载体”。

例如,以下是一个具有两个字段的记录类:

record Rectangle(double length, double width) { }

这个矩形的简洁声明相当于以下普通类:

public final class Rectangle {
    private final double length;
    private final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double length() { return this.length; }
    double width()  { return this.width; }

    // Implementation of equals() and hashCode(), which specify
    // that two record objects are equal if they
    // are of the same type and contain equal field values.
    public boolean equals...
    public int hashCode...

    // An implementation of toString() that returns a string
    // representation of all the record class's fields,
    // including their names.
    public String toString() {...}
}

记录类声明由以下部分组成:名称;可选的类型参数(支持泛型记录声明);头部,其中列出记录的“组件”;以及主体。

记录类自动声明以下成员:

对于头部中的每个组件,以下两个成员:

  1. 与记录组件相同名称和声明类型的私有final字段。有时这个字段被称为组件字段。
  2. 与组件相同名称和类型的公共访问器方法;在矩形记录类示例中,这些方法是Rectangle::length()和Rectangle::width()。

一个典型构造函数,其签名与头部相同。该构造函数将新实例化的记录类的每个参数分配给相应的组件字段。

equals和hashCode方法的实现,这些方法指定如果两个记录类是相同类型并且包含相等的组件值,则它们相等。

toString方法的实现,包括记录类所有组件的字符串表示,以及它们的名称。

由于记录类只是类的特殊类型,因此您可以使用new关键字创建记录对象(记录类的实例),例如:

Rectangle r = new Rectangle(4,5);

记录类的规范构造函数

以下示例显式声明 Rectangle 记录类的规范构造函数。它验证长度和宽度大于零。如果不是,它会抛出 IllegalArgumentException:

record Rectangle(double length, double width) {
    public Rectangle(double length, double width) {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
        this.length = length;
        this.width = width;
    }
}

重复在典范构造函数的签名中记录类的组件可能会很繁琐且容易出错。为了避免这种情况,您可以声明一个紧凑的构造函数,其签名是隐式的(自动从组件派生)。

例如,以下紧凑构造函数声明验证长度和宽度的方式与上一个示例相同:

record Rectangle(double length, double width) {
    public Rectangle {
        if (length <= 0 || width <= 0) {
            throw new java.lang.IllegalArgumentException(
                String.format("Invalid dimensions: %f, %f", length, width));
        }
    }
}

这种简洁形式的构造函数声明仅适用于记录类。请注意,出现在典范构造函数中的语句 this.length = length; 和 this.width = width; 在紧凑构造函数中并不存在。在紧凑构造函数的末尾,它的隐式形式参数被分配给记录类的私有字段,对应于其组件。

记录类成员的显式声明

您可以显式声明从标头派生的任何成员,例如对应于记录类组件的公共访问器方法,例如

record Rectangle(double length, double width) {
 
    // Public accessor method
    public double length() {
        System.out.println("Length is " + length);
        return length;
    }
}

如果您实现自己的访问器方法,请确保它们具有与隐式派生访问器相同的特征(例如,它们声明为public,并且具有与相应记录类组件相同的返回类型)。同样,如果您实现自己版本的equals、hashCode和toString方法,请确保它们具有与java.lang.Record类中相同的特征和行为,该类是所有记录类的共同超类。

您可以在记录类中声明静态字段、静态初始化程序和静态方法,它们的行为与普通类中的行为相同,例如:

record Rectangle(double length, double width) {
    
    // Static field
    static double goldenRatio;

    // Static initializer
    static {
        goldenRatio = (1 + Math.sqrt(5)) / 2;
    }

    // Static method
    public static Rectangle createGoldenRectangle(double width) {
        return new Rectangle(width, width * goldenRatio);
    }
}

不能在记录类中声明实例变量(非静态字段)或实例初始值设定项。

例如,以下记录类声明无法编译:

record Rectangle(double length, double width) {

    // Field declarations must be static:
    BiFunction<Double, Double, Double> diagonal;

    // Instance initializers are not allowed in records:
    {
        diagonal = (x, y) -> Math.sqrt(x*x + y*y);
    }
}

可以在记录类中声明实例方法,无论您是否实现自己的访问器方法。您还可以在记录类中声明嵌套类和接口,包括嵌套记录类(隐式静态)

record Rectangle(double length, double width) {

    // Nested record class
    record RotationAngle(double angle) {
        public RotationAngle {
            angle = Math.toRadians(angle);
        }
    }
    
    // Public instance method
    public Rectangle getRotatedRectangleBoundingBox(double angle) {
        RotationAngle ra = new RotationAngle(angle);
        double x = Math.abs(length * Math.cos(ra.angle())) +
                   Math.abs(width * Math.sin(ra.angle()));
        double y = Math.abs(length * Math.sin(ra.angle())) +
                   Math.abs(width * Math.cos(ra.angle()));
        return new Rectangle(x, y);
    }
}

不能在记录类中声明native方法。

记录类的特点

记录类是隐式final的,因此您不能显式扩展记录类。但是,除了这些限制之外,记录类的行为与普通类类似:

可以创建通用记录类,例如:

record Triangle<C extends Coordinate> (C top, C left, C right) { }

可以声明一个实现一个或多个接口的记录类,例如:

record Customer(...) implements Billable { }

可以注释记录类及其各个组件,例如:

import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface GreaterThanZero { }
record Rectangle(
    @GreaterThanZero double length,
    @GreaterThanZero double width) { }

如果您为记录组件添加注解,则该注解可能会传播到记录类的成员和构造函数。此传播由注解接口适用的上下文确定。在前面的示例中,@Target(ElementType.FIELD) 元注解表示 @GreaterThanZero 注解被传播到与记录组件对应的字段。因此,这个记录类声明将等效于以下普通类声明:

public final class Rectangle {
    private final @GreaterThanZero double length;
    private final @GreaterThanZero double width;
    
    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    
    double length() { return this.length; }
    double width() { return this.width; }
}

记录类以及密封类和接口

记录类与密封类和接口配合得很好。有关示例,请参阅将类记录为允许的子类。

局部记录类

局部记录类类似于局部类;它是在方法体内定义的记录类。

在下面的示例中,一个商户被建模为一个记录类Merchant。商户所做的销售也被建模为一个记录类Sale。Merchant和Sale都是顶级记录类。商户及其总月销售额的聚合被建模为一个局部记录类MonthlySales,它声明在findTopMerchants方法内部。这个局部记录类提高了后续流操作的可读性:

import java.time.*;
import java.util.*;
import java.util.stream.*;

record Merchant(String name) { }

record Sale(Merchant merchant, LocalDate date, double value) { }

public class MerchantExample {
    
    List<Merchant> findTopMerchants(
        List<Sale> sales, List<Merchant> merchants, int year, Month month) {
    
        // Local record class
        record MerchantSales(Merchant merchant, double sales) {}

        return merchants.stream()
            .map(merchant -> new MerchantSales(
                merchant, this.computeSales(sales, merchant, year, month)))
            .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
            .map(MerchantSales::merchant)
            .collect(Collectors.toList());
    }   
    
    double computeSales(List<Sale> sales, Merchant mt, int yr, Month mo) {
        return sales.stream()
            .filter(s -> s.merchant().name().equals(mt.name()) &&
                s.date().getYear() == yr &&
                s.date().getMonth() == mo)
            .mapToDouble(s -> s.value())
            .sum();
    }    

    public static void main(String[] args) {
        
        Merchant sneha = new Merchant("Sneha");
        Merchant raj = new Merchant("Raj");
        Merchant florence = new Merchant("Florence");
        Merchant leo = new Merchant("Leo");
        
        List<Merchant> merchantList = List.of(sneha, raj, florence, leo);
        
        List<Sale> salesList = List.of(
            new Sale(sneha,    LocalDate.of(2020, Month.NOVEMBER, 13), 11034.20),
            new Sale(raj,      LocalDate.of(2020, Month.NOVEMBER, 20),  8234.23),
            new Sale(florence, LocalDate.of(2020, Month.NOVEMBER, 19), 10003.67),
            // ...
            new Sale(leo,      LocalDate.of(2020, Month.NOVEMBER,  4),  9645.34));
        
        MerchantExample app = new MerchantExample();
        
        List<Merchant> topMerchants =
            app.findTopMerchants(salesList, merchantList, 2020, Month.NOVEMBER);
        System.out.println("Top merchants: ");
        topMerchants.stream().forEach(m -> System.out.println(m.name()));
    }
}

与嵌套记录类一样,本地记录类是隐式静态的,这意味着它们自己的方法无法访问封闭方法的任何变量,这与本地类不同,本地类永远不是静态的。

内部类的静态成员

在Java SE 16之前,除非成员是一个常量变量,否则您不能在内部类中声明显式或隐式的静态成员。这意味着内部类不能声明记录类成员,因为嵌套记录类是隐式静态的。

在Java SE 16及更高版本中,内部类可以声明显式或隐式静态的成员,包括记录类成员。以下示例演示了这一点:

public class ContactList {
    
    record Contact(String name, String number) { }
    
    public static void main(String[] args) {
        
        class Task implements Runnable {
            
            // Record class member, implicitly static,
            // declared in an inner class
            Contact c;
            
            public Task(Contact contact) {
                c = contact;
            }
            public void run() {
                System.out.println(c.name + ", " + c.number);
            }
        }        
        
        List<Contact> contacts = List.of(
            new Contact("Sneha", "555-1234"),
            new Contact("Raj", "555-2345"));
        contacts.stream()
                .forEach(cont -> new Thread(new Task(cont)).start());
    }
}

记录序列化

您可以序列化和反序列化记录类的实例,但是您无法通过提供writeObject、readObject、readObjectNoData、writeExternal或readExternal方法来定制这个过程。记录类的组件控制序列化,而记录类的典范构造函数控制反序列化。更多信息和一个扩展示例,请参阅Serializable Records。另请参阅Java对象序列化规范中的记录序列化部分。

与记录类相关的 API

抽象类java.lang.Record是所有记录类的共同超类。

如果您的源文件导入了一个名为Record的类,而这个类来自java.lang以外的包,您可能会收到编译器错误。Java源文件通过隐式的import java.lang.*;语句自动导入java.lang包中的所有类型。这包括java.lang.Record类,无论预览特性是否启用或禁用。

考虑以下com.myapp.Record类的声明:

package com.myapp;

public class Record {
    public String greeting;
    public Record(String greeting) {
        this.greeting = greeting;
    }
}

以下示例 org.example.MyappPackageExample 使用通配符导入 com.myapp.Record 但未编译:

package org.example;
import com.myapp.*;

public class MyappPackageExample {
    public static void main(String[] args) {
       Record r = new Record("Hello world!");
    }
}

编译器会生成类似于以下内容的错误消息:

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
       ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
                      ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

com.myapp包中的Record和java.lang包中的Record都被通配符导入。因此,没有一个类优先,当编译器遇到简单名称Record的使用时,会生成错误。

要使此示例编译通过,将import语句更改为导入Record的完全限定名称:

import com.myapp.Record;

注意:在java.lang包中引入类是罕见的,但有时是必要的,比如Java SE 5中的Enum、Java SE 9中的Module和Java SE 14中的Record。

java.lang.Class类有两个与记录类相关的方法:

RecordComponent[] getRecordComponents():返回一个java.lang.reflect.RecordComponent对象数组,这些对象对应于记录类的组件。

boolean isRecord():类似于isEnum(),但如果类被声明为记录类,则返回true。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表