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 VERSION | RELEASE DATE | END OF SUPPORT |
|---|---|---|
| 8 | March 2014 | December 2030 |
| 11 | September 2018 | January 2032 |
| 17 | September 2021 | September 2029 |
| 21 | September 2023 | September 2031 |
| 25 | September 2025 | September 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]
*/