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.
Function < Person, StringBuilder > returnNameInitials =
new Function < Person, StringBuilder > () {
@Override
public StringBuilder apply(Person person) {
// Sezin Karli
char firstCharOfName = Character.toUpperCase(person.getName().charAt(0)); // S
char firstCharOfLastName = Character.toUpperCase(person.getLastName().charAt(0)); // K
return new StringBuilder(firstCharOfName+
"."+firstCharOfLastName+"."); // S.K.
}
};
For the sake of completeness, lets give the Person object that'll be used during the examples.
class Person implements Comparable < Person > {
private String name;
private String lastName;
public Person(String name, String lastName){
this.name = name;
this.lastName = lastName;
}
public Person(){
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public int compareTo(Person arg0) {
return this.getName().compareTo(arg0.getName());
}
@Override
public String toString() {
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.
// process each Person in personList using
//returnNameInitials function
// return the result as a view of the initial list.
List < Person > personList =
Lists.newArrayList(new Person("sezin", "karli"),
new Person("brad", "delp"));
List < StringBuilder > transform3 =
Lists.transform(personList, returnNameInitials);
System.out.println("Name initials: "+transform3);
// o: Name initials: [S.K., B.D.]
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.
personList.add(new Person("rick","nielsen"));
//added an element o: [S.K., B.D., R.N.]
System.out.println(transform3);
// R.N. is added so the addition to the initial
// list has an impact on the result list
personList.get(0).setName("tom"); //changed an element content
System.out.println(transform3);
// T.K. becomes M.K. so the update to the initial
// list has an impact on the result list
personList.remove(0);// remove the first element.
//o: [B.D., R.N.]
System.out.println(transform3);
//T.K. is no more so the update to the initial
//list again has an impact on the result list
transform3.remove(0);
System.out.println(personList);
// o: [rick nielsen]. as you can see the
// removal from the result list (view) has an impact
//on the initial list
transform3.get(0).append("B.");
// lets try to change to element content
System.out.println("transform 3: "+transform3);
// o: transform 3: [R.N.]. you cant see the change
//because you cant do updates on the result list
// if you check the initial list
// you'll see no change too
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().
Set < Person > personSet =
Sets.newHashSet(new Person("sezin", "karli"),
new Person("brad", "delp"));
Collection < StringBuilder > transform4 =
Collections2.transform(personSet, returnNameInitials);
System.out.println(transform4); // o: [S.K., B.D.]
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.
//Assume that we used to have a function
//for taking the square of a double.
Function < Double, Double > squareOfADouble =
new Function < Double, Double > () {
@Override
public Double apply(Double double1) {
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.
Function < Float, Double > floatToDouble =
new Function < Float, Double > () {
@Override
public Double apply(Float float1) {
return float1.doubleValue();
}
};
// thanks to autoboxing
// float values will be put to Float wrappers.
ArrayList < Float > floatList =
Lists.newArrayList(12.3536343f, 142.3346435633f, 1.44564564f);
// first the second function then the first
// function must be written.
Function < Float, Double > floatToDoubleSquare =
Functions.compose(squareOfADouble, floatToDouble);
List < Double > squareOfFloatsAsDoubles =
Lists.transform(floatList, floatToDoubleSquare);
System.out.println(squareOfFloatsAsDoubles);
// o: [152.61227005628461, 20259.149887098232,
// 2.089891460912341]
Functions class contains a number of useful methods. One of them is toStringFunction(). This predefined Function takes each object and returns their toString() result.
List < Person > newPersonList =
Lists.newArrayList(new Person("sezin", "karli"),
new Person("bilbo", "baggins"));
List < String > toStringList =
Lists.transform(newPersonList, Functions.toStringFunction());
System.out.println(toStringList);
// o: [sezin karli, bilbo baggins]
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.
Map < String, Long > emailToEmployeeNo = Maps.newHashMap();
emailToEmployeeNo.put("luke@starwars.com", 1684684684L);
emailToEmployeeNo.put("darth@starwars.com", 245468687687L);
emailToEmployeeNo.put("obiwan@starwars.com", 368684674684L);
Function < String, Long > lookupFunction =
Functions.forMap(emailToEmployeeNo);
ArrayList < String > cantinaVisitOrder =
Lists.newArrayList("luke@starwars.com",
"darth@starwars.com", "obiwan@starwars.com", "luke@starwars.com", "darth@starwars.com");
List < Long > cantinaVisitOrderById =
Lists.transform(cantinaVisitOrder, lookupFunction);
System.out.println("cantina Visit Order By Id: "
+cantinaVisitOrderById);
//cantina Visit Order By Id:
//[1684684684, 245468687687, 368684674684,
// 1684684684, 245468687687]
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.
Long defaultId = 0L;
Function < String, Long > lookupFunctionWithDefault = Functions.forMap(emailToEmployeeNo, defaultId);
cantinaVisitOrder.add("greedo@starwars.com");
cantinaVisitOrder.add("yoda@starwars.com");
// greedo and yoda visited the cantina
// but they are not in the employee_email=id
// map
System.out.println("cantina visit order by emails: "
+cantinaVisitOrder);
//cantina visit order by emails:
//[luke@starwars.com, darth@starwars.com, obiwan@starwars.com,
// luke@starwars.com, darth@starwars.com,
// greedo@starwars.com, yoda@starwars.com]
List < Long > cantinaVisitOrderById2 =
Lists.transform(cantinaVisitOrder, lookupFunctionWithDefault);
// in the previous forMap() example we would
//take IllegalArgumentException
//for yoda and greedo. now you'll get the defaultId for them
System.out.println("cantina Visit Order By Id 2: "
+cantinaVisitOrderById2);
// as you can see the last 2 employees have 0 as id
//cantina Visit Order By Id 2: [1684684684,
//245468687687, 368684674684, 1684684684, 245468687687, 0, 0]
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.
ArrayList < String > characterList =
Lists.newArrayList("Luke 'Blondie' Skywalker",
"Darth 'DarkHelmet' Vader", "Obiwan 'Ben' Kenobi");
Function < String, String > extractNickname =
new Function < String, String > () {
@Override
public String apply(String name) {
//Luke 'Blondie' Skywalker
//as there will be no space in our nicknames
//we can apply the split below
Iterable < String > tokens = Splitter.on(" ").trimResults().split(name);
for(String token : tokens){
// Luke, 'Blondie', Skywalker
if(token.startsWith("'")) //'Blondie'
return token.substring(1, token.length()-1);
// will remove ' in both ends
}
return "none";
}
};
ImmutableMap < String, String > nicknameToNameMap =
Maps.uniqueIndex(characterList, extractNickname);
System.out.println("nicknameToNameMap: "+nicknameToNameMap);
//nicknameToNameMap: {Blondie=Luke 'Blondie' Skywalker,
//DarkHelmet=Darth 'DarkHelmet' Vader, Ben=Obiwan 'Ben' Kenobi}
Lets see the other useful method of Maps; transformValues()
//For the example I write a Function that'll
// extract the username part of an e-mail.
Function < String, String > onlyUsernamePart =
new Function < String, String > () {
@Override
public String apply(String string) { //luke@starwars.com
int index = string.indexOf("@");
return string.substring(0, index); //luke
}
};
//lets create a star wars map with nicknames as
//keys and emails as values
Map < String, String > nicknameToMailMap =
Maps.newHashMap();
nicknameToMailMap.put("luke skywalker", "luke@starwars.com");
nicknameToMailMap.put("darth vader", "vader@starwars.com");
//Function will process the values
Map < String, String > viewMap =
Maps.transformValues(nicknameToMailMap, onlyUsernamePart);
System.out.println(viewMap);
//After the transformation only username part of the emails is left
//{darth vader=vader, luke skywalker=luke}
viewMap.remove("darth vader");
System.out.println(nicknameToMailMap);
// removal on the initial map has an impact on the view as before
//{luke skywalker=luke@starwars.com}
/*
* As Lists.transform() the Maps.transformValues()
* return us a view which's backed by the initial map.
* All view-related stuff is similar with List' transform() method.
* */
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.
Function < String, String > onlyDomainPart =
new Function < String, String > () {
@Override
public String apply(String string) {
//luke@lightside.com
int index = string.indexOf("@");
return string.substring(index);
// lightside.com
}
};
ArrayList < String > usernames =
Lists.newArrayList("luke@lightside.com", "darth@darkside.com", "obiwan@lightside.com", "leia@starwars.com");
// onResultOf() will apply the given Function first
//and do the sort as it's desired.
List < String > sortedCopy8 =
Ordering.natural().onResultOf(onlyDomainPart).
sortedCopy(usernames);
System.out.println(sortedCopy8);
//[darth@darkside.com, luke@lightside.com,
// obiwan@lightside.com, leia@starwars.com]
/*As you can see the usernames list is
ordered based on the domain name
* */
Really enjoying these posts, keep up the good work.
ReplyDeleteSystem.out + comments is so oldschool.
ReplyDeleteWith an assert(junit, festasert, etc) , your code will be crystal clear.
Thank you very much, great posts.
ReplyDeleteGood job.
ReplyDelete