Java 8 Cheat Sheet: Ultimate Quick-Reference Guide

Table of Contents

Brief Overview of Java 8

Java is a very important computer language that helps millions of programs work all over the world. When Java 8 came out, it brought some cool new features that made programming easier and better. If you’re learning Java or you already know some and want to get better, our Java 8 Cheat Sheet: The Best Quick Guide is here to help you.

Java 8 made big changes with things like lambda expressions, which is a fancy way of making code shorter and easier to read, and the Stream API, which helps handle data in a new way. These changes help programmers do their job faster and make their programs run better. But, learning all these new things can be a bit hard.

That’s why we made this cheat sheet. It’s like a handy guide to quickly remind you how to use these new features in Java 8. Whether you’re just starting or have been coding for a while, this guide is for you. It makes learning Java 8 clear and simple, so you can make awesome programs.

With this cheat sheet, you’ll get:

  • Easy explanations of Java 8’s big updates.
  • Quick tips to use the new features.
  • Simple examples to show you how it’s done.

So, let’s dive in and make Java 8 easy to use and fun to learn!

1. What are Lambda Expressions?

Lambda Expressions can be thought of as a short block of code which takes in parameters and returns a value. Lambda expressions are similar to methods, but they do not have a name, and they can be implemented right in the body of a method.

The Syntax of Lambda Expressions

The basic syntax of a lambda expression is:

(parameter1, parameter2) -> expression

for multiple statements:

(parameter1, parameter2) -> { statements; }

Here are the key components:

  • Parameters: Like methods, lambda expressions can have zero, one, or multiple parameters.
  • Arrow Token (->): This separates the parameters from the body of the lambda.
  • Body: The body can be a single expression or a statement block.

Examples of Using Lambda Expressions in Code

Let’s dive into some examples to see lambda expressions in action.

Example 1: Using a Lambda with a Thread

Without lambda expressions, creating a thread would require a verbose syntax, either by implementing the Runnable interface or by providing an anonymous inner class. With lambda, the code becomes cleaner:

new Thread(() -> System.out.println("Thread is running")).start();
Example 2: Iterating through a Collection

Consider you have a list of names that you wish to print out. Without lambda expressions, you might use a loop. With lambda expressions, you can use the forEach method introduced in Java 8:

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

2. Functional Interfaces

Lambda expressions work closely with Functional Interfaces in Java. A functional interface is an interface that contains only one abstract method but it can contain multiple default or static methods. They serve as the type for lambda expressions. The @FunctionalInterface annotation is used to indicate that an interface is intended to be a functional interface.

It is not necessary to use @FunctionalInterface annotation but using @FunctionalInterface annotation, The compiler checks that the annotated interface satisfies the requirements of a functional interface, which is to have exactly one abstract method (apart from the methods of Object class). This prevents accidental addition of abstract methods in the future, which would violate the functional interface contract.

Java 8 comes with a package of built-in functional interfaces located in java.util.function, such as Predicate<T>, Function<T,R>, Supplier<T>, and Consumer<T>.

Built-in functional interfaces

1. Predicate<T>

Used for expressions that test a condition on an object of type T. It has a single method test(T t) that returns a boolean.

Example:

Predicate<String> isLongerThan5 = str -> str.length() > 5;

2. Consumer<T>

Represents an operation that accepts a single input argument and returns no result. It’s used for operations like printing or modifying a value.

Example:

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

3. Function<T,R>

Used for functions that take an object of type T and return an object of type R. It’s great for mapping or converting values.

Example:

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

4. Supplier<T>

Doesn’t take any argument but returns a new or some instance of type T. It’s useful for generating new values.

Example:

Supplier<Double> randomValue = Math::random;

3. Stream API

Java 8 introduced the Stream API, a powerful new way of processing collections of objects. In essence, streams represent a sequence of objects that can be processed in parallel or sequentially. This feature not only makes Java more expressive but also significantly improves the efficiency of operations on collections.

What are Streams?

A stream in Java is not a data structure but rather a pipe for conveying data from a source, such as collections, arrays, or I/O channels, through a pipeline of computational operations. Streams make it possible to process data in a declarative way, similar to SQL statements. Unlike collections, streams are not about data storage; they are about data processing.

Core Stream Operations

Stream operations are divided into intermediate and terminal operations.

  • Intermediate operations return a stream, allowing multiple operations to be chained to form a query.
  • Terminal operations produce a result or side-effect and terminate the stream.

List of Intermediate operations

List of Terminal operations

Examples of Stream Operations

Let’s explore some examples to understand how these operations work.

Example 1: Filtering a List

Suppose we have a list of integers and we want to find all even numbers:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());
System.out.println(evenNumbers); // Outputs: [2, 4, 6]
Example 2: Mapping

If we wish to square each number in a list of integers, we can use the map operation:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
                                      .map(n -> n * n)
                                      .collect(Collectors.toList());
System.out.println(squaredNumbers); // Outputs: [1, 4, 9, 16, 25]
Example 3: Sorting

To sort a list of strings:

List<String> words = Arrays.asList("Java", "Stream", "API", "Example");
List<String> sortedWords = words.stream()
                                .sorted()
                                .collect(Collectors.toList());
System.out.println(sortedWords); // Outputs: [API, Example, Java, Stream]

Parallel Streams

Parallel streams leverage multicore processors’ capability to perform operations in parallel. They are especially useful for large collections.

List<String> strings = Arrays.asList("Java", "Stream", "Parallel", "API");
List<String> filteredStrings = strings.parallelStream()
                                      .filter(s -> s.startsWith("P"))
                                      .collect(Collectors.toList());
System.out.println(filteredStrings); // Outputs: [Parallel]

Using parallel streams is as simple as replacing stream() with parallelStream(). However, it’s essential to note that not all operations benefit from parallelization and its effectiveness depends on the data size and the complexity of the operations being performed.

4. Optional Class

Java 8 introduced the Optional class, a container object which may or may not contain a non-null value. Its main purpose is to avoid NullPointerException that often comes in Java code, making it more verbose and error-prone than necessary.

The Purpose of Optional

Before Java 8, there was no clear way to express that a method might not return a value, leading to either excessive null checks or the get NullPointerException. Optional provides a way to express this explicitly, making your code more readable and helping to avoid errors.

Creating Optional Objects

To use Optional, you first need to create an Optional object. There are several ways to do this:

  • empty(): Creates an empty Optional instance.
  • of(value): Creates an Optional with a non-null value.
  • ofNullable(value): Creates an Optional from a value that could be null.
Optional<String> empty = Optional.empty();
Optional<String> nonEmpty = Optional.of("Java 8");
Optional<String> nullable = Optional.ofNullable(null);

Key Methods of Optional

Optional includes several methods to check and retrieve the contained value:

  • ifPresent(Consumer<? super T> consumer): If a value is present, it applies the given action to it, otherwise does nothing.
  • orElse(T other): Returns the value if present, otherwise returns other.
  • orElseGet(Supplier<? extends T> other): Similar to orElse, but the alternative value is provided by a Supplier interface, which is only invoked when necessary.
  • orElseThrow(Supplier<? extends X> exceptionSupplier): Throws an exception created by the provided Supplier if the value is not present.

Examples

ifPresent()

Optional<String> optional = Optional.of("Java 8");
optional.ifPresent(value -> System.out.println("Value is present, it's: " + value));
// Output: Value is present, it's: Java 8

Explanations

    orElse()

    String nullName = null;
    String name = Optional.ofNullable(nullName).orElse("John Doe");
    System.out.println(name); // Output: John Doe

    Explanations

      orElseGet()

      String nullName = null;
      String name = Optional.ofNullable(nullName).orElseGet(() -> "John Doe");
      System.out.println(name); // Output: John Doe

      Explanations

      orElseThrow()

      String nullName = null;
      String name = Optional.ofNullable(nullName).orElseThrow(
          () -> new IllegalArgumentException("Name cannot be null")
      );
      // This line throws an IllegalArgumentException

      Explanations

      Best Practices for Using Optional

      1. Do Not Use Optional for Fields: Optional should not be used on class fields because it’s not serializable and adds unnecessary wrapping.
      2. Avoid Using Optional in Method Parameters: It complicates method signatures and can be replaced with overloading.
      3. Return Optional from Methods: Especially for methods that might not return a result, indicating clearly that the method might not return a value.
      4. Use Optional Carefully with Collections: An empty collection can already represent the absence of values. Wrapping it in Optional might be redundant.
      5. Use Map with Optional If Dealing with Transformations: Instead of extracting the value and then transforming it, use map directly to transform the value if present.

      5. Date and Time API (JSR 310)

      Java 8 introduced a new Date and Time API under JSR 310, addressing the shortcomings of the older java.util.Date and java.util.Calendar. This new API, found in the java.time package, is designed to be immutable, thread-safe, and fluent, making date and time manipulation in Java more straightforward and error-resistant.

      Overview of the New Date and Time API

      The Date and Time API introduces several key classes designed for a variety of common programming scenarios:

      • LocalDate: Represents a date without a time zone.
      • LocalTime: Represents a time without a date or time zone.
      • LocalDateTime: Combines date and time but still without a time zone.
      • ZonedDateTime: Combines date and time with a time zone.

      These classes are part of a comprehensive model that includes instant, duration, period, and other time-zoning tools, making it far more capable than its predecessors.

      Key Classes and Examples

      Let’s explore the key classes with examples to understand their usage.

      LocalDate

      LocalDate represents a date in the ISO-8601 calendar system. It is useful for representing a date without a time.

      import java.time.*;
      public class LocalDateExample
      {
        public static void main (String[]args)
        {
      	LocalDate today = LocalDate.now ();
      	LocalDate specificDate = LocalDate.of (2024, Month.JANUARY, 1);
      
      	System.out.println ("Today: " + today);
      	System.out.println ("Specific Date: " + specificDate);
      
      	LocalDate tomorrow = today.plusDays (1);
      	LocalDate lastWeek = today.minusWeeks (1);
      
      	System.out.println ("Tomorrow: " + tomorrow);
      	System.out.println ("Last Week: " + lastWeek);
        }
      }

      Output

      Today: 2024-04-09
      Specific Date: 2024-01-01
      Tomorrow: 2024-04-10
      Last Week: 2024-04-02

      LocalTime

      LocalTime represents a time without a date or time zone.

      import java.time.*;
      public class LocalTimeExample
      {
        public static void main (String[]args)
        {
      	LocalTime now = LocalTime.now ();
      	LocalTime specificTime = LocalTime.of (14, 30);
      
      	System.out.println ("Now: " + now);
      	System.out.println ("Specific Time: " + specificTime);
      
      	LocalTime inAnHour = now.plusHours (1);
      	LocalTime thirtyMinutesAgo = now.minusMinutes (30);
      
      	System.out.println ("In an hour: " + inAnHour);
      	System.out.println ("Thirty minutes ago: " + thirtyMinutesAgo);
        }
      }

      Output

      Now: 11:59:49.489809
      Specific Time: 14:30
      In an hour: 12:59:49.489809
      Thirty minutes ago: 11:29:49.489809

      LocalDateTime

      LocalDateTime is a combination of LocalDate and LocalTime, representing a date-time without a time zone.

      import java.time.*;
      public class LocalDateTimeExample
      {
        public static void main (String[]args)
        {
      	LocalDateTime now = LocalDateTime.now ();
      	LocalDateTime specificDateTime =
      	LocalDateTime.of (2024, Month.JANUARY, 1, 14, 30);
      
      	System.out.println ("Now: " + now);
      	System.out.println ("Specific Date-Time: " + specificDateTime);
      
      	LocalDateTime nextMonth = now.plusMonths (1);
      	LocalDateTime twoHoursLater = specificDateTime.plusHours (2);
        
          System.out.println ("Next Month: " + nextMonth);
          System.out.println ("Two Hours Later: " + twoHoursLater);
        }
      }

      Output

      Now: 2024-04-09T12:04:09.743115
      Specific Date-Time: 2024-01-01T14:30
      Next Month: 2024-05-09T12:04:09.743115
      Two Hours Later: 2024-01-01T16:30

      ZonedDateTime

      ZonedDateTime includes time zone information with the date and time, ideal for handling time zone-specific dates and times.

      import java.time.*;
      public class ZonedDateTimeExample
      {
        public static void main (String[] args)
        {
      	ZonedDateTime nowInParis = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
          System.out.println("Now in Paris: " + nowInParis);
      
          ZonedDateTime nowInTokyo = nowInParis.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
          System.out.println("Now in Tokyo: " + nowInTokyo);
        }
      }

      Output

      Now in Paris: 2024-04-09T14:13:27.930307+02:00[Europe/Paris]
      Now in Tokyo: 2024-04-09T21:13:27.930307+09:00[Asia/Tokyo]

      6. Default Methods in Java Interfaces

      Java 8 introduced a significant enhancement to interfaces: the ability to define default methods. This feature allows developers to add new methods to interfaces without breaking the existing implementations.

      Understanding Default Methods in Interfaces

      Traditionally, interfaces in Java could only contain method declarations. Any class that implements the interface must provide an implementation for all its methods. This restriction made it difficult to evolve interfaces over time. For example, adding a new method to an interface meant that all implementing classes would break unless they provided an implementation for the new method.

      Default methods solves this limitation by allowing interfaces to provide a default implementation for methods. Implementing classes can override these default implementations, but it’s not mandatory, ensuring backward compatibility.

      Syntax and Usage

      The syntax for defining a default method in an interface is straightforward. You simply use the default keyword before the method signature and provide an implementation block.

      public interface MyInterface {
          // A default method in an interface
          default void defaultMethod() {
              System.out.println("This is a default method.");
          }
      }

      An implementing class can override this default method, just like it would override a method inherited from a superclass. If it doesn’t override the method, the default implementation will be used.

      public class MyClass implements MyInterface {
          // Overrides the default method
          @Override
          public void defaultMethod() {
              System.out.println("Overriding the default method.");
          }
      }
      
      public class AnotherClass implements MyInterface {
          // This class uses the default implementation of defaultMethod()
      }

      Supporting Multiple Inheritance

      One of the more subtle benefits of default methods is that they provide a mechanism for multiple inheritance of behavior in Java. Prior to Java 8, Java classes could inherit from only one superclass, although they could implement multiple interfaces. However, interfaces couldn’t provide any method implementation, so this form of multiple inheritance was limited to type inheritance, not behavior.

      With default methods, a class can inherit behavior from multiple interfaces. If two interfaces provide different default implementations for the same method, the implementing class must override the method to resolve the conflict.

      public interface InterfaceA {
          default void greet() {
              System.out.println("Hello from Interface A");
          }
      }
      
      public interface InterfaceB {
          default void greet() {
              System.out.println("Hello from Interface B");
          }
      }
      
      public class MyClass implements InterfaceA, InterfaceB {
          // MyClass must override greet() to resolve the ambiguity
          @Override
          public void greet() {
              InterfaceA.super.greet(); // Calls InterfaceA's version of greet()
          }
      }

      In this example, MyClass implements two interfaces (InterfaceA and InterfaceB), both of which define a default method greet(). To resolve the ambiguity, MyClass overrides greet() and explicitly chooses which interface’s greet method to use with the syntax InterfaceName.super.methodName().

      7. Nashorn JavaScript Engine

      The Nashorn JavaScript engine, introduced in Java 8, marked a significant milestone for integrating JavaScript into Java applications. It replaced the older Rhino engine, offering improved performance and a more seamless integration between Java and JavaScript.

      Introduction to Nashorn

      Nashorn is a JavaScript engine implemented in Java that complies with the ECMAScript-262 Edition 5.1 language specification. It runs on the Java Virtual Machine (JVM) and allows executing JavaScript code from within Java applications. Nashorn’s integration with Java is smooth and efficient, enabling Java applications to leverage the flexibility and power of JavaScript scripting.

      Embedding JavaScript Code in Java Applications

      Embedding JavaScript into Java is straightforward with Nashorn. You can use the javax.script package, which provides classes and interfaces for this purpose. The key components for executing JavaScript in Java are the ScriptEngine and ScriptEngineManager classes.

      Here’s a basic example of how to execute JavaScript code from a Java application:

      import javax.script.ScriptEngine;
      import javax.script.ScriptEngineManager;
      import javax.script.ScriptException;
      
      public class NashornExample {
          public static void main(String[] args) {
              // Create a script engine manager
              ScriptEngineManager manager = new ScriptEngineManager();
              // Obtain a Nashorn script engine
              ScriptEngine engine = manager.getEngineByName("nashorn");
              
              // JavaScript code as a String
              String script = "var welcome = 'Hello, Nashorn'; welcome;";
              
              // Execute the JavaScript code
              try {
                  Object result = engine.eval(script);
                  System.out.println(result);
              } catch (ScriptException e) {
                  e.printStackTrace();
              }
          }
      }

      In this example, the JavaScript code is a simple string that declares a variable and returns it. The eval method of ScriptEngine executes the script, and the result is printed to the console.

      Examples of Nashorn Usage

      Accessing Java from JavaScript

      Nashorn allows JavaScript code to access and manipulate Java objects, enabling a powerful interplay between the two languages.

      String script = "var ArrayList = Java.type('java.util.ArrayList');" +
                      "var list = new ArrayList();" +
                      "list.add('Nashorn');" +
                      "list.add('JavaScript');" +
                      "list.get(0);"; // Access the first element
      
      Object result = engine.eval(script);
      System.out.println(result); // Outputs: Nashorn

      Invoking JavaScript Functions from Java

      You can also define functions in JavaScript and invoke them from Java.

      String script = "function sum(a, b) { return a + b; }";
      engine.eval(script);
      
      // Cast the script engine to Invocable to invoke functions
      if (engine instanceof javax.script.Invocable) {
          javax.script.Invocable invocable = (javax.script.Invocable) engine;
          Object result = invocable.invokeFunction("sum", 10, 15);
          System.out.println(result); // Outputs: 25
      }

      8. Concurrent Enhancements

      Java 8 introduced several enhancements to its concurrent programming capabilities, focusing on improving performance and scalability in multi-threaded environments. These enhancements include improvements to the ConcurrentHashMap class and new additions to the java.util.concurrent package.

      Enhancements to ConcurrentHashMap

      ConcurrentHashMap has been a workhorse in Java’s concurrent package, allowing for high-performance, thread-safe access to a hash map. Java 8 brought several important updates to this class:

      • Improved Concurrent Updates: The structure of the map has been improved to reduce conflicts among threads. This means multiple threads can concurrently update the map without significant performance degradation.
      • New Functional Programming Methods: Java 8 introduced several new methods that leverage lambda expressions, making concurrent operations more straightforward and expressive.

      Example of Using ConcurrentHashMap in Java 8

      import java.util.concurrent.ConcurrentHashMap;
      
      public class ConcurrentHashMapExample {
          public static void main(String[] args) {
              ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
              
              // Using forEach to print all elements
              map.put("Key1", 1);
              map.put("Key2", 2);
              map.forEach((key, value) -> System.out.println(key + ":" + value));
              
              // Compute a new value for a given key using compute
              map.compute("Key1", (key, value) -> value == null ? 42 : value + 42);
              System.out.println(map.get("Key1")); // Outputs: 43
          }
      }

      New Additions to the java.util.concurrent Package

      Java 8 introduced new classes and interfaces to the java.util.concurrent package to support advanced concurrent programming concepts. Key additions include:

      • CompletableFuture: Represents a future result of an asynchronous computation. It can be manually completed and used to build complex asynchronous pipelines.
      • CountedCompleter: A kind of ForkJoinTask that triggers its completion action when a given number of computations (pending tasks) complete.
      • ConcurrentHashMap updates: New methods for aggregate operations and concurrent updates.

      Example of Using CompletableFuture

      CompletableFuture can be used to write non-blocking asynchronous code:

      import java.util.concurrent.CompletableFuture;
      import java.util.concurrent.ExecutionException;
      
      public class CompletableFutureExample {
          public static void main(String[] args) throws ExecutionException, InterruptedException {
              // Create a CompletableFuture
              CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
                  try {
                      Thread.sleep(1000); // Simulate a long-running operation
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  return "Result of the asynchronous computation";
              });
              
              // Attach a callback to the Future
              future.thenAccept(result -> System.out.println(result));
              
              // Wait for the future to complete
              System.out.println("Waiting for the future to complete...");
              System.out.println(future.get());
          }
      }

      Using New Concurrency Features for Better Performance

      The concurrent enhancements in Java 8 are designed to make it easier to write high-performance concurrent applications without having to deal with low-level synchronization details. For example, ConcurrentHashMap‘s new methods allow for more efficient atomic updates, reducing the need for explicit synchronization. CompletableFuture supports non-blocking asynchronous programming, allowing applications to make better use of system resources by freeing up threads to perform other tasks while waiting for asynchronous operations to complete.

      9. Method References

      Java 8 introduced Method References to make code cleaner and more readable, especially when working with functional interfaces and lambda expressions.

      Understanding Method References

      Method References are syntactic sugar in Java that allow you to refer to a method without executing it. They serve as a concise way to express instances where a lambda expression does nothing but call an existing method. Method References improve code readability and conciseness.

      Syntax and Types of Method References

      There are four types of Method References, each catering to different scenarios:

      1. Reference to a Static Method: ContainingClass::staticMethodName
      2. Reference to an Instance Method of a Particular Object: instance::instanceMethodName
      3. Reference to an Instance Method of an Arbitrary Object of a Particular Type: ContainingType::methodName
      4. Reference to a Constructor: ClassName::new

      1. Reference to a Static Method

      This type refers to static methods of a class. For example, consider a method reference to Math::max which refers to the static method max of the Math class.

      BinaryOperator<Integer> operator = Math::max;
      int result = operator.apply(10, 20);
      System.out.println(result); // Outputs: 20

      2. Reference to an Instance Method of a Particular Object

      This type refers to an instance method of a specific object.

      String myString = "Hello, World!";
      Supplier<Integer> stringLength = myString::length;
      System.out.println(stringLength.get()); // Outputs: 13

      3. Reference to an Instance Method of an Arbitrary Object of a Particular Type

      This type refers to an instance method of an object to be determined at runtime.

      List<String> strings = Arrays.asList("a", "b", "A", "B");
      strings.sort(String::compareToIgnoreCase);
      System.out.println(strings); // Outputs: [a, A, b, B]

      4. Reference to a Constructor

      Constructor references are used to refer to constructors of classes.

      Supplier<List<String>> listSupplier = ArrayList::new;
      List<String> list = listSupplier.get();

      Examples of Using Method References

      Let’s consider a scenario where we have a list of users and we want to extract a list of their names:

      class User {
          private String name;
          
          public User(String name) {
              this.name = name;
          }
          
          public String getName() {
              return name;
          }
      }
      
      List<User> users = Arrays.asList(new User("Alice"), new User("Bob"));

      Extracting Names with a Lambda Expression

      Without method references, you might use a lambda expression like this:

      List<String> names = users.stream()
                                .map(user -> user.getName())
                                .collect(Collectors.toList());

      Using Method References

      The same operation can be performed more concisely with a method reference:

      List<String> names = users.stream()
                                .map(User::getName)
                                .collect(Collectors.toList());

      How useful was this post?

      Click on a star to rate it!

      Average rating 0 / 5. Vote count: 0

      No votes so far! Be the first to rate this post.