Java’s been around for decades, but it’s still full of surprises. Even seasoned developers might not know all its hidden gems. Let’s dive into some of Java’s best-kept secrets that the experts often keep to themselves.
First up, we’ve got the var keyword. Introduced in Java 10, it’s a game-changer for local variable declarations. It makes your code cleaner and more readable. Instead of writing:
String myString = "Hello, World!";
You can simply write:
var myString = "Hello, World!";
The compiler figures out the type for you. It’s not just for simple types either. It works great with complex objects too.
Speaking of clean code, have you heard of the try-with-resources statement? It’s a nifty feature that automatically closes resources like files or database connections. No more forgetting to close your streams! Here’s how it looks:
try (var file = new FileInputStream("myfile.txt")) {
// Do something with the file
} catch (IOException e) {
e.printStackTrace();
}
The file will be closed automatically when you’re done with it. Pretty cool, right?
Now, let’s talk about something that might blow your mind: the StringJoiner class. It’s been around since Java 8, but it’s often overlooked. If you’ve ever had to join a bunch of strings with a delimiter, you’ll love this:
StringJoiner joiner = new StringJoiner(", ");
joiner.add("apple").add("banana").add("cherry");
System.out.println(joiner.toString()); // Outputs: apple, banana, cherry
It’s so much cleaner than building strings manually or using StringBuilder.
Here’s another secret: the Objects class. It’s packed with utility methods that can make your life easier. For example, Objects.requireNonNull() is great for parameter validation:
public void doSomething(String input) {
Objects.requireNonNull(input, "Input cannot be null");
// Rest of the method
}
If input is null, it throws a NullPointerException with your custom message. Simple and effective.
Now, let’s dive into something a bit more advanced: the CompletableFuture class. It’s a powerful tool for asynchronous programming that many developers underutilize. Here’s a simple example:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Simulate a long-running task
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, Future!";
});
future.thenAccept(System.out::println);
This code runs a task asynchronously and prints the result when it’s done. No need to manually manage threads or use callbacks.
Speaking of asynchronous programming, have you heard of the Flow API? It’s Java’s implementation of the Reactive Streams specification. It’s perfect for handling streams of data, especially when you have backpressure concerns. Here’s a simple publisher and subscriber:
public class MyPublisher implements Flow.Publisher<Integer> {
private final ExecutorService executor = Executors.newFixedThreadPool(1);
private List<Flow.Subscriber<? super Integer>> subscribers = new ArrayList<>();
@Override
public void subscribe(Flow.Subscriber<? super Integer> subscriber) {
subscribers.add(subscriber);
subscriber.onSubscribe(new Flow.Subscription() {
@Override
public void request(long n) {
executor.submit(() -> {
for (int i = 0; i < n; i++) {
subscriber.onNext(i);
}
});
}
@Override
public void cancel() {
subscribers.remove(subscriber);
}
});
}
}
public class MySubscriber implements Flow.Subscriber<Integer> {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(Integer item) {
System.out.println("Received: " + item);
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Done!");
}
}
This might look complex, but it’s incredibly powerful for handling streams of data.
Let’s switch gears and talk about something that’s been around since Java 5 but is often forgotten: the Scanner class. It’s not just for reading user input. It’s a powerful tool for parsing any kind of input. Check this out:
String input = "1 fish 2 fish red fish blue fish";
Scanner s = new Scanner(input);
s.findInLine("(\\d+) fish (\\d+) fish (\\w+) fish (\\w+) fish");
MatchResult result = s.match();
for (int i = 1; i <= result.groupCount(); i++) {
System.out.println(result.group(i));
}
This code parses the input string and extracts the numbers and colors. It’s like having a mini regular expression engine built right into Java.
Now, here’s something that might surprise you: Java has a built-in HTTP client since Java 11. No more need for third-party libraries for simple HTTP requests. Here’s how you can use it:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://openjdk.java.net/"))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
This sends an asynchronous GET request and prints the response body. It’s simple, efficient, and part of the standard library.
Let’s talk about something that’s been around since Java 8 but is often underused: the Optional class. It’s a container object that may or may not contain a non-null value. It’s great for avoiding null pointer exceptions. Here’s how you might use it:
Optional<String> optional = Optional.of("hello");
optional.ifPresent(s -> System.out.println(s.length()));
This code only prints the length of the string if it’s present. No null checks needed!
Now, here’s a secret that even some experienced Java developers don’t know about: the assert keyword. It’s great for debugging and testing. Here’s an example:
public void divide(int a, int b) {
assert b != 0 : "Divisor cannot be zero";
System.out.println(a / b);
}
If assertions are enabled (they’re off by default), this will throw an AssertionError if b is zero. It’s a great way to catch bugs early.
Speaking of debugging, have you heard of the jconsole tool? It comes with the JDK and allows you to monitor and manage Java applications. You can use it to watch memory usage, thread states, and even invoke methods on running applications. It’s an incredibly powerful tool that not enough developers take advantage of.
Now, let’s talk about something that’s been around since Java 7 but is often overlooked: the Files class. It’s packed with useful methods for file operations. Here’s how you can read all lines from a file in one go:
List<String> lines = Files.readAllLines(Paths.get("myfile.txt"));
Or if you’re dealing with really large files, you can use Files.lines() to get a Stream:
try (Stream<String> stream = Files.lines(Paths.get("myfile.txt"))) {
stream.forEach(System.out::println);
}
This reads the file line by line, which is much more memory-efficient for large files.
Here’s another secret: the Arrays class has a lot more to offer than just Arrays.sort(). For example, did you know you can use it to fill an array with a specific value?
int[] myArray = new int[10];
Arrays.fill(myArray, 42);
Now myArray is filled with 10 instances of the number 42. It’s a small thing, but it can save you from writing unnecessary loops.
Let’s talk about something a bit more advanced: method references. They’ve been around since Java 8, but many developers still don’t use them to their full potential. They’re a great way to make your code more readable. Instead of:
list.forEach(s -> System.out.println(s));
You can write:
list.forEach(System.out::println);
It’s shorter and clearer once you get used to the syntax.
Now, here’s a feature that’s often overlooked: the java.nio.file.WatchService. It allows you to watch a directory for changes. This is incredibly useful for applications that need to react to file system events. Here’s a simple example:
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get("C:\\MyFolder");
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println("Event kind: " + event.kind() + ". File affected: " + event.context() + ".");
}
key.reset();
}
This code watches a directory for new files and prints a message whenever a new file is created.
Let’s switch gears and talk about something that’s been around since Java 5 but is often underutilized: the Formatter class. It’s a powerful tool for creating formatted strings. Here’s an example:
Formatter formatter = new Formatter();
formatter.format("Hello, %s! You have $%d.%02d in your account.", "John", 1234, 56);
System.out.println(formatter.toString());
This outputs: “Hello, John! You have $1234.56 in your account.” It’s much more readable than string concatenation, especially for complex formatting.
Now, here’s a secret that can significantly improve your application’s performance: the ThreadLocal class. It allows you to create variables that are unique to each thread. This is great for maintaining thread-safe state without synchronization. Here’s an example:
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) {
return dateFormat.get().format(date);
}
This creates a separate SimpleDateFormat instance for each thread, avoiding the need for synchronization.
Let’s talk about something that’s been around since Java 7 but is often forgotten: the try-with-resources statement. It’s a great way to ensure that resources are closed properly. Here’s an example:
try (BufferedReader br = new BufferedReader(new FileReader("myfile.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
The BufferedReader is automatically closed at the end of the try block, even if an exception is thrown.
Now, here’s a feature that’s often overlooked: the SplittableRandom class. It’s great for generating random numbers in parallel streams. Here’s how you might use it:
SplittableRandom random = new SplittableRandom();
IntStream.generate(random::nextInt)
.limit(10)
.forEach(System.out::println);
This generates 10 random integers. The best part? It’s designed to work well with parallel streams, unlike the standard Random class.
Let’s wrap up with one last secret: the java.util.concurrent.atomic package. It provides a set of classes that support lock-free thread-safe programming on single variables. For example, AtomicInteger allows you to perform atomic operations on integers:
AtomicInteger counter = new AtomicInteger();
IntStream.range(0, 1000)
.parallel()
.forEach(i -> counter.incrementAndGet());
System.out.println(