Friends of OpenJDK Today

Did You Know You Can Create Mappers Without Creating Underlying Objects in Java?

September 20, 2022

Author(s)

  • Per Minborg

    Per Minborg is an inventor and developer with more than 20 years of Java coding experience. He is a frequent contributor to open-source projects, an Oracle Groundbreaker Ambassador, an Oracle ... Learn more

As most Java developers know, putting values in a Java Map (like a HashMap) involves creating a large number of auxiliary objects under the covers.

For example, a HashMap with int keys and long values might, for each entry, create a wrapped Integer, a wrapped Long object and a Node that holds the former values together with a hash value and a link to other potential Node objects sharing the same hash bucket.

Perhaps even more tantalizing is that a wrapped Integer might be created each time the Map is queried. For example, using the Map::get operation.

In this short tutorial, we will devise a way of creating an object-creation-free, light-weighted mapper with rudimentary lookup capability that is suitable for a limited number of associations.

The mapper is first created and initialized, whereafter it can be queried. Interestingly, these mappers can also be serialized/deserialized and sent over the wire using Chronicle’s open-source libraries without incurring additional object creation.

Setting the Scene

Suppose we have a number of Security objects with an “id” field of type int. We would like to create a reusable mapper for these objects allowing a number of Security objects to be looked up using the “id” field:

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
}

The SelfDescribingMarshallable is basically a serialization marker.

Implementing an IntMapper

We can now store these Security objects in an IntMapper containing the actual lookup method as shown hereunder:

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
}

That’s it!

We have created a reusable mapper with no object creation overhead with reasonable query performance.

Using the Mapper

Armed with the above classes, we can put together a small main method that demonstrates the use of the concept:

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);
    }
}

As expected, the program will produce the following output when run:

security100 = Security{id=100, averagePrice=45, count=2}

Binary Search Method Implementation

The binary search method used above might be implemented like this:

int binarySearch(final int key) {
        int low = 0;
        int high = values.size() - 1;
        while (low >> 1;
            final V midVal = values.get(mid);
            int cmp = Integer.compare(
                    extractor.applyAsInt(midVal), key);
            if (cmp  0)
                high = mid - 1;
            else
                return mid;
        }
        return -(low + 1);
    }
}

Unfortunately, we cannot use Arrays::binarySearch or Collections::binarySearch. One reason is that methods like these would create additional objects upon querying.

Other Key Types

If we want to use other types like CharSequence or other reference objects, there is an overload of the comparing() method that takes a custom comparator. This might look like the following in the case of CharSequence:

values.sort(
    comparing(Security::getId, CharSequenceComparator.INSTANCE));

More generally, if the key reference object is of type K, then the binary search method above can easily be modified to use an extractor of type Function instead and an added Comparator parameter.

A complete example of a generic Mapper is available here.

Serializing Across the Wire

Sending IntMapper objects over the wire without object creation requires special care on the receiver side so that old Security objects can be reused. This involves setting up a transient buffer that holds recycled Security objects.

private final transient List buffer = new ArrayList();

We also have to override the IntMapper::readMarshallable method and include:

wire.read("values").sequence(values, buffer, Security::new);

The complete setup is outside the scope of this article.

Analysis: HashMap vs. IntMapper

Looking at various properties of the two alternatives, we see the following:

Execution Performance

(*) Depending on key distribution, size, load factor and associations made.

(**) There is no add method in the IntMapper, instead all values are added in a batch

Memory usage in Bytes

(***): The figures above are under typical JVM use, excluding the Security objects themselves and excluding any backing array, both of which can be recycled between use.

Object Allocation in objects

All the figures above are excluding the Security objects themselves and excluding any backing array.

Resources

Related Articles

View All
  • Peter Lawrey Talks about Low-Latency, High-Performance Java

    About 7 years ago, I attended a session given by Java Champion Peter Lawrey, leader of Chronical Software, at a JavaOne conference. Since most of my prior development work in the realm of low-latency high-performance was C/C++ software, I was very interested in hearing what Peter might say about how Java addresses this problem.

    I caught up with Peter again recently, and asked him some questions about what’s happened since then, and where we are today. Here are my questions and Peter’s responses.

    Read More
    October 22, 2020
  • The Unix Philosophy for Low Latency

    How to realize the Unix Philosophy in Enterprise IT using a strongly-typed Enterprise language (Java), a suitable component technology (microservices) and an appropriate mechanism to glue them together (Chronicle Queue & Wire)

    Read More
    May 04, 2023
  • 7 Functional Programming Techniques in Java: A Primer

    There is a lot of hype around functional programming (FP) and a lot of cool kids are doing it but it is not a silver bullet.

    Like other programming paradigms/styles, functional programming also has its pros and cons and one may prefer one paradigm over the other.

    If you are a Java developer and wants to venture into functional programming, do not worry, you don’t have to learn functional programming oriented languages like Haskell or Clojure(or even Scala or JavaScript though they are not pure functional programming languages) since Java has you covered and this post is for you.

    Read More
    May 11, 2021

Author(s)

  • Per Minborg

    Per Minborg is an inventor and developer with more than 20 years of Java coding experience. He is a frequent contributor to open-source projects, an Oracle Groundbreaker Ambassador, an Oracle ... Learn more

Comments (1)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

david

thx very much for your code.

“(***): The figures above are excluding the Security objects themselves and excluding any backing array”

how to create a simple map/list/array of Security objects which are directly allocated off heap?

each time we do:

final Security s0 = new Security(100, 45, 2);
final List securities = new ArrayList();
securities.add(s0);

we create s0 on the heap !?

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard