Part 4 of 12

Module Import Declarations (JEP 511): One Import to Rule Them All

The Problem: Import Hell

Every Java developer has experienced this. You open a file and before you see a single line of business logic, you wade through a wall of imports:

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

This is not useful documentation — it is noise. The IDE manages it, developers scroll past it, and it accumulates cruft over time. Wildcard imports (import java.util.*) help but they only cover one package at a time.


The Java 25 Solution: import module

JEP 511 introduces a new import form:

import module java.base;

This single line imports all public top-level types from all packages exported by the java.base module. That includes java.util, java.io, java.nio, java.time, java.lang, java.math, java.net, and more — all at once.


Before vs. After

Before Java 25

import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;

public class UserReport {

    public static Map<String, List<String>> groupUsersByCity(Path csvFile) throws IOException {
        return Files.lines(csvFile)
                .map(line -> line.split(","))
                .collect(Collectors.groupingBy(
                        parts -> parts[1].trim(),
                        Collectors.mapping(parts -> parts[0].trim(), Collectors.toList())
                ));
    }
}

After Java 25

import module java.base;

public class UserReport {

    public static Map<String, List<String>> groupUsersByCity(Path csvFile) throws IOException {
        return Files.lines(csvFile)
                .map(line -> line.split(","))
                .collect(Collectors.groupingBy(
                        parts -> parts[1].trim(),
                        Collectors.mapping(parts -> parts[0].trim(), Collectors.toList())
                ));
    }
}

The logic is identical. The import wall is gone.


What Exactly Gets Imported?

A module import declaration import module M is equivalent to a type-import-on-demand (import pkg.*) for:

  1. Every package directly exported by module M
  2. Every package transitively exported by modules that M requires with transitive

Example: import module java.sql

The java.sql module requires java.logging and java.xml with transitive. So import module java.sql gives you:

  • java.sql.* — JDBC types
  • java.util.logging.* — via transitive dependency on java.logging
  • org.xml.sax.*, javax.xml.* — via transitive dependency on java.xml
  • Everything from java.base is implicitly available without an import (it’s always on the module path)

Common Module Imports

Here are the most useful module imports and what they cover:

import module java.base;
// Covers: java.lang, java.util, java.io, java.nio, java.time,
//         java.math, java.net, java.security, java.text, java.util.concurrent, ...

import module java.sql;
// Covers: java.sql, javax.sql, + transitive exports

import module java.net.http;
// Covers: java.net.http (HttpClient, HttpRequest, HttpResponse)

import module java.xml;
// Covers: org.xml.sax, javax.xml, org.w3c.dom, ...

import module java.logging;
// Covers: java.util.logging

import module java.desktop;
// Covers: java.awt, javax.swing, java.applet, java.beans, ...

You can combine them:

import module java.base;
import module java.net.http;
import module java.sql;

Resolving Ambiguity

What happens when two modules export a type with the same simple name?

import module java.base;    // exports java.util.Date
import module java.sql;     // exports java.sql.Date

Both modules export a Date. If you write Date in your code, the compiler will report an ambiguity error — just like with wildcard package imports.

Resolution: use a specific import for the one you actually want.

import module java.base;
import module java.sql;
import java.sql.Date;       // explicit import wins over module imports

Date today = new Date(System.currentTimeMillis());   // java.sql.Date

This follows the same priority rules that have always existed in Java:

  1. Explicit single-type import wins
  2. Module import / wildcard import loses to explicit

Module Imports in Non-Modular Code

You do not need to be writing modular Java (with a module-info.java) to use module imports. They work in regular class files too:

// Regular Java file — no module-info.java required
import module java.base;

public class DataProcessor {
    public List<String> process(Stream<String> input) {
        return input
            .filter(s -> !s.isBlank())
            .map(String::trim)
            .sorted()
            .collect(Collectors.toList());
    }
}

Real-World Example: HTTP Client

Here is a practical HTTP utility using java.net.http with module imports:

import module java.base;
import module java.net.http;

public class GitHubClient {

    private static final HttpClient HTTP = HttpClient.newHttpClient();
    private static final String BASE = "https://api.github.com";

    public static String fetchUser(String username) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE + "/users/" + username))
                .header("Accept", "application/json")
                .GET()
                .build();

        HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new IOException("GitHub API returned " + response.statusCode());
        }

        return response.body();
    }

    public static void main(String[] args) throws Exception {
        System.out.println(fetchUser("openjdk"));
    }
}

No imports for URI, IOException, HttpClient, HttpRequest, HttpResponse — they all come from the two module imports at the top.


When NOT to Use Module Imports

Module imports are great for exploratory code, scripts, demos, and utility classes that touch many APIs. But in a large codebase, explicit imports have advantages:

  • Readability: explicit imports tell future readers exactly what external types this class depends on
  • Refactoring safety: IDEs track explicit imports; renaming or moving a class updates them automatically
  • Conflict prevention: in a large monorepo with many third-party modules, wildcards invite Date/List-style ambiguities

A sensible guideline: use module imports in compact source files, scripts, tests, and demos. Use explicit imports in production classes with complex dependency graphs.


Summary

Before Java 25After Java 25
Import everything from java.base20+ individual imports OR multiple .* linesimport module java.base;
Import JDBC + XMLimport java.sql.*; import org.xml.sax.*; ...import module java.sql;
Ambiguity resolutionSame as wildcard importsSame: explicit import wins
Requires modular codeN/ANo — works in any file

Next up: Compact Source Files & Instance Main Methods →