LTS New Features – [ Java 8 – Part 2 ]

Since the first version released in 1996, the Java platform has been actively being developed for more than 30 years.

Many changes and improvements have been made to the technology over the years.

Major versions were released after every 2 years, however the Java 7 took 5 years to be available after its predecessor Java 6, and 3 years for Java 8 to be available to public afterward.

Since Java 10, new versions will be released every six months.

Versions 25, 21, 17, 11 and 8 are the currently long-term support (LTS) versions, where customers will receive Oracle Premier/Extended Support.

LTS VERSIONRELEASE DATEEND OF SUPPORT
8March 2014December 2030
11September 2018January 2032
17September 2021September 2029
21September 2023September 2031
25September 2025September 2033

Now that Java 8 has been widely used, it contains the following major new features compared to previous versions :

  • Lambda Expressions, a new language feature, let us to treat functionality as a method argument, or code as data;
  • Method references provide easy-to-read lambda expressions for methods that already have a name;
  • Classes in the new java.util.stream package provide a Stream API to support functional-style operations on streams of elements;
  • Default methods enable new functionality to be added to the interfaces of libraries and ensure binary compatibility with code written for older versions of those interfaces;
  • Optional<T> class can help to handle situations where there is a possibility of getting the NPE;
  • new APIs for Date and Time to address the shortcomings of the older java.util.Date and java.util.Calendar;
  • Type Annotations provide the ability to use annotation anywhere we use a type;
  • Repeating Annotations provide the ability to apply the same annotation type more than once to the same declaration or type use;
  • Iterable interface;
  • StringJoiner.

4. Default Methods

Before Java 8, interfaces could have only abstract methods.

The implementation of these methods has to be provided in a separate class.

So if a new method is to be added in an interface, then its implementation code has to be provided in the class implementing the same interface.

Java 8 has introduced the concept of default methods which allow the interfaces to have methods with implementation without affecting the classes that implement the interface.

The default methods were introduced to provide backward compatibility so that existing interfaces can use the lambda expressions without implementing the methods in the implementation class.

public class Java8NewFeaturesTest76 {
    public static void main(String[] args) {
        French french = new French();

        System.out.println(french.greeting());
        System.out.println(french.laugh());

        System.out.println();

        English english = new English();

        System.out.println(english.greeting());
        System.out.println(english.laugh());
    }

    private static interface Person {
        default String laugh() {
            return "hahahahaha";
        }

        String greeting();
    }

    private static class French implements Person {
        @Override
        public String greeting() {
            return "bonjour";
        }
    }

    private static class English implements Person {
        @Override
        public String greeting() {
            return "hello";
        }
    }
}
/**
 * Output:
    bonjour
    hahahahaha
    
    hello
    hahahahaha
 */

In case both the implemented interfaces contain default methods with same method signature, the implementing class should explicitly specify which default method is to be used or it should override the default method.

public class Java8NewFeaturesTest77 {
    public static void main(String[] args) {
        EnglandBornFrench p1 = new EnglandBornFrench();
        System.out.println(p1.greeting());

        System.out.println();

        FranceBornEnglish p2 = new FranceBornEnglish();
        System.out.println(p2.greeting());
    }

    private static interface French {
        default String greeting() {
            return "bonjour";
        }
    }

    private static interface English {
        default String greeting() {
            return "hello";
        }
    }

    private static class EnglandBornFrench implements French, English {
        @Override
        public String greeting() {
            return English.super.greeting();
        }
    }

    private static class FranceBornEnglish implements French, English {
        @Override
        public String greeting() {
            return French.super.greeting();
        }
    }
}
/**
 * Output:
    hello
    
    bonjour
 */

Java 8 has introduced as well the concept of static methods of interface which is similar to static method of classes.

Since static methods do not belong to a particular object, they are not part of the classes implementing the interface.

Therefore, they have to be called by using the interface name preceding the method name.

public class Java8NewFeaturesTest78 {
    public static void main(String[] args) {
        System.out.println(Person.laugh());
    }

    private static interface Person {
        static String laugh() {
            return "hahahaha";
        }
    }
}
/**
 * Output:
    hahahaha
 */

The idea behind static interface methods is to provide a simple mechanism of putting together related methods in one single place without having to create an object.

For example, static methods in interfaces make it possible to group related utility methods, without having to create artificial utility classes that are simply placeholders for static methods.

5. Optional Class

The null reference is the source of many problems because it is often used to denote the absence of a value.

In below code snippet, the following code looks pretty reasonable :

car.getFuel().getTank().getCapacity()

public class Java8NewFeaturesTest79 {
    public static void main(String[] args) {
        Car car = new Car(new FuelSystem(new FuelTank(20)));

        System.out.println(car.getFuel().getTank().getCapacity());
    }

    private static class Car {
        private FuelSystem fuel;

        public Car(FuelSystem fuel) {
            this.fuel = fuel;
        }

        public FuelSystem getFuel() {
            return fuel;
        }
    }

    private static class FuelSystem {
        private FuelTank tank;

        public FuelSystem(FuelTank tank) {
            this.tank = tank;
        }

        public FuelTank getTank() {
            return tank;
        }
    }

    private static class FuelTank {
        private int capacity;

        public FuelTank(int capacity) {
            this.capacity = capacity;
        }

        public int getCapacity() {
            return capacity;
        }
    }
}
/**
 * Output:
    20
 */

However, if it is a electric car which does not actually have a fuel system, what is the result of getFuel() ?

A common practice is to return the null reference to indicate the absence of a fuel system.

Unfortunately, this means the call to getTank() will try to return the fuel tank of a null reference, which will result in a NullPointerException at runtime and stop the program from running further.

To prevent unintended null pointer exceptions, we can add checks to prevent null dereferences as shown in below code snippet :

public class Java8NewFeaturesTest80 {
    public static void main(String[] args) {
        Car car = new Car(new FuelSystem(new FuelTank(20)));

        int capacity = 0;

        if (car != null) {
            FuelSystem fuel = car.getFuel();

            if (fuel != null) {
                FuelTank tank = fuel.getTank();

                if (tank != null) {
                    capacity = tank.getCapacity();
                }
            }
        }

        System.out.println(capacity);
    }

    private static class Car {
        private FuelSystem fuel;

        public Car(FuelSystem fuel) {
            this.fuel = fuel;
        }

        public FuelSystem getFuel() {
            return fuel;
        }
    }

    private static class FuelSystem {
        private FuelTank tank;

        public FuelSystem(FuelTank tank) {
            this.tank = tank;
        }

        public FuelTank getTank() {
            return tank;
        }
    }

    private static class FuelTank {
        private int capacity;

        public FuelTank(int capacity) {
            this.capacity = capacity;
        }

        public int getCapacity() {
            return capacity;
        }
    }
}
/**
 * Output:
    20
 */

However, the code quickly becomes very ugly due to the nested checks, because :

  • they are boilerplate code to make sure there is no NullPointerException;
  • they get in the way of the business logic;
  • they are decreasing the overall readability of the program.

Java 8 introduces a new class called java.util.Optional<T> that can alleviate some of these problems.

It is a class that encapsulates an optional value, we can view it as a single-value container that either contains a value or doesn’t (empty).

In below code snippet, the null-check patterns have been rewritten by using Optional.

import java.util.Optional;

public class Java8NewFeaturesTest81 {
    public static void main(String[] args) {
        Car car = new Car(new FuelSystem(new FuelTank(20)));

        int capacity = getCapacity(car);

        System.out.println(capacity);
        System.out.println();

        Car car2 = new Car(new FuelSystem(null));

        capacity = getCapacity(car2);

        System.out.println(capacity);
        System.out.println();

        Car car3 = new Car(null);

        capacity = getCapacity(car3);

        System.out.println(capacity);

    }

    private static int getCapacity(Car car) {
        return Optional.ofNullable(car)
                .map(Car::getFuel)
                .map(FuelSystem::getTank)
                .map(FuelTank::getCapacity)
                .orElseGet(() -> 0);
    }

    private static class Car {
        private FuelSystem fuel;

        public Car(FuelSystem fuel) {
            this.fuel = fuel;
        }

        public FuelSystem getFuel() {
            return this.fuel;
        }
    }

    private static class FuelSystem {
        private FuelTank tank;

        public FuelSystem(FuelTank tank) {
            this.tank = tank;
        }

        public FuelTank getTank() {
            return this.tank;
        }
    }

    private static class FuelTank {
        private int capacity;

        public FuelTank(int capacity) {
            this.capacity = capacity;
        }

        public int getCapacity() {
            return this.capacity;
        }
    }
}
/**
 * Output:
    20

    0

    0
 */

5.1 Creating Optional Objects

We can create an Optional object with the static method of().

However, the argument passed to the of() method can not be null.

Otherwise, we will get a NullPointerException.

In case we need null values, we can use the ofNullable() method.

By doing this, it does not throw an exception but rather returns an empty Optional object.

An empty Optional object does not contain any value, so when trying to access its value by using get(), it will throw no value present exception.

import java.util.Optional;

public class Java8NewFeaturesTest82 {
    public static void main(String[] args) {
        String name = "tom";
        Optional<String> nameOpt = Optional.of(name);
        System.out.println(nameOpt.isPresent());
        System.out.println(nameOpt.get());

        System.out.println();

        name = null;
        try {
            nameOpt = Optional.of(name);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println();

        nameOpt = Optional.ofNullable(name);
        System.out.println(nameOpt.isPresent());
        try {
            System.out.println(nameOpt.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println();

        Optional<Object> empty = Optional.empty();

        System.out.println(empty.isPresent());

        try {
            System.out.println(empty.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/**
 * Output:
    true
    tom
    
    java.lang.NullPointerException
            at java.base/java.util.Objects.requireNonNull(Objects.java:220)
            at java.base/java.util.Optional.of(Optional.java:113)
            at Java8NewFeaturesTest82.main(Java8NewFeaturesTest82.java:14)
    
    false
    java.util.NoSuchElementException: No value present
            at java.base/java.util.Optional.get(Optional.java:143)
            at Java8NewFeaturesTest82.main(Java8NewFeaturesTest82.java:24)
    
    false
    java.util.NoSuchElementException: No value present
            at java.base/java.util.Optional.get(Optional.java:143)
            at Java8NewFeaturesTest82.main(Java8NewFeaturesTest82.java:36)
 */

5.2 Checking Value Presence

To check if there is a value in an Optional object, we can use the isPresent() method which returns true if the wrapped value is not null.

As of Java 11, the isEmpty() method can be used as well, it returns true when the wrapped value is null or missing.

import java.util.Optional;

public class Java8NewFeaturesTest83 {
    public static void main(String[] args) {
        Optional<String> opt1 = Optional.of("tom");

        System.out.println(opt1.isPresent());
        System.out.println(!opt1.isEmpty());
        System.out.println();

        Optional<String> opt2 = Optional.ofNullable(null);

        System.out.println(opt2.isPresent());
        System.out.println(!opt2.isEmpty());
        System.out.println();

        Optional<Object> opt3 = Optional.empty();

        System.out.println(opt3.isPresent());
        System.out.println(!opt3.isEmpty());
        System.out.println();
    }
}
/**
 * Output:
    true
    true
    
    false
    false
    
    false
    false
 */

The ifPresent() method allows to run some code on the wrapped value if it is found to be non-null.

Below code snippet shows the different ways of coding for printing a variable’s value if its non-null check is ok.

import java.util.Optional;

public class Java8NewFeaturesTest84 {
    public static void main(String[] args) {
        String name = "tom";

        if (name != null) {
            System.out.println(name);
        }

        System.out.println();

        Optional<String> opt = Optional.of(name);

        opt.ifPresent(System.out::println);
    }
}
/**
 * Output:
    tom
    
    tom
 */

5.3 Returning Value

The approach for retrieving the wrapped value is the get() method.

It can only return a value if the wrapped object is not null, otherwise, it throws a no such element exception.

import java.util.Optional;

public class Java8NewFeaturesTest88 {
    public static void main(String[] args) {
        Optional<String> name = Optional.ofNullable("tom");

        System.out.println(name.get());

        System.out.println();

        name = Optional.ofNullable(null);

        System.out.println(name.get());
    }
}
/**
 * Output:
    tom
    
    java.util.NoSuchElementException: No value present
            at java.base/java.util.Optional.get(Optional.java:143)
            at Java8NewFeaturesTest88.main(Java8NewFeaturesTest88.java:12)
 */

5.4 Default Values

If the wrapped value is not present or null, the orElse() method is used to retrieve a default value.

The orElseGet() method is similar to orElse().

However, instead of taking a value to return as default one, it takes a supplier functional interface which is invoked and returns the default value.

import java.util.Optional;

public class Java8NewFeaturesTest85 {
    public static void main(String[] args) {
        Object name = Optional.ofNullable(null).orElse("tom");

        System.out.println(name);

        System.out.println();

        name = Optional.ofNullable(null).orElseGet(() -> "tom");

        System.out.println(name);
    }
}
/**
 * Output:
 * tom
 * 
 * tom
 */

When the wrapped value is not present, then both orElse() and orElseGet() work exactly the same way.

However, when using orElse(), whether the wrapped value is present or not, the default object is created.

So if it has to make a web service call or even query a database to create a default value, it is better to use orElseGet() method in order to minimize the cost.

import java.util.Optional;

public class Java8NewFeaturesTest86 {
    public static void main(String[] args) {
        String jerry = "jerry";

        Object name = Optional.ofNullable(jerry).orElse(getDefaultValue("Inside orElse"));

        System.out.println(name);

        System.out.println();

        name = Optional.ofNullable(jerry).orElseGet(() -> getDefaultValue("Inside orElseGet"));

        System.out.println(name);
    }

    private static String getDefaultValue(String message) {
        System.out.println(message);
        return "tom";
    }
}
/**
 * Output:
 * Inside orElse
 * jerry
 * 
 * jerry
 */

Instead of returning a default value when the wrapped value is not present, the orElseThrow() method throws an exception.

import java.util.Optional;

public class Java8NewFeaturesTest87 {
    public static void main(String[] args) {
        try {
            Optional.ofNullable(null).orElseThrow();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println();

        Optional.ofNullable(null).orElseThrow(() -> new RuntimeException("no such element"));
    }
}
/**
 * Output:
    java.util.NoSuchElementException: No value present
        at java.base/java.util.Optional.orElseThrow(Optional.java:377)
        at Java8NewFeaturesTest87.main(Java8NewFeaturesTest87.java:6)

    Exception in thread "main" java.lang.RuntimeException: no such element
        at Java8NewFeaturesTest87.lambda$0(Java8NewFeaturesTest87.java:13)
        at java.base/java.util.Optional.orElseThrow(Optional.java:403)
        at Java8NewFeaturesTest87.main(Java8NewFeaturesTest87.java:13)
 */

5.5 Rejecting Certain Values

The filter method of Optional takes a predicate as an argument.

If a value is present in the Optional object and it matches the predicate, the filter method returns that value; otherwise, it returns an empty Optional object.

It is normally used to reject wrapped values based on a predefined rule.

import java.util.Optional;

public class Java8NewFeaturesTest89 {
    public static void main(String[] args) {
        System.out.println("*********  Tom is >= 18 *********");
        Person p = new Person("tom", 18);

        traditionalWay(p);

        optionalWay(p);

        System.out.println();
        System.out.println("********* Jerry is < 18 *********");

        Person p2 = new Person("jerry", 16);

        traditionalWay(p2);

        optionalWay(p2);

        System.out.println();
        System.out.println("*********  No one here  *********");

        Person p3 = null;

        traditionalWay(p3);

        optionalWay(p3);
    }

    private static void optionalWay(Person person) {
        Optional.ofNullable(person).filter(p -> p.age >= 18).ifPresent(System.out::println);
    }

    private static void traditionalWay(Person person) {
        if (person != null && person.age >= 18) {
            System.out.println(person);
        }
    }

    private static class Person {
        private String name;
        private int age;

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

        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + "]";
        }
    }
}
/**
 * Output:
        *********  Tom is >= 18 *********
        Person [name=tom, age=18]
        Person [name=tom, age=18]
        
        ********* Jerry is < 18 *********
        
        *********  No one here  *********
 */

5.6 Transforming Values

Considering from a Computer object, we may want to extract its motherboard object and then from this motherboard object to extract its memory object and further check its capacity.

For every extracted object, we need to check for null and continue to extract.

Below code snippet show how to do by traditional way and by using map method which transforms the value contained inside Optional by the function passed as an argument, while nothing happens if Optional is empty.

import java.util.Optional;

public class Java8NewFeaturesTest90 {
    public static void main(String[] args) {
        traditionalWay();

        System.out.println();

        optionalWay();
    }

    private static void optionalWay() {
        Optional<Computer> computer = Optional.ofNullable(new Computer(new MotherBoard(new Memory(64))));

        Integer capacity = computer.map(Computer::getMotherBoard).map(MotherBoard::getMemory).map(Memory::getCapacity)
                .orElse(0);

        System.out.println(capacity);
    }

    private static void traditionalWay() {
        int capacity = 0;

        Computer computer = new Computer(new MotherBoard(new Memory(64)));

        if (computer != null) {
            MotherBoard motherBoard = computer.getMotherBoard();

            if (motherBoard != null) {
                Memory memory = motherBoard.getMemory();

                if (memory != null) {
                    capacity = memory.getCapacity();
                }
            }
        }

        System.out.println(capacity);
    }

    private static class Computer {
        private MotherBoard motherBoard;

        public Computer(MotherBoard motherBoard) {
            this.motherBoard = motherBoard;
        }

        public MotherBoard getMotherBoard() {
            return motherBoard;
        }
    }

    private static class MotherBoard {
        private Memory memory;

        public MotherBoard(Memory memory) {
            this.memory = memory;
        }

        public Memory getMemory() {
            return memory;
        }
    }

    private static class Memory {
        private int capacity;

        public Memory(int capacity) {
            this.capacity = capacity;
        }

        public int getCapacity() {
            return capacity;
        }
    }
}
/**
 * Output:
 * 64
 * 
 * 64
 */

Just like the map() method, there is also flatMap() method as an alternative for transforming values.

A simple rule to remember : If your function returns an Optional, use flatMap() otherwise if it returns a normal value, use map().

Below code snippet shows how to use flatMap() to transform values wrapped with two levels Optional along with using map().

import java.util.Optional;

public class Java8NewFeaturesTest91 {
    public static void main(String[] args) {
        Optional<Computer> computer = Optional.ofNullable(new Computer(new MotherBoard(new Memory(64))));

        Integer capacity = computer.flatMap(Computer::getMotherBoard).flatMap(MotherBoard::getMemory)
                .map(Memory::getCapacity).orElse(0);

        System.out.println(capacity);
    }

    private static class Computer {
        private MotherBoard motherBoard;

        public Computer(MotherBoard motherBoard) {
            this.motherBoard = motherBoard;
        }

        public Optional<MotherBoard> getMotherBoard() {
            return Optional.ofNullable(this.motherBoard);
        }
    }

    private static class MotherBoard {
        private Memory memory;

        public MotherBoard(Memory memory) {
            this.memory = memory;
        }

        public Optional<Memory> getMemory() {
            return Optional.ofNullable(this.memory);
        }
    }

    private static class Memory {
        private int capacity;

        public Memory(int capacity) {
            this.capacity = capacity;
        }

        public int getCapacity() {
            return capacity;
        }
    }
}
/**
 * Output:
 * 64
 */

6. New APIs for Date and Time

Java 8 introduced new APIs for Date and Time to overcome the following drawbacks of old date-time API java.util.Date and java.util.Calendar :

  • Thread Safety : the Date and Calendar classes are not thread safe, the new Date and Time APIs introduced in Java 8 are immutable and does not have setter methods and thread safe;
  • API Design : the Date and Calendar APIs are poorly designed with only few date operations, the new Date and Time APIs provides a wide variety of utility methods that support the most common operations.

6.1 LocalDate LocalTime and LocalDateTime

When time zones are not required to be explicitly specified, LocalDate, LocalTime and LocalDateTime are classes most commonly used.

The LocalDate represents a date in ISO format (yyyy-MM-dd) without time.

It can be used to store dates like birthdays and paydays.

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class Java8NewFeaturesTest92 {
    public static void main(String[] args) {
        LocalDate now = LocalDate.now();

        System.out.println(now);

        LocalDate future = LocalDate.of(2025, 02, 23);

        System.out.println(future);

        LocalDate past = LocalDate.parse("2022-03-17");

        System.out.println(past);

        LocalDate oneDayAfter = now.plusDays(1);

        System.out.println(oneDayAfter);

        LocalDate oneMonthAfter = now.plus(1, ChronoUnit.MONTHS);

        System.out.println(oneMonthAfter);

        int dayOfMonth = now.getDayOfMonth();

        System.out.println(dayOfMonth);

        int dayOfYear = now.getDayOfYear();

        System.out.println(dayOfYear);

        boolean isLeapYear = now.isLeapYear();

        System.out.println(isLeapYear);

        boolean isBefore = now.isBefore(future);

        System.out.println(isBefore);

        boolean isAfter = now.isAfter(past);

        System.out.println(isAfter);
    }
}
/**
 * Output:
        2026-02-28
        2025-02-23
        2022-03-17
        2026-03-01
        2026-03-28
        28
        59
        false
        false
        true
 */

The LocalTime represents time without a date.

import java.time.LocalTime;
import java.time.temporal.ChronoUnit;

public class Java8NewFeaturesTest93 {
    public static void main(String[] args) {
        LocalTime now = LocalTime.now();

        System.out.println(now);

        LocalTime past = LocalTime.of(06, 20, 32);

        System.out.println(past);

        LocalTime future = LocalTime.parse("22:27:39");

        System.out.println(future);

        LocalTime plus = now.plus(1, ChronoUnit.HOURS);

        System.out.println(plus);

        LocalTime minus = now.minus(1, ChronoUnit.HOURS);

        System.out.println(minus);

        boolean isBefore = now.isBefore(future);

        System.out.println(isBefore);

        boolean isAfter = now.isAfter(past);

        System.out.println(isAfter);

        int hour = now.getHour();
        int minute = now.getMinute();
        int second = now.getSecond();

        System.out.println(hour + ":" + minute + ":" + second);

        System.out.println(LocalTime.MAX);

        System.out.println(LocalTime.MIN);
    }
}
/**
 * Output:
        08:27:49.595050539
        06:20:32
        22:27:39
        09:27:49.595050539
        07:27:49.595050539
        true
        true
        08:27:49
        23:59:59.999999999
        00:00
 */

LocalDateTime is used to represent a combination of date and time.

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class Java8NewFeaturesTest94 {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();

        System.out.println(now);

        LocalDateTime future = LocalDateTime.of(2025, 12, 23, 23, 39, 33);

        System.out.println(future);

        LocalDateTime past = LocalDateTime.parse("2023-01-02T12:23:21");

        System.out.println(past);

        boolean isAfter = now.isAfter(past);

        System.out.println(isAfter);

        boolean isBefore = now.isBefore(future);

        System.out.println(isBefore);

        LocalDateTime plus = now.plus(1, ChronoUnit.DAYS);

        System.out.println(plus);

        LocalDateTime minus = now.minus(1, ChronoUnit.HOURS);

        System.out.println(minus);

        int year = now.getYear();
        int month = now.getMonthValue();
        int dayOfMonth = now.getDayOfMonth();

        int hour = now.getHour();
        int minute = now.getMinute();
        int second = now.getSecond();

        System.out.println(year + "-" + month + "-" + dayOfMonth + " " + hour + ":" + minute + ":" + second);
    }
}
/**
 * Output:
        2026-02-28T08:29:25.981021908
        2025-12-23T23:39:33
        2023-01-02T12:23:21
        true
        false
        2026-03-01T08:29:25.981021908
        2026-02-28T07:29:25.981021908
        2026-2-28 8:29:25
 */

6.2 ZonedDateTime

To deal with time-zone-specific date and time, Java 8 provides ZonedDateTime class.

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Java8NewFeaturesTest95 {
    public static void main(String[] args) {
        // Create ZonedDateTime
        ZonedDateTime paris = ZonedDateTime.parse("2015-05-03T10:15:30+02:00[Europe/Paris]");

        System.out.println(paris);

        ZonedDateTime shanghai = ZonedDateTime.of(2024, 05, 05, 10, 50, 04, 0, ZoneId.of("Asia/Shanghai"));

        System.out.println(shanghai);

        // Convert LocalDateTime to ZonedDateTime
        LocalDateTime now = LocalDateTime.now();

        System.out.println("now = " + now);

        ZoneId sourceZone = ZoneId.systemDefault();

        System.out.println("source zone = " + sourceZone);

        ZoneId targetZone = ZoneId.of("Europe/Lisbon");

        System.out.println("target zone = " + targetZone);

        ZonedDateTime zonedNow = now.atZone(sourceZone);

        System.out.println("now in source zone = " + zonedNow);

        ZonedDateTime zoned = zonedNow.withZoneSameInstant(targetZone);

        System.out.println("now in target zone = " + zoned);
    }
}
/**
    Output:
        2015-05-03T10:15:30+02:00[Europe/Paris]
        2024-05-05T10:50:04+08:00[Asia/Shanghai]
        now = 2026-02-28T08:33:30.676079079
        source zone = Europe/Paris
        target zone = Europe/Lisbon
        now in source zone = 2026-02-28T08:33:30.676079079+01:00[Europe/Paris]
        now in target zone = 2026-02-28T07:33:30.676079079Z[Europe/Lisbon]
 */

6.3 Period and Duration

Period and Duration classes can be used to represent an amount of time or determine the difference between two dates.

The Period class uses date-based values and represents a quantity of time in terms of years, months and days.

import java.time.LocalDate;
import java.time.Period;

public class Java8NewFeaturesTest96 {
    public static void main(String[] args) {
        LocalDate startDate = LocalDate.of(2024, 02, 02);
        LocalDate endDate = LocalDate.of(2025, 01, 03);

        Period p1 = Period.between(startDate, endDate);

        System.out.println(!p1.isNegative() ? startDate + " is before " + endDate : startDate + " is after " + endDate);
        System.out.println(p1.getYears());
        System.out.println(p1.getMonths());
        System.out.println(p1.getDays());

        System.out.println();

        Period p2 = Period.between(endDate, startDate);

        System.out.println(!p2.isNegative() ? endDate + " is before " + startDate : endDate + " is after " + startDate);
        System.out.println(p2.getYears());
        System.out.println(p2.getMonths());
        System.out.println(p2.getDays());

        System.out.println();

        Period p3 = Period.of(2, 3, 4);
        System.out.println(p3.isNegative());
        System.out.println(p3.getYears());
        System.out.println(p3.getMonths());
        System.out.println(p3.getDays());

        System.out.println();

        Period p4 = Period.of(-2, 3, 4);
        System.out.println(p4.isNegative());
        System.out.println(p4.getYears());
        System.out.println(p4.getMonths());
        System.out.println(p4.getDays());

        System.out.println();

        Period p5 = Period.parse("P2Y3M4D");
        System.out.println(p5.isNegative());
        System.out.println(p5.getYears());
        System.out.println(p5.getMonths());
        System.out.println(p5.getDays());

        System.out.println();

        Period p6 = Period.parse("P-2Y3M4D");
        System.out.println(p6.isNegative());
        System.out.println(p6.getYears());
        System.out.println(p6.getMonths());
        System.out.println(p6.getDays());

        System.out.println();

        startDate = startDate.plus(Period.ofYears(1));
        startDate = startDate.plus(Period.ofMonths(-1));
        startDate = startDate.plus(Period.ofDays(1));

        System.out.println("startDate = " + startDate);
        System.out.println("endDate = " + endDate);

        Period p7 = Period.between(startDate, endDate);

        System.out.println(p7);
    }
}
/**
 * Output:
        2024-02-02 is before 2025-01-03
        0
        11
        1
        
        2025-01-03 is after 2024-02-02
        0
        -11
        -1
        
        false
        2
        3
        4
        
        true
        -2
        3
        4
        
        false
        2
        3
        4
        
        true
        -2
        3
        4
        
        startDate = 2025-01-03
        endDate = 2025-01-03
        P0D
 */

The Duration class uses time-based values and represents a quantity of time in terms of seconds and nanoseconds.

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class Java8NewFeaturesTest97 {
    public static void main(String[] args) {
        LocalTime startTime = LocalTime.of(11, 01, 02);
        LocalTime endTime = LocalTime.of(12, 10, 13);

        Duration d1 = Duration.between(startTime, endTime);

        System.out.println(!d1.isNegative() ? startTime + " is before " + endTime : startTime + " is after " + endTime);
        System.out.println(d1.toHours());
        System.out.println(d1.toMinutes());
        System.out.println(d1.toSeconds());

        System.out.println();

        Duration d2 = Duration.between(endTime, startTime);

        System.out.println(!d2.isNegative() ? endTime + " is before " + startTime : endTime + " is after " + startTime);
        System.out.println(d2.toHours());
        System.out.println(d2.toMinutes());
        System.out.println(d2.toSeconds());

        System.out.println();

        Duration d3 = Duration.parse("PT2H3M4S");
        System.out.println(d3.isNegative());
        System.out.println(d3.toHours());
        System.out.println(d3.toMinutes());
        System.out.println(d3.toSeconds());

        System.out.println();

        startTime = startTime.plus(Duration.ofHours(1));
        startTime = startTime.plus(Duration.ofMinutes(-1));
        startTime = startTime.plus(Duration.ofSeconds(1));

        System.out.println("startTime = " + startTime);
        System.out.println("endTime = " + endTime);

        Duration d4 = Duration.between(startTime, endTime);

        System.out.println(d4);

        LocalDateTime startDateTime = LocalDateTime.of(2024, 03, 01, 12, 13, 14);
        LocalDateTime endDateTime = LocalDateTime.of(2025, 04, 02, 13, 14, 15);

        Duration d5 = Duration.between(startDateTime, endDateTime);

        System.out.println(d5);
    }
}
/**
    Output:
        11:01:02 is before 12:10:13
        1
        69
        4151

        12:10:13 is after 11:01:02
        -1
        -69
        -4151

        false
        2
        123
        7384

        startTime = 12:00:03
        endTime = 12:10:13
        PT10M10S
        PT9529H1M1S
*/

Alternatively, to find out the difference between date time, we can use between method of ChronoUnit class.

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class Java8NewFeaturesTest98 {
    public static void main(String[] args) {
        LocalDateTime startDate = LocalDateTime.of(2024, 8, 31, 10, 20, 55);
        LocalDateTime endDate = LocalDateTime.of(2027, 11, 9, 10, 21, 56);

        System.out.println(startDate);
        System.out.println(endDate);

        long years = ChronoUnit.YEARS.between(startDate, endDate);
        long months = ChronoUnit.MONTHS.between(startDate, endDate);
        long weeks = ChronoUnit.WEEKS.between(startDate, endDate);
        long days = ChronoUnit.DAYS.between(startDate, endDate);
        long hours = ChronoUnit.HOURS.between(startDate, endDate);
        long minutes = ChronoUnit.MINUTES.between(startDate, endDate);
        long seconds = ChronoUnit.SECONDS.between(startDate, endDate);
        long milis = ChronoUnit.MILLIS.between(startDate, endDate);
        long nano = ChronoUnit.NANOS.between(startDate, endDate);

        System.out.println(years + " years");
        System.out.println(months + " months");
        System.out.println(weeks + " weeks");
        System.out.println(days + " days");
        System.out.println(hours + " hours");
        System.out.println(minutes + " minutes");
        System.out.println(seconds + " seconds");
        System.out.println(milis + " milis");
        System.out.println(nano + " nano");
    }
}
/**
    Output :
        2024-08-31T10:20:55
        2027-11-09T10:21:56
        3 years
        38 months
        166 weeks
        1165 days
        27960 hours
        1677601 minutes
        100656061 seconds
        100656061000 milis
        100656061000000000 nano
*/

6.4 Compatibility With Date and Calendar

It is possible to convert existing Date and Calendar instance to new Date and Time API.

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;

public class Java8NewFeaturesTest99 {
    public static void main(String[] args) {
        Date d = new Date();

        System.out.println(d);

        Calendar c = Calendar.getInstance();

        System.out.println(c.getTime());

        System.out.println();

        LocalDateTime ldt1 = LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault());

        System.out.println(ldt1);

        LocalDateTime ldt2 = LocalDateTime.ofInstant(c.toInstant(), ZoneId.systemDefault());

        System.out.println(ldt2);
    }
}
/**
  Output:
        Sat Feb 28 08:42:23 CET 2026
        Sat Feb 28 08:42:23 CET 2026

        2026-02-28T08:42:23.744
        2026-02-28T08:42:23.884
 */

6.5 Date and Time Formatting

DateTimeFormatter can be used to format dates and times with predefined or user-defined patterns.

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class Java8NewFeaturesTest100 {
    public static void main(String[] args) {
        // predefined
        String now = DateTimeFormatter.ISO_DATE.format(LocalDate.now());

        System.out.println(now);

        String now2 = DateTimeFormatter.ISO_DATE_TIME.format(LocalDateTime.now());

        System.out.println(now2);

        // user defined
        LocalDate d = LocalDate.of(2015, 10, 03);

        String formatedD = d.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));

        System.out.println(formatedD);

        LocalTime t = LocalTime.of(11, 20, 58);

        String formatedT = t.format(DateTimeFormatter.ofPattern("hh:mm:ss"));

        System.out.println(formatedT);

        LocalDateTime dt = LocalDateTime.of(2016, 04, 03, 01, 30, 9);

        String formatedDT = dt.format(DateTimeFormatter.ofPattern("yyyy/MM/dd hh:mm:ss"));

        System.out.println(formatedDT);
    }
}
/**
 * Output:
        2026-02-28
        2026-02-28T08:50:42.446709507
        2015/10/03
        11:20:58
        2016/04/03 01:30:09
 */

7. Type Annotations and Repeating Annotations

Java 8 has included type and repeating annotations in its prior annotations topic.

7.1 Type Annotations

In early Java versions, annotations can only be applied to declarations.

Type Annotations are annotations that can be placed anywhere we use a type.

This includes the new operator, type casts, implements clauses and throws clauses.

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;

public class Java8NewFeaturesTest101 {
    @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ExpectedType {
        public Class<?> value();
    }

    public static class CustomStringUtils {
        public @ExpectedType(String.class) String convertCase(@ExpectedType(String.class) String s,
                @ExpectedType(boolean.class) boolean toUpperCase) {
            return toUpperCase ? s.toUpperCase() : s.toLowerCase();
        }
    }

    public static void main(String[] args) throws NoSuchMethodException, SecurityException {
        Method m = CustomStringUtils.class.getMethod("convertCase", String.class, boolean.class);

        AnnotatedType returnType = m.getAnnotatedReturnType();
        Annotation returnTypeAnnotation = returnType.getAnnotation(ExpectedType.class);
        System.out.println("returnTypeAnnotation :");
        System.out.println(returnTypeAnnotation);
        System.out.println();

        AnnotatedType[] parameters = m.getAnnotatedParameterTypes();
        System.out.println("parametersAnnotation :");
        for (AnnotatedType p : parameters) {
            Annotation parameterAnnotation = p.getAnnotation(ExpectedType.class);
            System.out.println(parameterAnnotation);
        }
    }
}
/**
    * Output:
        returnTypeAnnotation :
        @Java8NewFeaturesTest101.ExpectedType(java.lang.String.class)

        parametersAnnotation :
        @Java8NewFeaturesTest101.ExpectedType(java.lang.String.class)
        @Java8NewFeaturesTest101.ExpectedType(boolean.class)
 */

7.2 Repeating Annotations

Before Java 8, we can only use container annotation if we want to reuse annotation for the same class.

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

public class Java8NewFeaturesTest102 {
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Role {
        String value();
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Roles {
        Role[] value();
    }

    @Roles({ @Role("admin"), @Role("user") })
    private static class Administrator {
    }

    public static void main(String[] args) {
        Roles roles = Administrator.class.getAnnotation(Roles.class);

        for (Role role : roles.value()) {
            System.out.println(role.value());
        }
    }
}
/**
 * Output:
 * admin
 * user
 */

Repeating annotation is helpful to repeat an annotation anywhere that we would use a standard annotation.

For compatibility reasons, repeating annotations are stored in a container annotation that is automatically generated by the Java compiler.

So when creating a repeating annotation, we still need to associate it to a container annotation.

import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

public class Java8NewFeaturesTest103 {
    @Repeatable(Roles.class)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Role {
        String value();
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Roles {
        Role[] value();
    }

    @Role("admin")
    @Role("user")
    private static class Administrator {
    }

    public static void main(String[] args) {
        Roles roles = Administrator.class.getAnnotation(Roles.class);

        for (Role role : roles.value()) {
            System.out.println(role.value());
        }
    }
}
/**
 * Output:
 * admin
 * user
 */

8. Iterable Interface

The Iterable interface was introduced in JDK 1.5 and it belongs to java.lang package.

It represents a data structure that can be iterated over and it provides a method that produces an Iterator.

In Java 8, a new default method forEach() is added to the Iterable Interface.

So to iterate over an iterable, we can use either its iterator or enhanced for loop or for each loop.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Java8NewFeaturesTest104 {

    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();

        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        iterateUsingIterator(numbers);

        System.out.println();

        iterateUsingEnhancedForLoop(numbers);

        System.out.println();

        iterateUsingForEachLoop(numbers);

        System.out.println();

        removeElementsUsingIterator(numbers);

        System.out.println(numbers.size());
    }

    public static void iterateUsingIterator(List<Integer> numbers) {
        Iterator<Integer> iterator = numbers.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

    public static void iterateUsingEnhancedForLoop(List<Integer> numbers) {
        for (Integer number : numbers) {
            System.out.println(number);
        }
    }

    public static void iterateUsingForEachLoop(List<Integer> numbers) {
        numbers.forEach(System.out::println);
    }

    public static void removeElementsUsingIterator(List<Integer> numbers) {
        Iterator<Integer> iterator = numbers.iterator();
        while (iterator.hasNext()) {
            iterator.next();
            iterator.remove();
        }
    }
}
/**
 * Output:
        1
        2
        3
        4
        5
        
        1
        2
        3
        4
        5
        
        1
        2
        3
        4
        5
        
        0
 */

It is possible to create a custom implementation of the iterable interface in order to iterate over a custom data structure.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Java8NewFeaturesTest105 {

    public static void main(String[] args) {
        Product p1 = new Product(1, "candy");
        Product p2 = new Product(2, "milk");

        List<Product> products = new ArrayList<>();
        products.add(p1);
        products.add(p2);

        ShoppingCart<Product> cart = new ShoppingCart<>(products);

        Iterator<Product> it = cart.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }

        System.out.println();

        for (Product p : cart) {
            System.out.println(p);
        }

        System.out.println();

        cart.forEach(System.out::println);

        System.out.println();

        it = cart.iterator();

        while (it.hasNext()) {
            System.out.println(it.next());
            it.remove();
        }

        System.out.println();
        System.out.println(products.size());
    }

    private static class Product {
        private int id;
        private String name;

        public Product(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "Product [id=" + id + ", name=" + name + "]";
        }
    }

    private static class ShoppingCart<E> implements Iterable<E> {
        private List<E> elements;

        public ShoppingCart(List<E> elements) {
            this.elements = elements;
        }

        @Override
        public Iterator<E> iterator() {
            return new Iterator<E>() {
                private int index = 0;

                @Override
                public boolean hasNext() {
                    return index < elements.size();
                }

                @Override
                public E next() {
                    E next = elements.get(index++);
                    return next;
                }

                @Override
                public void remove() {
                    elements.remove(--index);
                }
            };
        }
    }
}
/**
 * Output:
        Product [id=1, name=candy]
        Product [id=2, name=milk]
        
        Product [id=1, name=candy]
        Product [id=2, name=milk]
        
        Product [id=1, name=candy]
        Product [id=2, name=milk]
        
        0
 */

9. StringJoiner

Java 8 introduces a new class StringJoiner which can be used for joining Strings making use of a delimiter, prefix, and suffix.

import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;

public class Java8NewFeaturesTest106 {

    public static void main(String[] args) {
        String delimiter = ",";
        String prefix = "[";
        String suffix = "]";

        StringJoiner sj1 = new StringJoiner(delimiter, prefix, suffix);

        sj1.add("hello").add("world");

        System.out.println(sj1.toString());

        StringJoiner sj2 = new StringJoiner(delimiter, prefix, suffix);

        List<String> greetings = new ArrayList<>();

        greetings.add("hello");
        greetings.add("world");

        for (String greeting : greetings) {
            sj2.add(greeting);
        }

        System.out.println(sj2.toString());
    }
}
/**
 * Output:
 * [hello,world]
 * [hello,world]
 */

A Joiner without prefix and suffix returns an empty String whereas joiner with prefix and suffix returns a String containing both prefix and suffix.

To change the default String, we can use setEmptyValue() method and it is only returned when the StringJoiner is empty.

import java.util.StringJoiner;

public class Java8NewFeaturesTest107 {

    public static void main(String[] args) {
        String delimiter = ",";
        String prefix = "[";
        String suffix = "]";

        StringJoiner sj1 = new StringJoiner(delimiter, prefix, suffix);

        System.out.println(sj1.toString());

        StringJoiner sj2 = new StringJoiner(delimiter);

        System.out.println(sj2.toString());

        StringJoiner sj3 = new StringJoiner(delimiter, prefix, suffix);

        sj3.setEmptyValue("defaultValue");

        System.out.println(sj3.toString());

        StringJoiner sj4 = new StringJoiner(delimiter);

        sj4.setEmptyValue("defaultValue");

        System.out.println(sj4.toString());

        StringJoiner sj5 = new StringJoiner(delimiter, prefix, suffix);

        sj5.setEmptyValue("defaultValue");
        sj5.add("hello");

        System.out.println(sj5.toString());

        StringJoiner sj6 = new StringJoiner(delimiter);

        sj6.setEmptyValue("defaultValue");
        sj6.add("world");

        System.out.println(sj6.toString());
    }
}
/**
 * Output:
 * []
 *
 * defaultValue
 * defaultValue
 * [hello]
 * world
 */

It is possible to merge two joiners using merge() method.

If we use joiner_1.merge(joiner_2), it takes joiner_2 and appends its contents (without its prefix/suffix) into joiner_1, using the joiner_1’s delimiter.

import java.util.StringJoiner;

public class Java8NewFeaturesTest108 {

    public static void main(String[] args) {
        String prefix = "[";
        String suffix = "]";

        StringJoiner sj1 = new StringJoiner(",", prefix, suffix);

        sj1.add("a").add("b");

        System.out.println("sj1 = " + sj1.toString());

        StringJoiner sj2 = new StringJoiner(",", prefix, suffix);

        sj2.add("c").add("d");

        System.out.println("sj2 = " + sj2.toString());

        sj1.merge(sj2);

        System.out.println("sj1 after merging sj2 = " + sj1.toString());
        System.out.println("sj2 being mered by sj1 = " + sj2.toString());

        System.out.println();

        StringJoiner sj3 = new StringJoiner(",", prefix, suffix);

        sj3.add("a").add("b");

        System.out.println("sj3 = " + sj3.toString());

        StringJoiner sj4 = new StringJoiner("-", prefix, suffix);

        sj4.add("c").add("d");

        System.out.println("sj4 = " + sj4.toString());

        sj3.merge(sj4);

        System.out.println("sj3 after merging sj4 = " + sj3.toString());
        System.out.println("sj4 after being merged by sj3 = " + sj4.toString());
    }
}
/**
 * Output:
 * sj1 = [a,b]
 * sj2 = [c,d]
 * sj1 after merging sj2 = [a,b,c,d]
 * sj2 being mered by sj1 = [c,d]
 *
 * sj3 = [a,b]
 * sj4 = [c-d]
 * sj3 after merging sj4 = [a,b,c-d]
 * sj4 after being merged by sj3 = [c-d]
 */

Collectors.joining() internally uses StringJoiner to perform the joining operation.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Java8NewFeaturesTest109 {

    public static void main(String[] args) {
        List<String> greetings = new ArrayList<>();

        greetings.add("hello");
        greetings.add("world");

        String greeting = greetings.stream().collect(Collectors.joining(",", "[", "]"));

        System.out.println(greeting);
    }
}
/**
 * Output:
 * [hello,world]
 */