Skip to content

POJO, Enum, and Final Classes in Java

Java Data Carrier Taxonomy You have already seen how to write a basic Java class. Now it is time to meet three special kinds of classes that show up everywhere in real Java projects: the POJO, the enum, and the final class. Each one solves a very specific problem. By the end of this article you will know exactly what each one does, why it exists, and how to use it confidently in your own code.


Part One: POJO

What Does POJO Actually Mean?

POJO stands for Plain Old Java Object. The name is deliberately humble. A POJO is just a normal Java class with no fancy dependencies, no framework requirements, nothing exotic. It is the simplest possible thing: a class that holds data and gives you access to it through getter and setter methods.

Think of a POJO like a plain cardboard box. You put things inside it, you take things out, and the box itself does nothing special. It does not know about databases, HTTP requests, or any specific framework. It is just a container for data.

Here is the checklist that makes a class a proper POJO:

The class must not extend any other class except Object, which every Java class extends by default. The moment you write extends SomeFrameworkClass, it is no longer a plain object because it is now tied to that framework.

The class must not implement any framework specific interface. Implementing Serializable from the Java standard library is generally considered acceptable, but implementing something like a framework callback interface crosses the line.

The class must have a public default constructor, meaning a constructor that takes no arguments. This lets any tool or framework create an instance of your class without knowing anything special about it in advance.

All fields must be private. Nobody should be able to reach directly into your object and grab or change a value without going through a controlled access point.

The class must provide public getter and setter methods for every field it wants to expose.

That is the complete checklist. If your class satisfies all five points, it is a POJO.

A Simple POJO Example

java
// A POJO representing a customer
public class Customer {

    // Fields are private - nobody can access them directly
    private int id;
    private String name;
    private String email;

    // Public default constructor - no arguments needed
    public Customer() {
        // body can be empty
    }

    // Getter for id - the only way to read the id from outside
    public int getId() {
        return id;
    }

    // Setter for id - the only way to change the id from outside
    public void setId(int id) {
        this.id = id;
    }

    // Getter for name
    public String getName() {
        return name;
    }

    // Setter for name
    public void setName(String name) {
        this.name = name;
    }

    // Getter for email
    public String getEmail() {
        return email;
    }

    // Setter for email
    public void setEmail(String email) {
        this.email = email;
    }
}

This class does nothing fancy. It stores three pieces of data and lets you read and write them. That is exactly what a POJO should do.

Where Do POJOs Actually Appear in Real Projects?

You might wonder why you need a special name for something so simple. The reason is that POJOs solve a very common problem in real software projects: different parts of your application speak different languages.

Imagine you have a web service and a client sends you an HTTP request. The data arrives as JSON or XML with field names chosen by whoever designed that API. Meanwhile the rest of your application was built using its own naming conventions and data structures. You need a translation layer.

A POJO sits exactly in that translation layer. You take the incoming request data, create a POJO object, fill its fields from the request data using the setters, and then pass that POJO around to all the other parts of your application. If the external API changes its field names tomorrow, you only update the mapping code in one place. Every other class in your application continues to work with the POJO as if nothing changed.

Here is a concrete scenario. A REST API sends you a request with fields called ID and name. But your internal application uses the names customerId and customerName. You create a POJO called CustomerRequest with the internal naming, and you write one small piece of mapping code at the entry point:

java
// The POJO with your internal naming convention
public class CustomerRequest {

    private int customerId;
    private String customerName;

    public CustomerRequest() {}

    public int getCustomerId() {
        return customerId;
    }

    public void setCustomerId(int customerId) {
        this.customerId = customerId;
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setCustomerName(String customerName) {
        this.customerName = customerName;
    }
}
java
// Mapping code at the boundary - this is the only place that knows about
// the external field names ID and name
public CustomerRequest mapFromExternalRequest(ExternalRequest external) {
    CustomerRequest pojo = new CustomerRequest();
    pojo.setCustomerId(external.getID());      // external says ID, we say customerId
    pojo.setCustomerName(external.getName());   // external says name, we say customerName
    return pojo;
}

After that mapping step, every other class in your system works with CustomerRequest and uses getCustomerId() and getCustomerName(). If the external API is redesigned and suddenly calls the field userId instead of ID, you update exactly one line in the mapping code. Everything else stays the same.

The second common place where POJOs appear is when talking to a database. You create a student POJO that represents one row in your students table. You fill the POJO from the database, pass it around your application, and eventually write it back. The POJO is the bridge between raw data storage and your application logic.


Part Two: Enum

The Problem That Enum Solves

Before Java had enums, developers faced a genuinely nasty problem when working with fixed sets of values. Imagine you are writing a system that needs to work with days of the week. A common approach was to use integer constants:

java
// Old approach before enum existed - the "magic numbers" problem
public class Day {
    public static final int MONDAY    = 0;
    public static final int TUESDAY   = 1;
    public static final int WEDNESDAY = 2;
    public static final int THURSDAY  = 3;
    public static final int FRIDAY    = 4;
    public static final int SATURDAY  = 5;
    public static final int SUNDAY    = 6;
}

Then you write a method that checks whether a day is a weekend:

java
// Method accepting an integer day value
public boolean isWeekend(int day) {
    if (day == Day.SATURDAY || day == Day.SUNDAY) {
        return true;
    }
    return false;
}

Now here is the problem. When you call this method, you are passing an int. The Java compiler sees only a plain integer. It has absolutely no idea that day is supposed to be a value between 0 and 6 representing a specific day of the week. There is nothing stopping you or any other developer from doing this:

java
// All of these compile without any error
isWeekend(100);    // 100 is not a valid day, but Java accepts it
isWeekend(-5);     // Negative value, still compiles
isWeekend(999);    // No error, no warning

The compiler cannot help you catch the mistake. You would only discover the bug at runtime, which is the worst possible time to find it. This is called the type safety problem. The integer type is too broad. It accepts any number, but your method only makes sense for values 0 through 6.

This is exactly the problem that enum was designed to eliminate.

Introducing Enum: A Collection of Named Constants

An enum is a special type in Java that represents a fixed collection of named constants. Here is how you declare the days of the week as an enum:

java
// Declaring an enum - the keyword is enum instead of class
public enum WeekDay {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;   // semicolon at the end is required
}

Now you rewrite the isWeekend method to accept a WeekDay instead of an int:

java
// Method now accepts only a WeekDay - no invalid values possible
public boolean isWeekend(WeekDay day) {
    if (day == WeekDay.SATURDAY || day == WeekDay.SUNDAY) {
        return true;
    }
    return false;
}

Try passing 100 or 999 to this method now. The compiler immediately refuses with a compile time error. You can only pass one of the seven named constants. The type system enforces your intention. That is type safety, and it is one of the biggest benefits of using enums.

The readability improvement is also enormous. Compare these two calls:

java
isWeekend(6);               // what does 6 mean? you have to go look it up
isWeekend(WeekDay.SUNDAY);  // instantly readable, no ambiguity

What Enum Actually Is Under the Hood

When you write public enum WeekDay { ... }, Java internally converts it into a special class. You are not actually writing it, but here is what the compiler generates for you behind the scenes:

java
// This is roughly what the compiler generates from your enum declaration
// You never write this yourself - Java does it for you
public final class WeekDay extends java.lang.Enum<WeekDay> {
    // Each constant becomes a public static final instance of the class
    public static final WeekDay MONDAY    = new WeekDay("MONDAY",    0);
    public static final WeekDay TUESDAY   = new WeekDay("TUESDAY",   1);
    public static final WeekDay WEDNESDAY = new WeekDay("WEDNESDAY", 2);
    public static final WeekDay THURSDAY  = new WeekDay("THURSDAY",  3);
    public static final WeekDay FRIDAY    = new WeekDay("FRIDAY",    4);
    public static final WeekDay SATURDAY  = new WeekDay("SATURDAY",  5);
    public static final WeekDay SUNDAY    = new WeekDay("SUNDAY",    6);

    // Private constructor - you cannot create new WeekDay instances
    private WeekDay(String name, int ordinal) {
        super(name, ordinal);
    }
}

Two things jump out from this generated code.

First, every enum implicitly extends java.lang.Enum. You do not write this yourself. Java adds it automatically. This is why an enum cannot extend any other class. Java does not support inheriting from multiple classes, and since java.lang.Enum is already occupying the one allowed parent slot, there is no room left. If you try to write public enum WeekDay extends SomeOtherClass, the compiler will refuse with an error.

Second, the constructor is private. This is why you cannot create an enum instance using new WeekDay(). The only instances that ever exist are the ones declared as constants inside the enum. This gives enums their defining property: a fixed, closed set of values.

An enum can implement interfaces though. The rule against extending applies to classes only. You can have public enum WeekDay implements SomeInterface and that works fine.

The Four Built In Methods Every Enum Has

Because every enum extends java.lang.Enum, every enum inherits several useful methods for free. You never have to define these yourself.

values() returns an array containing all the constants in the enum, in the order they were declared. You use this when you want to loop through every constant:

java
// Loop through every day of the week
for (WeekDay day : WeekDay.values()) {
    // WeekDay.values() returns [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
    System.out.println(day);
}

You might wonder how you can call WeekDay.values() when you never defined a values method in your enum. The answer is that Java adds it automatically during compilation because every enum inherits it from java.lang.Enum. It is a static method, so you call it on the class name directly.

ordinal() returns the position of the constant in the enum, starting from zero. MONDAY has ordinal 0, TUESDAY has ordinal 1, and so on up to SUNDAY with ordinal 6:

java
WeekDay day = WeekDay.FRIDAY;
System.out.println(day.ordinal()); // prints 4

name() returns the name of the constant exactly as you wrote it in the declaration, as a String:

java
WeekDay day = WeekDay.FRIDAY;
System.out.println(day.name()); // prints "FRIDAY" in capital letters

valueOf() is the reverse of name(). You pass a String and it finds the matching enum constant. If you pass "FRIDAY" it returns WeekDay.FRIDAY:

java
// Pass a String, get back the matching enum constant
WeekDay day = WeekDay.valueOf("FRIDAY");
System.out.println(day); // prints FRIDAY

This is useful when you receive a day name from user input or a configuration file as a String and need to convert it to a proper typed enum constant.

Adding Fields and Custom Constructors to an Enum

Plain enums with just names are already useful, but enums can do much more. You can add fields, constructors, and methods to an enum. This lets each constant carry its own extra data.

Think of it this way: the enum still represents a fixed set of constants, but now each constant has its own set of properties attached to it.

Here is an example where each day of the week carries a numeric value and a description:

java
public enum EnumSample {
    MONDAY(1, "Start of work week"),
    TUESDAY(2, "Second day"),
    WEDNESDAY(3, "Middle of week"),
    THURSDAY(4, "Fourth day"),
    FRIDAY(5, "End of work week"),
    SATURDAY(6, "Weekend begins"),
    SUNDAY(7, "Rest day");  // semicolon required before field/method declarations

    // Fields that belong to each constant
    private final int val;         // numeric value for this day
    private final String comment;  // description for this day

    // Constructor - always private in an enum, even if you write it as default
    // Java will make it private in the bytecode regardless
    EnumSample(int val, String comment) {
        this.val = val;
        this.comment = comment;
    }

    // Getter for val
    public int getVal() {
        return val;
    }

    // Getter for comment
    public String getComment() {
        return comment;
    }
}

Notice how each constant is followed by its values in parentheses: MONDAY(1, "Start of work week"). These values are passed to the constructor. Monday gets val 1 and the comment "Start of work week", Tuesday gets val 2, and so on.

Remember one important rule: whatever fields and methods you define inside an enum belong to each constant individually. Every single constant gets its own copy of those fields. MONDAY has its own val and comment. TUESDAY has its own val and comment. They are separate.

You access these values like this:

java
EnumSample day = EnumSample.MONDAY;
System.out.println(day.getVal());     // prints 1
System.out.println(day.getComment()); // prints "Start of work week"

Adding Regular Methods to an Enum

You can also add regular instance methods to an enum. Any method you define applies to every constant. Here is a method that prints a message:

java
public enum EnumSample {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    // A method available on every constant
    public void dummyMethod() {
        System.out.println("Default dummy method");
    }
}

Every constant gets this method:

java
EnumSample.TUESDAY.dummyMethod();   // prints "Default dummy method"
EnumSample.THURSDAY.dummyMethod();  // also prints "Default dummy method"

But what if one specific constant wants different behavior? You can override the method for just that constant by providing an inline implementation right after the constant name:

java
public enum EnumSample {
    // MONDAY overrides dummyMethod with its own version
    MONDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Monday specific behavior");
        }
    },
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;

    // Default implementation used by all constants that do not override
    public void dummyMethod() {
        System.out.println("Default dummy method");
    }
}

Now MONDAY has its own version of dummyMethod, while every other constant uses the default.

Enum with Abstract Methods

You can take this one step further and declare the method as abstract. When a method is abstract, there is no default implementation. Every single constant must provide its own version. This is a powerful pattern when each constant truly needs completely different behavior:

java
public enum EnumSample {
    // Every constant must implement the abstract method
    MONDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Monday implementation");
        }
    },
    TUESDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Tuesday implementation");
        }
    },
    WEDNESDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Wednesday implementation");
        }
    },
    // ... and so on for every constant
    THURSDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Thursday implementation");
        }
    },
    FRIDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Friday implementation");
        }
    },
    SATURDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Saturday implementation");
        }
    },
    SUNDAY {
        @Override
        public void dummyMethod() {
            System.out.println("Sunday implementation");
        }
    };  // semicolon required before the abstract method declaration

    // Abstract method - every constant MUST provide an implementation
    public abstract void dummyMethod();
}

If you declare a method abstract and forget to implement it in even one constant, the compiler will refuse to compile. This guarantees that every constant is complete.

Enum Implementing an Interface

An enum cannot extend a class, but it can implement an interface. This is useful when you want all constants to fulfill a contract defined by an interface. Interfaces are perfect for behavior that is truly common to all constants because you write the implementation once, not separately for each constant.

Here is an example where an interface requires a method to convert the constant's name to lowercase:

java
// Interface that requires a toLowercase method
public interface Displayable {
    String toLowercase();
}
java
// Enum implementing the interface - one implementation for all constants
public enum EnumSample implements Displayable {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    // One implementation shared by every constant
    @Override
    public String toLowercase() {
        // this.name() returns the constant name like "MONDAY"
        // .toLowerCase() converts it to "monday"
        return this.name().toLowerCase();
    }
}

Now every constant has the toLowercase() method:

java
System.out.println(EnumSample.MONDAY.toLowercase()); // prints "monday"
System.out.println(EnumSample.FRIDAY.toLowercase()); // prints "friday"

Because this behavior is truly the same for every constant, putting it in an interface implementation is cleaner than making it abstract and writing the same code seven times. When you find yourself repeating identical logic in every constant, that is a signal to use an interface instead of an abstract method.

The Complete Type Safety Advantage

Now you can clearly see why enum defeats the old integer constant approach. Look at the comparison:

java
// Old approach: accepts any integer, no safety
public boolean isWeekendOldWay(int day) {
    if (day == 5 || day == 6) {  // magic numbers, hard to read
        return true;
    }
    return false;
}

// New approach with enum: only WeekDay constants accepted
public boolean isWeekend(WeekDay day) {
    if (day == WeekDay.SATURDAY || day == WeekDay.SUNDAY) {  // self documenting
        return true;
    }
    return false;
}

When you call the old version with isWeekendOldWay(WeekDay.WEDNESDAY), it will just silently use the ordinal value 2 and give you a wrong answer without any warning. When you try to call isWeekend(100), the compiler immediately gives you an error. The type system does the checking before your code even runs.


Part Three: Final

The final Class: Closing the Door on Inheritance

In Java, any class can normally be extended by another class using the extends keyword. Inheritance is a powerful feature. But sometimes you explicitly do not want a class to be extended. You want to lock it down completely. That is what the final keyword does when applied to a class.

When you declare a class as final, no other class can extend it:

java
// This class cannot be extended - it is locked
public final class TestClass {
    public void doSomething() {
        System.out.println("Doing something");
    }
}

If someone tries to extend a final class, the compiler immediately refuses:

java
// This will not compile - TestClass is final
public class AnotherClass extends TestClass {
    // COMPILE ERROR: Cannot inherit from final TestClass
}

The error message is clear and immediate. You find out at compile time, not at runtime.

Why Would You Make a Class Final?

The most important example in the Java standard library is the String class. java.lang.String is declared final. You cannot extend it. This is a deliberate security and correctness decision.

Think about what would happen if String were not final. Someone could create a subclass of String and override the equals() method to behave unexpectedly. They could override other methods to intercept or corrupt data. Since String is used literally everywhere in Java programs, allowing subclassing would be dangerous.

By making String final, Java guarantees that when you have a String, it always behaves exactly as the Java specification defines. No surprises from unexpected subclass behavior.

The same reasoning applies to classes like Integer, Long, Double, and all the other primitive wrapper classes. They are all final. java.lang.Math is final. The security related classes in the standard library are final. Anywhere that correctness and predictability are essential, you use final.

Another practical reason to use final is preventing misuse of your own classes. If you are writing a library and you have a class with specific behavior that must not be changed, mark it final. This sends a clear message: this class is complete, do not extend it.

The final Method: Locking Individual Behavior

You can also apply final to individual methods without making the entire class final. A final method can be called normally but cannot be overridden by a subclass:

java
public class BaseService {

    // This method is locked - subclasses cannot override it
    public final void execute() {
        System.out.println("Executing in a specific way that must not change");
    }

    // This method is open - subclasses can override it
    public void doWork() {
        System.out.println("Default work");
    }
}

public class MyService extends BaseService {

    // This is fine - doWork is not final
    @Override
    public void doWork() {
        System.out.println("My custom work");
    }

    // This would be a compile error - execute is final
    // @Override
    // public void execute() { ... }  // COMPILE ERROR
}

Final methods are useful when you have a class that allows some customization through inheritance but needs to protect its core algorithm from being changed.

There is an important related fact: every private method is implicitly final. A private method is not visible to subclasses at all, so they cannot override it. Adding the final keyword to a private method is redundant but not an error. Similarly, every method in a final class is implicitly final because the class itself cannot be subclassed.

The final Variable: A Value That Never Changes

The final keyword can also be applied to variables. A final variable can be assigned a value exactly once, and after that it can never be changed.

java
// Final variable - can only be assigned once
final int MAX_RETRIES = 3;
MAX_RETRIES = 5;  // COMPILE ERROR: cannot assign a value to final variable MAX_RETRIES

When you combine static and final on a field, you get a true constant. This is the proper way to define constants in Java:

java
public class AppConfig {
    // Convention: static final constants are ALL_CAPS with underscores between words
    public static final int MAX_CONNECTIONS = 100;
    public static final String DEFAULT_HOST = "localhost";
    public static final double PI = 3.14159;
}

Final local variables work the same way. Once assigned, they cannot change:

java
public void processOrder(String orderId) {
    // Final local variable - must be assigned exactly once
    final String processedId = orderId.trim().toUpperCase();
    // processedId = "something else";  // COMPILE ERROR
    System.out.println("Processing: " + processedId);
}

Final parameters are also possible. Marking a method parameter as final means the method body cannot reassign that parameter variable:

java
public void process(final String input) {
    // input = "changed";  // COMPILE ERROR
    System.out.println(input);
}

Summary of What final Does

The final keyword means different things depending on where you put it, but the core idea is always the same: it says that this thing is complete and cannot be changed or extended.

On a class: the class cannot be extended. No subclasses allowed.

On a method: the method cannot be overridden by a subclass. The behavior is locked.

On a variable or field: the variable can only be assigned once. After that assignment it is fixed forever.


Interview Questions and Common Pitfalls

These are the questions that come up repeatedly in Java interviews. Understanding the reasoning behind each answer is more important than memorizing the answer itself.

What is a POJO?

A POJO is a Plain Old Java Object. It is a simple Java class with no framework dependencies, a public default constructor, private fields, and public getter and setter methods. It does not extend framework specific classes and does not implement framework specific interfaces.

Can a POJO extend another class?

No, a proper POJO should not extend any class other than the implicit Object superclass. Extending a framework class creates a dependency on that framework, which defeats the purpose of a POJO.

Why can an enum not extend another class?

Because every enum implicitly extends java.lang.Enum. Java only supports single class inheritance. Since the java.lang.Enum slot is already used, there is no room to extend another class. The compiler enforces this.

Can an enum implement an interface?

Yes. An enum can implement as many interfaces as needed. There is no restriction on implementing interfaces.

Why is the enum constructor always private?

Because you must not be able to create enum instances using new. The only instances of an enum are the constants declared inside it. Even if you write the constructor without an access modifier, Java will make it private in the compiled bytecode.

What does values() return and where is it defined?

values() returns an array of all the constants in the enum in declaration order. It is not defined in your enum source code. Java adds it automatically during compilation. It is inherited through the implicit java.lang.Enum parent class.

What is the difference between ordinal() and a custom value field?

ordinal() is automatically assigned by Java starting from zero for the first constant. You have no control over it. A custom value field is one you define yourself in the enum, and you assign whatever value you want through the constructor.

Why is String final in Java?

String is final to prevent subclassing. If String were not final, a malicious or buggy subclass could override its methods and break the expectations that all Java code has about String behavior. Making it final guarantees that String always behaves exactly as the Java specification defines.

What is the difference between a final class and a final method?

A final class prevents the class from being subclassed at all. A final method allows subclassing of the class but prevents that one specific method from being overridden. You use a final method when you want to allow inheritance but lock down a specific piece of behavior.

What happens if you declare a variable final but do not initialize it?

For local variables and fields, a final variable that is never assigned is a compile error. Java requires that a final variable be assigned exactly once before it is used. For fields, the assignment can happen either at the declaration site or in the constructor, but it must happen.

Can you use switch statements with enums?

Yes, and this is one of the cleanest uses of enums. Java switch statements work natively with enum constants:

java
WeekDay today = WeekDay.MONDAY;

switch (today) {
    case MONDAY:
        System.out.println("Start of the week");
        break;
    case FRIDAY:
        System.out.println("End of the work week");
        break;
    case SATURDAY:
    case SUNDAY:
        System.out.println("Weekend");
        break;
    default:
        System.out.println("Midweek");
}

This is far more readable than a switch on an integer constant.

What does valueOf() do and what happens if you pass an invalid string?

valueOf(String name) looks up the enum constant whose name exactly matches the given string. It is case sensitive. "friday" would throw an IllegalArgumentException because the constant is named "FRIDAY". Always match the exact case.


Putting It All Together

These three concepts address three very different needs.

POJO addresses the need for clean, framework independent data containers that survive changes in external APIs without rippling through your entire codebase. Whenever you need to carry data from one layer of your application to another, a POJO is the right tool.

Enum addresses the need for type safe, self documenting sets of named constants. Whenever your domain has a fixed set of values, like days of the week, months of the year, HTTP status categories, or order states, an enum eliminates the magic number problem and gives the compiler the ability to catch mistakes before they become runtime bugs.

Final addresses the need for correctness guarantees. When a class represents a concept that must not be modified through inheritance, mark it final. When a method implements an algorithm that must not be overridden, mark it final. When a value must never change after it is set, mark the variable final.

Together these three tools give you a much stronger grip on the structure and safety of your Java code. Practice writing each one until the syntax feels natural, and pay attention to the specific rules around each one because the compiler enforces them without exception.