Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial – we will illustrate one of many useful features in Guava‘s collect package: how to apply a Function to a Guava Set and obtain a Map.

We’ll discuss two approaches – creating an immutable map and a live map based on the built-in guava operations and then an implementation of a custom live Map implementation.

2. Setup

First, we’ll add the Guava library as a dependency in pom.xml:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

A quick note – you can check if there’s a newer version here.

3. The Mapping Function

Let’s first define the function that we’ll apply on the sets elements:

    Function<Integer, String> function = new Function<Integer, String>() {
        @Override
        public String apply(Integer from) {
            return Integer.toBinaryString(from.intValue());
        }
    };

The function is simply converting the value of an Integer to its binary String representation.

4. Guava toMap()

Guava offers a static utility class pertaining to Map instances. Among others, it has two operations that can be used to convert a Set to a Map by applying the defined Guava’s Function.

The following snippet shows creating an immutable Map:

Map<Integer, String> immutableMap = Maps.toMap(set, function);

The following tests asserts that the set is properly converted:

@Test
public void givenStringSetAndSimpleMap_whenMapsToElementLength_thenCorrect() {
    Set set = new TreeSet(Arrays.asList(32, 64, 128));
    Map<Integer, String> immutableMap = Maps.toMap(set, function);
    assertTrue(immutableMap.get(32).equals("100000")
      && immutableMap.get(64).equals("1000000")
      && immutableMap.get(128).equals("10000000"));
}

The problem with the created map is that if an element is added to the source set, the derived map is not updated.

4. Guava asMap()

If we use the previous example and create a map using the Maps.asMap method:

Map<Integer, String> liveMap = Maps.asMap(set, function);

We’ll get a live map view – meaning that the changes to the originating Set will be reflected in the map as well:

@Test
public void givenStringSet_whenMapsToElementLength_thenCorrect() {
    Set<Integer> set = new TreeSet<Integer>(Arrays.asList(32, 64, 128));
    Map<Integer, String> liveMap = Maps.asMap(set, function);
    assertTrue(liveMap.get(32).equals("100000")
            && liveMap.get(64).equals("1000000")
            && liveMap.get(128).equals("10000000"));
    
    set.add(256);
    assertTrue(liveMap.get(256).equals("100000000") && liveMap.size() == 4);
}

Note that the tests assert properly despite that we added an element through a set and looked it up inside the map.

5. Building Custom Live Map

When we talk of the Map View of a Set, we are basically extending the capability of the Set using a Guava Function.

In the live Map view, changes to the Set should be updating the Map EntrySet in real time. We will create our own generic Map, sub-classing AbstractMap<K,V>, like so:

public class GuavaMapFromSet<K, V> extends AbstractMap<K, V> {
    public GuavaMapFromSet(Set<K> keys, 
        Function<? super K, ? extends V> function) { 
    }
}

Worthy of note is that the main contract of all sub-classes of AbstractMap is to implement the entrySet method, as we’ve done. We will then look at 2 critical parts of the code in the following sub-sections.

5.1. Entries

Another attribute in our Map will be entries, representing our EntrySet:

private Set<Entry<K, V>> entries;

The entries field will always be initialized using the input Set from the constructor:

public GuavaMapFromSet(Set<K> keys,Function<? super K, ? extends V> function) {
    this.entries=keys;
}

A quick note here – in order to maintain a live view, we will use the same iterator in the input Set for the subsequent Map‘s EntrySet.

In fulfilling the contract of AbstractMap<K,V>, we implement the entrySet method in which we then return entries:

@Override
public Set<java.util.Map.Entry<K, V>> entrySet() {
    return this.entries;
}

5.2. Cache

This Map stores the values obtained by applying Function to Set:

private WeakHashMap<K, V> cache;

6. The Set Iterator

We will use the input Set‘s iterator for the subsequent Map‘s EntrySet. To do this, we use a customized EntrySet as well as a customized Entry class.

6.1. The Entry Class

First, let’s see how a single entry in the Map will look like:

private class SingleEntry implements Entry<K, V> {
    private K key;
    public SingleEntry( K key) {
        this.key = key;
    }
    @Override
    public K getKey() {
        return this.key;
    }
    @Override
    public V getValue() {
        V value = GuavaMapFromSet.this.cache.get(this.key);
  if (value == null) {
      value = GuavaMapFromSet.this.function.apply(this.key);
      GuavaMapFromSet.this.cache.put(this.key, value);
  }
  return value;
    }
    @Override
    public V setValue( V value) {
        throw new UnsupportedOperationException();
    }
}

Clearly, in this code, we don’t allow modifying the Set from the Map View as a call to setValue throws an UnsupportedOperationException.

Pay close attention to getValue – this is the crux of our live view functionality. We check cache inside our Map for the current key (Set element).

If we find the key, we return it, else we apply our function to the current key and obtain a value, then store it in cache.

This way, whenever the Set has a new element, the map is up to date since the new values are computed on the fly.

6.2. The EntrySet

We will now implement the EntrySet:

private class MyEntrySet extends AbstractSet<Entry<K, V>> {
    private Set<K> keys;
    public MyEntrySet(Set<K> keys) {
        this.keys = keys;
    }
    @Override
    public Iterator<Map.Entry<K, V>> iterator() {
        return new LiveViewIterator();
    }
    @Override
    public int size() {
        return this.keys.size();
    }
}

We have fulfilled the contract for extending AbstractSet by overriding the iterator and size methods. But there’s more.

Remember the instance of this EntrySet will form the entries in our Map View. Normally, the EntrySet of a map simply returns a complete Entry for each iteration.

However, in our case, we need to use the iterator from the input Set to maintain our live view. We know well it will only return the Set‘s elements, so we also need a custom iterator.

6.3. The Iterator

Here is the implementation of our iterator for the above EntrySet:

public class LiveViewIterator implements Iterator<Entry<K, V>> {
    private Iterator<K> inner;
    
    public LiveViewIterator () {
        this.inner = MyEntrySet.this.keys.iterator();
    }
    
    @Override
    public boolean hasNext() {
        return this.inner.hasNext();
    }
    @Override
    public Map.Entry<K, V> next() {
        K key = this.inner.next();
        return new SingleEntry(key);
    }
    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

LiveViewIterator must reside inside the MyEntrySet class, this way, we can share the Set‘s iterator at initialization.

When looping through GuavaMapFromSet‘s entries using the iterator, a call to next simply retrieves the key from the Set‘s iterator and constructs a SingleEntry.

7. Putting It All Together

After stitching together what we have covered in this tutorial, let us take a replace the liveMap variable from the previous samples, and replace it with our custom map:

@Test
public void givenIntSet_whenMapsToElementBinaryValue_thenCorrect() {
    Set<Integer> set = new TreeSet<>(Arrays.asList(32, 64, 128));
    Map<Integer, String> customMap = new GuavaMapFromSet<Integer, String>(set, function);
    
    assertTrue(customMap.get(32).equals("100000")
      && customMap.get(64).equals("1000000")
      && customMap.get(128).equals("10000000"));
}

Changing the content of input Set, we will see that the Map updates in real time:

@Test
public void givenStringSet_whenMapsToElementLength_thenCorrect() {
    Set<Integer> set = new TreeSet<Integer>(Arrays.asList(32, 64, 128));
    Map<Integer, String> customMap = Maps.asMap(set, function);
    
    assertTrue(customMap.get(32).equals("100000")
      && customMap.get(64).equals("1000000")
      && customMap.get(128).equals("10000000"));
    
    set.add(256);
    assertTrue(customMap.get(256).equals("100000000") && customMap.size() == 4);
}

8. Conclusion

In this tutorial, we have looked at the different ways that one can leverage Guava operations and obtain a Map view from a Set by applying a Function.

The full implementation of all these examples and code snippets can be found in my Guava github project – this is an Eclipse based project, so it should be easy to import and run as it is.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.