Why use getters and setters/accessors?

When developing object-oriented programs, it is a common practice to use getters and setters, also known as accessors and mutators, to access and modify the values of class fields or variables. While it may seem more convenient to use public fields for direct access, there are several advantages to using getters and setters instead. In this article, we will explore the reasons why getters and setters are preferred over public fields, and how they contribute to code organization, encapsulation, and abstraction.

Encapsulation and Information Hiding

One of the key principles of object-oriented programming is encapsulation. Encapsulation refers to the idea of bundling data and methods that operate on that data within a single unit, known as a class. By using private fields and public getters and setters, we can control access to the data and hide its implementation details from the outside world. This promotes better code organization and reduces the risk of unintended modifications to the data.

Consider the following example:


public class Person {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}
        

In this example, the fields name and age are private, meaning they can only be accessed within the Person class. The public getters and setters provide controlled access to these fields. This allows us to prevent direct modifications to the fields and add additional logic, such as validation checks, without modifying the calling code.

Validation and Error Handling

One of the main benefits of using getters and setters is the ability to perform validation and error handling. With public fields, there is no way to ensure that the assigned values are within a specific range or meet certain criteria. In contrast, by encapsulating the fields and using getters and setters, we can add the necessary validation logic.

For example, let's consider a class representing a bank account:


public class BankAccount {
    private double balance;

    public void setBalance(double balance) {
        if (balance >= 0) {
            this.balance = balance;
        } else {
            throw new IllegalArgumentException("Balance cannot be negative");
        }
    }

    public double getBalance() {
        return balance;
    }
}
        

In this example, the setter setBalance checks if the assigned balance is non-negative. If it is negative, an exception is thrown. This ensures that the balance is always in a valid state, preventing inconsistent data.

Flexibility and Compatibility

By using getters and setters, we gain flexibility and compatibility in our code. Suppose we start with a class that has a public field:


public class Rectangle {
    public int width;
    public int height;
}
        

If we decide later to change the implementation and introduce additional logic or calculations, it becomes difficult to make modifications without breaking compatibility with existing code that directly accesses the fields. However, by using getters and setters from the beginning, we can make changes to the implementation without affecting the external interfaces.

For example:


public class Rectangle {
    private int width;
    private int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public int getWidth() {
        return width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getHeight() {
        return height;
    }

    public int getArea() {
        return width * height;
    }
}
        

In this modified Rectangle class, we have added a new method getArea that calculates the area based on the width and height. If we had used public fields, we would have to modify all the external code that directly accessed the fields. With getters and setters, we can make the change internally while maintaining compatibility with the existing code.

Readability and Maintainability

Using meaningful method names like get and set improves code readability. It provides a clear indication of the purpose of the method and makes the code more self-documenting. Additionally, if we need to modify the behavior of the getters and setters in the future, we can do so without affecting the external code that relies on them.

For example:


public class Book {
    private String title;
    private String author;

    public void setTitle(String title) {
        this.title = title.trim();
    }

    public String getTitle() {
        return title.toUpperCase();
    }

    public void setAuthor(String author) {
        this.author = author.trim();
    }

    public String getAuthor() {
        return author.toUpperCase();
    }
}
        

In this example, the setters setTitle and setAuthor trim the input values to remove leading and trailing whitespace. The getters getTitle and getAuthor return the values in uppercase. If we decide to change the behavior in the future, we can easily modify the getters and setters without affecting the places where they are used.

Inheritance and Polymorphism

Getters and setters play an important role in inheritance and polymorphism. When a class extends another class, it can inherit the fields and methods of the parent class. By using getters and setters, we can provide specialized behavior for the inherited fields in the child classes.

For example:


public class Vehicle {
    private String manufacturer;
    private int year;

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public String getManufacturer() {
        return manufacturer;
    }

    public void setYear(int year) {
        if (year >= 1900 && year <= 2022) {
            this.year = year;
        } else {
            throw new IllegalArgumentException("Invalid year");
        }
    }

    public int getYear() {
        return year;
    }
}

public class Car extends Vehicle {
    private int numberOfDoors;

    public void setNumberOfDoors(int numberOfDoors) {
        this.numberOfDoors = numberOfDoors;
    }

    public int getNumberOfDoors() {
        return numberOfDoors;
    }
}
        

In this example, the Car class extends the Vehicle class and inherits the getters and setters for the manufacturer and year fields. Additionally, it provides specialized getters and setters for its own numberOfDoors field. This allows us to reuse and extend the functionality of the parent class while adding new properties specific to the child class.

Conclusion

In summary, using getters and setters instead of public fields offers several benefits in terms of code organization, encapsulation, abstraction, validation, flexibility, compatibility, readability, maintainability, and support for inheritance and polymorphism. While it may require writing some additional boilerplate code, the advantages far outweigh the drawbacks. By adhering to good programming practices and utilizing getters and setters, we can write robust, extensible, and maintainable code.