Skip to content

Maps in vanilla Java vs. io.vavr.collection.Map

Hi, everyone! This post wraps up a series of articles dedicated to Vavr immutable data structures and their comparasion with “vanilla” Java fellows. We discussed several important collections, including arrays, sets and queues and this time we will explore maps. Essentially, map is a “dictionary-like” data structure that stores its elements (records) in a key-value format. java.util.Map is “vanilla” Java implementation (and also is a part of Java Collections Framework). Its immutable alternative is Vavr io.vavr.collection.Map.

Table of contents

Maps as a data structure

Like it was done in previous posts about arrays, queues and sets, we will start from the definition of maps in general. In computer science, map is a data structure, that stores elements in a key-value format. In some programming languages you can also find another name of maps – dictionaries. As we declared a key-value form, we need to define that key stands for unique identifier of objects. As a rule, maps do not allow to have multiple same keys, however it is ok to have multiple same values. Java supplies maps implementation as a part of Java Collection Framework, although we need to acknowledge here that in a narrow sense, Java maps are not collections, as they do not implement java.util.Collection interface. “Vanilla” maps in Java are mutable, so it is safer to use immutable implementation from Vavr.

java.util.Map

In vanilla Java, maps are defined as objects that map keys to values. Maps restrict you to have duplicate, however they allow to have 1 null key (except TreeMap). There are 3 basic types of maps in Java Collection Framework – HashMap, LinkedHashMap and TreeMap. They are presented on the graph below:

Graph 1. Java maps hierarchy

The difference between them is the permission of null keys and order of elements. Let sum up this in the following table:

HashMapLinkedHashMapTreeMap
Allow null valuesYes (many)Yes (many)Yes (many)
Allow null keysYes (one)Yes (one)No
Ordered elementsNoYesYes

So, we can conclude that HashMap, comparing to LinkedHashMap and TreeMap does not guarantee sorted order of elements. To have this ordering, keys should be comparable or Comparator should be supplied during map’s initialization. An another important note about Java maps is that they are not synchronyzed.

Now, it is time to move to concrete methods implemented in vanilla Java maps.

Insert a new element

An element of map is basically a key-value pair (record). As maps don’t allow duplicate keys, the uniqueness of the key is validated at the moment of insertion. When key is already associated with a value, it is updated for a new value. To add new element we can use two methods:

  • put(K key, V value)
  • putIfAbsent(K key, V value)

Take a look on the code snippet below:

@Test
public void addToMapTest(){
    HashMap<Integer, Person> people = new HashMap<>();
    people.put(1, new Person("Jana", "Novakova"));
    people.put(2, new Person("Zuzana", "Dvorakova"));
    people.put(3, new Person("Tereza", "Prokopova"));
    people.put(4, new Person("Stela", "Husova"));
    people.put(3, new Person("Pavla", "Shvecova"));
    assertEquals(new Person("Pavla", "Shvecova"), people.get(3));
}

Note, that while we have here 5 elements, the size of map equals to 4 elements. The element with key 3 is replaced – we validate with a test.

Access an element

To retrieve a record’s value from a map, we need to pass the key, associated with it. Usually this is performed using get(K key) method. In the situation when the record does not present in the map, this method returns a null value. An another option is to use getOrDefault(K key, V default) method that allow to return the specified default value in a case of an absence of queried element. Think about it as Java Optional. Here is an example:

@Test
public void getFromMapTest(){
    TreeMap<String, String> words = new TreeMap<>();
    words.put("apple", "manzana");
    words.put("orange", "naranja");
    words.put("pineapple", "pina");
    words.put("lemon", "limon");

    String cucumber = words.get("cucumber");
    assertNull(cucumber);

    String fruit = words.getOrDefault("cucumber", "not a fruit");
    assertEquals("not a fruit", fruit);
}

Remove an element

To delete a record from Java map, we use remove() method that accepts a key of record as an argument. Observe this code snippet below:

@Test
public void removeFromMapTest(){
    HashMap<Integer, Person> people = new HashMap<>();
    people.put(1, new Person("Jana", "Novakova"));
    people.put(2, new Person("Zuzana", "Dvorakova"));
    people.put(3, new Person("Tereza", "Prokopova"));
    assertEquals(3, people.size());
    people.remove(3);
    assertEquals(2, people.size());
    assertFalse(people.containsKey(3));
}

Replace an element

Finally, let see how to replace an element in Java maps. Check these methods:

  • V replace (K key, V value)
  • boolean replace (K key, V old, V new)

The difference between both methods is that the first one replaces and returns old value or null in a case of absence. The second one replaces a value only it is equal to the specified old value and returns boolean result of the replacement. The code snippet below demontrates a record replacement:

@Test
public void replaceElementTest(){
    TreeMap<String, String> words = new TreeMap<>();
    words.put("apple", "manzana");
    words.put("orange", "naranja");
    words.put("pineapple", "pina");
    words.put("lemon", "limon");

    words.replace("lemon", "limón");
    String limon = words.get("lemon");

    assertEquals("limón", limon);

    boolean result = words.replace("lemon", "limon", "un limon");
    assertFalse(result);
}

As it was stated before, vanilla Java maps are mutable. A better alternative is to use immutable data structures. This means that operations on data does not affect a target map’s data, but returns a new modified version. While there are several immutable implementations of collections for Java, my personal choice is Vavr. Let see how Vavr handles maps.

Maps in Vavr

Vavr collections are immutable, so operations (insertion, deletion etc.) do not modify elements of the map, but create a new version. Let see how it looks like on this small example:

Map<String, Person> updated = people.put("123 111 435", new Person("Marketa", "Vojtechova"));
assertEquals(3, people.size());
assertEquals(4, updated.size());

Here you can observe, that the original map instance (result) was not changed – it still has same number of elements, but put() method created a new instance – updated that holds the new Person object.

The graph below demonstates the place of maps among other Vavr data structures:

Graph 2. Maps among Vavr collections

As other Vavr collection classes, io.vavr.collection.Map is a subclass of Traversable and inherits common for other data structures methods.

Insert a new element

Vavr also uses put(K key, V value) method to add a new element to the dataset. However, due to the immutable nature, the original data structure is not affected, and put operation returns new map. Take a look on the code snippet below:

@Test
public void addToMapTest(){
    Map<String, Person> people = HashMap.of(
    "123 456 789", new Person("Jana", "Dvorakova"),
    "456 789 123", new Person("Patricia", "Novakova"),
    "789 456 123", new Person("Zuzana", "Shvecova"));

    // return new map

    Map<String, Person> updated = people.put("123 111 435", new Person("Marketa", "Vojtechova"));

    // updated NOT equals to people

    boolean equals = people.equals(updated); // false

    assertFalse(equals);
    assertEquals(3, people.size());
    assertEquals(4, updated.size());
}

We can assert with this test, that the initial people map is not affected and is not equal to updated map.

Access an element

To retrieve an element from Vavr map, we can use get(K key) method. Different to vanilla maps, it returns Vavr’s Option as a result, which is much better solution that performing null checking. Consider Option as a container that can contain a value. In case of an absence you can specify default value that will be returned to a caller:

Option<Person> result = people.get("123 456 789");
Person person = result.getOrElse(new Person("Java", "Dvorakova"));

You can read more about Option object in this tutorial.

Remove an element

Vavr allows to either remove the specified element either to filter map. In any case, these operations create new data sets and do not affect the original collection. This example demonstrates how to remove elements from Vavr maps:

@Test
public void removeFromMapTest(){
    Map<String, String> countries = HashMap.of(
        "Spain", "es",
        "Colombia", "es",
        "Argentina", "es",
        "Brazil", "pt",
        "Czech Republic", "cz"
    );

    Map<String, String> result = countries.remove("Czech Republic");
    assertFalse(result.containsKey("Czech Republic"));

    // all elements with values which DO NOT satisfy the given predicate.
    Map<String, String> notSpanishSpeaking = 
                countries.filterNotValues(c->c.equalsIgnoreCase("es"));

    // all elements that DO satisfy
    Map<String, String> spanishSpeaking = 
                countries.filterValues(c->c.equalsIgnoreCase("es"));

    assertEquals(2, notSpanishSpeaking.size());
    assertEquals(3, spanishSpeaking.size());
}

In the first situation we use remove(K key) method to remove the Czech Republic from the map of countries. The resulted map does not contain such key. In next cases we use filter methods: filterNotValues and filterValues. Both of the accept Predicate conditions that are used to filter map data.

Replace an element

To change a value for the key we use replace() method. It accepts three arguments: key, old value and new value. In this snippet we replace the owner of phone number:

@Test
public void replaceElementTest(){
    Map<String, Person> people = HashMap.of(
    "123 456 789", new Person("Jana", "Dvorakova"),
    "456 789 123", new Person("Patricia", "Novakova"),
    "789 456 123", new Person("Zuzana", "Shvecova"));

    Map<String, Person> result = 
        people.replace("456 789 123",  
                new Person("Patricia", "Novakova"), 
                new Person("Jan", "Marek"));

    Option<Person> person = result.get("456 789 123");
    assertEquals(person.get().getFirstName(), "Jan");

}

Conclusion

In this post we finished a series of articles devoted to comparasion of JCF data structures vs. Vavr immutable collections. This time we observed maps – dictionary-like data structures that store elements in a key-value form. Java supplies java.util.Map implementation of this data structure, and Vavr provides better – immutable – alternative with io.vavr.collection.Map.

Copy link
Powered by Social Snap