在Java中不创建底层对象创建映射器的方法

75 阅读3分钟

你知道你可以在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

观察这两个替代品的各种属性,我们可以看到以下几点。

执行性能

操作HashMapIntMapper
放/加O(1) < x < O(log(N)) (*)O(1) (**)
排序-O(log(N))
得到O(1) < x < O(log(N)) (*)O(log(N))

(*) 取决于密钥分布、大小、负载系数和所做的关联。
(**) 在IntMapper中没有添加方法,而所有的值都是批量添加的。

以字节为单位的内存使用量

操作HashMapIntMapper
放入/添加48N (***)0 (***)
获取16N (***)0

(***):上面的数字是在典型的JVM使用下,不包括安全对象本身,也不包括任何支持数组,两者都可以在使用之间回收。

对象中的对象分配

操作HashMapIntMapper
放入/添加2 * N0
获取N0

以上所有数字都不包括Security 对象本身,也不包括任何支持数组。