你知道你可以在Java中不创建底层对象而创建映射器吗?
正如大多数Java开发者所知道的,在Java Map(如HashMap)中放值涉及到在底层创建大量的辅助对象。例如,一个有int键和long值的HashMap可能会为每个条目创建一个封装好的Integer、一个封装好的Long对象和一个Node,这个Node将前者的值与哈希值和共享同一哈希桶的其他潜在Node对象链接在一起。也许更诱人的是,每次Map被查询时都会创建一个封装好的Integer!例如,使用Map::get操作。
在这个简短的教程中,我们将设计一种方法来创建一个无对象创建的、轻量级的映射器,它具有基本的查询能力,适用于有限数量的关联。首先创建和初始化映射器,然后可以对其进行查询。有趣的是,这些映射器也可以被序列化/反序列化,并使用Chronicle的开源库 在电线上发送,而不会产生额外的对象创建。
设置场景
假设我们有一些具有int类型的 "id "字段的Security对象。我们想为这些对象创建一个可重复使用的映射器,允许使用 "id "字段来查询一些安全对象。
public final class Security extends SelfDescribingMarshallable {
private int id;
private long averagePrice;
private long count;
public Security(int id, long price, long count) {
this.id = id;
this.averagePrice = price;
this.count = count;
}
// Getters, setters and toString() not shown for brevity
}
SelfDescribingMarshallable 基本上是一个序列化标记。
实现一个IntMapper
我们现在可以将这些安全对象存储在一个包含实际查询方法的IntMapper中,如下所示。
public class IntMapper<V≶ extends SelfDescribingMarshallable {
private final List<V≶ values = new ArrayList<≶();
private final ToIntFunction<? super V≶ extractor;
public IntMapper(final ToIntFunction<? super V≶ extractor) {
this.extractor = Objects.requireNonNull(extractor);
}
public List<V≶ values() { return values; }
public IntStream keys() {
return values.stream().mapToInt(extractor);
}
public void set(Collection<? extends V≶ values) {
this.values.clear();
this.values.addAll(values);
// Sort the list in id order
this.values.sort(comparingInt(extractor));
}
public V get(int key) {
int index = binarySearch(key);
if (index ≶= 0)
return values.get(index);
else
return null;
}
// binarySearch() shown later in the article
}
这就是了!我们已经创建了一个可重复使用的映射器,没有创建对象的开销,具有合理的查询性能。
使用映射器
有了上面的类,我们可以把一个小的主方法放在一起,演示一下这个概念的使用。
public class SecurityLookup {
public static void main(String[] args) {
// These can be reused
final Security s0 = new Security(100, 45, 2);
final Security s1 = new Security(10, 100, 42);
final Security s2 = new Security(20, 200, 13);
// This can be reused
final List<Security≶ securities = new ArrayList<≶();
securities.add(s0);
securities.add(s1);
securities.add(s2);
// Reusable Mapper
IntMapper<Security≶ mapper =
new IntMapper<≶(Security::getId);
mapper.set(securities);
Security security100 = mapper.get(100);
System.out.println("security100 = " + security100);
}
}
正如预期的那样,该程序在运行时将产生以下输出:
security100 = Security{id=100, averagePrice=45, count=2}
二进制搜索方法的实现
上面使用的二进制搜索方法可能是这样实现的:
int binarySearch(final int key) {
int low = 0;
int high = values.size() - 1;
while (low <= high) {
final int mid = (low + high) >>> 1;
final V midVal = values.get(mid);
int cmp = Integer.compare(
extractor.applyAsInt(midVal), key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid;
}
return -(low + 1);
}
}
不幸的是,我们不能使用 Arrays::binarySearch 或 Collections::binarySearch。原因之一是,像这样的方法会在查询时创建额外的对象。
其他键类型
如果我们想使用其他类型,如 CharSequence 或其他引用对象,有一个 comparing() 方法的重载,它需要一个自定义的比较器。在CharSequence的情况下,这可能看起来像下面这样。
values.sort(
comparing(Security::getId, CharSequenceComparator.INSTANCE))
更普遍的是,如果键引用对象是K类型的,那么上面的二进制搜索方法可以很容易地被修改为使用Function<? super T, ? extends K>类型的提取器来代替,并增加一个比较器<? super K>参数。
这里有一个通用Mapper<K, V>的完整例子。
通过电线进行序列化
在不创建对象的情况下通过电线发送 IntMapper 对象,需要在接收方特别注意,以便旧的 Security 对象可以被重复使用。这涉及到设置一个暂存缓冲区,以容纳回收的 Security 对象。
private final transient List<V> buffer = new ArrayList<>();
我们还必须覆盖IntMapper::readMarshallable方法,并包括:
wire.read("values").sequence(values, buffer, Security::new);
完整的设置超出了本文的范围。
分析:HashMap vs. IntMapper
观察这两个替代品的各种属性,我们可以看到以下几点。
执行性能
| 操作 | HashMap | IntMapper |
| 放/加 | O(1) < x < O(log(N)) (*) | O(1) (**) |
| 排序 | - | O(log(N)) |
| 得到 | O(1) < x < O(log(N)) (*) | O(log(N)) |
(*) 取决于密钥分布、大小、负载系数和所做的关联。
(**) 在IntMapper中没有添加方法,而所有的值都是批量添加的。
以字节为单位的内存使用量
| 操作 | HashMap | IntMapper |
| 放入/添加 | 48N (***) | 0 (***) |
| 获取 | 16N (***) | 0 |
(***):上面的数字是在典型的JVM使用下,不包括安全对象本身,也不包括任何支持数组,两者都可以在使用之间回收。
对象中的对象分配
| 操作 | HashMap | IntMapper |
| 放入/添加 | 2 * N | 0 |
| 获取 | N | 0 |
以上所有数字都不包括Security 对象本身,也不包括任何支持数组。