Java 8 Deep Dive

Introduction

Java has been a cornerstone of the programming world since its inception, evolving through various iterations to meet the demands of developers and businesses alike. From its humble beginnings to the release of Java 7, the language has continually adapted to address emerging challenges and opportunities in software development. However, it was with the arrival of Java 8 that the programming landscape witnessed a seismic shift. The release of Java 8 brought with it a wave of excitement and anticipation, earning its place as a true game-changer in the realm of Java development.

In this article, we’ll embark on a journey through the evolution of Java, exploring its trajectory from inception to Java 7. We’ll delve into the factors that fueled the hype surrounding Java 8 and examine what exactly made it such a significant milestone in the Java ecosystem.

Furthermore, we’ll provide an overview of the key features introduced in Java 8, offering a tantalizing glimpse into the powerful capabilities that await developers. From lambdas to streams, Java 8 revolutionized the way we write code, unlocking new levels of expressiveness, efficiency, and flexibility.

So, fasten your seatbelts as we unravel the story behind Java 8 and uncover the transformative features that continue to shape the future of Java development.

Chapter 1: Lambda Expressions

What are Lambda Expressions?

Lambda expressions, a pivotal feature introduced in Java 8, revolutionized the way developers write code by providing a concise syntax for representing anonymous functions. In essence, a lambda expression encapsulates a block of code that can be passed around and executed at a later time, without the need for a formal method declaration.

Why use Lambda Expressions: Before and after scenarios

Before the introduction of lambda expressions, certain programming constructs in Java, such as event handling and functional programming patterns, often required verbose anonymous inner classes. This verbosity not only cluttered the code but also made it harder to grasp the intent of the program. With lambda expressions, developers can express the same functionality in a more streamlined and intuitive manner. By reducing the boilerplate code associated with anonymous inner classes, lambda expressions enhance code readability and maintainability.

Consider the example of sorting a list of strings in Java prior to Java 8:

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

// Prior to Java 8
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});

Compare the above approach to sorting with the use of lambda expressions:

// With Lambda Expressions (Java 8+)
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));

The latter example is not only more concise but also clearer in expressing the sorting logic.

Lambda expressions provide a significant improvement in code elegance and developer productivity, making Java more competitive in the rapidly evolving landscape of programming languages. They enable developers to write cleaner, more expressive code, leading to enhanced readability and maintainability of Java applications.

Deep dive into syntax and functional interfaces

Lambda expressions in Java consist of parameters, an arrow (->), and a body. The parameters specify the input to the function, the arrow separates the parameters from the body, and the body defines the behavior of the function. The syntax is concise and expressive, allowing developers to define functions inline without the need for verbose anonymous inner classes.

Functional interfaces play a crucial role in the use of lambda expressions. A functional interface is an interface that contains exactly one abstract method. Lambda expressions can be used to provide an implementation for this single abstract method, effectively creating an instance of the functional interface. Java provides several built-in functional interfaces, such as Predicate, Consumer, Function, and Supplier, which can be leveraged in conjunction with lambda expressions to perform common functional programming tasks.

Real-world applications: How Lambdas can make your code more readable and efficient

Lambda expressions have a wide range of real-world applications, making code more readable and efficient across various domains. One common use case is in collection processing, where lambda expressions can be used in conjunction with streams to perform data manipulation operations such as filtering, mapping, and reducing.

Consider the following example, where lambda expressions are used to filter a list of strings based on a predicate:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// Using lambda expressions with streams to filter names starting with 'A'
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());

In this example, the lambda expression name -> name.startsWith("A") serves as the predicate for filtering the list. This concise syntax makes the code easier to understand and maintain compared to traditional approaches using loops or anonymous inner classes.

Lambda expressions can also be used in event handling, concurrency, and functional programming paradigms, among other areas. By leveraging lambda expressions, developers can write cleaner, more expressive code that is both readable and efficient, ultimately leading to improved software quality and developer productivity.

Code snippets and examples: From simple to complex use cases

Let’s delve deeper into lambda expressions with a range of examples that illustrate their flexibility and utility in Java programming.

  • Simple Lambda Expression: This lambda expression represents a simple behavior of printing a message. It captures the essence of a function without needing to define a separate method or class.
// Simple lambda expression to print a message
Runnable r = () -> System.out.println("Hello, Lambda!");
  • Lambda Expression with Parameters: Here, Calculator is a functional interface with a method calculate(int a, int b). The lambda expression (a, b) -> a + b succinctly defines the behavior of the calculate method.
// Lambda expression with parameters to add two numbers
Calculator adder = (a, b) -> a + b;
int result = adder.calculate(10, 5); // result will be 15
  • Lambda Expression with Body: This example demonstrates a lambda expression with a body. It allows for more complex logic within the expression, making it suitable for tasks requiring multiple statements.
// Lambda expression with body to find the maximum of two numbers
MaxFinder maxFinder = (a, b) -> {
    if (a > b) {
        return a;
    } else {
        return b;
    }
};
int max = maxFinder.findMax(10, 20); // max will be 20
  • Real-world Application: Filtering a List using Lambda Expression: In this example, lambda expressions are used with Java streams to filter a list of names based on a predicate. This showcases how lambda expressions streamline common data processing tasks.
// Filtering a list of names starting with 'J'
List<String> names = Arrays.asList("Java", "Python", "JavaScript", "C++");
List<String> filteredNames = names.stream()
                                  .filter(name -> name.startsWith("J"))
                                  .collect(Collectors.toList());
// filteredNames will contain ["Java", "JavaScript"]
  • Sorting a List using Comparator with Lambda Expression: Lambda expressions can also be used with interfaces like Comparator to define custom sorting logic. This example sorts a list of strings based on their length in ascending order.
// Sorting a list of strings based on their length
List<String> words = Arrays.asList("banana", "apple", "orange", "grape");
Collections.sort(words, (s1, s2) -> s1.length() - s2.length());
// words will be sorted by length: ["apple", "grape", "banana", "orange"]

These examples highlight the versatility and expressiveness of lambda expressions in Java. They enable developers to write concise and readable code while tackling a wide range of programming tasks, from simple operations to complex data processing and manipulation.

Chapter 2: Stream API

Introduction to Stream API: Revolutionizing data processing in Java

The Stream API, introduced in Java 8, heralded a new era in Java programming by providing a modern and efficient way to process collections of data. Streams allow developers to express complex data processing tasks in a declarative and functional manner, leveraging the power of lambda expressions and method chaining.

Streams are designed to facilitate parallelism, enabling efficient utilization of multi-core processors for improved performance. They encourage a more functional style of programming, where data transformations are expressed as a series of operations on immutable data structures.

With streams, developers can perform a wide range of data processing tasks, including filtering, mapping, sorting, and aggregation, with ease. Streams are also highly composable, allowing developers to combine multiple operations into a single pipeline for maximum flexibility and expressiveness.

Stream operations: Intermediate vs. Terminal operations

Streams in Java offer a rich set of operations that can be classified into two main categories: intermediate operations and terminal operations.

Intermediate operations are those that transform or filter the elements of a stream, producing a new stream as a result. These operations are lazy, meaning they do not execute until a terminal operation is invoked. Examples of intermediate operations include map, filter, distinct, sorted, and flatMap.

Terminal operations, on the other hand, consume the elements of a stream and produce a result or side effect. Once a terminal operation is invoked, the stream is consumed, and no further operations can be performed on it. Common terminal operations include forEach, collect, reduce, count, and anyMatch.

Understanding the distinction between intermediate and terminal operations is essential for effectively working with streams in Java. By chaining together intermediate operations and terminating the stream with a terminal operation, developers can create powerful and efficient data processing pipelines tailored to their specific needs.

Furthermore, streams support parallel execution, allowing operations to be automatically parallelized for improved performance on multi-core processors. However, developers must be mindful of potential thread-safety issues when working with parallel streams to ensure correct behavior in concurrent environments.

A closer look at Stream methods: filter, map, sorted, collect, and more

The Stream API in Java 8 offers a plethora of methods to manipulate and process data seamlessly. Let’s delve deeper into some of the most commonly used stream methods:

  • filter:
    • The filter method is a fundamental operation that selects elements from a stream based on a specified predicate.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> filteredNames = names.stream()
                                  .filter(name -> name.length() > 4)
                                  .collect(Collectors.toList());
// filteredNames will contain ["Alice", "Charlie", "David"]
  • map:
    • The map method transforms each element of a stream using the provided function.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> nameLengths = names.stream()
                                .map(String::length)
                                .collect(Collectors.toList());
// nameLengths will contain [5, 3, 7]
  • sorted:
    • The sorted method sorts the elements of a stream based on their natural order or a specified comparator.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> sortedNames = names.stream()
                               .sorted()
                               .collect(Collectors.toList());
// sortedNames will contain ["Alice", "Bob", "Charlie"]
  • collect:
    • The collect method accumulates the elements of a stream into a collection or performs a reduction operation.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Set<String> nameSet = names.stream()
                           .collect(Collectors.toSet());
// nameSet will contain ["Alice", "Bob", "Charlie"]

These methods, combined with others like distinct, flatMap, and reduce, provide a robust toolkit for data manipulation and transformation with streams in Java.

Parallel Streams: Harnessing the power of multicore processors

Parallel streams enable developers to exploit the processing power of multicore processors for improved performance. By automatically partitioning the stream into multiple substreams and processing them concurrently, parallel streams can dramatically reduce the time required to perform computationally intensive tasks.

To create a parallel stream, simply invoke the parallel() method on an existing stream:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
long count = names.parallelStream()
.filter(name -> name.length() > 4)
.count();

However, it’s essential to use parallel streams judiciously, as they may not always yield performance gains and can introduce additional overhead in certain scenarios. Moreover, developers must ensure that their stream operations are thread-safe to avoid potential concurrency issues.

By understanding the nuances of parallel streams and strategically employing them, developers can leverage the full potential of multicore processors to optimize the performance of their Java applications.

Chapter 3: Date and Time API

The need for a new Date and Time API

Historically, handling date and time in Java has been cumbersome and error-prone, largely due to the limitations of the legacy java.util.Date and java.util.Calendar classes. These classes suffered from various issues, including mutability, lack of thread safety, and poor design choices, making them difficult to work with effectively.

As software systems became increasingly complex and distributed, the shortcomings of the existing Date and Time API became more apparent. Developers often resorted to third-party libraries or custom solutions to overcome these limitations, leading to fragmentation and inconsistency across codebases.

Recognizing the need for a modern and robust Date and Time API, Java introduced the new Date and Time API in Java 8. This new API addresses many of the shortcomings of its predecessor while providing a more intuitive and feature-rich framework for working with dates, times, and time zones.

Key classes in the new API: LocalDate, LocalTime, LocalDateTime, ZonedDateTime

The new Date and Time API introduces several key classes that serve as the building blocks for representing date and time information:

  • LocalDate:
    • Represents a date without time zone information.
LocalDate date = LocalDate.now(); // Represents the current date
  • LocalTime:
    • Represents a time without time zone information.
LocalTime time = LocalTime.now(); // Represents the current time
  • LocalDateTime:
    • Represents a date and time without time zone information.
LocalDateTime dateTime = LocalDateTime.now(); // Represents the current date and time
  • ZonedDateTime:
    • Represents a date, time, and time zone.
ZoneId zoneId = ZoneId.of("America/New_York");
ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneId); // Represents the current date, time, and time zone

These classes provide a more flexible and intuitive way to work with date and time information in Java, enabling developers to write cleaner and more reliable code for handling temporal data.

Manipulating, parsing, and formatting dates and times

The Date and Time API in Java 8 offers robust support for manipulating, parsing, and formatting dates and times, providing developers with powerful tools to handle temporal data effectively.

  • Manipulating dates and times:
    • The Date and Time API provides methods for performing various operations on dates and times, such as adding or subtracting days, months, or years.
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
LocalDate nextWeek = today.plusWeeks(1);
  • Parsing dates and times:
    • The API includes parsers for converting strings to date and time objects, allowing developers to parse dates and times in various formats.
LocalDate date = LocalDate.parse("2022-04-15");
LocalTime time = LocalTime.parse("10:30:00");
  • Formatting dates and times:
    • Formatting dates and times into strings is straightforward with the Date and Time API, using predefined or custom patterns.
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
  • Temporal adjusters:
    • The API includes built-in temporal adjusters for common adjustments to dates, such as finding the first or last day of the month.
LocalDate firstDayOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDayOfMonth = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
Legacy Date/Time support and interoperability

Despite the introduction of the new Date and Time API, Java continues to support legacy date and time classes (java.util.Date, java.util.Calendar) for backward compatibility. The new API includes utility methods for converting between legacy and modern date and time objects, ensuring interoperability between the two.

  • Converting legacy objects to new API:
    • Use the toInstant() method to convert java.util.Date objects to java.time.Instant, which can then be further converted to other modern date and time classes.
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
  • Converting new API objects to legacy:
    • Use the from() method to convert modern date and time objects to legacy java.util.Date objects.
LocalDateTime dateTime = LocalDateTime.now();
Date legacyDate = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());

These interoperability features enable seamless migration from legacy date and time classes to the new Date and Time API while ensuring compatibility with existing codebases.

Use cases and examples: Simplifying date and time manipulation in your projects

The Date and Time API introduced in Java 8 revolutionized how developers handle temporal data in Java applications. Let’s delve into some practical examples and use cases that showcase the versatility and simplicity of the Date and Time API:

  • Calculating durations:
    • The Duration class allows for easy calculation of time durations between two points in time, facilitating performance monitoring and profiling.
Instant start = Instant.now();
// Perform some time-consuming operation
Instant end = Instant.now();
Duration duration = Duration.between(start, end);
System.out.println("Time taken: " + duration.getSeconds() + " seconds");
  • Working with time zones:
    • The ZoneId and ZonedDateTime classes simplify handling of time zones, enabling developers to accurately represent and manipulate times across different regions.
ZoneId newYorkZone = ZoneId.of("America/New_York");
ZonedDateTime newYorkTime = ZonedDateTime.now(newYorkZone);
System.out.println("Current time in New York: " + newYorkTime);
  • Scheduling tasks:
    • The Date and Time API seamlessly integrates with concurrency utilities, allowing for easy scheduling of tasks at specific times or intervals.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable task = () -> System.out.println("Task executed at: " + LocalDateTime.now());
ScheduledFuture<?> future = scheduler.schedule(task, 5, TimeUnit.SECONDS);
  • Parsing and formatting:
    • The DateTimeFormatter class provides robust support for parsing and formatting dates and times, accommodating a wide range of date and time formats.
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
System.out.println("Formatted date and time: " + formattedDateTime);
  • Temporal adjusters:
    • Built-in temporal adjusters offer convenient methods for adjusting dates, such as finding the next or previous working day, streamlining date manipulation tasks.
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));

By embracing the Date and Time API in Java, developers can simplify complex date and time manipulation tasks, enhance code readability, and ensure accuracy in handling temporal data across various scenarios.

Chapter 4: Default and Static Methods in Interfaces

Evolution of interfaces in Java: From Java 7 to Java 8

In Java, interfaces have evolved significantly over time, particularly with the introduction of default and static methods in Java 8. Prior to Java 8, interfaces could only declare abstract methods, serving as contracts that classes implementing them had to adhere to strictly. While interfaces provided a way to achieve abstraction and polymorphism, they lacked the ability to evolve over time without breaking existing code.

With the advent of Java 8, interfaces gained the ability to define default and static methods, bringing about a paradigm shift in interface design. Default methods allow interfaces to provide implementations for methods, while static methods enable interfaces to define utility methods that can be called without an instance of the implementing class.

Default methods: Breaking the single inheritance barrier

Default methods in interfaces break the single inheritance barrier by allowing interfaces to contain concrete implementations for methods. This feature was introduced primarily to address the “diamond problem,” where the same method could be inherited from multiple superinterfaces, leading to ambiguity.

  • Defining default methods:
    • Default methods are declared using the default keyword in the interface definition.
public interface MyInterface {
    default void myMethod() {
        System.out.println("Default implementation of myMethod");
    }
}
  • Implementing default methods:
    • Classes implementing the interface can choose to override default methods if needed, but they are not required to do so.
public class MyClass implements MyInterface {
    // No need to provide an implementation for myMethod
}
  • Benefits of default methods:
    • Default methods allow interfaces to evolve over time without breaking backward compatibility, enabling the addition of new methods without affecting existing code.
    • They provide a mechanism for introducing new functionality to existing interfaces without requiring changes to implementing classes.
    • Default methods facilitate the creation of mixins, allowing interfaces to provide reusable behavior to unrelated classes.

Default methods have proven to be a powerful addition to the Java language, enabling greater flexibility and extensibility in interface design. However, developers must use them judiciously and adhere to established best practices to ensure that default methods are used appropriately and do not introduce unintended complexities or conflicts.

Static methods in interfaces: Utility methods right where you need them

In Java 8, along with default methods, interfaces gained the ability to declare static methods. Static methods in interfaces provide a way to include utility methods directly within the interface itself, offering convenient access to common functionality without the need for implementing classes to provide their own implementations.

  • Defining static methods:
    • Static methods in interfaces are declared using the static keyword.
public interface MyInterface {
    static void myStaticMethod() {
        System.out.println("Static method in MyInterface");
    }
}
  • Accessing static methods:
    • Static methods in interfaces can be accessed using the interface name.
MyInterface.myStaticMethod();
  • Utility methods in interfaces:
    • Static methods in interfaces are often used to provide utility methods that are closely related to the interface’s purpose.
public interface MathOperations {
    static int add(int a, int b) {
        return a + b;
    }
}
Use cases, benefits, and potential pitfalls

Static methods in interfaces offer several benefits and use cases, but they also come with potential pitfalls that developers should be aware of.

  1. Use cases:
    • Providing utility methods related to the interface’s functionality.
    • Facilitating code organization and encapsulation by keeping related methods together.
    • Offering a convenient way to define factory methods for creating instances of implementing classes.
  2. Benefits:
    • Encouraging code reuse by providing common functionality directly within the interface.
    • Simplifying the API by offering a single point of access to utility methods.
    • Enhancing code readability and maintainability by associating utility methods with the interface they are related to.
  3. Potential pitfalls:
    • Overuse of static methods in interfaces can lead to interface pollution and violate the interface’s intended purpose.
    • Static methods cannot be overridden by implementing classes, limiting flexibility in certain scenarios.
    • Care must be taken when modifying existing static methods in interfaces to avoid breaking backward compatibility with implementing classes.

By leveraging static methods in interfaces judiciously and adhering to established best practices, developers can enhance code organization, improve code reuse, and create more maintainable and readable Java codebases.

Code examples: Enhancing interfaces with default and static methods

Default and static methods in interfaces have become powerful tools for enhancing the flexibility and utility of interfaces in Java. Let’s explore some code examples to see how these features can be used to enrich interfaces:

  • Default methods:
public interface Vehicle {
    default void start() {
        System.out.println("Vehicle starting...");
    }

    default void stop() {
        System.out.println("Vehicle stopping...");
    }
}
public class Car implements Vehicle {
    // No need to implement start() and stop() methods
}
  • Static methods:
public interface MathOperations {
    static int add(int a, int b) {
        return a + b;
    }

    static int subtract(int a, int b) {
        return a - b;
    }
}
public class Calculator {
    public static void main(String[] args) {
        int sum = MathOperations.add(5, 3);
        int difference = MathOperations.subtract(5, 3);
        System.out.println("Sum: " + sum);
        System.out.println("Difference: " + difference);
    }
}
  • Combining default and static methods:
public interface Printable {
    default void print(String message) {
        System.out.println("Printing: " + message);
    }

    static void printTwice(String message) {
        System.out.println("Printing twice: " + message);
        System.out.println("Printing twice: " + message);
    }
}
public class Printer implements Printable {
    public static void main(String[] args) {
        Printable.printTwice("Hello");
        // Output:
        // Printing twice: Hello
        // Printing twice: Hello
    }
}

These examples demonstrate how default and static methods can be used to add functionality to interfaces without breaking existing code or requiring implementing classes to provide their own implementations. Default methods allow interfaces to define common behavior, while static methods provide utility methods that are tightly related to the interface’s purpose. By incorporating these features into interface design, developers can create more versatile and reusable APIs in Java.

Chapter 5: Optional Class

Dealing with null in Java: The problem statement

In Java programming, dealing with null values can be a source of errors and bugs. Null references can lead to NullPointerExceptions (NPEs), which are one of the most common runtime exceptions in Java applications. The problem becomes more significant as applications grow in complexity and size, making it challenging to track down and fix null-related issues.

Consider the following example:

String name = null;
int length = name.length(); // Throws NullPointerException

In this example, attempting to call the length() method on a null reference results in a NullPointerException at runtime.

Introduction to Optional: A new way to handle nullable values

To address the issue of null references and NullPointerExceptions, Java introduced the Optional class in Java 8. Optional is a container object that may or may not contain a non-null value. It provides a more expressive way to handle nullable values and encourages developers to handle null cases explicitly.

  • Creating Optional objects: This creates an Optional object containing the specified value, which may be null.
Optional<String> optionalName = Optional.ofNullable(name);
  • Accessing values safely: The isPresent() method checks if the Optional contains a non-null value, and get() retrieves the value if present.
if (optionalName.isPresent()) {
    String actualName = optionalName.get();
    int length = actualName.length();
    System.out.println("Length of name: " + length);
} else {
    System.out.println("Name is null");
}
  • Handling null cases gracefully: The orElse() method returns the value if present, or the specified default value if the Optional is empty.
String defaultName = optionalName.orElse("John Doe");

Optional provides a cleaner and more declarative way to handle nullable values in Java, helping to reduce the likelihood of NullPointerExceptions and improving code robustness. However, it’s essential to use Optional judiciously and not overuse it, as excessive Optional usage can lead to overly verbose and convoluted code.

Methods in Optional: isPresent, ifPresent, orElse, orElseGet, and more

The Optional class in Java provides a variety of methods for working with nullable values in a more expressive and concise manner. Let’s explore some of the key methods offered by Optional:

  • isPresent():
    • Checks if the Optional contains a non-null value.
Optional<String> optionalName = Optional.ofNullable(name);
if (optionalName.isPresent()) {
    // Value is present
} else {
    // Value is absent
}
  • ifPresent(Consumer<? super T> consumer):
    • Executes the specified consumer function if the Optional contains a non-null value.
Optional<String> optionalName = Optional.ofNullable(name);
optionalName.ifPresent(value -> System.out.println("Name: " + value));
  • orElse(T other):
    • Returns the value if present, otherwise returns the specified default value.
String defaultName = optionalName.orElse("John Doe");
  • orElseGet(Supplier<? extends T> supplier):
    • Returns the value if present, otherwise returns the result of calling the specified supplier function.
String defaultName = optionalName.orElseGet(() -> generateDefaultName());
  • orElseThrow(Supplier<? extends X> exceptionSupplier):
    • Returns the value if present, otherwise throws an exception produced by the specified supplier function.
String actualName = optionalName.orElseThrow(() -> new NoSuchElementException("Name is not present"));
Practical applications: Reducing NullPointerExceptions in your code

The Optional class offers a practical solution for reducing the occurrence of NullPointerExceptions in Java code, especially when dealing with potentially null values returned from methods or obtained from external sources.

  • Method return values:
    • Use Optional as a return type for methods that may or may not return a value.
public Optional<String> findNameById(int id) {
    // Logic to find name by id
    return Optional.ofNullable(name); // Return Optional<String>
}
  • Method parameters:
    • Declare method parameters as Optional to indicate that they may be absent.
public void processName(Optional<String> optionalName) {
    optionalName.ifPresent(name -> System.out.println("Name: " + name));
}
  • External sources:
    • Use Optional to handle potentially null values obtained from external sources, such as databases or user input.
Optional<String> userInput = Optional.ofNullable(scanner.nextLine());

By incorporating Optional into your codebase and leveraging its methods effectively, you can significantly reduce the likelihood of encountering NullPointerExceptions and write more robust and resilient Java applications.

Code snippets: From basics to advanced uses of Optional

Let’s dive into various code snippets showcasing the usage of Optional, ranging from basic scenarios to more advanced use cases:

  • Basic Usage:
Optional<String> optionalName = Optional.of("John");
// Check if value is present
if (optionalName.isPresent()) {
    String name = optionalName.get();
    System.out.println("Name: " + name);
} else {
    System.out.println("Name is absent");
}
  • ifPresent() method:
optionalName.ifPresent(name -> System.out.println("Name: " + name));
  • orElse() method:
String defaultName = optionalName.orElse("Unknown");
  • orElseGet() method:
String defaultName = optionalName.orElseGet(() -> generateDefaultName());
  • orElseThrow() method:
String actualName = optionalName.orElseThrow(() -> new NoSuchElementException("Name not found"));
  • Chaining methods:
Optional<String> upperCaseName = optionalName.map(String::toUpperCase);
  • Filtering values:
Optional<String> filteredName = optionalName.filter(name -> name.length() > 5);
  • Combining Optionals:
Optional<Integer> optionalLength = optionalName.map(String::length);
  • Handling null values from methods:
public Optional<String> findNameById(int id) {
    // Logic to find name by id
    return Optional.ofNullable(name); // Return Optional<String>
}
  • Advanced usage with flatMap():
Optional<String> flatMappedName = optionalName.flatMap(this::findFullName);

These code snippets demonstrate various ways to use Optional effectively in Java applications, from handling null values to transforming and filtering data in a concise and expressive manner. By mastering the Optional class and its methods, developers can write cleaner, safer, and more robust code that is resilient to null-related issues.

Chapter 6: Nashorn JavaScript Engine

What is Nashorn, and why does it matter?

Nashorn, introduced in Java 8, represents a significant upgrade to Java’s capabilities in handling JavaScript within the Java Virtual Machine (JVM). Its importance stems from several key factors:

  1. Performance Boost:
    • Nashorn offers significant performance improvements over its predecessor, Rhino. It achieves this through its use of modern JavaScript optimization techniques and Just-In-Time (JIT) compilation, resulting in faster execution of JavaScript code.
  2. Compatibility with Modern JavaScript:
    • Nashorn supports modern JavaScript standards, including ECMAScript 5.1 and partial support for ECMAScript 6 (ES6). This allows Java developers to work with JavaScript code that utilizes the latest language features and syntax, enhancing interoperability between Java and JavaScript ecosystems.
  3. Enhanced Integration with Java:
    • Nashorn provides seamless integration between JavaScript and Java, allowing for bidirectional communication and interaction between the two languages. JavaScript code can directly access Java objects, methods, and classes, and vice versa, enabling developers to leverage the strengths of both languages within the same application.
  4. Embeddable Scripting Engine:
    • Nashorn serves as an embeddable scripting engine within Java applications, enabling developers to incorporate scripting capabilities into their Java-based projects. This empowers users to extend and customize application behavior through scripting, without the need for modifying or recompiling the Java codebase.
Integrating JavaScript with Java: Use cases and advantages

The integration of JavaScript with Java using Nashorn presents numerous use cases and advantages:

  1. Customization and Extension:
    • JavaScript enables dynamic customization and extension of Java applications. Developers can expose certain functionalities of their Java codebase to JavaScript, allowing users to tailor the behavior of the application to their specific needs through scripting.
  2. Server-Side Scripting and Middleware:
    • Nashorn facilitates server-side scripting within Java-based web applications, enabling the implementation of dynamic server-side logic in JavaScript. This is particularly useful for implementing middleware components, handling HTTP requests, and generating dynamic content on the server side.
  3. Business Rules and Workflows:
    • JavaScript can be employed to define and execute business rules, workflows, and decision-making logic within Java applications. This provides a flexible and agile approach to implementing business logic, allowing for easier updates and modifications without requiring changes to the Java codebase.
  4. User Interface (UI) Customization:
    • Nashorn enables dynamic UI customization in JavaFX applications by allowing developers to define UI behavior using JavaScript. This facilitates the creation of more interactive and responsive user interfaces, with the ability to modify UI elements and behavior at runtime based on user interactions.
  5. Testing and Prototyping:
    • JavaScript can serve as a valuable tool for testing and prototyping Java code, providing a lightweight and flexible environment for experimenting with algorithms, APIs, and application features. This can expedite the development process and facilitate rapid iteration and experimentation.

By leveraging Nashorn’s capabilities for integrating JavaScript with Java, developers can create more dynamic, flexible, and extensible applications that are capable of meeting the diverse needs and requirements of modern software development.

Running scripts from Java code: A how-to guide

Running JavaScript scripts from Java using Nashorn is a straightforward process. Follow these steps to execute JavaScript code from your Java application:

  • Create a ScriptEngine:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
  • Load JavaScript code from a file or a string:
try {
    FileReader reader = new FileReader("script.js");
    engine.eval(reader);
} catch (ScriptException | FileNotFoundException e) {
    e.printStackTrace();
}
  • Access variables and functions defined in the script:
try {
    engine.eval("var result = add(3, 5);");
    Object result = engine.get("result");
    System.out.println("Result: " + result);
} catch (ScriptException e) {
    e.printStackTrace();
}
  • Pass Java objects to the script and vice versa:
try {
    engine.put("javaVariable", javaObject);
    engine.eval("javaVariable.doSomething();");
} catch (ScriptException e) {
    e.printStackTrace();
}
  • Handle exceptions thrown by the script:
try {
    engine.eval("throw new Error('Script error');");
} catch (ScriptException e) {
    e.printStackTrace();
}
  • Execute JavaScript code inline:
try {
    engine.eval("print('Hello from JavaScript');");
} catch (ScriptException e) {
    e.printStackTrace();
}
Examples: Scripting applications using Nashorn

Let’s explore some examples of how Nashorn can be used to script Java applications:

  • Server-Side Processing:
// script.js
var data = getDataFromDatabase();
processData(data);
  • UI Customization in JavaFX:
// script.js
var button = new Button();
button.setText("Click me");
button.setOnAction(function() {
    print("Button clicked");
});
  • Business Logic Rules:
// script.js
var order = getOrderDetails();
if (order.totalAmount > 1000) {
    applyDiscount(order);
}
  • Dynamic Configuration:
// script.js
var config = {
    debugMode: true,
    maxConnections: 10
};
  • Testing and Prototyping:
// script.js
function fibonacci(n) {
    if (n <= 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}
print(fibonacci(10));

By incorporating Nashorn into your Java applications, you can leverage the power and flexibility of JavaScript to dynamically extend, customize, and script your Java applications for various use cases.

Chapter 7: New Tools and Miscellaneous Features

A new JavaScript engine: Enhancements and use cases

Java 8 introduced Nashorn, a new JavaScript engine that replaced Rhino. Nashorn brought significant enhancements and several use cases to the Java platform:

  1. Improved Performance:
    • Nashorn offers superior performance compared to Rhino due to its use of modern JavaScript optimization techniques and Just-In-Time (JIT) compilation. This results in faster execution of JavaScript code within the Java Virtual Machine (JVM).
  2. Compatibility with Modern JavaScript:
    • Nashorn supports modern JavaScript standards, including ECMAScript 5.1 and partial support for ECMAScript 6 (ES6). This allows Java developers to work with JavaScript code that utilizes the latest language features and syntax.
  3. Tighter Integration with Java:
    • Nashorn provides seamless integration between JavaScript and Java, allowing bidirectional communication and interaction between the two languages. JavaScript code can directly access Java objects, methods, and classes, enabling developers to leverage the strengths of both languages within the same application.
  4. Embeddable Scripting Engine:
    • Nashorn serves as an embeddable scripting engine within Java applications, enabling developers to incorporate scripting capabilities into their Java-based projects. This facilitates dynamic customization and extension of application behavior through scripting, without modifying or recompiling the Java codebase.
New profiling and diagnostic tools: jcmd, jdeps

Java 8 introduced several new profiling and diagnostic tools to aid developers in analyzing and troubleshooting Java applications:

  1. jcmd:
    • jcmd is a command-line tool that allows developers to interact with the JVM and perform various diagnostic and troubleshooting tasks. It provides a wide range of capabilities, including:
      • Monitoring JVM performance metrics such as CPU usage, memory usage, and thread activity.
      • Generating thread dumps and heap dumps for analyzing application state and diagnosing performance issues.
      • Triggering garbage collection and gathering detailed garbage collection statistics.
      • Enabling and disabling JVM logging and profiling options dynamically.
  2. jdeps:
    • jdeps is a command-line tool that analyzes Java class files and identifies dependencies between classes and packages. It helps developers understand the dependencies of their Java applications and detect potential issues such as missing or obsolete dependencies. Key features of jdeps include:
      • Generating a dependency graph of a Java application, showing the relationships between classes and packages.
      • Identifying dependencies on internal JDK APIs, which can lead to compatibility issues when migrating to newer Java versions.
      • Providing options to filter and customize the output to focus on specific dependencies or types of dependencies.

These new profiling and diagnostic tools introduced in Java 8 empower developers with powerful capabilities for analyzing, diagnosing, and optimizing Java applications, ultimately improving the overall quality and performance of Java software.

Compact Profiles: Tailoring your Java runtime

Java 8 introduced Compact Profiles, a feature aimed at reducing the footprint of the Java Runtime Environment (JRE) by providing smaller, more modular runtime configurations. Here’s a closer look:

  1. What are Compact Profiles?
    • Compact Profiles are predefined subsets of the Java SE platform APIs that cater to different deployment scenarios. These profiles include essential APIs required for common use cases, such as desktop applications, embedded systems, and server environments.
  2. Tailoring your Java runtime:
    • With Compact Profiles, developers can create custom JRE distributions tailored to their specific application requirements. By including only the necessary APIs and libraries, these custom JREs have a smaller footprint, reduced startup time, and improved runtime performance.
  3. Usage scenarios:
    • Desktop applications: Compact Profile 2 includes APIs commonly used for desktop application development, such as Swing and AWT.
    • Embedded systems: Compact Profile 3 is optimized for embedded systems with limited resources, providing essential APIs for embedded development.
    • Server environments: Compact Profile 1 offers a minimal set of APIs suitable for server-side applications, reducing the overhead of unnecessary libraries.
  4. Benefits:
    • Reduced footprint: Compact Profiles allow developers to create smaller JRE distributions, which are particularly beneficial for resource-constrained environments and deployment scenarios with limited disk space.
    • Improved startup time: Smaller JREs result in faster application startup times, enhancing the user experience, especially for desktop and embedded applications.
    • Better security: By minimizing the attack surface, Compact Profiles help improve the security posture of Java applications, reducing the potential impact of security vulnerabilities.
Concurrent accumulators, and other concurrency enhancements

Java 8 introduced several enhancements to the concurrency API, including concurrent accumulators, which improve the performance of concurrent data processing operations. Here’s what you need to know:

  1. Concurrent accumulators:
    • Java 8 introduced the LongAccumulator and DoubleAccumulator classes, which provide atomic updates to an accumulated value. These accumulators support common operations such as addition, subtraction, maximum, and minimum, allowing for efficient concurrent aggregation of data.
  2. Usage scenarios:
    • Parallel data processing: Concurrent accumulators are well-suited for parallel data processing tasks, such as computing statistical aggregates, summarizing large datasets, and performing parallel reductions.
    • Concurrent counters: Accumulators can also be used as concurrent counters to track the occurrence of events or maintain global counters in multi-threaded environments.
  3. Other concurrency enhancements:
    • CompletableFuture API: Java 8 introduced the CompletableFuture API, which simplifies asynchronous programming and non-blocking IO operations. CompletableFuture provides a fluent interface for composing asynchronous tasks, combining multiple asynchronous operations, and handling completion callbacks.
    • StampedLock: Java 8 introduced the StampedLock class, which provides an optimistic locking mechanism for read-write synchronization. StampedLock offers improved performance compared to traditional ReadWriteLocks by reducing contention and allowing for more concurrent read operations.
  4. Benefits:
    • Improved scalability: Concurrent accumulators and other concurrency enhancements improve the scalability of Java applications by enabling efficient parallel processing of data and reducing contention on shared resources.
    • Enhanced performance: By leveraging modern concurrency primitives and techniques, Java 8 applications can achieve better performance and throughput, particularly in multi-core and highly concurrent environments.
    • Simplified asynchronous programming: The CompletableFuture API simplifies asynchronous programming in Java, making it easier to write scalable and responsive applications that leverage asynchronous IO and parallel computation.

These concurrency enhancements introduced in Java 8 provide developers with powerful tools for building high-performance, scalable, and responsive Java applications that can effectively harness the power of modern multi-core processors and handle concurrent data processing tasks with ease.

Type and Repetition Annotations: Deep dive and examples
  • Type Annotations:
    • Type Annotations allow annotations to be applied to any type use in Java code, including variable declarations, method return types, and generic type arguments.They provide a way to express additional metadata about types, such as nullability, thread safety, or intended usage.Type Annotations are specified using the @Target meta-annotation with the ElementType.TYPE_USE value.
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface NonNull {
}

public class Example {
    @NonNull String name;
    // More code...
}
  • Repeating Annotations:
    • Repeating Annotations allow multiple annotations of the same type to be applied to a single program element, eliminating the need for container annotations.They simplify the syntax for annotating elements with multiple annotations of the same type.Repeating Annotations are specified using the @Repeatable meta-annotation on the container annotation type.
import java.lang.annotation.*;

@Repeatable(Authors.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface Author {
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
public @interface Authors {
    Author[] value();
}

@Author("John")
@Author("Doe")
public class Book {
    // Class implementation
}
  • Combining Type and Repeating Annotations:
    • Type and Repeating Annotations can be combined to provide powerful annotation capabilities in Java code.This allows for expressing complex metadata requirements for types and program elements, enhancing code readability and maintainability.
@NonNull
@Author("John")
String name = "John";
  • Use Cases:
    • Type Annotations can be used for enforcing type-based constraints, such as nullability checks or resource management.
    • Repeating Annotations are useful for annotating elements with multiple attributes or labels, such as authors, tags, or categories.
    • Combined annotations can express complex metadata requirements for types and program elements, enhancing code readability and maintainability.

Type and Repeating Annotations provide developers with powerful tools for expressing additional metadata and constraints directly in the code, improving code quality, self-descriptiveness, and maintainability. These features offer a flexible and expressive mechanism for annotating Java code elements, facilitating better documentation, validation, and understanding of the codebase.

Chapter 8: Performance Improvements

Understanding JIT enhancements in Java 8

Java 8 introduced several enhancements to the Just-In-Time (JIT) compiler, improving the runtime performance of Java applications. Here’s what you need to know:

  1. Tiered Compilation:
    • Java 8 introduced Tiered Compilation, a new approach to JIT compilation that optimizes the balance between startup time and peak performance.
    • Tiered Compilation dynamically compiles methods at different optimization levels based on their usage patterns, allowing the JVM to achieve better overall performance.
    • During the startup phase, methods are quickly compiled with lower optimization levels to reduce startup time. As methods are identified as hotspots, they are recompiled with higher optimization levels to improve runtime performance.
  2. Code Caching:
    • Java 8 introduced Code Caching, a feature that caches compiled code in memory to improve performance and reduce compilation overhead.
    • Code Caching stores compiled code in memory between JVM sessions, allowing the JVM to reuse previously compiled code without recompilation.
    • This reduces the startup time of Java applications, particularly for short-lived processes or applications that are frequently restarted.
  3. Profile-Guided Optimization (PGO):
    • Java 8 introduced Profile-Guided Optimization (PGO), a technique that uses runtime profiling information to guide JIT compilation decisions.
    • PGO collects runtime performance data during application execution and uses this information to optimize code generation strategies.
    • By leveraging runtime profiling data, the JIT compiler can make more informed optimization decisions, leading to better runtime performance.
Lambda and Stream API performance considerations

While Lambda expressions and the Stream API introduced in Java 8 offer significant benefits in terms of code conciseness and readability, developers should be aware of performance considerations:

  1. Lazy Evaluation:
    • The Stream API uses lazy evaluation, meaning intermediate operations are only executed when a terminal operation is invoked.
    • While lazy evaluation improves memory efficiency and allows for more efficient processing of large datasets, it can also introduce overhead in certain scenarios.
    • Developers should be mindful of the potential performance implications of lazy evaluation and carefully consider the order and composition of stream operations to optimize performance.
  2. Avoiding Autoboxing:
    • When working with streams of primitive data types (e.g., int, double), autoboxing and unboxing operations can degrade performance.
    • To avoid autoboxing overhead, developers should use specialized versions of stream operations provided by the IntStream, DoubleStream, and LongStream interfaces for primitive types.
  3. Parallel Stream Considerations:
    • While parallel streams offer the potential for improved performance by leveraging multi-core processors, they also introduce overhead related to thread synchronization and coordination.
    • Developers should carefully assess the characteristics of their data and workload before using parallel streams and consider factors such as data size, computational complexity, and the potential for concurrent access to shared resources.

By understanding JIT enhancements and considering performance implications when using Lambda expressions and the Stream API, developers can effectively leverage the performance improvements introduced in Java 8 while writing efficient and scalable Java applications.

Garbage Collector improvements

Java 8 introduced significant enhancements to the Garbage Collector (GC), refining memory management and reducing application pause times. Let’s explore these improvements in detail:

  1. G1 Garbage Collector:
    • Java 8 debuted the Garbage-First (G1) Garbage Collector as an experimental feature, promising more predictable pause times and improved performance for applications with large heap sizes.
    • G1 GC divides the heap into regions and employs concurrent and parallel phases for garbage collection. This minimizes pause times and enhances overall application throughput.
    • Its adaptability to application requirements and system resources makes G1 GC suitable for a broad spectrum of applications.
  2. Parallel Full GC for G1:
    • Java 8 introduced parallel Full GC for G1, enhancing pause time reduction by parallelizing the Full GC phase.
    • This feature optimizes Full GC operations by utilizing multiple threads for garbage collection, leading to shorter pause times and enhanced application responsiveness.
  3. Ergonomics Improvements:
    • Java 8 includes several ergonomics improvements to G1 GC, such as smarter heap sizing and improved tuning defaults.
    • These enhancements simplify GC tuning and make G1 GC more adaptable to diverse applications by automatically adjusting GC parameters based on system resources and application characteristics.
  4. CMS Deprecation:
    • While not introduced in Java 8 itself, it’s worth noting that Java 9 deprecated the Concurrent Mark-Sweep (CMS) Garbage Collector, encouraging migration to G1 GC due to its superior performance and features.
Benchmarks: Before and after Java 8

Java 8 marked a significant leap in performance compared to earlier versions, as indicated by various benchmarks. Let’s delve deeper into the performance improvements observed before and after Java 8:

  1. Startup Time:
    • Benchmarks reveal noticeable improvements in startup time after migrating to Java 8, particularly for applications with extensive codebases and intricate class loading requirements.
    • Java 8’s advancements in the JIT compiler, code caching, and class metadata layout contribute to accelerated application startup times compared to previous Java versions.
  2. Throughput and Responsiveness:
    • Performance evaluations measuring application throughput and responsiveness demonstrate considerable enhancements post Java 8 migration.
    • Java 8’s refined JIT compiler optimizations, Tiered Compilation, and Profile-Guided Optimization (PGO) lead to enhanced application performance and responsiveness, especially for CPU-intensive workloads.
  3. Garbage Collection Overhead:
    • Benchmarks comparing garbage collection overhead before and after Java 8 showcase reduced pause times and improved GC efficiency.
    • The introduction of G1 Garbage Collector and its associated features, like parallel Full GC and ergonomics enhancements, contribute to lower GC overhead and more predictable pause times in Java 8.

By embracing the Garbage Collector enhancements in Java 8 and analyzing performance benchmarks pre and post-migration, developers can optimize their Java applications for superior throughput, responsiveness, and resource utilization.

Conclusion

Java 8 has undeniably left a significant impact on modern Java development, revolutionizing the way we write code and paving the way for future advancements in the language. Let’s reflect on the key aspects:

  1. The impact of Java 8 on modern Java development:
    • Java 8 introduced a plethora of groundbreaking features and enhancements, from Lambda expressions and the Stream API to the Date and Time API and improvements in the Garbage Collector.
    • These features have empowered developers to write cleaner, more concise, and more maintainable code, accelerating development cycles and improving productivity.
    • The introduction of functional programming constructs, such as Lambda expressions and method references, has fostered a paradigm shift in Java development, enabling developers to embrace more expressive and declarative coding styles.
  2. How Java 8 features are shaping the way we code:
    • Java 8 features have fundamentally altered the way we approach problem-solving and software design in Java.
    • Lambda expressions and the Stream API have made functional programming idioms more accessible to Java developers, enabling them to write code that is more modular, composable, and easier to reason about.
    • The introduction of modern APIs, such as the Date and Time API, has addressed long-standing pain points in Java development, providing developers with more robust and intuitive tools for working with dates and times.
  3. Java 8 in the context of later Java versions: A look ahead:
    • While Java 8 has laid a solid foundation for modern Java development, subsequent Java versions have continued to build upon its success, introducing new features, performance improvements, and enhancements to the platform.
    • Subsequent Java versions, such as Java 9, Java 10, Java 11, and beyond, have introduced features like modularization with the Java Platform Module System (JPMS), enhancements to the Java language and APIs, and improvements to the garbage collection algorithms and performance optimizations.
    • However, Java 8 remains a pivotal release in the history of Java, serving as a catalyst for innovation and modernization in the Java ecosystem.

In conclusion, Java 8 has not only transformed the way we write Java code but has also paved the way for a more modern, expressive, and efficient Java ecosystem. As we look ahead to the future of Java development, it’s clear that the legacy of Java 8 will continue to shape the evolution of the language and its ecosystem for years to come.

Resources

  1. Java SE 8 Documentation
  2. Java 8 Tutorials

FAQs Corner🤔:

Q1: What are some common pitfalls to avoid when using Lambda expressions in Java 8?
One common pitfall is capturing non-final variables from the enclosing scope in Lambda expressions. This can lead to unexpected behavior if the variable is modified after the Lambda expression is created. Another pitfall is excessive use of complex Lambdas, which can reduce code readability and maintainability.

Q2: How does Java 8’s Stream API handle exceptions thrown by operations in the stream pipeline?
The Stream API handles exceptions by wrapping checked exceptions thrown by stream operations in a UncheckedIOException or UncheckedExecutionException. However, care should be taken when using stream operations that may throw checked exceptions, as they can make code less readable and harder to debug.

Q3: Can I use Lambda expressions to implement recursive algorithms in Java 8?
While Lambda expressions in Java 8 do not support direct recursion, you can use the Function interface to create recursive methods. By defining a Function that takes itself as an argument, you can implement recursive algorithms using Lambda expressions.

Q4: How does the Optional class in Java 8 differ from other ways of handling null values?
The Optional class provides a more fluent and idiomatic way to handle potentially null values in Java code compared to traditional null checks. It encourages developers to explicitly handle the presence or absence of a value, making code more readable and reducing the risk of NullPointerExceptions.

Q5: Are there any performance considerations when using default and static methods in interfaces in Java 8?
While default and static methods in interfaces provide greater flexibility and code reuse, they can introduce method resolution ambiguity and potentially impact performance due to increased method dispatch overhead. Careful design and consideration of the interface hierarchy can mitigate these concerns.

Related Topics:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top