Reflections on Functional Programming in Java

2020/09/23

Preface

As I’ve gained more work experience and been exposed to more languages, I’ve gradually started to understand the multi-paradigm nature of other languages. After writing Scala for a while and then coming back to Java, I rethought the functional interfaces introduced with Java 8 lambdas. Even though it’s syntactic sugar, you can still see Java’s intention to elevate functions to first-class citizens. Here I’m sharing a summary of my own—starting from a tiny feature implementation—hoping it can be helpful.

Multiple ways to implement the business logic

Let’s assume our business logic is the operation quoted below. We’ll try to implement it in a few different ways.

There is an array containing numbers. We need to perform complex processing on the elements in the array, e.g., first scale it by 2.5x, then convert it to a string and append the character 'fen'

Write the code inside the method

public class Test {
  
  public static void main(String[] args) {
    List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);  
    
    List<String> collect = list.stream().map(num -> (num * 2.5) + "fen").collect(Collectors.toList());
  }
 
}

Above is the approach where we write the code directly inside the loop. We do this a lot—it’s simple and has a small amount of code (for example, this hypothetical business logic fits this style pretty well). But the drawbacks are also very obvious: the code can’t be reused, and if the processing logic is complex, stuffing it in there makes the code feel unstructured.

Personally, I really don’t recommend this approach. Java 8 introduced lambdas with a strong emphasis on functional interfaces and method-reference style programming. If you write your own logic directly inside stream operators, readers often need to spend a long time figuring out what’s going on. But if you define a self-explanatory variable and reference it inside the operator, people can tell what this step does just by looking at the variable name—no need to dig into the details. This keeps the code concise, efficient, and maintainable.

Define the code and assign it to a variable

public class Test {
  
  public static void main(String[] args) {
    List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);  
    
    Function<Integer, String> fun = num -> (num * 2.5) + "fen";
    List<String> collect2 = list.stream().map(fun::apply).collect(Collectors.toList());
  }
  
}

The second approach is interface-based programming in JDK 1.8. By lifting it to a more abstract level, it becomes easy to reuse. This is a good fit when the logic is simple but needs reuse, and it’s mainly nice because it’s straightforward to write.

Define a method

public class Test {
  
  public static void main(String[] args) {
    List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);  
    
    List<String> collect1 = list.stream().map(Test::convert).collect(Collectors.toList());
  }
  
  public static String convert(int num) {
      return (num * 2.5) + "fen";
  }
  
}

The third approach is also very common. If the business logic is complex and there are multiple reuse scenarios, you should define a method and call it—just remember to give it a clear, self-explanatory name. But sometimes reuse is low and people are a bit lazy; using this approach can feel like overkill.

Afterword

As my coding volume increases and I read more books, I’ll definitely have new thoughts and new takeaways each time. I’ll keep updating this article in the future to record some of my own ideas. I’ve always believed that the “ideas” behind a language—no matter how middleware and frameworks change—will never go out of date.

All articles in this blog, unless otherwise stated, are licensed under @Oreoft . Please indicate the source when reprinting!

Table of Contents