Module 1: Encapsulation

Module Overview

Learn about encapsulation, access modifiers, defensive copying, and the final keyword in Java.

Learning Objectives

  • Understand the concept of encapsulation in Java
  • Learn how to use access modifiers effectively
  • Implement defensive copying for better data protection
  • Use the final keyword appropriately

Key Concepts

Encapsulation

Encapsulation is the process of hiding implementation details and combining data and methods into a single unit (class). This protects internal state from external interference and allows implementation to change without affecting dependent code.

Benefits of Encapsulation:

  • Protects classes from misuse
  • Prevents unexpected behaviors
  • Allows internal representation to change without affecting dependents

Example:


public class Species {
    // Private fields (encapsulated data)
    private String name;
    private int population;
    private double yearlyGrowthRatePercentage;

    // Constructor validates data upon creation
    public Species(String name, int population, double yearlyGrowthRatePercentage) {
        this.name = name;
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
    
    // Getter methods provide controlled access
    public String getName() {
        return name;
    }
    
    public int getPopulation() {
        return population;
    }
    
    public double getYearlyGrowthRatePercentage() {
        return yearlyGrowthRatePercentage;
    }
    
    // Setter with validation
    public void setPopulation(int population) {
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
    }
}
                

Encapsulation

Encapsulation is the process of hiding all the details of how a piece of software works and describing only enough about the software to enable someone to use it. Data and actions are combined into a single unit (a class in Java) that hides the implementation details. Since the internal state is hidden, and users of the class are provided with all methods necessary to perform actions on the data, how the internal state is represented or modified can be changed by the designers of the class without any of its callers being affected.

To make a class's internal data secure, we need to use private access modifiers, restricting access to the data from outside the class. This provides security to the data of your class by keeping it safe from external interference. Once we have secured the class's data, we can decide what data to expose and how we want to allow callers to change the class data. You can expose data from the class by providing public getter methods (sometimes called accessor methods) and public setter methods (sometimes called mutator methods) to change data. When writing a setter method, we should consider what rules must be followed for the piece of data we are changing. When we have private instance variables and setter methods, the class will always know when its internal state has changed.

Species Example

Let's consider an example. The Species class below is not well encapsulated.

public class Species {

    public String name;
    public int population;
    public double yearlyGrowthRatePercentage;

    public Species(String name, int population, double yearlyGrowthRatePercentage){
        this.name = name;
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
    
    public int predictPopulation(int years) {
        int result = 0;
        double predictedPopulation = population;
        while(years > 0 && predictedPopulation > 0) {
            predictedPopulation += (yearlyGrowthRatePercentage / 100)
                    * predictedPopulation; 
            years--;
        }
        if(predictedPopulation > 0) {
            return (int)predictedPopulation;
        }
        return 0;
    }
}

A user of this class might write code like this to predict the population of a species.

public class SpeciesTester {

    public static void main(String[] args) {
        Species cat = new Species("Asiatic Lion", 20000, 2.0);
        System.out.println(cat.name + " population in 100 years: " + 
                cat.predictPopulation(100));

        //prints 144892
    }
}

However, with public fields, a user of this class might also write code that changes the population to an invalid value, yielding an unexpected population prediction. The constructor of the class prohibits this type of population value when creating a new object of type Species, but the public population field can be changed without the class knowing!

public class SpeciesTester {

    public static void main(String[] args) {
        Species cat = new Species("Asiatic Lion", 20000, 2.0);
        cat.population = -2; // Directly changes population!
        System.out.println(cat.name + " population in 100 years: " + 
                cat.predictPopulation(100));

        //prints 0
    }
}

Our first step to prevent our data fields from being changed to this illegal value is to use the private access modifier. Let's apply it to all of our instance variables.

public class Species {

    // Make instance variables private
    private String name;
    private int population;
    private double yearlyGrowthRatePercentage;

    public Species(String name, int population, double yearlyGrowthRatePercentage){
        this.name = name;
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
    
    public int predictPopulation(int years) {
        int result = 0;
        double predictedPopulation = population;
        while(years > 0 && predictedPopulation > 0) {
            predictedPopulation += (yearlyGrowthRatePercentage / 100)
                    * predictedPopulation; 
            years--;
        }
        if(predictedPopulation > 0) {
            return (int)predictedPopulation;
        }
        return 0;
    }
}

Now, the second line of code below will not compile, as population is private. However, we also cannot use cat.name in the third line of code for the same reason. We can allow access to our name field by writing an accessor method/getter.

Species cat = new Species("Asiatic Lion", 20000, 2.0);
cat.population = -2; // Won't compile! :)
System.out.println(cat.name + " population in 100 years: "
        + cat.predictPopulation(100)); // Also won't compile! :(

In fact, we may want to provide access to all of our instance variables. Below is the Species class with the getter methods.

public class Species {

    private String name;
    private int population;
    private double yearlyGrowthRatePercentage;

    public Species(String name, int population, double yearlyGrowthRatePercentage){
        this.name = name;
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
    
    public int predictPopulation(int years) {
        int result = 0;
        double predictedPopulation = population;
        while(years > 0 && predictedPopulation > 0) {
            predictedPopulation += (yearlyGrowthRatePercentage / 100)
                    * predictedPopulation; 
            years--;
        }
        if(predictedPopulation > 0) {
            return (int)predictedPopulation;
        }
        return 0;
    }

    // New getter methods for all member variables

    public String getName() {
        return name;
    }

    public int getPopulation() {
        return population;
    }

    public double getYearlyGrowthRatePercentage() {
        return yearlyGrowthRatePercentage;
    }
}

Now we can correct our code from above to use the new getName method we wrote.

public class SpeciesTester {

    public static void main(String[] args) {
        Species cat = new Species("Asiatic Lion", 20000, 2.0);
        System.out.println(cat.getName() + " population in 100 years: " + 
                cat.predictPopulation(100));

        //prints 144892
    }
}

We will not allow Species's name field to be changed, but we do want to provide users of the class the ability to change the population and yearlyGrowthRatePercentage values. As we saw above, we know that population cannot be a negative number, so we will need to ensure this when allowing a user to update the value.

public class Species {

    private String name;
    private int population;
    private double yearlyGrowthRatePercentage;

    public Species(String name, int population, double yearlyGrowthRatePercentage){
        this.name = name;
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
    
    public int predictPopulation(int years) {
        int result = 0;
        double predictedPopulation = population;
        while(years > 0 && predictedPopulation > 0) {
            predictedPopulation += (yearlyGrowthRatePercentage / 100)
                    * predictedPopulation; 
            years--;
        }
        if(predictedPopulation > 0) {
            return (int)predictedPopulation;
        }
        return 0;
    }

    public String getName() {
        return name;
    }

    public int getPopulation() {
        return population;
    }

    public double getYearlyGrowthRatePercentage() {
        return yearlyGrowthRatePercentage;
    }

    // Setters that check for invalid state

    public void setPopulation(int population) {
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
    }

    public void setYearlyGrowthRatePercentage(double yearlyGrowthRatePercentage) {
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
}

Hmmm. It seems like we've duplicated the code to validate the population field. Any time we copy code, we should consider if there is something we can do to refactor (change our code) to eliminate this duplication. Instead of having the constructor also have the code to do the validation, we can call our population setter method. This will do the validation and assign the population value. You can see the change below.

public class Species {

    private String name;
    private int population;
    private double yearlyGrowthRatePercentage;

    public Species(String name, int population, double yearlyGrowthRatePercentage){
        this.name = name;
        // Use setter so we get validation
        setPopulation(population);
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
    
    public int predictPopulation(int years) {
        int result = 0;
        double predictedPopulation = population;
        while(years > 0 && predictedPopulation > 0) {
            predictedPopulation += (yearlyGrowthRatePercentage / 100)
                    * predictedPopulation; 
            years--;
        }
        if(predictedPopulation > 0) {
            return (int)predictedPopulation;
        }
        return 0;
    }

    public String getName() {
        return name;
    }

    public int getPopulation() {
        return population;
    }

    public double getYearlyGrowthRatePercentage() {
        return yearlyGrowthRatePercentage;
    }

    public void setPopulation(int population) {
        if(population < 0) {
            throw new IllegalArgumentException("Population must be positive.");
        }
        this.population = population;
    }

    public void setYearlyGrowthRatePercentage(double yearlyGrowthRatePercentage) {
        this.yearlyGrowthRatePercentage = yearlyGrowthRatePercentage;
    }
}

Now our Species class is well encapsulated 😃

AmazonLockerCell Example

When designing an encapsulated class, you should not expose setter and getter methods to all of your class's fields by default. Consider the unfinished AmazonLockerCell class below.

/**
* A class representing an individual cell in an amazon locker. When a locker is not
* empty it will have an unlock code, meaning it is locked until the emptyLockerCell
* method is called. 
*/
public class AmazonLockerCell {

    private String unlockCode = "";
    private boolean isEmpty = true;

    public String fill() {
        if(isEmpty) {
            isEmpty = false;
            unlockCode = generateLockerCellCode();
            return unlockCode;
        } else {
            throw new InvalidStateException("Locker Cell is already occupied.");
        }
    }

    public boolean emptyLockerCell(String code) {
        if(code.isEmpty()) {
            throw new IllegalArgumentException("An unlock code must be provided.");
        }
        if(code.equals(unlockCode)) {
            unlockCode = "";
            isEmpty = true;
            return true;
        }
        return false;
    }

    private String generateLockerCellCode() {
        // code to generate a unique locker code
    }
}

Currently, the internal state of the AmazonLockerCell can only be changed by calling the public methods fill and emptyLockerCell. Allowing setter methods that update the unlockCode or isEmpty values outside of the fill and emptyLockerCell methods would result in an invalid state. For example, you could end up filling a locker cell that is not actually empty by editing the isEmpty field to be incorrectly true. In this case, it is the right choice not to provide setter methods. How about getter methods? Should we allow the users of our class to get the values of the locker cell's unlock code or whether it is empty? It certainly seems reasonable to allow a caller to check if a locker cell is empty before trying to fill it. Who should be able to see the locker cell's unlock code, and when? The person who fills it should be able to, so it makes sense that the fill method returns it, but it seems unsafe to reveal the unlock code to any arbitrary weirdo! Based on this reasoning, we will finish the AmazonLockerCell class by adding no setter methods and only a getter for the isEmpty field.

/**
* A class representing an individual cell in an amazon locker. When a locker is not
* empty it will have an unlock code, meaning it is locked until the emptyLockerCell
* method is called. 
*/
public class AmazonLockerCell {

    private String unlockCode = "";
    private boolean isEmpty = true;

    public String fill() {
        if(isEmpty) {
            isEmpty = false;
            unlockCode = generateLockerCellCode();
            return unlockCode;
        } else {
            throw new InvalidStateException("Locker Cell is already occupied.");
        }
    }

    public boolean emptyLockerCell(String code) {
        if(code.isEmpty()) {
            throw new IllegalArgumentException("An unlock code must be provided.");
        }
        if(code.equals(unlockCode)) {
            unlockCode = "";
            isEmpty = true;
            return true;
        }
        return false;
    }

    private String generateLockerCellCode() {
        // code to generate a unique locker code
    }

    public boolean isEmpty() {
        return isEmpty;
    }
}

TimeSpan Example

One of the benefits of encapsulation is that we can change the internal representation of a class without changing any of its callers. Consider the TimeSpan class below.

public class TimeSpan {
    private int minutes;

    // Contructs an empty time span 
    public TimeSpan() {
        this.minutes = 0;
    }

    public void add(int minutes) {
        if (minutes < 0) {
            throw new IllegalArgumentException("A positive number of "
                + "minutes must be added.");
        }

        this.minutes += minutes;
    }

    public double getTotalTimeInHours() {
        return minutes / (1.0 * 60);
    }

    public int getHours() {
        return minutes / 60;
    }

    public int getMinutes() {
        return minutes % 60;
    }

    public String toString() {
        return String.format("Timespan - [H: %d, M: %d]", getHours(), getMinutes());
    }
}

Since int primitive values have a maximum value, the TimeSpan class above can accommodate time spans of about 4000 years. What if Blue Origin wanted to use this class to calculate the time to reach the nearest star? Instead of keeping track of just minutes, we could store hours and minutes separately! This would allow us to keep track of time spans of around 249,000 years. Let's take a look at the new implementation below.

public class TimeSpan {
    private final BigInteger sixty = new BigInteger("60");
    private int minutes;
    private int hours;

    // Contructs an empty time span
    public TimeSpan() {
        this.minutes = 0;
        this.hours = 0;
    }

    public void add(int minutes) {
        if (minutes < 0) {
            throw new IllegalArgumentException("Minutes cannot be negative.");
        }

        BigInteger timeSpanInMinutes = getTimeSpanInMinutes();
        BigInteger minutesToAdd = new BigInteger(Integer.toString(minutes));

        BigInteger totalMinutes =  timeSpanInMinutes.add(minutesToAdd);

        this.hours = totalMinutes.divide(sixty).intValue();
        this.minutes = totalMinutes.mod(sixty).intValue();
    }

    private BigInteger getTimeSpanInMinutes() {
        BigInteger hoursBI = new BigInteger(Integer.toString(this.hours));
        BigInteger minutesBI = new BigInteger(Integer.toString(this.minutes));

        BigInteger hoursInMinutes = hoursBI.multiply(sixty);

        return hoursInMinutes.add(minutesBI);
    }

    public double getTotalTimeInHours() {
        return getTimeSpanInMinutes().divide(sixty).doubleValue();
    }

    public int getHours() {
        return hours;
    }

    public int getMinutes() {
        return minutes;
    }

    public String toString() {
        return String.format("Timespan - [H: %d, M: %d]", getHours(), getMinutes());
    }
}

We just saw a major benefit of encapsulation in action! The designer of the class was able to update the code of TimeSpan and no caller was affected. The public methods all remained the same; just how the time span is kept track of was changed. Since no user of the class could directly access any internal state, these changes were made easily!

Go forth and encapsulate your classes! Good luck!

Access Modifiers

Access modifiers control the visibility and accessibility of classes, methods, and fields.

Modifier Class Package Subclass World
public Yes Yes Yes Yes
protected Yes Yes Yes No
default (no modifier) Yes Yes No No
private Yes No No No

Defensive Copying

Defensive copying protects against unintentional or malicious modification of mutable objects by creating a copy of the object when passing it to or from a method.

Example:


import java.util.ArrayList;
import java.util.List;

public class EndangeredSpeciesList {
    private final List<String> speciesList;
    
    // Constructor with defensive copy
    public EndangeredSpeciesList(List<String> initialList) {
        // Create a defensive copy
        this.speciesList = new ArrayList<>(initialList);
    }
    
    // Getter with defensive copy
    public List<String> getSpeciesList() {
        // Return a copy, not the original
        return new ArrayList<>(speciesList);
    }
    
    // Add species to the list
    public void addSpecies(String species) {
        speciesList.add(species);
    }
}
                

Without defensive copying, the internal list could be modified outside the class:


// Without defensive copying:
List<String> originalList = new ArrayList<>();
originalList.add("Bengal Tiger");
EndangeredSpeciesList endangered = new EndangeredSpeciesList(originalList);

// External code could modify the internal state
originalList.add("Black Rhino"); // This would affect the internal list!

// With proper defensive copying, the original list changes won't affect the internal list
                

The Final Keyword

The final keyword can be applied to variables, methods, and classes to restrict their modification.

Final Variables:

  • Final variables can only be assigned once
  • For object references, the reference cannot change, but the object's state can still be modified (unless it's immutable)

Final Methods:

  • Cannot be overridden by subclasses
  • Used to prevent unexpected behavior in subclasses

Final Classes:

  • Cannot be extended (subclassed)
  • Used for security or design reasons (e.g., String, Integer)

Example:


public class Constants {
    // Final variable (constant)
    public static final double PI = 3.14159;
    
    // Final instance variable (must be initialized in constructor)
    private final String id;
    
    public Constants(String id) {
        this.id = id;
    }
    
    // Final method (cannot be overridden)
    public final String getId() {
        return id;
    }
}

// Final class (cannot be extended)
final class ImmutablePoint {
    private final int x;
    private final int y;
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() {
        return x;
    }
    
    public int getY() {
        return y;
    }
}
                

Guided Projects

Mastery Task 3: Class-ified Information

Mastery Task Guidelines

Mastery Tasks are opportunities to test your knowledge and understanding through code. When a mastery task is shown in a module, it means that we've covered all the concepts that you need to complete that task. You will want to make sure you finish them by the end of the week to stay on track and complete the unit.

Each mastery task must pass 100% of the automated tests and code styling checks to pass each unit. Your code must be your own. If you have any questions, feel free to reach out for support.

You have discovered a task on the backlog indicating that the Order class is not properly encapsulated. Demonstrating Ownership and Bias for Action, you pick up the task.

Milestone 1: Security Survey

Create a test file called OrderTest at com.amazon.ata.deliveringonourpromise.types.

Write a unit test that fails if the Order class has any externally modifiable variables.

You don't write any unit tests that try to reassign variables declared private directly (the compiler will prevent you!).

Focus your efforts on writing test(s) that try to modify objects that your class might expose, either by accepting the object as an argument or by returning the object as a method return value. Focus on the object(s) that is/are mutable, meaning you can modify the object the variable points to in some way without reassigning the variable itself. (Think of objects that contain other objects).

Milestone 2: Fortify

Encapsulate the Order class.

Each test should be run separately using the following commands - one command per test:

./gradlew -q clean :test --tests "com.amazon.ata.deliveringonourpromise.types.*"

./gradlew -q clean IRT

./gradlew -q clean MasteryTaskThreeTests

Exit Checklist

  • You have written unit tests that verify encapsulation of the Order class
  • Your Mastery Task 3 TCTs are passing locally
  • You have pushed your code
  • Mastery Task 3's TCTs are passing in CodeGrade

Additional Resources