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