转变馆藏
您是否曾經(jīng)想替換過HashSet或HashMap使用的equals和hashCode方法? 或者有一個(gè)List的一些元素類型偽裝成的List相關(guān)類型的?
轉(zhuǎn)換集合使這成為可能,并且本文將展示如何實(shí)現(xiàn)。
總覽
轉(zhuǎn)換集合是LibFX 0.3.0的一項(xiàng)功能,該功能將在今天每天發(fā)布。 這篇文章將介紹總體思路,涵蓋技術(shù)細(xì)節(jié),并提供一些可能會派上用場的用例。
正在進(jìn)行的示例是LibFX中包含的功能演示的稍微改編的變體。 請記住,這只是演示該概念的一個(gè)示例。
轉(zhuǎn)變館藏
轉(zhuǎn)換集合是另一個(gè)集合的視圖(例如,列表中的列表,地圖上的地圖等),其中似乎包含不同類型的元素(例如,整數(shù)而不是字符串)。
通過應(yīng)用轉(zhuǎn)換從內(nèi)部元素創(chuàng)建視圖元素。 這是按需發(fā)生的,因此轉(zhuǎn)換集合本身是無狀態(tài)的。 作為一個(gè)適當(dāng)?shù)囊晥D,內(nèi)部集合以及轉(zhuǎn)換視圖的所有更改都反映在另一個(gè)視圖中(例如Map及其entrySet )。
命名法
轉(zhuǎn)換的集合也可以視為裝飾器。 我將裝飾后的集合稱為內(nèi)部集合,并將其泛型稱為內(nèi)部類型。 轉(zhuǎn)換集合及其通用類型分別稱為外部集合和外部類型。
例
讓我們來看一個(gè)例子。 假設(shè)我們有一組字符串,但是我們知道這些字符串只包含自然數(shù)。 我們可以使用一個(gè)轉(zhuǎn)換集來獲取一個(gè)看起來像是整數(shù)集的視圖。
(類似// "[0, 1] ~ [0, 1]"的System.out.println(innerSet + " ~ " + transformingSet);是System.out.println(innerSet + " ~ " + transformingSet);的控制臺輸出。)
Set<String> innerSet = new HashSet<>(); Set<Integer> transformingSet = new TransformingSet<>(innerSet,/* skipping some details */); // both sets are initially empty: "[] ~ []"// now let's add some elements to the inner set innerSet.add("0"); innerSet.add("1"); innerSet.add("2"); // these elements can be found in the view: "[0, 1, 2] ~ [0, 1, 2]"// modifying the view reflects on the inner set transformingSet.remove(1); // again, the mutation is visible in both sets: "[0, 2] ~ [0, 2]"看看轉(zhuǎn)換有多愉快?
發(fā)布時(shí)間由Rooners玩具攝影下的CC-BY-NC-ND 2.0 。
細(xì)節(jié)
像往常一樣,魔鬼在細(xì)節(jié)中,所以讓我們討論這個(gè)抽象的重要部分。
轉(zhuǎn)寄
轉(zhuǎn)換集合是另一個(gè)集合的視圖。 這意味著它們本身不保存任何元素,而是將所有調(diào)用轉(zhuǎn)發(fā)給內(nèi)部/裝飾的集合。
他們通過將調(diào)用參數(shù)從外部類型轉(zhuǎn)換為內(nèi)部類型并使用這些參數(shù)調(diào)用內(nèi)部集合來實(shí)現(xiàn)此目的。 然后,將返回值從內(nèi)部類型轉(zhuǎn)換為外部類型。 對于以集合為參數(shù)的調(diào)用,這變得有些復(fù)雜,但是方法基本上是相同的。
所有轉(zhuǎn)換集合的實(shí)現(xiàn)方式都是將方法的每次調(diào)用轉(zhuǎn)發(fā)到內(nèi)部集合上的相同方法 (包括default方法 )。 這意味著內(nèi)部集合對線程安全性,原子性等的任何保證也將由轉(zhuǎn)換集合維護(hù)。
轉(zhuǎn)型
轉(zhuǎn)換是通過在構(gòu)造過程中指定的一對函數(shù)來計(jì)算的。 一個(gè)用于將外部元素轉(zhuǎn)換為內(nèi)部元素,另一個(gè)用于另一個(gè)方向。 (對于映射,存在兩對這樣的對:一對用于鍵,一對用于值。)
轉(zhuǎn)換函數(shù)關(guān)于equals必須彼此相反,即, outer.equals(toOuter(toInner(outer))和inner.equals(toInner(toOuter(inner))對于所有外部元素和內(nèi)部元素必須為true。并非如此,這些集合的行為可能無法預(yù)測。
對于身份而言,情況并非如此,即, outer == toOuter(toInner(outer))可能為false。 詳細(xì)信息取決于所應(yīng)用的轉(zhuǎn)換,并且通常未指定-它可能永遠(yuǎn)不會,有時(shí)或永遠(yuǎn)都是正確的。
例
讓我們看看轉(zhuǎn)換函數(shù)如何查找我們的字符串和整數(shù)集:
private Integer stringToInteger(String string) {return Integer.parseInt(string); }private String integerToString(Integer integer) {return integer.toString(); }這就是我們使用它們創(chuàng)建轉(zhuǎn)換集的方式:
Set<Integer> transformingSet = new TransformingSet<>(innerSet,this::stringToInteger, this::integerToString,/* still skipping some details */);直截了當(dāng)吧?
是的,但是即使這個(gè)簡單的示例也包含陷阱。 注意前導(dǎo)零的字符串如何映射到相同的整數(shù)。 這可以用于創(chuàng)建不良行為:
innerSet.add("010"); innerSet.add("10"); // now the transforming sets contains the same entry twice: // "[010, 10] ~ [10, 10]"// sizes of different sets: System.out.println(innerSet.size()); // "2" System.out.println(transformingSet.size()); // "2" System.out.println(new HashSet<>(transformingSet).size()); // "1" !// removing is also problematic transformingSet.remove(10) // the call returns true // one of the elements could be removed: "[010] ~ [10]" transformingSet.remove(10) // the call returns false // indeed, nothing changed: "[010] ~ [10]"// now things are crazy - this returns false: transformingSet.contains(transformingSet.iterator().next()) // the transforming set does not contain its own elements ~> WAT?因此,在使用轉(zhuǎn)換集合時(shí),仔細(xì)考慮轉(zhuǎn)換非常重要。 它們必須彼此相反!
但這僅限于實(shí)際發(fā)生的內(nèi)部和外部元素就足夠了。 在該示例中,問題僅在引入前導(dǎo)零的字符串時(shí)才開始。 如果這些被某些業(yè)務(wù)規(guī)則所禁止,并且已經(jīng)正確執(zhí)行,那么一切都會好起來的。
類型安全
以通常的靜態(tài),編譯時(shí)方式,對轉(zhuǎn)換集合進(jìn)行的所有操作都是類型安全的。 但是,由于收集接口中的許多方法都允許對象(例如Collection.contains(Object) )或未知通用類型的集合(例如Collection.addAll(Collection<?>) )作為參數(shù),因此這并不涵蓋所有可能發(fā)生在以下情況的情況運(yùn)行。
請注意,這些調(diào)用的參數(shù)必須從外部類型轉(zhuǎn)換為內(nèi)部類型,才能將調(diào)用轉(zhuǎn)發(fā)到內(nèi)部集合。 如果使用非外部類型的實(shí)例調(diào)用它們,則很可能無法將其傳遞給轉(zhuǎn)換函數(shù)。 在這種情況下,該方法可能會拋出ClassCastException 。 盡管這與方法的合同一致,但可能仍然是意外的。
為了減少這種風(fēng)險(xiǎn),轉(zhuǎn)換集合的構(gòu)造函數(shù)需要使用內(nèi)部和外部類型的令牌。 它們用于檢查元素是否為必需類型,如果不是,則可以毫無例外地優(yōu)雅地回答查詢。
例
我們終于可以確切地看到如何創(chuàng)建轉(zhuǎn)換集:
Set<Integer> transformingSet = new TransformingSet<>(innerSet,String.class, this::stringToInteger,Integer.class, this::integerToString);構(gòu)造函數(shù)實(shí)際上接受Class<? super I> Class<? super I>所以這也將編譯:
Set<Integer> transformingSetWithoutTokens = new TransformingSet<>(innerSet,Object.class, this::stringToInteger,Object.class, this::integerToString);但是由于所有內(nèi)容都是對象,所以針對令牌的類型檢查變得無用,并且調(diào)用轉(zhuǎn)換函數(shù)可能會導(dǎo)致異常:
Object o = new Object(); innerSet.contains(o); // false transformingSet.contains(o); // false transformingSetWithoutTokens.contains(o); // exception用例
我想說,轉(zhuǎn)換集合是一種非常專業(yè)的工具,不太可能經(jīng)常使用,但在每個(gè)分類良好的工具箱中仍然占有一席之地。
重要的是要注意,如果性能至關(guān)重要,則可能會出現(xiàn)問題。 每次調(diào)用包含或返回元素的轉(zhuǎn)換集合,都會導(dǎo)致至少創(chuàng)建一個(gè)(通常是多個(gè))對象。 這些對垃圾收集器施加了壓力,并導(dǎo)致通往有效負(fù)載的方式的間接級別更高。 (與以往一樣,在討論性能時(shí):首先要介紹!)
那么轉(zhuǎn)換集合的用例是什么? 上面我們已經(jīng)看到了如何將集合的元素類型更改為另一種。 盡管這代表了總體思路,但我認(rèn)為這不是一個(gè)非常普遍的用例(盡管在某些邊緣情況下是有效的方法)。
在這里,我將展示兩個(gè)更狹窄的解決方案,您可能希望在某些時(shí)候使用它們。 但是我也希望這能使您了解如何使用轉(zhuǎn)換集合來解決棘手的情況。 也許您的問題的解決方案在于巧妙地應(yīng)用此概念。
用Equals和HashCode代替
我一直很喜歡.NET的哈希圖(他們稱其為字典)如何具有將EqualityComparer作為參數(shù)的構(gòu)造函數(shù) 。 通常將在鍵上調(diào)用的所有對equals和hashCode調(diào)用都委派給該實(shí)例。 因此有可能即時(shí)替換有問題的實(shí)現(xiàn)。
當(dāng)您處理無法完全控制的有問題的舊版代碼或庫代碼時(shí),這可以節(jié)省生命。 當(dāng)需要一些特殊的比較機(jī)制時(shí),它也很有用。
使用轉(zhuǎn)換集合,這很容易。 為了使它更加容易,LibFX已經(jīng)包含一個(gè)EqualityTransformingSet和EqualityTransformingMap 。 它們修飾另一個(gè)集合或映射實(shí)現(xiàn),并且在構(gòu)造過程中可以提供鍵和元素的equals和hashCode函數(shù)。
例
假設(shè)您想將字符串用作set元素,但為了進(jìn)行比較,您僅對它們的長度感興趣。
Set<String> lengthSet = EqualityTransformingSet.withElementType(String.class).withInnerSet(new HashSet<Object>()).withEquals((a, b) -> a.length != b.length).withHash(String::length).build();lengthSet.add("a"); lengthSet.add("b"); System.out.println(lengthSet); // "[a]"從集合中刪除可選性
也許您正在與一個(gè)在各處使用Optional的想法的人一起工作,然后瘋狂地使用它,現(xiàn)在您有了Set<Optional<String>> 。 如果無法修改代碼(或您的同事),則可以使用轉(zhuǎn)換集合來獲取一個(gè)對您隱藏Optional的視圖。
同樣,實(shí)現(xiàn)起來很簡單,因此LibFX已經(jīng)以O(shè)ptionalTransforming[Collection|List|Set]的形式包含了它。
例
Set<Optional<String>> innerSet = new HashSet<>(); Set<String> transformingSet =new OptionalTransformingSet<String>(innerSet, String.class);innerSet.add(Optional.empty()); innerSet.add(Optional.of("A"));// "[Optional.empty, Optional[A]] ~ [null, A]"請注意, null表示空的optional的方式。 這是默認(rèn)行為,但是您也可以將另一個(gè)字符串指定為空的Optionals的值:
Set<String> transformingSet =new OptionalTransformingSet<String>(innerSet, String.class, "DEFAULT");// ... code as above ... // "[Optional.empty, Optional[A]] ~ [DEFAULT, A]"這樣可以避免使用Optional和null作為元素,但是現(xiàn)在您必須確保永遠(yuǎn)不會有包含DEFAULT的Optional。 (如果確實(shí)如此,則隱式轉(zhuǎn)換不是彼此相反的,我們已經(jīng)在上面看到了這些轉(zhuǎn)換會引起問題。)
有關(guān)此示例的更多詳細(xì)信息,請查看演示 。
反射
我們已經(jīng)介紹過,轉(zhuǎn)換集合是另一個(gè)集合的視圖。 使用類型標(biāo)記(以最大程度地減少ClassCastExceptions )和一對轉(zhuǎn)換函數(shù)(它們必須彼此相反),每個(gè)調(diào)用都將轉(zhuǎn)發(fā)到經(jīng)過修飾的集合。 轉(zhuǎn)換后的集合可以維護(hù)修飾后的集合所做的關(guān)于線程安全性,原子性的所有保證。
然后,我們看到了轉(zhuǎn)換集合的兩個(gè)特定用例:替換等于和哈希數(shù)據(jù)結(jié)構(gòu)使用的哈希碼,以及從Collection<Optional<E>>刪除可選性。
談?wù)凩ibFX
就像我說的那樣,轉(zhuǎn)換集合是我的開源項(xiàng)目LibFX的一部分。 如果您考慮使用它,我想指出一些事情:
- 這篇文章介紹了這個(gè)想法和一些細(xì)節(jié),但是并不能代替文檔。 查閱Wiki,獲取最新描述和指向Javadoc的指針。
- 我認(rèn)真對待測試。 多虧了Guava ,約6.500個(gè)單元測試涵蓋了轉(zhuǎn)換集合。
- LibFX是根據(jù)GPL許可的。 如果那不適合您的許可模式,請隨時(shí)與我聯(lián)系。
翻譯自: https://www.javacodegeeks.com/2015/05/transforming-collections.html
總結(jié)
- 上一篇: ddos攻击导致网站很慢吗(ddos攻击
- 下一篇: (qt linux安装)