After converting projects to JDK 8 I've been opening my eyes to opportunities where I could simplify or better express a block of code by using lambdas and the new collections API's. This post is to share with you some of what I have done and how you might be able to find the same sorts of elegant changes in your code.
Some of these samples may be from other libraries where I have seen how it could be better achieved with JDK 8.
I stated doing these by hand and then slapped myself, NetBeans' hints can do all this for me. With NetBeans generally a hint will appear, you smile and accept it, possibly modify, then another hint might appear so you grin and accept that too; before you know it you've taken 5 or more lines down to 1.
Samples here are all shown in stages of conversion to help with understanding what each step is doing. Sometimes this may mean that the complexity increases which will eventually result in simplifications in subsequent steps.
I've called this The Power of the Arrow in homage to the lambda symbol
->
which looks to me like an arrow.TIP: Many examples here make use of static methods which could reduce code by using a static import for the method, such as
Collectors.groupingBy
. I have chosen to explicitly include the class name so it is clear where the method is coming from.Collection Contains Another Collection Entry
There's many different implementations out there, I had my own and only just realised there was one from spring-core that I could share as a source of the example. Here's the method we will be rewriting: org.springframework.util.CollectionUtils.containsAny(Collection<?>,Collection<?>);
Before
This is a fairly common problem, we need to first perform null checks, then perform a for loop testing if the entry is within another collection.public static boolean containsAny(Collection<?> source, Collection<?> candidates) { if (isEmpty(source) || isEmpty(candidates)) { return false; } for (Object candidate : candidates) { if (source.contains(candidate)) { return true; } } return false; }
If you don't want to download the source for spring-core, the following is the `isEmpty' method:
public static boolean isEmpty(Collection<?> collection) { return (collection == null || collection.isEmpty()); }
Step 1: Remove the For Loop
NetBeans will be hinting "Can use functional operations".Accept the hint, you will now end up with the following reduction:
public static boolean containsAny(Collection<?> source, Collection<?> candidates) { if (isEmpty(source) || isEmpty(candidates)) { return false; } if (candidates.stream() .anyMatch((candidate) -> (source.contains(candidate)))) { return true; } return false; }
Step 2: Eliminate one if
Now if you are on the same line, you will now have the "The if statement is redundant" hint.Accept this hint for the following reduction:
public static boolean containsAny(Collection<?> source, Collection<?> candidates) { if (isEmpty(source) || isEmpty(candidates)) { return false; } return candidates.stream() .anyMatch((candidate) -> source.contains(candidate))); }
Step 3: Eliminate final if
You can now see how simple the method has become, the first if can now be combined with the return statement, this won't show up as a hint but the change is simple.public static boolean containsAny(Collection<?> source, Collection<?> candidates) { return !isEmpty(source) && !isEmpty(candidates) && candidates.stream() .anyMatch(candidate -> source.contains(candidate)); }
Step 4: Pretty it Up
You can now further refine this as the parentheses are not required for the lambda argument, likewise the lambda body is not required to be parenthesised. I also shorten the lambda argument name.public static boolean containsAny(Collection<?> source, Collection<?> candidates) { return !isEmpty(source) && !isEmpty(candidates) && candidates.stream().anyMatch(x -> source.contains(x)); }
Summary
Method simplified greatly by removing two if statements and a for loop, we have reduced the vertical footprint from 9 lines to 2, that's a 78% reduction. This could fit on a single line if you're that inclined. What's really cool about this function is theanyMatch
can be changed to allMatch
or noneMatch
, you may want to handle the `isEmpty' differently though.Extract Key Value From Collection
I often have a need where I want to take a collection of objects, let's say
Widget
s, of which contains an id
property that I wish to return a HashSet
containing each id that can be used for quick O(1) contains(Object)
operations.Before
Let us take a simplified example where we pass a collection ofWidget
objects to a function that returns the resulting HashSet<Widget>
for each id property.public Set<String> getKeySet(Collection<Widget> source) { Set<String> ids = new HashSet<>(); for (Widget w : source) { ids.add(w.getId()); } return ids; }
Step 1: Remove for Loop
Within NetBeans you will have see it has identified thefor
each loop can be converted into a functional stream operation.Accept this to have the function transformed.
public Set<String> getKeySet(Collection<Widget> source) { Set<String> ids = new HashSet<>(); source.stream().forEach((w) -> { ids.add(w.getId()); }); return ids; }
It looks like it's complicated the operation, and yes; it has. But we have a plan to simplify this by not using the
forEach
and instead using a map
and collect
Step 2: Simplify Expression
While it looks busy, we can simplify the expression as the lambda body only contains one line we can remove the braces, further we can remove the parentheses around the lambda parameter too.public Set<String> getKeySet(Collection<Widget> source) { Set<String> ids = new HashSet<>(); source.stream().forEach(w -> ids.add(w.getId())); return ids; }
Step 3: Map to Keys
Note that in the body of theforEach
we are calling the getId
method. What we can actually do is use the map
function to convert the Widget
into a String
for the id property before it gets to the forEach
public Set<String> getKeySet(Collection<Widget> source) { Set<String> ids = new HashSet<>(); source.stream().map(Widget::getId).forEach(id -> ids.add(id)); return ids; }
That
::
thing is a method reference, what it does is say to the function "Hey, here's a lambda expression that I'm going to pass in a Widget
object, you give me the result of calling this method reference on that object". This can be really handy as we can omit the need to specify the lambda arguments which makes this easier to read.Okay, again, it looks busy! But what you might notice now is that we have the stream before it gets to the
forEach
lambda that it's already in the correct format, a collection of String
objects for each id
property, woohoo! We can really simplify this now.Step 4: Reduce Complexity with a Collector
Now the really cool part happens. Since the stream is already in the right format, why not have the stream return the correct type? We do this with a collector by passing a collection type toCollectors.toCollection
.public Set<String> getKeySet(Collection<Widget> source) { return source.stream().map(Widget::getId) .collect(Collectors.toCollection(HashSet::new)); }
Here, everything is controlled by the stream, we don't even have to construct an object!
Summary
We have performed a small simplification on the original source and a vertical reduction from 7 to 4 (43%) which might not seem like much, but the greater benefit here is that we have moved everything into the stream which can now be read as it's written. Take a look at the final example and read it in the way it's written out loud "source stream map to widget id and collect to hash set", this can be extremely handy when trying to read code later.Using Method References with JDBCTemplate
While there's many persistence frameworks available I still use spring'sJdbcTemplate
as our environment has multiple databases within the one application making managing persistence context difficult.Never mind me though, let me show you something cool you can do with method references.
Before
Here we have a situation where you have to implement arowMapper
instance. There's two ways this is normally done, one; the quick way is with an anonymous class within the call to the query function.jdbcTemplate.query("select * from customers", new RowMapper<Customer> () { @Override public Customer mapRow(ResultSet rs, int i) throws SQLException { Customer cust = new Customer(); cust.setId("id"); // ... set other fields. return cust; } });
Usually though we find that you need to reuse the mapper, so you stick it in a dedicated class and create a
DEFAULT
static instance like this:public final class CustomerRowMapper implements RowMapper<Customer> { public static final CustomerRowMapper DEFAULT = new CustomerRowMapper(); @Override public Customer mapRow(ResultSet rs, int i) throws SQLException { Customer cust = new Customer(); cust.setId("id"); // ... set other fields. return cust; } }
And then use it in your query:
jdbcTemplate.query("select * from customers", CustomerRowMapper.DEFAULT);
So the problem is solved. But one thing that would be cool is the ability to group all common domain mappers in one class, and simply have static methods that perform the mapping. Well, now with method references you can do exactly that.
The first thing we do is convert the prior mapper into a simple static class with a static method that handles the mapping, we no longer need to implement the interface, but the method still must adhere to the contract.
public final class SalesRowMapper { public static Customer mapCustomerRow(ResultSet rs, int i) throws SQLException { Customer cust = new Customer(); cust.setId(rs.getString("id")); // ... set other fields. return cust; } }
And then use this as follows:
jdbcTemplate.query("select * from customers", SalesRowMapper::mapCustomerRow);
This might not look like a huge benefit over the dedicated
RowMapper
implementation, however you can now put all your domain mappers in one place, or have row mappers calling each other for base objects sharing similar structures.As Dan Ciborowski has pointed out a builder pattern would also make this even more expressive making the method for mapping customers very clean, hypothetically the
mapCustomer
method might look something like this:public static Customer mapCustomerRow(ResultSet rs, int i) throws SQLException { return Customer.builder() .id(rs.getString("id")) .name(rs.getString("name")) .build(); }
Group Objects Into Map
Often you have a need to group objects by a particular property of that object into a map that contains a list for the map values per entry. This is usually a tedious task that can often contain errors, i.e. forgetting to add a list to the map for a given key if it does not exist and then trying to add to it.Before
One such implementation that groups customers by state might look like the following:public static Map<String, List<Customer>> groupByState(Collection<Customer> custs) { Map<String, List<Customer>> res = new HashMap<>(); for (Customer cust : custs) { if (!res.containsKey(cust.getState())) { res.put(cust.getState(), new ArrayList<Customer>()); } res.get(cust.getState()).add(cust); } return res; }
TIP: with JDK 8 the compiler is now smart enough that the explicit type argument on line 5 is no longer required. JDK 7 required that this be explicitly defined. NetBeans will give you the "Use functional operation" hint, if you accept it it will look something like the following:
public static Map<String, List<Customer>> groupByState(Collection<Customer> custs) { Map<String, List<Customer>> res = new HashMap<>(); custs.stream().map((cust) -> { if (!res.containsKey(cust.getState())) { res.put(cust.getState(), new ArrayList<>()); } return cust; }).forEach((cust) -> { res.get(cust.getState()).add(cust); }); return res; }
This really isn't what we want. To get to where we want to go we really need to think about the
collect
method.Use a Collector to Group By
I'm not going to attempt to show how to refactor this as it's easier to just start from scratch, it's barely any code :)As earlier we can take a look at the
Collectors
methods to see what we could use. We find the method Collectors.groupBy
. Making use of this method really is simple.public static Map<String, List<Customer>> groupByState(Collection<Customer> custs) { return custs.stream() .collect(Collectors.groupingBy(Customer::getState)); }
You can simplify further by statically importing
Collectors.groupingBy
public static Map<String, List<Customer>> groupByState(Collection<Customer> custs) { return custs.stream().collect(groupingBy(Customer::getState)); }
Summary
We have taken a complex 11 line block ant transformed it into a 3 line block giving a 75% reduction which is super simple to read from left to right.Nested Grouping
Building on the prior example, say we wanted to further group our customers into state and city.Before
As we are building on the prior example we use it to represent an even more complicated method that must create doubleMap
structures.public static Map<String, Map<String, List<Customer>>> groupByStateCity(Collection<Customer> customers) { Map<String, Map<String, List<Customer>>> res = new HashMap<>(); for (Customer cust : customers) { if (!res.containsKey(cust.getState())) { res.put(cust.getState(), new HashMap<String, List<Customer>>()); } if (!res.get(cust.getState()).containsKey(cust.getCity())) { res.get(cust.getState()).put(cust.getCity(), new ArrayList<Customer>()); } res.get(cust.getState()).get(cust.getCity()).add(cust); } return res; }
After
The above is horrible and hard to maintain, lots of things can go wrong. We can greatly simplify this by passing anothergroupBy
collector to the collector.Again, I'm not going to attempt to show how to refactor this.
public static Map<String, Map<String, List<Customer>>> groupByStateCity(Collection<Customer> customers) { return customers.stream() .collect(Collectors.groupingBy(Customer::getState, Collectors.groupingBy(Customer::getCity))); }
This is now super compact, it can again be further simplified if you statically import
Collectors.groupingBy
.public static Map<String, Map<String, List<Customer>>> groupByStateCity(Collection<Customer> customers) { return customers.stream() .collect(groupingBy(Customer::getState, groupingBy(Customer::getCity))); }
Conclusion
I've given just 5 examples you can reuse in your own code that I have found crop up. There are many examples all over about lambdas in collections, I highly recommend reading Brian Goetz's article on The State of the Lambda: Libraries Edition.
There is a bit of interest in LINQ from Java developers that I'm thinking of writing java 8 versions of the 101-LINQ Samples, some may not be possible such as those that make use of anonymous types though I think the exercise would be useful.
They can tweak the chances relying on how a lot they have paid out vs. how a lot they've taken in. So, earlier than the "profitable pot" had been exhausted, I returned to the unique slot recreation to attempt again after a day or two. Firstly, I played an online in style slot machine after joining 온라인 카지노 a preferred bingo hall and online firm.
ReplyDelete