有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java如何以与List相同的方式计算流的哈希代码。hashCode()

我刚刚意识到,使用Stream.reduce(...)实现以下算法来计算流的哈希代码是不可能的。问题是哈希代码的初始种子是1,这不是累加器的标识

关于List.hashCode()的算法 :

int hashCode = 1;
for (E e : list)
  hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());

您可能会认为以下内容是正确的,但事实并非如此,尽管如果不拆分流处理,它将起作用

List<Object> list = Arrays.asList(1,null, new Object(),4,5,6);
int hashCode = list.stream().map(Objects::hashCode).reduce(1, (a, b) -> 31 * a + b);

似乎唯一明智的方法是获取StreamIterator并进行正常的顺序处理或首先将其收集到List


共 (4) 个答案

  1. # 1 楼答案

    Holger写对了solution,如果你想要一个简单的方法,还有两种可能性:

    一,。收集到List并呼叫hashCode()

    Stream<? extends Object> stream;
    int hashCode = stream.collect(toList()).hashCode();
    

    二,。使用Stream.iterator()

    Stream<? extends Object> stream;
    Iterator<? extends Object> iter = stream.iterator();
    int hashCode = 1;
    while(iter.hasNext()) {
      hashCode = 31 *hashCode + Objects.hashCode(iter.next());
    }
    

    提醒一下List.hashCode()使用的算法:

    int hashCode = 1;
    for (E e : list)
      hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
    
  2. # 2 楼答案

    作为第一种方法,我会使用collect-to-a-list解决方案,只要您不担心性能问题。这样你就避免了重新实现轮子,如果有一天哈希算法发生了变化,你就会从中受益,如果流是并行的,你也会安全(即使我不确定这是不是一个真正的问题)

    我实现它的方式可能会有所不同,这取决于您需要如何以及何时比较不同的数据结构(我们称之为^{

    如果手动且少量地执行此操作,一个简单的静态功能可能就足够了:

    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. # 3 楼答案

    我找到的最简单、最短的方法是使用Collectors.reducing实现Collector

    /**
     * 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);
    }
    
  4. # 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⁰
    

    或者对于大小为n的任意List

    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();