Introduction
The Google Guava Table is part of the Google Guava Collections. It allows us to map a value to two ordered keys, called as the row key and the column key. Let us explore the Google Guava Table collection in detail in this post.
Mapping a value to two ordered keys
Normally, when we have to index or map a value (say a String) against two (ordered) keys, we would have had something like,
Map<String, Map<String, String>> table = new HashMap<>();
The outer map is for the first level key (the row) and the inner map is for the second key (the column). The value of the inner map is the actual value.
For example, let us say we have the below income data for two companies for two quarters in a year.
Company Name | Q1 | Q2 |
---|---|---|
Company A | $1M | $1.2M |
Company B | $2.1M | $2M |
The row is the company name and column is the quarter for which the revenue income is shown.
Table implemented using a nested map
As we will see, it is very cumbersome to use the data structure of a nested map to support various operations or queries.
Map<String, Map<String, String>> table = new HashMap<>();
table.put("Company A", Map.of(
"Q1", "$1M",
"Q2", "$1.2M"));
table.put("Company B", Map.of(
"Q1", "$2.1M",
"Q2", "$2M"));
Let us say we want to fetch the income for each quarter for each company. Then the existing table data structure would be sufficient.
System.out.println(table);
This would give us,
{Company A={Q2=$1.2M, Q1=$1M}, Company B={Q2=$2M, Q1=$2.1M}}
Let us say we want to pivot it and group by the quarter. In other words, for each quarter, return the company to its income mapping. For this, we have to iterate over the map and re-group as per the requirement.
Map<String, Map<String, String>> quarterToCompanyIncome = new HashMap<>();
for (Map.Entry<String, Map<String, String>> entry : table.entrySet()) {
String company = entry.getKey();
Map<String, String> innerMap = entry.getValue();
for(Map.Entry<String, String> innerEntry: innerMap.entrySet()) {
if (!quarterToCompanyIncome.containsKey(innerEntry.getKey())) {
quarterToCompanyIncome.put(innerEntry.getKey(), new HashMap<>());
}
quarterToCompanyIncome.get(innerEntry.getKey())
.put(company, innerEntry.getValue());
}
}
System.out.println(quarterToCompanyIncome);
Prints,
{Q1={Company A=$1M, Company B=$2.1M}, Q2={Company A=$1.2M, Company B=$2M}}
If you are a stream (and records) fan in Java like me, you could write,
record CompanyQuarterIncome(String company, String quarter, String income) { }
Map<String, Map<String, String>> quarterToCompanyIncome = table.entrySet().stream()
.flatMap(entry -> entry.getValue().entrySet()
.stream()
.map(innerEntry -> new CompanyQuarterIncome(entry.getKey(), innerEntry.getKey(), innerEntry.getValue())))
.collect(Collectors.groupingBy(CompanyQuarterIncome::quarter,
Collectors.toMap(CompanyQuarterIncome::company, CompanyQuarterIncome::income)));
System.out.println(quarterToCompanyIncome);
We first stream the table entries and use flatMap to flatten the nested map to instances of the record (CompanyQuarterIncome). Then, we use Collectors.groupingBy to group by each quarter. We use Collectors.toMap as the downstream collectors to form mapping between the company name to the revenue/income for the grouped quarter.
Or say if we want the company to income mapping for a given quarter. Even then, we have to write similar logic to iterate over the map as shown below.
String quarter = "Q2";
Map<String, String> companyToIncomeForAQuarter = new HashMap<>();
for (Map.Entry<String, Map<String, String>> entry : table.entrySet()) {
companyToIncomeForAQuarter.put(entry.getKey(), entry.getValue()
.get(quarter));
}
System.out.println(companyToIncomeForAQuarter); //{Company A=$1.2M, Company B=$2M}
Again, using a stream based solution, it would look like,
Map<String, String> companyToIncomeForAQuarter = table.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get(quarter)));
Google Guava Table
Google Guava’s Table collection was added to support the exact above use case. It acts as a nice top level abstraction, providing various useful methods to work with a table (a value mapped to two ordered keys). Thus, we won’t be bothered about managing nested maps and writing all the complex code as seen earlier.
The Google Guava Table is a collection which associates two ordered keys, called as the row key and the column key, with a single value. The Table can be sparse, i.e., only a few row key/column key combinations have a value.
Google Guava Table interface
Let us now look at the Google Guava Table interface.
The Table interface is generic with three type parameters:
public interface Table<R, C, V>
where R, C, and V represent the type of row, column, and the value.
The Table interface has methods to get the value mapped to the row/column key pair, to get a view (map) of all row keys and values for a given column, get a view of all columns to values for a given row key etc., Summarizing some of the key methods,
- row(r): For a passed row key, it returns a Map<C, V> i.e., mapping of column key to the value (for the input row key).
- rowKeySet(): Returns a set of row keys that have one or more values in the table.
- rowMap(): Returns a Map<R, Map<C, V>> i.e., for each row, the nested map is the corresponding column keys to values in the table.
Similarly, for column, we have
- column(c): For a passed column key, it returns a Map<R, V>.
- columnKeySet(): Returns a set of column keys that have one or more values in the table.
- columnMap(): Returns a Map<C, Map<R, V>> i.e., for each column, the nested map is the corresponding row keys to values in the table.
Note that all the methods which return collections or map are just views of the underlying table. Hence, any update to the table will reflect in the returned view. And any updates on the returned view will affect the table as well.
Google Guava Table’s subclasses
There are four main subclasses of the Table interface:
- HashBasedTable
- TreeBasedTable
- ArrayTable
- ImmutableTable
We will look at the HashBasedTable first and then for the other implementations, I’ll highlight only the Table methods which behave differently.
HashBasedTable
HashBasedTable is a Table implemented using linked hash map. Thus, it will main the order in which data (row/column) was inserted into the table.
Creating a HashBasedTable
The simplest way to create a HashBasedTable is to use its static factory method create().
Table<String, String, String> table = HashBasedTable.create();
Now, we have an empty table which can map a string value to two ordered keys, row and column keys, both are Strings.
Let us populate the table with some data. For this post, I’ll be using the company revenue data introduced earlier. We can use the table’s put() method to insert data as follows.
table.put("Company A", "Q1", "$1M");
table.put("Company A", "Q2", "$1.2M");
table.put("Company B", "Q1", "$2.1M");
table.put("Company B", "Q2", "$2M");
The three arguments in order are the row, column and the cell value. In the first insert, we are mapping the value “$1M" against the row key “Company A” and the column key “Q1”.
Other overloaded HashBasedTable#create methods
If we know the expected row and column size (expected cells per row), we can use the overloaded create method passing them.
Table<String, String, String> table = HashBasedTable.create(2, 2);
Or if we have to create a new HashTable from an existing table (could be any type), we can pass the table to the create method.
Table<String, String, String> table1 = TreeBasedTable.create();
//...
//Create a HashBasedTable from the contents of the TreeBasedTable (table1)
Table<String, String, String> table2 = HashBasedTable.create(table1);
Retrieving the value for a row/column
To retrieve the value mapped for a given row and column keys, we can use the get() method as shown below. If no mapping exists, it will return a null.
System.out.println(table.get("Company A", "Q2")); //$1.2M
System.out.println(table.get("Company B", "Q2")); //$2M
System.out.println(table.get("Company C", "Q2")); //null
Checking if table contains a row/column/value
We will see four simple methods here - contains, containsRow, containsColumn, and containsValue
The contains takes a row key and column key and returns true if the table has a valid value mapped to the passed row key and column key combination.
System.out.println(table.contains("Company A", "Q2")); //true
System.out.println(table.contains("Company C", "Q2")); //false
The containsRow takes as input a row key and checks if the table has data for the passed row key. Similarly, the containsColumn works on a column key (checks if the table has a mapping with the specified column)
System.out.println(table.containsRow("Company A")); //true
System.out.println(table.containsRow("Company C")); //false
System.out.println(table.containsColumn("Q1")); //true
System.out.println(table.containsColumn("Q3")); //false
Finally, the containsValue checks if the table has a mapping for the passed value.
System.out.println(table.containsValue("$2.1M")); //true
System.out.println(table.containsValue("$3M")); //false
Table - Methods operating on row
In this section, we will see three methods which work against a row viz., row(), rowKeySet() and rowMap().
Table#row
Calling the row() method, we pass the key of the row to search in the table. As a result, it will return all the mappings present in the table for that row key. The map it returns is the map of column keys to values for the given row key. If the table has no data for the passed row key, it will return an empty map.
The below example lets us know how much profit a company made for the two quarters.
System.out.println(table.row("Company A"));
System.out.println(table.row("Company B"));
System.out.println(table.row("Company C"));
Prints,
{Q1=$1M, Q2=$1.2M}
{Q1=$2.1M, Q2=$2M}
{}
For the first two calls, we passed an existing row key. It hence returned a map which had mapping of the column keys (the two quarters) to the values. The last call returned an empty map as no data exists in the table for “Company C”.
Table#rowKeySet
The rowKeySet returns the set of all row keys which have one or more values in the table.
System.out.println(table.rowKeySet()); //[Company A, Company B]
Table#rowMap
This will return the entire table data as a nested map (Map<R, Map<C, V>>). For each row key, the corresponding nested map has column keys to values mapping. In other words, (as in Javadoc), it returns a map view from each row key to a secondary map from column keys to values.
System.out.println(table.rowMap());
Prints,
{Company A={Q1=$1M, Q2=$1.2M}, Company B={Q1=$2.1M, Q2=$2M}}
Table - Methods operating on column
Similar to the methods related to row(s), for column(s), we have column(), columnKeySet(), and columnMap().
Table#column
We pass a column key value when calling the column() method. It will return a view of the mappings for that column key, i.e., the returned map shows all the row keys mapped to value for the passed column key. If the passed column has no mappings in the table, it returns an empty map.
Each of the below queries fetches the income/revenue data for all companies for a quarter.
System.out.println(table.column("Q1"));
System.out.println(table.column("Q2"));
System.out.println(table.column("Q3"));
This will print,
{Company A=$1M, Company B=$2.1M}
{Company A=$1.2M, Company B=$2M}
{}
Table#columnKeySet
The columnKeySet method returns the list of column keys which have one or more values in the table.
System.out.println(table.columnKeySet()); //[Q1, Q2]
Table#columnMap
Calling the columnMap returns the table data as a nested map of type Map<C, Map<R, V>>. The returned data is similar to the rowMap, but rows and columns are pivoted. For each column key value, the nested map has mappings from the row key to the value.
System.out.println(table.columnMap());
Prints,
{Q1={Company A=$1M, Company B=$2.1M}, Q2={Company A=$1.2M, Company B=$2M}}
Table#cellSet
It returns a Set<Cell<R, C, V>> i.e., set of all cells (set of all row key/column key/value triplets). Cell is an interface within the Table interface.
interface Cell<R, C, V> {
/** Returns the row key of this cell. */
@Nullable
R getRowKey();
/** Returns the column key of this cell. */
@Nullable
C getColumnKey();
/** Returns the value of this cell. */
@Nullable
V getValue();
// equals and hashCode methods omitted
}
System.out.println(table.cellSet());
This prints,
[(Company A,Q1)=$1M, (Company A,Q2)=$1.2M, (Company B,Q1)=$2.1M, (Company B,Q2)=$2M]
Returning all the values in the table and size of the table
The values() returns collection of all the values in the table (hence may contain duplicates). The size() returns the number of values in the table.
System.out.println(table.values()); //[$1M, $1.2M, $2.1M, $2M]
System.out.println(table.size()); //4
Table - putAll and remove
We pass another table instance reference to the putAll method and it copies all the mappings from that table to this table. This is the same as calling the put() method with each row key/column key/value mapping in the other table.
Table<String, String, String> table1 = HashBasedTable.create();
table1.put("Company A", "Q1", "$1M");
table1.put("Company B", "Q1", "$2.1M");
Table<String, String, String> table2 = HashBasedTable.create();
table2.put("Company A", "Q1", "$1.5M");
table2.put("Company C", "Q1", "$3.1M");
table1.putAll(table2);
System.out.println(table1);
In the above code, we add all value mappings from table2 into table1. Hence, the value for “Company A”/“Q1” would be overwritten from table2.
Prints,
{Company A={Q1=$1.5M}, Company B={Q1=$2.1M}, Company C={Q1=$3.1M}}
The remove takes the row key and column key and removes the mapping associated with them. It then returns the value associated with the passed keys; or null if the passed keys had no mapping.
Table<String, String, String> table1 = HashBasedTable.create();
table1.put("Company A", "Q1", "$1M");
table1.put("Company B", "Q1", "$2.1M");
System.out.println(table1.remove("Company A", "Q1")); //$1M
System.out.println(table1.remove("Company A", "Q2")); //null
System.out.println(table1); //{Company B={Q1=$2.1M}}
TreeBasedTable
The TreeBasedTable implementation uses a TreeMap as the data structure for storing the data in the table. Thus, it orders the row keys and column keys by their natural ordering (unless we pass a comparator during the table construction time).
For this, I’ll show examples of only some of the Table methods and skip the ones which have similar behaviour as we’ve seen in HashBasedTable. In other words, I won’t be repeating all the methods we’ve seen in HashBasedTable again, but I’ll comment on the methods which have an impact due to the change in the underlying table data structure.
TreeBasedTable creation
The TreeBasedTable.create() method creates an empty TreeBasedTable which uses natural ordering for the row and column keys.
TreeBasedTable<String, String, String> treeBasedTable = TreeBasedTable.create();
treeBasedTable.put("Company A", "Q1", "$1M");
treeBasedTable.put("Company A", "Q2", "$1.2M");
treeBasedTable.put("Company B", "Q1", "$2.1M");
treeBasedTable.put("Company B", "Q2", "$2M");
System.out.println(treeBasedTable);
The rows and columns will be ordered by their natural order.
{Company A={Q1=$1M, Q2=$1.2M}, Company B={Q1=$2.1M, Q2=$2M}}
There is an overloaded method to which we can pass a custom comparator on how to order the row and column keys. In the below code, we sort the row keys in natural order, but the column keys in the reverse order.
TreeBasedTable<String, String, String> treeBasedTable = TreeBasedTable.create(Comparator.naturalOrder(),
Comparator.reverseOrder());
treeBasedTable.put("Company A", "Q1", "$1M");
treeBasedTable.put("Company A", "Q2", "$1.2M");
treeBasedTable.put("Company B", "Q1", "$2.1M");
treeBasedTable.put("Company B", "Q2", "$2M");
System.out.println(treeBasedTable);
Since we’ve reverse sorted the column keys, “Q2” comes before “Q1” in the ordering, as shown below.
{Company A={Q2=$1.2M, Q1=$1M}, Company B={Q2=$2M, Q1=$2.1M}}
There is another create() method which allows us to create a TreeBasedTable from another TreeBasedTable.
Row and column methods
All the methods related to the row and column work the same way as seen in HashBasedTable. But the methods, rowMap(), and row() return SortedMap instances. Similarly, rowKeySet() returns a SortedSet.
System.out.println(treeBasedTable.row("Company A")); //{Q2=$1.2M, Q1=$1M}
System.out.println(treeBasedTable.row("Company C")); //{}
System.out.println(treeBasedTable.rowKeySet()); //[Company A, Company B]
System.out.println(treeBasedTable.rowMap()); //{Company A={Q2=$1.2M, Q1=$1M}, Company B={Q2=$2M, Q1=$2.1M}}
The same applies for the column based methods. Even though the return types are still Set and Map here (as in the Table interface), the output follows the actual ordering as in the table.
System.out.println(treeBasedTable.column("Q1")); //{Company A=$1M, Company B=$2.1M}
System.out.println(treeBasedTable.column("Q3")); //{}
System.out.println(treeBasedTable.columnKeySet()); //[Q2, Q1]
System.out.println(treeBasedTable.columnMap()); //{Q2={Company A=$1.2M, Company B=$2M}, Q1={Company A=$1M, Company B=$2.1M}}
TreeBasedTable - cellSet method
It returns set of all row key/column key/value triplets. The order is dependent on the underlying TreeMap’s ordering. Hence, we get values for Q2 before Q1.
System.out.println(treeBasedTable.cellSet());
Prints,
[(Company A,Q2)=$1.2M, (Company A,Q1)=$1M, (Company B,Q2)=$2M, (Company B,Q1)=$2.1M]
ArrayTable
An ArrayTable uses a two-dimensional array to represent the table. This implementation of Table is rarely what you want (this fact is quoted in its Javadoc as well). The biggest problem is that you have to specify the exact set of row keys and column keys when you construct an ArrayTable. And it creates a 2D array to hold values for the row and column keys specified. Hence, it doesn’t work well for sparse tables as every pair of row key and column key would have a value (if not specified, it would be null).
Note: This class is marked as @Beta.
ArrayTable construction
We specify the row and column list when calling the create() method.
ArrayTable<String, String, String> table = ArrayTable.create(
List.of("Company A", "Company B"),
List.of("Q1", "Q2")
);
Then we can add mappings as,
table.put("Company A", "Q1", "$1M");
table.put("Company A", "Q2", "$1.2M");
table.put("Company B", "Q1", "$2.1M");
table.put("Company B", "Q2", "$2M");
The order of the row and column keys we specify during construction will determine the iteration order across rows and columns.
System.out.println(table);
Prints,
{Company A={Q1=$1M, Q2=$1.2M}, Company B={Q1=$2.1M, Q2=$2M}}
Since the underlying 2D array can only be accessed via integer indices, it maintains a mapping of the row and column names to the array index.
ArrayTable - row and column methods
The row and column related methods work the same way as we have seen in other implementation (but on a 2D array here).
System.out.println(table.row("Company A"));
System.out.println(table.rowKeySet());
System.out.println(table.rowMap());
System.out.println(table.column("Q1"));
System.out.println(table.columnKeySet());
System.out.println(table.columnMap());
Prints,
{Q1=$1M, Q2=$1.2M}
[Company A, Company B]
{Company A={Q1=$1M, Q2=$1.2M}, Company B={Q1=$2.1M, Q2=$2M}}
{Company A=$1M, Company B=$2.1M}
[Q1, Q2]
{Q1={Company A=$1M, Company B=$2.1M}, Q2={Company A=$1.2M, Company B=$2M}}
ArrayTable - remove
It doesn’t support the remove method and so we have to use the erase() method. That will just set the mapping (value) to null.
table.erase("Company B", "Q2");
System.out.println(table.get("Company B", "Q2")); // null
Unlike other implementations we have seen, for ArrayTable, even if a mapping is null, it will show up when printing the table data, as shown below.
// After the above erase call of "Company B" and "Q2"
System.out.println(table.rowMap());
System.out.println(table.columnMap());
This would print value for the tuple keys “Company B” and “Q2” as null.
{Company A={Q1=$1M, Q2=$1.2M}, Company B={Q1=$2.1M, Q2=null}}
{Q1={Company A=$1M, Company B=$2.1M}, Q2={Company A=$1.2M, Company B=null}}
Additional methods for ArrayTable
Accessing by index
The array table allows us to access the value or to change the value for a key pair by using the row and column index.
System.out.println(table.at(0, 0)); //$1M
System.out.println(table.at(0, 1)); //$1.2M
System.out.println(table.at(1, 0)); //$2.1M
System.out.println(table.at(1, 1)); //$2M
We can update (call set) with index of row and column as well. It returns the previous value mapped.
System.out.println(table.set(0, 0, "$1.1M")); //$1M
System.out.println(table.at(0, 0)); //$1.1M
System.out.println(table.get("Company A", "Q1")); //$1.1M
rowKeyList, columnKeyList
The rowKeyList and columnKeyList returns as immutable list, the row keys and column keys provided when the table was built, respectively (including those that are mapped to null values only).
System.out.println(table.rowKeyList()); //[Company A, Company B]
System.out.println(table.columnKeyList()); //[Q1, Q2]
ImmutableTable
The last implementation we will see is the ImmutableTable. It is a table whose contents (mappings) never change.
Creation of ImmutableTable
We can create an empty immutable table using ImmutableTable.of(). We can create an immutable table with one mapping as,
Table<String, String, String> oneEntryTable = ImmutableTable.of("Company B", "Q1", "$2.1M");
System.out.println(oneEntryTable); //{Company B={Q1=$2.1M}}
ImmutableTable builder method
ImmutableTable offers a builder to build an ImmutableTable. At the very basic, it supports put and putAll. Let us say we have the “Company B" details in a HashBasedTable. We want to build an ImmutableTable with this plus the “Company A” details. This is shown below.
Table<String, String, String> companyBTable = HashBasedTable.create();
companyBTable.put("Company B", "Q1", "$2.1M");
companyBTable.put("Company B", "Q2", "$2M");
Table<String, String, String> table = ImmutableTable.<String, String, String>builder()
.put("Company A", "Q1", "$1M")
.put(Tables.immutableCell("Company A", "Q2", "$1.2M"))
.putAll(companyBTable)
.build();
System.out.println(table); //{Company A={Q1=$1M, Q2=$1.2M}, Company B={Q1=$2.1M, Q2=$2M}}
There are two ways we can pass individual value - either by passing the row key, column key and the mapped value to the put() method, or by creating an instance of a Cell by using the Tables.immutableCell utility method. To the putAll() method we can pass another existing table.
The builder also allows us to pass comparators to order the rows and columns as well (orderRowsBy and orderColumnsBy).
Table<String, String, String> table = ImmutableTable.<String, String, String>builder()
.orderRowsBy(Comparator.naturalOrder())
.orderColumnsBy(Comparator.reverseOrder())
.put("Company A", "Q1", "$1M")
.put("Company A", "Q2", "$1.2M")
.build();
System.out.println(table); //{Company A={Q2=$1.2M, Q1=$1M}}
Beware of duplicate row/column/cell value
One interesting thing to notice is that we cannot do a duplicate put (even if the row, column and value are the same) when building an ImmutableTable as shown below.
//throws java.lang.IllegalArgumentException: Duplicate key: (row=Company A, column=Q1), values: [$1M, $1M].
Table<String, String, String> table = ImmutableTable.<String, String, String>builder()
.put("Company A", "Q1", "$1M")
.put("Company A", "Q1", "$1M")
.build();
Cannot update/mutate a mapping
Since it is an ImmutableTable, we cannot do a put() once the table is constructed.
//throws java.lang.UnsupportedOperationException
table.put("Company A", "Q1","$2.7M");
//throws java.lang.UnsupportedOperationException
table.remove("Company A", "Q1");
Conclusion
We learnt about the Google Guava Table interface and how it can help us manage a table (value mapped to two ordered keys). Then we saw the important methods of the Table interface and learnt about four subclasses of the Google Guava Table with examples. While you are here, check out the other google-guava posts and the awesome collection of useful classes the library offers.