Convert Java 7 Code to Java 8 Code Online in 2 Minutes


I have provided the tool where you can convert your code from Java 7 to Java 8 easily in just a matter of 2 minutes. Just by pressing the button convert you will be able to convert your JAVA 7 code to JAVA 8 for free and you don’t have to wait for a lot of time.

Below is a sample that I have taken from my laptop’s IntelliJ IDE just for your reference.

From Imperative to Declarative and Functional Programming

When I first transitioned to Java 8, I realized the significant shift from imperative programming, where I had to specify both what to do and how to do it, to declarative and functional programming, where I could focus on what to do and let the libraries handle the how. This transformation made my code cleaner and more concise.

Imperative programming is like writing a detailed recipe where every step is explicitly mentioned. It can become verbose and harder to maintain. On the other hand, declarative programming allows me to express what I want to achieve, leaving the “how” to underlying functions or libraries.

Key Concepts in Java 8

Higher-Order Functions

Higher-order functions were a game-changer for me. They are functions that can take other functions as arguments or return functions, providing a powerful way to create flexible and reusable code components.

For example, I wrote a function to process a list of numbers:

public void processNumbers(List<Integer> numbers, Function<Integer, Integer> processor) {
    for (Integer number : numbers) {
        System.out.println(processor.apply(number));
    }
}

I could use this function with different processors:

processNumbers(Arrays.asList(1, 2, 3, 4), x -> x * 2);  
processNumbers(Arrays.asList(1, 2, 3, 4), x -> x + 1);  

Immutability

Immutability became a crucial concept for me as it avoids side effects and makes concurrent programming easier. Java 8 encourages immutability through functional constructs.

Here’s an example where I avoided modifying the state:

public class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public ImmutablePerson withName(String newName) {
        return new ImmutablePerson(newName, this.age);
    }
}

Pure Functions

Pure functions do not have side effects; they only depend on their inputs and produce the same output for the same inputs. This predictability made my code easier to test and reason about.

Example of a pure function I used:

public int add(int a, int b) {
    return a + b;
}

Examples of Refactoring

Anonymous Inner Classes to Lambdas

In my old Java code, I often used anonymous inner classes. With Java 8, I replaced them with lambda expressions, making the code shorter and more readable.

Here’s an example:

ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
    final int index = i;
    service.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println("Task: " + index);
        }
    });
}
service.shutdown();

In Java 8, I simplified it with a lambda expression:

ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
    final int index = i;
    service.submit(() -> System.out.println("Task: " + index));
}
service.shutdown();

Using Streams for Collection Operations

The introduction of the Stream API was a revelation for me. It allowed me to perform operations like filtering, mapping, and reducing collections in a declarative style.

Consider when I needed to process a list of names:

List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
for (String name : names) {
    if (name.startsWith("J")) {
        System.out.println(name.toUpperCase());
    }
}

Using Streams, I simplified it:

List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
names.stream()
     .filter(name -> name.startsWith("J"))
     .map(String::toUpperCase)
     .forEach(System.out::println);

Avoiding Nulls with Optionals

Handling null values was always tricky. Java 8’s Optional class helped me handle the presence or absence of values more explicitly and safely.

Instead of returning null, I returned an Optional:

public Optional<String> getName() {
    return Optional.ofNullable(name);
}

I could then handle the value safely:

public Optional<String> getName() {
    return Optional.ofNullable(name);
}

Method References

Method references provided a shorthand for lambda expressions that call a method. They made my code more concise and readable.

For example, consider this lambda expression:

Optional<String> name = getName();
name.ifPresent(System.out::println);

I replaced it with a method reference:

List names = Arrays.asList("John", "Jane", "Jack", "Doe");
names.forEach(name -> System.out.println(name));

Enhanced Comparators

Java 8 simplified sorting with enhancements to the Comparator interface. I used lambda expressions and method references to create comparators easily.

Here’s an example of sorting a list of people by name and then by age:

List<Person> people = Arrays.asList(new Person("John", 30), new Person("Jane", 25));
people.sort(Comparator.comparing(Person::getName).thenComparing(Person::getAge));

This code sorted the list by name, and if the names were the same, by age.

The Execute Around Pattern

This pattern ensured that I managed resources efficiently, such as closing files or database connections. The try-with-resources statement in Java 8 made this even easier.

Here’s how I read a file:

try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    reader.lines().forEach(System.out::println);
}

The try-with-resources statement automatically closed the resource after use, reducing the risk of resource leaks.

Deep Dive into Streams

Streams in Java 8 became a powerful tool for me when working with collections of data. They allowed me to perform complex data processing tasks concisely and readably.

Creating Streams

I created streams from various data sources such as collections, arrays, or I/O channels.

// From a collection
Stream<String> streamOfCollection = list.stream();

// From an array
Stream<String> streamOfArray = Stream.of("a", "b", "c");

// From values
Stream<String> streamOfValues = Stream.of("a", "b", "c");

// Infinite stream
Stream<Double> randomNumbers = Stream.generate(Math::random);

Intermediate Operations

Intermediate operations return a new stream and are lazy, meaning they are not executed until a terminal operation is invoked. Common intermediate operations include:

  • filter(Predicate<T>): Filters elements based on a condition.
  • map(Function<T, R>): Transforms each element.
  • flatMap(Function<T, Stream<R>>): Flattens nested structures.
  • distinct(): Removes duplicate elements.
  • sorted(): Sorts elements.

Example of using intermediate operations:

List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
names.stream()
     .filter(name -> name.startsWith("J"))
     .map(String::toUpperCase)
     .distinct()
     .sorted()
     .forEach(System.out::println);

Terminal Operations

Terminal operations produce a result or a side effect. They trigger the execution of intermediate operations. Common terminal operations include:

  • forEach(Consumer<T>): Performs an action for each element.
  • collect(Collector<T, A, R>): Collects the elements into a collection.
  • reduce(BinaryOperator<T>): Reduces the elements to a single value.
  • count(): Returns the count of elements.
  • anyMatch(Predicate<T>): Checks if any element matches a condition.

Example of using terminal operations:

List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
long count = names.stream()
                  .filter(name -> name.startsWith("J"))
                  .count();
System.out.println("Count: " + count);

Parallel Streams

I leveraged parallel streams to utilize multi-core processors, significantly improving performance for large data sets.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream()
                 .filter(n -> n % 2 == 0)
                 .mapToInt(Integer::intValue)
                 .sum();
System.out.println("Sum of even numbers: " + sum);

Optional: A Deeper Dive

The Optional class was a lifesaver for handling the presence or absence of a value explicitly and type-safely, avoiding the pitfalls of null references.

Creating Optionals

I found several ways to create an Optional:

// Creating an empty Optional
Optional<String> empty = Optional.empty();

// Creating an Optional with a non-null value
Optional<String> name = Optional.of("John");

// Creating an Optional that may be null
Optional<String> maybeName = Optional.ofNullable(getName());

Working with Optionals

Once I had an Optional, I used various methods to work with the contained value.

Checking Presence

if (name.isPresent()) {
    System.out.println(name.get());
}

Performing Actions

name.ifPresent(System.out::println);

Default Values and Actions

String defaultName = maybeName.orElse("Default Name");
String computedName = maybeName.orElseGet(() -> "Computed Name");
maybeName.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("No name provided")
);

Throwing Exceptions

String nameOrException = maybeName.orElseThrow(() -> new IllegalArgumentException("No name provided"));

Transforming Optionals

Optional<String> upperName = name.map(String::toUpperCase);
Optional<Integer> nameLength = name.map(String::length);
Optional<String> filteredName = name.filter(n -> n.startsWith("J"));

Combining Optionals

Combining multiple Optional instances using flatMap was another powerful feature:

Optional<String> firstName = Optional.of("John");
Optional<String> lastName = Optional.of("Doe");

Optional<String> fullName = firstName.flatMap(f -> lastName.map(l -> f + " " + l));
fullName.ifPresent(System.out::println); // Outputs "John Doe"

Method References: Concise and Readable Code

Method references provided a shorthand for calling methods, making my code more concise and readable. They were particularly useful when I was just passing an existing method as a lambda expression.

Types of Method References

  1. Static Method Reference
    Function<String, Integer> parseInt = Integer::parseInt;
  2. Instance Method Reference of a Particular Object
    String str = "Hello"; Supplier<String> stringSupplier = str::toUpperCase;
  3. Instance Method Reference of an Arbitrary Object of a Particular Type
    Function<String, Integer> stringLength = String::length;
  4. Constructor Reference
    Supplier<List<String>> listSupplier = ArrayList::new;

These references simplified my lambda expressions and made the code more readable and concise.

Enhanced Comparators for Simplified Sorting

Sorting is a common operation, and Java 8 introduced enhancements to the Comparator interface to simplify sorting operations.

Using Lambdas and Method References

I created comparators using lambda expressions and method references, making the code more readable and maintainable.

List<Person> people = Arrays.asList(new Person("John", 30), new Person("Jane", 25));
people.sort((p1, p2) -> p1.getName().compareTo(p2.getName()));

With method references:

people.sort(Comparator.comparing(Person::getName));

Chaining Comparators

Java 8 allowed me to chain comparators using thenComparing for multi-level sorting.

people.sort(Comparator.comparing(Person::getName).thenComparing(Person::getAge));

This code sorted by name, and if the names were the same, by age.

Reversing Comparators

I reversed the order of a comparator using reversed.

people.sort(Comparator.comparing(Person::getName).reversed());

Custom Comparators

Creating custom comparators using static methods and default methods was straightforward.

Comparator<Person> byName = Comparator.comparing(Person::getName);
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Comparator<Person> byNameThenAge = byName.thenComparing(byAge);

These enhancements made sorting more powerful and easier to implement.

The Execute Around Pattern: Managing Resources Efficiently

The execute around pattern ensured that I managed resources efficiently and cleaned up properly. This pattern was particularly useful for managing resources like files or database connections.

Traditional Resource Management

In traditional Java, resource management involved a lot of boilerplate code to ensure resources were closed properly.

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("file.txt"));
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Using Try-With-Resources

Java 8 simplified this with the try-with-resources statement.

try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    reader.lines().forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}

Custom Resource Management

I applied the execute around pattern to custom resources by implementing the AutoCloseable interface.

public class MyResource implements AutoCloseable {
    public void doSomething() {
        System.out.println("Doing something with the resource");
    }

    @Override
    public void close() {
        System.out.println("Closing the resource");
    }
}

try (MyResource resource = new MyResource()) {
    resource.doSomething();
}

This ensured that close was called automatically when the try block was exited, even if an exception was thrown.

Lambda Expressions: The Heart of Java 8

Lambda expressions are one of the most significant features introduced in Java 8. They enabled functional programming by providing a clear and concise way to represent instances of single-method interfaces (functional interfaces).

Syntax of Lambda Expressions

A lambda expression consists of three parts: a parameter list, an arrow token, and a body. Here’s the basic syntax:

(parameters) -> expression
(parameters) -> { statements; }

Examples

  1. No Parameters
    Runnable r = () -> System.out.println("Hello, world!");
  2. One Parameter
    Consumer<String> greeter = (name) -> System.out.println("Hello, " + name);
  3. Multiple Parameters
    BinaryOperator<Integer> add = (a, b) -> a + b;

Using Lambdas with Collections

Lambdas were particularly powerful when used with Java collections.

Filtering a List

List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
List<String> filteredNames = names.stream()
                                  .filter(name -> name.startsWith("J"))
                                  .collect(Collectors.toList());

Mapping a List

List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase)
                                   .collect(Collectors.toList());

Reducing a List

int sum = Arrays.asList(1, 2, 3, 4, 5).stream()
                                       .reduce(0, (a, b) -> a + b);

Functional Interfaces

A functional interface is an interface with a single abstract method. They are the foundation of lambda expressions in Java. The @FunctionalInterface annotation can be used to mark an interface as a functional interface.

Examples of Functional Interfaces

  1. Runnable
    @FunctionalInterface public interface Runnable { void run(); }
  2. Comparator
    @FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); }
  3. Custom Functional Interface
    @FunctionalInterface public interface MyFunction { int apply(int a, int b); }

Method References with Functional Interfaces

Method references could be used in place of lambda expressions when the lambda expression was simply calling an existing method.

Static Method Reference

Function<String, Integer> parseInt = Integer::parseInt;

Instance Method Reference

Consumer<String> printer = System.out::println;

Constructor Reference

Supplier<List<String>> listSupplier = ArrayList::new;

Practical Use Cases of Lambdas

  1. Event Handling
Button button = new Button("Click Me");
button.setOnAction(event -> System.out.println("Button clicked!"));
  1. Threading
new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        System.out.println("Hello " + i);
    }
}).start();
  1. Collections API Enhancements

Java 8 enhanced the Collections API with methods that accept lambdas. For instance, the forEach method in the Iterable interface:

List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
names.forEach(name -> System.out.println(name));
  1. Custom Functional Interfaces
@FunctionalInterface
public interface Converter<F, T> {
    T convert(F from);
}

Converter<String, Integer> stringToInteger = Integer::valueOf;
Integer converted = stringToInteger.convert("123");

JPA Repository Java 8 Style

Transitioning my JPA repositories to use Java 8 features was another significant improvement. The new capabilities made my repository interfaces cleaner and more expressive.

Default Methods in Interfaces

I could now define default methods in my repository interfaces. This was particularly useful for custom queries.

Here’s an example of a JPA repository using Java 8:

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByLastName(String lastName);

    default Optional<User> findByEmail(String email) {
        return findAll().stream()
                        .filter(user -> user.getEmail().equals(email))
                        .findFirst();
    }
}

This allowed me to add custom logic without having to create a separate implementation class.

Stream Support

The ability to return streams directly from repository methods enhanced the flexibility and performance of my data access layer:

public interface UserRepository extends JpaRepository<User, Long> {
    Stream<User> findAllByLastName(String lastName);
}

This way, I could handle large datasets more efficiently, leveraging the power of streams for further processing.

CompletableFuture with Exception Handling

Java 8 introduced CompletableFuture, a powerful tool for asynchronous programming. One of the challenges I faced was handling exceptions in these asynchronous tasks.

Basic Usage

Creating a CompletableFuture was straightforward:

CompletableFuture.supplyAsync(() -> {
    // Simulating a long-running task
    if (new Random().nextBoolean()) {
        throw new RuntimeException("Something went wrong!");
    }
    return "Task completed successfully!";
}).thenAccept(result -> System.out.println(result))
  .exceptionally(ex -> {
      System.err.println("Task failed: " + ex.getMessage());
      return null;
  });

Combining Multiple Futures

I often needed to combine multiple asynchronous tasks. CompletableFuture provided methods like thenCombine and allOf to handle such scenarios:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

future1.thenCombine(future2, (f1, f2) -> f1 + " " + f2)
       .thenAccept(result -> System.out.println("Combined result: " + result))
       .exceptionally(ex -> {
           System.err.println("Combination failed: " + ex.getMessage());
           return null;
       });

Future

Before CompletableFuture, I often used Future for asynchronous tasks. While it was useful, it lacked the flexibility and power of CompletableFuture.

Creating and Using Futures

Using Future involved submitting tasks to an ExecutorService and then calling get to retrieve the result:

ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(() -> {
    Thread.sleep(2000); // Simulate long-running task
    return "Task result";
});

try {
    String result = future.get();
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    executor.shutdown();
}

Limitations of Future

While Future allowed me to perform asynchronous tasks, it had several limitations:

  • Blocking: Calling get was a blocking operation.
  • No support for chaining: I couldn’t easily chain tasks or handle exceptions without additional code.
  • No built-in support for combining multiple futures.

Runnable and Callable with Java 8 Enhancements

One of the first things I tackled with Java 8 was updating how I handled concurrent tasks. In earlier versions of Java, I frequently used Runnable and Callable for multi-threading, but Java 8’s enhancements made this process much smoother and more efficient.

Simplifying Runnable with Lambdas

Before Java 8, creating a new thread with a Runnable was somewhat verbose. Here’s an example from my old code:

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("Task executed");
    }
});
executor.shutdown();

Switching to Java 8, I replaced the anonymous inner class with a lambda expression, simplifying the code significantly:

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> System.out.println("Task executed"));
executor.shutdown();

This not only made the code more readable but also reduced boilerplate.

Enhancing Callable with Java 8

For tasks that needed to return a result or throw an exception, I used Callable. Before Java 8, my code looked like this:

ExecutorService executor = Executors.newFixedThreadPool(10);
Future<Integer> future = executor.submit(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return 123;
    }
});

try {
    Integer result = future.get();
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
executor.shutdown();

With Java 8, I could use lambda expressions to make this code more concise:

ExecutorService executor = Executors.newFixedThreadPool(10);
Future<Integer> future = executor.submit(() -> 123);

try {
    Integer result = future.get();
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
executor.shutdown();

Handling Exceptions in Callable

One of the challenges I faced was properly handling exceptions in Callable. Java 8 didn’t change the fundamental way Callable works, but combining it with CompletableFuture allowed me to handle exceptions more elegantly.

CompletableFuture.supplyAsync(() -> {
    if (new Random().nextBoolean()) {
        throw new RuntimeException("Something went wrong!");
    }
    return 42;
}).thenAccept(result -> System.out.println("Result: " + result))
  .exceptionally(ex -> {
      System.err.println("Task failed: " + ex.getMessage());
      return null;
  });

This approach made it easier to manage both the result and potential exceptions in a cleaner, more readable way.

CompletableFuture with Exception Handling

Java 8’s CompletableFuture was a significant improvement over Future. It provided a more powerful and flexible way to handle asynchronous programming, including better exception handling.

Basic Usage

Creating a CompletableFuture allowed me to run tasks asynchronously and handle results and exceptions seamlessly:

CompletableFuture.supplyAsync(() -> {
    if (new Random().nextBoolean()) {
        throw new RuntimeException("Something went wrong!");
    }
    return "Task completed successfully!";
}).thenAccept(result -> System.out.println(result))
  .exceptionally(ex -> {
      System.err.println("Task failed: " + ex.getMessage());
      return null;
  });

This code showed how to run a task asynchronously, process the result if successful, or handle the exception if it failed.

Combining Multiple Futures

In many projects, I needed to run multiple asynchronous tasks and combine their results. CompletableFuture made this straightforward:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (f1, f2) -> f1 + " " + f2);

combinedFuture.thenAccept(result -> System.out.println("Combined result: " + result))
              .exceptionally(ex -> {
                  System.err.println("Combination failed: " + ex.getMessage());
                  return null;
              });

This example demonstrated how to run two tasks concurrently and combine their results, with built-in exception handling.

The Challenges and Joys of Futures

Using Futures

Before Java 8 introduced CompletableFuture, I relied heavily on Future for asynchronous tasks. While useful, Futurehad its limitations, particularly with blocking operations and lack of chaining capabilities.

Here’s a typical Future usage:

ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(() -> {
    Thread.sleep(2000); // Simulate long-running task
    return "Task result";
});

try {
    String result = future.get();
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    executor.shutdown();
}

While this worked, the blocking nature of future.get() often led to inefficient code, especially in a highly concurrent environment.

Transitioning to CompletableFuture

Transitioning to CompletableFuture was a revelation. Not only did it provide a more flexible API, but it also allowed me to handle tasks more efficiently:

CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(2000); // Simulate long-running task
        return "Task result";
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    }
}).thenAccept(result -> System.out.println("Result: " + result))
  .exceptionally(ex -> {
      System.err.println("Task failed: " + ex.getMessage());
      return null;
  });

This approach provided a non-blocking way to handle asynchronous tasks, making my code more responsive and efficient.

Personal Experiences and Insights

JPA Repository Refactoring

Refactoring my JPA repositories was one of the most satisfying changes I made. By leveraging Java 8 features, I created more expressive and concise repository interfaces. The use of default methods allowed me to add custom query logic directly in the interface, reducing boilerplate and keeping the code clean.

The Power of CompletableFuture

Working with CompletableFuture was a game-changer. The ability to handle asynchronous tasks with built-in support for chaining and exception handling significantly improved the readability and maintainability of my code. The flexibility to combine multiple futures and handle their results efficiently made it a powerful tool in my arsenal.

Handling Runnable and Callable

Updating my usage of Runnable and Callable with lambda expressions reduced the verbosity of my code and made it more expressive. Handling exceptions in these asynchronous tasks became easier and more intuitive with Java 8’s enhanced capabilities.

Conclusion

Java 8 introduced a wealth of features that made my coding experience easier, more efficient, and more readable. By adopting functional programming principles, lambda expressions, streams, and other new features, I could write cleaner, more maintainable code. The transition from Java 7 to Java 8 involved a shift in mindset towards more declarative and functional programming styles, ultimately leading to better software design and implementation.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top