Modern Java Will Surprise You
Take a look at this code snippet.
public class Main {
public static final Path FILE_PATH = Paths.get("members.csv");
record Member(String id, String email) {...}
public static void main(String[] args) throws IOException {
if (args.length != 2) throw new IllegalArgumentException();
var member = new Member(args);
var members = fetchMembers(FILE_PATH);
var existing = members
.stream()
.filter(it -> it.email.equalsIgnoreCase(member.email))
.findAny();
if (existing.isEmpty()) {
members.add(member);
persistMembers(FILE_PATH, members);
}
}
}
Regardless of your background or experience, I’m pretty certain you can easily understand what it does. We defined a member data structure, and a member instance initialized with data received as arguments. Then, we fetch a list of already persisted members, check if the newly added member is already stored in this list, and persist the new value if necessary.
You can complain about Java, but this code is easy to write and understand while also providing a powerful environment for your backend services. Java is one of the most used languages in the world for a reason, and new modern features are rapidly improving its development experience.
So let’s spend a bit more time on the code snippet above, review some of the new and old language features Java has to offer, and I promise that by the end of this article you’ll learn something new that’ll improve your knowledge about software.
To be sure we all are on the same page, you can install the latest Java version either directly or via a manager. Java is a compiled language, so you can compile your source code using the “javac” command, and then run the resulting bytecode using the “java” command. The bytecode is a language abstraction that allows Java to run on any operating system that has a Java Virtual Machine. This is what allows other JVM based languages such as Scala or Kotlin to coexist successfully with Java.
sdk install java 21-oracle
sdk current
javac Main.java
java Main 1 hi@awesome.com
Back to the code, let’s get the boilerplate out of the way. Java code has to be written in classes, and the public static void main method is the entry point into our program. I agree, this doesn’t look good compared to some alternatives, but this line is a good introduction into some of Java’s core features. Don’t worry, in more recent versions, you can refactor this into something way more simpler but I digress.
// real java
public class Main {
public static void main(String[] args) {
System.out.println("hi!");
}
}
// java 21
main() {
System.out.println("hi!");
}
Back to our main method, public is an “access modifier” allowing this method to be called from anywhere in the codebase. The static modifier associates the method to the containing class instead of an instance. In other words, you can call a static method directly on the class name instead of having to create an object instance of that class.
public class Client {
public static void hello() {
System.out.println("hi!");
}
Client.hello();
var client = new Client();
client.hello();
Finally, void just tells the compiler that the main method will return nothing.
Classes were the main solution for organizing data for a long while. However they rely on a lot of boilerplate, so I’m using a record here instead.
record Member(String id, String email) {...}
This one gives us immutability by default, and hides away all the constructors, getters, setters and other internal methods we were forced to write in the past.
Records come with a canonical constructor (I know… Java loves this kind of naming), but I added a custom constructor as well so that we can initialize objects easier.
record Member(String id, String email) {
public Member(String[] bits) {
this(bits[0], bits[1]);
}
}
Next, we are fetching a list of existing members from a CSV file. The FILE_PATH is stored in a constant field associated with the Class via the static modifier.
public static final Path FILE_PATH = Paths.get("members.csv");
var members = fetchMembers(FILE_PATH);
The fetchMember method is more than self explanatory.
public class Main {
public static final Path FILE_PATH = Paths.get("members.csv");
record Member(String id, String email) {
public Member(String[] bits) {
this(bits[0], bits[1]);
}
}
public static void main(String[] args) throws IOException {...}
public static List<Member> fetchMembers (Path path) throws IOException {
return Files
.readAllLines(path)
.stream()
.map(line -> line.split(","))
.map(Member::new)
.collect(toList());
}
}
We are reading the file from the disk, and converting it into a list of text lines. This list then becomes the source for a stream, which is an abstract layer allowing us to perform operations like splitting the text lines into string values, and then passing those values to the Member record constructor. Finally, we collect the results of these operations into a new List.
A couple of things to note here:
-
This is all part of the Java Standard Library, so the platform comes packed with Streams, Files and a lot of other stuff.
-
When working with the outside world things can easily go wrong. Java puts a lot of focus on security, and its readAllLines method can throw an IOException. You can either address the exception locally via a try catch block, or explicitly throw it up the chain.
-
This notation might seem a bit weird, but this is how we pass method references in Java. We can refactor the lambda expression if we want to be more “old school”.
//before
.map(Member::new)
//after
.map(it -> new Member(it))
Once we have the member list, we can use the same .stream() approach to check if the newly added email is already present in the list. The findAny method will return an Optional object - a container which may or may not contain a non-null value.
var members = fetchMembers(FILE_PATH);
var existing = members
.stream()
.filter(it -> it.email.equalsIgnoreCase(member.email))
.findAny();
if (existing.isEmpty()) {
members.add(member);
persistMembers(FILE_PATH, members);
}
This is Java’s solution to work around the many issues that can arise from interacting with null references directly.
Finally, our member list is mutable, so we can simply add in the new record, and then persist the list to the csv file.
public class Main {
public static final Path FILE_PATH = Paths.get("members.csv");
record Member(String id, String email) {...}
public static void main(String[] args) throws IOException {...}
public static void persistMembers (Path path, List<Member> members) throws IOException {
var lines = members
.stream()
.map(it -> STR."\{it.id}, \{it.email}")
.toList();
var content = String.join("\n", lines);
Files.write(path, content.getBytes());
}
}
While this might not look like much for some of you guys, I know it is mind blowing for long-time Java devs.
There is a lot of knowledge and power hidden away in these lines, so let me know if you are interested in exploring similar codebases again while looking at some other cool features like sealed interfaces, pattern matching or multithreading.
If you found this useful, you’ll probably like some of the other articles.
Until next time, thank you for reading!