Back to Blog
Java

Java Package vs Import: What Each Statement Does

java package vs import: Explains the difference between Java package declarations and import statements, how they work together, and when to use fully qualified names.

java packagesjava importsjava namespacesjava compilationjava code organization
Illustration comparing a Java package namespace with an import statement resolving a type name.

The java package vs import distinction is a common source of confusion because both declarations appear near the top of a source file. They solve different problems. The package declaration assigns the types in that file to a named namespace. An import statement is a compile-time convenience that lets you refer to types from other packages by their simple names instead of their fully qualified names. Understanding this distinction matters because it affects how you organize code, how you resolve name collisions, and how you maintain a large codebase.

What the Package Declaration Does

A package is a namespace. When you write:

package com.example.inventory;

every type declared in that file belongs to the com.example.inventory namespace. The fully qualified name of a class Order in this package is com.example.inventory.Order.

The package name also maps to a directory structure on disk. The Java compiler and build tools expect source files to be placed in directories matching their package names. This is a convention that the compiler and class loaders rely on when locating compiled classes, not a runtime requirement of the language itself.

Package names use dot-separated identifiers. The reverse-domain convention (com.example.inventory) is a practical way to avoid collisions between organizations. Two libraries can both define a class named Order as long as their packages differ. Without packages, every class on a classpath would share a single flat namespace, and name collisions would be unavoidable.

What the Import Statement Does

An import statement is a compile-time reference. It tells the compiler: when I use the simple name List in this file, resolve it to java.util.List.

package com.example.inventory; import java.util.List; import java.util.ArrayList; public class Order { private List<Item> items = new ArrayList<>(); }

Without the imports, the same file requires fully qualified names:

package com.example.inventory; public class Order { private java.util.List<com.example.inventory.Item> items = new java.util.ArrayList<>(); }

The import statement does not copy code into your class. It does not change what is loaded at runtime. It has no effect on the compiled bytecode beyond allowing the compiler to resolve simple names. Two source files with identical imports produce the same bytecode as two source files that use fully qualified names everywhere.

How Package and Import Work Together

The package declaration must be the first statement in a Java source file, before any imports. Imports must appear after the package declaration and before the type declaration.

package com.example.inventory; import java.time.LocalDate; import java.util.List; public class Order { private List<Item> items; private LocalDate created; }

This ordering is enforced by the Java language specification. A source file without a package declaration places its types in the unnamed default package. Types in the default package cannot be imported by classes in named packages, which is why production code should always declare a package.

AspectPackage declarationImport statement
PurposeAssigns types to a namespaceResolves simple names to types
Position in fileFirst statementAfter package, before type
Runtime effectNoneNone
ScopeApplies to the whole fileApplies to the whole file

Common Misconceptions About Imports

Import is not inclusion. Unlike a preprocessor #include in C or C++, an import does not pull code into your compilation unit. It only resolves names.

Import does not affect runtime behavior. Class loading is driven by the fully qualified names in the bytecode. Whether you wrote import java.util.List; or java.util.List directly, the runtime behavior is identical.

Wildcard imports do not import subpackages. import java.util.*; imports every type declared directly in java.util, but not types in java.util.concurrent or java.util.stream. You need separate imports for those packages.

An import does not make your types visible to other classes. Visibility is controlled by the public, protected, and private modifiers, not by imports. Importing a class from another package does not grant that package access to your class's members.

When to Use Fully Qualified Names Instead of Imports

Fully qualified names are useful when two types share the same simple name. Suppose you need both java.util.Date and java.sql.Date in the same file. You can import one and fully qualify the other:

import java.util.Date; public class Report { private Date created; private java.sql.Date sqlDate; }

Importing both would cause a compile error because the simple name Date would be ambiguous. Fully qualifying one of them resolves the conflict without renaming either type.

Fully qualified names are also reasonable for one-off usage. If a type is used only once in a file, an import adds a line that must be maintained. Some developers prefer explicit imports for every type, while others use fully qualified names for rare references. Either approach is valid; consistency within a project matters more than the choice itself.

Static Imports and Their Limits

Static imports bring static members into scope:

package com.example.inventory; import static java.time.Month.JANUARY; import static java.util.Collections.sort; public class Order { public void process() { sort(items); if (created.getMonth() == JANUARY) { // ... } } }

Static imports are useful for constants and utility methods that are used frequently. Overusing them can reduce readability, because the reader no longer sees which class a method belongs to. A single static import of a well-known constant is usually clearer than a dozen static imports scattered across a file.

Maintainability and Compilation Considerations

Package names should follow the reverse-domain convention to avoid collisions when libraries are combined. Imports should be organized consistently. Most IDEs can sort imports automatically, which keeps diffs clean and reduces merge conflicts.

Explicit imports are generally easier to read than wildcard imports because they show exactly which types a file depends on. Wildcard imports are convenient during early development but make dependencies harder to track as the file grows. The Java compiler resolves both forms identically, so the choice is a maintainability decision, not a performance decision.

When a class is moved to a different package, every file that imports it must be updated. IDEs handle this refactoring automatically, but it is worth knowing that the package declaration and the import statements in dependent files are coupled. Changing a package name without updating imports produces compile errors that are straightforward to fix but can be numerous in a large codebase. Keeping imports explicit and packages stable reduces the blast radius of such changes.

java package vs import: Practical Usage and Code Examples | RYUSLOG DEV