Java Full Course 2026 - Chapter 18: AutoBoxing, Abstract Classes & POJOs
This chapter explores fundamental Java architectural principles, memory mechanisms, and class abstractions. It unpacks the strict structural rules governing Java source files and JVM execution, demystifies primitive wrappers alongside the mechanics of autoboxing and integer caching, resolves deep edge cases surrounding abstract classes, and establishes domain-modeling patterns using Plain Old Java Objects (POJOs).
1. Java File Structure Rules: Single Public Class & Naming Constraints
Java enforces two strict source code rules:
- A single
.javafile can contain at most onepublicclass. - The name of that
publicclass must match the file name exactly (case-sensitive).
If a second public class is added to Demo.java, the Java compiler (javac) emits a compilation error:
The public type Sample must be defined in its own file.
// File: Demo.java
// VALID: Primary public class matching the file name
public class Demo {
public static void main(String[] args) {
System.out.println("Execution Entry Point");
}
}
// VALID: Non-public class in the same file
class Helper {
void performTask() {
System.out.println("Helper task");
}
}
// INVALID: Compilation Error!
// public class Sample { }
Why Does Java Enforce These Rules?
To understand these constraints, consider how the Java Virtual Machine (JVM) locates and executes program code:
1. JVM Execution and Entry Point Resolution
The entry point of any standalone Java application is the main method:
public static void main(String[] args).
static: Allows the JVM to invokemain()directly using the class identifier (Demo.main()) without instantiating an object ofDemofirst.public: Ensuresmain()is accessible across package boundaries, enabling external invocation by the JVM runtime environment.
2. Preventing Ambiguity
If a single file could declare multiple public classes, and more than one contained a public static void main signature, the JVM could not deterministically identify which class to execute as the primary application entry point. Restricting each file to a single public class creates a clear contract between the source file and the execution engine.
3. Efficient Class Loading
The JVM loads .class bytecode files dynamically on demand. By forcing the public class name to match the file name, the JVM bypasses expensive directory scans. To load public class Demo, the class loader looks directly for Demo.java or Demo.class on the classpath.
Multi-File Applications and Execution Flow
In production systems comprising hundreds of .java files (each containing its own public class), execution remains deterministic because the operator or build script explicitly specifies the target class to run:
2. Primitive Types vs. Wrapper Classes
Java partitions its type system into two categories: Primitives and Non-Primitives (Objects).
To bridge these worlds, Java provides dedicated Wrapper Classes in java.lang for every primitive type.
Primitive-to-Wrapper Mapping Matrix
Memory Allocation: Stack vs. Heap
The structural difference between primitives and wrapper objects lies in their memory layout:
int primitiveVal = 10;
Integer wrapperVal = Integer.valueOf(10);
Memory Layout Mechanics
-
Primitive (
int primitiveVal = 10):- Allocated directly on the Stack Frame inside a compact memory slot.
- Contains the raw binary scalar value (
10). - Fast access, zero object overhead, automatic lifecycle management via frame popping.
-
Wrapper (
Integer wrapperVal = Integer.valueOf(10)):- The variable
wrapperValon the Stack holds a 64-bit reference address (pointer). - The actual object resides on the Heap, consuming significantly more space due to object headers, synchronization flags, type metadata, and internal field alignment padding.
- The variable
Why Do Both Primitives and Wrapper Classes Exist?
Why We Need Wrapper Classes:
- Java Collections Framework: Utility structures such as
ArrayList<T>,LinkedList<T>, andHashMap<K,V>work exclusively with objects. Generics cannot accept raw primitives (e.g.,ArrayList<int>is invalid;ArrayList<Integer>is required). - Object-Oriented Abstraction: Enables primitives to participate in polymorphic workflows, pass
nullto denote uninitialized states (e.g., databaseNULLcolumns), and utilize built-in utility methods (Integer.parseInt(),Integer.toHexString()).
Why Primitives Are Still Used:
- Performance: Primitive operations execute directly on CPU registers and stack frames without heap allocations or pointer dereferencing.
- Memory Efficiency: Storing an array of 1,000,000
intprimitives consumes ~4 MB, whereas storing 1,000,000Integerobject references consumes ~24 MB (due to heap overhead). - Legacy C/C++ Compatibility: Preserved language performance expectations for low-level computational code during Java's initial design.
3. Mechanics of Autoboxing and Unboxing
- Autoboxing: Automatic conversion performed by the Java compiler from a primitive type to its corresponding wrapper class object.
- Unboxing: Automatic conversion performed by the Java compiler from a wrapper class object back to its primitive scalar value.
Compiler Transformations Under the Hood
// Written by Developer:
int primitiveNum = 10;
Integer boxedObj = primitiveNum; // Autoboxing
int unboxedNum = boxedObj; // Unboxing
// Generated Bytecode / Compiler Equivalent (Modern Java):
int primitiveNum = 10;
Integer boxedObj = Integer.valueOf(primitiveNum); // Compiler inserts .valueOf()
int unboxedNum = boxedObj.intValue(); // Compiler inserts .intValue()
Note on Deprecation:
In legacy Java (Java 1.0–1.4), explicit constructor instantiation was used:Integer boxedObj = new Integer(primitiveNum);.
As of Java 9, constructors for wrapper classes (such asnew Integer(int)) are deprecated and marked for removal because they bypass internal object caches and force unnecessary heap allocations. Modern Java usesInteger.valueOf().
Contexts That Trigger Autoboxing and Unboxing
Autoboxing and unboxing occur automatically in three primary coding scenarios:
Scenario A: Direct Variable Assignment
// Autoboxing: Primitive int to Integer object
Integer scoreObj = 100; // Resolved as Integer.valueOf(100)
// Unboxing: Integer object to primitive int
int scorePrimitive = scoreObj; // Resolved as scoreObj.intValue()
Scenario B: Method Invocations (Argument Passing & Returns)
public class ConversionDemo {
public static void processPrimitive(int value) {
System.out.println("Primitive received: " + value);
}
public static void processWrapper(Integer value) {
System.out.println("Wrapper object received: " + value);
}
public static void main(String[] args) {
Integer wrapperObject = 50;
int primitiveScalar = 25;
// Unboxing: Passing Integer wrapper where int primitive is expected
processPrimitive(wrapperObject); // Compiler passes wrapperObject.intValue()
// Autoboxing: Passing int primitive where Integer wrapper is expected
processWrapper(primitiveScalar); // Compiler passes Integer.valueOf(primitiveScalar)
}
}
Scenario C: Arithmetic and Expression Evaluations
When operational characters (+, -, *, /, ++, --) interact with wrapper objects, the compiler unboxes the wrapper objects into primitives before executing the arithmetic operations.
Integer firstVal = 30; // Autoboxing: Integer.valueOf(30)
Integer secondVal = 20; // Autoboxing: Integer.valueOf(20)
// Unboxing occurs for both variables, followed by primitive addition
int sum = firstVal + secondVal;
// Bytecode Equivalent: int sum = firstVal.intValue() + secondVal.intValue();
4. Pitfalls of Wrapper Classes: NullPointerException
Because wrapper classes are reference types, variables of wrapper types can hold null. Attempting to unbox a null object variable triggers a runtime NullPointerException (NPE).
public class NullPointerUnboxing {
public static void main(String[] args) {
Integer nullableWrapper = null;
// CRITICAL BUG: Triggers NullPointerException
int primitiveTarget = nullableWrapper;
}
}
Execution Breakdown of the Failure
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "java.lang.Integer.intValue()" because "nullableWrapper" is null
5. Object Equality (== vs .equals()) and Integer Caching
Evaluating equality between wrapper instances using == vs .equals() introduces one of Java's most subtle edge cases.
The Identity Operator (==) vs. The .equals() Method
==Operator:- On Primitives: Compares raw values.
- On Objects/Wrappers: Compares memory reference addresses (verifies whether two reference variables point to the exact same object on the Heap).
.equals()Method: Compares the underlying logical values encapsulated within the wrapper objects.
public class EqualityTest {
public static void main(String[] args) {
// Primitives: == compares values
int p1 = 1000, p2 = 1000;
System.out.println(p1 == p2); // true
// Wrappers initialized outside cache range (> 127)
Integer obj1 = Integer.valueOf(200);
Integer obj2 = Integer.valueOf(200);
System.out.println(obj1 == obj2); // false (Different memory addresses!)
System.out.println(obj1.equals(obj2)); // true (Same logical value: 200)
}
}
The Integer Cache Pool Mechanics
When comparing two Integer wrappers initialized to 100 instead of 200, the behavior changes:
Integer cacheA = 100;
Integer cacheB = 100;
System.out.println(cacheA == cacheB); // SURPRISE: Prints 'true'!
Why does 100 == 100 evaluate to true while 200 == 200 evaluates to false?
To optimize memory usage and performance, Java maintains an internal Integer Cache Pool managed by Integer.valueOf().
- Cached Range: Byte-equivalent range of
-128to+127(inclusive). - Behavior: When an
Integerobject is created implicitly or viaInteger.valueOf(n)with a value inside[-128, 127], the JVM does not allocate a new heap object. Instead, it returns a reference to an existing pre-allocated instance from the internal cache array.
Memory Traces
-
Integer cacheA = 100; Integer cacheB = 100;:- Value
100falls within[-128, 127]. Integer.valueOf(100)returns the pre-existing cached instance for both assignments.cacheAandcacheBstore the same memory reference address.- Therefore,
cacheA == cacheBevaluates totrue.
- Value
-
Integer obj1 = 200; Integer obj2 = 200;:- Value
200falls outside[-128, 127]. Integer.valueOf(200)allocates a newIntegerobject on the Heap forobj1, and another newIntegerobject forobj2.obj1andobj2hold different memory reference addresses.- Therefore,
obj1 == obj2evaluates tofalse.
- Value
6. Conceptual Blueprint of java.lang.Integer
To visualize how wrapper classes operate internally, we can construct a simplified, conceptual implementation of java.lang.Integer:
// Conceptual reconstruction of internal wrapper mechanics
public final class CustomInteger {
// Primitive value encapsulated privately
private final int value;
// Internal Cache Pool for range -128 to 127
private static class CacheHolder {
static final CustomInteger[] cache = new CustomInteger[256];
static {
for (int i = 0; i < cache.length; i++) {
cache[i] = new CustomInteger(i - 128);
}
}
}
// Deprecated Constructor
@Deprecated
public CustomInteger(int value) {
this.value = value;
}
// Static Factory Method leveraging Cache optimization
public static CustomInteger valueOf(int i) {
if (i >= -128 && i <= 127) {
return CacheHolder.cache[i + 128]; // Return pre-allocated instance
}
return new CustomInteger(i); // Heap allocation for out-of-range values
}
// Unboxing Getter
public int intValue() {
return this.value;
}
// Value-based Equality Comparison
@Override
public boolean equals(Object obj) {
if (obj instanceof CustomInteger) {
return this.value == ((CustomInteger) obj).intValue();
}
return false;
}
}
7. Deep Dive: Edge Cases and Rules of Abstract Classes
An Abstract Class is a class marked with the abstract keyword that serves as an incomplete structural blueprint. It cannot be directly instantiated using the new operator and is designed specifically to be extended by concrete subclasses.
public abstract class Animal {
// Abstract method: forces subclasses to provide concrete implementations
public abstract void makeSound();
// Concrete method: provides shared default behavior
public void sleep() {
System.out.println("Zzz... Sleeping");
}
}
Key Questions Surrounding Abstract Classes
Below are analyses of six key design questions regarding abstract classes.
Q1: Can an abstract class have constructors?
YES.
Although an abstract class cannot be instantiated directly (new Animal() is invalid), its constructors execute whenever a concrete subclass is instantiated. The child constructor calls the parent constructor via implicit or explicit super() calls to initialize the abstract class's fields.
public abstract class Vehicle {
private String brand;
// Abstract class constructor
public Vehicle(String brand) {
this.brand = brand;
}
public String getBrand() {
return brand;
}
}
public class Car extends Vehicle {
private int doors;
public Car(String brand, int doors) {
super(brand); // Invokes parent abstract class constructor
this.doors = doors;
}
}
Q2: Can an abstract class be declared final?
NO.
Declaring an abstract class final produces a compilation error:
The class Vehicle can either be abstract or final, not both.
abstract: Requires the class to be extended by subclasses to complete its definition.final: Prohibits inheritance entirely.
These keywords represent opposing contract guarantees, making them mutually exclusive.
Q3: Can abstract classes contain static methods, variables, or static initialization blocks?
YES.
static members belong to the class blueprint itself rather than specific instance objects on the Heap. Because static members can be invoked via the class reference (AbstractClass.staticMethod()), they do not rely on object instantiation.
public abstract class DatabaseConnector {
public static String defaultDriver = "com.mysql.cj.jdbc.Driver";
public static void printDriverInfo() {
System.out.println("Driver: " + defaultDriver);
}
}
Q4: Can abstract classes have private methods?
YES, but private methods CANNOT be declared abstract.
- Concrete
privatemethods: Allowed. They act as internal utility helpers accessible only within the abstract class itself. private abstractmethods: Invalid. Aprivatemethod is invisible to subclasses, preventing child classes from overriding it to provide an implementation.
public abstract class PaymentProcessor {
// VALID: Internal private helper method
private void logTransaction() {
System.out.println("Transaction logged.");
}
// INVALID: Compilation Error!
// private abstract void process();
}
Q5: Can abstract classes have final methods?
YES, but final methods CANNOT be declared abstract.
- Concrete
finalmethods: Allowed. They provide shared implementations that child classes are forbidden from overriding. final abstractmethods: Invalid.finalforbids overriding, whereasabstractrequires overriding.
public abstract class BaseTask {
// VALID: Enforces an immutable process template across all subclasses
public final void executeTaskSequence() {
stepOne();
stepTwo();
}
protected abstract void stepOne();
protected abstract void stepTwo();
}
Q6: Can an abstract class contain ZERO abstract methods?
YES.
A class marked abstract is not strictly required to declare abstract methods. Developers use this design choice to prevent direct instantiation of a base class while providing fully implemented concrete methods for inheritance.
// VALID: Purely concrete methods, yet direct instantiation via 'new' is prohibited
public abstract class ComponentTemplate {
public void renderUI() {
System.out.println("Rendering generic template frame.");
}
}
8. Tabular Synthesis: Abstract Class Rules & Modifiers
9. Plain Old Java Objects (POJOs) and Domain Modeling
A POJO (Plain Old Java Object) is an unencumbered Java class designed to model application data structures.
Characteristics of a Strict POJO:
- Does not extend predefined framework-specific classes (e.g., Spring or Hibernate base classes).
- Does not implement framework-specific interfaces.
- Does not require external annotations to function.
- Encapsulates state using
privatefields, exposed throughpublicgetters and setters.
Domain Modeling: Anemic vs. Rich Domain Models
When incorporating POJOs into modern application architectures, two primary domain modeling approaches are used:
1. Anemic Domain Model
In an Anemic Domain Model, the POJO acts purely as a data holder containing state, constructors, getters, setters, and basic utility methods. It contains no business logic. Business rules and operations are isolated within external service layers (e.g., StudentService).
2. Rich Domain Model
In a Rich Domain Model, the POJO encapsulates both its state (fields) and its associated business behaviors (methods) directly within the class entity.
Comparative Synthesis: Anemic vs. Rich Domain Models
Code Demonstration: Anemic vs. Rich Domain Model
Implementation A: Anemic Domain Model (Data Holder Only)
// Pure POJO: Holds Data Fields & Accessors Only
public class StudentAnemic {
private String id;
private String name;
private double attendancePercentage;
public StudentAnemic(String id, String name, double attendancePercentage) {
this.id = id;
this.name = name;
this.attendancePercentage = attendancePercentage;
}
public String getId() { return id; }
public String getName() { return name; }
public double getAttendancePercentage() { return attendancePercentage; }
public void setAttendancePercentage(double attendancePercentage) {
this.attendancePercentage = attendancePercentage;
}
}
// External Service handling business rules procedurally
public class AttendanceService {
public boolean verifyEligibility(StudentAnemic student) {
// Business logic lives outside the POJO
return student.getAttendancePercentage() >= 75.0;
}
}
Implementation B: Rich Domain Model (Data + Behavior)
// Rich POJO: Combines Data Fields with Domain Logic Behavior
public class StudentRich {
private String id;
private String name;
private double attendancePercentage;
public StudentRich(String id, String name, double attendancePercentage) {
this.id = id;
this.name = name;
this.attendancePercentage = attendancePercentage;
}
// Encapsulated Business Logic Method directly within the Entity
public boolean isEligibleForExams() {
return this.attendancePercentage >= 75.0;
}
public void registerAttendedClass(int totalClassesHeld, int classesAttended) {
if (totalClassesHeld > 0) {
this.attendancePercentage = ((double) classesAttended / totalClassesHeld) * 100.0;
}
}
}
Summary and Key Takeaways
- Java Source File Rules: A
.javasource file can contain at most onepublicclass, whose name must match the file name. Non-public classes can coexist in the same file. - JVM Resolution: The single
publicclass restriction ensures that class loaders can quickly locate and execute the application entry point (public static void main). - Primitive vs. Wrapper Memory: Primitives are stored as raw scalar values on the Stack Frame, providing fast performance and low memory overhead. Wrappers are allocated on the Heap as full objects with stack references.
- Autoboxing Mechanics: Autoboxing uses static factory methods (
Integer.valueOf()), converting primitives into wrapper object references automatically. - Unboxing Mechanics: Unboxing invokes primitive getters (such as
.intValue()), extracting primitive values from wrapper object instances. - NullPointerException Risk: Attempting to unbox a wrapper variable holding
nullresults in a runtimeNullPointerExceptionwhen the JVM tries to call the unboxing method (.intValue()). - Integer Caching:
Integer.valueOf()caches instances for values in the range-128to+127. Within this range, reference equality (==) evaluates totrue. Outside this range,==evaluates tofalsebecause distinct heap objects are instantiated. - Value Comparisons: Always compare object wrappers using
.equals()or by unboxing to primitives explicitly with.intValue()rather than using reference equality (==). - Abstract Class Design Rules: Abstract classes cannot be directly instantiated. They can declare constructors, static methods, and private or final concrete methods. However, abstract methods cannot be declared
privateorfinal, and an abstract class cannot befinal. - POJO & Domain Models: POJOs are lightweight classes free of framework dependencies. Anemic Domain Models separate data from logic using external services, while Rich Domain Models encapsulate both state and business operations directly inside the object.