DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Increase Your Code Quality in Java by Exploring the Power of Javadoc
  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling

Trending

  • Internal Developer Portals: Modern DevOps's Missing Piece
  • Streamlining Event Data in Event-Driven Ansible
  • Unlocking AI Coding Assistants Part 1: Real-World Use Cases
  • How to Practice TDD With Kotlin
  1. DZone
  2. Coding
  3. Java
  4. What’s New Between Java 17 and Java 21?

What’s New Between Java 17 and Java 21?

In this blog, some of the changes between Java 17 and Java 21 are highlighted, mainly by means of examples. Take a look at the changes since the last LTS release.

By 
Gunter Rotsaert user avatar
Gunter Rotsaert
DZone Core CORE ·
Nov. 22, 23 · Tutorial
Likes (11)
Comment
Save
Tweet
Share
14.1K Views

Join the DZone community and get the full member experience.

Join For Free

On the 19th of September, 2023, Java 21 was released. It is time to take a closer look at the changes since the last LTS release, which is Java 17. In this blog, some of the changes between Java 17 and Java 21 are highlighted, mainly by means of examples. Enjoy!

Introduction

First of all, the short introduction is not entirely correct because Java 21 is mentioned in one sentence with being an LTS release. An elaborate explanation is given in this blog of Nicolai Parlog. In short, Java 21 is a set of specifications defining the behaviour of the Java language, the API, the virtual machine, etc. A reference implementation of Java 21 is implemented by OpenJDK. Updates to the reference implementation are made in this OpenJDK repository. After the release, a fork is created jdk21u. This jdk21u fork is maintained and will receive updates for a longer time than the regular 6-month cadence. Even with jdk21u, there is no guarantee that fixes are made during a longer time period. This is where the different Vendor implementations make a difference. They build their own JDKs and make them freely available, often with commercial support. So, it is better to say “JDK21 is a version, for which many vendors offer support."

What has changed between Java 17 and Java 21? A complete list of the JEPs (Java Enhancement Proposals) can be found at the OpenJDK website. Here you can read the nitty gritty details of each JEP. For a complete list of what has changed per release since Java 17, the Oracle release notes give a good overview.

In the next sections, some of the changes are explained by example, but it is mainly up to you to experiment with these new features in order to get acquainted with them. Do note that no preview or incubator JEPs are considered here. The sources used in this post are available at GitHub.

Check out an earlier blog if you want to know what has changed between Java 11 and Java 17.

Last thing to mention in this introduction, is the availability of a Java playground, where you can experiment with Java from within your browser.

Prerequisites

Prerequisites for this blog are:

  • You must have a JDK21 installed;
  • You need some basic Java knowledge.

JEP444: Virtual Threads

Let’s start with the most important new feature in JDK21: virtual threads. Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications. Up till now, threads were implemented as wrappers around Operating System (OS) threads. OS threads are costly and if you send an http request to another server, you will block this thread until you have received the answer of the server. The processing part (creating the request and processing the answer) is just a small portion of the entire time the thread was blocked. Sending the request and waiting for the answer takes up much more time than the processing part. A way to circumvent this, is to use asynchronous style. Disadvantage of this approach is the more complex implementation. This is where virtual threads come to the rescue. You are able to keep the implementation simple like you did before and still have the scalability of the asynchronous style.

The Java application PlatformThreads.java demonstrates what happens when creating 1.000, 10.000, 100.000 and 1.000.000 threads concurrently. The threads only wait for one second. Dependent on your machine, you will get different results because the threads are bound to the OS threads.

Java
 
public class PlatformThreads {
 
    public static void main(String[] args) {
        testPlatformThreads(1000);
        testPlatformThreads(10_000);
        testPlatformThreads(100_000);
        testPlatformThreads(1_000_000);
    }
 
    private static void testPlatformThreads(int maximum) {
        long time = System.currentTimeMillis();
 
        try (var executor = Executors.newCachedThreadPool()) {
            IntStream.range(0, maximum).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));
                    return i;
                });
            });
        }
 
        time = System.currentTimeMillis() - time;
        System.out.println("Number of threads = " + maximum + ", Duration(ms) = " + time);
    }
 
}


The output of running this application is the following:

Shell
 
Number of threads = 1000, Duration(ms) = 1094
Number of threads = 10000, Duration(ms) = 1625
Number of threads = 100000, Duration(ms) = 5292
[21,945s][warning][os,thread] Attempt to protect stack guard pages failed (0x00007f8525d00000-0x00007f8525d04000).
#
# A fatal error has been detected by the Java Runtime Environment:
# Native memory allocation (mprotect) failed to protect 16384 bytes for memory to guard stack pages
# An error report file with more information is saved as:
# /home/<user_dir>/MyJava21Planet/hs_err_pid8277.log
[21,945s][warning][os,thread] Attempt to protect stack guard pages failed (0x00007f8525c00000-0x00007f8525c04000).
[thread 82370 also had an error]
[thread 82371 also had an error]
[21,946s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached.
[21,946s][warning][os,thread] Failed to start the native thread for java.lang.Thread "pool-4-thread-32577"
...


What do you see here? The application takes about 1s for 1.000 threads, 1.6s for 10.000 threads, 5.3s for 100.000 threads and it crashes with 1.000.000 threads. The boundary for the maximum number of OS threads on my machine lies somewhere between 100.000 and 1.000.000 threads.

Change the application by replacing the Executors.newCachedThreadPool with the new Executors.newVirtualThreadPerTaskExecutor (VirtualThreads.java).

Java
 
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, maximum).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));
                    return i;
                });
            });
        }


Run the application again. The output is the following:

Shell
 
Number of threads = 1000, Duration(ms) = 1020
Number of threads = 10000, Duration(ms) = 1056
Number of threads = 100000, Duration(ms) = 1106
Number of threads = 1000000, Duration(ms) = 1806
Number of threads = 10000000, Duration(ms) = 22010


The application takes about 1s for 1.000 threads (similar to the OS threads), 1s for 10.000 threads (better than OS threads), 1.1s for 100.000 threads (also better), 1.8s for 1.000.000 (does not crash) and even 10.000.000 threads are no problem, taking about 22s in order to execute. This is quite amazing and incredible, isn’t it?

JEP431: Sequenced Collections

Sequenced Collections fill the lack of a collection type that represents a sequence of elements with a defined encounter order. Besides that, a uniform set of operations were absent that apply such collections. There have been quite some complaints from the community about this topic and this is now solved by the introduction of some new collection interfaces. The overview is available in the following image which is based on the overview as created by Stuart Marks.

sequenced diagram

Besides the new introduced interfaces, some unmodifiable wrappers are available now.

Java
 
Collections.unmodifiableSequencedCollection(sequencedCollection)
Collections.unmodifiableSequencedSet(sequencedSet)
Collections.unmodifiableSequencedMap(sequencedMap)


The next sections will show these new interfaces based on the application SequencedCollections.java.

SequencedCollection

A sequenced collection is a Collection whose elements have a predefined encounter order. The new interface SequencedCollection is:

Java
 
interface SequencedCollection<E> extends Collection<E> {
    // new method
    SequencedCollection<E> reversed();
    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}


In the following example, a list is created and reversed. The first and last item are retrieved and a new first and last item are added.

Java
 
private static void sequencedCollection() {
    List<String> sc = Stream.of("Alpha", "Bravo", "Charlie", "Delta").collect(Collectors.toCollection(ArrayList::new));
    System.out.println("Initial list: " + sc);
    System.out.println("Reversed list: " + sc.reversed());
    System.out.println("First item: " + sc.getFirst());
    System.out.println("Last item: " + sc.getLast());
    sc.addFirst("Before Alpha");
    sc.addLast("After Delta");
    System.out.println("Added new first and last item: " + sc);
}


The output is:

Shell
 
Initial list: [Alpha, Bravo, Charlie, Delta]
Reversed list: [Delta, Charlie, Bravo, Alpha]
First item: Alpha
Last item: Delta
Added new first and last item: [Before Alpha, Alpha, Bravo, Charlie, Delta, After Delta]


As you can see, no real surprises here, it just works.

SequencedSet

A sequenced set is a Set that is a SequencedCollection that contains no duplicate elements. The new interface is:

Java
 
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}


In the following example, a SortedSet is created and reversed. The first and last item are retrieved and it is tried to add a new first and last item.

Java
 
private static void sequencedSet() {
    SortedSet<String> sortedSet = new TreeSet<>(Set.of("Charlie", "Alpha", "Delta", "Bravo"));
    System.out.println("Initial list: " + sortedSet);
    System.out.println("Reversed list: " + sortedSet.reversed());
    System.out.println("First item: " + sortedSet.getFirst());
    System.out.println("Last item: " + sortedSet.getLast());
    try {
        sortedSet.addFirst("Before Alpha");
    } catch (UnsupportedOperationException uoe) {
        System.out.println("addFirst is not supported");
    }
    try {
        sortedSet.addLast("After Delta");
    } catch (UnsupportedOperationException uoe) {
        System.out.println("addLast is not supported");
    }
}


The output is:

Shell
 
Initial list: [Alpha, Bravo, Charlie, Delta]
Reversed list: [Delta, Charlie, Bravo, Alpha]
First item: Alpha
Last item: Delta
addFirst is not supported
addLast is not supported


The only difference with a SequencedCollection is that the elements are sorted alphabetically in the initial list and that the addFirst and addLast methods are not supported. This is obvious because you cannot guarantee that the first element will remain the first element when added to the list (it will be sorted again anyway).

SequencedMap

A sequenced map is a Map whose entries have a defined encounter order. The new interface is:

Java
 
interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}


In the following example, a LinkedHashMap is created, and some elements are added and the list is reversed. The first and last elements are retrieved and new first and last items are added.

Java
 
private static void sequencedMap() {
    LinkedHashMap<Integer,String> hm = new LinkedHashMap<Integer,String>();
    hm.put(1, "Alpha");
    hm.put(2, "Bravo");
    hm.put(3, "Charlie");
    hm.put(4, "Delta");
    System.out.println("== Initial List ==");
    printMap(hm);
    System.out.println("== Reversed List ==");
    printMap(hm.reversed());
    System.out.println("First item: " + hm.firstEntry());
    System.out.println("Last item: " + hm.lastEntry());
    System.out.println(" == Added new first and last item ==");
    hm.putFirst(5, "Before Alpha");
    hm.putLast(3, "After Delta");
    printMap(hm);
}


The output is:

Shell
 
== Initial List ==
1 Alpha
2 Bravo
3 Charlie
4 Delta
== Reversed List ==
4 Delta
3 Charlie
2 Bravo
1 Alpha
First item: 1=Alpha
Last item: 4=Delta
 == Added new first and last item ==
5 Before Alpha
1 Alpha
2 Bravo
4 Delta
3 After Delta


Also here no surprises.

JEP440: Record Patterns

Record patterns enhance the Java programming language in order to deconstruct record values. This will make it easier to navigate into the data. Let’s see how this works with application RecordPatterns.java.

Assume the following GrapeRecord which consists out of a color and a number of pits.

Java
 
record GrapeRecord(Color color, Integer nbrOfPits) {}


When you need to access the number of pits, you had to implicitely cast the GrapeRecord and you were able to access the nbrOfPits member using the grape variable.

Java
 
private static void singleRecordPatternOldStyle() {
    Object o = new GrapeRecord(Color.BLUE, 2);
    if (o instanceof GrapeRecord grape) {
        System.out.println("This grape has " + grape.nbrOfPits() + " pits.");
    }
}


With Record Patterns, you can add the record members as part of the instanceof check and access them directly.

Java
 
private static void singleRecordPattern() {
    Object o = new GrapeRecord(Color.BLUE, 2);
    if (o instanceof GrapeRecord(Color color, Integer nbrOfPits)) {
        System.out.println("This grape has " + nbrOfPits + " pits.");
    }
}


Introduce a record SpecialGrapeRecord which consists out of a record GrapeRecord and a boolean.

Java
 
record SpecialGrapeRecord(GrapeRecord grape, boolean special) {}


You have created a nested record. Record Patterns also support nested records as can be seen in the following example:

Java
 
private static void nestedRecordPattern() {
    Object o = new SpecialGrapeRecord(new GrapeRecord(Color.BLUE, 2), true);
    if (o instanceof SpecialGrapeRecord(GrapeRecord grape, boolean special)) {
        System.out.println("This grape has " + grape.nbrOfPits() + " pits.");
    }
    if (o instanceof SpecialGrapeRecord(GrapeRecord(Color color, Integer nbrOfPits), boolean special)) {
        System.out.println("This grape has " + nbrOfPits + " pits.");
    }
}


JEP441: Pattern Matching for Switch

Pattern matching for instanceof has been introduced with Java 17. Pattern matching for switch expressions will allow to test expressions against a number of patterns. This leads to several new and interesting possibilities as is demonstrated in application PatternMatchingSwitch.java.

Pattern Matching Switch

When you want to verify whether an object is an instance of a particular type, you needed to write something like the following:

Java
 
private static void oldStylePatternMatching(Object obj) {
    if (obj instanceof Integer i) {
        System.out.println("Object is an integer:" + i);
    } else if (obj instanceof String s) {
        System.out.println("Object is a string:" + s);
    } else if (obj instanceof FruitType f) {
        System.out.println("Object is a fruit: " + f);
    } else {
        System.out.println("Object is not recognized");
    }
}


This is quite verbose and the reason is that you cannot test whether the value is of a particular type in a switch expression. With the introduction of pattern matching for switch, you can refactor the code above to the following, less verbose code:

Java
 
private static void patternMatchingSwitch(Object obj) {
    switch(obj) {
        case Integer i   -> System.out.println("Object is an integer:" + i);
        case String s    -> System.out.println("Object is a string:" + s);
        case FruitType f -> System.out.println("Object is a fruit: " + f);
        default -> System.out.println("Object is not recognized");
    }
}


Switches and Null

When the object argument in the previous example happens to be null, a NullPointerException will be thrown. Therefore, you need to check for null values before evaluating the switch expression. The following code uses pattern matching for switch, but if obj is null, a NullPointerException is thrown.

Java
 
private static void oldStyleSwitchNull(Object obj) {
    try {
        switch (obj) {
            case Integer i -> System.out.println("Object is an integer:" + i);
            case String s -> System.out.println("Object is a string:" + s);
            case FruitType f -> System.out.println("Object is a fruit: " + f);
            default -> System.out.println("Object is not recognized");
        }
    } catch (NullPointerException npe) {
        System.out.println("NullPointerException thrown");
    }
}


However, now it is possible to test against null and determine in your switch what to do when the value happens to be null.

Java
 
private static void switchNull(Object obj) {
    switch (obj) {
        case Integer i -> System.out.println("Object is an integer:" + i);
        case String s -> System.out.println("Object is a string:" + s);
        case FruitType f -> System.out.println("Object is a fruit: " + f);
        case null -> System.out.println("Object is null");
        default -> System.out.println("Object is not recognized");
    }
}


Case Refinement

What if you need to add extra checks based on a specific FruitType in the previous example? This would lead to extra if-statements in order to determine what to do.

Java
 
private static void inefficientCaseRefinement(Object obj) {
    switch (obj) {
        case String s -> System.out.println("Object is a string:" + s);
        case FruitType f -> {
            if (f == FruitType.APPLE) {
                System.out.println("Object is an apple");
            }
            if (f == FruitType.AVOCADO) {
                System.out.println("Object is an avocado");
            }
            if (f == FruitType.PEAR) {
                System.out.println("Object is a pear");
            }
            if (f == FruitType.ORANGE) {
                System.out.println("Object is an orange");
            }
        }
        case null -> System.out.println("Object is null");
        default -> System.out.println("Object is not recognized");
    }
}


This type of problem is solved by allowing when-clauses in switch blocks to specify guards to pattern case labels. The case label is called a guarded case label and the boolean expression is called the guard. The above code becomes the following code, which is much more readable.

Java
 
private static void caseRefinement(Object obj) {
    switch (obj) {
        case String s -> System.out.println("Object is a string:" + s);
        case FruitType f when (f == FruitType.APPLE) -> {
            System.out.println("Object is an apple");
        }
        case FruitType f when (f == FruitType.AVOCADO) -> {
            System.out.println("Object is an avocado");
        }
        case FruitType f when (f == FruitType.PEAR) -> {
            System.out.println("Object is a pear");
        }
        case FruitType f when (f == FruitType.ORANGE) -> {
            System.out.println("Object is an orange");
        }
        case null -> System.out.println("Object is null");
        default -> System.out.println("Object is not recognized");
    }
}


Enum Constants

Enum types can be used in switch expressions, but the evaluation is limited to the enum constants of the specific type. What if you want to evaluate based on multiple enum constants?

Introduce a new enum CarType.

Java
 
public enum CarType { SUV, CABRIO, EV
}


Now that it is possible to use a case refinement, you could write something like the following.

Java
 
private static void inefficientEnumConstants(Object obj) {
    switch (obj) {
        case String s -> System.out.println("Object is a string:" + s);
        case FruitType f when (f == FruitType.APPLE) -> System.out.println("Object is an apple");
        case FruitType f when (f == FruitType.AVOCADO) -> System.out.println("Object is an avocado");
        case FruitType f when (f == FruitType.PEAR) -> System.out.println("Object is a pear");
        case FruitType f when (f == FruitType.ORANGE) -> System.out.println("Object is an orange");
        case CarType c when (c == CarType.CABRIO) -> System.out.println("Object is a cabrio");
        case null -> System.out.println("Object is null");
        default -> System.out.println("Object is not recognized");
    }
}


This code would be more readable if you would have a separate case for every enum constant instead of having a lots of guarded patterns. This turns the above code into the following, much more readable code.

Java
 
private static void enumConstants(Object obj) {
    switch (obj) {
        case String s -> System.out.println("Object is a string:" + s);
        case FruitType.APPLE -> System.out.println("Object is an apple");
        case FruitType.AVOCADO -> System.out.println("Object is an avocado");
        case FruitType.PEAR -> System.out.println("Object is a pear");
        case FruitType.ORANGE -> System.out.println("Object is an orange");
        case CarType.CABRIO -> System.out.println("Object is a cabrio");
        case null -> System.out.println("Object is null");
        default -> System.out.println("Object is not recognized");
    }
}


JEP413: Code Snippets

Code snippets allow you to simplify the inclusion of example source code in API documentation. Code snippets are now often added by means of the <pre> HTML tag. See application Snippets.java for the complete source code.

Java
 
/**
 * this is an example in Java 17
 * <pre>{@code
 *    if (success) {
 *        System.out.println("This is a success!");
 *    } else {
 *        System.out.println("This is a failure");
 *    }
 * }
 * </pre>
 * @param success
 */
public void example1(boolean success) {
    if (success) {
        System.out.println("This is a success!");
    } else {
        System.out.println("This is a failure");
    }
}


Generate the javadoc:

Shell
 
$ javadoc src/com/mydeveloperplanet/myjava21planet/Snippets.java -d javadoc


In the root of the repository, a directory javadoc is created. Open the index.html file with your favourite browser and click the snippets URL. The above code has the following javadoc.

example1

There are some shortcomings using this approach:

  • no source code validation;
  • no way to add comments because the fragment is already located in a comment block;
  • no code syntax highlighting;
  • etc.

Inline Snippets

In order to overcome these shortcomings, a new @snippet tag is introduced. The code above can be rewritten as follows.

Java
 
/**
 * this is an example for inline snippets
 * {@snippet :
 *    if (success) {
 *        System.out.println("This is a success!");
 *    } else {
 *        System.out.println("This is a failure");
 *    }
 * }
 *
 * @param success
 */
public void example2(boolean success) {
    if (success) {
        System.out.println("This is a success!");
    } else {
        System.out.println("This is a failure");
    }
}


The generated javadoc is the following.

example2

You notice here that the code snippet is visible marked as source code and a copy source code icon is added. As an extra test, you can remove in the javadoc of methods example1 and example2 a semi-colon, introducing a compiler error. In example1, the IDE just accepts this compiler error. However, in example2, the IDE will prompt you about this compiler error.

External Snippets

An interesting feature is to move your code snippets to an external file. Create in package com.mydeveloperplanet.myjava21planet a directory snippet-files.

Create a class SnippetsExternal in this directory and mark the code snippets by means of an @start tag and an @end tag. With the region parameter, you can give the code snippet a name to refer to. The example4 method also contains the @highlight tag which allows you highlight certain elements in the code. Many more formatting and highlighting options are available, it is too much to cover them all.

Java
 
public class SnippetsExternal {
 
    public void example3(boolean success) {
        // @start region=example3
        if (success) {
            System.out.println("This is a success!");
        } else {
            System.out.println("This is a failure");
        }
        // @end
    }
 
    public void example4(boolean success) {
        // @start region=example4
        if (success) {
            System.out.println("This is a success!"); // @highlight substring="println"
        } else {
            System.out.println("This is a failure");
        }
        // @end
    }
 
}


In your code, you refer to the SnippetsExternal file and the region you want to include in your javadoc.

Java
 
/**
 * this is an example for external snippets
 * {@snippet file="SnippetsExternal.java" region="example3" }"
 *
 * @param success
 */
public void example3(boolean success) {
    if (success) {
        System.out.println("This is a success!");
    } else {
        System.out.println("This is a failure");
    }
}
 
/**
 * this is an example for highlighting
 * {@snippet file="SnippetsExternal.java" region="example4" }"
 *
 * @param success
 */
public void example4(boolean success) {
    if (success) {
        System.out.println("This is a success!");
    } else {
        System.out.println("This is a failure");
    }
}


When you generate the javadoc as before, you will notice in the output that the javadoc tool cannot find the SnippetsExternal file.

Shell
 
src/com/mydeveloperplanet/myjava21planet/Snippets.java:48: error: file not found on source path or snippet path: SnippetsExternal.java
     * {@snippet file="SnippetsExternal.java" region="example3" }"
                 ^
src/com/mydeveloperplanet/myjava21planet/Snippets.java:62: error: file not found on source path or snippet path: SnippetsExternal.java
     * {@snippet file="SnippetsExternal.java" region="example4" }"


You need to add the path to the snippet files by means of the --snippet-path argument.

Shell
 
$ javadoc src/com/mydeveloperplanet/myjava21planet/Snippets.java -d javadoc --snippet-path=./src/com/mydeveloperplanet/myjava21planet/snippet-files


The javadoc for method example3 contains the defined snippet.

example3

The javadoc for method example4 contains the highlighted section.

example4

JEP408: Simple Web Server

Simple Web Server is a minimal HTTP server for serving a single directory hierarchy. Goal is to provide a web server for computer science students for testing or prototyping purposes.

Create in the root of the repository a httpserver directory, containing a simple index.html file.

HTML
 
Welcome to Simple Web Server


You can start the web server programmatically as follows (see SimpleWebServer.java). The path to the directory must refer to the absolute path of the directory.

Java
 
private static void startFileServer() {
    var server = SimpleFileServer.createFileServer(new InetSocketAddress(8080),
            Path.of("/<absolute path>/MyJava21Planet/httpserver"),
            SimpleFileServer.OutputLevel.VERBOSE);
    server.start();
}


Verify the output.

Shell
 
$ curl http://localhost:8080
Welcome to Simple Web Server


You can change the contents of the index.html file on the fly and it will serve the new contents immediately after a refresh of the page.

It is also possible to create a custom HttpHandler in order to intercept the response and change it.

Java
 
class MyHttpHandler implements com.sun.net.httpserver.HttpHandler {
 
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        if ("GET".equals(exchange.getRequestMethod())) {
            OutputStream outputStream = exchange.getResponseBody();
            String response = "It works!";
            exchange.sendResponseHeaders(200, response.length());
            outputStream.write(response.getBytes());
            outputStream.flush();
            outputStream.close();
        }
    }
}


Start the web server on a different port and add a context path and the HttpHandler.

Java
 
private static void customFileServerHandler() {
    try {
        var server = HttpServer.create(new InetSocketAddress(8081), 0);
        server.createContext("/custom", new MyHttpHandler());
        server.start();
    } catch (IOException ioe) {
        System.out.println("IOException occured");
    }
}


Run this application and verify the output.

Shell
 
$ curl http://localhost:8081/custom
It works!


Conclusion

In this blog, you took a quick look at some features added since the last LTS release Java 17. It is now up to you to start thinking about your migration plan to Java 21 and a way to learn more about these new features and how you can apply them into your daily coding habits. Tip: IntelliJ will help you with that!

Javadoc Java (programming language)

Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Increase Your Code Quality in Java by Exploring the Power of Javadoc
  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!