Java 8 Features

Photo by James Orr on Unsplash

Java 8 Features

What's the Purpose of Java 8 ?

  • Concise and minimal code

  • utilize functional programming benefits

  • parallel programming to utilize multi core processors

Features in java8

  • Functional Interface

  • Lambda Expression

  • Method Reference and Constructor Reference

  • Stream API

  • Date and Time API

  • Optional class

  • Base 64 Encode and Decode

Functional Interface

Any interface having Exactly one abstract method but can have multiple default and static methods.

We can implement/invoke that abstract method using Lambda Expression.

Lets look at some interfaces:

Below interface is a functional interface as it has exactly one abstract method

interface Adder{
    // It is a functional interface
    public int add(int a, int b);
}

Below interface is a functional interface as it has exactly one abstract method. But you can see there is an annotation @FunctionalInterface , this annotation tells the compiler to throw an compile time error if it is not a functional interface, that means if the interface doesn't own exactly one abstract method then you will get a CTE.

@FunctionalInterface
interface Printer{
    // It is a functional interface
    public void say(String content);
}

Below interface is a functional interface as it has exactly one abstract method, and it doesn't matter how many default or static method it has.

interface Worker{
    // It is a functional interface
    public void work();
    default boolean isWorking(){
        return true;
    }
    static boolean isWorkDone(){
        return true;
    }
}

Below interface is not a functional interface because it has two abstract method.

interface Starter{
    // It is not a functional interface
    public void start();
    public boolean isStarted();
}

Lambda Expression

Lambda expression is basically used to implement the functional interface in efficient way.

Lets understand this with example, We have below two functional interface.

interface Adder{
    // It is a functional interface
    public int add(int a, int b);
}

@FunctionalInterface
interface Printer{
    // It is a functional interface
    public void say(String content);
}

Now a old java user will either create a new class and implement it or create an anonymous class and implement the abstract methods like below.

        Adder adder  = new Adder() {
            @Override
            public int add(int a, int b) {
                return a+b;
            }
        };
        Printer printer = new Printer() {
            @Override
            public void say(String content) {
                System.out.println(content);
            }
        };

but java 8 is smart

he says, we already are writing the Interface name as reference type so there is no point of writing it again as "new Adder" or "new Printer" so remove it i will understand which interface you are implementing.

Then again he says, it has only one abstract method so its obvious that you will implement that only so no point writing the method name, so remove it and just put a lambda expression (->).

Lets follow what ever java 8 is asking us to do, and our implementation will look like below.

        Adder adder  = (int a, int b) -> {
                return a+b;
        };
        Printer printer = (String content) -> {
            System.out.println(content);
        };

wait, just now java 8 called me and said "you have all ready mentioned the data type/reference type of the method parameters in the interface please remove it bro" so let me try that.

        Adder adder  = (a,b) -> {
                return a+b;
        };
        Printer printer = (content) -> {
            System.out.println(content);
        };

Now java 8 texted me "you have only one line of code, why the formalities with those curly brackets and also you have return type so that one line will be returned. that is so obvious i am not a dumbo please remove it"

so after all the modification it looks like this

Adder adder = (a,b) -> a+b;
Printer printer = str -> System.out.println(str);

The whole code looks like this:

public class LambdaExpression {
    public static void main(String[] args) {
        Adder adder = (a,b) -> a+b;
        Printer printer = str -> System.out.println(str);

        System.out.println(adder.add(1,2));
        printer.say("hello");
    }
}
interface Adder{
    // It is a functional interface
    public int add(int a, int b);
}
@FunctionalInterface
interface Printer{
    // It is a functional interface
    public void say(String content);
}

Now we understood that we can use lambda expression to implement the abstract method of a functional interface but how often do we create interfaces ? may be 10% of time or less. But the thing is, Java already has so many predefined functional interface such as Runnable, Comparator and etc.Lets see them.

Runnable :

        //Runnable interface is used to create threads
        /*Life before java 8 : create a class implementing Runnable and create object of that class and then create thread using that object,
         Or else create an anonymous class implementing Runnable and create thread as below. */
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i<=10; i++ )
                    System.out.println(i);
            }
        });
        thread1.start();

        /*Life After java 8: As Runnable is a functional interface we can directly pass the lambda expression*/
        Thread thread2 = new Thread(() -> {
            for (int i = 1; i<=15; i++ )
                System.out.println(i);
        });
        thread2.start();

Comparator :

        //Comparator is also a functional interface used to sort Collections
        List<Integer> integers = new ArrayList<>();
        integers.add(56);
        integers.add(90);
        integers.add(36);
        integers.add(26);
        integers.add(106);
        integers.add(6);
        System.out.println(integers);

        //Life before Java 8
        Comparator<Integer> integerComparatorAsc = new Comparator<Integer>() {
            @Override
            public int compare(Integer a, Integer b) {
                return a-b;
            }
        };
        Collections.sort(integers,integerComparatorAsc);
        System.out.println(integers);

        //Life After java 8
        //Comparator<Integer> integerComparatorDesc = (a, b) -> b-a;
        //Collections.sort(integers,integerComparatorDesc);
        Collections.sort(integers,(a, b) -> b-a);
        System.out.println(integers);

Predefined FI:

import java.util.Arrays;
import java.util.List;
import java.util.function.*;

public class PredefinedFI {
    public static void main(String[] args) {
        System.out.println("===========Predicate FI===========");
        predicateFI();

        System.out.println("===========BiPredicate FI===========");
        biPredicateFI();

        System.out.println("===========Function FI===========");
        FunctionFI();

        System.out.println("===========BiFunction FI===========");
        biFunctionFI();

        System.out.println("===========Consumer FI===========");
        ConsumerFI();

        System.out.println("===========BiConsumer FI===========");
        biConsumerFI();

        System.out.println("===========Supplier FI===========");
        SupplierFI();

        System.out.println("===========UnaryOperator FI===========");
        UnaryOperatorFI();

        System.out.println("===========UnaryOperator FI===========");
        BinaryOperator();
    }

    private static void BinaryOperator() {
        //BinaryOperator FI is a child of BiFunction FI, speciality os this FI is the two method arguments and the return type is same in BinaryOperator
        BinaryOperator<Integer> multiplier = (num1,num2) -> num1*num2;
        System.out.println("Multiplication of 50 and 40 is :"+multiplier.apply(50,40));
    }

    private static void UnaryOperatorFI() {
        //UnaryOperator FI is a child of Function FI, specialty os this FI is the return type and method argument is same in UnaryOperator
        UnaryOperator<Integer> square = number -> number*number;
        System.out.println("square of 90 = " + square.apply(90));
    }

    private static void biConsumerFI() {
        //BiConsumer FI is similar to Consumer, which takes any two parameter and returns nothing
        BiConsumer<Integer,Integer> printAreaOfRectangle = (length,width) -> System.out.println(length*width);
        printAreaOfRectangle.accept(12,8);
    }

    private static void biFunctionFI() {
        //BiFunction FI is similar to Function, which takes any two parameter and returns something
        BiFunction<Integer,Integer,Integer> multiplier = (num1,num2) -> num1*num2;
        System.out.println(multiplier.apply(10,8));
        System.out.println(multiplier.apply(15,5));
    }

    private static void biPredicateFI() {
        //BiPredicate FI is similar to Predicate, which takes two parameter and returns boolean
        BiPredicate<String,Integer> confirmStringLength = (str,nbr) -> str.length()==nbr;
        System.out.println(confirmStringLength.test("hemant",6));
        System.out.println(confirmStringLength.test("hemant",7));
    }

    private static void SupplierFI() {
        //Supplier FI has one abstract method 'get', which takes zero parameter and returns something
        Supplier<Integer> supplier = () -> 100;
        System.out.println(supplier.get());
        //it has no default or static method
    }

    private static void ConsumerFI() {
        //Consumer FI has one abstract method 'accept', which takes one parameter and returns nothing
        Consumer<String> printString = string -> System.out.println("string = " + string);
        printString.accept("Asur");

        Consumer<List<Integer>> printListOfIntegers = integers -> {
            System.out.print("{ ");
            for (Integer integer : integers) {
                System.out.print(integer + " ");
            }
            System.out.println("}");
        };
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
        printListOfIntegers.accept(numbers);

        //use default methods of Consumer FI i.e andThen
        Consumer<List<Integer>> printifOdd = integers -> {
            System.out.print("Odd numbers : { ");
            for (Integer integer : integers) {
                if (integer % 2 != 0)
                    System.out.print(integer + " ");
            }
            System.out.println("}");
        };
        printListOfIntegers.andThen(printifOdd).accept(numbers);
    }

    private static void FunctionFI() {
        //Function FI has one Abstract method 'apply', which takes one any parameter and returns any value
        Function<Integer, Integer> doubler = number -> number * 2;
        Function<Integer, Integer> square = number -> number * number;
        System.out.println("double of 5 is : " + doubler.apply(5));
        System.out.println("square of 5 is : " + square.apply(5));

        //Find avg of a list of Integer using Function FI
        Function<List<Integer>, Double> average = numbers -> {
            double sum = 0.0;
            for (Integer number : numbers)
                sum = sum + number;
            return sum / numbers.size();
        };
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        System.out.println("avg is : " + average.apply(numbers));

        //use default methods of Function FI i.e andThen , compose
        System.out.println("use andThen method :");
        System.out.println("find double of 5 and then square  it: " + doubler.andThen(square).apply(5));
        System.out.println("find square of 5 and then double  it: " + square.andThen(doubler).apply(5));

        System.out.println("use compose method :");
        System.out.println("find double of 5 and then square  it: " + square.compose(doubler).apply(5));
        System.out.println("find square of 5 and then double  it: " + doubler.compose(square).apply(5));

        //use static method of Function FI i.e. identity, it just creates a Function which returns same thing which is sent as parameter
        System.out.println("use identity method :");
        Function<Integer, Integer> identity = Function.identity();
        System.out.println(identity.apply(5));
    }

    private static void predicateFI() {
        //Predicate FI has one Abstract method which takes one parameter and returns boolean
        Predicate<Integer> isOverPaidSalary = salary -> salary > 100000;
        if (isOverPaidSalary.test(50000))
            System.out.println("yes, salary 50000 is overpaid");

        //use default methods of Predicate FI i.e and , or , negate
        Predicate<Integer> isUnderPaidSalary = isOverPaidSalary.negate();
        if (isUnderPaidSalary.test(50000))
            System.out.println("yes, salary 50000 is underpaid");

        Predicate<Integer> isPerfectSalary = salary -> salary % 1000 == 0;
        Predicate<Integer> isPerfectAndUnderPaidSalary = isPerfectSalary.and(isUnderPaidSalary);
        if (isPerfectAndUnderPaidSalary.test(50000))
            System.out.println("yes, salary 50000 is underpaid&perfect");
        if (!isPerfectAndUnderPaidSalary.test(50111))
            System.out.println("No, salary 50111 is not underpaid&perfect");
    }
}

/*
===========Predicate FI===========
yes, salary 50000 is underpaid
yes, salary 50000 is underpaid&perfect
No, salary 50111 is not underpaid&perfect
===========BiPredicate FI===========
true
false
===========Function FI===========
double of 5 is : 10
square of 5 is : 25
avg is : 3.0
use andThen method :
find double of 5 and then square  it: 100
find square of 5 and then double  it: 50
use compose method :
find double of 5 and then square  it: 100
find square of 5 and then double  it: 50
use identity method :
5
===========BiFunction FI===========
80
75
===========Consumer FI===========
string = Asur
{ 1 2 3 4 5 6 7 }
{ 1 2 3 4 5 6 7 }
Odd numbers : { 1 3 5 7 }
===========BiConsumer FI===========
96
===========Supplier FI===========
100
===========UnaryOperator FI===========
square of 90 = 8100
===========UnaryOperator FI===========
Multiplication of 50 and 40 is :2000
*/

Method Reference and Constructor Reference

Now we understood that functional interface exactly one abstract method and we can provide implementation using lambda expression(->).

Now Lets say we have an existing method whose method parameter and return type is same as the abstract method of FI and also the implementation is same as our requirement, then java 8 says don't implement the abstract method just refer the existing method with symbol and rest i will take care. Its called method reference.

Same goes for constructor, if the method parameters of constructor and abstract method are same and the abstract methos returns same abject as the new object we want to create then we can use constructor reference.


import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class MethodConstructorReference {
    public static void main(String[] args) {
        MethodReference();
        ConstructorReference();
    }

    private static void ConstructorReference() {
        //get Person object from person names

        //without Constructor reference
        List<String> strings = Arrays.asList("hemant", "hari", "himesh", "harish", "harshit");
        Function<String, Person> getPersonFromName = str -> new Person(str);
        List<Person> persons = transformStringtoPerson(strings, getPersonFromName);
        System.out.println(persons);


        //with Constructor reference, transformStringtoPerson(-,-) takes a Function which takes String as input and returns Person Object as output.
        // So in java 8 we can refer another constructor which does same thing, takes String as input arg and creates Person object
        List<String> names = Arrays.asList("Darshan", "dinesh", "dharma", "daya", "daman");
        List<Person> personList = transformStringtoPerson(strings, Person::new);
        System.out.println(personList);
    }

    static class Person {
        private String name;
        public Person(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }

    private static void MethodReference() {
        // convert list of String to uppercase

        //without method reference
        Function<String, String> toUpper = string -> string.toUpperCase();
        List<String> strings = Arrays.asList("hemant", "hari", "himesh", "harish", "harshit");
        strings = transformStringList(strings, toUpper);
        System.out.println(strings);

        //wit method reference, transformStringList(-,-) takes a Function which takes String as input and returns String as output.
        // So in java 8 we can refer another method which does same thing, takes String as input and returns string output as an implementations
        List<String> names = Arrays.asList("Darshan", "dinesh", "dharma", "daya", "daman");
        names = transformStringList(names, String::toUpperCase);
        System.out.println(names);
    }

    private static List<String> transformStringList(List<String> list, Function<String, String> function) {
        List<String> transformedList = new ArrayList<>();
        for (String item : list) {
            transformedList.add(function.apply(item));
        }
        return transformedList;
    }

    private static List<Person> transformStringtoPerson(List<String> list, Function<String, Person> function) {
        List<Person> transformedList = new ArrayList<>();
        for (String item : list) {
            transformedList.add(function.apply(item));
        }
        return transformedList;
    }
}

/*
[HEMANT, HARI, HIMESH, HARISH, HARSHIT]
[DARSHAN, DINESH, DHARMA, DAYA, DAMAN]
[Person{name='hemant'}, Person{name='hari'}, Person{name='himesh'}, Person{name='harish'}, Person{name='harshit'}]
[Person{name='hemant'}, Person{name='hari'}, Person{name='himesh'}, Person{name='harish'}, Person{name='harshit'}]
*/

Stream API

We can create stream out of any collection or array, Once we get the stream we can write declarative/functional coding on it to do desired operation on it.

Benefit of stream:

  • Readability

  • Concise

  • Parallelism

How to create stream:

Example:

       //Without Stream
        int sum=0;
        int[] arr = {1,2,3,4,5,6,7,8,8,9,9,10};
        for(int i=0; i<arr.length; i++){
            if(arr[i]%2 == 0)
                sum = sum + arr[i];
        }
        System.out.println("sum : " + sum);

        //with stream
        int newSum = Arrays.stream(arr).filter(x -> x%2 == 0).sum();
        System.out.println("sum : " + newSum);

in above example Arrays.stream(arr) creates a stream from array, then we call filter(-) method which takes an predicate to filter the data and then we are calling sum which adds up the filtered data. Easy peasy.

Like filter, we can do many more operations like below:

  1. Intermediate Operations(returns stream for reuse):

    • filter(Predicate): Filters elements based on a predicate.

    • map(Function): Transforms each element using the provided function.

    • flatMap(Function): Transforms each element into a stream and then flattens the resulting streams into a single stream.

    • distinct(): Removes duplicate elements from the stream.

    • sorted(): Sorts the elements of the stream.

    • limit(long): Limits the size of the stream to the specified number of elements.

    • skip(long): Skips the specified number of elements from the beginning of the stream.

  2. Terminal Operations(does not return stream):

    • forEach(Consumer): Performs an action for each element in the stream.

    • collect(Collector): Collects the elements of the stream into a collection.

    • reduce(BinaryOperator): Reduces the elements of the stream to a single value.

    • count(): Counts the number of elements in the stream.

    • min(Comparator): Finds the minimum element in the stream.

    • max(Comparator): Finds the maximum element in the stream.

    • anyMatch(Predicate): Checks if any element in the stream matches the given predicate.

    • allMatch(Predicate): Checks if all elements in the stream match the given predicate.

    • noneMatch(Predicate): Checks if none of the elements in the stream match the given predicate.

  3. Short-circuiting Operations(do not need to process entire stream):

    • findFirst(): Returns the first element of the stream.

    • findAny(): Returns any element of the stream.

    • anyMatch(), allMatch(), noneMatch(): Short-circuits when a matching element is found.

Example:

Code Example:

package com.hk.corejava.java8;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamAPI {

    public static void main(String[] args) {
        demo();
        createStream();
        playWithStream();

    }

    private static void playWithStream() {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 33, 3, 2, 2, 6, 2, 2, 3, 23, 2, 3, 23, 3332, 3, 2, 32, 2, 3244232);
        List<Integer> newList = list.stream()
                .filter(x -> x % 2 == 0)
                .map(x -> x / 2)
                .peek(System.out::println)
                .skip(1)
                .limit(10)
                .sorted((a, b) -> b - a)
                .peek(System.out::println)
                .collect(Collectors.toList());

        System.out.println(newList);
    }

    private static void createStream() {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 76);
        Stream<Integer> numberStream = numbers.stream();

        String[] arr = {"hemant", "kumar", "Besra"};
        Stream<String> stringStream = Arrays.stream(arr);

        Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6, 6);

        //1st arg: initial point, 2nd arg is FI of Function which will be used to find next element
        Stream<Integer> limit = Stream.iterate(0, x -> x + 1).limit(100);

        //generate method takes FI Supplier which supplies the data for stream
        Stream<Integer> limit1 = Stream.generate(() -> (int) Math.random()).limit(5);
    }

    private static void demo() {
        //Without Stream
        int sum = 0;
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10};
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] % 2 == 0) sum = sum + arr[i];
        }
        System.out.println("sum : " + sum);

        //with stream
        int newSum = Arrays.stream(arr).filter(x -> x % 2 == 0).sum();
        System.out.println("sum : " + newSum);
    }
}

Date and Time API

Why do we need new Date and Time API ?

  • old date and time API (java.util.Date and Calendar) are not thread safe

  • not immutable

  • limited functionality when it comes to zones

Code:


import java.time.*;
import java.time.format.DateTimeFormatter;

public class DateAndTime {
    public static void main(String[] args) {
        localDate();
        localTime();
        localDateTime();
        ZonedDateTime();
        instantDurationPeriod();
        dateTimeFormatter();
    }

    private static void dateTimeFormatter() {
        LocalDate now = LocalDate.now();
        DateTimeFormatter myFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        String today = now.format(myFormatter);
        System.out.println("today = " + today);


        String bDay="16/10/1996";
        LocalDate parse = LocalDate.parse(bDay,myFormatter);
        System.out.println("parse = " + parse);
    }

    private static void instantDurationPeriod() {
        Instant now = Instant.now();//UTC Clock
        System.out.println("now = " + now);

        ZonedDateTime newYorkTime = now.atZone(ZoneId.of("America/New_York"));
        System.out.println("newYorkTime = " + newYorkTime);

        Instant now1 = Instant.now();

        Duration d1 = Duration.between(now, now1);
        System.out.println("d1 = " + d1);


        LocalDate date1 = LocalDate.of(1996,10,16);
        LocalDate today = LocalDate.now();
        Period p = Period.between(date1,today);
        System.out.println("p = " + p);
    }

    private static void ZonedDateTime() {
        //Time with zone
        ZonedDateTime now = ZonedDateTime.now();
        System.out.println("now = " + now);//now = 2024-03-25T13:54:08.897+05:30[Asia/Calcutta]

        System.out.println("Zones: \n" + ZoneId.getAvailableZoneIds());

        ZonedDateTime kolkataTime = ZonedDateTime.of(1996, 10, 16, 05, 30, 30, 30, ZoneId.of("Asia/Kolkata"));
        System.out.println("kolkataTime = " + kolkataTime);

        ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
        System.out.println("newYorkTime = " + newYorkTime);

        ZoneId zone = now.getZone();
        System.out.println("zone = " + zone);
    }

    private static void localDateTime() {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("now = " + now);

        LocalDateTime parsedDateTime = LocalDateTime.parse("2013-01-02T13:18");
        System.out.println("parsedDateTime = " + parsedDateTime);

        LocalDateTime beforeOneHour = parsedDateTime.minusHours(1);
        System.out.println("beforeOneHour = " + beforeOneHour);
    }

    private static void localTime() {
        LocalTime now = LocalTime.now();
        System.out.println("now = " + now);

        LocalTime beforeOneHour = now.minusHours(1);
        System.out.println("beforeOneHour = " + beforeOneHour);

        LocalTime localTime = LocalTime.of(14, 50, 30);
        System.out.println("localTime = " + localTime);

        LocalTime parsedTime = LocalTime.parse("15:30:30");
        System.out.println("parsedTime = " + parsedTime);
    }

    private static void localDate() {
        LocalDate now = LocalDate.now();
        System.out.println("now = " + now);
        System.out.println("getMonth: "+now.getMonth());
        System.out.println("getMonthValue : "+now.getMonthValue());
        LocalDate yesterday = now.minusDays(1);
        System.out.println("yesterday = " + yesterday);

        if (now.isAfter(yesterday))
            System.out.println("Yes brother..!");

        LocalDate bDay  = LocalDate.of(1996, 10, 16);
        System.out.println("bDay = " + bDay);
    }
}

Optional class

Optional class helps us from NullPointerException

Example:

import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Scanner;

public class OptionalJava8 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("Enter id : ");
        int i = sc.nextInt();

        try {
            //Without Optional
            String name = getNamefromDB(i);
            System.out.println(name.toUpperCase());
        } catch (Exception e) {
            System.out.println("e.getMessage() = " + e.getMessage());
        }


        //With Optional
        System.out.println("Enter id : ");
        int id = sc.nextInt();
        Optional<String> data = getDatafromDB(id);

        if(data.isPresent())System.out.println("data = " + data);
        data.ifPresent(System.out::println);

        System.out.println(data.orElse("NA"));

        try {
            System.out.println(data.orElseThrow(NoSuchElementException::new));
        } catch (NoSuchElementException e) {
            System.out.println("e.getMessage() = " + e.getMessage());
        }
    }

    private static Optional<String> getDatafromDB(int id) {
        String name = (id < 100) ? "ram" : null;
        return Optional.ofNullable(name);
    }

    private static String getNamefromDB(int i) {
        // lets assume we have 100 records in db, id id is greater then 100 then return null
        return (i < 100) ? "ram" : null;
    }
}

Did you find this article valuable?

Support Hemant Besra by becoming a sponsor. Any amount is appreciated!