Beyond the String methods, Java 11 added a handful of
small API improvements. Each one removes a few lines you used to write by hand, and two of them —
Files.readString and Path.of — you will use constantly.
Reading and writing a whole file
class Demo {
void run() throws IOException {
Path path = Path.of("notes.txt");
Files.writeString(path, "hello\nworld\n");
String all = Files.readString(path);
System.out.println(all.lines().count()); // 2
Files.writeString(path, "more\n", StandardOpenOption.APPEND);
System.out.println(Files.readString(path).lines().count()); // 3
}
}
Both default to UTF-8, which is the important detail. The pre-Java-11 equivalent was
new String(Files.readAllBytes(path), StandardCharsets.UTF_8), and the version people
actually wrote omitted the charset — picking up the platform default, which differs between a
developer's Mac and a Linux server. That class of bug is what these two methods delete.
They read the whole file into memory, so they suit configuration, templates and small data files. For anything large, stream it:
class Demo {
void run(Path path) throws IOException {
try (Stream<String> lines = Files.lines(path)) { // lazy, one line at a time
lines.filter(l -> l.contains("ERROR")).forEach(System.out::println);
}
}
}
Files.lines holds the file open, so it must be closed — hence try-with-resources.
Files.readString does not, because it has already finished by the time it returns.
Path.of
class Demo {
void run() {
Path a = Path.of("data", "input", "file.txt"); // joins with the right separator
Path b = Paths.get("data", "input", "file.txt"); // the old form, identical result
System.out.println(a.equals(b)); // true
System.out.println(a.getFileName()); // file.txt
System.out.println(a.getParent()); // data/input
}
}
Paths.get still works and always will. Path.of is preferred simply
because the factory now lives on the interface it produces, matching List.of,
Set.of and Map.of. Building paths from segments rather than concatenating
strings is the habit worth keeping either way — it uses the platform separator and cannot produce a
double slash.
Collection.toArray(IntFunction)
class Demo {
void run() {
List<String> names = List.of("Ana", "Bo");
String[] old = names.toArray(new String[0]); // works, slightly odd
String[] modern = names.toArray(String[]::new); // Java 11
System.out.println(modern.length); // 2
}
}
The new String[0] idiom always looked wrong — you allocate an empty array purely to
tell the method what type you want. The new overload takes a function that makes the array instead,
which reads as what it is. Performance is identical; this is purely about the code saying what it
means.
List.copyOf, Set.copyOf, Map.copyOf
class Demo {
void run() {
List<String> mutable = new ArrayList<>(List.of("Ana", "Bo"));
List<String> snapshot = List.copyOf(mutable); // unmodifiable COPY
mutable.add("Cy");
System.out.println(mutable.size()); // 3
System.out.println(snapshot.size()); // 2 — unaffected
// snapshot.add("Dee"); // UnsupportedOperationException
}
}
This is the one-call answer to defensive copying. The distinction from
Collections.unmodifiableList matters and catches people:
unmodifiableList returns a read-only view of the original, so changes to the
original still show through it. List.copyOf takes a snapshot, so they do not.
One caveat: these reject null elements outright. That is usually a feature — it
surfaces a bug at the point it is made — but it will throw on a list that legitimately contains
nulls.
Predicate.not and Optional.isEmpty
class Demo {
void run() {
List<String> raw = List.of("Ana", " ", "Bo");
System.out.println(raw.stream().filter(Predicate.not(String::isBlank)).toList());
// [Ana, Bo]
Optional<String> found = Optional.empty();
System.out.println(!found.isPresent()); // the old way
System.out.println(found.isEmpty()); // Java 11 — reads better
}
}
Both exist for the same reason: a negation that had to be spelled with a ! in front
of a longer expression, where the ! is easy to miss when scanning. Neither adds
capability; both make the intent visible.
var in lambda parameters
class Demo {
void run() {
List<String> names = new ArrayList<>(List.of("Ana", "Bo"));
names.sort((var a, var b) -> a.compareTo(b)); // legal since Java 11
names.sort((a, b) -> a.compareTo(b)); // and this was always fine
}
}
This one is worth knowing about mainly so you are not confused when you meet it. It exists so an
annotation can be attached to a lambda parameter, which requires a type or var to attach
it to. On its own it adds nothing, and the rule is all-or-nothing — you cannot write
(var a, b).
Nest-based access control
One Java 11 change you will never write code for, but which explains a stack trace you may have seen. A nested class and its outer class can access each other's private members — that is ordinary Java and always has been:
class Outer {
private int secret = 42;
class Inner {
int read() {
return secret; // private field of the enclosing class
}
}
}
The JVM had no concept of that relationship, so before Java 11 the compiler faked it by generating
hidden synthetic bridge methods — the access$000 frames that used to appear in stack
traces and confuse everyone reading them.
Java 11 taught the JVM about nests directly, so the bridge methods are gone. The visible effects are cleaner stack traces, slightly smaller class files, and reflection that behaves the way the source code says it should. Nothing to do; just one fewer piece of unexplained noise when you are reading a trace.
What to take from this
None of these is a feature you go looking for. They are the kind of thing you adopt by noticing them in someone else's code, which is exactly why a version-by-version list is worth reading once even for releases you think you know.
The two to change your habits over are Files.readString/writeString,
because the alternative had a real charset bug in it, and List.copyOf, because defensive
copying is the difference between an immutable object and one that merely looks immutable — a point
the static and final post makes at length.
Next
The HttpClient API is next — the largest thing Java 11 added, and the end of needing a third-party library to make an HTTP call.