Java is not a functional programming language but thanks to Google Guava Library
you can incorporate functional programming with it. In the third part of my tutorial I'll talk about Function which's one of the two classes related to the functional programming. The other is the Predicate class.
If you are not familiar to functional programming, I can briefly say that functions are like factories. They accept objects. They work using these objects and they return new and useful objects.
Usual Function definition in Guava contains two generics. The first one is the object type the function accepts, the second one is the object type it returns. The function below will accept a Person object, find the initial of this name and last name, create a StringBuilder containing these initials and return the StringBuilder object.
01 | Function < Person, StringBuilder > returnNameInitials = |
02 | new Function < Person, StringBuilder > () { |
05 | public StringBuilder apply(Person person) { |
07 | char firstCharOfName = Character.toUpperCase(person.getName().charAt( 0 )); |
08 | char firstCharOfLastName = Character.toUpperCase(person.getLastName().charAt( 0 )); |
10 | return new StringBuilder(firstCharOfName+ |
11 | "." +firstCharOfLastName+ "." ); |
For the sake of completeness, lets give the Person object that'll be used during the examples.
01 | class Person implements Comparable < Person > { |
03 | private String lastName; |
05 | public Person(String name, String lastName){ |
07 | this .lastName = lastName; |
12 | public String getName() { |
15 | public void setName(String name) { |
18 | public String getLastName() { |
21 | public void setLastName(String lastName) { |
22 | this .lastName = lastName; |
26 | public int compareTo(Person arg0) { |
27 | return this .getName().compareTo(arg0.getName()); |
31 | public String toString() { |
32 | return this .name+ " " + this .lastName; |
If you want, you can transform a whole list of Persons (the initial list) to a list of StringBuilders of person name initials (result list) using returnNameInitials function.
04 | List < Person > personList = |
05 | Lists.newArrayList( new Person( "sezin" , "karli" ), |
06 | new Person( "brad" , "delp" )); |
07 | List < StringBuilder > transform3 = |
08 | Lists.transform(personList, returnNameInitials); |
09 | System.out.println( "Name initials: " +transform3); |
Notice that any change in the initial list will result in updates in the obtained list. e.g. if you remove, change, add an element in the initial list you'll see that the corresponding element in the result list will get updated as well. The result list is basically the view of the initial list in hand. Functions work one way (from Person to StringBuilder here), so although it's said (in javadoc) that the updates are bi-directional you can't do much operation in the result (view) list. For instance, you can't add new elements. If I could add "T.P." to the result list, what name and last name will this Person have in the initial list? This information cannot be obtained from the Function we use. You can't change the element content of the result (view) list too. The reason is the same with the previous case. But as you probably guess, removing an element from the result (view) list has no negative impact. Corresponding element in the initial list will be removed as well.
01 | personList.add( new Person( "rick" , "nielsen" )); |
03 | System.out.println(transform3); |
06 | personList.get( 0 ).setName( "tom" ); |
07 | System.out.println(transform3); |
12 | System.out.println(transform3); |
17 | System.out.println(personList); |
21 | transform3.get( 0 ).append( "B." ); |
23 | System.out.println( "transform 3: " +transform3); |
Collections2.transform() is similar to Lists.transform() with the difference that it can work on a larger number of classes. For instance, if you want to transform an HashSet of Person can't do that with Lists' or Sets' methods (though you can do it with Iterable.transform()). All the live view-related stuff is valid for Lists.transform().
1 | Set < Person > personSet = |
2 | Sets.newHashSet( new Person( "sezin" , "karli" ), |
3 | new Person( "brad" , "delp" )); |
4 | Collection < StringBuilder > transform4 = |
5 | Collections2.transform(personSet, returnNameInitials); |
6 | System.out.println(transform4); |
Functions' compose() method allowsus to compose functions. if f: A->B and g: B->C then f o g: A->C. The output of the first function will be the input of the second function.
03 | Function < Double, Double > squareOfADouble = |
04 | new Function < Double, Double > () { |
07 | public Double apply(Double double1) { |
08 | return double1 * double1; |
But I have a list of floats, what should I do in this case? I'd like to change the squareOfADouble function but there are previous code that uses it. What if I write a float to double function and compose both functions? First I'll convert float to double, then I'll apply square of a double function.
01 | Function < Float, Double > floatToDouble = |
02 | new Function < Float, Double > () { |
05 | public Double apply(Float float1) { |
06 | return float1.doubleValue(); |
12 | ArrayList < Float > floatList = |
13 | Lists.newArrayList( 12 .3536343f, 142 .3346435633f, 1 .44564564f); |
18 | Function < Float, Double > floatToDoubleSquare = |
19 | Functions.compose(squareOfADouble, floatToDouble); |
22 | List < Double > squareOfFloatsAsDoubles = |
23 | Lists.transform(floatList, floatToDoubleSquare); |
24 | System.out.println(squareOfFloatsAsDoubles); |
Functions class contains a number of useful methods. One of them is toStringFunction(). This predefined Function takes each object and returns their toString() result.
1 | List < Person > newPersonList = |
2 | Lists.newArrayList( new Person( "sezin" , "karli" ), |
3 | new Person( "bilbo" , "baggins" )); |
4 | List < String > toStringList = |
5 | Lists.transform(newPersonList, Functions.toStringFunction()); |
6 | System.out.println(toStringList); |
There are 2 quite useful methods in Functions for working with Maps.
The first forMap() performs a lookup from the given map and returns the value corresponding to the key in hand.
01 | Map < String, Long > emailToEmployeeNo = Maps.newHashMap(); |
02 | emailToEmployeeNo.put( "luke@starwars.com" , 1684684684L); |
03 | emailToEmployeeNo.put( "darth@starwars.com" , 245468687687L); |
04 | emailToEmployeeNo.put( "obiwan@starwars.com" , 368684674684L); |
06 | Function < String, Long > lookupFunction = |
07 | Functions.forMap(emailToEmployeeNo); |
08 | ArrayList < String > cantinaVisitOrder = |
09 | Lists.newArrayList( "luke@starwars.com" , |
10 | "darth@starwars.com" , "obiwan@starwars.com" , "luke@starwars.com" , "darth@starwars.com" ); |
11 | List < Long > cantinaVisitOrderById = |
12 | Lists.transform(cantinaVisitOrder, lookupFunction); |
13 | System.out.println( "cantina Visit Order By Id: " |
14 | +cantinaVisitOrderById); |
First "luke@starwars.com" is checked from the map and 1684684684L is returned by the function. Beware that you have to be sure that every element in the initial list is available in the function's map as a key else you'll get IllegalArgumentException.
The other forMap() is more useful if you don't want to check the initial list's validity. If the corresponding key is not found during the lookup then the given value is returned by the function.
02 | Function < String, Long > lookupFunctionWithDefault = Functions.forMap(emailToEmployeeNo, defaultId); |
03 | cantinaVisitOrder.add( "greedo@starwars.com" ); |
04 | cantinaVisitOrder.add( "yoda@starwars.com" ); |
10 | System.out.println( "cantina visit order by emails: " |
17 | List < Long > cantinaVisitOrderById2 = |
18 | Lists.transform(cantinaVisitOrder, lookupFunctionWithDefault); |
24 | System.out.println( "cantina Visit Order By Id 2: " |
25 | +cantinaVisitOrderById2); |
Maps class has two interesting methods that work with Functions; uniqueIndex() and transformValues(). The first one's a bit counterintuive but the second one is similar to the transform() methods we saw before.
Maps' uniqueIndex() takes two args: an iterable i and a function f. For each element e in the iterable i, function f is called and the result of f is used as the key to the value e. For the mathematically inclined, a new map of f(e)-e pairs will be built using f() and i.
In the example below, extractNickName extracts nicknames from the names (name in between '') when we call uniqueIndex with this function we will have nicknames as keys and full names as values. The result is an immutable map of nickname=fullname pairs. We will explain immutables at another part of the tutorial.
01 | ArrayList < String > characterList = |
02 | Lists.newArrayList( "Luke 'Blondie' Skywalker" , |
03 | "Darth 'DarkHelmet' Vader" , "Obiwan 'Ben' Kenobi" ); |
05 | Function < String, String > extractNickname = |
06 | new Function < String, String > () { |
09 | public String apply(String name) { |
13 | Iterable < String > tokens = Splitter.on( " " ).trimResults().split(name); |
15 | for (String token : tokens){ |
17 | if (token.startsWith( "'" )) |
18 | return token.substring( 1 , token.length()- 1 ); |
25 | ImmutableMap < String, String > nicknameToNameMap = |
26 | Maps.uniqueIndex(characterList, extractNickname); |
28 | System.out.println( "nicknameToNameMap: " +nicknameToNameMap); |
Lets see the other useful method of Maps; transformValues()
03 | Function < String, String > onlyUsernamePart = |
04 | new Function < String, String > () { |
07 | public String apply(String string) { |
08 | int index = string.indexOf( "@" ); |
09 | return string.substring( 0 , index); |
15 | Map < String, String > nicknameToMailMap = |
17 | nicknameToMailMap.put( "luke skywalker" , "luke@starwars.com" ); |
18 | nicknameToMailMap.put( "darth vader" , "vader@starwars.com" ); |
22 | Map < String, String > viewMap = |
23 | Maps.transformValues(nicknameToMailMap, onlyUsernamePart); |
24 | System.out.println(viewMap); |
27 | viewMap.remove( "darth vader" ); |
28 | System.out.println(nicknameToMailMap); |
While we were explaining Ordering in part 2 of this tutorial, we skipped an important method because we didn't explain Functions. It's time for onResultOf() of Ordering class. Before the ordering the chosen function is applied to the iterable and then the sorting takes places.
I'd like to sort emails based on their domain name. For the purpose I wrote a Function that'll extract the domain part of an e-mail.
01 | Function < String, String > onlyDomainPart = |
02 | new Function < String, String > () { |
05 | public String apply(String string) { |
07 | int index = string.indexOf( "@" ); |
08 | return string.substring(index); |
14 | ArrayList < String > usernames = |
15 | Lists.newArrayList( "luke@lightside.com" , "darth@darkside.com" , "obiwan@lightside.com" , "leia@starwars.com" ); |
19 | List < String > sortedCopy8 = |
20 | Ordering.natural().onResultOf(onlyDomainPart). |
22 | System.out.println(sortedCopy8); |