推上看到的一道题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
4kingRAS
V2EX    Java

推上看到的一道题

  •  1
     
  •   4kingRAS Jun 8, 2021 3433 views
    This topic created in 1784 days ago, the information mentioned may be changed or developed.
     public static void main(String[] args) { List<StringBuilder> list = // ... StringBuilder sb = // ... Set<StringBuilder> set = new HashSet<>(list); set.add(sb); System.out.println(set.contains(sb)); // should print true sb.append("oops"); System.out.println(set.contains(sb)); // should print false } 

    替换 ... 的内容,实现第一次打印 true 第二次打印 false,JDK 11 以上环境。

    其他备注:

    • No reflection;
    • No hacking the output stream;
    • No unchecked code (e. g., List<StringBuilder> contains StringBuilder objects only);
    • No hidden replacement of library classes (List is standard java.util.List, Set is java.util.Set, etc.).

    目前只知道从 hashcode 入手,Stringbuilder 不可继承不好弄。

    Supplement 1    Jun 10, 2021

    Tagir自己写的答案,对Java学习很有帮助: https://twitter.com/tagir_valeev/status/1402520496143540228

    下面是我的理解:

    首先我们知道,HashSet里面是一个 HashMap ,set.contains(sb) 最终执行的是 HashMap 的contains 方法。通常情况下 contains 方法会先比较 hashCode,再调用 equals()

    StringBuilder 在 内容改变后(如 append ),它的 hashCode 是不会变的,而StringBuilder 也没有override equals() 也就是说如果不做什么改动,两次 print 都会打印 true 。

    而 StringBuilder 是个final 类,没法继承重写,怎么办?

    V2的Java 人我想八股文都背得滚瓜烂熟了吧,都知道Java 8 以后,hash冲突会形成链表,超过8个会变成红黑树,而红黑树在比较的时候会用 compareTo 而不是 equals 。如果用 compareTo 可以看到 StringBuilder 的 compareTo 实现是有比较内容的。

    所以, sb.append("oops"); 执行后,HashMap 在红黑树中比较就会返回 false。

    整个思路现在就很明朗了,就是制造大量的 hashCode 相同的 StringBuilder ,从而在 HashMap 中他们会都放在一个 bucket 里,并形成红黑树,调用set.contains(sb) 时,就会调用 compareTo

    具体的解法很多,以 Tagir 的代码为例,比较好懂:

    List<StringBuilder> list = Stream.generate(() -> { while (true) { StringBuilder sb = new StringBuilder("a"); int hc = sb.hashCode(); if (((hc ^ (hc >>> 16)) & 0x3F) == 0) { return sb; } } }).limit(40) .sorted(Comparator.comparingInt(Object::hashCode)) .toList(); 

    首先要想办法制造 hash 冲突的 StringBuilder,比较朴素的办法就是while循环里不断的new

    ((hc ^ (hc >>> 16)) & 0x3F) 这段其实就是hashMap 源码里的 hash(),0x3F即63, 是 hashMap里树化后的最小长度:

    static final int MIN_TREEIFY_CAPACITY = 64;

    static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 

    让 hash 等于 0 是为了让他们落在第一个bucket里。

    构造这40个之后排序一下,之所以是40个,跟树化有关,要保证这40个 StringBuilder 恰好落在同一个 bucket里并形成红黑树。

    StringBuilder sb = Stream.generate(StringBuilder::new) .dropWhile(b -> Collections.binarySearch(list, b, Comparator.comparingInt(Object::hashCode)) < 0) .findFirst().get(); 

    构造 sb 很简单,不断 new 一个StringBuilder,找到 hash碰撞的那一个 即可。

    完事,当第二次执行 set.contains(sb) 时,因为会调用 compareTo ,而内容已经变化,所以会返回错误的值。

    Supplement 2    Jun 10, 2021
    另一个高手的解法:

    大同小异,都是大量生成,然后找碰撞,其他解法思路一样,只是找碰撞的方式不同。不过都很值得学习。

    ```java
    List<StringBuilder> list = IntStream.range(0, 10_000_000)
    .mapToObj(i -> new StringBuilder(0))
    .filter(s -> ((s.hashCode() ^ s.hashCode() >>> 16) & 0xfff) == 0)
    .sorted(Comparator.comparingInt(Object::hashCode))
    .collect(Collectors.toList());

    StringBuilder sb = list.remove(IntStream.range(1, list.size())
    .filter(i -> list.get(i).hashCode() == list.get(i - 1).hashCode())
    .findFirst()
    .orElseThrow());
    ```
    7 replies    2021-06-10 11:19:24 +08:00
    iminto
        1
    iminto  
       Jun 8, 2021
    这跟 Stringbuilder 无关,就是 HashSet 的特性
    x66
        2
    x66  
       Jun 8, 2021
    插个眼,等思路。
    4kingRAS
        3
    4kingRAS  
    OP
       Jun 8, 2021
    低估这道题了,本来想着能重写 compareTo 或者 equals 这种简单思路。后来看到有个大佬做出来,是制造碰撞,我贴出原链接,研究透了说说,或者有大佬看懂的说说。

    twitter.com/quydxnguyen/status/1402151079635308544
    iminto
        4
    iminto  
       Jun 8, 2021
    我理看错了,楼主的理解是对的,最终就是重写 haset 中的 object,即 sb 的 equals 方法。但 sb 的 equals 不好重写
        5
    aguesuka  
       Jun 9, 2021
    /**
    * Returns k.compareTo(x) if x matches kc (k's screened comparable
    * class), else 0.
    */
    @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
    static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
    ((Comparable)k).compareTo(x));
    }

    hashmap 的这段代码, 如果 key instance Comparable,那么红黑树是按 Comparable 排序的.而 stringbuilder 的 equals 和 hashcode 是不可变的,但 compareTo 是可变的.

    The main conclusion from this puzzle: never use mutable objects as Map keys. You may get unpredictable results, even with HashMap.
    4kingRAS
        6
    4kingRAS  
    OP
       Jun 10, 2021
    回来填坑了,确实是好题,不过自己想不出来。看了别人的豁然开朗。
    **下面是我的理解:**

    首先我们知道,HashSet 里面是一个 HashMap ,`set.contains(sb)` 最终执行的是 HashMap 的 contains 方法。通常情况下 contains 方法会先比较 hashCode,再调用 `equals()`。

    StringBuilder 在 **内容改变后(如 append ),它的 hashCode 是不会变的**,而 StringBuilder 也没有 override `equals()` 也就是说如果不做什么改动,两次 print 都会打印 true 。

    而 StringBuilder 是个 final 类,没法继承重写,怎么办?

    V2 的 Java 人我想八股文都背得滚瓜烂熟了吧,都知道 Java 8 以后,hash 冲突会形成链表,超过 8 个会变成红黑树,而红黑树在比较的时候会用 `compareTo` 而不是 equals 。如果用 `compareTo` 可以看到 StringBuilder 的 `compareTo` 实现是有比较内容的。

    所以, `sb.append("oops");` 执行后,HashMap 在红黑树中比较就会返回 false 。

    整个思路现在就很明朗了,就是制造大量的 hashCode 相同的 StringBuilder,从而在 HashMap 中他们会都放在一个 bucket 里,并形成红黑树,调用`set.contains(sb)` 时,就会调用 `compareTo`

    具体的解法很多,以 Tagir 的代码为例,比较好懂:

    ```java
    List<StringBuilder> list = Stream.generate(() -> {
    while (true) {
    StringBuilder sb = new StringBuilder("a");
    int hc = sb.hashCode();
    if (((hc ^ (hc >>> 16)) & 0x3F) == 0) {
    return sb;
    }
    }
    }).limit(40)
    .sorted(Comparator.comparingInt(Object::hashCode))
    .toList();
    ```

    首先要想办法制造 hash 冲突的 StringBuilder,比较朴素的办法就是 while 循环里不断的 new

    `((hc ^ (hc >>> 16)) & 0x3F)` 这段其实就是 hashMap 源码里的 `hash()`,0x3F 即 63, 是 hashMap 里树化后的最小长度:

    `static final int MIN_TREEIFY_CAPACITY = 64;`

    ```java
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    ```

    让 hash 等于 0 是为了让他们落在第一个 bucket 里。

    构造这 40 个之后排序一下,之所以是 40 个,跟树化有关,要保证这 40 个 StringBuilder 恰好落在同一个 bucket 里并形成红黑树。

    ```java
    StringBuilder sb = Stream.generate(StringBuilder::new)
    .dropWhile(b -> Collections.binarySearch(list, b,
    Comparator.comparingInt(Object::hashCode)) < 0)
    .findFirst().get();
    ```

    构造 sb 很简单,不断 new 一个 StringBuilder,找到 hash 碰撞的那一个 即可。

    完事,当第二次执行 `set.contains(sb)` 时,因为会调用 compareTo,而内容已经变化,所以会返回错误的值。

    另一个高手的解法:

    大同小异,都是大量生成,然后找碰撞,其他解法思路一样,只是找碰撞的方式不同。不过都很值得学习。

    ```java
    List<StringBuilder> list = IntStream.range(0, 10_000_000)
    .mapToObj(i -> new StringBuilder(0))
    .filter(s -> ((s.hashCode() ^ s.hashCode() >>> 16) & 0xfff) == 0)
    .sorted(Comparator.comparingInt(Object::hashCode))
    .collect(Collectors.toList());

    StringBuilder sb = list.remove(IntStream.range(1, list.size())
    .filter(i -> list.get(i).hashCode() == list.get(i - 1).hashCode())
    .findFirst()
    .orElseThrow());
    ```

    如五楼所说,永远不要在 hashmap 里装可变对象,更深入的思考是当你的程序是建立在假设一些极小概率事情不可能发生的时候,要小心。因为某些时候极小概率事件会集中发生。
    4kingRAS
        7
    4kingRAS  
    OP
       Jun 10, 2021
    @4kingRAS 回复怎么删除啊,好蠢
    About     Help     Advertise     Blog     API     FAQ     Solana     4079 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 40ms UTC 00:52 PVG 08:52 LAX 17:52 JFK 20:52
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86