2011-01-17 11 views
17

następujące klasy:Zmienne obiekty i hashCode

public class Member { 
private int x; 
private long y; 
private double d; 

public Member(int x, long y, double d) { 
    this.x = x; 
    this.y = y; 
    this.d = d; 
} 

@Override 
public int hashCode() { 
    final int prime = 31; 
    int result = 1; 
    result = prime * result + x; 
    result = (int) (prime * result + y); 
    result = (int) (prime * result + Double.doubleToLongBits(d)); 
    return result; 
} 

@Override 
public boolean equals(Object obj) { 
    if (this == obj) { 
     return true; 
    } 
    if (obj instanceof Member) { 
     Member other = (Member) obj; 
     return other.x == x && other.y == y 
       && Double.compare(d, other.d) == 0; 
    } 
    return false; 
} 

public static void main(String[] args) { 
    Set<Member> test = new HashSet<Member>(); 
    Member b = new Member(1, 2, 3); 
    test.add(b); 
    System.out.println(b.hashCode()); 
    b.x = 0; 
    System.out.println(b.hashCode()); 
    Member first = test.iterator().next(); 
    System.out.println(test.contains(first)); 
    System.out.println(b.equals(first)); 
      System.out.println(test.add(first)); 

} 

}

produkuje następujące wyniki:
30814 29853 false true true

Ponieważ hashCode zależy od stanu obiektu, nie może dłuższe, gdy zostaną poprawnie pobrane, więc sprawdzenie zgodności nie powiedzie się. HashSet nie działa poprawnie. Rozwiązaniem byłoby uczynienie Członka niezmiennym, ale czy to jedyne rozwiązanie? Czy wszystkie klasy dodane do HashSets powinny być niezmienne? Czy istnieje jakakolwiek inna metoda radzenia sobie z sytuacją?

Pozdrawiam.

+0

Dlaczego kompilator nie wymusza reguły nie zezwalającej na modyfikowanie obiektów jako kluczy w tablicach skrótów lub obiektach w zestawach skrótów? Wydaje mi się to okropne. – ncmathsadist

Odpowiedz

26

Obiekty hashsets powinny albo być niezmienne, lub trzeba wykonywać dyscypliny w nich nie zmieniając po tym jak zostały wykorzystane w HashSet (lub hashmap).

W praktyce rzadko uważałem to za problem - rzadko zdarza mi się potrzebować użycia skomplikowanych obiektów, ponieważ klawisze są ustawionymi elementami, a kiedy to robię, zwykle nie jest to problem, żeby nie mutować ich. Oczywiście, jeśli do tego czasu ujawnisz odniesienia do innego kodu, może to być trudniejsze.

+0

Dla obiektów wartości. Obiekty referencyjne mogą być tak zmienne, jak chcesz. –

+0

@ Tom Hawtin: Czy nie byłoby również niebezpieczne mutowanie obiektów referencyjnych. W szczególności mówię o mutacjach, które zmienią kod hash obiektu lub uczynią go członkiem innej klasy równoważności. – Brian

+2

@Brian: Podejrzewam, że Tom mówi o typach, które nie zastępują hashCode lub są równe. –

7

Tak. Utrzymując klasa zmienny, można obliczyć hashCode i metod jest równa podstawie niezmiennych wartościach klasy (może wygenerowany id) do przestrzegania umowy hashCode zdefiniowanej w klasie Object:

  • ilekroć jest wywoływany na tym samym obiekcie więcej niż jeden raz podczas wykonywania aplikacji Java, metoda hashCode musi konsekwentnie zwracać tę samą liczbę całkowitą, pod warunkiem, że nie są modyfikowane żadne informacje używane w porównywaniu równań na obiekcie. Ta liczba całkowita nie musi pozostawać spójna od jednego wykonania aplikacji do innego wykonania tej samej aplikacji.

  • Jeśli dwa obiekty są równe zgodnie z metodą equals (Object), wówczas wywołanie metody hashCode na każdym z dwóch obiektów musi dać taki sam wynik całkowity.

  • Nie jest wymagane, że jeśli dwa obiekty są nierówne zgodnie z metodą equals (java.lang.Object), wywołanie metody hashCode na każdym z dwóch obiektów musi dać różne wyniki liczb całkowitych. Jednak programista powinien mieć świadomość, że generowanie różnych wyników całkowitych dla nierównych obiektów może poprawić wydajność tablic asocjacyjnych.

W zależności od sytuacji może to być łatwiejsze, czy nie.

class Member { 
    private static long id = 0; 

    private long id = Member.id++; 
    // other members here... 


    public int hashCode() { return this.id; } 
    public boolean equals(Object o) { 
     if(this == o) { return true; } 
     if(o instanceOf Member) { return this.id == ((Member)o).id; } 
     return false; 
    } 
    ... 
} 

Jeśli potrzebujesz bezpiecznego atrybut wątku, można rozważyć użycie: AtomicLong zamiast, ale znowu, to zależy od tego jak masz zamiar korzystać z obiektu.

+0

Problem z tym podejściem polega na tym, że jeśli wstawię dwie różne instancje w HashSet (np. Nowy element członkowski (1,2, 3)), zostaną one wstawione dwukrotnie. Również metoda równań nie zadziała zgodnie z oczekiwaniami. Ale dziękuję za wskazanie umowy hashCode: "Za każdym razem, gdy jest wywoływana na tym samym obiekcie więcej niż jeden raz podczas wykonywania aplikacji Java, metoda hashCode musi konsekwentnie zwracać tę samą liczbę całkowitą, pod warunkiem, że żadna informacja użyta w porównywaniu równań na obiekcie nie jest zmodyfikowany. " Nie czytałem tego wystarczająco uważnie. – robert

+0

Cóż, to była tylko próbka, możesz ją stworzyć tak: 'public Member (int, int b, int) {this.id = a * 31 + b * 31 + 31 *; } 'Jeśli utworzysz innego' nowego użytkownika (1,2,3) 'to oblicza ten sam skrót, ** ale ** jeśli zmodyfikujesz jeden z nich, będziesz musiał zsynchronizować. Można użyć metody rejestru/fabryki i utworzyć obiekty takie jak: 'Member a = Member.newInstance (1,2,3); Członek b = Member.newInstance (1,2,3); 'i stwórz instancję tylko raz. – OscarRyz

0

Teoretycznie (i nie częściej niż praktycznie też) klasa albo:

  1. ma naturalną niezmiennej tożsamości, które można wywieść z podzbioru swoich dziedzinach, w których przypadku można użyć tych pól w celu wygenerowania hashCode z.
  2. nie ma naturalnej tożsamości. W takim przypadku niepotrzebne jest przechowywanie ich przy użyciu Set. Można również użyć numeru List.
3

Jon Skeet podał wszystkie możliwe alternatywy. Jak, dlaczego klawisze mapie lub zestawu nie musi się zmienić:

Umowa o tematyce Zestaw oznacza, że ​​w każdej chwili, nie istnieją dwa obiekty O1 i O2, tak że

o1 != o2 && set.contains(o1) && set.contains(o2) && o1.equals(o2) 

Dlaczego, co jest wymagane jest szczególnie wyraźny w przypadku mapy. Z umowy Map.get():

Bardziej formalnie, czy ta mapa zawiera mapowania z klucza k do wartości v tak że (key==null ? k==null : key.equals(k)), to metoda zwraca v, w przeciwnym wypadku zwraca null. (Może być co najwyżej jedno takie odwzorowanie.)

Teraz, jeśli zmodyfikujesz klucz włożony do mapy, możesz uczynić go równym innemu klawiszowi już wstawionemu. Co więcej, mapa nie może wiedzieć, że to zrobiłeś. A więc co powinna zrobić mapa, jeśli wykonasz map.get(key), gdzie key jest równa kilku kluczom na mapie? Nie ma intuicyjnego sposobu na określenie, co by to miało znaczyć - głównie dlatego, że nasza intuicja dla tych typów danych jest matematycznym ideałem zbiorów i mapowań, które nie muszą zajmować się zmieniającymi się kluczami, ponieważ ich klucze są obiektami matematycznymi, a zatem niezmiennymi.

-1

Nigdy nie zmieniaj „hashable pole” po umieszczeniu w pojemniku z siedzibą hash.

Jakbyś (Członek) zarejestrował swój numer telefonu (Member.x) w żółtej stronie (kontener na podstawie hash), ale zmienił swój numer , wtedy nikt nie może cię znaleźć w żółtej stronie Obojętnie więcej

2

Jak już wspomniano, można przyjąć następujące trzy rozwiązania:.

  1. Stosować niezmienne obiekty, nawet wtedy, gdy klasa jest zmienny, można użyć niezmienne tożsamości w twoim implemencie hashcode sprawdzanie, np. wartość podobna do ID.
  2. Podobnie jak wyżej, zaimplementuj add/remove, aby uzyskać klon wstawionego obiektu, a nie rzeczywistą referencję. HashSet nie oferuje funkcji get (np. W celu umożliwienia późniejszej zmiany obiektu); w ten sposób jesteś bezpieczny, nie będzie duplikatów.
  3. dyscyplina Ćwiczenia w nich nie zmieniając po ich stosować jako @Jon Skeet sugeruje

Ale, jeśli z jakiegoś powodu naprawdę trzeba modyfikować obiekty po włożeniu do HashSet, trzeba znaleźć sposób "poinformowania" swojej kolekcji o nowych zmianach.Aby osiągnąć tę funkcjonalność:

  1. Można użyć wzorca projektowego obserwatora i przedłużyć HashSet zaimplementować interfejs Observer. Twoje obiekty Member muszą być Observable iw dowolnej konfiguracji lub innej metodzie, która wpływa na hashcode i/lub .

Uwaga 1: Rozszerzenie 3, użycie 4: możemy zaakceptować zmiany, ale te, które nie tworzą już istniejącego obiektu (np. Zaktualizowałem identyfikator użytkownika, przypisując nowy identyfikator, nie ustawiając go w istniejącym jeden). W przeciwnym razie należy wziąć pod uwagę scenariusz, w którym obiekt jest przekształcany w taki sposób, że jest teraz równy innemu obiektowi już istniejącemu w Set. Jeśli zaakceptujesz to ograniczenie, czwarta sugestia zadziała dobrze, w przeciwnym razie musisz być proaktywny i zdefiniować politykę dla takich przypadków.

Uwaga 2: trzeba zapewnić zarówno wcześniejsze i aktualne stany zmienionego obiektu od implementacji update, bo trzeba najpierw usunąć starszą elementu (np używać getClone() przed ustawieniem nowych wartości), a następnie dodać obiekt z nowy stan. Poniższy fragment jest tylko przykładową implementacją, wymaga zmian w oparciu o zasady dodawania duplikatu.

@Override 
public void update(Observable newItem, Object oldItem) { 
    remove(oldItem); 
    if (add(newItem)) 
     newItem.addObserver(this); 
} 

Używałem podobnych technik w projektach, gdzie wymagają wielu indeksów w klasie, więc mogę patrzeć z O (1) do zestawów przedmiotów, które dzielą wspólną tożsamość; wyobraź sobie, że jest to MultiKeymap z HashSets (jest to naprawdę przydatne, ponieważ możesz wtedy przecinać/indeksować związki i działać podobnie do wyszukiwania typu SQL). W takich przypadkach przypisuję metody (zazwyczaj setery), które muszą fireChange aktualizować każdy z indeksów, gdy nastąpi znacząca zmiana, więc indeksy są zawsze aktualizowane o najnowsze stany.