TreeMap 使用注意事项

数据结构集合框架这种内功总是会被忽略,这次总算是掉到“坑”里一次,也算是一个警醒。

一个 POJO:

@Getter
@Setter
@ToString
@AllArgsConstructor
public class User {

    private Integer id;
    private String name;
    private Integer seq;
}

先以老生常谈的 HashMap (1.8)为例,如果我以 User 对象作为 key 进行存储:

public static void main(String[] args) {
    Map<User, String> map = new HashMap<>();
    map.put(new User(1, "张一", 1), "a");
    map.put(new User(3, "张三", 3), "c");
    map.put(new User(2, "张二", 2), "b");
    map.forEach((k, v) -> System.out.println(k + "->" + v));
}

此时 IDEA 也会有提示,说需要重写 User 类的 hashCode()equals() 方法:

[外链图片转存失败(img-r5J27cTl-1566780350982)(./1.png)]

其实这个提示说的已经很明显了,主要是因为 HashMap 存储的时候会根据 key 的 hashCode() 去判断应该落在哪个桶里,如果这个桶里已经有数据了,会根据 hash 值和 equals() 一起再作进一步判断:

...
...
if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
...
...

HashSetHashMap 是一样的,因为 HashSet 其实就是 HashMap 对 key 的实现,value 就是一个 final 参数,PRESENT,可以看到 HashSet 里面没有几行代码的,都是用的 HashMap 的。

这也是为什么会有这样一段注释的原因:

* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.

那么 TreeMap 是怎么样的呢,TreeMap 是基于红黑树实现的:

 * A Red-Black tree based {@link NavigableMap} implementation.
 * The map is sorted according to the {@linkplain Comparable natural
 * ordering} of its keys, or by a {@link Comparator} provided at map
 * creation time, depending on which constructor is used.

基于 Comparator 进行比较插入新的元素。

先看下面这个例子:

扫描二维码关注公众号,回复: 9248313 查看本文章

POJO:

@Getter
@Setter
@ToString
@AllArgsConstructor
public class User {

    private Integer id;
    private String name;
    private Integer seq;
}
 public static void main(String[] args) {
        Map<User, String> map = new TreeMap<>();
        map.put(new User(1, "张一", 1), "a");
        map.put(new User(3, "张三", 1), "c");
        map.put(new User(2, "张二", 2), "b");
        map.forEach((k, v) -> System.out.println(k + "->" + v));

    }

此时程序运行出现了异常:

Exception in thread "main" java.lang.ClassCastException: dongguabai.demo.testing.testTreeMap.User cannot be cast to java.lang.Comparable
	at java.util.TreeMap.compare(TreeMap.java:1294)
	at java.util.TreeMap.put(TreeMap.java:538)
	at dongguabai.demo.testing.testTreeMap.TestMain.main(TestMain.java:17)

进入 TreeMap 源码可以看到在 put() 方法中会调用 compare() 方法:

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        ...
        ...
      }

而在 compare() 方法中要么使用构造传入的 Comparator 要么就是 key 实现了 Comparable 接口:

    final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }

我这里又没有传入比较器又没有实现 Comparable 接口,所以会出现类型转换异常。

之前也介绍过了,TreeMap 是基于红黑树实现的,那么在插入元素的时候,首先要将插入的元素融入树中。在 TreeMap 中就是通过 Comparator 或者 Comparable 实现的。再看一个例子:

    public static void main(String[] args) {
        Map<User, String> map = new TreeMap<>(Comparator.comparing(User::getSeq));
        map.put(new User(1, "张一", 1), "a");
        map.put(new User(3, "张三", 3), "c");
        map.put(new User(2, "张二", 2), "b");
        map.forEach((k, v) -> System.out.println(k + "->" + v));
    }

我这里传入了一个基于 seq 的升序比较器,所以程序最终输出结果为:

User(id=1, name=张一, seq=1)->a
User(id=2, name=张二, seq=2)->b
User(id=3, name=张三, seq=3)->c

那么再变化一下:

    public static void main(String[] args) {
        Map<User, String> map = new TreeMap<>(Comparator.comparing(User::getSeq));
        map.put(new User(1, "张一", 1), "a");
        map.put(new User(3, "张三", 3), "c");
        //map.put(new User(2, "张二", 2), "b");
        map.put(new User(2, "张二", 1), "b");
        map.forEach((k, v) -> System.out.println(k + "->" + v));
    }

先传入了一个 seq 为 1 的 User 对象,随后又传入了一个 User 为 1 的对象,再看输出结果:

User(id=1, name=张一, seq=1)->b
User(id=3, name=张三, seq=3)->c

可以发现后传入的 seq 为 1 的User对象的 value 覆盖了前面 seq 为 1 的 User 对象的 value,但是 key 还是没有变,那么这是什么原因呢,可以再看看 put() 方法:

do {
    parent = t;
    cmp = k.compareTo(t.key);
    if (cmp < 0)
    	t = t.left;
    else if (cmp > 0)
    	t = t.right;
    else
    	return t.setValue(value);
} while (t != null);

红黑树是以 key 来进行排序的,所以这里以 key 来进行比较检索出合适的叶子结点,而比较多依据就是 Comparator 或者 Comparable ,如果比较出来的结果是 0,那么后面的 value 会覆盖前面的 value。所以在使用 Map 的时候,一定要注意底层的数据结构和对 key 的处理方式。

欢迎关注公众号:
​​在这里插入图片描述

发布了406 篇原创文章 · 获赞 127 · 访问量 81万+

猜你喜欢

转载自blog.csdn.net/Dongguabai/article/details/100070757