Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll compare two Java-based open source libraries: Apache Commons and Google Guava. Both libraries have a rich feature set with lots of utility APIs majorly in the collections and I/O area.

For brevity, here we'll only describe a handful of the most commonly used ones from the collections framework along with code samples. We'll also see a summary of their differences.

Additionally, we have a collection of articles for a deep dive into various commons and Guava utilities.

2. A Brief History of the Two Libraries

Google Guava is a Google project, mainly developed by the organization's engineers, although it's been open-sourced now. The main motivation to start it was to include generics introduced in JDK 1.5 into Java Collections Framework, or JCF, and enhance its capability.

Since its inception, the library has expanded its capabilities and now includes graphs, functional programming, range objects, caching, and String manipulation.

Apache Commons started as a Jakarta project to supplement the core Java collections API and eventually became a project of the Apache Software Foundation. Over the years, it has expanded into a vast repertoire of reusable Java components in various other areas, including (but not limited to) imaging, I/O, cryptography, caching, networking, validation, and object pooling.

As this is an open-source project, developers from the Apache community keep adding to this library to expand its capabilities. However, they take great care to maintain backward compatibility.

3. Maven Dependency

To include Guava, we need to add its dependency to our pom.xml:

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

It's latest version information can be found on Maven.

For Apache Commons, it's a bit different. Depending on the utility we want to use, we have to add that particular one. For example, for collections, we need to add:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

In our code samples, we'll be using commons-collections4.

Let's jump into the fun part now!

4. Bi-directional Maps

Maps that can be accessed by their keys, as well as values, are known as bi-directional maps. JCF does not have this feature.

Let's see how our two technologies offer them. In both cases, we'll take an example of days of the week to get the name of the day given its number and vice-versa.

4.1. Guava's BiMap

Guava offers an interface – BiMap, as a bi-directional map. It can be instantiated with one of its implementations EnumBiMap, EnumHashBiMap, HashBiMap, or ImmutableBiMap.

Here we're using HashBiMap:

BiMap<Integer, String> daysOfWeek = HashBiMap.create();

Populating it is similar to any map in Java:

daysOfWeek.put(1, "Monday");
daysOfWeek.put(2, "Tuesday");
daysOfWeek.put(3, "Wednesday");
daysOfWeek.put(4, "Thursday");
daysOfWeek.put(5, "Friday");
daysOfWeek.put(6, "Saturday");
daysOfWeek.put(7, "Sunday");

And here are some JUnit tests to prove the concept:

@Test
public void givenBiMap_whenValue_thenKeyReturned() {
    assertEquals(Integer.valueOf(7), daysOfWeek.inverse().get("Sunday"));
}

@Test
public void givenBiMap_whenKey_thenValueReturned() {
    assertEquals("Tuesday", daysOfWeek.get(2));
}

4.2. Apache's BidiMap

Similarly, Apache provides us with its BidiMap interface:

BidiMap<Integer, String> daysOfWeek = new TreeBidiMap<Integer, String>();

Here we're using TreeBidiMap. However, there're other implementations, such as DualHashBidiMap and DualTreeBidiMap as well.

To populate it, we can put the values as we did for BiMap above.

Its usage is also pretty similar:

@Test
public void givenBidiMap_whenValue_thenKeyReturned() {
    assertEquals(Integer.valueOf(7), daysOfWeek.inverseBidiMap().get("Sunday"));
}

@Test
public void givenBidiMap_whenKey_thenValueReturned() {
    assertEquals("Tuesday", daysOfWeek.get(2));
}

In a few simple performance tests, this bi-directional map lagged behind its Guava counterpart only in insertions. It was much faster in fetching keys as well as values.

5. Map Keys to Multiple Values

For a use case where we'd want to map multiple keys to different values, such as a grocery cart collection for fruits and vegetables, the two libraries offer us unique solutions.

5.1. Guava's MultiMap

First, let's see how to instantiate and initialize MultiMap:

Multimap<String, String> groceryCart = ArrayListMultimap.create();

groceryCart.put("Fruits", "Apple");
groceryCart.put("Fruits", "Grapes");
groceryCart.put("Fruits", "Strawberries");
groceryCart.put("Vegetables", "Spinach");
groceryCart.put("Vegetables", "Cabbage");

Then, we'll use a couple of JUnit tests to see it in action:

@Test
public void givenMultiValuedMap_whenFruitsFetched_thenFruitsReturned() {
    List<String> fruits = Arrays.asList("Apple", "Grapes", "Strawberries");
    assertEquals(fruits, groceryCart.get("Fruits"));
}

@Test
public void givenMultiValuedMap_whenVeggiesFetched_thenVeggiesReturned() {
    List<String> veggies = Arrays.asList("Spinach", "Cabbage");
    assertEquals(veggies, groceryCart.get("Vegetables"));
}

Additionally, MultiMap gives us the ability to remove a given entry or an entire set of values from the map:

@Test
public void givenMultiValuedMap_whenFuitsRemoved_thenVeggiesPreserved() {
    
    assertEquals(5, groceryCart.size());

    groceryCart.remove("Fruits", "Apple");
    assertEquals(4, groceryCart.size());

    groceryCart.removeAll("Fruits");
    assertEquals(2, groceryCart.size());
}

As we can see, here we first removed Apple from the Fruits set and then removed the entire Fruits set.

5.2. Apache's MultiValuedMap

Again, let's begin with instantiating a MultiValuedMap:

MultiValuedMap<String, String> groceryCart = new ArrayListValuedHashMap<>();

Since populating it is the same as we saw in the previous section, let's quickly look at the usage:

@Test
public void givenMultiValuedMap_whenFruitsFetched_thenFruitsReturned() {
    List<String> fruits = Arrays.asList("Apple", "Grapes", "Strawberries");
    assertEquals(fruits, groceryCart.get("Fruits"));
}

@Test
public void givenMultiValuedMap_whenVeggiesFetched_thenVeggiesReturned() {
    List<String> veggies = Arrays.asList("Spinach", "Cabbage");
    assertEquals(veggies, groceryCart.get("Vegetables"));
}

As we can see, its usage is also the same!

However, in this case, we don't have the flexibility to remove a single entry, such as Apple from Fruits. We can only remove the entire set of Fruits:

@Test
public void givenMultiValuedMap_whenFuitsRemoved_thenVeggiesPreserved() {
    assertEquals(5, groceryCart.size());

    groceryCart.remove("Fruits");
    assertEquals(2, groceryCart.size());
}

6. Map Multiple Keys to One Value

Here, we'll take an example of latitudes and longitudes to be mapped to respective cities:

cityCoordinates.put("40.7128° N", "74.0060° W", "New York");
cityCoordinates.put("48.8566° N", "2.3522° E", "Paris");
cityCoordinates.put("19.0760° N", "72.8777° E", "Mumbai");

Now, we'll see how to achieve this.

6.1. Guava's Table

Guava offers its Table that satisfies the above use case:

Table<String, String, String> cityCoordinates = HashBasedTable.create();

And here are some usages we can derive out of it:

@Test
public void givenCoordinatesTable_whenFetched_thenOK() {
    
    List expectedLongitudes = Arrays.asList("74.0060° W", "2.3522° E", "72.8777° E");
    assertArrayEquals(expectedLongitudes.toArray(), cityCoordinates.columnKeySet().toArray());

    List expectedCities = Arrays.asList("New York", "Paris", "Mumbai");
    assertArrayEquals(expectedCities.toArray(), cityCoordinates.values().toArray());
    assertTrue(cityCoordinates.rowKeySet().contains("48.8566° N"));
}

As we can see, we can get a Set view of the rows, columns, and values.

Table also offers us the ability to query its rows or columns.

Let's consider a movie table to demonstrate this:

Table<String, String, String> movies = HashBasedTable.create();

movies.put("Tom Hanks", "Meg Ryan", "You've Got Mail");
movies.put("Tom Hanks", "Catherine Zeta-Jones", "The Terminal");
movies.put("Bradley Cooper", "Lady Gaga", "A Star is Born");
movies.put("Keenu Reaves", "Sandra Bullock", "Speed");
movies.put("Tom Hanks", "Sandra Bullock", "Extremely Loud & Incredibly Close");

And here are some sample, self-explanatory searches that we can do on our movies Table:

@Test
public void givenMoviesTable_whenFetched_thenOK() {
    assertEquals(3, movies.row("Tom Hanks").size());
    assertEquals(2, movies.column("Sandra Bullock").size());
    assertEquals("A Star is Born", movies.get("Bradley Cooper", "Lady Gaga"));
    assertTrue(movies.containsValue("Speed"));
}

However, Table limits us to map only two keys to a value. We don't have an alternative as yet in Guava to map more than two keys to a single value.

6.2. Apache's MultiKeyMap

Coming back to our cityCoordinates example, here's how we can manipulate it using MultiKeyMap:

@Test
public void givenCoordinatesMultiKeyMap_whenQueried_thenOK() {
    MultiKeyMap<String, String> cityCoordinates = new MultiKeyMap<String, String>();

    // populate with keys and values as shown previously

    List expectedLongitudes = Arrays.asList("72.8777° E", "2.3522° E", "74.0060° W");
    List longitudes = new ArrayList<>();

    cityCoordinates.forEach((key, value) -> {
      longitudes.add(key.getKey(1));
    });
    assertArrayEquals(expectedLongitudes.toArray(), longitudes.toArray());

    List expectedCities = Arrays.asList("Mumbai", "Paris", "New York");
    List cities = new ArrayList<>();

    cityCoordinates.forEach((key, value) -> {
      cities.add(value);
    });
    assertArrayEquals(expectedCities.toArray(), cities.toArray());
}

As we can see from the above code snippet, to arrive at the same assertions as for Guava's Table, we had to iterate over the MultiKeyMap.

However, MultiKeyMap also offers the possibility to map more than two keys to a value. For example, it gives us the ability to map days of the week as weekdays or weekends:

@Test
public void givenDaysMultiKeyMap_whenFetched_thenOK() {
    days = new MultiKeyMap<String, String>();
    days.put("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Weekday");
    days.put("Saturday", "Sunday", "Weekend");

    assertFalse(days.get("Saturday", "Sunday").equals("Weekday"));
}

7. Apache Commons Collections vs. Google Guava

As per its engineers, Google Guava was born out of the need to use generics in the library, which Apache Commons didn't offer. It also follows the collections API requirements to the tee. Another major advantage is that it's in active development with new releases coming out frequently.

However, Apache offers an edge when it comes to performance while fetching a value from a collection. Guava still takes the cake though, in terms of insertion times.

Although we compared only the collections APIs in our code samples, Apache Commons as a whole offers a much bigger gamut of features as compared to Guava.

8. Conclusion

In this tutorial, we compared some of the functionality offered by Apache Commons and Google Guava, specifically in the area of the collections framework.

Here, we merely scratched the surface of what the two libraries have to offer.

Moreover, it's not an either-or comparison. As our code samples demonstrated, there're features unique to each of the two, and there can be situations where both can coexist.

As always, the source code is available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
guest
0 Comments
Inline Feedbacks
View all comments