Lesson Wednesday

So far we've made all attributes of our custom classes public. But do you remember what we discussed in this lesson? Making all properties public, and accessing them with dot notation (ie: testVehicle.mPrice;) isn't actually best practice. We did this for the first few days of the course in order to focus on Java fundamentals without added complexity.

Now that we've begun getting comfortable with Java, let's learn the proper way to handle a custom class' properties and member variables. In this lesson we will discuss best practices for access level modifiers, and the concepts of encapsulation and visibility.

Access Level Modifiers

So far, all our class' properties have been declared public, like this:

car-dealership/src/main/java/Vehicle.java
class Vehicle {
  public int mYear; 
  public String mBrand;
  public String mModel;
  public int mMiles;
  public int mPrice;

  ...

}

As discussed previously, public in the example above is an access level modifier. It determines who may access a property or method. Declaring the properties above public means they're available to everyone. Unfortunately, this isn't very secure.

Fields may also be declared private instead of public, which means you cannot directly access the variables. Only the class itself may access properties declared private. This is far more secure, and considered to be best practice.

Private Access Level Modifiers

Let's make the properties of the Vehicle class private:

car-dealership/src/main/java/Vehicle.java
class Vehicle {

  private int mYear;
  private String mBrand;
  private String mModel;
  private int mMiles;
  private int mPrice;

  public Vehicle(int year, String brand, String model, int miles, int price) {
    mYear = year;
    mBrand = brand;
    mModel = model;
    mMiles = miles;
    mPrice = price;
  }

  public boolean worthBuying(int maxPrice){
    return (mPrice < maxPrice);
  }

}

All we had to do was change the access level modifier in front of each member variable from public to private. Now, if we recompile and run the program...

$ gradle compileJava

...We're met with 15 compiler errors. Oh no!

$ gradle compileJava

:compileJava

/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:32: error: mYear has private access in Vehicle
          System.out.println( individualVehicle.mYear );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:33: error: mBrand has private access in Vehicle
          System.out.println( individualVehicle.mBrand );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:34: error: mModel has private access in Vehicle
          System.out.println( individualVehicle.mModel );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:35: error: mMiles has private access in Vehicle
          System.out.println( individualVehicle.mMiles );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:36: error: mPrice has private access in Vehicle
          System.out.println( individualVehicle.mPrice );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:46: error: mYear has private access in Vehicle
            System.out.println( individualVehicle.mYear );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:47: error: mBrand has private access in Vehicle
            System.out.println( individualVehicle.mBrand );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:48: error: mModel has private access in Vehicle
            System.out.println( individualVehicle.mModel );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:49: error: mMiles has private access in Vehicle
            System.out.println( individualVehicle.mMiles );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:50: error: mPrice has private access in Vehicle
            System.out.println( individualVehicle.mPrice );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:68: error: mYear has private access in Vehicle
          System.out.println( userVehicle.mYear );
                                         ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:69: error: mBrand has private access in Vehicle
          System.out.println( userVehicle.mBrand );
                                         ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:70: error: mModel has private access in Vehicle
          System.out.println( userVehicle.mModel );
                                         ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:71: error: mMiles has private access in Vehicle
          System.out.println( userVehicle.mMiles );
                                         ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:72: error: mPrice has private access in Vehicle
          System.out.println( userVehicle.mPrice );
                                         ^
15 errors
:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.675 secs

As you can see, each of the 15 errors this message summarizes is the same: Each Vehicle property (mPrice, mMiles, mModel, mBrand, and mYear) has private access in Vehicle. All of the 15 errors above also cite the same file: App.java.

Because these properties are now private, they cannot be accessed from outside the class. That means code in App.java cannot access the properties declared in Vehicle.java. This is the cause the errors from the message depicted above.

Getters and Setters

But if outside classes shouldn't access Vehicle properties directly, how can we reference the details of a Vehicle in our program? After all, our command line interface application needs to print information about each vehicle to the user.

The most common solution is using special methods known as getters and setters. They are responsible for indirectly accessing private variables. As their names imply, getters "get" information, and setters "set" information.

Writing Getter Methods

Let's begin by adding a getter method to our Vehicle class for the mPrice property:

car-dealership/src/main/java/Vehicle.java
class Vehicle {

  private int mYear;
  private String mBrand;
  private String mModel;
  private int mMiles;
  private int mPrice;

  public Vehicle(int year, String brand, String model, int miles, int price) {
    mYear = year;
    mBrand = brand;
    mModel = model;
    mMiles = miles;
    mPrice = price;
  }

  public boolean worthBuying(int maxPrice){
    return (mPrice < maxPrice);
  }

  public int getPrice() {
    return mPrice; 
  }

}
  • Here, we define a getter method called getPrice(). The commonly-used naming convention for a getter method is getNameOfProperty().

  • Getter methods are also public. Unlike the mPrice field itself, we want to access this method from outside the class.

  • As you can see, the method simply returns (or "gets") mPrice.

Important Note: As with any other method or behavior, you should always write a JUnit test for each getter method before coding their logic. In order to focus exclusively on encapsulation and visibility, this lesson will temporarily omit testing. The next lesson will explore testing with objects, including writing tests for getter and setter methods.

Calling Getter Methods

We can call getPrice() from outside the Vehicle class in App.java. Because the method is part of the Vehicle class, it will have access to the mPrice property. It can therefore provide this property to App.java without error.

We'll replace all instances of mPrice in App.java with the getPrice() method:

car-dealership/src/main/java/App.java
...

public class App {
  public static void main(String[] args) {

    ...

      if (navigationChoice.equals("All Vehicles")){
        for ( Vehicle individualVehicle : allVehicles ) {

          ...

          System.out.println( individualVehicle.getPrice(); );
        }
      } else if (navigationChoice.equals("Search Price")){

          ...

        for ( Vehicle individualVehicle : allVehicles ) {
          if (individualVehicle.worthBuying(userMaxBudget)){

            ...

            System.out.println( individualVehicle.getPrice(); );
          }
        }
      } else if (navigationChoice.equals("Add Vehicle")){
          ...

          System.out.println("Alright, here's your new vehicle:");

          ...

          System.out.println( userVehicle.getPrice() );

      ...
  ...
...

If we recompile, we can see there are now only 12 errors. We no longer receive private access errors when accessing a vehicle's cost with getPrice():

$ gradle compileJava

:compileJava
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:32: error: mYear has private access in Vehicle
          System.out.println( individualVehicle.mYear );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:33: error: mBrand has private access in Vehicle
          System.out.println( individualVehicle.mBrand );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:34: error: mModel has private access in Vehicle
          System.out.println( individualVehicle.mModel );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:35: error: mMiles has private access in Vehicle
          System.out.println( individualVehicle.mMiles );
                                               ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:46: error: mYear has private access in Vehicle
            System.out.println( individualVehicle.mYear );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:47: error: mBrand has private access in Vehicle
            System.out.println( individualVehicle.mBrand );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:48: error: mModel has private access in Vehicle
            System.out.println( individualVehicle.mModel );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:49: error: mMiles has private access in Vehicle
            System.out.println( individualVehicle.mMiles );
                                                 ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:68: error: mYear has private access in Vehicle
          System.out.println( userVehicle.mYear );
                                         ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:69: error: mBrand has private access in Vehicle
          System.out.println( userVehicle.mBrand );
                                         ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:70: error: mModel has private access in Vehicle
          System.out.println( userVehicle.mModel );
                                         ^
/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:71: error: mMiles has private access in Vehicle
          System.out.println( userVehicle.mMiles );
                                         ^
12 errors
:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.72 secs

Let's create similar getter methods for each remaining Vehicle property:

car-dealership/src/main/java/Vehicle.java
class Vehicle {

  private int mYear;
  private String mBrand;
  private String mModel;
  private int mMiles;
  private int mPrice;

  public Vehicle(int year, String brand, String model, int miles, int price) {
    mYear = year;
    mBrand = brand;
    mModel = model;
    mMiles = miles;
    mPrice = price;
  }

  public boolean worthBuying(int maxPrice){
    return (mPrice < maxPrice);
  }

  public int getPrice() {
    return mPrice;
  }

  public int getYear() {
    return mYear;
  }

  public String getBrand() {
    return mBrand;
  }

  public String getModel() {
    return mModel;
  }

  public int getMiles() {
    return mMiles;
  }

}

And replace each instance of a member variable in App.java with its corresponding getter method:

car-dealership/src/main/java/App.java
...

public class App {
  public static void main(String[] args) {

    ...

      if (navigationChoice.equals("All Vehicles")){
        for ( Vehicle individualVehicle : allVehicles ) {
          System.out.println( "----------------------" );
          System.out.println( individualVehicle.getYear() );
          System.out.println( individualVehicle.getBrand() );
          System.out.println( individualVehicle.getModel() );
          System.out.println( individualVehicle.getMiles() );
          System.out.println( individualVehicle.getPrice() );
        }
      } else if (navigationChoice.equals("Search Price")){

          ...

        for ( Vehicle individualVehicle : allVehicles ) {
          if (individualVehicle.worthBuying(userMaxBudget)){
            System.out.println( "----------------------" );
            System.out.println( individualVehicle.getYear() );
            System.out.println( individualVehicle.getBrand() );
            System.out.println( individualVehicle.getModel() );
            System.out.println( individualVehicle.getMiles() );
            System.out.println( individualVehicle.getPrice() );
          }
        }
      } else if (navigationChoice.equals("Add Vehicle")){

          ...

          System.out.println( userVehicle.getYear() );
          System.out.println( userVehicle.getBrand() );
          System.out.println( userVehicle.getModel() );
          System.out.println( userVehicle.getMiles() );
          System.out.println( userVehicle.getPrice() );
      } else if (navigationChoice.equals("Exit")){
          ...
    ...
...       

We should now be able to successfully compile:

$ gradle compileJava

:compileJava

BUILD SUCCESSFUL

Total time: 0.657 secs

And, if we launch our program it still functions as normal:

$ java App

Welcome to our car dealership. What would you like to do? Enter one of the following options: All Vehicles, Search Price, Add Vehicle or Exit
Add Vehicle

Alright, let's add a vehicle! What year was this vehicle made?
1991

Great! What make or brand is the vehicle?
Chrysler

Got it! What model is it?
Lebaron

And how many miles does it have on it?
300000

Finally, what's its price?
800

Alright, here's your new vehicle:
----------------------
1991
Chrysler
Lebaron
300000
800

Setter Methods

Similar to the manner that getter methods are responsible for "getting" an object's private property, setter methods "set" a private property of an existing object.

For example, if our dealership held a sale we would need to lower the cost of each Vehicle. However, since our mPrice property is private, we would need to define a setter method to access it and set it to a new, updated sale price.

Because our small applications won't be required to update an object's properties until next week, we won't create setter methods until then. For now, just know a setter method is responsible for accessing a private field from an outside class and setting it to a new value.

Encapsulation and Visibility

This state of being public or private is known as a property's visibility. Additionally, making all properties private and managing all data manipulation inside an object's own class is called encapsulation. Both are very important concepts in object-oriented programming across many different languages.

Benefits of Encapsulation

Encapsulating a class' information is considered the best, most professional practice for several reasons:

  • It allows a class to have total control over its own fields, which is more secure.
  • It prevents other classes from accessing and altering properties, which can lead to difficult-to-untangle bugs.
  • While we may not see this benefit firsthand until our applications become larger, it results in far easier to maintain code. Imagine that we eventually needed to change the datatype of an object's public property. If outside classes were directly accessing this property (ie: testVehicle.mPrice;, instead of testVehicle.getPrice();), we would need to update all code in all outside classes that references this property. This might not seem like a big deal, but imagine a Java application with tens or hundreds of classes. We would have to comb through each one, and change any reference to this property. As you can imagine, this is a lot of work. However, if the property were private and outside classes accessed it through its getter method, only the object's getter method and class would need to be altered. We could update this class independently without effecting other classes that rely upon it. As you begin developing more complex programs, you'll begin to see how huge of a benefit this is.
  • Managing all aspects of a class within that class itself leads to more organized code. Organized code allows other developers to comprehend, collaborate, and/or implement your logic much more easily.

From this point forward, declare properties of your classes private, and define getter methods when class properties are required outside of the class (like in the front-end user interface).

In the next lesson, we'll walk through how these object-oriented practices fit into the Behavior-Driven Development workflow. Particularly the process of creating JUnit tests for our getter and setter methods.

Terminology


  • Access Level Modifier: The declaration of public, private or protected in front of a property or method of a class. Determines who may access a particular element in a class.

  • Getters and Setters are methods which either get or set the values of an object's properties, and can include some error checking to make sure it is done correctly.

  • Encapsulation: Keeping all the data manipulation localized inside of the object. For instance, making all class properties private, and only accessing them via a method defined in that same class.

Overview


  • When a property's visibility is set to private instead of public, it is inaccessible for reading or writing from anywhere in the code, except for within the object's class.

  • It is common practice to declare all properties of a class as private, and access them by means of getters and setters.

  • Moving forward, we will make all object properties private and access them solely by means of getter methods we define for ourselves.

Examples


Compiler error message alerting that we cannot access a member variable or field due to it being private:

$ gradle compileJava

:compileJava

/Users/staff/Desktop/java/car-dealership/src/main/java/App.java:32: error: mYear has private access in Vehicle
          System.out.println( individualVehicle.mYear );
                                               ^
1 error
:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.675 secs

If you receive this error, it means you're accessing a property of an object that is not public. You should use a getter method instead.

Example class with getter methods for all properties:

car-dealership/src/main/java/Vehicle.java
class Vehicle {

  private int mYear;
  private String mBrand;
  private String mModel;
  private int mMiles;
  private int mPrice;

  public Vehicle(int year, String brand, String model, int miles, int price) {
    mYear = year;
    mBrand = brand;
    mModel = model;
    mMiles = miles;
    mPrice = price;
  }

  public boolean worthBuying(int maxPrice){
    return (mPrice < maxPrice);
  }

  public int getPrice() {
    return mPrice;
  }

  public int getYear() {
    return mYear;
  }

  public String getBrand() {
    return mBrand;
  }

  public String getModel() {
    return mModel;
  }

  public int getMiles() {
    return mMiles;
  }

}