【问题标题】:How to compute the hash code for a stream in the same way as List.hashCode()如何以与 List.hashCode() 相同的方式计算流的哈希码
【发布时间】:2016-09-08 08:16:17
【问题描述】:

我刚刚意识到使用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

【问题讨论】:

  • 问题是你为什么要计算hash code for a stream。你打算用它做什么?
  • 听起来像XY problemcomplex datastructure 不实现 hashCode() 吗?如果没有,您是否允许实施它?您是否真的需要在流上并行计算此哈希,以便 reduce 不相关?也许作为最后的手段,你考虑过stream.collect(Collectors.toList()).hashCode() 吗?
  • 所以如果我理解正确,你想知道 2 个对象是否相等,仅基于它们的一部分已被某些 Functions 提取/转换的状态?
  • 针对这个新问题,您能否也更新一下您的问题,以便更多人更容易地帮助您?
  • @Eran:因为我们可以。好吧,至少我喜欢解决问题。 ;^) 而且,谁知道呢,也许在未来,有人真的有一个非常大的列表要并行散列......

标签: java hash java-stream hashcode


【解决方案1】:

虽然乍一看,哈希码算法由于其非关联性似乎是不可并行的,但如果我们转换函数,这是可能的:

((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⁰

或者对于任意大小为nList

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

【讨论】:

  • @Roland:数组实际上只是缺少pair&lt;int,int&gt;tuple&lt;int,int&gt; 类型的解决方法。拥有可变容器类型是收集器的一般概念。即使是内置的收集器在需要可变的intlong 容器时也会使用单元素数组。
  • @Roland:这些是 unicode 字符,如果你有正确的键盘,很容易输入 ;^),例如我使用NEO2。如果您没有这样的键盘(布局),您可以简单地复制和粘贴,例如来自here
  • 哪个浏览器/操作系统?
  • 似乎只是字体问题。因此,除非有一个简单的替代方案(我们不在 TeX 网站上),否则我认为等待软件/字体赶上来是最好的选择。
  • 我测试了 Chrome、Explorer 和 Edge。但它是较新的 Windows 版本。我还使用 Android 进行了测试,其中某些字符无法在浏览器窗口中显示,但将文本从浏览器复制到其他输入字段,甚至是同一浏览器的地址字段,使其看起来正确。所以很明显,html视图中使用的字体缺少字符,但其他字体有。
【解决方案2】:

作为第一种方法,只要您不担心性能问题,我就会使用收集到列表的解决方案。这样一来,您就可以避免重新实现轮子,并且 如果 有一天哈希算法改变了您将从中受益,并且如果流是并行化的,您也是安全的(即使我不确定这是一个真正的问题)。

我实现它的方式可能会有所不同,具体取决于您需要比较不同数据结构的方式和时间(我们称之为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】:

    Holger 写了正确的solution,如果您想要一种简单的方法,还有另外两种可能性:

    1。收集到List并致电hashCode()

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

    2。使用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());
    

    【讨论】:

      【解决方案4】:

      我发现的最简单和最短的方法是使用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);
      }
      

      【讨论】:

      • 这是不正确的,因为 1 不是此缩减的标识。如果处理是并行的,它将中断。
      猜你喜欢
      • 1970-01-01
      • 2017-02-04
      • 2012-01-17
      • 2011-08-27
      • 2019-04-23
      • 2010-09-11
      • 1970-01-01
      • 2017-12-07
      • 2010-10-16
      相关资源
      最近更新 更多