Enums, Records & Sealed Types

Enums with Fields & Methods

15 min Lesson 2 of 13

Enums with Fields & Methods

A plain enum whose constants are just names is useful, but it only scratches the surface. In Java, every enum is a full class — and that means each constant can carry its own data and expose behaviour. This lesson shows you how to give enum constants constructors, fields, and methods, and explains why doing so often beats a parallel array or a utility class full of static maps.

Why attach data to constants?

Imagine you model the planets of the solar system. You could store each planet's mass and radius in a separate HashMap, but then every piece of code that needs a planet's mass has to look it up by key and risk a null. A far better design is to let the constant itself carry the data — then the data and the constant are inseparable and always in sync.

Enum constructors and fields

Add a constructor to an enum exactly like you would to a regular class — but note two rules: the constructor is always private (Java enforces this), and each constant must supply the required arguments in parentheses right after its name.

public enum Planet { MERCURY(3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6); private final double mass; // in kilograms private final double radius; // in metres Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } public double mass() { return mass; } public double radius() { return radius; } static final double G = 6.67300E-11; public double surfaceGravity() { return G * mass / (radius * radius); } public double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); } }

You can now write expressive code with zero external data structures:

double earthWeight = 75.0; // kg double mass = earthWeight / Planet.EARTH.surfaceGravity(); for (Planet p : Planet.values()) { System.out.printf("Your weight on %s is %6.2f%n", p, p.surfaceWeight(mass)); }
Enum constructors are implicitly private. Writing private Planet(...) is legal but redundant. You cannot make an enum constructor public or protected — the language forbids it because only the compiler creates enum instances.

Enums as a replacement for int constants

A very common real-world use is HTTP status codes or database error codes that need both a numeric value and a human-readable message:

public enum HttpStatus { OK(200, "OK"), CREATED(201, "Created"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, "Unauthorized"), NOT_FOUND(404, "Not Found"), INTERNAL_SERVER_ERROR(500, "Internal Server Error"); private final int code; private final String reason; HttpStatus(int code, String reason) { this.code = code; this.reason = reason; } public int code() { return code; } public String reason() { return reason; } @Override public String toString() { return code + " " + reason; } public boolean isSuccess() { return code >= 200 && code < 300; } public boolean isError() { return code >= 400; } }

Usage becomes readable and type-safe:

HttpStatus status = HttpStatus.NOT_FOUND; System.out.println(status); // 404 Not Found System.out.println(status.isError()); // true if (status == HttpStatus.NOT_FOUND) { System.out.println("Resource missing: " + status.reason()); }
Override toString() purposefully. The default toString() returns the constant name (e.g. "NOT_FOUND"). Overriding it to return a meaningful representation — like "404 Not Found" — makes logs and error messages much clearer without any extra ceremony at the call site.

Lookup methods — finding a constant by its field value

A common need is the reverse lookup: given a status code integer coming in from a network response, you want the matching HttpStatus constant. The idiomatic approach is a static factory method on the enum itself:

public static HttpStatus fromCode(int code) { for (HttpStatus s : values()) { if (s.code == code) return s; } throw new IllegalArgumentException("Unknown HTTP status code: " + code); }

For performance-critical hot paths with many constants, pre-build a Map in a static initialiser:

import java.util.Map; import java.util.stream.Collectors; import java.util.Arrays; // inside the enum body: private static final Map<Integer, HttpStatus> BY_CODE = Arrays.stream(values()) .collect(Collectors.toUnmodifiableMap(HttpStatus::code, s -> s)); public static HttpStatus fromCode(int code) { HttpStatus s = BY_CODE.get(code); if (s == null) throw new IllegalArgumentException("Unknown code: " + code); return s; }
Static fields in enums initialise after the constants. A static field that references values() must appear after the constant list in the source file, otherwise the JVM will throw an ExceptionInInitializerError at startup. The enum constants are always initialised first.

Abstract methods — per-constant behaviour

Sometimes each constant needs a different implementation of the same operation. You can declare an abstract method on the enum and provide a body for each constant using an anonymous-class-style override:

public enum Operation { PLUS("+") { @Override public double apply(double x, double y) { return x + y; } }, MINUS("-") { @Override public double apply(double x, double y) { return x - y; } }, TIMES("*") { @Override public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { @Override public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } public abstract double apply(double x, double y); @Override public String toString() { return symbol; } }
for (Operation op : Operation.values()) { System.out.printf("%.1f %s %.1f = %.1f%n", 6.0, op, 2.0, op.apply(6.0, 2.0)); } // 6.0 + 2.0 = 8.0 // 6.0 - 2.0 = 4.0 // 6.0 * 2.0 = 12.0 // 6.0 / 2.0 = 3.0

This pattern is powerful because the compiler forces you to implement apply for every constant. Add a new operation? The compiler immediately tells you if you forgot to implement the method.

Implementing interfaces

An enum can implement one or more interfaces, which is particularly useful when you want to treat enum constants and regular objects polymorphically:

public interface Describable { String describe(); } public enum Priority implements Describable { LOW(1), MEDIUM(5), HIGH(10); private final int weight; Priority(int weight) { this.weight = weight; } public int weight() { return weight; } @Override public String describe() { return name() + " (weight=" + weight + ")"; } }
Enums cannot extend classes (they implicitly extend java.lang.Enum), but they can implement as many interfaces as needed. Use this when you want a group of constants to fit into an existing polymorphic design without introducing a separate class hierarchy.

Summary

Java enums are classes. Each constant is a singleton instance of that class, constructed once by the JVM. Giving constants fields and a private constructor bundles data with the constant so it can never get out of sync. Methods on the enum — including abstract ones with per-constant bodies — let each constant encapsulate its own behaviour. This produces code that is type-safe, readable, and easy to extend: adding a new constant forces you to supply all required data and behaviour, and the compiler checks your work.