Map.get (Nesne anahtarı) 'nın (tam) genel olmamasının nedenleri nelerdir?


405

Arayüzünde tam jenerik bir alma yöntemine sahip olmama kararının arkasındaki nedenler nelerdir java.util.Map<K, V>.

Soruyu açıklığa kavuşturmak için yöntemin imzası

V get(Object key)

onun yerine

V get(K key)

ve nedenini merak ediyorum (aynı şey için remove, containsKey, containsValue).




1
İnanılmaz. 20 yıldan fazla bir süredir Java kullanıyorum ve bugün bu sorunu fark ediyorum.
GhostCat

Yanıtlar:


260

Başkaları tarafından belirtildiği gibi get(), vb. Nedenlerin genel olmaması, çünkü aldığınız girdinin anahtarının ilettiğiniz nesne ile aynı tür olması gerekmez get(); yöntemin spesifikasyonu sadece eşit olmalarını gerektirir. Bu, equals()yöntemin yalnızca nesneyle aynı türden değil, bir Nesne parametresiyle nasıl alındığından kaynaklanır .

Çoğu sınıfın equals(), nesnelerinin yalnızca kendi sınıfındaki nesnelere eşit olabilmesi için tanımladığı genel olarak doğru olsa da, Java'da durumun böyle olmadığı birçok yer vardır. Örneğin, için belirtim List.equals(), iki List nesnesinin hem Listeler hem de farklı uygulamaları olsa bile aynı içeriğe sahip olması durumunda eşit olduğunu belirtir List. Bu nedenle, bu sorudaki örneğe geri dönersek, yöntemin spesifikasyonuna göre bir Map<ArrayList, Something>ve benim için get()bir LinkedListargüman olarak çağırmak mümkündür ve aynı içeriğe sahip bir liste olan anahtarı almalıdır. Bu get(), genel ve argüman türünü kısıtlamış olsaydı mümkün olmazdı .


28
O zaman neden V Get(K k)C # 'da?

134
Soru şu ki, aramak istiyorsanız m.get(linkedList)neden mtürünü tanımlamıyorsunuz Map<List,Something>? Bir arabirim almak m.get(HappensToBeEqual)için Maptürü değiştirmeden arama mantıklı bir usecase düşünemiyorum .
Elazar Leibovich

58
Vay, ciddi tasarım hatası. Derleyici uyarısı almıyorsunuz, berbat. Elazar ile aynı fikirdeyim. Bu gerçekten yararlı, hangi sık sık olur şüpheliyim, bir getByEquals (Nesne anahtarı) daha makul geliyor ...
mmm

37
Bu karar pratiklikten ziyade teorik saflık temelinde yapılmış gibi görünüyor. Kullanımların çoğunda, geliştiriciler, yanıtında newacct tarafından belirtilen gibi son durumları desteklemenin sınırsız olmasını sağlamaktan ziyade şablon türüyle sınırlı argümanı görmeyi tercih ederler. Geçici olmayan imzaları bırakmak, çözdüğünden daha fazla sorun yaratır.
Sam Goldberg

14
@newacct: “mükemmel tipte güvenli”, çalışma zamanında tahmin edilemeyen bir yapı için güçlü bir iddiadır. Görüşünüzü bununla çalışan karma haritalarla daraltmayın. yönteme TreeMapyanlış türde nesneler ilettiğinizde başarısız olabilir, getancak zaman zaman geçebilir, örneğin harita boş olduğunda. Ve daha da kötüsü, bir durumunda verilen yöntemi (genel imzası vardır!) Herhangi kontrolsüz uyarmadan yanlış türde argümanlar ile adlandırılabilir. Bu ise kırık davranış. Comparatorcompare
Holger

105

Google'da harika bir Java kodlayıcısı olan Kevin Bourrillion, bir süre önce bir blog yayınında tam olarak bu konuyu yazdı (kuşkusuz Setyerine bağlamında Map). En alakalı cümle:

Tekdüze olarak, Java Koleksiyonlar Çerçevesi (ve Google Koleksiyonlar Kütüphanesi) yöntemleri, koleksiyonun bozulmasını önlemek gerektiğinde parametrelerinin türlerini asla kısıtlamaz.

Ben tamamen bir ilke olarak kabul emin değilim - .NET, örneğin doğru anahtar türü gerektiren iyi görünüyor - ama blog yazı akıl yürütme izlemeye değer. (.NET'ten bahsettikten sonra, .NET'te bir sorun olmamasının nedeninin bir kısmının .NET'te daha sınırlı varyansın daha büyük bir sorunu olduğu açıklamakta fayda var ...)


4
Kıyamet: bu doğru değil, durum hala aynı.
Kevin Bourrillion

9
@ user102008 Hayır, gönderi yanlış değil. A Integerve a Doubleasla birbirine eşit olmasa da , a'nın Set<? extends Number>değeri içerip içermediğini sormak hala adil bir sorudur new Integer(5).
Kevin Bourrillion

33
Ben asla bir kez üyelik kontrol etmek istedim Set<? extends Foo>. Çok sık bir haritanın anahtar türünü değiştirdim ve sonra derleyici kodun güncellenmesi gereken tüm yerleri bulamadığı için sinirli. Bunun doğru bir çıkış olduğu konusunda gerçekten ikna olmadım.
Porculus

4
@EarthEngine: Her zaman kırıldı. Bütün mesele bu - kod bozuldu, ancak derleyici onu yakalayamıyor.
Jon Skeet

1
Ve hala kırık ve sadece bir hataya neden oldu ... harika bir cevap.
GhostCat

28

Sözleşme şöyle ifade edilir:

Daha resmi olarak, bu harita k anahtarından v değerine bir eşleme içeriyorsa (key == null? K == null: key.equals (k) ), bu yöntem v döndürür; aksi takdirde null değerini döndürür. (Bu tür haritalardan en fazla biri olabilir.)

(benim vurgum)

ve bu nedenle, başarılı bir anahtar araması, giriş anahtarının eşitlik yöntemini uygulamasına bağlıdır. Bu mutlaka k sınıfına bağlı değildir .


4
Ayrıca bağımlıdır hashCode(). HashCode () yönteminin düzgün bir şekilde uygulanması olmadan equals(), bu durumda güzel bir şekilde uygulanan oldukça kullanışsızdır.
rudolfson

5
Prensip olarak, bu, tüm anahtarı yeniden oluşturmak pratik olmazsa, bir anahtar için hafif bir proxy kullanmanıza izin verir - equals () ve hashCode () doğru şekilde uygulandığı sürece.
Bill Michell

5
@rudolfson: Bildiğim kadarıyla, doğru grubu bulmak için karma koduna yalnızca bir HashMap güveniyor. Örneğin, bir TreeMap ikili arama ağacı kullanır ve hashCode () ile ilgilenmez.
Rob

4
Açıkçası, kişiyi tatmin get()etmek için bir tür argüman almasına gerek yoktur Object. Get yönteminin anahtar türüyle sınırlı olduğunu düşünün K- sözleşme yine de geçerli olacaktır. Tabii ki, derleme zaman türünün bir alt sınıfının olmadığı kullanımlar Kartık derlenemez, ancak sözleşmeler kod derlenirse ne olduğunu dolaylı olarak tartıştığından sözleşmeyi geçersiz kılmaz.
BeeOnRope

16

Postel Yasasının bir uygulamasıdır , "yaptığınız işte muhafazakar olun, başkalarından kabul ettiğiniz şeylerde liberal olun."

Türüne bakılmaksızın eşitlik kontrolleri yapılabilir; equalsyöntem tanımlanır Objectsınıfı ve herhangi bir kabul Objectparametre olarak. Bu nedenle, herhangi bir Objecttürü kabul etmek, anahtar denkliği ve anahtar denkliğine dayalı işlemler için anlamlıdır .

Bir harita anahtar değerleri döndürdüğünde, type parametresini kullanarak olabildiğince fazla tür bilgisini korur.


4
O zaman neden V Get(K k)C # 'da?

1
O var V Get(K k)o da mantıklı çünkü C #. Java ve .NET yaklaşımları arasındaki fark gerçekten sadece eşleşmeyen şeyleri engelleyen şeydir. C # 'da derleyici, Java' da koleksiyon. Bir süredir .NET'in tutarsız koleksiyon sınıfları kez yaklaşık öfke, ancak Get()ve Remove()ancak kesinlikle eşleşen bir türü kabul yanlışlıkla yanlış bir değer geçirerek engeller.
Wormbo

26
Postel Yasasının yanlış uygulanmasıdır. Başkalarından kabul ettiğiniz şeylerde liberal olun, ama çok liberal değil. Bu aptalca API, "koleksiyonda değil" ile "statik bir yazma hatası yaptınız" arasındaki farkı anlayamayacağınız anlamına gelir. Binlerce kayıp programcı saati get: K -> boolean ile engellenmiş olabilir.
Yargıç Zihinsel

1
Tabii ki böyle olmalıydı contains : K -> boolean.
Yargıç Zihinsel


13

Generics Tutorial'ın bu bölümünün durumu (benim vurguladığım) açıkladığını düşünüyorum:

"Genel API'nin gereğinden fazla kısıtlayıcı olmadığından emin olmanız gerekir; API'nın orijinal sözleşmesini desteklemeye devam etmelidir. Yine java.util.Collection'dan bazı örnekleri düşünün. Genel jenerik API şöyle görünür:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

Bunu doğrulamak için saf bir girişim:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

Bu kesinlikle tür güvenli olsa da, API'nın orijinal sözleşmesine uymuyor. İncludeAll () yöntemi her türlü gelen koleksiyonla çalışır. Yalnızca gelen koleksiyon gerçekten yalnızca E örneklerini içeriyorsa başarılı olur, ancak:

  • Gelen koleksiyonun statik türü değişebilir, belki de arayan, iletilen koleksiyonun tam türünü bilmediği için veya belki de S'nin E'nin bir alt türü olduğu bir Koleksiyon <S> olduğu için.
  • Farklı türden bir koleksiyonla includeAll () öğesini çağırmak tamamen meşru. Rutin çalışmalı, yanlış döndürülmeli. "

2
neden olmasın containsAll( Collection< ? extends E > c )?
Yargıç Zihinsel

1
Sağlamak için de gereklidir yukarıda @JudgeMental olsa bir örnek olarak verilmemiş containsAlla ile a, süper tip arasında . Öyle olsaydı buna izin verilmezdi . Ayrıca, örnekte açıkça belirtildiği gibi , farklı türde bir koleksiyonun geçilmesi meşrudur (o zaman dönüş değeri olmak üzere ). Collection<S>SEcontainsAll( Collection< ? extends E > c )false
davmac

E'nin bir süper tip koleksiyonu ile tümAE'ye izin vermemeliyim. Bu asıl sorunun amacı olduğunu düşündüğüm aptalca bir sözleşme.
Yargıç Zihinsel

6

Nedeni çevreleme tarafından belirlenir olmasıdır equalsve hashCodehangi yöntemleri vardır Objectve bir alınırolur.ç Objectparametre. Bu, Java'nın standart kitaplıklarındaki erken tasarım hatasıydı. Java'nın tip sistemindeki sınırlamalarla birleştiğinde, eşittir ve hashCode'a dayanan her şeyi zorlar Object.

Java tip-güvenli hash tabloları ve eşitliği sağlamanın tek yolu sakınmaktır Object.equalsve Object.hashCodeve genel ürün kullanmayın. Fonksiyonel Java sadece bu amaçla tip sınıflarıyla birlikte gelir: Hash<A>ve Equal<A>. Yapıcısını HashMap<K, V>alan Hash<K>ve Equal<K>içine alan bir sargı temin edilmiştir. Bu sınıf getve containsyöntemler bu nedenle genel bir tür argümanı alır K.

Misal:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error

4
Bu kendi başına 'get' türünün "V get (K key)" olarak bildirilmesini engellemez, çünkü 'Object' her zaman K'nın atasıdır, bu nedenle "key.hashCode ()" geçerli olmaya devam eder.
finnw

1
Bunu engellemese de, açıkladığını düşünüyorum. Eğer sınıf eşitliğini zorlamak için eşittir yöntemini değiştirdiyseler, insanlara haritadaki nesneyi bulmak için temel mekanizmanın eşittir () ve hashmap () kullandığını söyleyemezlerdi.
cgp

5

Uyumluluk.

Jenerikler mevcut olmadan önce sadece get (Object o) vardı.

Bu yöntemi (<K> o) almak için değiştirmiş olsaydı, java kullanıcıları için sadece çalışma kodunu tekrar derlemek için büyük kod bakımını zorunlu olarak zorlardı.

Onlar olabilir bir girmiştik ek yöntem, get_checked demek (<K> o), daha nazik bir geçiş yolu yoktu çok eski olsun () yöntemini deprecate. Ancak bazı nedenlerden dolayı bu yapılmadı. (Şu anda bulunduğumuz durum, get () argümanı ile haritanın bildirilen <K> anahtar türü arasındaki tür uyumluluğunu kontrol etmek için findBugs gibi araçlar yüklemeniz gerektiğidir.)

Sanırım .equals () anlambilimiyle ilgili argümanlar sahte. (Teknik olarak doğrudurlar, ama yine de sahte olduklarını düşünüyorum. O1 ve o2'nin ortak bir üst sınıfı yoksa, sağ zihnindeki hiçbir tasarımcı o1.equals (o2) 'yi doğru yapmaz.)


4

Daha ağır bir neden var, teknik olarak yapılamıyor çünkü Harita'yı kırıyor.

Java gibi polimorfik jenerik yapıya sahiptir <? extends SomeClass>. Böyle bir referans işaretlendi ile imzalanmış türü işaret edebilir <AnySubclassOfSomeClass>. Ancak polimorfik jenerik bu referansı salt okunur yapar . Derleyici, genel türleri yalnızca dönen yöntem türü olarak (basit alıcılar gibi) kullanmanıza izin verir, ancak genel türün bağımsız değişken olduğu yöntemlerin (sıradan ayarlayıcılar gibi) kullanımını engeller. Bu, yazarsanız Map<? extends KeyType, ValueType>, derleyicinin yöntemi çağırmanıza izin vermediği get(<? extends KeyType>)ve haritanın işe yaramayacağı anlamına gelir. Tek çözüm bu yöntem genel değil yapmaktır: get(Object).


set metodu neden güçlü yazılıyor?
Sentenza

'put' demek istiyorsanız: put () yöntemi haritayı değiştirir ve <? gibi jeneriklerle kullanılabilir olmayacaktır. SomeClass> 'ı genişletir. Eğer derlerseniz derleme istisnanız olur. Böyle bir harita "salt okunur" olacak
Owheee

1

Geriye dönük uyumluluk, sanırım. Map(veya HashMap) hala desteklenmesi gerekiyor get(Object).


13
Ancak aynı argüman için de yapılabilir put(bu, genel türleri kısıtlar). Ham türleri kullanarak geriye dönük uyumluluk elde edersiniz. Jenerikler "kabul edilir".
Thilo

Şahsen, bu tasarım kararının en olası sebebinin geriye dönük uyumluluk olduğunu düşünüyorum.
geekdenz

1

Buna bakıyordum ve neden bu şekilde yaptıklarını düşünüyordum. Mevcut cevapların hiçbirinin neden yeni jenerik arayüzü sadece anahtar için uygun türü kabul etmelerini sağlayamadıklarını düşünmüyorum. Asıl sebep, jenerikleri tanıtmış olmalarına rağmen yeni bir arayüz oluşturmamış olmalarıdır. Harita arayüzü aynı zamanda hem jenerik hem de jenerik olmayan versiyon olarak hizmet veren aynı eski jenerik olmayan Haritadır. Bu şekilde, jenerik olmayan bir haritayı kabul eden bir yönteminiz varsa onu geçirebilir Map<String, Customer>ve yine de çalışır. Aynı zamanda get için sözleşme Object'i kabul eder, böylece yeni arayüz bu sözleşmeyi de desteklemelidir.

Bence yeni bir arayüz eklemeli ve hem mevcut koleksiyona uygulamalıydılar, ancak get yöntemi için daha kötü tasarım anlamına gelse bile uyumlu arayüzler lehine karar verdiler. Koleksiyonların kendilerinin mevcut yöntemlerle sadece arayüzlerin uyumlu olmayacağını unutmayın.


0

Biz şimdi büyük refactoring yapıyoruz ve biz eski tip bazı get () kaçırmadım kontrol etmek için bu güçlü yazılan get () eksik.

Ama derleme süresi kontrolü için geçici / çirkin hile buldum: güçlü bir şekilde yazılmış olsun, içerirAnahtar, kaldır ... ile Harita arayüzü oluşturun ve projenizin java.util paketine koyun.

Sadece yanlış türlerle get (), ... çağırmak için derleme hataları alırsınız, diğer her şey derleyici için tamam görünüyor (en azından eclipse kepler içinde).

Yapımınızı kontrol ettikten sonra bu arayüzü silmeyi unutmayın, çünkü bu çalışma zamanında istediğiniz şey değildir.

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.