Koleksiyonları güvenli bir şekilde nasıl kopyalayabilirim?


9

Geçmişte, bir koleksiyonu güvenli bir şekilde kopyalamayı şöyle söyledim:

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

veya

public static void doThing(NavigableSet<String> strs) {
    NavigableSet<String> newStrs = new TreeSet<>(strs);

Ancak bu "kopya" kurucuları, benzer statik oluşturma yöntemleri ve akışları gerçekten güvenli midir ve kurallar nerede belirtilmiştir? Güvenli bir şekilde , Java dili tarafından sunulan temel anlamsal bütünlük garantileri ve makul bir kişi tarafından desteklenen SecurityManagerve kusurların olmadığı varsayılarak kötü amaçlı bir arayan için uygulanan koleksiyonlar vardır.

Ben metod atma mutluyum ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, vb hatta belki asılı.

StringDeğişmez tipte bir argüman örneği olarak seçtim . Bu soru için, kendi gotcha'ları olan değişebilir tiplerin koleksiyonları için derin kopyalarla ilgilenmiyorum.

(Açık olmak gerekirse, ben OpenJDK kaynak koduna bakıp için cevap çeşit var ArrayListve TreeSet.)


2
Eğer neyi kastediyorsunuz güvenli ? Genel olarak, koleksiyon çerçevesindeki sınıflar, javadoklarda belirtilen istisnalar dışında benzer şekilde çalışma eğilimindedir. Kopya kurucuları diğer kurucular gibi "güvenlidir". Aklınızda olan belirli bir şey var mı, çünkü bir koleksiyon kopya kurucusunun güvenli olup olmadığını sormak çok özel geliyor mu?
Kayaman

1
Eh, NavigableSetve diğer Comparabletabanlı koleksiyonlar bazen bir sınıfın compareTo()doğru bir şekilde uygulanıp uygulanmadığını tespit eder ve bir istisna atar. Güvenilmeyen argümanlarla ne demek istediğiniz biraz net değil. Yani bir kötü adam kötü Dizeleri bir koleksiyon yapar ve bunları koleksiyonunuza kopyaladığınızda kötü bir şey olur mu? Hayır, koleksiyon çerçevesi oldukça sağlam, 1.2'den beri var.
Kayaman

1
Eğer kendi içlerinde, içine hack olmadan standart koleksiyonları bir çok tehlikeye atabilir @JesseWilson HashSet(ve genel olarak tüm diğer karma koleksiyonları) doğruluğu / bütünlüğü dayanıyor hashCodeelemanlarının uygulanması, TreeSetve PriorityQueuebağlı Comparator(ve hatta olamaz özel karşılaştırıcıyı kabul etmeden eşdeğer bir kopya oluştur) (varsa), derlemeden sonra hiçbir zaman doğrulanmayan EnumSetbelirli enumtürün bütünlüğüne güvenir , böylece üretilmeyen javacveya el işi olmayan bir sınıf dosyası onu bozabilir.
Holger

1
Senin örneklerde, var new TreeSet<>(strs)nerede strsbir olduğunu NavigableSet. Bu toplu bir kopya değildir, çünkü sonuç TreeSet, semantiği korumak için bile gerekli olan kaynağın karşılaştırıcısını kullanacaktır. Sadece içerilen elemanları işlemede iyi iseniz, toArray()gitmek için yoldur; yineleme sırasını bile koruyacaktır. “Elemanı al, elemanı doğrula, elemanı kullan” konusunda sorun yaşıyorsanız, bir kopya bile yapmanız gerekmez. Sorunlar, tüm öğeleri doğrulamak ve ardından tüm öğeleri kullanmak istediğinizde başlar. O zaman, TreeSetözel karşılaştırıcı w kopyalamaya güvenemezsiniz
Holger

1
checkcastHer öğe için a efektine sahip tek toplu kopyalama işlemi toArraybelirli bir türdedir. Biz her zaman bununla bitiyoruz. Genel koleksiyonlar, gerçek öğe türlerini bile bilmezler, bu nedenle kopya oluşturucuları benzer bir işlev sağlayamaz. Tabii ki, herhangi bir kontrolü doğru kullanımdan önce erteleyebilirsiniz, ancak sonra sorularınızın neyi amaçladığını bilmiyorum. Elemanları kullanmadan hemen önce kontrol etme ve hata verme konusunda iyi olduğunuzda "anlamsal bütünlük" e ihtiyacınız yoktur.
Holger

Yanıtlar:


12

Koleksiyon API'si gibi sıradan API'larda aynı JVM içinde çalışan kasıtlı olarak kötü amaçlı koda karşı gerçek bir koruma yoktur.

Kolayca gösterilebileceği gibi:

public static void main(String[] args) throws InterruptedException {
    Object[] array = { "foo", "bar", "baz", "and", "another", "string" };
    array[array.length - 1] = new Object() {
        @Override
        public String toString() {
            Collections.shuffle(Arrays.asList(array));
            return "string";
        }
    };
    doThing(new ArrayList<String>() {
        @Override public Object[] toArray() {
            return array;
        }
    });
}

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}
made a safe copy [foo, bar, baz, and, another, string]
[bar, and, string, string, another, foo]
[and, baz, bar, string, string, string]
[another, baz, and, foo, bar, string]
[another, bar, and, foo, string, and]
[another, baz, string, another, and, foo]
[string, and, another, foo, string, foo]
[baz, string, foo, and, baz, string]
[bar, another, string, and, another, baz]
[bar, string, foo, string, baz, and]
[bar, string, bar, another, and, foo]

Gördüğünüz gibi, bir beklemek List<String>aslında bir Stringörnek listesi almayı garanti etmez . Tür silme ve ham türler nedeniyle, liste uygulama tarafında bir düzeltme bile mümkün değildir.

Yapıcısını suçlayabileceğiniz bir diğer şey ArrayListde, gelen koleksiyonun toArrayuygulanmasına duyulan güvendir . TreeMapaynı şekilde etkilenmez, ancak bir dizinin yapımında olduğu gibi, diziyi geçmekten böyle bir performans kazancı olmadığı için ArrayList. Her iki sınıf da yapıcıda bir korumayı garanti etmez.

Normalde, her köşede kasıtlı olarak kötü amaçlı kod olduğunu varsayarak kod yazmaya çalışmanın bir anlamı yoktur. Her şeye karşı korumak için yapabileceği çok şey var. Bu tür bir koruma, yalnızca kötü niyetli bir kişinin bir şeye erişmesine neden olabilecek bir eylemi gerçekten içine alan, bu kod olmadan erişemeyen kod için kullanışlıdır.

Belirli bir kod için güvenliğe ihtiyacınız varsa,

public static void doThing(List<String> strs) {
    String[] content = strs.toArray(new String[0]);
    List<String> newStrs = new ArrayList<>(Arrays.asList(content));

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}

Ardından, newStrsyalnızca dizeler içerdiğinden ve yapıldıktan sonra diğer kodlarla değiştirilemeyeceğinden emin olabilirsiniz.

Veya List<String> newStrs = List.of(strs.toArray(new String[0]));Java 9 veya daha yeni bir sürümle kullanın Java
10'ların List.copyOf(strs)aynı şeyi yaptığını unutmayın, ancak belgelerinde gelen koleksiyonun toArrayyöntemine güvenmediği garanti edilmez . Bu nedenle List.of(…), dizi tabanlı bir liste döndürmesi durumunda kesinlikle bir kopya yapacak olan arama yapmak daha güvenlidir.

Hiçbir arayan yolu değiştiremediğinden, diziler çalışır, gelen koleksiyonu bir diziye döker ve ardından yeni koleksiyonu onunla doldurur, her zaman kopyayı güvenli hale getirir. Koleksiyon, yukarıda gösterildiği gibi döndürülen diziye bir başvuru taşıyabildiğinden, kopyalama aşamasında bu diziyi değiştirebilir, ancak koleksiyondaki kopyayı etkileyemez.

Bu nedenle, herhangi bir tutarlılık kontrolü, belirli bir eleman diziden veya bir bütün olarak ortaya çıkan koleksiyonda alındıktan sonra yapılmalıdır.


2
Java'nın güvenlik modeli, yığındaki tüm kodların izin kümelerinin kesişimine kod vererek çalışır, bu nedenle kodunuzun arayanı kodunuzu istenmeyen şeyler yaptığında, başlangıçta olduğundan daha fazla izin almaz. Bu nedenle, kodunuz, kodunuz olmadan da kötü amaçlı kodun yapabileceği şeyleri yapar. Sadece yükseltilmiş ayrıcalıklarla çalıştırmak istediğiniz kodu sertleştirmeniz gerekirAccessController.doPrivileged(…) vb . Ancak uygulama güvenliği ile ilgili hataların uzun listesi bize bu teknolojinin neden terk edildiğine dair bir ipucu veriyor…
Holger

1
Ama cevapta odaklandığım gibi “Koleksiyon API'sı gibi sıradan API'lara” eklemeliydim.
Holger

2
Görünüşe göre güvenlikle ilgili olmayan kodunuzu, kötü amaçlı bir koleksiyon uygulamasının kaymasına izin veren ayrıcalıklı koda karşı neden sertleştirmelisiniz? Bu varsayımsal arayan, kodunuzu çağırmadan önce ve sonra yine de kötü niyetli davranışlara maruz kalır. Kodunuzun doğru davranan tek kod olduğunu bile fark etmez. new ArrayList<>(…)Kopya oluşturucu olarak kullanmak , doğru toplama uygulamaları olduğunu varsayar. Çok geç olduğunda güvenlik sorunlarını düzeltmek sizin göreviniz değildir. Güvenliği aşılan donanım ne olacak? İsletim sistemi? Çoklu diş açmaya ne dersiniz?
Holger

2
Gerçeklerden sonra bozuk bir ortamı düzeltmeye çalışmak yerine “güvenlik yok” değil, doğru yerlerde güvenlik savunuyorum. “Süper tiplerini doğru bir şekilde uygulamayan birçok koleksiyon var ” ama ilginç bir iddia, kanıtlar istemek, bunu daha da genişletmek için çok ileri gitti. Orijinal soru tamamen cevaplanmıştır; şimdi getirdiğiniz noktalar asla onun bir parçası değildi. Dediğim gibi, List.copyOf(strs)gelen koleksiyonun bu konuda, açık bir fiyata, doğruluğuna güvenmez. ArrayListgünlük mantıklı bir uzlaşmadır.
Holger

4
Tüm “benzer statik oluşturma yöntemleri ve akışları” için böyle bir spesifikasyon olmadığını açıkça belirtiyor. Bu nedenle, kesinlikle güvende olmak istiyorsanız, toArray()kendinizi çağırmanız gerekir , çünkü diziler geçersiz kılınan davranışa sahip olamaz ve ardından dizinin bir toplama kopyası gibi new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))veya oluşturamaz List.of(strs.toArray(new String[0])). Her ikisinin de eleman türünü zorlamanın yan etkisi vardır. Ben şahsen, copyOfdeğişmez koleksiyonları tehlikeye atmaya izin vereceklerini düşünmüyorum , ama cevapta alternatifler var.
Holger

1

Bu bilgiyi yorumda bırakmayı tercih ederim, ama yeterince üne sahip değilim, üzgünüm :) O zaman yapabildiğim kadar ayrıntılı anlatmaya çalışacağım.

constNesne içeriğini değiştirmesi gerekmeyen üye işlevlerini işaretlemek için C ++ 'da kullanılan değiştirici gibi bir şey yerine, Java'da başlangıçta "değişmezlik" kavramı kullanılmıştır. Kapsülleme (veya OCP, Açık-Kapalı Prensibi) bir nesnenin beklenmedik mutasyonlarına (değişikliklerine) karşı koruma sağlamalıdır. Elbette yansıma API'sı bu konuda yürüyor; doğrudan bellek erişimi aynı şeyi yapar; Bu daha çok kendi bacağını vurmakla ilgili :)

java.util.Collectionkendisi değiştirilebilir bir arayüzdür: addkoleksiyonu değiştirmesi gereken bir yöntemi vardır. Tabii ki programcı koleksiyonu atacak bir şeye sarabilir ... ve tüm çalışma zamanı istisnaları gerçekleşir çünkü başka bir programcı koleksiyonun değişmez olduğunu açıkça söyleyen javadoc'u okuyamadı.

java.util.IterableArabirimlerimdeki değişmez koleksiyonu göstermek için yazı tipini kullanmaya karar verdim . Anlamsal Iterableolarak "değişebilirlik" gibi bir toplama özelliği yoktur. Yine de (büyük olasılıkla) altta yatan koleksiyonları akışlar aracılığıyla değiştirebileceksiniz.


JIC, haritaları değişmez bir şekilde ortaya çıkarmak java.util.Function<K,V>için kullanılabilir (haritanın getyöntemi bu tanıma uyar)


Salt okunur arayüzler ve değişmezlik kavramları dikeydir. C ++ ve C'nin anlamı semantik bütünlüğü desteklememesidir . Ayrıca kopya nesne / struct argümanları - const & bunun için tehlikeli bir optimizasyon. Eğer Iteratoro zaman pratik olarak elementwise bir kopyayı zorlarsanız, ama bu hoş değil. forEachRemaining/ forEachKomutunu kullanmak tam bir felaket olacaktır. (Bunun Iteratorbir removeyöntemi olduğunu da belirtmeliyim .)
Tom Hawtin - tackline

Scala koleksiyon kütüphanesine bakarsanız değişebilir ve değişmez arabirimler arasında katı bir ayrım vardır. Yine de (sanırım) tamamen farklı nedenlerden dolayı yapıldı, ancak yine de güvenliğin nasıl sağlanabileceğinin bir göstergesidir. Salt okunur arabirim anlamsal olarak değişmezliği varsayar, bunu söylemeye çalışıyorum. (Ben Iterableaslında değişmez değil konusunda katılıyorum , ama herhangi bir sorun görmüyorum forEach*)
Alexander
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.