2017-01-13 7 views
0

Ich versuche, die folgende Übung von „Core-Java für Ungeduldige“ von Cay Horstmann zu lösen:Streams mit TreeMap Rückkehr inkohärent Ergebnisse

Wenn ein Encoder eines Charset mit teilweise Unicode-Abdeckung kann kein kodieren Zeichen, es ersetzt es mit einem Standard - normalerweise, aber nicht immer, die Codierung von "?". Hier finden Sie alle Ersetzungen aller verfügbaren Zeichensätze, die die Codierung unterstützen. Verwenden Sie die newEncoder Methode, um einen Encoder zu erhalten, und rufen Sie die replacement Methode auf, um den Ersatz zu erhalten. Geben Sie für jedes eindeutige Ergebnis die kanonischen Namen der Zeichensätze an, die es verwenden.

Aus Gründen der Ausbildung hat ich beschlossen, die Übung mit gargantuan Einzeiler mit dem Streaming-API, obwohl in Angriff zu nehmen - meiner Meinung nach - eine saubere Lösung, die Berechnungen in mehreren Schritten unterteilen würde, mit Zwischenvariablen dazwischen (sicherlich würde es das Debuggen erleichtern). Ohne weitere Umschweife, hier ein Monster von Code, die ich erstellt haben:

Charset.availableCharsets().values().stream().filter(charset -> charset.canEncode()).collect(
      Collectors.groupingBy(
        charset -> charset.newEncoder().replacement(), 
        () -> new TreeMap<>((arr1, arr2) -> Arrays.equals(arr1, arr2) == true ? 0 : Integer.compare(arr1.hashCode(), arr2.hashCode())), 
        Collectors.mapping(charset -> charset.name(), Collectors.toList()))). 
      values().stream().map(list -> list.stream().collect(Collectors.joining(", "))).forEach(System.out::println); 

Grundsätzlich berücksichtigen wir nur die charsets dass canEncode; Erstellen Sie eine Map mit replacement als Schlüssel und eine Liste der kanonischen Namen als Werte; Da die Gruppierung für Arrays mit der Standardimplementierung groupingBy, die HashMap verwendet, nicht funktioniert, habe ich mich entschieden, zu verwenden. Wir arbeiten dann mit den Lists kanonischen Namen, verbinden sie mit Komma und drucken.

Leider habe ich gefunden, dass es zu inkohärenten Ergebnissen führt. Wenn ich die Funktion zweimal im selben Programm ausführe, gibt die erste Instanz Ergebnisse zurück, die aus 23 Strings bestehen, der zweite - nur 21 Strings. Ich vermute, es hat etwas mit einer schlechten Umsetzung Comparator für TreeMap zu tun, das wurde wie folgt definiert:

((arr1, arr2) -> Arrays.equals(arr1, arr2) == true ? 0 : Integer.compare(arr1.hashCode(), arr2.hashCode())) 

Wenn das die Ursache ist, was eine richtiges Comparator in diesem Fall sein sollte? Abgesehen davon, kann der One-Liner in irgendeiner Weise verbessert werden?

Ich bin auch neugierig, wenn solche verworrenen Konstrukte wie der Code, den ich geschrieben habe, in professionellen Programmen angetroffen werden? Vielleicht finde ich es nur für mich unleserlich?

+0

stellen Sie sicher, dass Ihr Vergleicher die folgenden erfüllen * Die Reihenfolge, die von einem Komparator c auf einer Menge von Elementen S auferlegt wird gesagt, mit genau gleich zu sein, wenn c.compare (e1, e2) == 0 hat Derselbe boolesche Wert wie e1.equals (e2) für jedes e1 und e2 in S * – Jobin

+0

Vielen Dank. Ich bin nicht sicher, ob ein Komparator, der mit equals konsistent ist, in diesem Fall definiert werden kann, da equars für Arrays Referenzen vergleicht und ich die in diesem Array gespeicherten Werte vergleichen muss. Gibt es eine andere Datenstruktur, die ich in diesem Fall verwenden könnte, abgesehen von TreeMap? – lukeg

Antwort

2

Es gibt keine Garantie, dass der Hash-Code von zwei verschiedenen Instanzen unterschiedlich sein wird. Das wäre eine ideale Situation, ist aber niemals garantiert. Nur das Gegenteil ist der Fall: Wenn zwei Objekte gleich sind, haben sie den gleichen Hash-Code.

Wenn Sie also einen Vergleicher erstellen, der die Objekte als identisch betrachtet, wenn sie denselben Hash-Code haben, können beliebige Objekte als identisch betrachtet werden. Da die byte[] Arrays, die von replacement() zurückgegeben werden, defensive Kopien sind, lesen Sie temporäre Objekte. Das Ergebnis kann bei jedem Durchlauf dieses Codes variieren. Da der Hash-Code eines Arrays mit seinem Inhalt nichts zu tun hat, verstößt der Vergleicher gegen die Transitivitätsregel: Zwei Arrays mit gleichem Inhalt sollen identisch sein, aber sie könnten/könnten sehr wahrscheinlich unterschiedliche Hash-Werte haben Codes haben sie eine andere Beziehung, wenn sie mit einem dritten Array verglichen werden, das nicht den gleichen Inhalt hat, a == b, aber a < c und b > c.Dies ist der Grund, warum sogar gleiche Arrays, die Sie mit Arrays.equals vergleichen, in verschiedenen Gruppen landen können, da der TreeSet den vorhandenen Schlüssel nicht gefunden hat, wenn er mit anderen Schlüsseln verglichen wird.

Wenn Sie die Arrays wollen nach Wert verglichen werden, können Sie verwenden:

Charset.availableCharsets().values().stream().filter(Charset::canEncode).collect(
    Collectors.groupingBy(
      charset -> charset.newEncoder().replacement(), 
      () -> new TreeMap<>(Comparator.comparing(ByteBuffer::wrap)), 
      Collectors.mapping(Charset::name, Collectors.joining(", ")))) 
    .values().forEach(System.out::println); 

ByteBuffer s sind Comparable und konsequent den Inhalt des umwickelten Array auszuwerten.

Ich habe den Collector in den Collector grouping verschoben, um die Erstellung des temporären List zu vermeiden, dessen Inhalt Sie sowieso später beitreten werden.

Übrigens, niemals Code wie expression == true verwenden. Es gibt keinen Grund, == true anzufügen, da expression bereits ausreicht.


Da Sie interessieren sich nur für die Werte, mit anderen Worten, brauchen nicht die Schlüssel eines bestimmten Typs zu sein, Sie alle Arrays vorher wickeln können, um den Betrieb zu vereinfachen und sogar machen es etwas effizienter :

Charset.availableCharsets().values().stream().filter(Charset::canEncode).collect(
    Collectors.groupingBy(
      charset -> ByteBuffer.wrap(charset.newEncoder().replacement()), 
      TreeMap::new, 
      Collectors.mapping(Charset::name, Collectors.joining(", ")))) 
    .values().forEach(System.out::println); 

Diese Änderung Hashing greifen ermöglicht sogar, wenn keine konsistente Iteration erforderlich ist:

Charset.availableCharsets().values().stream().filter(Charset::canEncode).collect(
    Collectors.groupingBy(
      charset -> ByteBuffer.wrap(charset.newEncoder().replacement()), 
      Collectors.mapping(Charset::name, Collectors.joining(", ")))) 
    .values().forEach(System.out::println); 

Dies funktioniert, weil ByteBuffer implementiert auch equals und hashCode.

Verwandte Themen