Java 14 kayıtları ve dizileri


11

Aşağıdaki kod verildiğinde:

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
}

O dizinin, belli ki, görünüyor toString, equalsyöntemler (yerine statik yöntemler kullanılır Arrays::equals, Arrays::deepEquals ya da Array::toString).

Yani Java 14 Kayıtları ( JEP 359 ) dizilerle çok iyi çalışmıyor, ilgili yöntemlerin bir IDE (en azından IntelliJ'de, varsayılan olarak "yararlı" yöntemler üreten, yani statik yöntemleri kullandıkları bir IDE ile oluşturulması gerekir içinde Arrays).

Yoksa başka bir çözüm var mı?


3
ListDizi yerine kullanmaya ne dersiniz ?
Oleg

Neden böyle yöntemler bir IDE ile oluşturulması gerektiğini anlamıyorum? Tüm yöntemler elle üretilebilmelidir.
NomadMaker

1
toString(), equals()Ve hashCode()bir kayıt yöntemleri uygulanmaktadır bir invokedynamic referans kullanarak. . Arrays.deepToStringGünümüzde yalnızca derlenmiş sınıf eşdeğeri, yöntemin aşırı yüklenmiş özel yönteminde ne yaptığına yakın olsaydı , ilkel durumlar için çözülmüş olabilir .
Naman

1
Uygulama için tasarım seçimini ikincisi olarak, bu yöntemlerin geçersiz kılınmış bir uygulamasını sağlamayan bir şey için Object, kullanıcı tanımlı sınıflarda da olabileceğinden geri dönmek kötü bir fikir değildir . örneğin yanlış eşittir
Naman

1
@Naman Kullanım seçiminin invokedynamicsemantik seçimiyle hiçbir ilgisi yoktur; indy burada saf bir uygulama detayı. Derleyici aynı şeyi yapmak için bayt kodu yayınlayabilirdi; bu sadece oraya ulaşmak için daha verimli ve esnek bir yol oldu. Kayıtların tasarımı sırasında daha nüanslı bir eşitlik anlambilimi (diziler için derin eşitlik gibi) kullanıp kullanmayacağı kapsamlı bir şekilde tartışıldı, ancak bu, tahmin edilenden daha fazla soruna yol açtığı ortaya çıktı.
Brian Goetz

Yanıtlar:


18

Java dizileri kayıtlar için çeşitli zorluklar yaratır ve bunlar tasarıma bir takım kısıtlamalar ekler. Diziler değiştirilebilir ve eşitlik semantikleri (Object'den miras alınan) içerikle değil kimlikle ilgilidir.

Örneğinizin temel sorunu equals()dizilerde referans eşitliği değil, içerik eşitliği olmasını istemenizdir . equals()Kayıtlar için (varsayılan) semantik , bileşenlerin eşitliğine dayanır; senin örnekte, iki Fooayrı dizileri içeren kayıtlar vardır farklı ve kayıt düzgün davranıyor. Sorun şu ki, sadece eşitlik karşılaştırması farklı olsaydı.

Bununla birlikte, istediğiniz semantikle bir kayıt beyan edebilirsiniz, sadece daha fazla iş gerektirir ve çok fazla iş olduğunu hissedebilirsiniz. İşte istediğinizi yapan bir kayıt:

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)); }
}

Bunun yaptığı şey, (kurucuda) ve çıkış yolunda (erişimci içinde) ve aynı zamanda dizinin içeriğini kullanmak için eşitlik semantiğini ayarlayan savunma kopyasıdır. Bu, üst sınıfta gerekli java.lang.Recordolan "bileşenlerine bir kayıt ayırmak ve bileşenleri yeni bir kayda yeniden yapılandırmak", eşit bir kayıt sağlayan " değişmezi destekler ."

"Ama bu çok fazla iş, kayıtları kullanmak istedim, bu yüzden hepsini yazmak zorunda kalmadım" diyebilirsiniz. Ancak, kayıtlar temel olarak sözdizimsel bir araç değildir (sözdizimsel olarak daha hoş olsalar da), semantik bir araçtır: kayıtlar nominal tupllerdir . Çoğu zaman, kompakt sözdizimi de istenen semantiği verir, ancak farklı semantikler istiyorsanız, biraz fazladan iş yapmanız gerekir.


6
Ayrıca, hiç kimsenin referans olarak dizi eşitliği istemediğini varsaymak, dizi eşitliğinin içeriklere göre olmasını isteyen insanlar tarafından yaygın bir hatadır . Ancak bu doğru değildir; sadece tüm durumlar için çalışan tek bir cevap yoktur. Bazen referans eşitliği tam olarak istediğiniz şeydir.
Brian Goetz

9

List< Integer > geçici çözüm

Çözüm: kullanın Listve Integernesne ( List< Integer >) yerine ilkel dizisi ( int[]).

Bu örnekte, Java 9'a eklenen özelliği kullanarak değiştirilemeyen bir sınıf değiştirilmemiş listesini somutlaştırıyorum . Bir dizi tarafından desteklenen değiştirilebilir bir liste için List.ofde kullanabilirsiniz 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
    }
}

Bir dizi üzerinden Liste'yi seçmek her zaman iyi bir fikir değildir ve bunu yine
Naman

@Naman asla “iyi fikir” olma iddiasında bulunmadım. Soru tam anlamıyla başka çözümler istedi. Bir tane verdim. Bağlantı kurduğunuz yorumlardan bir saat önce bunu yaptım.
Basil Bourque

5

Geçici çözüm: BirIntArraysınıfoluşturunveint[].

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

Mükemmel değil, çünkü şimdi foo.getInts()yerine aramak zorundasınız foo.ints(), ancak diğer her şey istediğiniz gibi çalışıyor.

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);
    }
}

Çıktı

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

1
Bu, böyle bir durumda bir kayıt değil, bir sınıf kullanmayı söylemenin karşılığı değil mi?
Naman

1
@Naman Hiç de değil, çünkü recordbirçok alandan oluşabilir ve yalnızca dizi alanları bu şekilde sarılır.
Andreas

Yeniden kullanılabilirlik açısından değinmeye çalıştığınız noktayı anlıyorum, ancak Listburada önerilen çözümle arayabileceğiniz bu tür bir sargı sağlayan dahili sınıflar var. Yoksa bu tür kullanım durumları için bir ek yük olabileceğini düşünüyor musunuz?
Naman

3
@Naman Birini saklamak istiyorsanız int[], a List<Integer>en az iki nedenden ötürü aynı değildir: 1) Liste çok daha fazla bellek kullanır ve 2) Aralarında yerleşik bir dönüşüm yoktur. Integer[]🡘 List<Integer>oldukça kolaydır ( toArray(...)ve Arrays.asList(...)), ancak int[]🡘 List<Integer>daha fazla çalışma gerektirir ve dönüşüm zaman alır, bu yüzden her zaman yapmak istemediğiniz bir şeydir. Bir varsa int[]ve (rekor kullanmak aksi neden diğer şeylerle) rekor depolamak istiyor ve bir olarak ihtiyaç int[]o zaman gerekir her zaman dönüştürme, bu yanlış.
Andreas

Gerçekten iyi bir nokta. Eğer izin verirseniz olsa kusur arama faydaları hala göremediğimiz ne soran olabilir Arrays.equals, Arrays.toStringüzerinde Listdeğiştirirken kullanılan uygulanmasında int[]diğer yanıtında önerildiği gibi. (Her ikisinin de zaten geçici çözüm olduğunu varsayarsak.)
Naman
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.