What's new in Java 8 (Aggregation of all the features in a single Post)


Hi, I am Malathi Boggavarapu working at Volvo Group and i live in Gothenburg, Sweden. I have been working on Java since several years and had vast experience and knowledge across various technologies. In this course we will learn the features of Java8. As you all know Java8 got major changes like Lambda expressions, Streams and also changes in Date API.

So let's get started with Lambda expressions.

Introduction to Lambda expressions


Lets start with FileFilter example.

public interface Filefilter{
    boolean  accept(File file);
}

public class JavaFileFilter implements FileFilter{
    boolean accept(File file){
         return file.getName().endsWith(".java")
   }
}

JavaFileFilter fileFilter = new JavaFileFilter();
File dir = new File("d:/tmp");
File[] files = dir.listFiles(fileFilter);


2)

FileFilter fileFilter = new FileFilter(){
      boolean accept(File file){
         file.getName().endsWith(".java");
     }
};

File dir = new File("d:/tmp");
File[] files = dir.listFiles(fileFilter);

Purpose of Lambda expressions

It is an another way of writing instances of anonymous classes and to make instances of anonymous classes easier to write and read!

Example 1:

FileFilter filter = (File file) -> file.getName().endsWith(".java");

If more than one arguments then,

FileFilter filter = (File file, boolean test) -> {
   file.getName().endsWith(".java");
   println("test variable is :: " +test);
}

Example 2:

Runnable runnable = new Runnable(){
      public void run(){
           for(int i = 0; i < 10; i++){
               println("i : " +i);
           }
     }
}

Runnable runnableLambda = () -> {
      for(int i = 0; i < 10; i++){
               println("i : " +i);
     }
};

Type of lambda expression?

Type is a functional interface - interface with only one abstract method
Ex: public interface Runnable{
 run();
};
interface comparator(){
  int compareTo();
}

Methods from object class don't count. If we declare some method from Object class, we need to define some documentation for that abstract method in the functional interface.

Functional interface can be annotated with @FunctionalInterface and it is optional. .
The compiler will check whether interface is functional or not if we annotate it. If the interface is not a FunctionalInterface then it throws an exception and the code does not compile

Can i put lambda in variable?

Yes! Lambda expressions can be stored in a variable and pass them as arguments to methods and return them as return values.

Is a lambda expression an object?

If we see above examples, we have anonymous class and lambda expression. Anonymous class is created using keyword new. When we create an object with new keyword, it allocates some memory to the object, initiate static variables and call a constructor and so on. There is lot of overhead work done by the JVM here but in case of lambda all this overhead work does not exists and provide better performance than anonymous classes.

It is a complex question to answer. JVM does not create a new object with lambda expression and the answer is No but not a clear No. Because it is recorded as an object inside the JVM and  it is actually a new kind of object defined in java 8. Lambda is an object without an identity.

Lambda expression should not be used as an object.

Method references

Method references are the other way of writing lambda expression that is strictly equivalent to the normal way as writing lambda expressions.

Examples

1) This lambda expression:
Consumer<String> c = s-> System.out.println(s);

Can be written like this
Consumer<String> c =  System::out.println;

2) This lambda expression
Comparator<Integer> c = (i1, i2) -> Integer.compare(i1, i2);

can be written like this
Comparator<Integer> c = Integer::compare;

Processing data objects using Lambda expressions and Stream API

Usually objects will be stored in Collection (List, Map, Set) most of the time. So we will see here how the objects are accessed using lambda expressions.

List<Customer> list = new ArrayList<Customer>();

1) list.forEach(customer -> System.out.println(customer));
2) list.forEach(System.out::println(customer));

Performance is same for both the above ways. Only readability matters

forEach is a method in Iterable interface. Well..in java8 we can add method implementation inside the interface. It allows to change the old interfaces without breaking the existing implementations. If we see forEach method in Iterable interface, according to Java7 it should be implemented in all classes which implements Iterable. But that is not possible to do. Though it is possible for jdk developers to change all the implementation classes, it does not work with custom Iterable implementation classes that exists in applications.

And by the way static methods are also allowed in interfaces. Earlier only static final variables are only allowed but now static methods are allowed too.

public interface Iterable<E>{
    default void forEach(Consumer<E> consumer){
          for(E e : this){
              consumer.accept(e);
          }
    }
}

New patterns

Predicate : Is the functional interface inside java.util.Function
Predicate<String> p1 = < -> s.length() < 20;
Predicate<String> p2 = < -> s.length() > 10;

Predicate<String> p3 = p1.and(p2);

"and" is the default method inside the functional interface of Predicate.

Example

List<String> strings = Arrays.asList("one", "two", "Three");
List<String> result = new ArrayList<String>();

//Consumer<String> c1 = s -> System.out.println(s);
Consumer<String> c1 = System.out::println(c1); // Method reference

//Consumer<String> c2 = s -> result.add(s);
Consumer<String> c2 = result::add; // Method reference

strings.forEach(c1.andThen(c2));

Here addThen is the default implementation method of Consumer functional interface.

Stream and Collectors

Stream : It is a new concept. It gives ways to efficiently process large amounts of data and also smaller amounts of data. Data can be processed automatically in parallel to leverage the computing power of multi core CPU's.

What is a Stream?


- Object on which one can define operations.Operations can be Map, Filter or Reduce operations
- An object that does not hold any data. For example, in Collection i have data and do basic operations on it. On stream i don't have any data.
- An object that should not change the data it processes. Because it is distributed across multiple CPU's, since we don't want to be bothered with any visibility issues with atomic variable, volatile variable or synchronization.
- Object able to process data in one pass. For example Stream defined on collection of Person and define 3 operations Map, Reduce, Filter and in order to be processed effeciently it should not be stored in any of intermediate collection and should be processed in one Pass.
- Should be optimized from algorithm point of view and able to process data in parallel.

How can we build a Stream?

Stream method is added to collection interface

List<Person> persons = --
Stream<Person> stream = persons.stream();
stream.forEach(c -> System.out.println(c));

Filter operation

List<Person> persons = --
Stream<Person> stream = persons.stream();
Stream<Person> filtered = stream.filter(person -> person.getAge() > 20);
Predicate p = person -> person.getAge() > 20;

Predicate is a FunctionalInterface which exists in java.util.function.
Predicate interface with default methods

@FunctionalInterface
public interface Predicate<T>{
   boolean test(T t);
default Predicate<T> and(..)
default Predicate<T> or(..)
default Predicate<T> negate(..){..}
static Predicate isEqual(Object o){..}
}

Example

Predicate<String> p = Predicate.isEqual("two");
Stream<String> stream1 = Stream.of("one", "two", "three");
Stream<String> stream2 = stream1.filter(p);

 'of' is static method of Stream interface and is a way to create a Stream.

Example

Stream stream = Stream.of("one", "two", "three", "four", "five");
Predicate<String> p1 = p -> p.length() > 3;
Predicate<String> p2 = p -> p.isEqual("two");
Predicate<String> p3 = p -> p.isEqual("three");

stream.filter(p1).forEach(System.out::prinltln); // prints only the Strings "three", "four", "five"
stream.filter(p2).forEach(System.out::prinltln);// prints only the String "two"
stream.filter(p2.or(p3)).forEach(System.out::prinltln); // prints only the Strings "two" and "three"

- The call to the methods of the Stream are lazy. For example, call to the filter method is lazy because it is declaration of an operation on a given stream and it does not process any data.
- All the methods of the Stream that returns another Stream is lazy because of the reason stated above.
- Stream that is operated using some methods (filter) returns a Stream which is called an intermediary operation. If we check the javadoc of Stream interface, it is said clearly whether each method of the interface is intermediary operation or final operation.

peek method - This method returns the stream whereas forEach does not return the stream.

List<String> result = new ArrayList();
List<Person> persons = ...;
persons.stream().peek(System.out::println).filter(person -> person.getAge() > 20).peek(result::add)

The above code does not print anything and does not add elements to ArrayList because peek and filter methods are the intermediary operation and they are just the declaration  of the methods. forEach method is final or terminal operation.

For example, if we change the above piece of code as follows

List<String> result = new ArrayList();
List<Person> persons = ...;
persons.stream().peek(System.out::println).filter(person -> person.getAge() > 20).forEach(result::add)

Here peek method returns a stream and filters the data using filter method and finally adds the data to result arraylist. Because forEach is a terminal or final operation, data is processed here.

Mapping operations

Map operations returns a stream and does not process any data. Takes argument as an object and returns an object.

@FunctionalInterface
public interface Function<T, R>{
  R apply(T t);
}

The interface also have two default methods Compose and andThen. Also have static method called identity.

Example

List<Person> list = ....
Stream<Person> stream = list.stream();
Stream<String> names = stream.map(person -> person.getName());

flatMap operation
-----------------
flatMap operation takes object as a parameter or argument and returns a Stream.
Example

List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
List<Integer> list2 = Arrays.asList(2, 4, 6);
List<Integer> list3 = Arrays.asList(3, 5, 7);

List<List<Integer>> list = Arrays.asList(list1, list2, list3);
Function<List<Integer>, Integer>  size = List::size;
Function<List<Integer>, Stream<Integer>> flatMapper = s -> s.stream();
list.stream().map(flatMapper).forEach(System.out::println);

The above code which uses map returns stream of streams. The output prints out 3 streams. Try it!!

list.stream().flatMap(flatMapper).forEach(System.out::println);

IF we use flatMap instead of map as shown above, all the 3 streams are flattened and made as a single stream. Try the above exmaple.

Reduction

Two kinds of Reduction included in stream API
Basic and classical sql operations like min, max, sum etc

How does it work?

List<Integer> ages = ...;
Stream<Integer> stream = ages.stream();
Integer sum = stream.reduce(0, (age1, age2) -> age1 + age2);

1st argument - Should be the identity element of reduction operation
2nd argument - reduction operation of type BinaryOperator<T>

BinaryOperator is special case of BiFunction. It takes two objects of type T and U and returns of the type R

@FunctionaInterface
public interface BiFunction<T, U, R>{
      R apply(T t, U u);
}

@FunctionaInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T>{
   
}

The BiFunction takes two arguments, so..

what happens if the stream is empty?
 - The reduction of an empty stream is the identity element
what happens if the stream has only one element?
- If the stream has only one element, then the reduction is that element.

Example 1

Stream<Integer> stream = Stream.empty();
BinaryOperation<Integer> sum = (i1, i2) -> i1 + i2;
Integer id = 0;
int red = stream.reduce(id, sum)

The above code prints 0 because the stream is empty and it simply returns and print identity element.

Example 2

Stream<Integer> stream = Stream.of(1);
BinaryOperation<Integer> sum = (i1, i2) -> i1 + i2;
Integer id = 0;
int red = stream.reduce(id, sum)

The above code prints 1 because the stream contains only one value, it returns that one element plus identity element. (1 + 0)

<Integer> stream = Stream.of(1,2,3,4)
BinaryOperation<Integer> sum = (i1, i2) -> i1 + i2;
Integer id = 0;
int red = stream.reduce(id, sum)

The above code prints 10 ofcourse.

Reduction as max operation

BinaryOperation<Integer> sum = (i1, i2) -> i1 > i2 ? i1 : i2;

The problem is the max operation does not have identity element but we need to provide identity element as the first argument in reduce method. when the stream is empty then it should return some

List<Integer> ages = ...;
Stream<Integer> stream = ages.stream();
Optional<Integer> max = stream.max(Comparator.naturalOrder());

Optional is the new concept in java8. It is just like a wrapper type. of Integer, Float, Double etc. The difference between a wrapper type in java and Optional is that Wrapper type always have some value but Optional means <There might be no result>

How to use Optional?
isPresent() returns true if there is something in the optional.

Optional<String> opt = ...;
if(opt.isPresent()){
   String s = opt.get(); // Used to get the value
}else{
 ....
}

orElse() method encapsulates both the calls isPresent() and get(). If get() does not return any value, it defaults to the value set inside the method.

Example:
String s = opt.orElse("");  // If get() does not return any value, then the empty string is returned as default value

orElseThrow(MyException::new) - lazy construct. we can use this method to throw the exception if get() does not return any value.

Reductions are terminal operations. They trigger processing of the data.

Example of Terminal operation - Reduction

List<Person> persons = ...;
Optional<Integer> minAge = persons.stream()
                                                            .map(person -> person.getAge()) // Stream<Integer>
                                                            .filter(age -> age > 20)                  // Stream<Integer>
                                                            .min(Comparator.naturalOrder()) // Terminal operation

persons.stream().map(person -> person.getLastName())
                          .allMatch(length < 20); // Terminal operation

Collectors


This is the second kind of reduction.
According to javadoc they are called as mutable reduction. Going to reduce the stream in a container that is mutable. i.e we are going to add all the elements of the stream to that container.

Example

List<Person> persons = ...;
String result = persons.stream().filter(person -> person.getAge() > 20)
                                                   .map(Person::getLastName)
                                                   .collect(Collectors.joining(","));


 Result is the String with the lastNames older than 20 seperated by comma. We can also collect in a List, Map

Example for collecting in a Map

Example:1

List<Person> persons = ...;
Map<Integer, List<Person>> result = persons.stream().filter(person -> person.getAge() > 20)
                                                  .collect(Collectors.groupingBy(Person::getAge));

Result is a Map containing the persons older than
    - The keys are the ages the people
    - The values are the list of the people of that age.

Example:2

Map<Integer, Long> result = persons.stream().filter(person -> person.getAge() > 20)
                                                  .collect(Collectors.groupingBy(Person::getAge,                                                                                      Collectors.counting())); // the downstream collector

Collectors.counting() just count the number of persons of each age.

Stream can not be reused. Once the stream has been used to process some set of data, we can not use the same stream to process another set of data. We should create a new stream

Date API in java8

java.time - completly replaces java.util and Calendar classes and still keeps inter-operability with legacy API

Instant - It is a class and is a point on the timeline
Timeline - Assume it is line of time with dates on it and it has given precision and precision is the nanosecond.

Conventions of instant class

Instant 0 is the January the 1st, 1970 at midnight GMT
Instant.MIN is 1 billion years ago
Instant.MAX is Dec 31 of the year 1,000,000,000
Instant.now() is the current instant

The timeline begins from 1 billion years ago and end with 1 billion years from now.
Precision of the time would be in the nanosecond

Instant object is immutable

Instant start = Instant.now();
.......
Instant end = Instant.now();

Duration


Duration gives the elapsed time between two Instant objects
Duration elapsed = Duration.between(start, end);
long mills = elapsed.toMillis();

Methods
--------

toNanos(), toMillis(), toSeconds(), toMinues(), toHours(), toDays() - Used to convert duration between two instant objects to nano seconds, milli seconds, seconds, minuets, hours and days
minusNanos() -  Used to add  some nano seconds to the duration
plusNanos() - Used to add some nano seconds to the duration
multipliedBy(), dividedBy(), negated() - Used to multiply or divide the duration with a number
isZero(), isNegative() - Check whether the duration is zero or negative

There are many cases where date is not an Instant
Ex: Shakespere was born Apr 23, 1564
Let us meet at 1Pm and have lunch together

In the first example, it is just a date without nanosecond precision but Instant represents a date with nanoseconds precision. So Instant is not suitable for such dates without nanosecond precision

The same applies to Example 2. It just says at 1PM, the hour of the day.
So we need another concept for those dates. LocaDate is used for that.

LocalDate

LocalDate is used to create a date without requiring precision like Instant class. So the above examples could be addressed using the LocalDate class.

LocalDate now = LocalDate.now();
LocalDate dateOfBirth = LocalDate.of(1564, Month.APRIL, 23)

Here if you see we represent Month using Month.APRIL which is more readable. But in Java 7, it should represent month as 3 which is April.

Period

The Period is the amount of time between two LocalDate whereas Duration is the amount of time between two Instant objects

Example: When was shakesphere was born?

Period p = dateOfBirth.until(now);
System.out.println("# years "+ p.getYears());

long days = dateOfBirth.until(now, ChronoUnit.DAYS);
System.out.println("# days "+ days);

Example

People.txt

Malathi 1984 05 18
Naresh 1983 10 01
Neelima 1980 10 20

DateAndTime.java

List<Person> persons = new ArrayList();
try{
     BufferedReader reader = new BufferedReader(new InputStreamReader(DateAndTime.class.getResourceAsStream("People.txt")));
Stream<String> stream = reader.lines();
stream.map(line -> {
     String s[] = line.split(" ");
     String name = s[0]
     int year = Integer.parseInt(s[1]);
     Month month = Month.of(Integer.parseInt(s[2]));
    int day = Integer.parseInt(s[3]);
    Person p = new Person(name, LocalDate(year, month, day));
    persons.add(p);
   return p;   
}).forEach(System.out::println);
LocalDate now = LocalDate.of(12, Month.MARCH, 2014);
persons.stream().forEach(p -> {
      Period period = Period.between(p.getDateOfBirth(), now);
System.out.println(p.getName() + "was born " +
                             period.get(ChronoUnit.YEARS) + "years and " +
                             period.get(ChronoUnit.MONTHS) + "months  " +
                             "[" + p.getDateOfBirth().until(now, ChronoUnit.MONTHS) + " months]"
                           );
                   }
               );
}

Person.java

public class Person{
   private String name;
   private LocalDate dateOfBirth;
  public void Person(String name, LocalDate dateOfBirth){
        this.name = name;
       this.dateOfBirth = dateOfBirth;
  }
  // getter and setter methods for name and LocalDate
}

The above example reads the input from Persons.txt file which contains Person name, Year, Month, Day. Using Stream API , LocalDate and Period classes we compare the date of birth of a person and current date and extract number of years, months and number of months since the Person is born.

Hope the above example is useful to easily understand.

DateAdjuster

Useful to add or subtract an amount of time to an instant or a LocalDate

LocalDate now = LocalDate.now();
LocalDate nextSunday = now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));

See more in javadoc for more methods available for TemporalAdjusters class

LocalTime


It is just a time in the day.
Ex: 10:20

LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(10, 20) //10:20

we have set of methods available to manipulate that time

LocalTime time = LocalTime.of(10, 20)
LocalTime wakeupTime = time.plusHours(8); 10:20 + 8hours

Zoned Time

There are Time zones all over the earth
The zones are available from

Set<String> allZoneIds = ZoneId.getAvailableZoneIds();
String uKTZ = ZoneId.of("Europe/London");

Example

System.out.println(
     ZonedDateTime.of(
      1564, Month.APRIL.getValue(), 23, //year, month, day
      10, 0, 0, 0,                                         // h/mm/s/nanos
     ZoneId.of("Europe/London"))
); // prints 1564-04-23T10:00-00:01:15[Europe/London]

See more methods available in javadoc

DateTimeFormatter
---------------------------
check javadoc...

Bridges between legacy (java 1 API inherited to java7)


How to interoperate with the legacy Date API from java 7? We have certain methods available to provide inter-operability between Java 7 date API (legacy API ) and Java 8 API.

1) Instant and Date

Date date = Date.from(instant); // Java 8 to legacy code (old java version API)
Instant instant = date.toInstant(); // legacy code to Java 8

2) Instant and TimeStamp

TimeStamp time = TimeStamp.from(instant); // Java 8 to legacy code (old java version API)
Instant instant = time.toInstant();  // legacy code to Java 8

3) LocalDate and Date

Date date = Date.from(localDate); // Java 8 to legacy code (old java version API)
LocalDate localDate = date.toLocalDate();  // legacy code to Java 8

4) LocalDate and Time

Time time = Time.from(localTime); // Java 8 to legacy code (old java version API)
LocalTime localTime = time.toLocalTime();  // legacy code to Java 8

Strings, IO's and other bits and pieces

Java 8 is not only about lambda expressions and Streams.

String class
Java I/O package
Collection interface
Comparators - New way of writing them in Java8.
Numbers
Map
Annotations

Strings in Java 8


String s = "Hello world!"
IntStream stream = s.chars(); // creates a stream on letters of s
stream.mapToObj(letter -> (char)letter)
           .map(Character::toUpperCase)             
           .forEach(System.out::print);

Output will be HELLO WORLD!

StringJoiner


String s1 = "Hello"
String s2 = "World"
String s = s1 + s2;

The above way of string concatenation is not efficient and should not be used because of the multiple creations/deletions of intermediary strings. So StringBuffer class is used using append method.

But StringBuffer is synchronized. So in Java5 StringBuilder is used which is not synchronized and would be better to append the strings than StringBuffer

Join Strings with builtin seperator.

StringJoiner sj = new StringJoiner(",");
s1.add("one").add("two").add("three")
System.out.println(sj.toString()); // outputs one, two, three

We can also pass prefix and suffix as arguments to StringJoiner class

StringJoiner sj = new StringJoiner(",", "{", "}");
s1.add("one").add("two").add("three")
System.out.println(sj.toString()); // outputs {one, two, three}

StringJoiner can also be used from String class but we can not add prefix and suffix to the join method

String s = String.join(",", "one", "two", "three");

we can also write the above line as follows

String s[] = {"one", "two", "three"};
String s = String.join(",", s);

Java I/O enhancements

1) Reading text files

The lines() method has been added to the BufferedReader class

try(
 BufferedReader reader = new BufferedReader(new FileReader(new File("D:/debug.log")));
){
   Stream<String> stream = reader.lines(); // new method in Java 8
 stream.filter(line -> line.contains("ERROR")).findFirst().ifPresent(System.out::println)
}catch(IOExeption e){
     .....
}

If we see the above code, we write BufferedReader code as an argument of try block. This is the feature in Java7. In this way, we no need to explicitly close the reader of the inputStream. It is automatically done for us in the bytecode of java class.

lines()  method is newly added in Java8 which returns the stream of lines from debug.log file
And the stream is operated using several intermediary methods findFirst,isPresent...

File.lines(path)
----------------
Path path = Paths.get("D:", "tmp", "debug.log");
try(
Stream<String> stream = File.lines(path);
){
   Stream<String> stream = reader.lines(); // new method in Java 8
 stream.filter(line -> line.contains("ERROR")).findFirst().ifPresent(System.out::println)
}catch(IOExeption e){
     .....
}

Stream implements AutoCloseable and will close the underlying file.

Reading Directory Entries
------------------------

Method File.list(path) - Used to read the directory files taking Path as an argument but it does not read the sub directories inside the directory. But no problem, we can acheive this using different method available in java8

Path path = Paths.get("D:", "windows");
try(Stream<Path> stream : Files.list(path)){
   stream.filter(path -> path.tiFile().isDirectory()).forEach(System.out::println);
}catch(Exception e){
 ......
}

Files.walk(path) method - Used to list the files even from sub directories from a directory. The code is same as above except that we use Files.walk(path)


Path path = Paths.get("D:", "windows");
try(Stream<Path> stream : Files.walk(path)){
   stream.filter(path -> path.tiFile().isDirectory()).forEach(System.out::println);
}catch(Exception e){
 ......
}

we can also limit the depth of exploration of sub directories inside a directory by simply passing an additional parameter to walk method

Ex: Follow the same code as above but pass an integer to walk method as below
Files.walk(path, 2). It limits exploration of sub directories. Try it!!

New methods in Iterable, Collection and List
------------------------------------------------------------
New method on Iterable
forEach() - strings.forEach(System.out::println);

New methods on Collection
removeIf() - It accepts a predicate and return boolean value

Collection<String> strings = Arrays.asList("one", "two", "three", "four", "five");
Collection<String> list = new ArrayList(strings);
boolean b = list.removeIf(s -> s.length() > 4);
System.out.println(list.stream().collect(Collectors.joining(",")));

New methods on list
------------------------------

replaceAll() method - It takes function as a parameter which is special type of function called unary operator that takes element from that list and returns another element of the same type. Each element of the list is processed by the function

Ex:

List<String> strings = Arrays.asList("one", "two", "three", "four", "five");
List<String> list  = new ArrayList(strings);
 list.replaceAll(String::toUpperCase);
 System.out.println(list.stream().collect(Collectors.joining(",")));

Method sort() - Sorts content of a list by providing Comparator

Example:
list.sort(Comparator.naturalOrder());
 System.out.println(list.stream().collect(Collectors.joining(",")));

Comparators
---------------

Comparator is an interface. In java 7, we should implement our own Comparator interface to compare and arrange the elemnts in an order. We need to address all sort of conditions on the objects that we compare.

Example: while we compare two objects using a field called lastName (Ex: Person), and if they are equal, we should consider some other fields on the objects to compare with.
Also we should add null check on the objects while comparing

All these problems are addressed easily in java8 using comparing method

Comparator<Person> compareLastName =
                                       Comparator.comparing(Person::getLastName)
                                      .thenComparing(Person::getFirstName)

The above code is much better and is very readable. If we see the code, if lastName is same for two objects, thenComparing method is called and compare the objects depending on firstName. thenComparing is used to chain the comparisons of different fields.

thenComparing  is the default method of the interface Comparator

We can reverse a given comparator using reversed method. This is also a default method on interface
Comparator<Person> comp = ....;
Comparator reversedComp = comp.reversed();

naturalOrder method is static method of Comparator interface. It compares comparable objects, for example Strings in a natural order - alphabetical order

Comparator comp =  Comparator.naturalOrder();

nullsFirst is a static method of Comparator interface. If the list have null values, then they are put at the starting of the list.

Comparator comp =  Comparator.nullsFirst(Comparator.naturalOrder());

nullsLast is a static method of Comparator interface. If the list have null values, then they are put at the end of the list

Comparator comp =  Comparator.nullsLast(Comparator.naturalOrder());

Numbers
-------------

In java we have primitive types such int, float, double, char, short and they all got associated wrapper type

Java8 provides new useful methods such as sum, max and min
long max = Long.max(1L, 2L);

The methods are useful to create reduction operations
BinaryOperator<Long> max = (i1, i2) -> i1 + i2; // we can write it as Long::sum

Hashcode computation

Hashcode of integer is an integer itself. But we need to follow special algorithm to generate hascode of Long

Following is the solution in jdk7 to compute hashcode for long variables.

long l = 3141592653589793238L;
int hash = new Long(l).hashCode();

Here there is costly boxing and unboxing of primitive type  to compute the hashcode.

So we have a method in java8 called  hashCode which accepts the primitve type as an argument to compute hashcode for primitive types. See below example.

long l = 3141592653589793238L;
int hash = Long.hashCode(l);

This method is available on 8 wrapper types.

Map - Enhancements of existing methods

forEach() - This is the same kind of method that is defined on iterable interface which is available for all instances of Collection.  forEach is also available for Map and takes BiConsumer as parameter. BiConsumer is a regular Consumer which takes two parameters instead of one and those two parameters are ofcourse Key and value pair of the Map

Map<String, Person> map = ...;
map.forEach((key, person) -> System.out.println(key + " " +person));

getOrDefault() - get() method in earlier versions may return sometimes null value. null value will be returned if either the key is not present in the Map or the key is associated with null value.
In java8, getorDefault method is used to specify the default value to be returned in case when the Map does not contain any value for the key.

Map<String, Person> map = ...;
Person defaultPErson = PErson.DEFAULT_PERSON;
Person p = map.getOrDefault(key, defaultPerson);

putIfAbsent() - This is the newer version of put method in java8. In earlier versions, If put method is called on the key, the existing value for the key will be erased and replaced with the new value.

In java8, putIfAbsent() does not erase the existing value in the map and put the value if and only if the key does not have any value.

Map<String, Person> map = ...;
map.put(key, person);
map.putIfAbsent(key, person);

replace(key, oldValue, newValue) - replace the value for the key with newValue only if the oldValue is mapped to the key.

replaceAll() - Takes a BiFunction that takes key and value and that will produce a new value.
remove(key, value) - remove the key associated with the value specified in the method

compute(), computeIfAbsent() and computeIfPresent()

Often we fetch a value from a map, make some calculations on it and put the value back into the map. This can be hard to get right when concurrency involved. With java8, we can pass a BiFunction to the methods to handle it properly.

Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// Compute a new value for the existing key
System.out.println(map.compute("A",
    (k, v) -> v == null? 42: v + 41));
System.out.println(map);
// This will add a new (key, value) pair
System.out.println(map.compute("X",
    (k, v) -> v == null? 42: v + 41));
System.out.println(map);

computeIfPresent()

the key and the value associated should be present in the map otherwise the BifFunction is not called and does not compute a new value.

computeIfAbsent()

We just provide a key and if this key is not present in map then BiFunction executed and return a new value for the key.

computeIfAbsent is used to compute Bimaps. It is a map in which a value is itself an another map.

Map<String, Map<Integer, Person>> biMap = ...;
Person p = ..;
bimap.computeIfAbsent(key1, key -> new HashMap<>().put(key2, p));

Case 1 : If key1 is already present in the map, then the function expressed as lambda expression is not evaluated at all but returns existing hashMap that has been put as value associated with key1.
So we can directly call put on this existing hashMap and associated key2 with person p.

Case 2: Now key1 is not present, then lambda expression will be executed and new HashMap will be built and associate it to the key1 key in the first map and then this new empty hashmap will also be returned by the method and this time we call put on this  new hashMap and associate person to the new key key2.

This basically allows us to create bimaps in java with just one line of code.

merge() - This method assumes that key is present in the map or associated to a null value. when we call merge, we pass key and person as a parameter, the key should be present in the map and person should match the person already associated to the key in the map and then we compute a new person with lambda expression passed as a parameter. This lambda expression is just the implementation of BiFunction that takes the key and value of the map as a parameters and builds a new value out of those parameters.

Map<String, Person> map = ...;
map.merge(key, person, (key ,person)) -> newPerson);

Example:

People.txt

Malathi 34 F
Neelima 27 F
Naresh 34 M

public class MergingMaps{
 public static void main(String args[]){
       List<Person>       persons = new ArrayList<Person>();
     BufferedReader reader = new BufferedReader(new InputStreamReader(MergingMaps.class.getResourceAsStream("People.txt")));
Stream<String> stream = reader.lines();
stream.map(line -> {
     String s[] = line.split(" ");
     Person p = new Person(s[0], Integer.parseInt(s[1]), s[2]);
     persons.add(p);
     return p;
}).forEach(System.out::println);

 List<Person> list1 = persons.subList(0,10);
 List<Person> list2 = persons.subList(10, persons.size());

Map<Integer, List<Person>> map1 = mapByAge(list1);
map1.forEach((age, list) -> System.out.println("age : " + age + "list " + list));


Map<Integer, List<Person>> map2 = mapByAge(list2);
map1.forEach((age, list) -> System.out.println("age : " + age + "list " + list));

map2.entrySet().stream().forEach(entry -> {
    map1.merge(entry.getKey(), entry.getValue(), (l1, l2) -> {
            l1.addAll(l2);
           return l1;
    }) ;
});
map1.forEach((age, list) -> System.out.println("age : " + age + "list " + list));
}

private static Map<Integer, List<Person>> mapByAge(List<Person> list){
    Map<Integer, List<Person>> map =     list.stream().collect(Collectors.groupingBy(Person:getAge));
  return map;
}

}

In the above code we read the People.txt content to the lists list1 and list2. Later we created two maps map1 and map2 using the method mapByAge(). This method returns map with age as key and value as List which holds the list of people of the same age. Thus we create two maps for two given lists. Later we are merging map1 and map2 using merge method as shown above.

In the example above, for each entry of map2, we are checking against with map1 entries. If key of map2 matches with the key of map1 then we return list of combined map1 and map2 list values. Hence the final result will be map1 which holds list of persons grouped by age.

Hope it is clear. Try the above example. Dont forget ot create Person DAO :)

Annotations
.........................

Java8 brings the concept of multiple annotations. So what is the problem with java7 annotations??
If we see the below example, suppose if we want to test the case with multiple parameters, we need to wrap the annotations with another annotation. Here we wrap a list of annotations using TestCases annotation.

@TestCases({
       @TestCase(param=1, expected = false),
       @TestCase(param=2, expected = true),
})
public boolean even(int param){
    return param % 2 == 0
}

In java8 it is made simple. we no need to wrap the annotations using TestCases. See below

@TestCase(param=1, expected = false),
@TestCase(param=2, expected = true)

public boolean even(int param){
    return param % 2 == 0
}

How does it work? The wrapping annotation is automatically added for us.
First create the annotations as usual

@Repeatable(TestCases.class)
@interface TestCase{
 int param();
 boolean expected();
}

@interface TestCases{
   TestCase[] value;

}

we just provide the wrapping annotation; in our case TestCases as an attribute to the Repeatable annotation. So the compiler will automatically add wrapping annotation - TestCases for us

2) Java8 allows annotations to be put on types
Example 1: To declare that a variable should not be null
private @NonNull List<Person> persons = ...;

Example 2 To declare that a list should not be null, and should not contain null values
private @NonNull List<@NonNull Person> persons = ...;


So now we came to the end of the course. I have covered all the features of Java8 in here. Please post your comments if you have any questions.

Happy Learning!!!

Comments

Popular posts from this blog

Bash - Execute Pl/Sql script from Shell script

Gradle Fundamentals

Load Balancing using Spring Cloud Netflix Ribbon