这是看过的第一个jdk源码(从立下目标以来):TreeMap。说实话断断续续的看了有好几天了,我觉得我犯了一个错误,就像一开始说的那样,我打算完完全全看懂TreeMap关于红黑树的实现方式,后来我想了想,相对于花费这个对我的收益并不是特别大,而且看的过程中也有很多困惑,虽然我知道它每一步在做什么,但是我还不知道为什么要这么做,经过权衡,我决定先暂时放下TreeMap中关于对红黑树具体的实现,暂且记下有这回事。

一、简单理解

  最近也在看数据结构和算法,对红黑树也有了大致的理解,TreeMap是对红黑树的Java实现完美实现版本。本篇不打算讨论TreeMap是怎么实现红黑树的。

1.1 使用Entry做为基本单元

  首先在红黑树中既要保存数据,还要保存数据所处的位置,自然而然就想到用一个对象来包装存储的数据,TreeMap中是用一个静态最终类Entry来实现这个功能的,而这个类是实现自Map的Entry接口:

static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK; Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
} public K getKey() {
return key;
} public V getValue() {
return value;
} public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
} public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o; return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
} public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
} public String toString() {
return key + "=" + value;
}
}

  可以看到每一个除了包含自己数据的key和value值,同样有左、右、父节点的变量,并包括一个默认的黑色字段,这样,一个红黑树节点的所有信息就包含进去了,通过不断扩充节点的left、right和parent来描述整个树。

  另外在equals中为了比较两个值是否相等还新建了一个比较方法:

static final boolean valEquals(Object o1, Object o2) {
return (o1==null ? o2==null : o1.equals(o2));
}

  这个比较方法避免了上层对null值的判断,其实在Objects里面已经有了,不过Objects出现的晚,所以说常用的工具方法就应该集中起来,不然每个类都写一遍可不是我心目中的jdk。

1.2 对transient的疑惑

  我对transient还是有基本的了解的,当序列化对象的时候,被transient标记的字段就不会被序列化,因此我看到如下代码时很惊讶:

private transient Entry<K,V> root;
private transient int size = 0;

  我认为根节点和容量是两个很重要的属性,序列化时为什么要去除呢,去除后会有错误的,为了验证我的想法我写了个测试代码:

TreeMap<Integer, Integer> map=new TreeMap<>();
map.put(1, 1);
map.put(2, 1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
oos.writeObject(map);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
TreeMap<Integer, Integer> mapClone = (TreeMap<Integer, Integer>) ois.readObject();
ois.close();
mapClone.put(3, 1);
System.out.println(mapClone);

  并在反序列化对象put新数据的时候,打断点进去看,发现root居然不是null,而size也不是0,也是真实容量2,我晕了,超出了我的理解。经过测试和查找没有解决问题,在这留个坑,希望有人能帮到我。

1.3 对吞异常的反感

  之前我写过一篇博客,是讲解异常的,可以去看一下:Java异常体系简析。所以我对把异常捕获而不做任何处理非常反感,因此看到以下代码,觉得非常不适:

public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}

  什么叫做cannotHappen,不可能发生那异常还有必要存在吗,我还是认为应该抛一个虚拟机异常。不理解的可以去看我上面说的那篇博客。

1.4 遍历时防止另外线程的修改

  TreeMap不是一个线程安全的类,当你在遍历一个TreeMap的时候,如果有另外一个线程修改了TreeMap,会立即抛异常,是通过一个变量来实现的:

private transient int modCount = 0;

  每次修改都会让这个数加一,然后遍历时每次都会判断这个数是否发生变化:

@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
int expectedModCount = modCount;
for (Entry<K, V> e = getFirstEntry(); e != null; e = successor(e)) {
action.accept(e.key, e.value); if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
}
}

  这个可以在一定程度上防止遍历时被修改,只是被动防御,但是真要想达到随意的增删改查还是要使用ConcurrentSkipListMap。

1.5 创建时没有值的做法

  当我们需要遍历集合的时候,有一种做法是先获取entrySet,然后遍历entrySet的Entry并获取key和value,在看到下面获取entrySet的代码时:

public Set<Map.Entry<K,V>> entrySet() {
EntrySet es = entrySet;
return (es != null) ? es : (entrySet = new EntrySet());
}

  产生了疑惑,当获取时若为null就去创建一个new EntrySet(),而它的构造方法就是一个空的???,也就是说返回的确实是空的一个EntrySet,但是使用它的时候确实也有值:

TreeMap<Integer,Integer> map=new TreeMap<>();
map.put(1, 1);
Set<Entry<Integer, Integer>> set = map.entrySet();
System.out.println(set);

  我在map.entrySet()这一行,debug进去一步一步看,确实是没有值,找了好一阵子,后来在debug这System.out.println(set)的时候才发现玄机。

  简单来说,当你获得entrySet的时候确实是没有值的,但是它内部持有一个TreeMap的一个遍历器,而在真正使用的时候才去遍历关联的TreeMap,感觉和jdk1.5的Stream一样,因此方法的注释也都说明是一个view。而TreeMap中大量应用了这种方式,我觉得其他集合应该也是这么实现的。

  这就给我们一种编程思路,不是说所有数据在获取的时候都需要给它准备好,等他真正使用的时候再给也不晚。

1.6 对常用方法的封装

  TreeMap值有一些这样的方法:

private static <K,V> boolean colorOf(Entry<K,V> p) {
return (p == null ? BLACK : p.color);
}
private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
return (p == null ? null: p.parent);
}
private static <K,V> void setColor(Entry<K,V> p, boolean c) {
if (p != null)
p.color = c;
}
private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
return (p == null) ? null: p.left;
}
private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
return (p == null) ? null: p.right;
}

  TreeMap中最常用的,判断和获取颜色,获取左右节点等,虽然可以通过【.】直接来获得属性,但是有时候还必须要判断是否是null值,所以当大量需要此处判断的时候,封装方法或许是我们最好的选择。

二、问题及总结

  这个TreeMap代码有3000多行,虽然也有很多注释,代码量依然很大,不过细看起来,只针对红黑树实现的增删改查并没有那么多的代码量,而多出来的实现,只是为了方便使用者提供了各种各样的方法,并因此而新建了很多内部类,而对于用户来说只是感觉到了方便。方法特别多,建议看Java API。稍微总结一点:

  • TreeMap是有序Map,按照指定比较器或者key的比较性
  • TreeMap不是线程安全的

2.1 遗留问题

  看到现在还遗留两个问题:

2.1.1 transient没有生效

  这个我随后会深入测试研究。

2.1.2 红黑树的具体实现方式

  这个我暂时不去研究,等以后需要自己实现的时候,再来找它。

TreeMap源码学习的更多相关文章

  1. 【jdk源码1】TreeMap源码学习

    这是看过的第一个jdk源码(从立下目标以来):TreeMap.说实话断断续续的看了有好几天了,我觉得我犯了一个错误,就像一开始说的那样,我打算完完全全看懂TreeMap关于红黑树的实现方式,后来我想了 ...

  2. TreeMap的源码学习

    TreeMap的源码学习 一).TreeMap的特点 根据key值进行排序. 二).按key值排序的两种排序算法实现 1).在构造方法中传入比较器 public TreeMap(Comparator& ...

  3. JDK源码学习笔记——TreeMap及红黑树

    找了几个分析比较到位的,不再重复写了…… Java 集合系列12之 TreeMap详细介绍(源码解析)和使用示例 [Java集合源码剖析]TreeMap源码剖析 java源码分析之TreeMap基础篇 ...

  4. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  5. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  6. Dubbo源码学习--集群负载均衡算法的实现

    相关文章: Dubbo源码学习文章目录 前言 Dubbo 的定位是分布式服务框架,为了避免单点压力过大,服务的提供者通常部署多台,如何从服务提供者集群中选取一个进行调用, 就依赖Dubbo的负载均衡策 ...

  7. TreeMap 源码分析

    简介 TreeMap最早出现在JDK 1.2中,是 Java 集合框架中比较重要一个的实现.TreeMap 底层基于红黑树实现,可保证在log(n)时间复杂度内完成 containsKey.get.p ...

  8. Java - TreeMap源码解析 + 红黑树

    Java提高篇(二七)-----TreeMap TreeMap的实现是红黑树算法的实现,所以要了解TreeMap就必须对红黑树有一定的了解,其实这篇博文的名字叫做:根据红黑树的算法来分析TreeMap ...

  9. HashMap的源码学习以及性能分析

    HashMap的源码学习以及性能分析 一).Map接口的实现类 HashTable.HashMap.LinkedHashMap.TreeMap 二).HashMap和HashTable的区别 1).H ...

随机推荐

  1. Linux——文件搜索命令简单笔记

    一: 命令名称:which 命令所在路径:/usr/bin/which 执行权限:所有用户 功能描述:显示系统命令所在目录 范例:$ which ls 还有一个whereeis ls 命令 二: 命令 ...

  2. 装B必备之 快捷键配置

    作为一个程序员 所有程序都用快捷来这是装B必备的无形装B 最为致命.... 开始搞起 第一步配置环境变量 在系统D盘新建一个文件夹  D:\cache; 然后把这个路径 配置上 D:\cache; 最 ...

  3. jdk1.8

    Jdk1.8新特性 毫无疑问,Java 8是Java自Java 5(发布于2004年)之后的最重要的版本.这个版本包含语言.编译器.库.工具和JVM等方面的十多个新特性.在本文中我们将学习这些新特性, ...

  4. spring boot:创建一个简单的web(maven web project)

    1.新建一个maven web project; 2.在pom.xml文件中添加相应的依赖包: 3.新建一个HelloController请求控制类: 4.编写index.jsp页面: 5.编写启动类 ...

  5. [Java学习] JSON工具学习记录--FastJSON

    最近做项目,总是与json打交道,在用了即可json工具后,个人认为fastJson最好用,很方便,API也清晰可见,所以记录下使用方法,给需要的人提供帮助.(部分摘抄自网络) 一.API入口 Fas ...

  6. English trip V1 - 6.Accidents Happen! 发生意外! Teacher:Corrine Key: 过去进行时 was or were + Ving

    In this lesson you will learn to talk about past occurences. 过去进行时 课上内容(Lesson) C: Hi, Loki! L: Hi, ...

  7. SSH 反向代理

    SSH反向代理 被控制端没有NAT或者没有静态公网IP,把本端一台服务器映射到外网给远端SSH进来,建立SSH反向隧道. 先映射本端机器到外网  nat server 2222to22 protoco ...

  8. C++技能重拾2

    13.类成员函数重载:局部同名函数将隐藏而不是重载全局声明,不引入父类名字空间时子类的同名函数不会和父类的构成重载,静态成员函数可以和非静态成员函数构成重载.本质是重载函数的定义是在相同的声明域里!! ...

  9. 有名管道mkfifo

    int mkfifo(const char *pathname, mode_t mode); int mknod(const char *pathname, mode_t mode, dev_t de ...

  10. SQL基础用法(实例二)

    /* 2006年10月01日 SQL Server 数据库的高级操作 (1) 批处理 (2) 变量 (3) 逻辑控制 (4) 视图 (5) 函数 (6) 高级查询 */ ()批处理 将多条SQL语句作 ...