Week 3

Collections

Generics

Enums

Stream APIs

Unit Testing with JUnit

Debugging in Java

Practice

Assignment

Back end Track

Introduction

Using magic strings or integers for status codes, user roles, and method options can quickly get out of hand and make your code difficult to organize. That’s where Enums come in to provide a clean, compile-time way to represent a fixed set of values directly in the Java language!

We'll start by exploring the problem with magic strings and numbers, then learn how to solve it with constants, and finally discover an even better solution with enums!

Magic strings and numbers

Magic strings and numbers are called magic because their meaning exist only in the developer’s head. Take the below code for example

if (account.getType().equals(2)) {
	...
}

Who's the only person who knows what 2 means here? The developer who wrote it. You could look at documentation to find the account type specifications, but that's minutes lost for what could have been a moment. Worse yet, what if you need to ask an API team about it? Now you have to wait even longer. A lot of time could be saved here 🙂

Magic strings and numbers might seem evil, but not all are equal. Some are worse than others. The least problematic ones aren't ideal, but they're temporarily tolerable if you can justify their use. Before going further, let's do a quick exercise:

💬 Try sorting the scenarios below by tolerability. Number them from 1 to 4, with 1 being least tolerable (requiring an immediate refactor) and 4 being acceptable technical debt.

<aside> ❗

The above exercise isn't meant to justify using magic strings. Later in this chapter, you'll learn tools to handle all these cases—and you should handle them. Even if you have a good reason for a magic string, it's often better to avoid it entirely so your pull request doesn't get delayed while you defend your choice.

</aside>

Constants

One solution for magic numbers and strings is constants. You likely know what constants are from JavaScript—for example, const applicationName = "WeatherAPI".

Java handles constants differently and more semantically. Here's how:

final Keyword:

The final keyword in Java is somewhat like a constant. If thinking of it that way helps you learn, that's fine—but the keyword is meant to be semantic. It simply means: this final class, method, variable, or field can be set only once.

For example, the final field manufacturer below can only be set once during class instantiation (in your class constructor):

package com.hyf;

public class Car {
  // A final String that is not set directly
  private final String manufacturer;

  public Car(String manufacturer) {
    // Here the manufacturer field is being set and where it becomes constant
    this.manufacturer = manufacturer;
  }

  public String getManufacturer() {return manufacturer;}
}

const Keyword:

This is a reserved keyword in Java but not usable. Possibly to avoid confusion with other programming languages.

Compile-time Constant:

Compile-time constant means a constant that is final when an application is compiling, of course by extension it is also final in runtime. In plain English, the value you see, will be the value your running JVM sees always.

However, final on its own doesn’t make a compile-time constant. Let’s take our Car class above, importing the class and running it:

import com.hyf.Car;

void main() {
  Car bmw = new Car("BMW");
  Car audi = new Car("Audi");
  IO.println("Car 1 manufacturer: " + bmw.getManufacturer());
  IO.println("Car 2 manufacturer: " + audi.getManufacturer());
}

As you can see, the final field is only final with respect to the class it’s in. To force compile-time immutability, you have to add static keyword to final. Example:

public class Car {
  private final String manufacturer;
  // Compile-time constant. Can now be public as it's fully immutable
  public static final String TRANSPORT_TYPE = "Land";

  public Car(String manufacturer) {
    this.manufacturer = manufacturer;
  }

  public String getManufacturer() {return manufacturer;}
}

void main() {
  Car bmw = new Car("BMW");
  Car audi = new Car("Audi");

  IO.println("Car 1 manufacturer: " + bmw.getManufacturer() + " and transport type: " + Car.TRANSPORT_TYPE);
  IO.println("Car 1 manufacturer: " + audi.getManufacturer() + " and transport type: " + Car.TRANSPORT_TYPE);
}

Java offers two types of constants: compile-time constants using static final and instance-level final variables using final. Both replace magic strings and numbers with clearly named variables, making your code more readable and reusable!

Let's revisit our magic string examples to see how compile-time constants help:

package com.hyf.constants;

public class OrderStatus {
  public static final Integer COMPLETED = 0;
  public static final Integer CANCELLED = 1;
  public static final Integer PENDING = 2;
}

Then we can import and use like below

import com.hyf.constants.OrderStatus;

void shipOrder(int status) {
  if (OrderStatus.PENDING.equals(status)) {
    IO.println("Shipping order...");
  }
}

This is much better, but there's still one issue: the shipOrder method accepts any int. This means you can introduce a bug that slips past the compiler and only appears at runtime. This is where enums come in!

Enums

Enums are a type-safe way of defining fixed values that the compiler enforces. Instead of using magic strings like "PENDING" or magic numbers like 2, you create a small list of allowed options: OrderStatus.PENDING, UserType.MEMBER, or HttpStatus.FORBIDDEN.

This means your code becomes much safer and easier: the compiler immediately stops you from typing a typo (OrderStatus.PENDNG) or passing a wrong value (999). Your IDE shows autocomplete with only the valid choices, so you write faster and catch bugs before the app even starts running.

Defining an Enum

Defining an enum in Java is simple! Take a look:

public enum OrderStatus {
  COMPLETED,
  SHIPPED,
  PENDING,
  CANCELLED
}

Now we can use the enum like below:

// The method now takes OrderStatus enum rather than an "unbounded" integer
void shipOrder(OrderStatus status) {
  if (OrderStatus.PENDING.equals(status)) {
    IO.println("Shipping order...");
  }
}

<aside> 💡

With autocomplete/code-completion, enum values are much easier to type than ints and strings too! Give it a try in IntelliJ!

</aside>

Enum built-in methods

Java enums automatically provide handy built-in methods that you can use. Going over the 4 main ones:

name()

Returns the exact constant name as a String.

void main() {
  IO.println(OrderStatus.PENDING.name());
}

ordinal()

Returns the constant's position in the declaration. It's not very useful for developers and is more intended for helping other data structures. You can find its usage example with values()

<aside> ⚠️

The order isn't guaranteed to stay the same. Don't base your business logic on ordinal(). Developers don't expect business logic to depend on it, so they might reorder the enum values—breaking your code.

</aside>

values()

A static method that you can directly call on OrderStatus rather than on one of its constants, say OrderStatus.PENDING

import com.hyf.constants.OrderStatus;

void main() {
  for (OrderStatus orderStatus : OrderStatus.values()) {
    IO.println("Enum " + orderStatus + " has ordinal value: " + orderStatus.ordinal());
  }
}
// OUTPUT:
// Enum COMPLETED has ordinal value: 0
// Enum SHIPPED has ordinal value: 1
// Enum PENDING has ordinal value: 2
// Enum CANCELLED has ordinal value: 3

valueOf(String)

A static method that converts a string back to the matching enum constant (throws IllegalArgumentException if the name doesn’t exist).

OrderStatus orderStatus = OrderStatus.valueOf("PENDING");

IO.println(orderStatus);

Customizing Enums

You might be wondering what we mean by static method for an enum and why the fixed values defined in them are called its constants or instances.

Enums: Immutable objects under the hood

Enums are instantiated once for each fixed value you define. So the enum below is instantiated four times to produce four constant instances:

public enum OrderStatus {
  COMPLETED,
  SHIPPED,
  PENDING,
  CANCELLED
}

We can prove it even further!

💬 How does the new keyword instantiate an object out of a class? Eg new Car("BMW")

Since that’s how the new keyword works, we can look for the enum constructor, override it, add a print statement and see if it prints.

package com.hyf.enums;

public enum OrderStatus {
  COMPLETED,
  SHIPPED,
  PENDING,
  CANCELLED; // Semicolon is the end of the constants section
  // From here below, enums function more-or-less like a class
  // Some things you can't do but compiler/IDE will quickly let you know

  // Adding a constructor without any arguments and hoping it gets called.
  OrderStatus() {
    // If the constructor is called then this line will be printed
    IO.println("Initializing the order status " + this.name());
  }
}

And now testing:

import com.hyf.enums.OrderStatus;

void main() {
  // Have to use enum once otherwise Java will skip
  IO.println(OrderStatus.CANCELLED);
}
// OUTPUT:
// Initializing the order status COMPLETED
// Initializing the order status SHIPPED
// Initializing the order status PENDING
// Initializing the order status CANCELLED
// CANCELLED

<aside> 🎉

This is how Java provides new functionality while keeping the underlying behavior consistent. Once you understand a few core concepts in Java well, the rest will fall into place naturally. Congratulations on making it this far!

</aside>

Adding More Functionality to Enums

Now that you know enums behave like instantiated objects, you can customize them in many ways. One useful customization is to store your application's error messages in an enum. Go through the code below by the numbered comments 1→2→3

package com.hyf.enums;

public enum ErrorMessage {
	// 2. So that our constants can take another value, the String here
  ACCOUNT_NOT_FOUND("Account could not be found in our system"),
  USER_NOT_AUTHORIZED("Your credentials are not sufficient, please try login in again.");
  
  // 3. Which is being set to this private field "message"
	private String message;

	// 1. We've customized our enum constructor here
  ErrorMessage(String message) {
    this.message = message;
  }

	// Getter public method to safely retrieve the private field message
  public String getMessage() {
    return message;
  }
}

Usage:

import com.hyf.enums.ErrorMessage;

void printErrorMessage(ErrorMessage errorMessage) {
  IO.println(errorMessage.getMessage());
}

void main() {
  printErrorMessage(ErrorMessage.ACCOUNT_NOT_FOUND);
}
// OUTPUT:
// Account could not be found in our system

When to use enums vs constants?

Use a static final constant when you have a single, unchanging value that doesn’t belong to a group. Timeout, API base-url, tax rate, application name etc…

Use an enum when you have a fixed, closed set of related options (statuses, roles, payment types, etc.).

Exercise

Modeling Payment Methods with an Enum

Online stores—especially in the Netherlands—need to handle different payment options, each with its own display name and transaction fee. Now that you know enums, can you model this cleanly with type safety, fields, a constructor, and a useful method?

Your task:

Create an enum called PaymentMethod with these values:

Add:

Example usage you should be able to write after completing it:

PaymentMethod method = PaymentMethod.IDEAL;
System.out.println(method.getDisplayName());        // "iDEAL"
System.out.println(method.calculateFee(100.0));     // 0.5

Bonus: Use values() in a loop to print every payment method and its fee for a €200 purchase.