Java 14记录和数组


11

给出以下代码:

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 Records(JEP 359)在数组上不能很好地工作,相应的方法必须由IDE生成(至少在IntelliJ中,默认情况下会生成“有用的”方法,即它们使用静态方法)在Arrays)。

还是有其他解决方案?


3
如何使用List而不是数组?
奥列格

我不明白为什么必须使用IDE生成此类方法?所有方法都应该可以手工生成。
NomadMaker

1
toString()equals()hashCode()记录的方法来实现使用invokedynamic参考。。如果仅当量的编译类等效项可以更接近该方法Arrays.deepToString在其私有重载方法中所做的工作,那么它可能已经解决了原始情况。
纳曼

1
对于实现的第二个设计选择,对于没有提供这些方法的重写实现的某些事物,退回到Object,这不是一个坏主意,因为用户定义的类也可能发生这种情况。例如,不正确的等于
Naman

1
@Naman使用invokedynamic的选择与语义的选择完全无关;indy在这里是纯实现细节。编译器可能发出了字节码来执行相同的操作;这只是一种更有效,更灵活的方法。在记录的设计过程中,是否使用更细微的相等语义(例如数组的深度相等)进行了广泛的讨论,但是事实证明,这会导致更多的问题。
Brian Goetz

Answers:


18

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即“将记录分解成其组成部分,并将这些组成部分重构为新记录,产生相等的记录”。

您可能会说:“但这工作太多了,我想使用记录,因此不必键入所有内容。” 但是,记录主要不是语法工具(尽管它们在语法上更令人愉快),但它们是语义工具:记录是名义元组。大多数时候,紧凑语法还会产生所需的语义,但是如果您想要不同的语义,则必须做一些额外的工作。


6
同样,那些希望数组相等于按内容的人普遍认为,没有人希望通过引用来获得数组相等是一个普遍的错误。但这根本不是真的。只是没有一个答案适用于所有情况。有时引用相等正是您想要的。
Brian Goetz

9

List< Integer > 解决方法

解决方法:使用一个ListInteger对象(List< Integer >),而不是基元的阵列(int[])。

在此示例中,我通过使用Java 9中添加的功能来实例化指定类的不可修改列表List.of。您也可以将用作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而不是数组并不总是一个好主意,无论如何
纳曼

@Naman我从未宣称自己是一个“好主意”。该课题从字面上要求其他解决方案。我提供了一个。在您链接的评论之前一个小时,我就这样做了。
罗勒·布尔克

5

解决方法:创建一个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

1
在这种情况下,这不等于告诉使用类而不是记录吗?
纳曼

1
@Naman完全没有,因为您record可能包含许多字段,并且仅将数组字段这样包装。
安德里亚斯

我明白了您要提出的可重用性,但是然后有一些内置的类,例如此类,List它们提供了一种包装,可能有人会用此处提出的解决方案来寻求这种包装。还是您认为这可能是此类用例的开销?
纳曼

3
@Naman如果要存储an int[],则a List<Integer>不相同,至少有两个原因:1)列表使用了更多的内存,并且2)它们之间没有内置的转换。Integer[]🡘 List<Integer>非常容易(toArray(...)Arrays.asList(...)),但是int[]🡘 List<Integer>需要做更多的工作,并且转换需要花费时间,因此您不想一直这样做。如果您有个,int[]并且想要将其存储在记录中(与其他内容一起使用,否则为什么要使用记录),并且需要将其作为int[],那么每次需要时进行转换都是错误的。
安德里亚斯

确实很好。但是,如果您允许挑剔地询问,与替换其他答案中所建议的实现所使用的实现相比Arrays.equals,我们仍然能从中看到什么好处。(无论如何,假设这都是解决方法。)Arrays.toStringListint[]
Naman
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.