Minborg

Minborg
Minborg
Showing posts with label Map. Show all posts
Showing posts with label Map. Show all posts

Monday, November 1, 2021

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

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

    }


}


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<? super T, ? extends K>  instead and an added Comparator<? super K> parameter.


A complete example of a generic Mapper<K, V> 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<V> 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

Operation

HashMap

IntMapper

put/add

O(1) < x < O(log(N)) (*)

O(1) (**)

sort

-

O(log(N))

get

O(1) < x < O(log(N)) (*)

O(log(N))


(*) 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

Operation

HashMap

IntMapper

put/add

48N (***)

0 (***)

get

16N (***)

0


(***): 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

Operation

HashMap

IntMapper

put/add

2 * N

0

get

N

0


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


Resources

Chronicle Software Home Page


Chronicle Wire on GitHub (open-source)


Complete source code for all examples in this article (open-source)


Wednesday, July 31, 2019

Java: ChronicleMap Part 3, Fast Microservices

Standard Java Maps needs to be initialized upon startup. Learn how to leverage ChronicleMaps that is initializable from a file and reduce microservice startup times significantly and how to share Maps between JVMs.

The built-in Map implementations, such as HashMap and ConcurrentHashMap are fast but they must be initialized with mappings before they can be used for looking up values. Also, they are limited in size by practical means such as heap and RAM size. Lastly, they are local to the JVM it runs in.

The initialization process can slow down critical startup for microservices, especially when reading mappings from a remote REST interface or a remote database. In this article, you will learn how you can start your microservice applications in seconds instead of minutes by using memory-mapped ChronicleMap instances and how Maps can be shared between JVMs in this third article in an article series about CronicleMap.

Read more about the fundamentals of CronicleMap in the first article.

Read more about file mapped CronicleMap objects in the second article.

Creating a Shared Map

As described in the second article in the series, we can easily create a file mapped Map like this:
private static Map<Long, Point> createFileMapped() {
    try {
        return ChronicleMap
            .of(Long.class, Point.class)
            .averageValueSize(8)
            .valueMarshaller(PointSerializer.getInstance())
            .entries(10_000_000)
            .createPersistedTo(new File("my-map"));

    } catch (IOException ioe) {
        throw new RuntimeException(ioe);
    }
}
Created Map objects can now be accessed by any JVM that has access to the “my-map” file. Updates to the maps will be shared among the participating JVMs via the shared file.

Initializing the Map

As also shown in the second article, we could create and initialize a Map like this:

final Map<Long, Point> m3 = LongStream.range(0, 10_000_000)
    .boxed()
        .collect(
            toMap(
                Function.identity(),
                FillMaps::pointFrom,
                (u, v) -> {
                    throw new IllegalStateException();
                },
                FillMaps::createFileMapped
            )
        );
When running on my laptop (MacBook Pro mid 2015, 16 GB, 2.2 GHz Intel Core i7), it takes about 10 seconds to create and fill the Map with 10 million entries.

If the Map contents were retrieved externally (as opposed to being created locally by the pointFrom() method), it would likely take much longer time to fill the Map. For example, if we get 50 Mbit/s REST throughput and each JSON Point representation consumes 25 bytes, then it would take some 60 seconds to fill the Map.

Starting a new JVM

Now that there is a pre-existing mapped file, we can start directly off this file as shown in this snippet:

return ChronicleMap
    .of(Long.class, Point.class)
    .averageValueSize(8)
    .valueMarshaller(PointSerializer.getInstance())
    .entries(10_000_000)
    .createOrRecoverPersistedTo(new File("my-map"));
This will create a Map directly from the existing “my-map” file.

Running this on my laptop will yield a start time of 5 seconds. This could be compared to the 60 second REST example, yielding a 90% startup time reduction.

Running Several JVMs on the Same Node

We could elect to run several JVMs on the same physical server node. By doing so, we benefit from the OS’es ability to make mappings of the file available for each JVM by exposing shared memory. This constitutes an efficient and low latency means of communication between the JVMs. The fact that there is a common pool of mapped memory makes the memory management much more efficient compared to a situation where each and every JVM/OS would have to maintain its own separate mappings.

Summary

ChronicleMaps can be shared between participating JVM via shared files
Startup times can be reduced significantly using shared files
If JVMs are running on the same physical machine, performance and efficiency is further improved
Shared files via ChronicleMap provides a low latency means of communication between JVMs

Tuesday, July 30, 2019

Java: ChronicleMap Part 2, Super RAM Maps

The standard Java Maps, such as the ubiquitous HashMap, are ultimately limited by the available RAM. Read this article and learn how you can create Java Maps with virtually unlimited sizes even exceeding the target machine’s RAM size.

The built-in Map implementations, such as HashMap and ConcurrentHashMap work fine as long as they are relatively small. In all cases, they are limited by the available heap and therefore eventually the available RAM size. ChronicleMap can store its contents in files, thereby circumventing this limitation, opening up for terabyte-sized mappings as shown in this second article in an article series about CronicleMap.

Read more about the fundamentals of CronicleMap in my previous first article.

File Mapping

Mapping of a file is made by invoking the createPersistedTo() method on a ChronicleMap builder as shown in the method below:
private static Map<Long, Point> createFileMapped() {
   try {
        return ChronicleMap
            .of(Long.class, Point.class)
            .averageValueSize(8)
            .valueMarshaller(PointSerializer.getInstance())
            .entries(10_000_000)
            .createPersistedTo(new File("my-map"));

    } catch (IOException ioe) {
        throw new RuntimeException(ioe);
    }
}

This will create a Map that will layout its content in a memory-mapped file named “my-map” rather than in direct memory. The following example shows how we can create 10 million Point objects and store them all in a file mapped map:

final Map<Long, Point> m3 = LongStream.range(0, 10_000_000)
    .boxed()
    .collect(
        toMap(
            Function.identity(),
            FillMaps::pointFrom,
            (u, v) -> {
                throw new IllegalStateException();
           },
           FillMaps::createFileMapped
       )
   );
The following command shows the newly created file:

Pers-MacBook-Pro:target pemi$ ls -lart my-map 
-rw-r--r--  1 pemi  staff  330305536 Jul 10 16:56 my-map
As can be seen, the file is about 33 MB and thus, each entry occupies 33 bytes on average.

Persistence

When the JVM terminates, the mapped file is still there, making it easy to pick up a previously created map including its content. This works much like a rudimentary superfast database. Here is how we can start off from an existing file:

return ChronicleMap
    .of(Long.class, Point.class)
    .averageValueSize(8)
    .valueMarshaller(PointSerializer.getInstance())
    .entries(10_000_000)
    .createOrRecoverPersistedTo(new File("my-map"));

The Map will be available directly, including its previous content.

Java Map Exceeding RAM Limit

One interesting aspect of memory-mapped files is that they can exceed both the heap and RAM limits. The file mapping logic will make sure that the parts being currently used are loaded into RAM on demand. The mapping logic will also retain recent portions of accessed mapped memory in physical memory to improve performance. This occurs behind-the-scenes and need not be managed by the application itself.

My desktop computer is an older MacBook Pro with only 16GB of memory (Yes, I know that sucks). Nevertheless, I can allocate a Map with 1 billion entries potentially occupying 33 * 1,000,000,000 = 33 GB memory (We remember from above that each entry occupied 33 bytes on average). The code looks like this:

return ChronicleMap
    .of(Long.class, Point.class)
    .averageValueSize(8)
    .valueMarshaller(PointSerializer.getInstance())
    .entries(1_000_000_000)
    .createPersistedTo(new File("huge-map"));

Even though I try to create a Java Map with 2x my RAM size, the code runs flawlessly and I get this file:

Pers-MacBook-Pro:target pemi$ ls -lart | grep huge-map 
-rw-r--r--   1 pemi  staff  34573651968 Jul 10 18:52 huge-map

Needless to say, you should make sure that the file you are mapping to is located on a file system with high random access performance. For example, a filesystem located on a local SSD.

Summary

ChronicleMap can be mapped to an external file
The mapped file is retained when the JVM exits
New applications can pick up an existing mapped file
ChronicleMap can hold more data than there is RAM
Mapped files are best placed on file systems with high random access performance

Friday, July 26, 2019

Java: ChronicleMap Part 1, Go Off-Heap

Filling up a HashMap with millions of objects will quickly lead to problems such as inefficient memory usage, low performance and garbage collection problems. Learn how to use off-heap CronicleMap that can contain billions of objects with little or no heap impact.

The built-in Map implementations, such as HashMap and ConcurrentHashMap are excellent tools when we want to work with small to medium-sized data sets. However, as the amount of data grows, these Map implementations are deteriorating and start to exhibit a number of unpleasant drawbacks as shown in this first article in an article series about open-sourceed  CronicleMap.

Heap Allocation

In the examples below, we will use Point objects. Point is a POJO with a public default constructor and getters and setters for X and Y properties (int). The following snippet adds a million Point objects to a HashMap:

final Map<Long, Point> m = LongStream.range(0, 1_000_000)
    .boxed()
    .collect(
        toMap(
            Function.identity(),
            FillMaps::pointFrom,
            (u,v) -> { throw new IllegalStateException(); },
             HashMap::new
        )
    );

    // Conveniency method that creates a Point from
    // a long by applying modulo prime number operations
    private static Point pointFrom(long seed) {
        final Point point = new Point();
        point.setX((int) seed % 4517);
        point.setY((int) seed % 5011);
        return point;
    }

We can easily see the number of objects allocated on the heap and how much heap memory these objects consume:

Pers-MacBook-Pro:chronicle-test pemi$ jmap -histo 34366 | head
 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:       1002429       32077728  java.util.HashMap$Node (java.base@10)
   2:       1000128       24003072  java.lang.Long (java.base@10)
   3:       1000000       24000000  com.speedment.chronicle.test.map.Point
   4:           454        8434256  [Ljava.util.HashMap$Node; (java.base@10)
   5:          3427         870104  [B (java.base@10)
   6:           185         746312  [I (java.base@10)
   7:           839         102696  java.lang.Class (java.base@10)
   8:          1164          89088  [Ljava.lang.Object; (java.base@10)
For each Map entry, a Long, a HashMap$Node and a Point object need to be created on the heap. There are also a number of arrays with HashMap$Node objects created. In total, these objects and arrays consume 88,515,056 bytes of heap memory. Thus, each entry consumes on average 88.5 bytes.

NB: The extra 2429 HashMap$Node objects come from other HashMap objects used internally by Java.

Off-Heap Allocation

Contrary to this, a CronicleMap uses very little heap memory as can be observed when running the following code:

final Map<Long, Point> m2 = LongStream.range(0, 1_000_000)
    .boxed()
    .collect(
        toMap(
            Function.identity(),
            FillMaps::pointFrom,
            (u,v) -> { throw new IllegalStateException(); },
            () -> ChronicleMap
                .of(Long.class, Point.class)
                .averageValueSize(8)
                .valueMarshaller(PointSerializer.getInstance())
                .entries(1_000_000)
                .create()
        )
    );
Pers-MacBook-Pro:chronicle-test pemi$ jmap -histo 34413 | head
 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:          6537        1017768  [B (java.base@10)
   2:           448         563936  [I (java.base@10)
   3:          1899         227480  java.lang.Class (java.base@10)
   4:          6294         151056  java.lang.String (java.base@10)
   5:          2456         145992  [Ljava.lang.Object; (java.base@10)
   6:          3351         107232  java.util.concurrent.ConcurrentHashMap$Node (java.base@10)
   7:          2537          81184  java.util.HashMap$Node (java.base@10)
   8:           512          49360  [Ljava.util.HashMap$Node; (java.base@10)
As can be seen, there are no Java heap objects allocated for the CronicleMap entries and consequently no heap memory either.

Instead of allocating heap memory, CronicleMap allocates its memory off-heap. Provided that we start our JVM with the flag -XX:NativeMemoryTracking=summary, we can retrieve the amount off-heap memory being used by issuing the following command:

Pers-MacBook-Pro:chronicle-test pemi$ jcmd 34413 VM.native_memory | grep Internal
-                  Internal (reserved=30229KB, committed=30229KB)
Apparently, our one million objects were laid out in off-heap memory using a little more than 30 MB of off-heap RAM. This means that each entry in the CronicleMap used above needs on average 30 bytes.

This is much more memory effective than a HashMap that required 88.5 bytes. In fact, we saved 66% of RAM memory and almost 100% of heap memory. The latter is important because the Java Garbage Collector only sees objects that are on the heap.

Note that we have to decide upon creation how many entries the CronicleMap can hold at maximum. This is different compared to HashMap which can grow dynamically as we add new associations. We also have to provide a serializer (i.e. PointSerializer.getInstance()), which will be discussed in detail later in this article.

Garbage Collection

Many Garbage Collection (GC) algorithms complete in a time that is proportional to the square of objects that exist on the heap. So if we, for example, double the number of objects on the heap, we can expect the GC would take four times longer to complete.

If we, on the other hand, create 64 times more objects, we can expect to suffer an agonizing 1,024 fold increase in expected GC time. This effectively prevents us from ever being able to create really large HashMap objects.

With ChronicleMap we could just put new associations without any concern of garbage collection times.

Serializer

The mediator between heap and off-heap memory is often called a serializer. ChronicleMap comes with a number of pre-configured serializers for most built-in Java types such as Integer, Long, String and many more.

In the example above, we used a custom serializer that was used to convert a Point back and forth between heap and off-heap memory. The serializer class looks like this:

public final class PointSerializer implements
    SizedReader<Point>,
    SizedWriter<Point> {

    private static PointSerializer INSTANCE = new PointSerializer();

    public static PointSerializer getInstance() { return INSTANCE; }

    private PointSerializer() {}

    @Override
    public long size(@NotNull Point toWrite) {
        return Integer.BYTES * 2;
    }

    @Override
    public void write(Bytes out, long size, @NotNull Point point) {
        out.writeInt(point.getX());
        out.writeInt(point.getY());
    }

    @NotNull
    @Override
    public Point read(Bytes in, long size, @Nullable Point using) {
        if (using == null) {
            using = new Point();
        }
        using.setX(in.readInt());
        using.setY(in.readInt());
        return using;
    }

}
The serializer above is implemented as a stateless singleton and the actual serialization in the methods write() and read() are fairly straight forward. The only tricky part is that we need to have a null check in the read() method if the “using” variable does not reference an instantiated/reused object.

How to Install it?

When we want to use ChronicleMap in our project, we just add the following Maven dependency in our pom.xml file and we have access to the library.

<dependency>
    <groupId>net.openhft</groupId>
    <artifactId>chronicle-map</artifactId>
    <version>3.17.3</version>
</dependency>
If you are using another build tool, for example, Gradle, you can see how to depend on ChronicleMap by clicking this link.

The Short Story

Here are some properties of ChronicleMap:

Stores data off-heap
Is almost always more memory efficient than a HashMap
Implements ConcurrentMap
Does not affect garbage collection times
Sometimes needs a serializer
Has a fixed max entry size
Can hold billions of associations
Is free and open-source

Saturday, December 10, 2016

Day 10, Java Holiday Calendar 2016, MapStream

Day 10, Java Holiday Calendar 2016, MapStream



Today's tips is about the open-source class MapStream that allows us to stream not only over elements but over pair of key, value elements and make changes either to the keys, values or both.

You can find the source code for MapStream here together with some examples of how to use it. It is free so go ahead and use or copy it in your application! MapStream is a part of open-source Speedment, a stream ORM tool and runtime.

With MapStream you can do this:

Map<String, Integer> numberOfCats = new HashMap<>();

numberOfCats.put("Anne", 3);
numberOfCats.put("Berty", 1);
numberOfCats.put("Cecilia", 1);
numberOfCats.put("Denny", 0);
numberOfCats.put("Erica", 0);
numberOfCats.put("Fiona", 2);

System.out.println(
  MapStream.of(numberOfCats)
      .filterValue(v -> v > 0)
      .sortedByValue(Integer::compareTo)
      .mapKey(k -> k + " has ")
      .mapValue(v -> v + (v == 1 ? " cat." : " cats."))
      .map((k, v) -> k + v)
      .collect(Collectors.joining("\n"))
);

This would produce the following:

Cecilia has 1 cat.
Berty has 1 cat.
Fiona has 2 cats.
Anne has 3 cats.

Learn more on MapStream here.

Follow the Java Holiday Calendar 2016 with small tips and tricks all the way through the winter holiday season.




Sunday, December 4, 2016

Creating Maps With Named Lambdas


The Magical Map

Wouldn't it be great if we could create Java maps like this?

Map<String, Integer> map = mapOf(
      one -> 1,
      two -> 2
);

Map<String, String> map2 = mapOf(
      one -> "eins",
      two -> "zwei"
);



Well, we can! Read this post and learn more about lambdas and how we can get the name of their parameters.

The solution

By introducing the following interface we get the functionality above.

public interface KeyValueStringFunction<T> extends Function<String, T>, Serializable {

    default String key() {
        return functionalMethod().getParameters()[0].getName();
    }

    default T value() {
        return apply(key());
    }

    default Method functionalMethod() {
        final SerializedLambda serialzedLabmda = serializedLambda();
        final Class<?> implementationClass = implementationClass(serialzedLabmda);
        return Stream.of(implementationClass.getDeclaredMethods())
            .filter(m -> Objects.equals(m.getName(), serialzedLabmda.getImplMethodName()))
            .findFirst()
            .orElseThrow(RuntimeException::new);
    }

    default Class<?> implementationClass(SerializedLambda serializedLambda) {
        try {
            final String className = serializedLambda.getImplClass().replaceAll("/", ".");
            return Class.forName(className);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    default SerializedLambda serializedLambda() {
        try {
            final Method replaceMethod = getClass().getDeclaredMethod("writeReplace");
            replaceMethod.setAccessible(true);
            return (SerializedLambda) replaceMethod.invoke(this);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @SafeVarargs
    static <V> Map<String, V> mapOf(KeyValueStringFunction<V>... mappings) {
        return Stream.of(mappings).collect(toMap(KeyValueStringFunction::key, KeyValueStringFunction::value));
    }

}

Limitations

We can only create maps with keys that are of type String (or anything super String like CharSequence, Serializable or Comparable<String>) because obviously lambda names are strings.

We must use a Java version that is higher than Java 8u80 because it was at that time lambda names could be retrieved run-time.

The most annoying limitation is that we have to compile (i.e. "javac") our code with the "-parameter" flag or else the parameter names will not be included in the run time package (e.g. JAR or WAR). We can do this automatically by modifying our POM file like this:

          <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgs>
                        <arg>-Xlint:all</arg>

                        <!-- Add this line to your POM -->
                        <arg>-parameters</arg>
                    </compilerArgs>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>

If you forget to add the "-parameter" flag, the Java runtime will always report a default name of "arg0" as the name of the parameter. This leads to that the maps will (at most) contain one key "arg0", which is not what we want.

Opportunities

The values, can be of any type. In particular they can be other maps, enabling us to construct more advanced map hierarchies. We could, for example, create a map with different countries with values that are also maps containing the largest towns and how many inhabitants each city has like this:


Map<String, Map<String, Integer>> map3 = mapOf(
            usa -> mapOf(
                new_york    -> 8_550_405,
                los_angeles -> 3_971_883,
                chicago     -> 2_720_546
            ),
            canada -> mapOf(
                toronto  -> 2_615_060,
                montreal -> 1_649_519,
                calgary  -> 1_096_833
            )
        );

Update: Important Notes

A number of people have pointed out that SerializedLambda is "thin ice" that we should not rely on in production code. Tagir Valeev (@tagir_valeev) made a performance test of the scheme in this post and compared it to the old fashioned way of just creating a Map and using a regular put() to enter data. The findings were that the old way is orders of magnitudes faster. You can find the entire benchmark here. Thanks Tagir for making this test available. You should view this way of entering data in a Map as academic an as an inspiration of what can be done in Java. Do not use it in production code.

Keep on mapping!

Saturday, December 3, 2016

Day 3, Java Holiday Calendar 2016, Initialize Maps

3. Initializing Maps in the Smartest Way


Today's tips is how to initialize Java Maps in a type safe way with Java 8. With Java 9, we will get even better ways of creating immutable Maps. Until then, by defining two utility methods:

    public static <K, V> Entry<K, V> entry(K key, V value) {
        return new AbstractMap.SimpleEntry<>(key, value);
    }

    public static <K, U> Collector<Entry<K, U>, ?, Map<K, U>> entriesToMap() {
        return Collectors.toMap(Entry::getKey, Entry::getValue);
    }

You Can Do This:

protected static Map<Integer, String> createMap() {
        return Stream.of(
                entry(0, "zero"),
                entry(1, "one"),
                entry(2, "two"),
                entry(3, "three"),
                entry(4, "four"),
                entry(5, "five"),
                entry(6, "six"),
                entry(7, "seven"),
                entry(8, "eight"),
                entry(9, "nine"),
                entry(10, "ten"),
                entry(11, "eleven"),
                entry(12, "twelve")).
                collect(entriesToMap());
    }

Read more in the original article at http://minborgsjavapot.blogspot.com/2014/12/java-8-initializing-maps-in-smartest-way.html

Follow the Java Holiday Calendar 2016 with small tips and tricks all the way through the winter holiday season.

Saturday, March 5, 2016

Java 8: A Type Safe Map Builder Using Alternating Interface Exposure

Expose Your Classes Dynamically

Duke and Spire exposing another look... 
When I was a Java newbie, I remember thinking that there should be a way of removing or hiding methods in my classes that I did not want to expose. Like overriding a public method with a private or something like that (which of corse cannot and should not be possible). Obviously today, we all know  that we could achieve the same goal by exposing an interface instead.

By using a scheme named Alternating Interface Exposure, we could view a class' methods dynamically and type safe, so that the same class can enforce a pattern in which it is supposed to to be used.

Let me take an example. Let's say we have a Map builder that can be called by successively adding keys and values before the actual Map can be built. The Alternating Interface Exposure scheme allows us to ensure that we call the key() method and the value() exactly the same number of times and that the build() method is only callable (and seen, for example in the IDE) when there are just as many keys as there are values.

The Alternating Interface Exposure scheme is used in the open-source project Speedment that I am contributing to. In Speedment, the scheme is for example used when building type-safe Tuples that subsequently will be built after adding elements to a TupleBuilder. This way, we can get a typed Tuple2<String, Integer> = {"Meaning of Life", 42}, if we write TupleBuilder.builder().add("Meaning of Life).add(42).build().

Using a Dynamic Map Builder

I have written about the Builder Pattern several times in some of my previous posts (e.g. here) and I encourage you to revisit an article on this issue, should you not be familiar with the concept, before reading on.

The task at hand is to produce a Map builder that dynamically exposes a number of implementing methods using a number of context dependent interfaces. Furthermore, the builder shall "learn" its key/value types the first time they are used and then enforce the same type of keys and values for the remaining entries.

Here is an example of how we could use the builder in our code once it is developed:
    public static void main(String[] args) {

        // Use the type safe builder
        Map<Integer, String> map = Maps.builder()
                .key(1)                 // The key type is decided here for all following keys
                .value("One")           // The value type is decided here for all following values
                .key(2)                 // Must be the same or extend the first key type
                .value("Two")           // Must be the same type or extend the first value type
                .key(10).value("Zehn'") // And so on...
                .build();               // Creates the map!

        // Create an empty map
        Map<String, Integer> map2 = Maps.builder()
                .build();
        
        
    }

}

In the code above, once we start using an Integer using the call key(1), the builder only accepts additional keys that are instances of Integer. The same is true for the values. Once we call value("one"), only objects that are instances of String can be used. If we try to write value(42) instead of value("two") for example, we would immediately see the error in our IDE. Also, most IDE:s would automatically be able to select good candidates when we use code completion.

Let me elaborate on the meaning of this:

Initial Usage

The builder is created using the method Maps.builder() and the initial view returned allows us to call:
  1. build() that builds an empty Map (like in the second "empty map" example above)
  2. key(K key) that adds a key to the builder and decides the type (=K) for all subsequent keys (like key(1) above)

Once the initial key(K key) is called, another view of the builder appears exposing only:
  1. value(V value) that adds a value to the builder and decides the type (=V) for all subsequent values (like value("one"))

Note that the build() method is not exposed in this state, because the number of keys and values differ. Writing Map.builder().key(1).build(); is simply illegal, because there is no value associated with key 1.

Subsequent Usage

Now that the key and value types are decided, the builder would just alternate between two alternating interfaces being exposed depending on if key() or value() is being called. If key() is called, we expose value() and if value() is called, we expose both key() and build().

The Builder

Here are the two alternating interfaces that the builder is using once the types are decided upon:
public interface KeyBuilder<K, V> {

        ValueBuilder<K, V> key(K k);
        
        Map<K, V> build();
    
}

public interface ValueBuilder<K, V> {

    KeyBuilder<K, V> value(V v);

}

Note how one interface is returning the other, thereby creating an indefinite flow of alternating interfaces being exposed. Here is the actual builder that make use of the alternating interfaces:
public class Maps<K, V> implements KeyBuilder<K, V>, ValueBuilder<K, V> {

    private final List<Entry<K, V>> entries;
    private K lastKey;

    public Maps() {
        this.entries = new ArrayList<>();
    }

    @Override
    public ValueBuilder<K, V> key(K k) {
        lastKey = k;
        return (ValueBuilder<K, V>) this;
    }

    @Override
    public KeyBuilder<K, V> value(V v) {
        entries.add(new AbstractMap.SimpleEntry<>(lastKey, v));
        return (KeyBuilder<K, V>) this;
    }

    @Override
    public Map<K, V> build() {
        return entries.stream()
                .collect(toMap(Entry::getKey, Entry::getValue));
    }

    public static InitialKeyBuilder builder() {
        return new InitialKeyBuilder();
    }

}

We see that the implementing class implements both of the alternating interfaces but only return one of them depending on if key() or value() is called. I have "cheated" a bit by created two initial help classes that take care about the initial phase where the key and value types are not yet decided. For the sake of completeness, the two "cheat" classes are also shown hereunder:
public class InitialKeyBuilder {

    public <K> InitialValueBuilder<K> key(K k) {
        return new InitialValueBuilder<>(k);
    }
    
    public <K, V> Map<K, V> build() {
        return new HashMap<>();
    }

}

public class InitialValueBuilder<K> {
    
    private final K k;

    public InitialValueBuilder(K k) {
        this.k = k;
    }
    
    public <V> KeyBuilder<K, V> value(V v) {
        return new Maps<K, V>().key(k).value(v);
    }

}

These latter classes work in a similar fashion as the main builder in the way that the InitialKeyBuilder returns a InitialValueBuilder that in turn, creates a typed builder that would be used indefinitely by alternately returning either a KeyBuilder or a ValueBuilder.

Conclusions

The Alternating Interface Exposure scheme is useful when you want a type safe and context aware model of your classes. You can develop and enforce a number of rules for your classes using this scheme. These classes will be much more intuitive to use, since the context sensitive model and its types propagate all the way out to the IDE. The schema also gives more robust code, because potential errors are seen very early in the design phase. We will see potential errors as we are coding and not as failed tests or application errors.