4 ответа

Лучший ответ

Хотя на первый взгляд алгоритм хэш-кода кажется непараллелизируемым из-за его неассоциативности, это возможно, если мы преобразуем функцию:

((a * 31 + b) * 31 + c ) * 31 + d

К

a * 31 * 31 * 31 + b * 31 * 31 + c * 31 + d

Который в основном

a * 31³ + b * 31² + c * 31¹ + d * 31⁰

Или для произвольного List размера n:

1 * 31ⁿ + e₀ * 31ⁿ⁻¹ + e₁ * 31ⁿ⁻² + e₂ * 31ⁿ⁻³ +  …  + eₙ₋₃ * 31² + eₙ₋₂ * 31¹ + eₙ₋₁ * 31⁰

Причем первое 1 является начальным значением исходного алгоритма, а eₓ - хэш-кодом элемента списка по индексу x. Хотя теперь слагаемые не зависят от порядка оценки, очевидно, что существует зависимость от положения элемента, которую мы можем решить, в первую очередь, путем потоковой передачи индексов, что работает для списков и массивов с произвольным доступом, или решить в целом с помощью сборщика, который отслеживает количество встреченных объектов. Сборщик может прибегать к повторным умножениям для накопления и прибегать к степенной функции только для объединения результатов:

static <T> Collector<T,?,Integer> hashing() {
    return Collector.of(() -> new int[2],
        (a,o)    -> { a[0]=a[0]*31+Objects.hashCode(o); a[1]++; },
        (a1, a2) -> { a1[0]=a1[0]*iPow(31,a2[1])+a2[0]; a1[1]+=a2[1]; return a1; },
        a -> iPow(31,a[1])+a[0]);
}
// derived from http://stackoverflow.com/questions/101439
private static int iPow(int base, int exp) {
    int result = 1;
    for(; exp>0; exp >>= 1, base *= base)
        if((exp & 1)!=0) result *= base;
    return result;
}
List<Object> list = Arrays.asList(1,null, new Object(),4,5,6);
int expected = list.hashCode();

int hashCode = list.stream().collect(hashing());
if(hashCode != expected)
    throw new AssertionError();

// works in parallel
hashCode = list.parallelStream().collect(hashing());
if(hashCode != expected)
    throw new AssertionError();

// a method avoiding auto-boxing is more complicated:
int[] result=list.parallelStream().mapToInt(Objects::hashCode)
    .collect(() -> new int[2],
    (a,o)    -> { a[0]=a[0]*31+Objects.hashCode(o); a[1]++; },
    (a1, a2) -> { a1[0]=a1[0]*iPow(31,a2[1])+a2[0]; a1[1]+=a2[1]; });
hashCode = iPow(31,result[1])+result[0];

if(hashCode != expected)
    throw new AssertionError();

// random access lists allow a better solution:
hashCode = IntStream.range(0, list.size()).parallel()
    .map(ix -> Objects.hashCode(list.get(ix))*iPow(31, list.size()-ix-1))
    .sum() + iPow(31, list.size());

if(hashCode != expected)
    throw new AssertionError();
12
Holger 9 Сен 2016 в 12:34

Самый простой и короткий способ, который я нашел, - это реализовать Collector с помощью Collectors.reducing:

/**
 * Creates a new Collector that collects the hash code of the elements.
 * @param <T> the type of the input elements
 * @return the hash code
 * @see Arrays#hashCode(java.lang.Object[])
 * @see AbstractList#hashCode()
 */
public static <T> Collector<T, ?, Integer> toHashCode() {
    return Collectors.reducing(1, Objects::hashCode, (i, j) -> 31 *  i + j);
}

@Test
public void testHashCode() {
    List<?> list = Arrays.asList(Math.PI, 42, "stackoverflow.com");
    int expected = list.hashCode();
    int actual = list.stream().collect(StreamUtils.toHashCode());
    assertEquals(expected, actual);
}
0
simon04 11 Июн 2020 в 08:58

В качестве первого подхода я бы использовал решение «сбор в список», если у вас нет проблем с производительностью. Таким образом вы избегаете повторной реализации колеса и если однажды алгоритм хеширования изменится, вы выиграете от этого, и вы также будете в безопасности, если поток будет распараллелен (даже если я не уверен, что это реальная проблема).

То, как я бы это реализовал, может варьироваться в зависимости от того, как и когда вам нужно сравнивать различные структуры данных (назовем это Foo).

Если вы делаете это вручную и редко, может быть достаточно простой статической функции:

public static int computeHash(Foo origin, Collection<Function<Foo, ?>> selectors) {
    return selectors.stream()
            .map(f -> f.apply(origin))
            .collect(Collectors.toList())
            .hashCode();
}

И используйте это так

if(computeHash(foo1, selectors) == computeHash(foo2, selectors)) { ... }

Однако, если экземпляры Foo сами хранятся в Collection и вам нужно реализовать как hashCode(), так и equals() (из Object), я бы обернул его внутри FooEqualable:

public final class FooEqualable {
    private final Foo origin;
    private final Collection<Function<Foo, ?>> selectors;

    public FooEqualable(Foo origin, Collection<Function<Foo, ?>> selectors) {
        this.origin = origin;
        this.selectors = selectors;
    }

    @Override
    public int hashCode() {
        return selectors.stream()
                .map(f -> f.apply(origin))
                .collect(Collectors.toList())
                .hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof FooEqualable) {
            FooEqualable that = (FooEqualable) obj;

            Object[] a1 = selectors.stream().map(f -> f.apply(this.origin)).toArray();
            Object[] a2 = selectors.stream().map(f -> f.apply(that.origin)).toArray();

            return Arrays.equals(a1, a2);
        }
        return false;
    }
}

Я полностью осознаю, что это решение не оптимизировано (с точки зрения производительности), если выполняется несколько вызовов hashCode() и equals(), но я стараюсь не оптимизировать, кроме случаев, когда это вызывает беспокойство.

3
Spotted 8 Сен 2016 в 11:32