集合中Map
一、说明HashMap、LinkedHashMap、Hashtable、TreeMap区别
Map : 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
- HashMap:非线程安全的,JDK1.8 之前 HashMap 由数组+链表组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突存在的(“拉链法”)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阀值(默认8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会先选择数组扩容,而不是转换为红黑树),将链表转换为红黑树,以减少搜索时间。
- LinkedHashMap:继承 HashMap,底层仍然是基于拉链式散列结构(数组和链表或红黑树)。LinkedHashMap 增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。通过对链表进行相应的操作,实现了访问顺序相关逻辑,
- Hashtable:线程安全的,数组+链表组成,数组是主体,链表为了解决哈希冲突。继承Dictionary类。
- TreeMap:红黑树(自平衡的排序二叉树)
key | value | |
HashMap | 只能一个key为null | 可以多个值为null |
Hashtable | 不能为null | 不能为null |
TreeMap | 不能为null | 不能为null |
二、HashMap 和 TreeMap 区别
TreeMap 和 HashMap 都继承自 AbstractMap,但是需要注意的是 TreeMap 它还实现了 NavigableMap 接口和 SortedMap 接口。
实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
实现 SortedMap 接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序。
public class Person {
private Integer age;
public Person(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public static void main(String[] args) {
TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
@Override
public int compare(Person person1, Person person2) {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
}
});
treeMap.put(new Person(4), "person1");
treeMap.put(new Person(28), "person2");
treeMap.put(new Person(35), "person3");
treeMap.put(new Person(30), "person4");
treeMap.entrySet().stream().forEach(personStringEntry -> {
System.out.println(personStringEntry.getValue());
});
}
}
输出:
person1
person2
person4
person3
三、如何选用集合
主要根据集合的特点选用,需要根据键值对获取元素值选用 Map 接口下集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap
四、应用场景
1、社保付款中服务费计算,不同服务费需要考虑不同的结算地区,ESC中系统数据源存放的地区按照省-市-区的三个字段存放,在EIS系统中存放的结算地区都是按照最后一级code存放
地区 | ESC(小爱) | EIS(自研) |
江苏省 | 320000 | 320000 |
江苏省苏州市 | 320000-320500 | 320500 |
江苏省苏州市工业园区 | 320000-320500-320510 | 320510 |
2、EIS系统服务设置
服务费名称 | 结算地区 |
1-固定单价-10 | 江苏省 |
2-打包价-100 | 江苏省苏州市 |
3-阶梯单价-20 | 江苏省苏州市工业园区 |
3、社保付款服务计算需要按照地区匹配计算,如果统一按照地区匹配计算,结算地区江苏省苏州市会把江苏省苏州市工业园区多计算了。实际处理中将接口获取到的服务规则,转换成以城市的值大小排序的TreeMap。可以实现优先计算区级别->市级别->省级别服务费计算。
// 将服务费规则转换成以城市编码为key的Map集合
// 原来一个服务费对应多个城市,转换成一个城市对应多个服务费
private static TreeMap<Long, List<SupplierContractServiceRuleVO>> buildCityRuleMap(List<SupplierContractServiceRuleVO> rules) {
// 地区编码按照从大到小排序,编码大的优先放在前面(不同省份的同级别比较不影响)
TreeMap<Long, List<SupplierContractServiceRuleVO>> map = new TreeMap<>(new Comparator<Long>() {
@Override
public int compare(Long o1, Long o2) {
return (int) (o2 - o1);
}
});
if (CollUtil.isEmpty(rules)) {
return map;
}
for (SupplierContractServiceRuleVO rule : rules) {
if (CollUtil.isEmpty(rule.getCities())) {
/// 没有设置城市的服务费规则,默认城市编码0,排序在最后面
List<SupplierContractServiceRuleVO> list = map.getOrDefault(0L, new ArrayList<>());
list.add(rule);
map.put(0L, list);
} else {
for (String city : rule.getCities()) {
List<SupplierContractServiceRuleVO> list = map.getOrDefault(Long.valueOf(city), new ArrayList<>());
list.add(rule);
map.put(Long.valueOf(city), list);
}
}
}
return map;
}
4、数据匹配,按照转换后的城市为key的服务费TreeMap匹配对应的社保缴纳数据,计算对应的服务值,并移除已经计算过的数据。
for (Map.Entry<Long, List<SupplierContractServiceRuleVO>> entry : cityRuleMap.entrySet()) {
if (0L == entry.getKey()) {
continue;
}
BigDecimal temp = BigDecimal.ZERO;
Map<Integer, List<SupplierContractServiceRuleVO>> rulesMap2 = entry.getValue().stream()
.collect(Collectors.groupingBy(SupplierContractServiceRuleVO::getChargeType));
SupplierContractServiceRuleVO rule = entry.getValue().get(0);
if (CollUtil.isNotEmpty(rulesMap2.get(0))) {
List<SocialRequestData> filterNormalList = filterMatchCityData(entry.getKey().toString(), rule.getServiceScope(), normalList);
Set<String> normalIdNumSet = filterNormalList.stream().map(SocialRequestData::getIdNum).collect(Collectors.toSet());
temp = temp.add(reduceAmount(rule.getRule(), rulesMap2.get(0), normalIdNumSet.size()));
// 这边移除匹配上城市的数据
normalList.removeIf(next -> normalIdNumSet.contains(next.getIdNum()));
}
if (CollUtil.isNotEmpty(rulesMap2.get(1))) {
List<SocialRequestData> filterSpecList = filterMatchCityData(entry.getKey().toString(), rule.getServiceScope(), specList);
Set<String> specIdNumSet = filterSpecList.stream().map(SocialRequestData::getIdNum).collect(Collectors.toSet());
temp = temp.add(reduceAmount(rule.getRule(), rulesMap2.get(1), specIdNumSet.size()));
// 这边移除匹配上城市的数据
specList.removeIf(next -> specIdNumSet.contains(next.getIdNum()));
}
// 最低收费标准 与 (正常收费+补缴收费)比较
temp = temp.max(rule.getBottomPrice());
serviceAmount.set(serviceAmount.get().add(temp));
}
五、思考
在想到这种方式之前,打算将所有城市放进优先队列(PriorityQueue),但在计算数据服务费时,需要筛选出对应的服务费规则,还要筛选对应的社保缴纳数据,同时还需要对优先队列进行出队操作,代码量较多,逻辑复杂程度比使用TreeMap复杂,出问题排查也比较困难。