【问题标题】:Java 14 records and arraysJava 14 记录和数组
【发布时间】:2020-07-30 08:11:00
【问题描述】:

给定以下代码:

public static void main(String[] args) {
    record Foo(int[] ints){}

    var ints = new int[]{1, 2};
    var foo = new Foo(ints);
    System.out.println(foo); // Foo[ints=[I@6433a2]
    System.out.println(new Foo(new int[]{1,2}).equals(new Foo(new int[]{1,2}))); // false
    System.out.println(new Foo(ints).equals(new Foo(ints))); //true
    System.out.println(foo.equals(foo)); // true
}

显然,使用了数组的toStringequals 方法(而不是静态方法,Arrays::equalsArrays::deepEqualsArray::toString)。

所以我猜 Java 14 记录 (JEP 359) 不能很好地处理数组,必须使用 IDE 生成相应的方法(至少在 IntelliJ 中,默认情况下会生成“有用”的方法,即它们使用Arrays 中的静态方法)。

或者有其他解决方案吗?

【问题讨论】:

  • List代替数组怎么样?
  • 我不明白为什么必须使用 IDE 生成这些方法?所有方法都应该可以手动生成。
  • 记录的toString(),equals()hashCode()方法实现using an invokedynamic reference.。如果只有编译后的类等价物可以更接近 Arrays.deepToString 今天在其私有重载方法中所做的,它可能已经解决了原始情况。
  • 第二个实现的设计选择,对于不提供这些方法的覆盖实现的东西,回退到Object 不是一个坏主意,因为这可能发生在用户定义的类中好吧。例如incorrect equals
  • @Naman 使用invokedynamic的选择与语义的选择完全无关; indy 在这里是一个纯粹的实现细节。编译器可以发出字节码来做同样的事情;这只是到达那里的一种更有效和更灵活的方式。在记录设计过程中广泛讨论了是否使用更细微的相等语义(例如数组的深度相等),但结果证明这导致的问题比预期的要多得多。

标签: java arrays java-14 java-record


【解决方案1】:

Java 数组对记录提出了一些挑战,这些挑战给设计增加了许多限制。数组是可变的,它们的相等语义(从 Object 继承)是通过标识,而不是内容。

您的示例的基本问题是您希望数组上的equals() 意味着内容相等,而不是引用相等。记录的equals() 的(默认)语义基于组件的相等性;在您的示例中,包含不同数组的两个 Foo 记录 不同,并且该记录的行为正确。问题是你只是希望平等比较不同。

也就是说,你可以用你想要的语义声明一个记录,只是需要更多的工作,你可能会觉得工作太多了。这是一个可以满足您要求的记录:

record Foo(String[] ss) {
    Foo { ss = ss.clone(); }
    String[] ss() { return ss.clone(); }
    boolean equals(Object o) { 
        return o instanceof Foo 
            && Arrays.equals(((Foo) o).ss, ss);
    }
    int hashCode() { return Objects.hash(Arrays.hashCode(ss)); }
}

它的作用是在传入(在构造函数中)和在传出(在访问器中)的途中进行防御性副本,以及调整相等语义以使用数组的内容。这支持超类java.lang.Record 中所需的不变量,即“将记录拆分为其组件,并将组件重新构建为新记录,从而产生相等的记录。”

您可能会说“但工作量太大,我想使用记录,所以我不必输入所有这些东西。”但是,记录主要不是一种句法工具(尽管它们在句法上更令人愉悦),它们是一种语义工具:记录是名义元组。大多数时候,紧凑的语法也会产生所需的语义,但如果你想要不同的语义,你必须做一些额外的工作。

【讨论】:

  • 此外,希望数组相等是通过内容来假设没有人希望数组通过引用相等是一个常见的错误。但这根本不是真的。只是没有一个适用于所有情况的答案。有时引用相等正是您想要的。
  • Objects.hash() in Objects.hash(Arrays.hashCode(ss)) 是否必要/有用?我想Arrays.hashCode(ss) 就足够了,对吧?
  • @jcsahnwaldtReinstateMonica Records 通常有多个组件;为每个组件计算一个哈希值,然后组合这些哈希值(例如与Objects.hash(Object...))是明智的。看起来很奇怪,因为这里只有一个组件。
【解决方案2】:

List< Integer > 解决方法

解决方法:使用 ListInteger 对象 (List< Integer >) 而不是基元数组 (int[])。

在此示例中,我使用 Java 9 中添加的 List.of 功能实例化了一个未指定类的 unmodifiable list。您也可以使用 ArrayList,作为由数组支持的可修改列表。

package work.basil.example;

import java.util.List;

public class RecordsDemo
{
    public static void main ( String[] args )
    {
        RecordsDemo app = new RecordsDemo();
        app.doIt();
    }

    private void doIt ( )
    {

        record Foo(List < Integer >integers)
        {
        }

        List< Integer > integers = List.of( 1 , 2 );
        var foo = new Foo( integers );

        System.out.println( foo ); // Foo[integers=[1, 2]]
        System.out.println( new Foo( List.of( 1 , 2 ) ).equals( new Foo( List.of( 1 , 2 ) ) ) ); // true
        System.out.println( new Foo( integers ).equals( new Foo( integers ) ) ); // true
        System.out.println( foo.equals( foo ) ); // true
    }
}

【讨论】:

  • 虽然在数组上选择 List 并不总是一个好主意,我们是 discussing this anyway.
  • @Naman 我从来没有声称自己是一个“好主意”。该问题从字面上要求其他解决方案。我提供了一个。我在你链接的 cmets 前一个小时这样做了。
  • 为确保无法使用可变列表创建记录,您应该在记录构造函数中执行List.copyOfcopyOf 只会在必要时创建不可变副本,如果列表已经不可变,则为空操作。
【解决方案3】:

解决方法:创建一个IntArray 类并包装int[]

record Foo(IntArray ints) {
    public Foo(int... ints) { this(new IntArray(ints)); }
    public int[] getInts() { return this.ints.get(); }
}

并不完美,因为您现在必须调用 foo.getInts() 而不是 foo.ints(),但其他一切都按照您想要的方式进行。

public final class IntArray {
    private final int[] array;
    public IntArray(int[] array) {
        this.array = Objects.requireNonNull(array);
    }
    public int[] get() {
        return this.array;
    }
    @Override
    public int hashCode() {
        return Arrays.hashCode(this.array);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        IntArray that = (IntArray) obj;
        return Arrays.equals(this.array, that.array);
    }
    @Override
    public String toString() {
        return Arrays.toString(this.array);
    }
}

输出

Foo[ints=[1, 2]]
true
true
true

【讨论】:

  • 在这种情况下,这不等于告诉使用类而不是记录吗?
  • @Naman 完全没有,因为您的record 可能包含许多字段,并且只有数组字段是这样包装的。
  • 我明白您在可重用性方面试图提出的观点,但是还有诸如List 之类的内置类,它们提供了人们可能会在此处提出的解决方案中寻求的那种包装。或者您认为这可能是此类用例的开销?
  • @Naman 如果你想存储一个int[],那么一个List&lt;Integer&gt;就不一样了,至少有两个原因:1)列表使用了更多的内存,2)有它们之间没有内置的转换。 Integer[] ? List&lt;Integer&gt; 相当容易(toArray(...)Arrays.asList(...)),但是 int[] ? List&lt;Integer&gt; 需要更多的工作,并且转换需要时间,所以这是你不想做的所有事情时间。如果你有一个int[] 并且想将它存储在一个记录中(与其他东西一起,否则为什么要使用记录),并且需要它作为int[],那么每次需要它时都转换是错误的。
  • 确实不错。如果您允许挑剔,我可以询问Arrays.equalsArrays.toString 在替换 int[] 时使用的 List 实现有什么好处,如其他答案中所建议的那样。 (假设这两个都是解决方法。)
猜你喜欢
  • 1970-01-01
  • 2020-08-02
  • 1970-01-01
  • 2015-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-01
  • 2021-10-22
相关资源
最近更新 更多