Lesson Tuesday

Let's take a brief break from testing to discuss objects and best practices for accessing their properties. Until now, 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.price;) as we did in JavaScript isn't actually best practice for Java. We did this for the first few days of the course in order to focus on Java fundamentals without added complexity.

Now that we're more comfortable with Java, let's learn the proper way to handle a custom class' properties. In this lesson we will discuss best practices for access level modifiers, and the concepts of encapsulation and visibility. We'll revisit our Car Dealership for this purpose.

Access Level Modifiers

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

src/main/java/Vehicle.java
public class Vehicle {
  public int year;
  public String brand;
  public String model;
  public int miles;
  public int price;

 ...

}

As discussed previously, public in front of properties 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 or scalable. This means any method anywhere could simply change the property of an object, that might affect some other code in a large codebase, causing problems and crashes! Imagine being able to simply set bankAccount.balance="100000000000000" instead of having a properly checked-and-balanced, test-backed method to accurately test and set balances?

Fields (aka properties) 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.

Not being able to directly set or retrieve properties may seem awkward at first, especially since we've gotten used to this in Intro. But, as with many things in Java, it makes our code more stable and maintainable.

Private Access Level Modifiers

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

src/main/java/models/Vehicle.java
public class Vehicle {
   private int year;
   private String brand;
   private String model;
   private int miles;
   private int price;

   public Vehicle(int year, String brand, String model, int miles, int price) {
       this.year = year;
       this.brand = brand;
       this.model = model;
       this.miles = miles;
       this.price = price;
   }

   public boolean worthBuying(int maxPrice){
       return (price < 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...

/Users/epicodus-student/Desktop/car-dealership/src/main/java/App.java
Error:(37, 61) java: year has private access in models.Vehicle
Error:(38, 61) java: brand has private access in models.Vehicle
Error:(39, 61) java: model has private access in models.Vehicle
Error:(40, 61) java: miles has private access in models.Vehicle
Error:(41, 61) java: price has private access in models.Vehicle
Error:(51, 65) java: year has private access in models.Vehicle
Error:(52, 65) java: brand has private access in models.Vehicle
Error:(53, 65) java: model has private access in models.Vehicle
Error:(54, 65) java: miles has private access in models.Vehicle
Error:(55, 65) java: price has private access in models.Vehicle
Error:(74, 51) java: year has private access in models.Vehicle
Error:(75, 51) java: brand has private access in models.Vehicle
Error:(76, 51) java: model has private access in models.Vehicle
Error:(77, 51) java: miles has private access in models.Vehicle
Error:(78, 51) java: price has private access in models.Vehicle

As you can see, each of the errors this message summarizes is the same: Each Vehicle property (price, miles, model, brand, and year) has private access inVehicle`. All of the 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 price property:

src/main/java/Vehicle.java
class Vehicle {

  private int year;
  private String brand;
  private String model;
  private int miles;
  private int price;

  public Vehicle(int year, String brand, String model, int miles, int price) {
       this.year = year;
       this.brand = brand;
       this.model = model;
       this.miles = miles;
       this.price = price;
  }

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

  public int getPrice() {
    return price;
  }

}
  • Here, we define a getter method called getPrice(). The naming convention for a getter method is getNameOfProperty(). Never give getters different names - this can cause issues with Java apps we build later on in this course.

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

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

Important Note: We should always write a JUnit test for each getter method before coding its 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 price property. It can therefore provide this property to App.java without error.

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

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():

Error:(37, 61) java: year has private access in models.Vehicle
Error:(38, 61) java: brand has private access in models.Vehicle
Error:(39, 61) java: model has private access in models.Vehicle
Error:(40, 61) java: miles has private access in models.Vehicle
Error:(51, 65) java: year has private access in models.Vehicle
Error:(52, 65) java: brand has private access in models.Vehicle
Error:(53, 65) java: model has private access in models.Vehicle
Error:(54, 65) java: miles has private access in models.Vehicle
Error:(74, 51) java: year has private access in models.Vehicle
Error:(75, 51) java: brand has private access in models.Vehicle
Error:(76, 51) java: model has private access in models.Vehicle
Error:(77, 51) java: miles has private access in models.Vehicle

Let's create similar getter methods for each remaining Vehicle property. We can even use a shortcut in IntelliJ to help us out:

In our Vehicle class, right-click within the class definition, right below getPrice() and select Generate > Getter _. Select all the remaining private fields that require getter methods, and click _OK. We should see IntelliJ generate all the getters for our code!

Our Vehicle.java should now look like this:

src/main/java/Vehicle.java
package models;

public class Vehicle {
   private int year;
   private String brand;
   private String model;
   private int miles;
   private int price;

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

   public Vehicle(int year, String brand, String model, int miles, int price) {
       this.year = year;
       this.brand = brand;
       this.model = model;
       this.miles = miles;
       this.price = price;
   }

   public int getPrice() {
       return price;
   }

   public int getYear() {
       return year;
   }

   public String getBrand() {
       return brand;
   }

   public String getModel() {
       return model;
   }

   public int getMiles() {
       return miles;
   }
}

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

src/main/java/App.java
import models.Vehicle;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;

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

       boolean programRunning = true;

       Vehicle hatchback = new Vehicle(1994, "Subaru", "Legacy", 170000, 4000);
       Vehicle suv = new Vehicle(2002, "Ford", "Explorer", 100000, 7000);
       Vehicle sedan = new Vehicle(2015, "Toyota", "Camry", 50000, 30000);
       Vehicle truck = new Vehicle(1999, "Ford", "Ranger", 100000, 4000);
       Vehicle crossover = new Vehicle(1998, "Toyota", "Rav-4", 200000, 3500);

       ArrayList<Vehicle> allVehicles = new ArrayList<Vehicle>();

       allVehicles.add(hatchback);
       allVehicles.add(suv);
       allVehicles.add(sedan);
       allVehicles.add(truck);
       allVehicles.add(crossover);
       while (programRunning) {
           BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
           System.out.println("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");

           try {

               String navigationChoice = bufferedReader.readLine();

               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")) {
                   System.out.println("What is your maximum budget for a vehicle?");
                   String stringUserMaxBudget = bufferedReader.readLine();
                   int userMaxBudget = Integer.parseInt(stringUserMaxBudget);
                   System.out.println("Alright, here's what we have in your price range:");
                   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("Alright, let's add a vehicle! What getYear() was this vehicle made?");
                   int userVehicleYear = Integer.parseInt(bufferedReader.readLine());
                   System.out.println("Great! What make or getBrand() is the vehicle?");
                   String userVehicleBrand = bufferedReader.readLine();
                   System.out.println("Got it! What getModel is it?");
                   String userVehicleModel = bufferedReader.readLine();
                   System.out.println("And how many getMiles() does it have on it?");
                   int userVehicleMiles = Integer.parseInt(bufferedReader.readLine());
                   System.out.println("Finally, what's its price?");
                   int userVehiclePrice = Integer.parseInt(bufferedReader.readLine());
                   Vehicle userVehicle = new Vehicle(userVehicleYear, userVehicleBrand, userVehicleModel, userVehicleMiles, userVehiclePrice);
                   allVehicles.add(userVehicle);

                   System.out.println("Alright, here's your new vehicle:");
                   System.out.println("----------------------");
                   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")){
                   System.out.println("Goodbye!");
                   programRunning = false;

               }
               else {
                   System.out.println("I'm sorry, we don't recognize your input");
               }
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }
}

We should now be able to successfully compile and run our code with no errors:

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 price 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 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 best practice in many programming languages 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 bugs.
  • We can add logic and formatting to our getters to more easily display content.

Compare the following two snippets of theoretical code. Both code construct an address to display out of different fields, but one uses a custom getter method, and one does not:

String concatenatedShippingAddress =  "Your order was sent to: " + customer.housenumber + " " + customer.street + " " + customer.city + ", " + customer.state + " " + customer.zipcode + ". Thank you for your purchase";
String theConcatHappensInTheGetter = "Your order was sent to: " + customer.getAddress() + " . Thank you for your purchase";

Which one is easier to maintain and keep error free?

Encapsulation results in far easier to maintain code. Imagine we eventually needed to change the datatype of an object's public property. If outside classes were directly accessing this property (ie: testVehicle.price;, instead of testVehicle.getPrice();), we would need to update all code in all outside classes that reference 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 affecting 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.

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.


Example GitHub Repo for Car Dealership Code

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.