Сегодня я просто описываю три очень важных метода, которые часто используются вместе для сортировки и сравнения в разных коллекциях Java.

равно ()

Это используется в большинстве коллекций, чтобы определить, содержит ли коллекция данный элемент. Например:

List<MyClass> list = new ArrayList<MyClass>();
MyClass c1 = new MyClass( .. );  // Some parameters are passed
MyClass c2 = new MyClass( .. );  // Some parameters are passed
list.add(c1);
...
boolean hasObject = list.contains(c2);

Как показано, в таких операциях, как contains (), remove () и т. Д., ArrayList выполняет итерацию по всему списку и выполняет element.equals (someObject), чтобы определить, равен ли элемент объекту параметра. При совпадении он выполняет соответствующие действия, такие как подтверждение присутствия (для contains ()), удаление элемента (для remove ()) и т. Д.

Согласно equals() Javadoc, метод должен соответствовать следующим правилам:

Метод equals() реализует отношение эквивалентности:

  • Он рефлексивен: для любого ссылочного значения x x.equals(x) должен возвращать истину
  • Он симметричен: для любых справочных значений x и y x.equals(y) должен возвращать истину тогда и только тогда, когда y.equals(x) возвращает истину
  • Это транзитивно: для любых справочных значений x, y и z, если x.equals(y) возвращает истину, аy.equals(z) возвращает истину, тогда x.equals(z) должен возвращать истину
  • Это согласованно: для любых ссылочных значений x и y несколько вызовов ofx.equals(y) последовательно возвращают истину или последовательно возвращают ложь, при условии, что никакая информация, используемая в равных сравнениях на объекте, не изменена
  • Для любого ненулевого ссылочного значения x x.equals(null) должен возвращать false

Джошуа Блох предлагает пятишаговый рецепт для написания метода effectiveequals(). Вот рецепт в виде кода:

public class EffectiveEquals {
    private int valueA;
    private int valueB;
    . . .
    public boolean equals( Object o ) {
        if(this == o) {  // Step 1: Perform an == test
            return true;
        }
        if(!(o instanceof EffectiveEquals)) {  // Step
2: Instance of check
            return false;
        }
        EffectiveEquals ee = (EffectiveEquals) o; //
Step 3: Cast argument
        // Step 4: For each important field, check to
see if they are equal
        // For primitives use ==
        // For objects use equals() but be sure to also
        // handle the null case first
        return ee.valueA == valueA &&
               ee.valueB == valueB;
    }
    . . .
}

Примечание. Вам не нужно выполнять нулевую проверку, поскольку null instanceof EffectiveEquals будет иметь значение false.

Наконец, для шага 5 вернитесь к equals() контракту и спросите себя, является ли equals()метод рефлексивным, симметричным и транзитивным. Если нет, поменяйте.

Теперь важный момент, который следует обсудить в этом контексте, - это разница между равенством ссылок и равенством объектов. Проверьте приведенный ниже код:

int a = 4; int b = 4;
System.out.println(a==b);

Integer a1 = new Integer(2);
Integer b1 = new Integer(2);
System.out.println(a1==b1);
String a2 = "as";
String b2 = "as";
System.out.println(a2==b2);
System.out.println(a2.equals(b2));
String a3 = new String("as");
String b3 = new String("as");
System.out.println(a3==b3);
System.out.println(a3.equals(b3));
// Output:
true
false
true
true
false
true

В приведенном выше примере первый вывод - это сравнение ссылок на примитивные типы Java между примитивными типами с использованием «==», сравнение их значений. Во втором случае сравнение происходит между двумя адресами в памяти (равенство ссылок) и, следовательно, оно возвращает false, даже если значения совпадают. Теперь для третьего и четвертого случая ссылки на строки равны, потому что их значения равны. JVM не создает отдельного держателя, а повторно использует держатель, имеющий такое же значение из пула строк. Для последних двух случаев мы создаем отдельные объекты String, и, следовательно, равенство ссылок не выполняется. Следовательно, мы всегда должны использовать equals () для сравнения значений.

хэш-код()

Метод объектов hashCode () используется, когда вы вставляете их в HashTable, HashMap или HashSet.

Чуть более подробное объяснение поможет понять механизм хранения и извлечения HashTable. Внутри HashTable содержатся сегменты, в которых хранятся пары ключ / значение. HashTable использует хэш-код ключа, чтобы определить, с каким сегментом должна сопоставляться пара ключ / значение.

На изображении выше показан HashTable и его сегменты. Когда вы передаете ключ / значение в HashTable, он запрашивает хэш-код ключа. HashTable использует этот код для определения сегмента, в который следует поместить ключ / значение. Так, например, если хэш-код равен нулю, HashTable помещает значение в сегмент 0. Аналогично, если хэш-код равен двум, HashTable помещает значение в сегмент 2. (Это упрощенный пример; HashTable будет массировать хэш-код. во-первых, чтобы HashTable не пытался вставить значение вне корзины.)

Используя хэш-код таким образом, HashTable может также быстро определить, в какой сегмент он поместил значение, когда вы пытаетесь его получить.

Однако хеш-коды представляют только половину картины. Хэш-код только сообщает HashTable, в какую корзину следует поместить ключ / значение. Иногда, однако, несколько объектов могут отображаться в одной и той же корзине, и это событие называется столкновением. В Java HashTable реагирует на коллизию, помещая несколько значений в одно и то же ведро (другие реализации могут обрабатывать коллизии по-разному). Ниже показано, как HashTable может выглядеть после нескольких столкновений.

Теперь представьте, что вы вызываете get () с ключом, который отображается в Bucket 0. Теперь HashTable нужно будет выполнить последовательный поиск по парам ключ / значение в Bucket 0, чтобы найти запрошенное значение. Чтобы выполнить этот поиск, HashTable выполняет следующие шаги:

  1. Запросить хэш-код ключа
  2. Получить список ключей / значений, находящихся в сегменте, заданном хэш-кодом.
  3. Последовательно просматривайте каждую запись, пока не будет найден ключ, равный ключу, переданному в get ().

Я знаю длинный ответ на короткий вопрос, но становится еще хуже. Правильное переопределение equals() и hashCode() - нетривиальное упражнение. Вы должны проявить особую осторожность, чтобы гарантировать контракты обоих методов.

Согласно hashCode() документации Javadoc, метод должен соответствовать следующим правилам:

  • Каждый раз, когда он вызывается для одного и того же объекта более одного раза во время выполнения приложения Java, метод hashCode() должен последовательно возвращать одно и то же целое число, при условии, что никакая информация, используемая в равных сравнениях для объекта, не изменяется. Это целое число не обязательно должно оставаться согласованным от одного выполнения приложения к другому выполнению того же самого приложения.
  • Если два объекта равны согласно методу equals(Object), то вызов метода thehashCode() для каждого из двух объектов должен привести к одинаковому целочисленному результату.
  • Не требуется, чтобы, если два объекта не равны согласно методу equals(Object), тогда вызов метода hashCode() для каждого из двух объектов должен давать различные целочисленные результаты. Однако программист должен знать, что получение различных целочисленных результатов для неравных объектов может улучшить производительность хэш-таблиц.

Создание hashCode ()

Создание правильно работающего hashCode() метода оказывается трудным; это становится еще более трудным, если рассматриваемый объект не является неизменным. Вы можете вычислить хэш-код для данного объекта разными способами. Самый эффективный метод основывает число на полях объекта. Более того, вы можете комбинировать эти значения по-разному. Вот два популярных подхода:

  • Вы можете превратить поля объекта в строку, объединить строки и вернуть полученный хэш-код.
  • Вы можете добавить хэш-код каждого поля и вернуть результат

Хотя существуют и другие, более тщательные подходы, два вышеупомянутых подхода оказываются наиболее простыми для понимания и реализации.

по сравнению с()

Он находится в той же лиге, что и equals () и hashcode (), и используется для реализации естественного порядка объектов. Метод compareTo () определен в интерфейсе java.lang.Comparable и используется для реализации естественной сортировки классов Java. Естественная сортировка означает порядок сортировки, который естественным образом применяется к объекту, например. лексический порядок для String, числовой порядок для Integer или Sorting Employee по его идентификатору и т. д. Большинство основных классов Java, включая String и Integer, реализуют метод CompareTo () и обеспечивают естественную сортировку.

Необходимость

Сортировка - это важная часть разработки приложений, которую вам часто требуется реализовать в своей системе. в Java сортировка реализована с помощью Comparator и Comparable в Java. Поскольку мы храним java-объекты в Collection, также есть определенные Set и Map, которые обеспечивают автоматическую сортировку, когда вы вставляете в него элемент, например. TreeSet и TreeMap. для реализации сортировки вам необходимо переопределить метод compareTo (Object o) или класс Comparable или метод compare (Object o1, Object o2) класса Comparator. Большинство классов реализуют Comparable для реализации естественного порядка. например, если вы пишете объект Employee, вы, вероятно, захотите реализовать интерфейс Comparable и переопределить метод compareTo () для сравнения текущего сотрудника с другим сотрудником на основе идентификатора. По сути, вам нужно переопределить compareTo (), потому что вам нужно отсортировать элементы в ArrayList или любой другой коллекции.

Внедрение Comparable позволяет:

  • вызов Collections.sort и Collections.binarySearch
  • вызов Arrays.sort и Arrays.binarySearch
  • использование объектов в качестве ключей в TreeMap
  • использование объектов как элементов в TreeSet

Как

Метод compareTo должен удовлетворять следующим условиям. Эти условия имеют целью позволить объектам быть полностью отсортированными, подобно сортировке набора результатов базы данных по всем полям.

  • антикоммутация: x.compareTo (y) является противоположным знаком y.compareTo (x)
  • Симметрия исключений: x.compareTo (y) генерирует точно такие же исключения, что и y.compareTo (x)
  • транзитивность: если x.compareTo (y) ›0 и y.compareTo (z)› 0, то x.compareTo (z) ›0 (и то же самое для меньшего, чем)
  • если x.compareTo (y) == 0, то x.compareTo (z) имеет тот же знак, что и y.compareTo (z)
  • согласованность с equals настоятельно рекомендуется, но не обязательно: x.compareTo (y) == 0, если и только если x.equals (y); согласованность с equals требуется для обеспечения правильного поведения сортированных коллекций (таких как TreeSet).

Можно значительно повысить производительность compareTo, сравнивая сначала элементы, которые, скорее всего, будут отличаться. Кроме того, если не требуется, всегда реализуйте интерфейс Comparable вместо расширения класса Comparable.

Сравните различные типы полей следующим образом:

  • числовой примитив: используйте ‹и›. Из этого правила есть исключение: примитивы float и double следует сравнивать с помощью Float.compare (float, float) и Double.compare (double, double). Это позволяет избежать проблем, связанных со специальными значениями границ.
  • логический примитив: используйте тесты формы (x &&! y)
  • Объект: используйте compareTo. (Обратите внимание, что поля с возможно нулевым значением представляют проблему: в то время как x.equals (null) возвращает false, x.compareTo (null) всегда будет генерировать исключение NullPointerException)
  • типобезопасное перечисление: используйте compareTo, как любой объект
  • коллекция или массив: Comparable, похоже, не предназначен для таких полей. Например, List, Map и Set не реализуют Comparable. Кроме того, в некоторых коллекциях нет определенного порядка итерации, поэтому поэлементное сравнение не может иметь смысла в таких случаях.

Пример:

import java.util.*;
import java.io.*;
public final class Account implements Comparable<Account> {
  
  enum AccountType {CASH, MARGIN, RRSP};

   public Account (
      String aFirstName,
      String aLastName,
      int aAccountNumber,
      int aBalance,
      boolean aIsNewAccount,
      AccountType aAccountType
  ) {
      //..parameter validations elided
      fFirstName = aFirstName;
      fLastName = aLastName;
      fAccountNumber = aAccountNumber;
      fBalance = aBalance;
      fIsNewAccount = aIsNewAccount;
      fAccountType = aAccountType;
   }

  /**
  * @param aThat is a non-null Account.
  *
  * @throws NullPointerException if aThat is null.
  */
  @Override public int compareTo(Account aThat) {
    final int BEFORE = -1;
    final int EQUAL = 0;
    final int AFTER = 1;

    //this optimization is usually worthwhile, and can
    //always be added
    if (this == aThat) return EQUAL;

    //primitive numbers follow this form
    if (this.fAccountNumber < aThat.fAccountNumber) return BEFORE;
    if (this.fAccountNumber > aThat.fAccountNumber) return AFTER;

    //booleans follow this form
    if (!this.fIsNewAccount && aThat.fIsNewAccount) return BEFORE;
    if (this.fIsNewAccount && !aThat.fIsNewAccount) return AFTER;

    //objects, including type-safe enums, follow this form
    //note that null objects will throw an exception here
    int comparison = this.fAccountType.compareTo(aThat.fAccountType);
    if (comparison != EQUAL) return comparison;

    comparison = this.fLastName.compareTo(aThat.fLastName);
    if (comparison != EQUAL) return comparison;

    comparison = this.fFirstName.compareTo(aThat.fFirstName);
    if (comparison != EQUAL) return comparison;

    if (this.fBalance < aThat.fBalance) return BEFORE;
    if (this.fBalance > aThat.fBalance) return AFTER;

    //all comparisons have yielded equality
    //verify that compareTo is consistent with equals (optional)
    assert this.equals(aThat) : "compareTo inconsistent with equals.";

    return EQUAL;
  }

   /**
   * Define equality of state.
   */
   @Override public boolean equals(Object aThat) {
     if (this == aThat) return true;
     if (!(aThat instanceof Account)) return false;

     Account that = (Account)aThat;
     return
       ( this.fAccountNumber == that.fAccountNumber ) &&
       ( this.fAccountType == that.fAccountType ) &&
       ( this.fBalance == that.fBalance ) &&
       ( this.fIsNewAccount == that.fIsNewAccount ) &&
       ( this.fFirstName.equals(that.fFirstName) ) &&
       ( this.fLastName.equals(that.fLastName) )
     ;
   }

   /**
   * A class that overrides equals must also override hashCode.
   */
   @Override public int hashCode() {
     int result = HashCodeUtil.SEED;
     result = HashCodeUtil.hash( result, fAccountNumber );
     result = HashCodeUtil.hash( result, fAccountType );
     result = HashCodeUtil.hash( result, fBalance );
     result = HashCodeUtil.hash( result, fIsNewAccount );
     result = HashCodeUtil.hash( result, fFirstName );
     result = HashCodeUtil.hash( result, fLastName );
     return result;
   }

   //PRIVATE

   private String fFirstName; //non-null
   private String fLastName;  //non-null
   private int fAccountNumber;
   private int fBalance;
   private boolean fIsNewAccount;

   /**
   * Type of the account, expressed as a type-safe enumeration (non-null).
   */
   private AccountType fAccountType;

   /**
   * Exercise compareTo.
   */
   public static void main (String[] aArguments) {
     //Note the difference in behaviour in equals and compareTo, for nulls:
     String text = "blah";
     Integer number = new Integer(10);
     //x.equals(null) always returns false:
     System.out.println("false: " + text.equals(null));
     System.out.println("false: " + number.equals(null) );
     //x.compareTo(null) always throws NullPointerException:
     //System.out.println( text.compareTo(null) );
     //System.out.println( number.compareTo(null) );

     Account flaubert = new Account(
      "Gustave", "Flaubert", 1003, 0,true, AccountType.MARGIN
     );

     //all of these other versions of "flaubert" differ from the
     //original in only one field
     Account flaubert2 = new Account(
       "Guy", "Flaubert", 1003, 0, true, AccountType.MARGIN
     );
     Account flaubert3 = new Account(
       "Gustave", "de Maupassant", 1003, 0, true, AccountType.MARGIN
     );
     Account flaubert4 = new Account(
       "Gustave", "Flaubert", 2004, 0, true, AccountType.MARGIN
     );
     Account flaubert5 = new Account(
       "Gustave", "Flaubert", 1003, 1, true, AccountType.MARGIN
     );
     Account flaubert6 = new Account(
       "Gustave", "Flaubert", 1003, 0, false, AccountType.MARGIN
     );
     Account flaubert7 = new Account(
       "Gustave", "Flaubert", 1003, 0, true, AccountType.CASH
     );

     System.out.println( "0: " +  flaubert.compareTo(flaubert) );
     System.out.println( "first name +: " +  flaubert2.compareTo(flaubert) );
     //Note capital letters precede small letters
     System.out.println( "last name +: " +  flaubert3.compareTo(flaubert) );
     System.out.println( "acct number +: " +  flaubert4.compareTo(flaubert) );
     System.out.println( "balance +: " +  flaubert5.compareTo(flaubert) );
     System.out.println( "is new -: " +  flaubert6.compareTo(flaubert) );
     System.out.println( "account type -: " +  flaubert7.compareTo(flaubert) );
   }
}

// ================================
// A sample run of this class gives: 
>java -cp . Account 
false: false 
false: false 
0: 0 
first name +: 6 
last name +: 30 
acct number +: 1 
balance +: 1 
is new -: -1 
account type -: -1

Поведение вышеупомянутой программы отличается от более ранней версии JDK 1.4.

Спасибо.