Polyglot
Languages Java scope
Java § scope

Packages

Java organises code into packages — namespaces that group related types — and into modules (since Java 9) — units of strong encapsulation that aggregate packages. Within a class, the language admits four scopes: block, method, class, and the (implicit) package scope. Access modifiers — public, protected, package-private (the default), and private — control visibility across these scopes. Together with the module system’s exports directives, the visibility surface is one of the most thorough access-control systems in mainstream languages.

This page covers packages, the four kinds of scope, the access modifiers, and the conventions for using each. The module system itself is treated in Modules and the build.

Packages

A package is a named region in which types live. Every Java type belongs to exactly one package; types without an explicit package declaration belong to the unnamed package (rare in production code).

package com.example.utils;

public class StringHelper {
    public static String reverse(String s) {
        return new StringBuilder(s).reverse().toString();
    }
}

Conventional package names use reverse domain notation — the organisation’s domain reversed, plus project-specific subpackages: com.example.utils, org.apache.commons.lang3, java.util.concurrent. The convention reduces collisions between packages developed independently.

The package name maps to a directory structure. A class com.example.utils.StringHelper lives in the file com/example/utils/StringHelper.java. The javac compiler enforces the correspondence; the JVM’s class loader uses the directory layout (or jar-file paths) to find compiled classes at runtime.

The import directive

A class in another package is referenced by its fully-qualified name:

java.util.List<java.lang.String> names = new java.util.ArrayList<>();

The verbosity is impractical. The import directive brings types from another package into scope:

package com.example;

import java.util.List;
import java.util.ArrayList;

public class App {
    public List<String> names = new ArrayList<>();
}

import may take three forms:

import java.util.List;             // single-type import
import java.util.*;                // package-wide import (all types in java.util)
import static java.lang.Math.PI;   // static import (since Java 5)
import static java.lang.Math.*;    // package-wide static import

The conventional discipline is to use single-type imports rather than wildcard imports — the explicit list makes dependencies visible and avoids ambiguity when a name is added to a wildcard-imported package.

The java.lang package is implicitly imported into every Java source file: String, Integer, Object, Math, Thread, and the other core types are accessible without explicit import.

Static imports

Static imports admit referring to a class’s static members without the class qualification:

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

double area = PI * radius * radius;
double root = sqrt(area);

The mechanism is convenient for math-heavy code and for utility methods used heavily. The conventional advice is to use static imports sparingly; importing too many static members produces unqualified names whose origin is unclear.

The four kinds of scope

Java distinguishes:

  • Block scope — the curly-brace-delimited region in which a declaration appears. Method bodies, compound statements, and loop bodies introduce block scope.
  • Method scope — the entire method body. Method parameters and locals declared in the method body have method or block scope.
  • Type (class) scope — the scope of members within a class, interface, or enum. Members are accessible by their unqualified name within other members of the same type.
  • Package scope — the scope of types declared in a package. Types are accessible by their unqualified name within the same package; from other packages, they require an import directive.
package com.example;

public class Counter {
    private int count = 0;          // type-scope (field)

    public void increment() {        // method scope begins
        int delta = 1;               // method-scope local

        if (delta > 0) {              // block scope begins
            int times = 1;            // block-scope
            count += delta * times;
        }                              // times goes out of scope
        // delta and count remain in scope
    }                                  // delta goes out of scope
}

Local variable shadowing by inner declarations is forbidden — Java rejects programs that attempt to redeclare an outer-scope name inside an inner block. The exception is field shadowing by parameter or local: a method parameter or local with the same name as a field is admitted, with this.fieldName resolving the ambiguity.

public class Counter {
    private int count = 0;

    public void setCount(int count) {
        this.count = count;          // explicit this. for the field
    }
}

Access modifiers

Four access modifiers (the third is the default and has no keyword):

ModifierVisibility
publicVisible from anywhere
protectedVisible within the same package and to subclasses (in any package)
(none — package-private)Visible only within the same package
privateVisible only within the declaring class
public class Widget {
    public  int  publicField    = 0;        // any code
    protected int protectedField = 0;        // same package or subclasses
    int          packageField    = 0;        // same package only
    private int  privateField    = 0;        // this class only
}

The modifiers apply uniformly to fields, methods, constructors, nested types, and (for public) top-level types. The language does not admit protected or private on top-level types — a top-level type is either public (visible from anywhere with the right import) or package-private (visible only within the same package).

The conventional discipline:

  • public for the API: the surface intended for callers.
  • Package-private (no modifier) for assembly-internal helpers — the type is visible to other classes in the same package, useful for tightly-coupled internal collaborators.
  • private for type-internal implementation details.
  • protected rarely; only when the type is explicitly designed for inheritance.

Package-private as a deliberate choice

Many Java codebases default to public for every type and method, but the conventional discipline is to start with the most restrictive access and widen as needed. A package-private class is invisible to consumers outside its package; within the package, it is freely usable. The mechanism is the principal Java replacement for the C/C++ static-at-file-scope idiom.

Nested types

A class may contain nested types — classes, interfaces, enums, or records declared inside another type. Java distinguishes four kinds:

Static nested classes

Declared with static:

public class Outer {
    public static class Nested {
        // ...
    }
}

Outer.Nested n = new Outer.Nested();

Static nested classes have no implicit reference to the enclosing instance; they are essentially top-level classes that happen to live inside another class’s scope.

Inner classes

Non-static nested classes are inner classes; they hold an implicit reference to the enclosing instance:

public class Outer {
    private int outerField;

    public class Inner {
        public int read() { return outerField; }    // accesses Outer.this.outerField
    }
}

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

The implicit reference admits access to the outer instance’s members but produces lifetime entanglement: an inner class instance keeps the outer instance alive for as long as it lives.

Local classes

Declared inside a method:

public void process() {
    class Helper {
        public void doWork() { /* ... */ }
    }

    Helper h = new Helper();
    h.doWork();
}

Local classes are scoped to the enclosing method. They may access final (or effectively final) local variables of the enclosing method.

Anonymous classes

A one-off class expression:

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("running");
    }
};

Anonymous classes are largely superseded by lambdas (Java 8+), which provide the same functionality more concisely. Anonymous classes remain in the language for backward compatibility and for cases where the target type has more than one method.

The module system

Java 9 introduced modules — units of strong encapsulation that aggregate packages with explicit dependencies and exports. A module declaration lives in module-info.java at the module root:

module com.example.app {
    requires java.base;            // implicit; always present
    requires java.sql;
    requires com.example.utils;

    exports com.example.app.api;
    // packages not in 'exports' are invisible outside the module
}

The principal directives:

  • requires <module> — declares a dependency on another module.
  • exports <package> — makes the package visible to importing modules.
  • opens <package> — admits reflection access to the package (for frameworks such as Spring that use reflection).
  • uses <service> and provides <service> with <impl> — service-provider declarations.

Modules are additive to packages: a package that is not exported by its module is invisible to other modules even if its types are public. The mechanism gives Java a strong-encapsulation story comparable to namespaces in C# or pub in Rust; the full treatment is in Modules and the build.

For most applications, the conventional discipline is:

  • Small applications and libraries: skip the module system; rely on the classpath.
  • Large applications: adopt modules to gain compile-time dependency checking and runtime encapsulation.
  • Migrations: gradual adoption is supported through automatic modules and the unnamed module.

Common patterns

File-internal helpers via package-private

// in com/example/utils/Helper.java
package com.example.utils;

class Helper {       // package-private
    static String process(String input) { /* ... */ }
}

// in com/example/utils/Public.java
package com.example.utils;

public class Public {
    public String use(String s) {
        return Helper.process(s);     // OK: same package
    }
}

The pattern admits internal collaboration between classes in the same package without exposing the helpers publicly.

Selective static imports

import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
import static java.util.stream.Collectors.toList;

Per-symbol imports are clearer than wildcard static imports.

Module declaration with explicit exports

module com.example.api {
    requires java.base;

    exports com.example.api;       // public surface
    // com.example.api.internal is not exported and invisible to consumers
}

The mechanism is the conventional Java idiom for “public API surface vs internal implementation”.

Type aliases via static fields

Java does not have type aliases. The conventional substitute is a typed static field:

public class Catalog {
    public static final List<Map<String, Object>> EMPTY = List.of();
    // refers to List<Map<String, Object>> by the alias EMPTY
}

The pattern is rare; in practice Java code spells out the long type names.