0
点赞
收藏
分享

微信扫一扫

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;
...
...

​HashSet​​​ 和 ​​HashMap​​​ 是一样的,因为 ​​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​​ 进行比较插入新的元素。

先看下面这个例子:

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 的处理方式。




举报

相关推荐

0 条评论