Core and Web-based Java
Packages, Nested Types, Enums and Object Lifetime
PGCP-AC
Large Java programs need more than classes and methods. They need a naming system, clear boundaries, compact ways to model fixed choices and a correct understanding of object lifetime. Packages provide the naming and access boundary. Nested types place an implementation near the class that owns it. Enums model a fixed set of meaningful instances. Garbage collection manages unreachable memory, while explicit resource-management constructs handle files, sockets and database connections.
1. Packages as namespaces
A package is a named group of related types. It prevents naming conflicts and provides an access boundary. Two projects may both have a class named User, but com.shop.model.User and com.school.model.User are different fully qualified names.
package com.acme.billing;
public class Invoice {
// ...
}
The package declaration, when present, must be the first non-comment source declaration. Convention uses lowercase, reversed domain names followed by project and feature names. Build tools normally expect the directory tree to match the package:
src/
└── com/
└── acme/
└── billing/
└── Invoice.java
The JVM identifies a type by its binary name and defining class loader. Directory placement alone does not declare the package; the source declaration does. Matching them keeps compilation, builds and maintenance predictable.
2. Imports and name resolution
Code can use a fully qualified name:
java.time.LocalDate today = java.time.LocalDate.now();
An import makes the simple type name available:
import java.time.LocalDate;
LocalDate today = LocalDate.now();
java.lang types and types in the current package need no explicit import. A wildcard such as import java.util.*; imports accessible types from that one package; it does not import java.util.concurrent or any other subpackage. Imports do not load classes, copy code or change access permissions.
If two imported packages contain the same simple name, use at least one fully qualified name. Java has no import alias syntax. Unused imports are harmless to runtime behaviour, though compilers or style tools may report them.
3. Static imports
A static import allows an accessible static member to be referenced without its class name:
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
double circumference(double radius) {
return 2 * PI * radius;
}
double diagonal(double side) {
return sqrt(2) * side;
}
Use static imports when the shortened expression remains clear, such as assertion methods in tests. Heavy use can obscure which class defines a name. Like an ordinary import, a static import cannot expose a private or otherwise inaccessible member.
4. Package access and source-file rules
A top-level type is either public or package-private. It cannot be declared private or protected. A source file may contain several top-level types, but at most one can be public and its name must match the file name.
Package-private access is created by omitting a modifier:
package com.acme.billing;
class TaxCalculator { // visible inside this package
static double tax(double amount) {
return amount * 0.18;
}
}
Package structure should follow responsibility, not merely technical kind. A feature package that keeps its controller, service and repository near one another can be easier to understand than global packages containing every controller or every service.
5. Nested-type categories
Java supports four common types declared within another context:
| Kind | Declaration location | Implicit enclosing instance | Can capture local variables |
|---|---|---|---|
| Static nested class | Member with static | No | Not applicable |
| Member inner class | Non-static member | Yes | Not applicable |
| Local class | Inside a block | According to context | Yes |
| Anonymous class | Expression at use site | According to context | Yes |
Nested types improve encapsulation when a helper belongs only to one enclosing abstraction. They should not be used merely to avoid creating a separate file when the nested type has an independent responsibility.
6. Static nested classes
A static nested class is associated with the enclosing class, not with an enclosing object:
class Order {
private final int number;
Order(int number) { this.number = number; }
static class Validator {
boolean isValidNumber(int value) {
return value > 0;
}
}
}
Order.Validator validator = new Order.Validator();
It has no implicit Order.this reference and cannot directly access an individual Order's instance fields. Like other class members, it can access private members when it has an object through which to do so. Static nested classes are useful for builders, result objects, comparators and implementation helpers that conceptually belong to the outer type.
7. Member inner classes
A non-static member inner class is tied to an enclosing instance:
class Cart {
private int itemCount;
class Summary {
String text() {
return "Items: " + itemCount;
}
}
}
Cart cart = new Cart();
Cart.Summary summary = cart.new Summary();
Summary can directly access Cart's fields, including private fields, through its implicit outer reference. Inside the inner class, Cart.this explicitly names that enclosing object. This relationship has a memory consequence: retaining the inner object also retains its outer object. Prefer a static nested class when the outer instance is unnecessary.
8. Local and anonymous classes
A local class is declared inside a method, constructor or block:
Runnable task(String label) {
class LabeledTask implements Runnable {
@Override public void run() {
System.out.println(label);
}
}
return new LabeledTask();
}
An anonymous class declares and instantiates an unnamed subclass or interface implementation in one expression:
Runnable task = new Runnable() {
@Override public void run() {
System.out.println("Working");
}
};
Anonymous classes are suitable for a small one-off implementation with state or multiple methods. A lambda is usually clearer for a stateless functional-interface implementation.
Both local and anonymous classes may capture local variables only when those variables are final or effectively final. A variable is effectively final when it is assigned once and never reassigned. The restriction gives captured code a stable value even after the declaring method has returned.
9. Shadowing and enclosing references
Nested scopes may contain same-named fields or variables:
class Outer {
int value = 10;
class Inner {
int value = 20;
void print(int value) {
System.out.println(value); // parameter
System.out.println(this.value); // Inner field
System.out.println(Outer.this.value); // Outer field
}
}
}
Qualifying names removes ambiguity. Excessive shadowing still reduces readability, so distinct names are preferable outside questions designed to test scope.
10. Enums as fixed sets of instances
An enum models a closed set of named constants:
enum OrderStatus {
NEW, PAID, SHIPPED, CANCELLED
}
Each constant is a single instance of the enum type. Enum comparison normally uses ==, which is safe because constants are unique. An enum cannot extend another class because it already extends java.lang.Enum, but it can implement interfaces.
values() returns a new array of constants in declaration order. valueOf("PAID") returns the exact named constant; it is case-sensitive and throws IllegalArgumentException for an unknown name. name() returns the declared identifier, while toString() may be overridden for display.
11. Enums with fields and behaviour
Enums can contain constructors, fields and methods:
enum HttpStatus {
OK(200, "Success"),
NOT_FOUND(404, "Not Found"),
SERVER_ERROR(500, "Server Error");
private final int code;
private final String description;
HttpStatus(int code, String description) {
this.code = code;
this.description = description;
}
public int code() { return code; }
public String description() { return description; }
}
Enum constructors are not invoked directly with new; constants invoke them during enum initialization. Constants may also provide class-body-specific implementations of an abstract enum method. This can replace a fragile switch when behaviour naturally belongs to each constant.
ordinal() reports declaration position starting at zero. Never persist it as a business or database identifier: inserting or reordering constants changes ordinals. Persist a stable explicit code or, when appropriate, the constant name.
12. Object reachability
Java's garbage collector reclaims heap memory for objects that are no longer reachable from garbage-collection roots. Roots include active thread stacks, live static fields and references maintained by the JVM or native code. Reachability follows chains of strong references.
Account first = new Account(10);
Account second = first;
first = null; // object remains reachable through second
second = null; // eligible if no other reachable reference exists
Assigning one reference to null does not destroy an object. It only removes that particular path. Eligibility also occurs through reassignment, scope exit or when the containing object itself becomes unreachable.
The commonly demonstrated eligibility patterns all follow the same reachability rule. Nulling removes one root path by assigning null. Reassignment makes a variable point to another object, possibly leaving the old object unreachable. An island of isolation is a group of objects that still reference one another but has no reference path from any live root. Mutual references inside the island do not keep it alive.
13. Cycles and collection timing
Tracing collectors do not rely only on reference counts. Therefore, an unreachable cycle can be reclaimed:
class Node { Node next; }
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;
The two nodes reference each other, but no root reaches either of them. They are eligible for collection. Eligibility does not mean immediate destruction. The JVM decides when and whether to collect based on memory pressure and collector policy.
System.gc() and Runtime.getRuntime().gc() are requests, not guarantees. Programs must never depend on prompt collection for correctness.
The historical finalize() mechanism is deprecated and unreliable. The JVM never promised prompt finalization, finalizer queues could delay reclamation, exceptions could be ignored and resurrection could make object lifetime unpredictable. Do not use finalization to close files, sockets, database connections or other resources. Use try-with-resources and AutoCloseable; specialized cleanup facilities such as Cleaner are fallback mechanisms rather than substitutes for explicit ownership.
14. Memory lifetime versus resource lifetime
Garbage collection releases managed heap memory. It does not provide timely release of operating-system resources such as files, sockets, locks or database connections. Those resources require deterministic cleanup:
try (BufferedReader reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
} // close() is invoked even when the body throws
Try-with-resources works with AutoCloseable resources and closes them in reverse declaration order. This is preferable to waiting for garbage collection. The historic finalization mechanism is deprecated and unreliable for resource cleanup.
Keep these similar terms separate:
finalrestricts reassignment, overriding or inheritance;finallyis a block associated with exception control flow;finalizerefers to the obsolete finalization facility.
15. References, immutability and copying
A final reference cannot be reassigned, but the object may still be mutable:
final List<String> names = new ArrayList<>();
names.add("Asha"); // legal
// names = new ArrayList<>(); // illegal
An immutable class normally uses private final fields, validates during construction, exposes no mutators and protects mutable components with defensive copies. Immutability is a property of the object's observable state, not merely the presence of final references.
A shallow copy creates a new outer object while sharing referenced nested objects. A deep copy duplicates the intended object graph. “Deep” is a design policy: immutable shared values may not need duplication, while mutable owned state usually does. Copy constructors or factory methods often express this policy more clearly than clone().
16. Annotations and retention
Annotations attach structured metadata to program elements. @Override asks the compiler to verify overriding and @Deprecated marks an API that callers should avoid.
A custom annotation may declare where and how long it exists:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audited {
String value() default "";
}
SOURCE retention discards metadata after compilation, CLASS stores it in the class file without requiring runtime visibility and RUNTIME makes it available through reflection. Retention alone does not perform behaviour; a compiler, framework, annotation processor or application must inspect and act on the metadata.
17. Design principles in context
Package and type design improve when responsibilities remain focused:
- Single responsibility: a type should have one coherent reason to change.
- Open–closed: add new behaviour through stable abstractions where practical instead of repeatedly modifying central conditionals.
- Substitution: a subtype must preserve its supertype's contract.
- Interface segregation: prefer small, role-specific interfaces over one broad interface that forces unused methods.
- Dependency inversion: high-level policy should depend on abstractions; concrete collaborators can be supplied through constructors.
These are reasoning tools rather than mechanical rules. For example, moving every method into a separate class would reduce cohesion rather than improve it.
Practical considerations
| Mistake | Correct reasoning |
|---|---|
| Assuming a wildcard imports subpackages | It imports types from one package only |
| Believing import grants access | Access modifiers remain decisive |
| Treating a static nested class as an inner class | It has no implicit enclosing object |
| Reassigning a captured local variable | Captured locals must be final or effectively final |
| Persisting an enum ordinal | Use a stable explicit code or suitable name |
| Assuming a cycle can never be collected | Unreachable cycles are collectible |
Using System.gc() as a cleanup guarantee | Close resources explicitly |
| Calling a final reference immutable | Its referenced object may remain mutable |
Worked reachability trace
Node root = new Node();
Node child = new Node();
root.next = child;
Node alias = child;
child = null;
root = null;
After child = null, the child object remains reachable through root.next and alias. After root = null, the root object may become eligible, but the child remains reachable through alias. Only after alias is cleared or leaves its live scope can the child become eligible, assuming no other root path exists.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.