Skip to content
One year using Java 8 in production

One year using Java 8 in production

We've been using Java8 since the beginning of our startup. Java8 was released on 18 March 2014. We started using Java in September 2014, when AWS made it available on ElasticBeanStalk. At this time, Java 8 was available for almost 6 months. It was mature enough to give it a try.

Frameworks compatible with Java 8 like Spring 4.0 started to be available. Java 8 is a huge step forward compared to Java 7 :

  • Lambdas: concise way to write Function and Predicate, a step toward functional programming,
  • New Apis: Optional, Streams, Dates,
  • Default Methods: define default method behavior in interfaces.

One year later, what did we really use? How did we use it? These are some of the questions we're going to answer.

What about Guava

Guava is a Google Utility API. Before Java 8, we were widely using Guava's features like Predicate, Function, Optional and more. We feel like Java 8 brings almost everything what Guava is providing. We gradually replace Guava features by their Java 8 equivalent.

The question whether to keep Guava or not is a good question. Java 8 picks a great number of features from Guava, and it's not a surprise. Guava is one of the most famous Java API, just behind JUnit.

OctoPerf is JMeter on steroids!
Schedule a Demo

Is Guava still useful? Guava should still be in your classpath because it provides extremely useful APIs like:

  • Immutable collections: we use them almost every time a collection is required. Immutability is vital to maintain your sanity,
  • EventBus: publish / subscribe style communication between components that does not require to know each other,
  • Caches: Argh! We use them rarely because cache invalidation is the second most difficult thing in programming.

Maybe the day will come when all Guava utilities are inside the JDK.

Lambdas

Lambdas are the greatest improvement in Java 8 so far for us. It makes code more concise, while still being understandable. Lambdas have definetely changed the way we are programming: the functional programming paradigm is a growing trend we tend to follow.

Java 7 vs Java 8

// Java 7
final Predicate<String> isEmpty = new Predicate<>() {
  @Override
  public void test(final String input) {
    return Strings.isNullOrEmpty(input);
  }
};

// Java 8
final Predicate<String> isEmpty = Strings::isNullOrEmpty;

Predicate checking whether a string is null or empty.

Java 7 is verbose whereas Java 8 is concise. We're not especially advocates of concise code: we think the Scala language goes too far in this way. Lambdas are a good balance between the unnecessary Java 7 verbosity and Scala's ultimate conciseness.

RxJava and lambdas

RxJava profits from lamdbas too. RxJava was almost unusable with Java 7 due to the verbosity of the syntax. Now our code is just flawless.

@Override
public Observable<T> save(final Iterable<T> entities) {
return Observable.from(entities)
    .map(serializer)
    .flatMap(bucket::upsert)
    .map(deserializer)
    .timeout(timeout, unit);
}

Needless to say we wouldn't have used RxJava with Java 7 due to extra verbosity.

RxJava still lacks comprehensiveness when it comes to debugging. The asynchronous nature of Observable makes it painful to debug when something goes wrong. We use RxJava sparingly, mainly because of the Couchbase Java SDK. Code maintainability profits from better code readability. Although, lambdas are difficult to understand for developers not used to functional programming. We've been developing with functional programming in mind for years, since we discovered Google Guava.

When we use lambdas

We are using Lambda expressions extensively. We try to avoid overusing them: sometimes code with lambdas can be less readable than plain old code. When the execution within a lambda fails, it can show such kind of stacktraces:

at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)

Stacktraces with exceptions thrown by lambda functions are cumbersome.

Lambdas are not well-suited for simple operations. If the plain code using for loops or if conditions looks simpler, resist the temptation to convert it to lambda expressions. Developers are prone to overuse when they find a new golden hammer. Also, we avoid chaining more than 6 operations. The sanity gain provided by the concise code is lost if it's being used to increase the local code complexity. We try to follow the K.I.S.S. principle: Keep It Simple And Stupid.

Optional

We were already extensively using Guava's Optional. Optional is the perfect usage to explicitly indicate when a method does not return an object.

public interface Repository<T> {
  Optional<T> find(String id);
}

A typical database repository interface.

Let's explore the alternatives to Optional to better understand why using it.

Avoiding Null

We're advocate of never have NULL objects. Null is not obvious, Null is not safe. Null requires cumbersome code like this:

if(instance != null) {
  doSomething();
} else {
  // don't know what to do
}

What to do when the object is null? Does it mean the method failed? Is it a special case? With Optional things are becoming clearer. Using and avoiding null explains this very well.

Example

Let's take the following example code.

public interface OutputStrategy {
  void output(String id);
}

The output strategy interface defines the contract.

public class SystemOutputStrategy implements OutputStrategy {
  public void output(String id) {
    System.out.println(id);
  }
}

Output the id in the console.

public interface OutputStrategyService {
  OutputStrategy get();
}

The OutputStrategyService returns an OutputStrategy instance. Its behavior depends on the pattern used below.

Null Pattern

In this case, the OutputStrategy can be NULL. Caller must check the nullity each time the service is called.

// Code using the service with null object pattern
final OutputStrategy s = service.get();
if(s != null) {
  s.output("test"); 
} else {
  // What to do? throw? silently go further?
}

Using Null Pattern.

This is particularly cumbersome and inefficient. It requires a conditional Null check every time the strategy is queried. The developer must read the code of each called service to check whether it may return NULL.

Null Object Pattern

Another alternative to NULL is to use the Null Object Pattern. The following example shows a very basic usage of the Null Object Pattern.

The null object pattern is defined as following.

public class NullOutputStrategy implements OutputStrategy {
  public void output(String id) {
    // Do nothing
  }
}

Noop output.

The latest strategy does nothing. The following code does not require a null check because the service always returns an OutputStrategy instance.

// Code using the service with null object pattern
final OutputStrategy s = service.get();
s.output("test");

Using Null Object Pattern.

The call to the output method does nothing. No extra check is required.

Optional Pattern

The code below shows how it would have been using an Optional.

public interface OutputStrategyService {
  Optional<OutputStrategy> get();
}

// Code using the service with optional pattern
final Optional<OutputStrategy> optional = service.get();
optional.ifPresent(s -> s.output("test"));

Using Optional Pattern.

Optional explicitly tells the developer that the returned instance may not be available. The code doesn't compile if you don't manage the returned Optional properly. Developers save time: they don't need to read the service implementation to check for nullity.

Guava Optional vs Java Optional

There are some differences between Guava's and Java's Optional:

public final class Optional {
    static Optional<T> absent();
    static Optional<T> fromNullable(instance);
    boolean isPresent();
    Set<T> asSet();
}

Guava Optional main methods.

The difference with Java's Optional is negligible, some methods are renamed, and asSet() method is missing

public final class Optional {
    static Optional<T> empty();
    static Optional<T> ofNullable(instance);
    boolean isPresent();
}

Java Optional main methods.

Migrating from Guava's to Java's Optional has been almost flawless. We use Jackson for object to Json serialization and it just worked fine. Although, we experienced issues with some externals APIs depending on Guava. We have some classes cluttered with both Java's and Guava's Optional, but it's only marginally happening.

Lombok

What is Lombok

Lombok is a life saver. It uses annotation processing to generate toString(), hashcode(), constructors. It generates boilerplate code for you. Lombok and Java 8 are working together without any issue. Lombok requires to be installed as Java Agent to work with Eclipse. Intellij has its own Lombok Plugin, but it doesn't support all the annotations.

@Value
@Builder
public class Person {
  @NonNull
  String firstname;
  String lastname;
  int age;
}

Generates an immutable Person which can be instanciated using a Builder.

Less code

I had used Lombok before on a small project (45K lines of code). Lombok allowed us to remove about 7K lines of code (15% of the code). Not only does it save lines of code to maintain, it also prevents bugs. We were previously generating equals and hashcode using Eclipse built-in feature. It was generating code that looks like:

@Override
public boolean equals(Object obj)
{
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    CompanyRole other = (CompanyRole) obj;
    if (name == null)
    {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}

Ugly Eclipse generated equals method.

What happens when you add a new field to the object? Nothing! You have to manually remove and regenerate the equals and hashcode methods. With Lombok, this is automatic. You see now how bad it can get with manually generated code!

Lombok and Spring

Spring works flawlessly with lombok too. We generate autowired immutable Controller constructors.

@RestController
@RequestMapping("/bench/report")
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@AllArgsConstructor(access = AccessLevel.PACKAGE, onConstructor = @__(@Autowired))
class BenchReportController extends AbstractCrudServiceController<BenchReport> {
  @NonNull
  BenchReportCrudService benchReports;

  ...

}

Immutable Controller with autowired Spring constructor, private final fields.

JUnit testing

Lombok generated equals(), hashcode() and constructors can be easily JUnit tested using EqualsVerifier and NullPointerTester (Guava-testlib).

public class BenchReportTest {

  @Test
  public void shouldPassEqualsVerifier() {
    EqualsVerifier.forClass(BenchReport.class).verify();
  }

  @Test
  public void shouldPassNullPointerTester() {
    new NullPointerTester().testConstructors(BenchReport.class, Visibility.PACKAGE);
  }
}

JUnit testing the equals / hashcode contract, and null check preconditions on constructor.

Beware that unit testing the constructor preconditions using NullPointerTester does get you 100% test coverage on the constructor generated by Lombok. You have to create the constructor manually as shown in the following section.

Jackson Immutable objects

Jackson is a Json serializer / deserializer. Lombok can be paired with Jackson to easily generate immutable beans. You just need to create the constructor manually.

@Value
@Builder
public class Person {
  @NonNull
  String firstname;
  String lastname;

  @JsonCreator
  Person(
    @JsonProperty("firstname") final String firstname,
    @JsonProperty("lastname") final String lastname) {
    this.firstname = checkNotNull(firstname);
    this.lastname = checkNotNull(lastname);
  }
}

Jackson immutable object using Lombok.

Getters, equals and hashcode, builder are generated automatically on the fly.

Dates API

The new Java Dates API becomes very close to Joda-Time. We're still using Joda-time because the new Java Dates lacks some features that Joda have. We use the Java Dates API when Joda's features are not required. Joda dates also serialize better using Jackson than Java 8 Dates. We had some issues with Jackson serialized beans using JDK8 dates.

Streams

You can finally get ride of Guava's FluentIterable, Streams are here! Commons operations like map, filter, or flatMap make collections processing way easier. We use streams almost everywhere now, it saves a good amount of code. Like lambdas, streams should be used wisely to avoid code complexity build up. Straight code may be a better option sometimes.

Parallel Streams

Parallel streams use a static ForkJoinPool which is configured by default to System.availableProcessors(). This can lead to competition between streams executing in different web context and create contention. Read why parallel streams are bad to get a better insight about the issue.

Function and Predicate

When you're used to Guava Function and Predicate, you'll be quickly used to Java ones. Some methods are renamed while the principle stays the same. Predicate and Function are mandatory when using Streams. We used to create reusable package protected implementations for testing purpose. Now, we mostly create those using Lambdas.

Tomcat 8 on BeanStalk

Tomcat 8 running on Java 8 has been successful for us so far. We couldn't migrate to Java 8 until AWS made Tomcat 8 with Java 8 available on BeanStalk.

Conclusion

Java 8 is a major release which brings features making Java development fun again. We feel like it's the best update Oracle (previously Sun) has made to Java in the last 10 years. If you haven't bumped to Java 8 yet, do it quickly. We appreciate the productivity gain everyday.

Want to become a super load tester?
Request a Demo