本文JDK源码来自JDK 1.8
1 SortedMap接口
作者是Josh Bloch,主导了Java平台很多特性的设计和实现,包括著名的Java Collections框架,java.math包,还是《Effective Java》一书的作者。
SortedMap接口(从Java 1.2起),映射根据其键的自然顺序排序(即对Comparable接口的实现),或者由创建时提供的Comparator排序。 声明了一下方法:
// 返回对键排序的comparator,如果使用键的自然排序,则返回null
Comparator<? super K> comparator();
// 返回子视图,可包含fromKey且不包含toKey
SortedMap<K,V> subMap(K fromKey, K toKey);
// 返回一个从头部起、到toKey为止(不包含)的子视图
SortedMap<K,V> headMap(K toKey);
// 返回一个从fromKey(可包含))起、到尾部为止的子视图
SortedMap<K,V> tailMap(K fromKey);
// 返回value的集合
Collection<V> values();
2 NavigableMap接口
Java 1.6中新增了NavigableMap,源码中部分描述如下:
A SortedMap extended with navigation methods returning the closest matches for given search targets.
一个SortedMap,扩展了导航方法,返回给定搜索目标的最接近匹配项。All of these methods are designed for locating, not traversing entries.
所有这些方法都是为定位而设计的,而不是遍历条目。A NavigableMap may be accessed and traversed in either ascending or descending key order.
NavigableMap可以按升序或降序键来访问和遍历。
NavigableMap是SortedMap的子接口,提供了更多查找方法。
// 返回比给定key小且最接近的那个Entry
Map.Entry<K,V> lowerEntry(K key);
// 返回与给定key相等,或比给定key小且最接近的那个Entry
Map.Entry<K,V> floorEntry(K key);
// 返回与给定key相等,或比给定key大且最接近的那个Entry
Map.Entry<K,V> ceilingEntry(K key);
// 返回比给定key大且最接近的那个Entry
Map.Entry<K,V> higherEntry(K key);
// 返回第一个Entry(升序时即key最小的,降序时即key最大的)
Map.Entry<K,V> firstEntry();
// 返回最后一个Entry(升序时即key最大的,降序时即key最小的)
Map.Entry<K,V> lastEntry();
// 移除第一个Entry,相应的有pollLastEntry
Map.Entry<K,V> pollFirstEntry();
// 返回将原Map排序反转后的一个视图,对此视图的修改,将作用在原Map上。
NavigableMap<K,V> descendingMap();
// 返回Map某个区间的视图,可包含fromKey,但不会含toKey
SortedMap<K,V> subMap(K fromKey, K toKey);
// 返回给定Key之前所有entry的一个视图,不包含toKey
SortedMap<K,V> headMap(K toKey);
// 返回给定Key之后所有entry的一个视图,可包含fromKey
SortedMap<K,V> tailMap(K fromKey);
// 与tailMap(K fromKey)相似,可指定能否包含给定key
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
3 TreeMap类
3.1 认识TreeMap
TreeMap,一个基于红黑树的NavigableMap子实现,根据键的自然顺序或在创建Map时提供的Comparator进行排序。为containsKey、get、put和remove操作提供了保证的log(n)时间成本。
TreeMap并不像HashMap那样基于散列表实现,因此TreeMap中没有键的哈希冲突,也没有负载因子,也不涉及扩容。
TreeMap的主要属性如下:
// 用于维护顺序,如果使用key的自然排序,则它为null
private final Comparator<? super K> comparator;
// 树的根节点
private transient Entry<K,V> root;
// 节点数量
private transient int size = 0;
// 常量,标记节点颜色
private static final boolean RED = false;
private static final boolean BLACK = true;
// Entry、Key的集合视图
private transient EntrySet entrySet;
private transient KeySet<K> navigableKeySet;
TreeMap的创建方式主要有两种:
3.2 put重复key
TreeMap中的重复key,指两个key比较大小时相等,即key1.compareTo(key2)==0,或comparator.compare(key1, key2)==0。而不是指key1.equals(key2)==true。
因此,创建TreeMap未提供comparator时,要求key必须实现Comparable接口。
但是,不要求key的类型覆写equals()和hashCode(),因为TreeMap不使用equals()和hashCode()。
当put一个重复的key时,会用新value替换旧value,而键值对的key不变。
3.3 注意key对Comparable或Comparator的实现
在put()方法中,通过comparator或自然顺序,从树的root节点开始,逐层比较待插入key与已有entry的key,直到找到合适的位置(或者找到一个相等的key来覆盖它的值)
来看看get(Object key)方法,也是依赖于key的大小比较。
可见,使用TreeMap时,正确定义key的Comparable接口或Comparator接口,十分重要,特别要明确key的相等条件。
3.4 key不能未null
put()方法中有如下代码:当key==null时,将抛出NullPointerException。
下面示例报错的原因在于,Integer.compareTo()方法需要拆箱,null拆箱会报空指针。
4 一个反例——错误地Comparator
4.1 key相等时未返回0
假设学校要对某个学科,将学生按成绩排序,先定义Student类:
public class Student {
// 用于生成id
public static AtomicInteger identity = new AtomicInteger(1);
private String name;
// 分数
private Integer score;
// 唯一学号
private final Integer id;
public Student(String name, Integer score) {
this.id = identity.getAndIncrement();
this.name = name;
this.score = Objects.isNull(score) || score < 0 ? 0 : score;
}
public String getName() {
return name;
}
public Integer getScore() {
return score;
}
public Integer getId() {
return id;
}
@Override
public String toString() {
return name + "-" + id;
}
}
当你写下这样的代码时,会发现无法从TreeMap中查找任何学生:因为Comparator没有定义score相同时返回0。
public class TreeMapTest {
public static void main(String[] args) {
TreeMap<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 成绩降序
return o1.getScore() > o2.getScore() ? -1 : 1;
}
});
Student student1 = new Student("张三", 77);
Student student2 = new Student("李四", 64);
Student student3 = new Student("王五", 91);
Student student4 = new Student("赵六", 77);
map.put(student1, student1.getId());
map.put(student2, student2.getId());
map.put(student3, student3.getId());
map.put(student4, student4.getId());
// 输出:[王五-3, 张三-1, 赵六-4, 李四-2]
System.out.println(map.keySet());
// 输出:false
System.out.println(map.containsKey(student4));
}
}
4.2 判断key相等未使用标识性属性
于是,你修改了Comparator的定义:当score相同时返回0。
TreeMap<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 成绩降序
if (o1.getScore() == o2.getScore()) {
return 0;
}
return o1.getScore() > o2.getScore() ? -1 : 1;
}
});
这下TreeMap确实可以查找任一学生了。但是,意外再次来临:
当执行
System.out.println(map.keySet())时,返回了[王五-3, 张三-1, 李四-2],赵六哪去了?
向TreeMap中put重复key时,只会用新值覆盖旧值,而key不变。因为赵六的成绩与张三相等,导致赵六并没有真正放入TreeMap中。
所以,在定义TreeMap的Comparator或Comparable时,除了定义排序规则外,还需思考Key相等的根本条件。
上面例子中,当score相等即判定两个Student相同,显然不合适;应当加入Student的标识性信息,如只有id相等时,才会返回0。
TreeMap<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 成绩降序
if (o1.getScore() == o2.getScore()) {
// 成绩相同时,按学号升序排序
return o1.getId() < o2.getId() ? -1 : 1;
}
return o1.getScore() > o2.getScore() ? -1 : 1;
}
});
5 TreeMap实现一致性路由算法
5.1 什么是一致性路由算法
在分布式系统处理请求路由时,将来自某个IP的多次请求,都会被分发给固定的机器去处理,即一致性路由。
为了使流量在服务集群中分布地更加均匀,避免出现倾斜,往往会将一个服务节点虚拟化为N个,节点的key即ip地址的哈希值,从而形成一个hash环。
当请求某个服务时,根据请求来源IP的哈希值,从hash环中查找距离最近的一个服务节点,并由这个节点来处理该请求。
5.2 使用TreeMap实现
我们已知:TreeMap能够将Entry排序,查找与给定目标最接近的匹配项。 因此,使用TreeMap能方便地实现一致性路由功能。如下:
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
public class ConsistentHashRoute {
// 每个server虚拟化后的节点个数
private static final int VIRTUAL_NODE_NUM = 100;
private volatile List<String> serverAddressList;
private volatile TreeMap<Long, String> hashRing;
public ConsistentHashRoute(List<String> serverAddressList) {
if (Objects.isNull(serverAddressList) || serverAddressList.isEmpty()) {
throw new IllegalArgumentException("Servers are empty.");
}
this.serverAddressList = serverAddressList;
this.hashRing = hashRing(this.serverAddressList);
}
// 可以使用更好的hash算法,以减少hash冲突
private static long hash(String key) {
return key.hashCode();
}
// 使用TreeMap实现hash环
private TreeMap<Long, String> hashRing(List<String> serverAddressList) {
TreeMap<Long, String> hashRing = new TreeMap<>();
for (String address : serverAddressList) {
// 每个节点虚化为100个节点
for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
long addressHash = hash(address + "-NODE-" + i);
hashRing.put(addressHash, address);
}
}
return hashRing;
}
public String route(String sourceIp) {
Map.Entry<Long, String> ceilingEntry = hashRing.ceilingEntry(hash(sourceIp));
// 存在最接近的一个entry
if (Objects.nonNull(ceilingEntry)) {
return ceilingEntry.getValue();
}
// 不存在时取第一个entry
return hashRing.firstEntry().getValue();
}
// 追加Server
public synchronized void addServer(String serverAddress) {
serverAddressList.add(serverAddress);
hashRing = hashRing(this.serverAddressList);
}
// 移除Server
public synchronized void removeServer(String serverAddress) {
serverAddressList.remove(serverAddress);
hashRing = hashRing(this.serverAddressList);
}
}
来测试一下,可以看到当Server列表无变化时,固定来源的请求总会被分发给固定的server节点。
import java.util.ArrayList;
import java.util.List;
public class ConsistentHashRouteTest {
private static final List<String> servers = new ArrayList<>();
static {
servers.add("192.168.10.221");
servers.add("192.168.10.89");
servers.add("192.168.10.13");
servers.add("192.10.122.5");
servers.add("192.10.122.84");
servers.add("192.10.122.132");
}
public static void main(String[] args) {
ConsistentHashRoute hashRoute = new ConsistentHashRoute(servers);
String sourceIp = "192.25.13.59";
for (int i = 0; i < 10; i++) {
System.out.println(hashRoute.route(sourceIp));
}
}
}