Friday, November 14, 2014

Exploring Java 8: Lambda expressions and default interface methods

The introduction of lambda expressions in Java 8 is arguably the biggest change in Java since generics and annotations were added to the language in Java 5. This week, I got a chance to experiment with lambda expressions a little bit when I helped answer a question posted on JavaRanch.

The topic was about filtering a portion of a list. I have changed it up a little here but the idea is essentially the same.

The challenge is to write a method which, given a list of numbers, would remove all odd numbers that occurred between a starting index (inclusive) and an ending index (exclusive). Elements that are outside the given index range should not be affected.

For example, given a list of eleven numbers, {3, 18, 7, 1, 16, 11, 9, 4, 33, 5, 10}, a starting index of 3, and an ending index of 9, the method would remove the odd numbers in the highlighted range: {3, 18, 7, 1, 16, 11, 9, 4, 33, 5, 10}, producing a new list, {3, 18, 7, 16, 4, 5, 10}.

To appreciate how lambda expressions can dramatically improve Java code, let's start with a solution that doesn't use them.

Revision #1 - The "standard" implementation

    List<Integer> removeOdd(List<Integer> aList,
            int fromIndex, int toIndex) {

        List<Integer> newList = new ArrayList<>();

        for (int i = 0; i < fromIndex; i++) {
            newList.add(aList.get(i));
        }

        for (int i = fromIndex; i < toIndex; i++) {
            Integer e = aList.get(i);
            if (e % 2 == 0) {
                newList.add(e);
            }
        }

        for (int i = toIndex; i < aList.size(); i++) {
            newList.add(aList.get(i));
        }

        return newList;
    }


This solution uses three for-loops to process the head, body, and tail of the list, respectively. The head includes elements with an index less than the starting index, while the tail includes elements with an index greater than or equal to the ending index. It works but it's not exactly the most succinct piece of code you've ever seen.

The trouble with this solution is that it doesn't clearly reveal its intent. Anyone coming into this code without prior knowledge of it will have to spend a bit of time reading through it to grok what's going on. We can improve this with a little bit of refactoring.

The bookend for-loops can be eliminated by using the List.subList() method which returns a view of a portion of a list. This helps make the code that copies elements from the head and tail portions a little bit clearer. Now we are left to contend with only one for-loop.


Revision #2 - Refactored to use List.subList()

    List<Integer> removeOdd(List<Integer> aList, 
            int fromIndex, int toIndex) {

        List<Integer> newList = new ArrayList<>();

        newList.addAll(aList.subList(0, fromIndex));

        for (int i = fromIndex; i < toIndex; i++) {
            Integer e = aList.get(i);
            if (e % 2 == 0) {
                newList.add(e);
            }
        }

        newList.addAll(aList.subList(toIndex; aList.size())

        return newList;
    }


The remaining for-loop still is not quite up to snuff with the Single Level of Abstraction Principle or SLAP for short. To make this a well-composed method, we can extract the detailed code into another method. This results in the following, which is relatively clean as far as Java code goes.


Revision #3 - Refactored to SLAP

    List<Integer> removeOdd(List<Integer> aList, 
            int fromIndex, int toIndex) {

        List<Integer> newList = new ArrayList<>();

        newList.addAll(aList.subList(0, fromIndex));
        newList.addAll(evenNumbers(aList, fromIndex, toIndex));
        newList.addAll(aList.subList(toIndex, aList.size())

        return newList;
    }

    List<Integer> evenNumbers(List<Integer> aList, 
            int fromIndex, int toIndex) {

        List<Integer> newList = new ArrayList<>();
        
        for (int i = fromIndex; i < toIndex; i++) {
            Integer e = aList.get(i);
            if (e % 2 == 0) {
                newList.add(e);
            }
        }

        return newList;
    }
   

The removeOdd method is about as close to having a single level of abstraction as we can get but the evenNumbers helper method is still a bit of an eyesore. The business of having to create a new list and return it is a bit repetitious if you ask me. Refactoring evenNumbers a little bit more makes it somewhat better but you may have trouble even noticing the difference from the previous revision.


Revision #4 - Refactored helper method

    List<Integer> removeOdd(List<Integer> aList, 
            int fromIndex, int toIndex) {

        List<Integer> newList = new ArrayList<>();

        newList.addAll(aList.subList(0, fromIndex));
        newList.addAll(evenNumbers(aList.subList(fromIndex, toIndex)));
        newList.addAll(aList.subList(toIndex, aList.size());
    }

    List<Integer> evenNumbers(List<Integer> aList) {

        List<Integer> newList = new ArrayList<>();
        
        for (Integer e : aList) {
            if (e % 2 == 0) {
                newList.add(e);
            }
        }

        return newList;
    }

Now, let's see what lambda expressions can do.


Revision #5 - Using a lambda expression

    List<Integer> removeOdd(List<Integer> aList, 
            int fromIndex, int toIndex) {

        List<Integer> newList = new ArrayList<>();
        newList.addAll(aList);

        newList.subList(fromIndex, toIndex).removeIf(e -> e % 2 != 0);

        return newList;
    }

If you're wondering why I didn't choose to modify the given list in place, it's because I program defensively. I prefer to not do anything that would change the List passed to the method and avoid potentially giving callers an unexpected and unwanted surprise. This approach will also help me transition to a more functional style later.

The lambda expression, with the help of the new List.removeIf() method, replaces the arguably ugly helper method and dramatically simplifies the code. The removeIf method was introduced in Java 8 as a default method of the Collection interface. Default methods are also new in Java 8 and are a boon for developers of libraries who want to unobtrusively extend their existing APIs.

One subtle difference here is that the expression used to select elements changed from (e % 2 == 0) to (e % 2 != 0). This difference is key to getting the correct behavior but it seems like every time I look at it, I have to do a double take and pause to make sure I didn't make a mistake. That's a little annoying.

This annoyance made me feel like I could do a little more to make the code really reveal its intent. To ease my angst, I performed one final refactoring to introduce an explaining variable.


Revision #6 - Introduce a local explaining variable for the lambda

    List<Integer> removeOdd(List<Integer> aList, 
            int fromIndex, int toIndex) {

        final Predicate<Integer> oddNumber = (e -> e % 2 != 0);

        List<Integer> newList = new ArrayList<>();
        newList.addAll(aList);

        newList.subList(fromIndex, toIndex).removeIf(oddNumber);

        return newList;
    }

Looking back at the previous versions again, I noticed that there was a little bit of a shift from the way the problem is stated, "remove odd numbers from the list" versus the name of helper method, evenNumbers. I used that name because it's the only one that makes sense as the argument to the call to addAll.

This little shift goes away with the name oddNumber. It matches the problem statement and it works in both the context of the lambda expression and as an argument to the removeIf method. I'm hard-pressed to think of a way to make that line of code any more expressive. That's pretty cool.

Meanwhile, back in the JavaRanch thread that started this, I got into a bit of a discussion about the merits of introducing a local explaining variable. Some may think that adding that line to create a local variable defeats the purpose of using lambdas but I think that the small sacrifice made to brevity is worth the gain in clarity. I suppose it's just a matter of taste at this point. I like this one more.

In conclusion, this exercise helped me appreciate just how much lambda expressions can help to make for better, cleaner code.  And as an added bonus, I also saw the utility of default interface methods in making it easier to extend existing APIs.

No comments: