Imports and Static Imports
Use single-type, wildcard and static imports deliberately, settle name clashes such as the two Date classes, and keep imports readable in reviews.
import java.util.List; // one type
import java.util.*; // every public type in a package
import static java.util.Map.entry; // one static member
import static org.assertj.core.api.Assertions.*; // all static membersAn import is a naming shortcut for the compiler and nothing else. import java.util.List; lets you write List instead of java.util.List. It loads no code, adds no dependency and costs nothing at run time; the class file records the full name either way. Types in java.lang (String, Math, Integer) and types in your own package are visible without an import.
Single-type and on-demand
import java.util.List; // single-type import
import java.util.*; // on-demand: every public type in java.util
A wildcard covers one package only. import java.util.*; does not bring in java.util.concurrent.ConcurrentHashMap; that needs its own import. Wildcards do not slow anything down, but they hide where a name comes from, and a library upgrade that adds a class with the same simple name can break a file that did not change. Google's Java style guide does not allow them. IDEs write and tidy explicit imports for you, so most teams leave that job to the IDE.
Name clashes
The classic clash is Date: java.util.Date and java.sql.Date. With java.util.* and java.sql.* both imported, the compiler only complains when you use the name: "reference to Date is ambiguous". A single-type import beats any wildcard, so import the one you use most and write the other with its full name. Two single-type imports of the same simple name are a compile error; one of them has to be written out in full.
Back-end code meets the same problem with framework types. Both Spring and Jakarta EE have a @Transactional, and several libraries ship a class called Logger or Response. When behaviour looks wrong, check which one the import line actually names.
Static imports
import static brings in static methods, fields and enum constants, so you can use them without the class name:
import static java.util.stream.Collectors.toMap;
import static java.util.concurrent.TimeUnit.SECONDS;
They read well when the member name explains itself: assertThat(...), toMap(...), entry("a", 1), SECONDS. They read badly when the name needs its class: a bare of(...) leaves the reader guessing whether it is List.of, Set.of or your own method. Keep them for tests, collectors and well-known constants.
One trap: a method declared in your class hides a statically imported method with the same name, even when the parameters differ. The call then fails with "cannot be applied to given types" instead of quietly using the import.
Example
import java.sql.Timestamp;
import java.util.Date;
public class Main {
public static void main(String[] args) {
// Date means java.util.Date because of the single-type import
Date legacy = new Date(0);
// The other Date is written out in full
java.sql.Date day = java.sql.Date.valueOf("2026-03-01");
Timestamp createdAt = Timestamp.valueOf("2026-03-01 09:30:00");
System.out.println(legacy.getClass().getName());
System.out.println(day.getClass().getName() + " " + day);
System.out.println(createdAt);
}
}
// Output:
// java.util.Date
// java.sql.Date 2026-03-01
// 2026-03-01 09:30:00.0When to use it
- Use java.util.Date and java.sql.Timestamp in one DAO without an ambiguity error.
- Statically import AssertJ's assertThat so every test assertion reads as one line.
- Check that a service imports Spring's @Transactional and not a same-named annotation from another library.
More examples
Static imports that read well
entry, SECONDS and joining are clear without their class names, so the static imports make the code shorter without hiding anything. Static imports conventionally go in their own block above the normal imports.
import static java.util.Map.entry;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.joining;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> requestsPerMinute = Map.ofEntries(entry("free", 60), entry("team", 600));
long windowMs = SECONDS.toMillis(45);
String plans = List.of("free", "team").stream()
.map(plan -> plan + "=" + requestsPerMinute.get(plan))
.collect(joining(", "));
System.out.println(plans);
System.out.println(windowMs);
}
}
// Output:
// free=60, team=600
// 45000Settling an ambiguous wildcard
Remove the third import and the file stops compiling with "reference to Date is ambiguous". The last line shows how tightly the two packages are tied: java.sql.Timestamp, like java.sql.Date, extends java.util.Date, so legacy JDBC code often needs both names.
import java.sql.*;
import java.util.*;
import java.util.Date; // a single-type import beats both wildcards
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("b", "a"));
Collections.sort(names);
Date stamp = new Date(0);
Timestamp ts = new Timestamp(0);
System.out.println(names);
System.out.println(stamp.getClass().getName());
System.out.println(ts.getClass().getName() + " extends " + ts.getClass().getSuperclass().getName());
}
}
// Output:
// [a, b]
// java.util.Date
// java.sql.Timestamp extends java.util.DateStatic imports in a test class
Tests are where static imports earn their keep: assertThat reads like a sentence and every test file uses it. Keeping the test in the production class's package lets it use package-private types without an import.
package com.example.shop.orders;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
class OrderTotalsTest {
// OrderTotals is in the same package, so it needs no import
private final OrderTotals totals = new OrderTotals();
@Test
void addsLineAmounts() {
assertThat(totals.sum(List.of(new BigDecimal("2.50"), new BigDecimal("1.25"))))
.isEqualByComparingTo("3.75");
}
@Test
void rejectsNegativeAmounts() {
assertThatThrownBy(() -> totals.sum(List.of(new BigDecimal("-1"))))
.isInstanceOf(IllegalArgumentException.class);
}
}
Discussion