🚀
Java 11

Java 11 Features

var, HTTP Client & New Methods
💡 Java 11 is like a "quality of life" update — it doesn't bring a big new paradigm (like Java 8 did), but makes everyday little tasks easier. Like an old car getting new convenience features — automatic wipers, better AC — the engine's the same, but driving it feels a lot smoother.

Java 11 (September 2018) is an LTS (Long-Term Support) release — meaning companies trust it in production, since Oracle supports it for a long time (the most popular LTS version after Java 8).

It brought several small but useful additions: new String methods (isBlank, strip, lines, repeat), easier file I/O with Files.readString()/writeString(), a new standard HTTP Client API, and the ability to run a single-file Java program directly without compiling it first.

In interviews, Java 11 features often come up in follow-ups like "what's new after Java 8" — especially var in lambdas, the new String methods, and the HTTP Client API.

// Naye String methods
System.out.println("  ".isBlank());        // true
System.out.println("Hi".repeat(3));         // HiHiHi

// Files.readString() — ek line mein file padho
String content = Files.readString(Paths.get("notes.txt"));

// var in lambda parameters
BiFunction<Integer, Integer, Integer> add = (var a, var b) -> a + b;
🚀
Java 11 is like a "quality of life" update — it doesn't bring a big new paradigm (like Java 8 did), but makes everyday little tasks easier. Like an old car getting new convenience features — automatic wipers, better AC — the engine's the same, but driving it feels a lot smoother.
1 / 6
⚡ Quick Recap
  • Java 11 = LTS release (Sept 2018)
  • var can be used in lambda parameters
  • isBlank/strip/lines/repeat = new String methods
  • java.net.http = the new standard HTTP Client
  • Single-file .java programs can run without compiling
On this page (8 subtopics)

Java 10 introduced "var" for local variables (var x = 10;), but you couldn't use it in lambda parameters. Java 11 filled that gap — now you can write var in a lambda's parameters too, like (var a, var b) -> a + b.

The benefit shows up when you need to put an annotation on a parameter (like @NotNull var x) — without var, putting an annotation on a lambda parameter wasn't syntactically possible. Writing just the name, (a, b) -> ..., and writing var, (var a, var b) -> ..., compile to exactly the same thing — no performance difference, this feature exists purely for annotation support.

Rule: either write var on all the parameters, or on none of them — you can't mix, so (var a, b) -> ... is invalid (a compile error).

It might come up in an interview whether var is a "dynamic typing" feature — it's not, Java is still statically typed; var just lets the compiler infer the type at compile time, nothing changes at runtime.

  • Without var: (a, b) -> a + b — short, good enough for most cases
  • With var: (var a, var b) -> a + b — needed for annotations
  • Mixing is invalid: (var a, b) -> ... gives a compile error
// Normal lambda
BiFunction<Integer, Integer, Integer> add1 = (a, b) -> a + b;

// var ke saath (Java 11+)
BiFunction<Integer, Integer, Integer> add2 = (var a, var b) -> a + b;

// Annotation ke saath — sirf var se hi possible
// (var a, @Deprecated var b) -> a + b;
⚠️Common Mistake: Mixing isn't allowed — writing (var a, b) -> a + b gives a compile error. Either all parameters use var, or none do.

isBlank() checks whether a String is empty or contains only whitespace — isEmpty() only checks if the length is 0, but for " " (just spaces), isEmpty() returns false while isBlank() returns true. This was a common source of bugs before Java 11.

strip()/stripLeading()/stripTrailing() work like trim(), but are Unicode-aware — trim() only understands ASCII whitespace (characters with a code less than or equal to space), while strip() also handles Unicode whitespace (like non-breaking spaces). Modern code prefers strip() over trim().

lines() splits a String into a Stream<String>, line by line (useful for multi-line text, without manually calling split("\n")). repeat(n) builds a new String by repeating a String n times.

A real-world use-case: in form input validation, isBlank() instantly tells you whether a user typed something meaningful or just spaces — something that previously needed a manual trim().isEmpty().

  • isBlank() — checks if it's only whitespace
  • strip()/stripLeading()/stripTrailing() — Unicode-aware trim
  • lines() — splits a String into a Stream, line by line
  • repeat(n) — repeats a String n times
String s = "   ";
System.out.println(s.isEmpty());  // false (length > 0 hai)
System.out.println(s.isBlank());  // true (sirf whitespace hai)

String text = "Line1\nLine2\nLine3";
text.lines().forEach(System.out::println); // 3 lines print

System.out.println("Ab".repeat(3)); // "AbAbAb"
💡Tip: Think about which one you actually need before choosing isEmpty() vs isBlank() — in form validation (where a user might type only spaces), isBlank() is the more correct check.

Before Java 11, reading a whole file into a String meant calling Files.readAllBytes() and manually doing new String(bytes, charset), or collecting a stream from Files.lines() — a bit of boilerplate. Files.readString(path) does all of this in one line.

Files.writeString(path, content) similarly writes a String straight to a file, without manually converting to bytes. Both methods default to UTF-8 (an overload also lets you pass a custom Charset).

These methods are great for small config files, templates, or test fixtures — for large data files or log processing, NIO's stream-based APIs (like Files.lines()) are still the better fit.

// Purana tareeka (Java 11 se pehle):
byte[] bytes = Files.readAllBytes(Paths.get("notes.txt"));
String content = new String(bytes, StandardCharsets.UTF_8);

// Java 11+:
String content2 = Files.readString(Paths.get("notes.txt"));
Files.writeString(Paths.get("output.txt"), "Hello Java 11!");
⚠️Common Mistake: Files.readString() loads the entire file into memory — risky for very large files (GBs); for large files, stream line-by-line with a BufferedReader instead.

It arrived as an incubator (experimental) module in Java 9, and became the standard java.net.http package in Java 11 — a modern replacement for the old HttpURLConnection (which dates back to Java 1.1, and is clunky and limited).

The new client supports HTTP/2 (by default), gives you both synchronous (send()) and asynchronous (sendAsync(), which returns a CompletableFuture) ways to make requests, and uses a fluent Builder pattern to build requests — reducing the need for third-party libraries (like Apache HttpClient or OkHttp) for simple use-cases.

In a microservices architecture, where one service calls another over REST, this built-in client gets the job done without adding a third-party dependency — especially handy in smaller projects where pulling in a whole library feels like overkill.

  • HTTP/2 support by default
  • send() — synchronous, waits for the response
  • sendAsync() — asynchronous, returns a CompletableFuture
  • Build requests with a fluent Builder pattern
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/data"))
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
💡Tip: Use the async version (sendAsync) when you need to send multiple requests in parallel, or don't want to block the UI thread — it returns a CompletableFuture<HttpResponse<String>> that you can chain with thenApply()/thenAccept().

Normally in Java you'd first compile with javac to create a .class file, then run it with java. Since Java 11, you can run a single .java file directly with the java command, without an explicit compile step — java Hello.java. The JVM compiles it in the background and runs it directly in memory (no .class file gets written to disk).

This feature is only for single-file programs (not multi-file projects) — it's very useful for quick scripts, prototyping, or learning, since one command replaces the usual two (javac + java).

This is especially handy for coding interviews and competitive programming — you can write code directly for an online judge or a quick script without ever setting up a project.

// Hello.java
public class Hello {
  public static void main(String[] args) {
    System.out.println("Direct run, no compile step!");
  }
}

// Terminal mein:
// $ java Hello.java
// Direct run, no compile step!
💡Tip: This feature isn't meant for production deployment — real projects still use a proper build via Maven/Gradle. It's just convenient for quick testing/learning.

Collection.toArray(IntFunction<T[]> generator) is a new overload — previously you'd write something like toArray(new String[0]), now you can use a cleaner method reference like toArray(String[]::new), where the collection itself figures out the array size.

Optional.isEmpty() was added — previously there was only isPresent(), so checking "if there's no value" meant writing !optional.isPresent(). isEmpty() is more readable (avoids the negation).

toArray(String[]::new) is better than the old toArray(new String[0]) because the JIT compiler can intrinsify this pattern, giving a small performance boost on large collections too — a real payoff from a tiny syntax change.

List<String> names = List.of("Riya", "Aman");
String[] arr = names.toArray(String[]::new); // naya, cleaner tareeka

Optional<String> opt = Optional.empty();
if (opt.isEmpty()) {
  System.out.println("Koi value nahi hai");
}

Sometimes you need to negate an existing Predicate (like String::isBlank) — you can't negate a method reference directly (String::isBlank has no negate() to call on it directly), so previously you had to write a lambda instead: s -> !s.isBlank().

Predicate.not(predicate) is a static helper that cleans this up — Predicate.not(String::isBlank) directly negates the method reference, without writing an extra lambda.

With a static import (import static java.util.function.Predicate.not;), it gets even cleaner — you can drop the "Predicate." prefix entirely and just write not(String::isBlank).

List<String> words = List.of("Java", "", "  ", "Code");

// Java 11 se pehle:
words.stream().filter(s -> !s.isBlank()).forEach(System.out::println);

// Java 11+, Predicate.not() ke saath:
words.stream().filter(Predicate.not(String::isBlank)).forEach(System.out::println);

Epsilon GC (JEP 318) is a "no-op" (does-nothing) garbage collector — it allocates memory, but never collects any of it. Sounds strange, but it's useful for performance testing (measuring GC's overhead) or very short-lived programs (that finish before they'd ever run out of memory anyway).

ZGC (JEP 333, experimental in this version) is a scalable, low-latency garbage collector that can handle heaps from a few GB up to multiple TB, keeping pause times under 10ms — designed for very large applications where even a small pause is noticeable. For interviews, knowing their names and a one-line purpose is usually enough — deep internals are rarely asked.

If an interviewer asks "when would you use Epsilon GC in production" — the honest answer is: probably never, it's purely a testing/benchmarking tool. For production, G1 (the default) or ZGC for specific latency needs are the practical choices.

GCPurpose
EpsilonNo-op — only allocates, never collects (for testing/measurement)
ZGCLarge heaps, very low pause time (experimental in Java 11)
G1 (default since Java 9+)General-purpose, balanced throughput + pause time