Learning JAVA 8 by Vipul Tyagi

Nishi Tiwari
9 min readNov 13, 2023

Why did it come into existence?

1) Concise and Minimal Code
2) To utilize functional programming
3) To enable parallel programming. More compatible code for multi-core processors.

Features:

1) Lambda Expressions: Similar to methods, but they do not need a name and can be implemented right in the body of a method.
Example: (x,y)->x+y

2) Stream API: For bulk Data Operations on Collections

3) Date and Time API: Under the package java.time, Java 8 offers a new date-time API

4) BASE64 Encode Decode: For Base64 encoding, Java 8 has built-in encode and decode functions. The Base64 encoding class in Java.util.Base64.

5) Method reference and constructor reference :: operator

6) Default methods in Interfaces: Default methods in interfaces allow you to define methods with a default implementation. This feature was introduced to facilitate the evolution of interfaces without breaking the existing implementations. Here’s how default methods work:

Purpose: To allow interfaces to provide default behavior for methods, enabling the addition of new methods to interfaces without requiring all implementing classes to provide an implementation.

Syntax: Use the default keyword followed by the method implementation within the interface.

Key Points:
Default methods help in adding new methods to interfaces without affecting existing implementations.
Implementing classes can override default methods if they need to provide a different behavior.

7) Static methods in Interface: Static methods in interfaces are methods that belong to the interface class, rather than to instances of the interface. They can be called without an object instance and are useful for providing utility or helper methods related to the interface.

Purpose: To provide a way to define utility methods that are relevant to the interface, which can be called directly on the interface itself.
Syntax: Use the static keyword before the method signature in the interface.

Key Points:
Static methods in interfaces are similar to static methods in classes.
They can be called using the interface name (e.g., MyInterface.staticMethod()).
Static methods in interfaces cannot be overridden by implementing classes, as they belong to the interface itself, not to the instances

8) Functional Interface: Has exactly one abstract method. To designate an interface as a Functional Interface, we don’t need to use @FunctionalInterface annotation.

9) Optional Class: The `Optional` class in Java 8 is a container object used to represent the presence or absence of a value. It is primarily used to avoid `null` checks and `NullPointerException`. By using `Optional`, developers can specify methods to handle both the presence and absence of a value explicitly, promoting cleaner and more readable code.

10) Java IO Improvements: Java 8 introduced several improvements to the IO (Input/Output) API, focusing on enhancing the performance and usability of file operations. Notable enhancements include the addition of the `java.nio.file` package with utility classes like `Files` and `Paths`, which provide more efficient and flexible ways to handle file operations, such as reading, writing, and manipulating file paths.

11) Collection API Improvements: The Collection API in Java 8 saw significant improvements, including the introduction of new methods for existing collections. One of the major additions was the `Stream` API, which allows for functional-style operations on collections, such as filtering, mapping, and reducing. Other enhancements include methods like `forEach`, `removeIf`, `replaceAll`, and `sort`, which provide more concise and expressive ways to work with collections.

Lambda Expression: Anonymous function
1) Not having any name

2) Not having any return type

3) Not having any modifier

Steps to make any function lambda expression:
1. Remove modifier

2. Remove the return type

3. Remove method name

4. Place arrow eg: ()-> {System.out.println(“Hello World”);}

Changes in Code:

  1. If the body has just one statement, then we can remove curly brackets
(String str)-> return str.length();

2. Use type inference, compiler guess the situation or context.

private void add(int a, int b){
System.out.println(a+b);
}

Converted to: (a, b) -> System.out.println(a+b);

3. No return keyword

private int getStringLength(String str){
return str.length();
}
Converted to: (str) -> str.length();

4. If only one param remove small brackets

(str)-> str.length();

Converted to: str -> str.length();

Benefits:

1. To enable functional programming in Java
2. To make code more readable, maintainable, and concise code
3. To enable parallel processing
4. JAR file size reduction
5. Elimination of shadow variables.

Functional Interface: Having exactly a single abstract method but can have any number of defaults and static methods. We can invoke lambda expression by using a functional Interface.

@FunctionalInterface
public interface MyInteerface
{
public void sayHello();
default void sayBye () {
};
public static void sayOk() {
};
}

What is the advantage of this annotation?
1. It restricts the interface to be a Functional Interface. So, if people have already used some lambda expressions and some new team member added another abstract method in the interface all lambda expressions will have errors.

Inheritance in Function Interface

public interface Parent 
{
public void sayHello();
}
@FunctionalInterface
public interface Child extends Parent
{
public void sayBye() //This will give error because now it contains 2 functional interface one in parent and one in child.
}

Default Methods in Interface:

interface Parent {
default void sayHello() {
System.out.println("Hello")
}
void sayBye();
}
class Child implements Parent {
@Override
public void sayHello(){
System.out.println("Child says Hello");
}
}
public class MyClass {
public static void main(String[] args){
Parent c = new Parent();
c.sayHello();
}
}

Now:

interface A {
default void sayHello() {
System.out.println("A says Hello");
}
}
interface B {
default void sayHello() {
System.out.println("B says Hello");
}
}
public class MyClass implements A, B {
public static void main(String[] args){
MyClass myClass = new MyClass();
myClass.sayHello();
}
@Override
public void sayHello() {
A.super.sayHello();
}
}
  1. Static Methods in Interface are those methods, which are defined in the interface with the keyword static.
  2. Static methods contain the complete definition of the function.
  3. Cannot be overridden or changed in the implementation class.
interface A {
static void sayHello() {
System.out.println("Hello !");
}
default void sayBye() {
System.out.println("Bye !");
}
}
public class MyClass implementd A {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.sayBye();
A.sayHello(); // This is how we call interface method.
}
}

Earlier (Employee Interface):

package org.example;
public interface Employee {
String getName();
}

Class:

package.org.example;
public class SoftwareEngineer implements Employee {
@Override
public String getName(){
return "Software Engineer";
}
}

Main:

package.org.example;
public class Main{
public statis void main(String[] args){
Employee employee = new SoftwareEngineer();
System.out.println(Employee.getName());
}
}

Now: (Note: Functional Interface act as type for lambda expression.)

package.org.example
public class Main {
public static void main(String[] args){
Employee employee = () -> "Software Enginner";
System.out.prinln(employee.getName());
}
}

Lambda expression is implementation of single abstract method inside a functional interface.

Interface reference can be used to hold lambda expression.

Using lambda expression, we don’t need to use any separate implementation class.

In Runnable only have one abstract method. It is used for creating threads.

package.org.example;
public class Main{
public static void main(String[] args){
Runnable runnable = () -> {
for(int i = 1; i <= 10; i++){
Sytem.out.println("Hello " + i)
}
};
Thread childThread = new Thread(runnable);
childThread.run();

Comparator in java using lambda expression

package.org.example;
import java.util.Arraylist;
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args){
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(6);
list.add(0);
list.add(99);
list.add(6);
Collections.sort(list, (a, b) -> a - b);
System.out.println(list);

}
}
package.org.example;
import java.util.Compartor;
public class MyClass implements Compartor<Integer> {
@Override
public int compare(Integer a, int b){
return b - a;
}
}

For custom sort we use collections.sort

public class Main {
public static void main(String[] args){
Map<Integer, String> m = new TreeMap<>();
m.put(2, "z");
m.put(3 "f");
m.put(1, "y");
System.out.println("Before manual Sorting: " + m);
Map<Integer, String> m = new TreeMap<>((a, b) -> b -a);;
m.put(2, "z");
m.put(3 "f");
m.put(1, "y");
System.out.println("After manual Sorting: " + mn);
}
}
static class Student {
public Integer id;
public String name;
public Student(Integer id, String name){
this.id = id;
this.name = name;
}
public String toString(){
return this.id + ": " + this.name;
}

}

Difference between lambda expression and anonymous inner class

Mastering Java 8 Predicates: AND, OR, and isEqual Explained with Examples

Prediacte: Boolean Valued Function

package org.example;
import java.util.function.Supplier;
public class Main{
public static void main(String[] args){
Supplier<Integer> supplier = () -> 1;
System.out.println(supplier.get());
}
}

****

package org.example;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.funtion.Predicate;
import java.util.function.Supplier;
public class Main{
public static void main(String[] args){
Predicate<Integer> predicate = x -> x % 2 == 0;
BiPredicate<Integer, Integer> biPredicate = (x, y) -> x % 2 == 0 && y % 2 == 0;
System.out.println(biPredicate,test(2, 4));)
Function<Integer> Integer function = x -> x * x;
Consumer<Integer> consumer = x -> System.out.println(x);
Supplier<Integer> supplier = () ->100;
if (predicate.test(supplier.get())){
consumer.accept(function.apply(Supplier.get()));
}
}

}

****

package org.example;
import java.util.function.BiPredicate;
public class Main {
public static void main(String[] args){
BiPredicate<String, Integer> biPredicate = (str, x) -> str.length() == x;
System.out.println(biPredicate.test("Nishi", 5));
}
}

****

package org.example;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args){
Function<String, Integer> function = str -> str.length();
BiFunction<String, String, Integer> bifunction = (x, y) -> x.length() + y.length();
System.out.println(biFunction.apply("Hi", "Hello"));
Consumer<Integer> consumer = (x) -> {
System.out.println(x);
};
//System.out.println(consumer.accept(67;);

BiConsumer<Intger, Intger>biConsumer = (x, y) -> {
System.out.println(x + y);
};
biConsumer.accpet(1, 2);
}
}

*****

package org.example;
import java.util.function.Supplier;
public class Main {
public static void main(String[] args){
Supplier<Integer> supplier = () -> 1;
System.out.println(supplier.get());
}
}

****

package org.example;
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class Main {
public static void main(String[] args){
Function<Integer, Integer> function1 = x -> x * x;
Function<String, String> function3 = str -> str.toLowerCase();
UnaryOperator<String> unaryOperator = str -> str.toLowerCase();
UnaryOperator<Integer> unaryOperator = x -> x * x; ;
System.out.println(unaryOperator.apply(t));
}
}

*****

package org.example;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
//import java.util.function.Function;
//import java.util.function.UnaryOperator;
public class Main {
public static void main(String[] args){
BiFunction<String, String, String> biFunction = (str1, str2) -> str1 + str2;
BinaryOperator<String> binaryOperator = (str1, str2) -> str1 + str2;
System.out.println(binaryOperator.apply("Hello", "World"));
}
}

*****

Method references allow us to refer to a method without invoking it, making our code cleaner and more readable. Thye can be used in place of a lambda expression when the lambda expression only calls an existing method.

Streams:

Readability: Streams provide a more readable and expressive way to perform operations on collections. The syntax is concise and easy to understand, making it easier for other developers to read and maintain your code.

Flexibility: Streams allow you to perform a variety of operations on collections, such as filtering, mapping, and reducing, without having to write multiple loops or methods. This can save time and make your code more flexible.

Parallelism: Streams can be processed in parallel, which can provide a significant performance boost for large collections. With a for loop, you would have to write your own multi-threaded code to achieve parallelism.

Encapsulation: Streams encourage encapsulation, as you can perform a series of operations on a collection without modifying the original data. This can help prevent bugs and improve code reliability.

package org.example;
import java.util.Arrays;
import java.util.List;
public class Test {
public static void print(String s){
System.out.println(s);
}
public static vpid main(String[] args) {
List<String> students = Arrays.asList("Alice", "Bob", "Charile");
students.forEach(Test::print);
System.out.println(hello());
}
private static int hello() {
return 1;
}
public void print(String s){
System.out.println(s);
List<Students> students = names.stream().map(x -> new Student(x)).collect(Collectors.toList());
}
}
public class Student {
private String name;
public Student(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name){
this.name = name;
}
}
public class Test {

public static void main(String[] args) {
//imperative approach
int[] array = {1, 2, 3, 4, 5};
int sum = 0;
for(int i = 0; i < array.length; i++){
if(array[i] % 2 == 0){
sum += array[i];
}
}
//Stream
int[] array2 = {1, 2, 3, 4, 5};
int sum2 = Arrays.stream(array2).filter(n -> n % 2 == 0)
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Stream;
public class Test {

public static void main(String[] args) {
List<String> list = Arrays.ssList("apple", "banana", "cherry");
Stream<String> myStream = list.stream();
String[] array = {"apple", "banana", "cherry" };
Stream<String> stream = Arrays.stream(array);
Stream<Integer> intgerStream = Stream.of(1, 2, 3);
Stream<Integer> limit = Stream.iterate(0, n -> n + 1).limit(100);
Stream<Integer> limit1 = Stream.generate(() -> (int) Math.random() * 100).limit(5);
}
}
import java.util.stream.Stream;
public class Test {

public static void main(String[] args){

List<Integer> collect = Stream.iterate(0, x -> x + 1)
.limit(maxSize: 101)
.skip(1)
.filter(x -> x % 2 == 0)
.map(x -> x /10)
.distinct()
.sorted()
.peek(x -> System.out.println(x))
.collect(Collectors.toList());
List<Integer> collect = Stream.iterate(0, x -> x + 1)
.limit(maxSize: 101)
.skip(1)
.filter(x -> x % 2 == 0)
.map(x -> x /10)
.distinct()
.sorted()
.collect(Collectors.toList());
}
}

Issues with Legacy Date and Calendar Classes:

  1. Mutable 2) Confusing 3) Limited

1.LocalDate: Represents a date without a time zone.

2. LocalTime: Represents a time without a date or time zone.

3.LocalDateTime: Represents a date and time without a time zone.

4. ZonedDateTime: Represents a date and time with a time zone.

5. Instant: Represents an instantaneous point on the timeline, typically used for machine timestamps.

6. Duration: Represents a duration of time between two points in time.

7. Period: Represents a period of time between two dates.

8.DateTimeFormatter: Formats and parses dates and times.

package org.example;
public class Main {
public static void main(String[] args) {
Employee employee = new Employee() {
@Override
public String getSalary() {
return *100;
}
@Override
public String getDesignation() {
return "Software Enginner";
}
};
System.out.println(employee.getSalary());
}
}
Employee.java (File)
pacakage class Main{
//int a = 2;
int x = 10;
private void doSomething() {
//a = 3;
Employee employee = () -> {
a = 3;
return "100";
};
System.out.println(employee.getSalary());
Employee employee1 = new Employee() {
int x = 10;
@Override
public String getSalary() {
System.out.println(this.x);
return "100";
}
};
}
}
package org.example;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args){
Consumer<String> consumer = s -> System.out.println(s);
Consumer<List<Integer>> listConsumer = li -> {
for(Integer i : li){
System.out.println(i + 100);
}
};
Consumer<List<Integer>> listConsumer2 = li -> {
for(Integer i : li){
System.out.println(i + 100);
}
};
Consumer<List<Integer>> listConsumer = listConsumer2.andThen(listConsumer1);
ListConsumer2.andThen(listConsumer1).accept(Arrays.asList(1, 2, 3, 4));
}

}

--

--