Skip to main content

Section 12.5 Using Code Across Packages: Imports

In the previous section, we saw how packages organize our code into distinct namespaces, preventing naming conflicts and grouping related classes like ADTs.ListADT and DataStructures.ArrayList. This organization is essential.
However, it raises a practical question. Our ArrayList class, which will reside in the DataStructures package, needs to implement the ListADT interface, which lives in the separate ADTs package. How do we tell the Java compiler, inside ArrayList.java, that when we write "ListADT", we specifically mean the one defined in the ADTs package?
One way is to always use the fully qualified name everywhere the type is needed:
package DataStructures;

// NO import statement used here yet

// Using fully qualified names everywhere...
public class ArrayList<T> implements ADTs.ListADT<T> { // Verbose!

    // ... other fields ...

    // Method parameter using fully qualified name
    public boolean contains(ADTs.ListADT<? extends T> otherList) { // Very verbose!
        // ... implementation ...
        return false;
    }

    // Return type using fully qualified name
    public ADTs.ListADT<T> getSublist(int fromIndex, int toIndex) { // Also verbose!
        // ... implementation ...
        return null;
    }

    // Method signatures inherited from ADTs.ListADT must also be implemented...
    @Override
    public T get(int index) { /* ... */ return null;} // Example method from interface

    // ... many other methods ...
}
While this works (the compiler knows exactly which ListADT you mean), it makes the code incredibly verbose, harder to read, and more tedious to write. Imagine typing ADTs.ListADT dozens of times throughout the file! This clearly violates our goal of writing clean, maintainable code.
The solution is the import statement. The import keyword allows you to tell the compiler which classes or interfaces you intend to use from other packages, enabling you to refer to them using only their simple name (e.g., ListADT) within your current file.
Import statements appear at the top of your .java file, after the package declaration (if one exists) but before the class or interface definition. The most common form is the single-type import:
import package.path.ClassName; // Imports a specific class or interface
Let’s rewrite our ArrayList skeleton using an import for ListADT:
package DataStructures;

import ADTs.ListADT; // Import the specific interface we need

// Now we can use the simple name "ListADT"
public class ArrayList<T> implements ListADT<T> { // Much cleaner!

    // ... other fields ...

    // Method parameter using simple name
    public boolean contains(ListADT<? extends T> otherList) { // Readable!
        // ... implementation ...
        return false;
    }

    // Return type using simple name
    public ListADT<T> getSublist(int fromIndex, int toIndex) { // Readable!
        // ... implementation ...
        return null;
    }

    @Override
    public T get(int index) { /* ... */ return null;} // Still just needs simple name T

    // ... many other methods ...
}
By adding import ADTs.ListADT;, our code becomes significantly more readable and concise, as we can now use the simple name ListADT throughout the file.

Subsection 12.5.1 Types of Imports

  • Single-Type Import: import package.path.ClassName; Imports one specific public class or interface. This is the recommended approach because it makes the dependencies of your class explicitly clear just by looking at the import statements. Anyone reading the code knows exactly which types are being used from other packages.
  • On-Demand Import: import package.path.*; Imports all public types from the specified package. While sometimes convenient if you use many types from a single package (like java.util.*), it’s generally discouraged in modern practice. Why? It can make it less clear exactly which types are being used, and if two different imported packages happen to contain types with the same simple name (e.g., both packageA.* and packageB.* contain a Helper class), it creates ambiguity that requires you to use fully qualified names anyway, defeating the purpose.

Note 12.5.1. Trade-off: Imports vs. Fully Qualified Names.

So, if imports make code cleaner, why would you ever use the fully qualified name like ADTs.ListADT or java.util.ArrayList?
You always have the option to use the fully qualified name instead of importing. The main reason to do so is to resolve ambiguity or enhance clarity when necessary. For instance:
  • If you needed to use both our DataStructures.ArrayList and Java’s standard java.util.ArrayList in the same file, you couldn’t import both with their simple name ArrayList. The compiler wouldn’t know which one you meant. In this case, you would typically import one (perhaps the one used more frequently) and use the fully qualified name for the other (e.g., import DataStructures.ArrayList; ... java.util.ArrayList stdList = ...;).
  • Sometimes, for a type used only once or twice in a file, a developer might choose to use the fully qualified name just to be extremely explicit about where that less common type comes from, even without a direct conflict.
However, for types that you use frequently within a file and that don’t have naming conflicts (like using ListADT throughout our ArrayList implementation, or using standard types like java.util.List or java.util.Map), using a single-type import statement is the standard convention. It significantly improves readability and reduces verbosity without sacrificing clarity about the code’s dependencies, which are listed right at the top of the file.

Note 12.5.2. Implicit Imports: java.lang and Same Package.

You might wonder why you’ve never needed to import common types like String, System, Integer, Exception, or Object. That’s because the Java compiler automatically imports all public types from the java.lang package into every source file. Since these core types reside in java.lang, they are always available by their simple names.
Additionally, you do not need to import types that are defined within the same package as the file you are currently writing. They are also automatically available by their simple names. Imports are only needed to access public types from different packages.
In our project, the ArrayList.java file (in package DataStructures) will definitely need import ADTs.ListADT;. You will also likely need imports for standard Java exceptions like java.lang.IndexOutOfBoundsException (although types in java.lang are implicitly imported, explicit imports for exceptions can sometimes aid clarity) and others not in java.lang, such as java.util.NoSuchElementException. Understanding imports is therefore essential before we start implementing our class.
You have attempted of activities on this page.