Stepping in 2024 with Powerful Java Language Features
Ana-Maria Mihalceanu on January 16, 2024Whether you are a beginner or senior Java developer, you strive to accomplish ambitious goals through your code while enjoying incremental progress. Along with many performance, stability, and security updates, Java 21 delivers new features and enhancements aiming to boost Java development productivity. And the best way to learn these language features is by using them in a Java project.
Setup
December holidays have passed, but there are many more other opportunities to offer a gift. So let’s build a Java application where you can order a wrapped gift for someone. Project wrapup is a simple http handler implementation that returns a gift as JSON from a sender to a receiver via HTTP POST method.
Before jumping into action, you should know that you need an IDE, at least JDK 21 and Maven installed on your local machine to reproduce the examples.
I generated my project with Oracle Java Platform Extension for Visual Studio Code via View > Command Palette > Java: New Project > Java with Maven
, named the project wrapup
and chose the package name org.ammbra.advent
.
So, let’s check out how we can use Java 21 language constructs to package gifts as JSONs.
Towards a Simplified Beginning with Java
The IDE generated project contains a starter class Wrappup.java
in the package org.ammbra.advent
.
package org.ammbra.advent;
public class Wrapup {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Although the main method declares arguments, those are not later processed within its scope. In JDK 21, JEP 445 introduced unnamed classes and instance main methods as a preview feature to reduce the verbosity when writing simple programs. In consequence, you can refactor the previous code to:
package org.ammbra.advent;
class Wrapup {
void main() { System.out.println("Hello, World!");}
}
To run the previous snippet, go to a terminal window and type the following command:
java --enable-preview --source 21 src/main/java/org/ammbra/advent/Wrapup.java
For the moment, let’s evolve the Wrapup
class to process only HTTP POST requests and produce a JSON output, by implementing com.sun.net.httpserver.HttpHandler
.
class Wrapup implements HttpHandler {
void main() throws IOException {
var server = HttpServer.create(
new InetSocketAddress("", 8081), 0);
var address = server.getAddress();
server.createContext("/", new Wrapup());
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
System.out.printf("http://%s:%d%n",
address.getHostString(), address.getPort());
}
@Override
public void handle(HttpExchange exchange)
throws IOException {
int statusCode = 200;
String requestMethod = exchange.getRequestMethod();
if (!"POST".equalsIgnoreCase(requestMethod)) {
statusCode = 400;
}
// Get the request body input stream
InputStream reqBody = exchange.getRequestBody();
// Read JSON from the input stream
JSONObject req = RequestConverter.asJSONObject(reqBody);
String sender = req.optString("sender");
String receiver = req.optString("receiver");
String message = req.optString("celebration");
String json = "{'receiver':'" + receiver
+ "', 'sender':'" + sender
+ "','message':'" + message + "'}";
exchange.sendResponseHeaders(statusCode, 0);
try (var stream = exchange.getResponseBody()) {
stream.write(json.getBytes());
}
}
}
The project relies on Maven for dependencies management, and in order to process the JSON you should modify the pom.xml
to have the json
dependency:
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
To launch the program, you don’t need to use mvn
features, you can use java/javac
directly by going to the terminal window and running:
#export path to .m2 json library
export $JSON_PATH=/<YOU>/.m2/repository/org/json/json/20231013
#launch the app
java -classpath $JSON_PATH/json-20231013.jar --enable-preview --source 21 \
src/main/java/org/ammbra/advent/Wrapup.java
In case your operating system is Windows, go to a Command Prompt
window and run:
set JSON_PATH=C:\.m2\repository\org\json\json\20231013
java -classpath $JSON_PATH/json-20231013.jar --enable-preview --source 21
src/main/java/org/ammbra/advent/Wrapup.java
Let’s try a simple curl
request to check the output:
curl -X POST http://127.0.0.1:8081 -H 'Content-Type: application/json' \
-d '{"receiver":"Duke","sender":"Ana","celebration":"Happy New Year!"}'
You should receive the following response:
{'receiver':'Duke', 'sender':'Ana','message':'Happy New Year!'}
Greeting someone is a nice gesture, but the application should also serve more complex responses when the sender wishes to send a more substantial gift. To address that, let’s model the application domain.
Data Modelling with Records and Sealed Types
Gifting on a special occasion can vary from a postcard to a more substantial gift.
For the wrapup
project let’s consider the following requirements:
- A sender can do a nice gesture and offer a gift.
- A gift can be either a postcard or add to it one of the following: an online coupon, buy an experience or a material present.
- A postcard does not have an associated cost, all the other 3 types of gifts have a price.
- An online coupon has an expiry date.
- A present can be placed inside a box, which has an extra cost.
- A sender can give a different postcard or surprise depending on celebration, but never send 2 postcards as a gift.
Figure 1 Project Class Diagram
The diagram above shows a possible way to model previously described scenario. Postcard
,Coupon
, Experience
and Present
are records because they should be carriers of immutable data representing possible surprise options. They also share a common formatting process to JSON through the sealed interface Intention
.
package org.ammbra.advent.surprise;
import org.json.JSONObject;
public sealed interface Intention
permits Coupon, Experience, Present, Postcard {
JSONObject asJSON();
}
A Gift
is another record type containing a Postcard
and an Intention
.
package org.ammbra.advent.surprise;
import org.json.JSONObject;
public record Gift(Postcard postcard, Intention intention) {
public JSONObject merge(String option) {
JSONObject intentionJSON = intention.asJSON();
JSONObject postcardJSON = postcard.asJSON();
return postcardJSON.put(option, intentionJSON);
}
}
Celebration
is an enum storing the defined occasions for sending a gift. Depending on the value of the Choice
enum,
wrapup
will return the appropriate gift in JSON format. Next, let’s define Coupon
, Experience
, Postcard
and Present
records
and format their data using String templates.
Syntax Flexibility with Expressive String Templates
The sealed interface Intention
limits inheritance, by only allowing specific subtypes, but is also a useful language construct to communicate the purpose of Coupon
, Experience
, Postcard
and Present
records.
For example, the characteristics of a Coupon
object are its price, the date when it expires and the currency of its cost. As a gift representation should follow a JSON format, let’s leverage string templates to achieve that.
String templates became available as a preview feature in Java 21 and will get a second preview in JDK 22.
String templates mix literal text with embedded expressions and template processors to produce specialized results, like JSONObject
.
To return a JSONObject
, a template expression would need:
- A template processor (
JSON
) - A dot character (
U+002E
) and - A template which contains an embedded expression (
Coupon
record fields).
The Coupon
, Experience
, Postcard
and Present
records can share the same template processor from String to JSON:
package org.ammbra.advent.surprise;
import org.json.JSONObject;
public sealed interface Intention
permits Coupon, Experience, Present, Postcard {
StringTemplate.Processor<JSONObject, RuntimeException> JSON = StringTemplate.Processor.of(
(StringTemplate st) -> new JSONObject(st.interpolate())
);
JSONObject asJSON();
}
Note: For the sake of brevity and clarity, this example takes some shortcuts and does not necessarily implement all the best practices recommended for an application that would go into production that would go into production (security, scaling, extensive error handling, etc.). For example, you should consider to check the template’s values and throw a checked exception (
JSONException
), if a value is suspicious!
And with this template processor, the Coupon
record becomes:
package org.ammbra.advent.surprise;
import org.json.JSONObject;
import java.time.LocalDate;
import java.util.Currency;
public record Coupon(double price, LocalDate expiringOn, Currency currency)
implements Intention {
public Coupon {
Objects.requireNonNull(currency, "currency is required");
if (price < 0) {
throw new IllegalArgumentException("Price of an item cannot be negative");
}
}
@Override
public JSONObject asJSON() {
return JSON. """
{
"currency": "\{currency}",
"expiresOn" : "\{ expiringOn}",
"cost": "\{price}"
}
""" ;
}
}
Experience
and Postcard
records share a similar template formatting logic. As the cost of a Present
varies depending on the gift-wrapping cost,
the asJSON
method implementation looks as follows:
package org.ammbra.advent.surprise;
import org.json.JSONObject;
import java.util.Currency;
public record Present(double itemPrice, double boxPrice, Currency currency)
implements Intention {
public Present {
Objects.requireNonNull(currency, "currency is required");
if (itemPrice < 0) {
throw new IllegalArgumentException("Price of an item cannot be negative");
} else if (boxPrice < 0) {
throw new IllegalArgumentException("Price of the box cannot be negative");
}
}
@Override
public JSONObject asJSON() {
return JSON. """
{
"currency": "\{currency}",
"boxPrice": "\{boxPrice}",
"packaged" : "\{ boxPrice > 0.0}",
"cost": "\{(boxPrice > 0.0) ? itemPrice + boxPrice : itemPrice}"
}
""" ;
}
}
Now that the project has each element of the data model, let’s investigate how to prototype the HTTP response containing the gift as JSON.
A Clear Control Flow with Pattern Matching In Switch Expressions
A user of the wrapup
application should be able to emit different requests to send a personalized gift to someone:
#send a postcard with a greeting for current year
curl -X POST http://127.0.0.1:8081 -H 'Content-Type: application/json' \
-d '{"receiver":"Duke","sender":"Ana","celebration":"CURRENT_YEAR", "type":"NONE"}'
#send a coupon and a postcard with a greeting for current year
curl -X POST http://127.0.0.1:8081 -H 'Content-Type: application/json' \
-d '{"receiver":"Duke","sender":"Ana","celebration":"CURRENT_YEAR", "option":"COUPON", "itemPrice": "24.2"}'
#send a birthday present and postcard
curl -X POST http://127.0.0.1:8081 -H 'Content-Type: application/json' \
-d '{"receiver":"Duke","sender":"Ana","celebration":"BIRTHDAY", "option ":"PRESENT", "itemPrice": "27.8", "boxPrice": "2.0"}'
#send a happy new year postcard and an experience
curl -X POST http://127.0.0.1:8081 -H 'Content-Type: application/json' \
-d '{"receiver":"Duke","sender":"Ana","celebration":"NEW_YEAR", "option ":"EXPERIENCE", "itemPrice": "47.5"}'
To support all these operations, the customized HTTPHandler
should be capable to process each of these request bodies and return an appropriate gift as JSON.
Given the complexity of the POST request body, let’s represent it as a record which builds based on potential data:
package org.ammbra.advent.request;
import org.ammbra.advent.surprise.Celebration;
public record RequestData(String sender, String receiver,
Celebration celebration, Choice choice,
double itemPrice, double boxPrice) {
private RequestData(Builder builder) {
this(builder.sender, builder.receiver,
builder.celebration, builder.choice,
builder.itemPrice, builder.boxPrice);
}
public static class Builder {
private String sender;
private String receiver;
private Celebration celebration;
private Choice choice;
private double itemPrice;
private double boxPrice;
public Builder sender(String sender) {
this.sender = sender;
return this;
}
public Builder receiver(String receiver) {
this.receiver = receiver;
return this;
}
public Builder celebration(Celebration celebration) {
this.celebration = celebration;
return this;
}
public Builder choice(Choice choice) {
this.choice = choice;
return this;
}
public Builder itemPrice(double itemPrice) {
this.itemPrice = itemPrice;
return this;
}
public Builder boxPrice(double boxPrice) {
this.boxPrice = boxPrice;
return this;
}
public RequestData build() throws IllegalStateException {
return new RequestData(this);
}
}
}
RequestData
uses an alternative constructor to pass the Builder
instance to the record constructor.
With this record definition, the logic inside handle(HttpExchange exchange)
method refactors to:
@Override
public void handle(HttpExchange exchange) throws IOException {
// ...
// Get the request body input stream
InputStream reqBody = exchange.getRequestBody();
// Read JSON from the input stream
JSONObject req = RequestConverter.asJSONObject(reqBody);
RequestData data = RequestConverter.fromJSON(req);
// ... }
Next, let’s evaluate the surprise content based on the gift option present in the request and make sure each case is treated accordingly using an exhaustive switch expression:
double price = data.itemPrice();
double boxPrice = data.boxPrice();
Choice choice = data.choice();
Intention intention = switch (choice) {
case NONE -> new Coupon(0.0, null, Currency.getInstance("USD"));
case COUPON -> {
LocalDate localDate = LocalDateTime.now().plusYears(1).toLocalDate();
yield new Coupon(data.itemPrice(), localDate, Currency.getInstance("USD"));
}
case EXPERIENCE -> new Experience(data.itemPrice(), Currency.getInstance("EUR"));
case PRESENT -> new Present(data.itemPrice(), data.boxPrice(), Currency.getInstance("RON"));
};
Without a default branch, adding new Choice
values will lead to compilation errors, which will make us consider how to handle those new cases.
As the gift intention is now clear, let’s process the final JSONObject
response by using pattern matching for switch.
Postcard postcard = new Postcard(data.sender(), data.receiver(), data.celebration());
Gift gift = new Gift(postcard, intention);
JSONObject json = switch (gift) {
case Gift(Postcard p1, Postcard p2) -> {
String message = "You cannot send two postcards!";
throw new UnsupportedOperationException(message);
}
case Gift(Postcard p, Coupon c)
when (c.price() == 0.0) -> p.asJSON();
case Gift(Postcard p, Coupon c) -> {
String option = choice.name().toLowerCase();
yield gift.merge(option);
}
case Gift(Postcard p, Experience e) -> {
String option = choice.name().toLowerCase();
yield gift.merge(option);
}
case Gift(Postcard p, Present pr) -> {
String option = choice.name().toLowerCase();
yield gift.merge(option);
}
};
In this scenario, the switch
expression uses the nested record pattern of Gift
to determine the final JSON.
As mentioned in the initial requirements, a sender cannot send two postcards as a gift so that operation is not supported.
Another special scenario is when the sender offers only a complimentary postcard and the final gift has no associated cost.
Hence, the switch
expression first treats this situation in a guarded case label –case Gift(Postcard p, Coupon c) when (c.price() == 0.0)
–
because an unguarded pattern case label –case Gift(Postcard p, Coupon c)
– dominates the guarded pattern case label with the same pattern.
Records and record patterns are great to streamline data processing, but Wrapup
program needed only some of the components for further processing.
Concise Code with Unnamed Patterns and Variables
When a switch executes the same action for multiple cases, you can improve its readability by using unnamed pattern variables. The unnamed patterns and variables became a preview feature in JDK 21 and target to be finalized in JDK 22 (see JEP 456).
Some cases from the previous switch expression required Postcard
, Coupon
, Experience
and Present
, but never used further values from these records.
After refactoring the switch with unnamed pattern variables, it becomes:
Gift gift = new Gift(postcard, intention);
JSONObject json = switch (gift) {
case Gift(Postcard _, Postcard _) -> {
String message = "You cannot send two postcards!";
throw new UnsupportedOperationException(message);
}
case Gift(Postcard p, Coupon c)
when (c.price() == 0.0) -> p.asJSON();
case Gift(_, Coupon _), Gift(_, Experience _),
Gift(_, Present _) -> {
String option = choice.name().toLowerCase();
yield gift.merge(option);
}
};
Now that the Wrapup
implementation reached its final state, build the project and launch it again from a terminal window:
#compile the sources
javac --enable-preview --source 21 -g -classpath $JSON_PATH/json-20231013.jar \
-sourcepath src/main/java -d target/classes src/main/java/org/ammbra/advent/Wrapup.java
#launch the app
java --enable-preview --source 21 -classpath $JSON_PATH/json-20231013.jar:target/classes \
src/main/java/org/ammbra/advent/Wrapup.java
and issue a POST request via curl:
curl -X POST http://127.0.0.1:8081 -H 'Content-Type: application/json' \
-d '{"receiver":"Duke","sender":"Ana","celebration":"NEW_YEAR", "option ":"EXPERIENCE", "itemPrice": "47.5"}'
If you would like to further try the code used in this article, go to the wrapup repository.
Final thoughts
JDK 21’s preview features like string templates, unnamed patterns, variables, unnamed classes and instance main methods help you minimize the amount of repetitive and verbose code, enabling you to express intent more clearly and concisely. Use records and sealed types to model your domain and enable a powerful form of data navigation and processing with record patterns and pattern matching for switch. As the new year just started, I encourage to try these features to boost your productivity with Java.
The content of this article was initially shared in the The JVM Programming Advent Calendar.