Exploring Java's Modern Features: From Java 7 to Java 17

Exploring Java's Modern Features: From Java 7 to Java 17

Here is the features matrix that provides information about the availability of features starting from a specific version.

Article content
Note: Check mark refers to it is introduced onwards

Version and features introduced

  • Java 7 Diamond Operator (<>) :The Diamond Operator allows you to simplify the declaration of generic types by inferring the type from the context, reducing code verbosity.

// Before Java 7
List<String> list = new ArrayList<String>();

// With Diamond Operator (Java 7)
List<String> list = new ArrayList<>();        

Try-With-Resources : Try-With-Resources automates resource management by automatically closing resources like files or sockets when they are no longer needed, improving code readability and robustness

// Before Java 7
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("somefile.txt"));
    // something crazy
} catch (IOException e) {
    // handle
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            // Handle IOE
        }
    }
}

// With Try-With-Resources (Java 7)
try (BufferedReader reader = new BufferedReader(new FileReader("somefile.txt"))) {
    // Read and process the file
} catch (IOException e) {
    // Handle exception
}        

  • Java 8

Lambda Expressions: Lambda Expressions introduce a concise way to define and use anonymous functions, enabling more expressive and functional programming styles.

// Before Java 8 - Using anonymous inner class
Runnable runnable = new Runnable() {
    public void run() {
        System.out.println("Hello, Java 8!");
    }
};

// With Lambda Expression (Java 8)
Runnable runnable = () -> {
    System.out.println("Hello, Java 8!");
};        

Stream API :The Stream API provides a powerful way to work with collections of data, allowing operations like filtering, mapping, and reducing to be performed in a functional and declarative manner. - Dive into Streams API

// Traditional Loop
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (int number : numbers) {
    sum += number;
}

// Using Stream API (Java 8)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);        

Date and Time API (java.time) : The Date and Time API introduces a modern and comprehensive way to handle date and time-related operations, addressing the shortcomings of the old Date and Calendar classes.

// Working with Dates
LocalDate date = LocalDate.now();
LocalDate tomorrow = date.plusDays(1);

// Working with Times
LocalTime time = LocalTime.now();
LocalTime noon = LocalTime.of(12, 0);

// Combining Date and Time
LocalDateTime dateTime = LocalDateTime.of(date, time);        

Nashorn JavaScript Engine: Nashorn is a JavaScript engine that allows Java applications to execute JavaScript code, enabling seamless integration of JavaScript and Java applications.

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class NashornExample {
    public static void main(String[] args) throws ScriptException {
        ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
        engine.eval("print('Hello from Nashorn JavaScript!')");
    }
}        

  • Java 11Local Variable Type Inference (var) :var allows for type inference when declaring local variables, reducing boilerplate code while maintaining strong typing.

// Without var (explicit type declaration)
List<String> names = new ArrayList<>();

// With var (type inference)
var names = new ArrayList<String>();        

  • Java 17Pattern Matching for instanceof : Pattern Matching simplifies and enhances the code for type checking and casting, making it more concise and readable.

before 
if (object instanceof String) {

    System.out.println("Length of str: " + (String)object.length());
}

//after

if (object instanceof String str) {
    // Use 'str' as a String within this block
    System.out.println("Length of str: " + str.length());
}        


// no need of breaks    
 int dayNumber = switch (dayOfWeek) {
            case "Monday" -> 1;
            case "Tuesday" -> 2;
            case "Wednesday" -> 3;
            case "Thursday" -> 4;
            case "Friday" -> 5;
            case "Saturday" -> 6;
            case "Sunday" -> 7;
            default -> {
                System.out.println("Invalid day of the week");
                yield -1; // Default value for invalid input
            }
        };        

Records : Records provide a concise way to declare classes that are primarily used to store data, automatically generating common methods like equals, hashCode, and toString.

// Traditional class- either we used to write or Use Lombok Data
public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getters, equals, hashCode, toString...
}

// With Records (Java 17)
record Person(String name, int age) {}        

Sealed Classes: Sealed Classes restrict the set of classes that can extend or implement them, enhancing code maintainability and security by explicitly defining the permitted subclasses.

// Traditional class hierarchy
public abstract class Shape {}

public class Circle extends Shape {}
public class Rectangle extends Shape {}
// ...

// With Sealed Classes (Java 17)
public sealed abstract class Shape permits Circle, Rectangle {}
public final class Circle extends Shape {}
public final class Rectangle extends Shape {}        

Java 8 Streams



Here are the list of functions available in stream:

  • map: Transforms each element in the stream based on a provided function.

// need Upper case words list
List<String> words = Arrays.asList("apple", "banana", "cherry");
List<String> uppercasedWords = words.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());        

  • filter: Selects elements from the stream based on a specified condition.

// need only even numbers
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
        

  • forEach: Performs an action on each element in the stream.

// print all Names
List<String> names = Arrays.asList("Ram", "Bob", "Charlie");
names.stream()
    .forEach(System.out::println);
        

  • peek: Provides a way to inspect elements as they flow past a certain point in the stream without altering the stream.

// print the stream in  the process
List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubledValues = values.stream()
    .peek(v -> System.out.println("Doubling: " + v))
    .map(v -> v * 2)
    .collect(Collectors.toList());
        

  • reduce: Combines elements in the stream into a single result using a specified operation.

//Sum the given int list
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .reduce(0, Integer::sum);
        

  • collect: Accumulates elements into a collection or other data structure.

List<String> words = Arrays.asList("apple", "banana", "cherry");
Map<Integer, List<String>> wordsByLength = words.stream()
    .collect(Collectors.groupingBy(String::length));        

  • flatMap: Flattens nested streams or maps elements to streams and then flattens them.

//merge the lists
List<List<Integer>> nestedLists = Arrays.asList(Arrays.asList(1, 2,4,5), Arrays.asList(3, 4,4,5,6));
List<Integer> flatList = nestedLists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());        

  • distinct: Removes duplicate elements from the stream.

//Distinct
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4);
List<Integer> distinctNumbers = numbers.stream()
    .distinct()
    .collect(Collectors.toList());
        

  • sorted: Sorts the elements of the stream.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> sortedNames = names.stream()
    .sorted()
    .collect(Collectors.toList());        

  • min: Finds the minimum element based on a provided comparator.

List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 7);
Optional<Integer> minNumber = numbers.stream()
    .min(Integer::compare);
        

  • max: Finds the maximum element based on a provided comparator.

List<Integer> numbers = Arrays.asList(5, 2, 9, 1, 7);
Optional<Integer> maxNumber = numbers.stream()
    .max(Integer::compare);
        

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

List<String> words = Arrays.asList("apple", "banana", "cherry");
long wordCount = words.stream()
    .count();
        

  • anyMatch: Checks if at least one element in the stream matches a given condition. - exits the loop once match found

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean hasEvenNumber = numbers.stream()
    .anyMatch(n -> n % 2 == 0);
        

  • allMatch: Checks if all elements in the stream satisfy a given condition.

List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
boolean allEven = numbers.stream()
    .allMatch(n -> n % 2 == 0);
        

  • noneMatch: Checks if none of the elements in the stream satisfy a given condition.

List<Integer> numbers = Arrays.asList(1, 3, 5, 7, 9);
boolean noEvenNumber = numbers.stream()
    .noneMatch(n -> n % 2 == 0);
        

  • findFirst: Returns the first element of the stream.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> firstName = names.stream()
    .findFirst();
        

  • findAny: Returns any element of the stream (useful for parallel streams).

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> anyName = names.parallelStream()
    .findAny();
        

  • skip: Skips a specified number of elements in the stream.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> afterSkipping = numbers.stream()
    .skip(2)
    .collect(Collectors.toList());
        

  • limit: Limits the stream to a specified maximum number of elements.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> limitedNumbers = numbers.stream()
    .limit(3)
    .collect(Collectors.toList());
        

  • toArray: Converts the stream into an array.

List<String> words = Arrays.asList("apple", "banana", "cherry");
String[] wordArray = words.stream()
    .toArray(String[]::new);
        

  • of: Creates a stream of specified elements.

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

  • range: Generates a stream of integers within a specified range.

IntStream.range(1, 6) // Generates 1, 2, 3, 4, 5
        

  • empty: Creates an empty stream.

Stream<String> emptyStream = Stream.empty();
        

  • concat: Combines two streams into one.

Stream<Integer> stream1 = Stream.of(1, 2, 3);
Stream<Integer> stream2 = Stream.of(4, 5, 6);
Stream<Integer> combinedStream = Stream.concat(stream1, stream2);
        

  • iterate: Generates an infinite stream by applying a function to each element.

Stream.iterate(1, n -> n * 2) // Generates 1, 2, 4, 8, 16, ...
        

  • generate: Generates an infinite stream using a Supplier.

Stream.generate(() -> "Hello, World!") // Generates "Hello, World!", "Hello, World!", ...        


  • Filtering Data: You can use streams to filter elements in a collection based on specific criteria. For example, filtering a list of employees to find those with a certain salary range or job title.

// Filtering payments above a certain amount
List<Payment> highValuePayments = payments.stream()
        .filter(payment -> payment.getAmount() > 1000.0)
        .collect(Collectors.toList());        

  • Mapping Data: Streams allow you to transform elements in a collection. You can map elements to another type, format, or extract specific attributes. For instance, converting a list of user objects to their corresponding usernames.

// Extracting payment IDs from a list of payments
List<String> paymentIds = payments.stream()
        .map(Payment::getPaymentId)
        .collect(Collectors.toList());
        

  • Sorting Data: Streams make it easy to sort elements in a collection based on various criteria, such as alphabetical order, numerical value, or custom comparators.

// Sorting payments by date in ascending order
List<Payment> sortedPayments = payments.stream()
        .sorted(Comparator.comparing(Payment::getPaymentDate))
        .collect(Collectors.toList());


        // Sorting by last name in ascending order, then by age in ascending order
        List<Person> sortedPeople = people.stream()
            .sorted(
                Comparator.comparing(Person::getLastName)
                    .thenComparing(Person::getAge)
            )
            .collect(Collectors.toList());        

  • Aggregating Data: You can use streams to compute summary statistics like sum, average, minimum, and maximum values for numeric data within a collection.

  List<Payment> payments = // List of Payment objects

        // Calculate the average payment amount using stream aggregation
        OptionalDouble averageAmount = payments.stream()
            .mapToDouble(Payment::getAmount)
            .average();

// Calculating the total sum of payments
double totalAmount = payments.stream()
        .mapToDouble(Payment::getAmount)
        .sum();

  Optional<Double> largestAmount = payments.stream()
            .map(Payment::getAmount)
            .max(Double::compareTo);
        

  • Grouping and Partitioning: Streams enable you to group elements by specific attributes or partition them into subsets based on conditions. This is helpful for creating summary reports or organizing data.

// Grouping payments by payment method
Map<PaymentMethod, List<Payment>> paymentsByMethod = payments.stream()
        .collect(Collectors.groupingBy(Payment::getPaymentMethod));

   List<Payment> payments = // List of Payment objects
        double thresholdAmount = 100.0; // Threshold amount

        // Partition payments into two groups based on the threshold amount using stream partitioningBy aggregation
        Map<Boolean, List<Payment>> partitionedPayments = payments.stream()
            .collect(Collectors.partitioningBy(payment -> payment.getAmount() > thresholdAmount));
        

  • Searching Data: You can use streams to find elements that match certain conditions. For example, searching for specific keywords in a list of documents.

// Searching for a payment by payment ID
Optional<Payment> foundPayment = payments.stream()
        .filter(payment -> payment.getPaymentId().equals(targetPaymentId))
        .findFirst();
        

  • Combining Data: Streams allow you to combine or concatenate data from multiple sources or collections into a single stream.

 List<Payment> payments1 = new ArrayList<>();
        payments1.add(new Payment("Payment1", 100.0));
        payments1.add(new Payment("Payment2", 200.0));

        List<Payment> payments2 = new ArrayList<>();
        payments2.add(new Payment("Payment3", 300.0));
        payments2.add(new Payment("Payment4", 400.0));

        // Combining payment data from two collections into a single stream
        Stream<Payment> combinedStream = Stream.concat(payments1.stream(), payments2.stream());

        // Collecting the combined stream into a list of payments
        List<Payment> combinedPayments = combinedStream.collect(Collectors.toList());        

Flattening Data

 List<Invoice> invoices = new ArrayList<>();
        invoices.add(new Invoice("Invoice1", List.of(new Payment("Payment1"), new Payment("Payment2"))));
        invoices.add(new Invoice("Invoice2", List.of(new Payment("Payment3"), new Payment("Payment4"))));

        // Flatten the list of payments using flatMap
        List<Payment> allPayments = invoices.stream()
            .flatMap(invoice -> invoice.getPayments().stream())
            .collect(Collectors.toList());
        

  • Chaining Operations: One of the strengths of streams is the ability to chain multiple operations together, creating complex data processing pipelines. This is often used to perform a sequence of transformations on data.

// Chaining filter, mapping, and aggregation operations
double averageAmount = payments.stream()
        .filter(payment -> payment.getAmount() > 500.0)
        .mapToDouble(Payment::getAmount)
        .average()
        .orElse(0.0);
        

  • Parallel Processing: Java 8 introduced parallel streams, which can significantly speed up data processing for large datasets by leveraging multi-core processors.

// Parallel processing for calculating total amount
double totalAmount = payments.parallelStream()
        .mapToDouble(Payment::getAmount)
        .sum();
        

  • I/O Operations: Streams can be used for reading from and writing to files, making it easier to perform file-related operations.

// Reading payments from a file and processing using streams
List<Payment> payments = Files.lines(Paths.get("payments.txt"))
        .map(Payment::fromCsvLine)
        .collect(Collectors.toList());
        

  • Database Operations: Java 8 streams can be used to work with databases, simplifying the retrieval and manipulation of database records.

// Retrieving payments from a database and processing using streams
List<Payment> payments = paymentRepository.findAll()
        .stream()
        .filter(payment -> payment.getAmount() > 100.0) //NOT RECOMENDED 
        .collect(Collectors.toList());
        

  • Data Validation: Streams can be used to validate data by applying checks or rules to each element in a collection.

      List<Payment> payments = new ArrayList<>();
        payments.add(new Payment("Payment1", 100.0));
        payments.add(new Payment("Payment2", -50.0)); // Negative amount
        payments.add(new Payment("Payment3", 200.0));

        // Using streams to validate payment data
        boolean allValid = payments.stream()
                .allMatch(payment -> payment.getAmount() >= 0.0);
        

  • Creating Data: Streams offer ways to generate data, such as ranges of numbers, random values, or repeated elements.

  // Generating a range of payment amounts from $100 to $500 with a step of $100
        List<Payment> generatedPayments = Stream.iterate(100.0, amount -> amount <= 500.0, amount -> amount + 100.0)
                .map(amount -> new Payment("Payment" + amount, amount))
                .collect(Collectors.toList());
          

  • Filtering Null Values: Streams are handy for filtering out null or empty values from a collection, ensuring data quality.

 // Sample payment data with null and empty values
        List<Payment> payments = new ArrayList<>();
        payments.add(new Payment("Payment1", 100.0));
        payments.add(null); // Null value
        payments.add(new Payment("Payment3", 0.0)); // Empty amount

        // Using streams to filter out null and empty payment data
        List<Payment> validPayments = payments.stream()
                .filter(Objects::nonNull) // Filter out null payments
                .filter(payment -> payment.getAmount() > 0.0) // Filter out empty payments
                .collect(Collectors.toList());        

  • Event Processing: Streams can be applied in event-driven systems to filter, transform, and process events as they occur.

 // Sample event data representing events in an event-driven system
        List<Event> events = new ArrayList<>();
        events.add(new Event("Event1", EventType.LOGIN, "User1"));
        events.add(new Event("Event2", EventType.LOGOUT, "User2"));
        events.add(new Event("Event3", EventType.PURCHASE, "User1"));

        // Using streams to filter and transform events
        List<String> filteredUserEvents = events.stream()
                .filter(event -> event.getType() == EventType.LOGIN)
                .map(event -> "User: " + event.getUsername() + " logged in")
                .collect(Collectors.toList());        

To view or add a comment, sign in

More articles by RamiReddy P.

  • Monoliths are under rated!

    Despite the rise of microservices and other distributed architectures, monolithic architecture is still a good choice…

    3 Comments
  • AWS Lambda Best Practices

    Lambda pricing is calculated as a combination of: 1. Total number of requests 2.

  • Design Principles - Java

    Design principles are fundamental guidelines and best practices that help software developers create well-structured…

    1 Comment

Others also viewed

Explore content categories