Java 11 New File and Collection Methods

July 18, 20264 min readUpdated 8/20/2026

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.