Skip to content
Java encapsulation 6 min read

Packages

Packages are Java’s way of organizing related classes and interfaces into named groups — think of them like folders on your filesystem. They keep large codebases manageable, prevent naming conflicts across libraries, and work hand-in-hand with access modifiers to control what code can see what.

Packages directory tree

Why Packages Matter

Imagine two developers on the same team both write a class called Date. Without packages, the compiler would have no way to tell them apart. Packages solve this by giving every class a fully qualified name: com.myapp.util.Date vs java.util.Date — completely different, no collision.

Beyond naming, packages:

  • Group related code together (e.g. all networking classes live in java.net)
  • Allow package-private access — classes in the same package can share members that the outside world cannot see
  • Map directly to directory structure, making projects predictable and navigable

Declaring a Package

The package statement must be the very first line of a source file (before any imports or class declarations).

package com.devcraftly.shapes;

public class Circle {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double area() {
        return Math.PI * radius * radius;
    }
}

This file must live at the path com/devcraftly/shapes/Circle.java on your filesystem. The directory structure mirrors the package name exactly.

Note: If you omit the package statement entirely, the class belongs to the default (unnamed) package. This is fine for tiny experiments but should never be used in real projects — you cannot import default-package classes from named packages.

Naming Conventions

Java package names follow a well-established reverse-domain convention to guarantee global uniqueness:

LevelExampleWhat it means
Top-level domaincom, org, netOrganization type
Organizationcom.googleCompany/owner
Projectcom.google.gsonThe library
Module/layercom.google.gson.internalInternal sub-module

All package names are lowercase — no camelCase, no underscores (unless the domain name requires one). See Naming Conventions for the full style guide.

Importing Classes from Other Packages

Once a class is in a package, other code references it either by its fully qualified name or with an import statement.

package com.devcraftly.app;

// Import a specific class
import com.devcraftly.shapes.Circle;

// Import all public types in a package (the "wildcard import")
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Circle c = new Circle(5.0);          // works because of import
        java.util.Date d = new java.util.Date(); // fully qualified — no import needed
        System.out.println("Area: " + c.area());
    }
}

Output:

Area: 78.53981633974483

Tip: Wildcard imports (import java.util.*) do not slow down compilation or runtime — only the classes you actually reference are loaded. But explicit single-type imports make it obvious which classes your file depends on, which is why most style guides prefer them.

Static Imports

You can import static members (fields and methods) directly so you don’t have to qualify them with the class name every time.

import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

public class Geometry {
    public static void main(String[] args) {
        double hypotenuse = sqrt(3 * 3 + 4 * 4); // no Math. prefix needed
        System.out.println("PI = " + PI);
        System.out.println("Hypotenuse = " + hypotenuse);
    }
}

Output:

PI = 3.141592653589793
Hypotenuse = 5.0

See Static Import for a deeper look.

Subpackages

A subpackage is simply a package whose name has more dot-separated segments. com.devcraftly.shapes.threed is a subpackage of com.devcraftly.shapes — but Java treats them as completely independent namespaces. A wildcard import of the parent does NOT include the child.

// This does NOT import com.devcraftly.shapes.threed.*
import com.devcraftly.shapes.*;

If you need classes from both, you import them separately.

Built-In Packages You Use Every Day

Java ships with a rich standard library organized into packages:

PackageWhat it contains
java.langString, Math, Object, Systemauto-imported
java.utilCollections, Scanner, Date, Random
java.ioFile I/O streams — see Java I/O
java.netSockets, URLs — see Networking
java.timeModern date/time — see Date/Time API
java.util.concurrentThread-safe utilities — see Concurrent Collections

java.lang is the only package that Java imports for you automatically. Everything else requires an explicit import or fully qualified name.

Packages and Access Control

Packages are a core dimension of Java’s access modifier system. Here’s how they interact:

ModifierSame classSame packageSubclass (other package)Anywhere
privateYesNoNoNo
(no modifier) — package-privateYesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes

The default (package-private) access level is powerful: it lets classes in the same package cooperate closely without exposing internal details to the rest of the world. This is a key tool in encapsulation.

package com.devcraftly.internal;

class Helper {               // package-private — only visible inside this package
    static String format(String s) {
        return "[" + s + "]";
    }
}

public class PublicAPI {     // public — visible everywhere
    public static void printFormatted(String s) {
        System.out.println(Helper.format(s));  // can use Helper freely
    }
}

Code outside com.devcraftly.internal can call PublicAPI.printFormatted() but has no idea Helper even exists.

Compiling and Running with Packages

When you compile and run code organized into packages from the command line, you must be at the project root (the directory that contains your top-level package folder).

project-root/
└── com/
    └── devcraftly/
        └── shapes/
            └── Circle.java
# Compile (from project-root)
javac com/devcraftly/shapes/Circle.java

# Run using the fully qualified class name (no .java, no slash)
java com.devcraftly.shapes.Circle

Most developers use a build tool (Maven, Gradle) that handles this automatically — but knowing the underlying mechanics helps when things go wrong.

Under the Hood

When the JVM needs a class like com.devcraftly.shapes.Circle, the class loader translates the package name to a filesystem path by replacing each . with the platform’s file separator. It then searches each entry in the classpath (-cp flag or CLASSPATH env var) for that relative path.

At the bytecode level, every .class file stores its fully qualified name in the constant pool. The JVM uses this to enforce the link between the binary and the directory it came from — if the package declaration says com.devcraftly.shapes but the file lives in com/devcraftly/util/, you’ll get a NoClassDefFoundError at runtime.

Since Java 9, the module system (JPMS) adds a layer on top of packages. Modules declare which packages they export and which other modules they require, giving you stronger encapsulation than packages alone. Before modules, any code on the classpath could reach any public class — modules close that gap for production libraries and the JDK itself.

Tip: Even if you’re not writing a module, understanding packages deeply prepares you for the module system, which is increasingly common in modern Java codebases.

Last updated June 13, 2026
Was this helpful?