Are you Java 8 programmer or Java 8 coder?

Piyush N
8 min readFeb 15, 2020

--

This article is to understand the power of Java 8 and how a programmer can utilise this power to build effective code. When people say that we have migrated to Java 8, the question that arises is “ whether a programmer is using JDK8 to compile the project or using JDK8 features in it?” In other words, one should focus on effective usages of JDK8 features rather than to abuse the strength of Java 8.

Prerequisites:
Following prerequisites are required for understanding this blog:
1. Basics of Java 8 features [Stream, Optional, Interface enhancement in Java 8, Functional Interface] are must.
2. Require basic understanding between a programmer and a coder. Want to have quick walkthrough about it? see here

The following practices convey the effectiveness of Java 8:

  • Stream is more than just forEach() function
  • Optional.map() and Optional.orElse() is not same as if and else
  • Java 8 Interface with default method is not a substitute for abstract class
  • Keep method return type as Stream whenever make sense
  • Change functional interface to non-functional interface carefully

1] Stream is more than just forEach() function:

Java 8 Stream is not just for using forEach function but it’s more about organizing your logic with different stream functions. Let’s take a problem statement to understand this and try different approaches to solve it.

Problem Statement: Get data from DB and convert it to UI model.

Following steps will be involved to write the logic for this:
a) For the given ids fetch data from DB
b) Process further if data is not null.
c) Change one of the fields
d) Convert DB model to the UI model
e) Add UI model to list
f) return UI model list

Wrong-Way: This is how the coder will code the given problem statement:

List<UiModel> uiModels = new ArrayList<>;
ids.stream()
.forEach(id -> {
List<Data> datas = getDataById(id); //step a)
if (checkNotNullNotEmpty(datas)) { //step b)
datas.forEach(data -> {
data.setMark(0); //step c)
});
List<UiModel> uiModel =
convertDbModelToUiModel(datas); //step d)
uiModels.addAll(uiModels); //step e)
}
});
return uiModels;

Going with the above approach has a potential drawback of redundant usage of the stream. Because collection itself provides forEach function.

Right Way: This is how a programmer will code the given problem statement:

return ids.stream()
.map(id -> getDataById(id)) //step a)
.filter(datas -> checkNotNullNotEmpty(datas)) //step b)
.flatMap(datas -> datas.stream()) //flatten
.peek(data -> data.setMark(0)) //step c)
.map(data -> convertDbModelToUiModel(data)) //step d)
.collect(Collectors.toList()); //step e)
//Return it.

Key takeaways from this:
1) If you have to only use “Stream.forEach()” function of Stream, don’t use stream. The “Iterable interface” already provides “forEach” method, we can directly utilise it.
2) “Stream” is a template to organise your code.

2] Optional.map() and Optional.orElse() is not same as if and else:

As we all know that the content of the “else block” only executes if the condition in the “if statement” returns false. Conversely, the argument of “Optional.orElse” always gets executed irrespective of the value of the object in optional (null, empty, or with value). Always consider the above-mentioned point in mind while using “Optional.orElse”, otherwise use of “Optional.orElse” can be very risky in the following situation.
a. If content inside orElse contains any log statement:
In this case, you will end up logging it every time.

Optional.of(getModel())
.map(x -> {
})
.orElse(getDefaultAndLogError());
getDefaultAndLogError() {
log.error("No Data found, Returning default");
return defaultValue;
}

b. If content inside orElse is time-intensive:
Time-intensive content can be any i/o operations DB call, API call, or file reading. If we put such content in orElse(), the system will end up executing a code of no use.

Optional.of(getModel())
.map(x -> //some logic)
.orElse(getDefaultFromDb());
getDefaultFromDb() {
return dataBaseServe.getDefaultValue(); //api call, db call.
}

c. If content inside orElse is mutating some object state:
We might be using the same object at another place let say inside Optional.map function and it can put us in a critical bug.

List<Model> list = new ArrayList<>();Optional.of(getModel())
.map(x -> {
})
.orElse(get(list));
get(List < String > list) {
log.error("No Data found, Returning default");
list.add(defaultValue);
return defaultValue;
}

Prefer using orElse when the default value is some constant object, enum. In all above cases we can go with Optional.orElseGet() (which only executes when Optional contains non empty value)instead of Optional.orElse(). Why?? In orElse, we pass the default result value, but in orElseGet we pass Supplier, and the method of Supplier only executes if the value in Optional is null.

// execute and pass the 'other' value to 'orElse' method.
public T orElse(T other) {
return value != null ? value : other;
}

// Pass Supplier to 'orElseGet' method and
// supplier will execute its 'get' method only if 'value' in
// optional is null.

public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

Key takeaways from this:
1) Do not use “Optional.orElse” if it contains any log statement.
2) Do not use “Optional.orElse” if it contains time-intensive logic.
3) Do not use “Optional.orElse” if it is mutating some object state.
4) Use “Optional.orElse” if we have to return a constant, enum.
5) Prefer “Optional.orElseGet” in the situations mentioned in 1,2 and 3rd points.

3] Java 8 Interface with default method is not a substitute for abstract class:

The difference between an interface and an abstract class can be seen in two ways, technically and logically.[ I am dividing the difference into two parts] This is because sometimes it is technically possible to do something; however, it is not right to do it logically. For instance, it is always possible to set a value in an instance variable directly (object.x=3) but it is recommended to use a constructor, setter method, or builder pattern for this.

Technical difference:
1) An interface can not have a constructor, but an Abstract class can.
2) An interface can not have instance variables, but the Abstract class can have.

Although the technical difference is sufficient to prove that the interface and abstract class are not the same, why is it necessary to discuss the logical difference? 😆

Logical Difference:
Let’s start with the initial version of Java. The abstract class was intended to keep both abstract and not abstract instance methods whereas Interface was never intended to keep the non-abstract method and this is perfectly fine because it doesn’t seem good to have to implement the method inside the interface logically. Unfortunately, all this was in place before releasing Java 8.

Java 8 introduced the concept of the default method because of the following reason.

The default method is introduced to maintain the backward compatibility. So there was trade of between design and backward compatibility and Java developer chose backward compatibility. It make sense as

Java is always known for its backward compatibility and introducing new abstract method in interface would create compatibility issues all over the world. You can read it on documentation provided by Oracle and many blogs available on internet.

Since the reason behind the creation of the default method is clear, should we consider the default method as a feature of Java 8? I have seen many times that people started putting common code in the default method. By doing this we are really violating the concept of interface.

Key takeaways from this:

  1. The default method is not a feature of Java 8, it is a compromise on oops design to backward compatibility.
  2. Abstract class is not the same as interface technically as well as logically.

4] Keep method return type as Stream whenever make sense

I observed the following things very frequently.

People return a collection from method and again created stream out of it. Why!!😮 Though it make sense to do this if we want to consume stream data twice because same stream can not be consume twice.

Let’s understand this with an example, we created a stream inside getCollection() function and then collected it to list. We again created the stream on the result of the getCollection() function.
Wrong-Way:

getAndSave() {
getCollection().stream()
.save(x -> saveToDb());
}
List<T> getCollection() {
return s.getFromApi()
.stream()
.flatMap(// some logic)
.map(// some logic)
.collect(Collectors.toList());
}

Right Way:

getAndSave() {
getStream().save(x -> saveToDb());
}
Stream<T> getStream() {
return apiService.getFromApi()
.stream()
.flatMap(// some logic)
.map(// some logic);
}

This small change will make code shorter, cleaner, memory efficient, and time-efficient.

5] Change functional interface to non-functional interface carefully

Let’s take some coding scenarios to understand this point.

Scenario:

  1. We have two repos “Repo-A” and “Repo-B”.
  2. “Repo-B” is using the dependency of “Repo-A”.
  3. “Repo-A” contains a functional interface “A” that will be used as a parameter of the method “clientMethod” in class “C” of “Repo-B”.
  4. While calling “clientMethod” in class “C” of “Repo-B” we are passing lambda function as a function parameter.

Repo-A

public interface A extends B {
void aMethod(int o);
}

Repo-B

public class C {
public static void main(String[] args) {
clientMethod((o) -> {//do something});
}
public static void clientMethod(A a) {
}
}

Now someone changed the design in “Repo-A”.

Changes in the above Scenario:

1. Created a new interface “B” in “Repo-A” with one method “bMethod”.
2. As per the new design, interface “A” in “Repo-A” will be inheriting interface “B” in “Repo-A”.

Repo-A

public interface A extends B {
void aMethod(int i);
}
public interface B {
void bMethod(int i);
}

Repo-B [No change in Repo 2]

public class C {
public static void main(String[] args) {
clientMethod((c) -> {});
}
public static void clientMethod(A a) {
}
}

Problem After Changes: When we will compile the “Repo-A” it will compile fine without any error. But when you try to compile the “Repo-B” code with a new version of “Repo-A”, it will fail during compilation with the following error.

Error: Multiple non-overriding abstract methods found in interface “A”.

Why!!! Because as per the new design interface “A” is no more a functional interface. It has now two abstract methods one is a direct method and one is inherited from parent interface “B”. The following two cases will help you to understand the concept of functional interface.

a) Case-1

public interface A extends B {
void aMethod(int i);
}
public interface B {
void bMethod(int i);
}

Following is true about the above snippet:
1) B is a functional interface: Because directly or indirectly it has only one abstract method.

2) A is not a functional interface: Because It has two abstract methods, one is direct and another one is inherited from the parent interface.

b) Case-2

public interface A extends B {
}
public interface B {
void bMethod(int i);
}

Following is true about the above snippet:
1) B is a functional interface: Because directly or indirectly it has only one abstract method.

2) A is a functional interface: Because It has only one abstract method inherited from the parent interface.

Key takeaways from this:

  1. For an interface to be a functional interface count of direct and inherited abstract methods should be exactly one.
  2. Decide whether you really want to define an interface as a functional interface or is it just a coincidence that it has only one abstract method for the time being. If the intention is to define a functional interface then better to use @FunctionalInterface annotation while declaring an interface.
  3. Have a plan and process to notify your consumers/clients if you plan on making any breaking changes.

--

--