Google Guava Converter

Introduction

The Google Guava Converter class represents a mapping or conversion from type A to B and vice versa. This post explores the Google Guava Converter class and how we can create a converter and use it.

Google Guava Converter

The Google Guava Converter is an abstract class which represents a function to convert a value of type A to a value of type B. It also supports the transformation in the reverse direction, i.e., from B to A. The values of types A and B represent the same information in a different format or representation. Hence, we can say that the Converter is used for converting back and forth between different representations of the same information.

You can think of a Converter as a Java Function (functional interface), but capable of converting in the reverse direction as well.

Google Guava Converter class and its methods

Let us look at the key methods in the Converter class. We will explore each of the methods in detail with examples in this post.

The Converter class has two type parameters indicating the types A and B. Interestingly, it implements Function<A, B> where the Function interface is Google Guava Function.

public abstract class Converter<A, B> implements Function<A, B> {
    public final B convert(@CheckForNull A a) {
        ....
    }

    public Converter<B, A> reverse() {
        ....
    }
}

The two of the most important methods are the convert(A a) and the reverse() method.

The convert method returns a representation of a (type A) as an instance of type B and the reverse method returns the reversed view of the converter which does the inverse or reverse conversion. Hence it returns a Converter<B, A> which returns a representation of b (type B) to as an instance of type A (Remember that is the same piece of information which is represented in two ways or two types here).

Since the Converter class implements the Google Guava Function interface, it also implements the apply(A a) method which just calls the convert(A a) method.

There are a few other methods as well which I’ll introduce and explain later in this post.

Two ways of creating a Converter instance

Let us look at two ways of creating a Converter instance.

Subclassing the Converter abstract class

The first way to create a Converter object is to extend the Google Guava Converter class and implement the doForward(A) and doBackward(B) (protected) abstract methods.

The doForward(A) method takes an object of type A and returns a representation of a as an instance of type B. Similarly, doBackward(B) method takes an object of type B and returns a representation of b as an instance of type A. If the passed values cannot be converted (say they are not valid), then an IllegalArgumentException should be thrown.

Example of subclassing the Converter abstract class

Let us implement a Converter to convert an Integer represented as a String to the Integer type. Here, we are converting between two representations (String and Integer) of the same value (some integer).

import com.google.common.base.Converter;

public class StringToIntegerConverter extends Converter<String, Integer> {
    @Override
    protected Integer doForward(String s) {
        try {
            return Integer.valueOf(s);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("The input was not a valid integer");
        }
    }

    @Override
    protected String doBackward(Integer integer) {
        return String.valueOf(integer);
    }
}

We extend the Converter class where the type parameters A and B would be String and Integer, respectively, as we would be converting from a String to an Integer (in the forward direction). Then, we implement the forward transformation logic in the doForward method. To adhere to the API contract, for invalid inputs, rather than propagating the NumberFormatException unchecked exception, we catch it and throw an IllegalArgumentException. In the doBackward method, we implement the logic to do the reverse conversion, i.e., an Integer to a String value.

Shown below is the code to use the Converter class instance and perform the conversion of an integer (123) from String representation to its Integer representation.

Converter<String, Integer> stringToIntegerConverter = new StringToIntegerConverter();
Integer intResult = stringToIntegerConverter.convert("123");
System.out.println(intResult); //123

If we pass an invalid value to the convert method, it will throw an IllegalArgumentException.

// java.lang.IllegalArgumentException: The input was not a valid integer
System.out.println(stringToIntegerConverter.convert("abc"));

We can call the reverse() method and get a reverse converter, i.e., the converter which converts an Integer to its String format. As seen from the below code, the reverse converter type is Converter<Integer, String> and we pass an Integer to its convert method and get back a String result.

Converter<Integer, String> integerToStringConverter = stringToIntegerConverter.reverse();
String strResult = integerToStringConverter.convert(123);
System.out.println(strResult); //123

Converter#from method

The second method for creating a Google Guava Converter instance is to use the Converter#from static method. It takes two Google Guava Functions, namely the forward function, and the backward function. The method signature is shown below.

public static <A, B> Converter<A, B> from(
      Function<? super A, ? extends B> forwardFunction,
      Function<? super B, ? extends A> backwardFunction) { ... }

It uses the forward function to convert from type A to type B and the backward function for the reverse transformation.

Let us now use this to implement the same String to Integer converter. In the below code, the first function (the forward function) is a lambda expression, which takes a String and returns an Integer using Integer#valueOf. We have the same try…catch mechanism to handle invalid inputs as seen before. The second function (the backward function) is a method reference to convert an Integer to a String (it does it by passing the Integer to the valueOf method in the String class).

Converter<String, Integer> stringToIntegerConverter = Converter.from(
        s -> {
            try {
                return Integer.valueOf(s);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("The input was not a valid integer");
            }
        },
        String::valueOf
);

We can improve the readability of the above by moving the first lambda expression to a (static) helper function and use a method reference instead. Let’s say the above code is in a main method of a class named ConverterUsingFrom. Then we can move the code to a helper method and use a method reference, as shown below.

Converter<String, Integer>  stringToIntegerConverter = Converter.from(
            ConverterUsingFrom::stringToInteger,
            String::valueOf
    );

private static Integer stringToInteger(String s) {
    try {
        return Integer.valueOf(s);
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("The input was not a valid integer");
    }
}

Since this is just a different (and a simpler) way to create a Converter instance, the code behaves in a similar way as seen before. Calling the convert() method and passing a string returns the appropriate Integer as seen below.

Integer intResult = stringToIntegerConverter.convert("123");
System.out.println(intResult);

And as before, passing an invalid input will throw an IllegalArgumentException.

//throws java.lang.IllegalArgumentException: The input was not a valid integer
System.out.println(stringToIntegerConverter.convert("abc"));

Similarly, we can use the reverse() to get a reverse converter, as illustrated below.

Converter<Integer, String> integerToStringConverter = stringToIntegerConverter.reverse();
String strResult = integerToStringConverter.convert(123);
System.out.println(strResult); //123

Converting multiple values using convertAll method

We have looked at how to create a Converter object and use it convert a passed value. We can convert multiple values from type A to B (or from type B to A if using the reverse converter) as well. For this, we can use the convertAll() method. It takes an Iterable<? extends A> and returns an Iterable<B>. Internally, it applies the convert logic to each element of the passed Iterable.

public Iterable<B> convertAll(final Iterable<? extends A> fromIterable) { ... }

Here, we use the StringToIntegerConverter class for the converter and pass a list of strings as input (A List is an Iterable). We get back Iterable<Integer> as the result and we get the Iterator from it and we iterate over each element and print it.

Converter<String, Integer> stringToIntegerConverter = new StringToIntegerConverter();
List<String> strings = List.of("1", "2", "3");
Iterable<Integer> integersResult = stringToIntegerConverter.convertAll(strings);
Iterator<Integer> integersResultIterable = integersResult.iterator();
while (integersResultIterable.hasNext()) {
    System.out.println(integersResultIterable.next());
}

This prints,

1
2
3

The reverse converter construction and using convertAll on the reverse converter is shown below.

Converter<Integer, String> integerToStringConverter = stringToIntegerConverter.reverse();
List<Integer> ints = List.of(1, 2, 3);
Iterable<String> stringsResult = integerToStringConverter.convertAll(ints);
Iterator<String> stringsResultIterable = stringsResult.iterator();
while (stringsResultIterable.hasNext()) {
    System.out.println(stringsResultIterable.next());
}

Prints,

1
2
3

The conversion is lazy

Note that the conversion is done lazily. In other words, unless we call the next() on the resultant Iterable, it will not call the convert() method for the next element in the input Iterable.

Let us add a print statement in the doForward method in the StringToIntegerConverter class.

protected Integer doForward(String s) {
    try {
        System.out.println("Converting the input string: " + s);
        return Integer.valueOf(s);
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("The input was not a valid integer");
    }
}

Now, after creating a Converter and calling the convertAll method let us call the next() on the resultant Iterator only twice. We can see that the newly added print statement is also printed only two times as the conversion of the values in the passed input Iterable happens lazily.

Converter<String, Integer> stringToIntegerConverter = new StringToIntegerConverter();
List<String> strings = List.of("1", "2", "3");
Iterable<Integer> integersResult = stringToIntegerConverter.convertAll(strings);
Iterator<Integer> integersResultIterable = integersResult.iterator();

System.out.println(integersResultIterable.next());
System.out.println(integersResultIterable.next());

Running this, we get,

Converting the input string: 1
1
Converting the input string: 2
2

Chaining multiple converters

We can chain one Converter with another, i.e., take the result of converter-1 and apply it to converter-2 (chaining as many as we want).

The andThen method takes a second converter and returns a converter whose convert() method applies the second converter to the result of this converter. Its reverse() method applies the converters in reverse order. Shown below is the andThen method. Since the current converter is Converter<A, B> and the andThen method takes a Converter<B, C>, it returns a Converter<A, C>.

public abstract class Converter<A, B> implements Function<A, B> {
    public final <C> Converter<A, C> andThen(Converter<B, C> secondConverter) {
      ...
    }
}

Let us use the StringToIntegerConverter we’ve created earlier as the first converter. Remember that it takes a String and returns an Integer. We define a second converter which takes an Integer and returns double the passed value in the forward direction and divides the passed Integer by two in the reverse direction.

Converter<String, Integer> stringToIntegerConverter = new StringToIntegerConverter();
Converter<Integer, Integer> scaleOrDescaleByTwoConverter = Converter.from(
        i -> i * 2,
        i -> i / 2
);

We can chain these together using the andThen() method and get back a Converter<String, Integer> as shown below.

Converter<String, Integer> converter = stringToIntegerConverter
                .andThen(scaleOrDescaleByTwoConverter);

It works as follows:

  • If we pass the String “100”, the first converter (stringToIntegerConverter), converts the String “100” to Integer 100.
  • The second converter (scaleOrDescaleByTwoConverter), multiplies the integer 100 to give us 200 as the result.

Similarly, the input string “101” results in integer 202.

Integer intResult = converter.convert("100");//200
System.out.println(intResult);
System.out.println(converter.convert("101")); //202

Let us use these chained converters in reverse. We can get a reversed converter of the chained converter by calling the reverse() method. This gives a Converter to convert an Integer to String (Converter<Integer, String>).

It works in the reverse order as follows (note that the converters will be applied in the reverse order).

  • When we pass as input an integer 200, the scaleOrDescaleByTwoConverter first divides the input by 2 and it results in integer 100.
  • Then the converter stringToIntegerConverter converts the passed Integer to a String, thus giving us the string “100” as the result.

Similarly, for the second call, the input Integer 202 results in String “101” after applying the conversion.

Converter<Integer, String> reverseConverter = converter.reverse();
String stringResult = reverseConverter.convert(200); //100
System.out.println(stringResult);
System.out.println(reverseConverter.convert(202)); //101

Converter equals method

Most implementations of the Converter class need not override the Object#equals method. But some may choose to override the equals method. It will take another Converter instance and return true if both converter instances are equal, i.e., both are considered interchangeable.

In other words, two converter instances are equal (and thus interchangeable) when Objects.equal(this.convert(a), that.convert(a)) is true for all a of type A (and similarly for reverse). Since many Converter classes do not override the equals method, getting back a false from the equals() method doesn’t mean that the converters are not interchangeable.

Example – Converter equals method

Since the StringToIntegerConverter converter class we created doesn’t override the equals() method, creating two instances of StringToIntegerConverter and checking if they are equal will return a false.

Converter<String, Integer> stringToIntegerConverter1 = new StringToIntegerConverter();
Converter<String, Integer> stringToIntegerConverter2 = new StringToIntegerConverter();
System.out.println(stringToIntegerConverter1.equals(stringToIntegerConverter2)); //false

Let us use Enums#stringConverter to get a Converter which converts between string and enum values. It uses Enum#valueOf for conversion in the forward direction and the Enum#name for the reverse conversion.

Let us define a simple Color enum as follows.

private enum Color {
    RED,
    BLACK,
    BLUE,
    GREEN;
}

We will create a stringToEnumConverter and an enumToStringConverter. It works as shown below.

Converter<String, Color> stringToEnumConverter = Enums.stringConverter(Color.class);
Color color = stringToEnumConverter.convert("RED");
System.out.println(color); //RED

Converter<Color, String> enumToStringConverter =  stringToEnumConverter.reverse();
String enumString = enumToStringConverter.convert(Color.RED);
System.out.println(enumString); //RED

Let us create two instances of converters using Enums#stringConverter on the same enum class type. When checking if these two converters are equal, we get true as the result. This is because the converter is capable of checking if it is interchangeable with another converter instance by just checking the enum class they operate on.

Converter<String, Color> stringToEnumConverter1 = Enums.stringConverter(Color.class);
Converter<String, Color> stringToEnumConverter2 = Enums.stringConverter(Color.class);
System.out.println(stringToEnumConverter1.equals(stringToEnumConverter2)); //true

If we create a string to enum converter using some other enum (say SomeOtherColorEnum.class), then comparing our first converter instance with this will show us that they are not equal.

Converter<String, SomeOtherColorEnum> stringToEnumConverter3 = Enums.stringConverter(SomeOtherColorEnum.class);
System.out.println(stringToEnumConverter1.equals(stringToEnumConverter3)); //false

Converter identity method

The Converter.identity() returns a Converter<T, T> i.e., a converter that always converts or reverses an object to itself.

Converter<String, String> identityConverter = Converter.identity();
System.out.println(identityConverter.convert("a")); //a

System.out.println(identityConverter.reverse()
                .convert("b")); //b

Nullability and Invertibility

If we pass a null to the convert method, it is guaranteed to always return null as the result. Thus, a converter always converts null to null and non-null references to non-null references. Thus, the doForward(A) and doBackward(B) methods will never get null as the input.

In the below example, we can see that we get null as the result when passing a null string. If it had called the doForward method with null string, we would have got a NumberFormatException.

Converter<String, Integer> stringToIntegerConverter = new StringToIntegerConverter();
System.out.println(stringToIntegerConverter.convert(null)); //null

Let us look into the Invertibility aspect of a converter. The reverse operation may be lossy. In most cases, converting a value from a to b and again from b to a will give us the original value (as shown below).

System.out.println(Objects.equals(
        "123",
        stringToIntegerConverter.reverse()
                .convert(stringToIntegerConverter.convert("123"))
)); //true

But in some cases, this may not be true as the conversion can be lossy. One example is using the Doubles#stringConverter converter.

  1. stringConverter().convert("1.00") returns the Double value 1.0
  2. stringConverter().reverse().convert(1.0) returns the string "1.0" . It is not the same string ("1.00") we started with

Note that it is still that the round-tripped and original objects are similar.

Converter<String, Double> stringToDoubleConverter = Doubles.stringConverter();
System.out.println(stringToDoubleConverter.convert("1.00")); //1.0
System.out.println(stringToDoubleConverter.reverse().convert(1.0)); //1.0

Conclusion

In this post, we saw in detail about the Google Guava Converter class with examples. I have other posts which cover the other useful utilities in the Google Guava library. Do check it out if interested.

Leave a Reply