Java [8..17] new features

Esteban Sáenz Cáceres 22 Feb 2021

In Java 8 major changes where introduced to Java. This is a review of the main new features from Java 8 to 17 focusing in the practical uses of the language

 

Java 8

Java 9

Java 10

  • Local-variable Type Inference // Type inferred by compiler var name = "test";
  • Improved Garbage Collector with G1GC</a>
  • Application Class Data Sharing (for different VMs running the same code or repeated executions)
  • Improved Container Awareness

Java 11

Java 12

Java 13

Java 14

Java 15

Java 16

  • Vector API (Incubator)
  • Enable C++14 Language Features
  • Migrate from Mercurial to Git
  • ZGC: Concurrent Thread-Stack Processing
  • Alpine Linux Port
  • Elastic Metaspace
  • Windows/AArch64 Port

Java 17 (TBD)

 

 

Java 8

Interfaces

Prior to java 8, interface in Java can only have abstract methods. Now default and static methods are allowed in.


interface MyInterface{
    // Default method do not need to be implemented the implementation classes
    default void newMethod() {
        System.out.println("Newly added default method");
    }

    // Static method in interface is similar to default method except that we
    // cannot override them in the implementation classes.
    static void anotherNewMethod() {
    	System.out.println("Newly added static method");
    }

    // Typical abstract method that must be implemented in the implementation classes
    void existingMethod(String str);
}

Functional programming

Functional interface (@FunctionalInterface)

Interface with only one abstract method (default or static methods don't count!!!)

@FunctionalInterface public interface Comparator {
  int compare(T o1, T o2);
  // Methods implemented from Object (e.g. equals)
  boolean equals(Object obj);
}

Lambda expression

Implementation of an anonymous class. The type is a functional interface. Do not creates an object (not using new) so it is much cheaper (do not need to gets the memory, static and non-static blocks, executes the constructor and all super constructors). It is an object without identity.

// (params) -> {body}
Comparator c = (String s1, String s2) -> Integer.compare(s1.length(), s2.length));
// Compiler can infer the type
Comparator c = (s1, s2) -> Integer.compare(s1.length(), s2.length));

// using an anonymous class
Runnable r = new Runnable() {
  @Override
  public void run() {
    System.out.println("Hello");
  }
// using a lambda expression
Runnable r2 = () -> System.out.println("Hello");};

//Anonymous Consumer (this  instantiate an implementation of the Consumer interface using an anonymous class) vs Lambda
Consumer<String> printConsumer= new Consumer<String>() {
    public void accept(String name) {
        System.out.println(name);
    }
}; // same than: name -> System.out.println(name)

Functional classes predefined


Predicate p1 = s -> s.length() < 20;
Predicate p2 = s -> s.length() > 10;
Predicate p3 = p1.and(p2);

List strings = Arrays.asList("one", "two");
List list = new ArrayList<>();
Consumer print = System.out::println;
Consumer add = list::add;
// Channing consumers!
strings.forEach(print.andThen(add));

Method reference

Simplified way to write some labmda expresions

a -> Class.method(a) === Class::method
Comparator c = (i1,i2) -> Integer.compare(i1, i2)
Comparator c = Integer::compare

Streams

Stream in Java can be defined as a sequence of elements from a source that supports aggregate operations on them. 

Can be executed only once (the terminate operation closes the stream so only one terminate operation can be executed for one stream). Only declares a pipeline (very cheap -until terminate is executed-)

It is a way to process data (in parallel and pipelined). There are 3 kinds of operations: Stream, intermediary and terminate.

Stream operation

Creates the stream. A stream is lazy executed. This means it is not executed until the terminate operation does. Then the stream is closed (only can be executed once)

Intermediary operations

They are lazy. Only declares a pipeline, do not executes the operation. Returns a stream (Stream<T>).

Teminate operation

Executes all the stream pipeline and then closes the Stream. So the Stream can be executed only once.  Examples:

Examples


people.stream().forEach(System.out::println);
Map<Integer,List> result = persons.stream().collect(Collectors.groupingBy(Person::getAge());
Map<Integer,Long> result = persons.stream().collect(Collectors.groupingBy(Person::getAge(),Collectors.counting()); //downstream collector counts the number of people of each age
Map<Integer,List> result = persons.stream().collect(Collectors.groupingBy(Person::getName(),Collectors.toList());

ages.stream().reduce(0,(age1,age2) -> age1+age2); (T,T->T)
[-10,-5].stream().reduce(0, Integer::max) // max(0,max(-10,-5))) = 0

Optional

Store an Object or a void value.

Optional opt = Optional.ofNullable(myNullableString);
List people = ...
Optional minAgeBiggerThan20 = people.stream()
                      .map(person -> person.getAge()) // Stream, intermediary
                      .filter(age -> age > 20) // Stream, intermediary
                      .min(Comparator.naturalOrder()); // Optional, terminal

New Date API

Instant instant = Instant.now() //is the current instant. Is inmutable (small cost)
Duration elapsed = Duration.between(startInstant, endInstant); // amount of time between two instants
long millis = elapsed.toMillis();

// LocalDate: Is just a date with a day presition (not nanosecond presition)
LocalDate now = LocalDate.now();
LocalDate birth = LocalDate.of(1985, Month.APRIL, 23);
Period: Amount of time between to LocalDates;
Period p = birth.until(LocalDate.now(); p.getYears();
long days = birth.until(LocalDate.now(), ChronoUnit.DAYS);

// DateAdjuster: Useful to add or substract amounts of time to Instants or LocalDate
LocalDate nextSunday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.SUNDAY));

LocalTime: Is just a time of a day
LocalTime time = LocalTime.of(10, 20); // 10:20

// Zone Time: Implements the time different time zones
String ukTimeZone= ZoneId.getAvailableZoneIds().of("Europe/London");
ZonedDateTime.of(1943, Month.APRIL.getValue(), 23, // year month day
		 10, 0, 0, 0,			   // h / mn / s / nanos
                 ZoneIf.of("Europe/London")); // 1943-04-23T10:00-00:01:15[Europe/London]

ZonedDateTime z = ZonedDateTime.of(LocalDate.of(2014,Month.MARCH,12), Localtime.of(9,30), ZoneId.of("Europe/London"));
z.plus(Period.ofMonth(1));
z.withZoneSameInstant(ZoneId.of("US/Central")); // change time zone

System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(z)); // format date to print it

//Use java.time instead java.util.Date
LocalDate date1 = LocalDate.of(1985,12,5);
date1.datesUntil(LocalDate.now(),Period.ofYears(1)).map(date -> Year.of(date.getYear())).count():

String

// Stream of chars
IntStream stream = "Hello".chars();
stream.mapToObj(letter -> (char)letter).map(Character::toUpperCase).forEach(System.out::print);

//Concatenation: Use StringJoiner
StringJoiner sj = new StringJoiner(", ");
sj.add("one").add("two").add("three"); // one, two, three
StringJoiner sj = new StringJoiner(", ", "{","}");
sj.add("one").add("two").add("three"); // {one, two, three}
String.join(", ","one","two","three");

Try with resources

Autoclose the try enclosed object. Implements AutoCloseable interface (Java 7)

Scanner scanner = null;
try {
    scanner = new Scanner(new File("test.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
    // Multiple catch exception also added in Java 7
} catch (FileNotFoundException | IOException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}
// this is equivalent, and closes automatically the scanner object since it is inside the try-with-resources
try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

Java I/O

Reading text files (File.lines)


// try with resources from Java 7 + Files.lines from Java 8
// Stream implements AutoCloseable and closes the file
Path path = Paths.get("d:", "tmp", "debug.log");
try (Stream<String> stream = Files.lines(path)) {
	stream.filter(line -> line.contains("ERROR")).findFirst().ifPresent(System.out::println);
} catch (IOException ioe) {
	// handle exception
}

Reading directory entries (File.list) - Only visitrs first level / Files.walk -> visits the whole subtree Files or the deph level set Files.walk(path,2);
try (Stream<String> stream = Files.list(path)) {
	stream.filter(path -> path.toFile().isDirectory()).forEach(System.out::println);
} catch (IOException ioe) {
	// handle exception
}

Collections

Reading text files (File.lines)


Collection<String> list = new ArrayList<>(strings);
boolean b = list.removeIf(s -> s.length() > 4); // returns if the list was modified
list.replaceAll(String::toUpperCase); // modifies the list
list.sort(Comparator.naturalOrder()); // order the list using the comparator
list.stream().collect(Collectors.joining(", "));

// Comparator:
Comparator<Person> compareLastName = Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName);
compareLastName.reversed(); // compares the reverse
Comparator c = Comparator.naturalOrder();
Comparator c = Comparator.nullsFirst(Comparator.naturalOrder()); // or nullsLast

// Utility methods:
long max = Long.max(1L,2L);
BinaryOperator sum = (l1, l2) -> l1 + l2; // = Long::sum;
int hash = new Long.hashCode(3141592653589793238L); // -1985256439

Map<String, Person> map = ...
Person p = map.getOrDefault(key,defaultPerson); // prevents a null
map.putIfAbsent(key,person);
map.replace(key,person);


Java 9

Collection Factory Methods

Initialization methods for collections that creates Inmutable Collection (In Java 11 the name is changed to Unmodifiable Collection because although the collection itself is not modifiable, the elements contained are not inmutable)


List<Integer> list = List.of(1,2,3)
Set<Integer> set = Set.of(1,2,3)
Map<String,Integer> map = Map.of("Key1",1,"Key2",2) // up to 10 key/value  -> remember iteration order not guaranteed
Map.ofEntries(Map.entry("key","value"), Map.entry("key","value") // no limit

list.add(4) -> UnsupportedOperationException
list.getClass() -> InmutableCollection$ListN

List.of(1).getClass() -> InmutableCollection$List1 (optimized implementation for single/dual collections)

Stream API

New methods added in the Stream API.


// For ordered streams
takeWhile(Predicate <? super T> p) -> list.takeWhile(a -> a < 4)  [1,4,5] -> [1]
dropWhile(Predicate <? super T> p) -> drop.takeWhile(a -> a < 4)  [1,4,5] -> [4,5]

// Print code inside comments -> File.lines -> Stream
Files.lines(Path.get("file.html")).dropWhile(l -> !l.contains("<<<<<<<<<")).skip(1).takeWhile(l -> !l.contains(">>>>>")).forEach(...);

// ofNullable(T t)
Stream.ofNullable(null).count() -> 0 // Stream.ofNullable(getBook()).count() -> 1
Stream.ofNullable(getPossibleNull()).flatmap(b -> b.authors.stream()).forEach(System.out::println);

iterate(T seed, Predicate <? super T> hasNext, UnaryOperator next)

// Stream collectors: collect -> Transform Stream into a Collection
Stream.of(1,2,3).map(n -> n + 1).collect(Collectors.toList()); -> [2,3,4]
Stream.of(1,2,3,4).collect(groupingBy(i -> i % 2, toList())); -> {0=[2], 1=[1,3,3]}

// Optional: Holds single value or null, new methods added
ifPresentOrElse(Consumer action, Runnable emptyAction)
or(Supplier<Optional<T>> supplier)
Stream<T> stream()

Stream<Optional<Integer>> optionals = Stream.of(Optional.of(1), Optional.empty(), Optional.of(2));
Stream ints = optionals.flatMap(Optional::steam);
ints.forEach(System.out::print); -> 1 2

myObject.ifPresentOrElse(System.out::println, () -> System.out.println("Object null"));

Private interface methods

New methods added in the Stream API.


public interface Price {
	double getPrice();

	default int getDefaultPrice1() {
		return 1 + defaultPriceInternal();
	}

	default int getDefaultPrice2() {
		return 2 + defaultPriceInternal();
	}

	private int defaultPriceInternal() { // vision only in the interface
		return 1;
	}
}

Http Client

New HTTP API.


HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://...")).GET().build();
//Blocking synchronous
HttpResponse response = client.send(request,HttpResponse.BodyHandler.asString());
// Non-blocking asyncrhonous
CompletableFuture<HttpResponse> response = client.sendAsync(request,HttpResponse.BodyHandler.asString());
response.thenAccept(r-> {System.out.println(r.body())});
response.join(); // waits for the completable future to be completed

Reactive Streams

Flow API - To be usable for other thrid party libraries (Akka Streams, RxJava2, Spring 5)

 

Java 11

Scripting

Java 11 can be executed as script directly in the OS.

// With the shebang we can execute scripts directly in command line:
./listfiles

#!/usr/bin/java --source 11
import java.nio.file.*;

public class ListFiles {
	public static void main(String[] args) throws Exception {
		Files.walk(Paths.get(args[0])).forEach(System.out::println);
	}
}

Unmodifiable Collections

Java 11 changes from inmutable to unmodifiable because although the object itself is inmutable, the objects contained by the unmodifiable collection can be mutable (e.g. UnmodifiableList of Person -mutable-). https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html#unmodifiable

Annotations in "var"

Use of var to allow annotations in lambdas (@Nonnull var a, @Nullable var b) -> a.concat(b)

New methods

// isBlank, repeat, strip, trim
"".isBlank() -> true
"na".repeat(3) -> "nanana"
"\n\t  text \u2005".strip() -> "text"
"\n\t  text \u2005".trim() -> "text \u2005"
"1\n2\n".lines().forEach(System.println::out) ->
1
2
strings.stream().filter(Predicate.not(String::isBlank))).forEach(...)
Optional.isEmpty() -> true if empty object

Java 12

String and compact numbers

// New String methods
"hello".indent(2) -> "  hello\n"
"1.aaa\n2.bbb"indent(5).lines().forEach(System.out::println) ->
     1.aaa
     2.bbb

String::transform
StringUtils.words(StringUtils.clean(text));

// Compact numbers
// 1K -> 1000
// 1M -> 1000000

Teeing Collector. Collectors.teeing(c1,c2,combiner)

Stream.of(10,20,30).collect(Collectors.teeing(
	Collectors.summingInt(Integer::valueOf),
	Collectors.counting(),
	(sum, count) -> sum / count)); -> 10+20+30 / 3

Switch expressions

String monthName = switch(monthNumber) {
	case 1 -> monthName = "January";
	case 2,3 -> {String month = "February"; break monnth;}
	default -> monthName = "Unknown";
};

Switch Expressions

Switch expressions

String monthName = switch(monthNumber) {
	case 1 -> monthName = "January";
	case 2,3 -> {String month = "February"; break monnth;}
	default -> monthName = "Unknown";
};

Micro-benchmarking suite with JMH

@Benchmarkmode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(1) //number of JVM forked
// @Setup @Teardown
public class mybenchmark {
@Benchmark
public void testMethod() {}
}

Java 13

Text Blocks (Preview)

Multiline text definition similar to Python using """..."""


String html = "\n" +
    "   \n" +
    "      <p class=\"text\">Hello, Escapes</p>\n" +
    "   \n" +
    "\n";

//In Java 13 also can be defined as:
String html = """
  
      
          

Hello, Text Blocks

""";

Auxiliary methods in Strings (preview in Java 13 & 14 and promoted to full feature in Java 15)

String::stripIndent

String::translateEscapes

String::formatted -> In java 13 now is possible "Value: %s".formatted(value) that is equivalent to String.format("Value: %s", value)

Switch Expressions (Preview)

Java 12 introduced returning a value in the Switch expressions & Java 13 extends this feature allowing blocks and using yield to return the values


// Java 12
String numericString = switch(integer) {
   case 0 -> "zero";
   case 1, 3 -> "odd";
   default -> "N/A";
};

//In Java 13
String numericString = switch(integer) {
   case 0 -> {
       String value = calculateZero();
       yield value;
   } ;
   case 1, 3 -> {
       String value = calculateOdd();
       yield value;
   };
   default -> {
       String value = calculateDefault();
       yield value;
   };
};

Java 14

Records (Preview)

This is a very powerful feature added to the language. It is a simplified way to define a classical POJO. It adds:Private fields for each variable defined, a constructor, getters and setters, equals and hashCode & toString methods

Constraints: Records can't extend any other class. Can't declare more fields than the set in the description (any other has to be declared as static). Records are final and can't be abstract. Componets of a Record are final and then inmutable

For reflection the following methods are added to class Class: RecordComponent[], getRecordComponents(), isRecord()


// Before
public class PhoneNumber {

    private Integer lineNumber;
    private Integer prefix;
    private Integer areaCode;

    public Integer getLineNumber() {
        return lineNumber;
    }

    public Integer getPrefix() {
        return prefix;
    }

    public Integer getAreaCode() {
        return areaCode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;
        if (getClass() != o.getClass())
            return false;

        PhoneNumber that = (PhoneNumber) o;
        return super.equals(that)
            && Objects.equals(this.lineNumber, that.lineNumber)
            && Objects.equals(this.prefix, that.prefix)
            && Objects.equals(this.areaCode, that.areaCode);
    }
}

// Now AWESOME simplification!
public record PhoneNumber(Integer lineNumber, Integer prefix, Integer areaCode) {
}

Pattern Matching for instanceof (Preview)

Now an object can be defined in the same sentence than instance of without casting it


// Before the object had to be cast
if (obj instanceof String) {
    String s = (String) obj;
    // use s
}
// Now the object can be defined in the same sentence
if (obj instanceof String s) {
    // use s
}

Helpful NullPointerExceptions

Before Java indicates just the line where the NullPointerExceptions was produced. Now the exceptions indicates also which part of the line produced the NullPointerException


a.b.c.i = 99;
a[i][j][k] = 99;
a.i = b.j;
x().y().i = 99;

// Output
Exception in thread "main" java.lang.NullPointerException:
       Cannot assign field "i" because "a" is null
   at Prog.main(Prog.java:5)

Exception in thread "main" java.lang.NullPointerException:
       Cannot read field "c" because "a.b" is null
   at Prog.main(Prog.java:5)

Exception in thread "main" java.lang.NullPointerException:
      Cannot load from object array because "a[i][j]" is null
   at Prog.main(Prog.java:5)

Exception in thread "main" java.lang.NullPointerException:
       Cannot read field "j" because "b" is null
   at Prog.main(Prog.java:5)

Java 15

Sealed Classes (Preview)

Restricts a class to be inherited only by certain Classes


// Shape can be inherited only by Circle and Rectangle
public abstract sealed class Shape permits Circle, Rectangle {
    ...
}

public Circle extends Shape {
    ...
}

public Rectangle extends Shape {
    ...
}

// Error can't be inherited because is sealed to Circle and Rectangle
public Polygon extends Shape {
    ...
}