Module 8, Part 1: Programming Interfaces and Encapsulation#

This Module presents new ways to make our Java code better structured and more reusable. We explore the general notion of programming interface, i.e. the way different parts of a program can interact with each other.

Then, we explore how we can shape our programming interfaces by restricting the access to (some) class fields and methods.

Since the size and complexity of our Java programs is about to increase, we also have a quick look at how to use Visual Studio Code with the Java Extension Pack: this will be especially useful in part 2 of this Module.

Recommendation: Using VS Code With the Java Extension Pack#

From this point of the course, you are encouraged to install a Visual Studio Code extension that makes Java programming more efficient: the Extension Pack for Java.

After you install the extension, you can launch VS Code on a whole directory containing one or more Java files. To do that, you have two ways:

  • open a terminal in the directory containing your files and execute:

    code .
    

    (where the dot . means “the current directory”); or

  • launch VS Code (e.g. by clicking on its icon), then click on “File” ➜ “Open Folder…” and select the folder containing your Java files.

Now, VS Code (and the Java extension pack) will be running a Java compiler behind the scenes, and it will let the compiler analyse your Java code as you write it. Therefore, VS Code will help you by:

  • Reporting errors and warnings as you write Java code. For instance:

    • if you write var foo = "Hello" * 2;, the expression "Hello" * 2 will be immediately emphasised in red, and if you hover the mouse pointer over it, you will see a pop-up explaining why the expression is wrong;

    • if you fix the expression and write e.g. var foo = 3 * 2;, you should see the variable foo emphasised in yellow, and if you hover the mouse pointer over it, you will see a pop-up explaining that the variable foo is never actually used in your program.

  • Reporting type information about your code: when you hover the mouse pointer over a variable or method, you will see a pop-up showing its type.

  • Providing method autocompletion, i.e. suggesting what method(s) you might call, depending on the type of an expression. For example, if you write var x = "a". you will see that, as soon as you type the dot . after the expression "a", VS Code shows the list of methods that you can call on "a" — which are all methods provided by the class String.

Important

Even if you use the helpful features of the Java Extension Pack, you can (and should!) still execute java and javac from the terminal to compile and execute your code. This way, you may see more detailed error messages about your code. Moreover, using javac and java from the terminal can be handy if VS Code or the Java extension report strange errors, e.g. due to some misconfiguration.

Note

There is another extension designed to help Java programming in Visual Studio code, called the Java Platform Extension for Visual Studio Code. The recommendation for this course is to install the Extension Pack for Java instead (as suggested above), because it has better support for small Java projects.

The General Notion of “Programming Interface”#

When we talk about programming (in any programming language), a programming interface is the specification of how a software component can be accessed and used by the rest of a program, or by other programs.

In Java, each class and object has a programming interface — which is the set of fields that can be read or written, and methods that can be called.

For instance, consider again the class ShopItem from Example 40:

 1class ShopItem {
 2    String name;
 3    double netPrice;
 4    double vat;
 5    int availability;
 6
 7    ShopItem(String name, double netPrice, double vat, int avail) {
 8        this.name = name;
 9        this.netPrice = netPrice;
10        this.vat = vat;
11        this.availability = avail;
12    }
13
14    String description() {
15        var str = this.name + " (price: " + this.netPrice + " DKK + "
16                    + (this.vat * 100) + "% VAT; "
17                    + "availability: " + this.availability + ")";
18        return str;
19    }
20
21    void inflateNetPrice(double percentage) {
22        this.netPrice = this.netPrice + (this.netPrice * (percentage / 100.0));
23
24        // This "return" is optional, since this method returns nothing (void)
25        return;
26    }
27}

The fields and methods of the class ShopItem constitute its programming interface, i.e. how a program can access the data stored in the objects of the class ShopItem, and the code provided by the class ShopItem. More specifically, the programming interface of ShopItem includes:

  • the constructor, which allows us to create new objects of the class ShopItem by writing e.g.:

    var item = new ShopItem("Plant pot", 61.5, 0.25, 23);
    
  • the fields, which allow us to read and write the values of the fields stored in any object of the class ShopItem, e.g.:

    item.name = "Cup";   // Change the value of the field 'name' of object 'item'
    System.out.println("VAT: " + item.vat);  // Read field 'vat' of object 'item'
    
  • the methods, which allow us to perform operations on objects and/or compute results based on the object’s field values, e.g.:

    item.inflateNetPrice(20);
    System.out.println("Item description: " + item.toString());
    

Every time we write a class in Java, we are also specifying a programming interface, which determines how that class and its objects can be used.

Encapsulation: public vs. private Fields and Methods#

When we write a class, we may define a series of fields and methods that should be for “internal use only” — i.e. we need those fields/methods to write other methods of the class, but we may not want those fields/methods to be used in other parts of our program (or by other programs). In other words, we may want to restrict the programming interface of our class by restricting the access to some of its fields or methods: this concept is called encapsulation.

More specifically, when writing classes we may want to distinguish between two main levels of access, using two different modifiers:

  • public fields and methods that can be accessed by any other part of a program, and

  • private fields and methods that can only be accessed from inside the class that defines them.

To illustrate the concept and the motivation, let us examine a scenario: designing and implementing a Java class that represents a bank account. We discuss:

Finally, we briefly discuss what happens when we do not specify whether a field or method is public or private.

The Bank Account Scenario#

Suppose you are asked to design and develop a class called BankAccount representing a bank account, with the following requirements:

  • Each object of the class BankAccount must store the bank account number, owner, and balance;

  • The class BankAccount must provide a constructor to create an empty bank account with a given number for a given owner;

  • The class BankAccount must provide methods for performing the following operations on its objects:

    • deposit, for adding a given amount to the balance of this account;

    • withdraw, for removing a given amount from the balance of this account. This method must return the actual amount withdrawn from the account, ensuring that the balance does not become negative: e.g., withdrawing the amount 1000 from a bank account that only has balance 800 must set the balance to 0, and return 800;

    • transfer, which has the effect of withdrawing a given amount from this account, and deposit it to a destination account. This method must return the actual amount being transferred, with the same constraints explained for withdraw above.

  • Each object of the class BankAccount must also remember how many operations (deposit, withdraw, transfer) have been performed in total since its creation.

  • Finally, the class BankAccount must provide a method returning a description (as a String) including the account number, owner, balance, and total number of operations.

The goal is to use BankAccount objects to write programs that may look like the following:

 1class BankAccountTest {
 2    public static void main(String args[]) {
 3        var acc1 = new BankAccount(1234, "Abigail Abildgaard");
 4        var acc2 = new BankAccount(4321, "Marcus Marcussen");
 5        var accounts = new BankAccount[]{ acc1, acc2 };
 6
 7        System.out.println("New bank accounts:");
 8        printAccounts(accounts);
 9
10        System.out.println("Depositing 5000 DKK on the 1st account...");
11        acc1.deposit(5000);
12        printAccounts(accounts);
13
14        System.out.println("Withdrawing 300.55 DKK from the 1st account...");
15        var withdrawn = acc1.withdraw(300.55);
16        System.out.println("* Amount withdrawn: " + withdrawn + " DKK");
17        printAccounts(accounts);
18
19        System.out.println("Transferring 7500 DKK from 1st to 2nd account...");
20        var transferred = acc1.transfer(acc2, 7500);
21        System.out.println("* Amount transferred: " + transferred + " DKK");
22        printAccounts(accounts);
23    }
24
25    // Utility method to print the descriptions of the given bank accounts
26    static void printAccounts(BankAccount[] accounts) {
27        for (var acc: accounts) {
28            System.out.println("    - " + acc.description());
29        }
30        System.out.println(""); // Leave an empty line for spacing
31    }
32}

The Class BankAccount Without Encapsulation#

Here is a possible implementation of the class BankAccount that satisfies all the requirements listed above. Observe that it uses a field called operations to count the total number of operations performed on a bank account object, and the value of that field is incremented whenever the methods deposit or withdraw are called (and therefore, indirectly, also the method transfer increments the operations count).

 1class BankAccount {
 2    int number;
 3    String owner;
 4    double balance;
 5    int operations;
 6
 7    BankAccount(int number, String owner) {
 8        this.owner = owner;
 9        this.number = number;
10        this.balance = 0.0;
11        this.operations = 0;
12    }
13
14    void deposit(double amount) {
15        this.balance = this.balance + amount;
16        this.operations++;
17    }
18
19    double withdraw(double amount) {
20        if (amount > this.balance) {
21            // Ensure the amount being withdrawn does not exceed the balance
22            amount = this.balance;
23        }
24        this.balance = this.balance - amount;
25        this.operations++;
26
27        return amount;
28    }
29
30    double transfer(BankAccount destination, double amount) {
31        var transferredAmount = this.withdraw(amount);
32        destination.deposit(transferredAmount);
33
34        return transferredAmount;
35    }
36
37    String description() {
38        return "Account " + this.number + " owned by " + this.owner + ": "
39                + this.balance + " DKK (" + this.operations + " operations)";
40    }
41}

Note

You can try this version of the class BankAccount and the test program above by copy&pasting their code in separate files, called BankAccount.java and BankAccountTest.java, saved in the same directory. Then, you can compile the resulting multi-file Java program (using javac) and then run it (using java), as explained in Splitting Code into Separate Files.

This implementation of the class BankAccount satisfies the requirements, but it has a potential design issue: after a bank account object is created, the code that can access the object can directly manipulate its fields, without using the methods deposit, withdraw, or transfer. This is possible because the programming interface of the class BankAccount includes the possibility of reading and writing the values in the object fields — but in this scenario, this might be undesirable.

For example, the main static method of the test program above might be changed to “transfer” money between the bank accounts acc1 and acc2 as follows:

acc1.balance = acc1.balance - 25000;
acc2.balance = acc2.balance + 25000;

Java will happily compile and execute this code — but notice that, as a consequence:

  • the balance of acc1 becomes negative, and

  • the “transfer” is not counted in the total number of operations performed on acc1 nor acc2.

This is arguably undesirable, and not compatible with the requirements listed above. The possibility of writing this undesirable code increases the risk of introducing bugs in the programs that use our class BankAccount (especially when such programs become larger and many people work on them). It would be better to prevent this situation from happening.

The Class BankAccount With Encapsulation#

To avoid the issues illustrated above, we can explicitly add modifiers to each field, constructor, and method in a class definition, to state which parts of a Java program can access them.

Here is a possible revision the class BankAccount above, which restricts the programming interface offered by the class with the following changes (highlighted):

  • The field owner, the constructor, and all the methods now have the modifier public. This means that:

    • any part of a Java program can create BankAccount objects using its constructor;

    • after a BankAccount object is created, its owner field can be read and changed by any part of a Java program;

    • moreover, any part of a Java programs can call the methods deposit, withdraw, transfer, and description.

  • The fields number, balance, and operations now have the modifier private. This means that those fields can only be read and written by Java code that is written inside the class BankAccount.

 1class BankAccount {
 2    public String owner;
 3    private int number;
 4    private double balance;
 5    private int operations;
 6
 7    public BankAccount(int number, String owner) {
 8        this.owner = owner;
 9        this.number = number;
10        this.balance = 0.0;
11        this.operations = 0;
12    }
13
14    public void deposit(double amount) {
15        this.balance = this.balance + amount;
16        this.operations++;
17    }
18
19    public double withdraw(double amount) {
20        if (amount > this.balance) {
21            // Ensure the amount being withdrawn does not exceed the balance
22            amount = this.balance;
23        }
24        this.balance = this.balance - amount;
25        this.operations++;
26
27        return amount;
28    }
29
30    public double transfer(BankAccount destination, double amount) {
31        var transferredAmount = this.withdraw(amount);
32        destination.deposit(transferredAmount);
33
34        return transferredAmount;
35    }
36
37    public String description() {
38        return "Account " + this.number + " owned by " + this.owner + ": "
39                + this.balance + " DKK (" + this.operations + " operations)";
40    }
41}

After these changes, the main static method of the test program above will still compile and run correctly (try it!). However, if we try writing again the “bad” bank transfer:

acc1.balance = acc1.balance - 25000;
acc2.balance = acc2.balance + 25000;

then this code would now violate the programming interface of the class BankAccount, and we would get compilation errors like the following:

BankAccountTest.java:... error: balance has private access in BankAccount
        acc1.balance = acc1.balance - 25000;

Therefore, a programmer that uses the class BankAccount will be only allowed to access and use the public parts of the class definition, and will not be allowed to write the “bad” bank transfer above.

Note

In this example, we have declared the field owner in the class BankAccount as public. There was no strong reason to do it: if you change modifier of the owner field to private, the code and the test program above will still work (try it!).

A common rule of thumb for designing programming interfaces in Java is that fields should not be made public, while methods can be made public (or not) depending on their intended use.

What If a Constructor, Field, or Method Is Neither public nor private?#

Until now, we have not used the public nor private modifiers in our classes — except for the main static method, which must be declared public in order to be executable by Java.

So, a natural question is: if a field, method, or constructor is neither public nor private, what kind of access does it have? Which parts of a Java program can use it?

The answer is that Java has a default access level called “package-private,” which is used if no other modifier is specified. For instance, all fields, methods, and constructors defined above in the class BankAccount without encapsulation are “package-private.”

The access level “package-private” is “in between” public and private — but for the kind of Java programs we have written thus far, it is pretty much indistinguishable from public. In order to truly appreciate the difference between public and “package-private,” we would need to discuss some elements of Java that we will address only later in the course.

Therefore, we will adopt this approach:

  • We will always specify whether a class field, or constructor, or method, is public or private. This is good programming practice, and familiarising with it will be very useful.

  • If a class field, or constructor, or method has no public nor private modifier, you should consider which one of the two would be more suitable. The code will often work anyway without modifiers (because “no modifier” means “package-private”, which is not far from public); however, it is usually better to use private as much as possible, in order to minimise the programming interface exposed by a class to other Java code that uses it.

Concluding Remarks#

You should now have an understanding of two main topics:

At this stage, you should also start using Visual Studio Code with the Java Extension Pack.

References and Further Readings#

You are invited to read the following section of the reference book: (Note: it might mention Java features that we will address later in the course)

  • Section 4.3 - “Encapsulation”

Lab and Weekly Assessments#

During the lab you can begin your work on the weekly assessments below.

Important

01 - Cars 3#

Note

This is a variation of the assessment 04 - Cars 2, and you might use its solution as a starting point. Observe that here you are asked to implement public constructors and methods, and keep all class fields private.

Edit the file Car.java provided in the handout, and implement a class called Car representing a car. The requirements are the following.

  • When defining the class Car you can can declare all the fields you like, but they must be all private.

  • The class Car must have a public constructor that takes the car’s brand, model, number plate, and colour, and can be called e.g. as:

    var c = new Car("FIAT", "Topolino", "EZ 13623", "blue");
    
  • Car objects must have the following methods:

    public String getBrand()
    
    public String getModel()
    
    public String getNumberPlate()
    
    public String getColor()
    

    When called on a Car object, these methods must return, respectively, the brand, model, number plate, and colour used to construct the object.

  • Car objects must have the following method:

    public String description()
    

    When called, the method returns a string with the car information. For instance, calling c.description() (using c from the previous point) must return a string of the following form:

    FIAT Topolino, blue [EZ 13623]
    
  • Car objects must have the following method:

    public boolean equals(Car other)
    

    When called, the method returns true if the brand, model, numberPlate, and color fields of this object are equal to the corresponding fields of the other object; otherwise, the method returns false.

  • Car objects must have the following method:

    public boolean isAlike(Car other)
    

    When called, the method returns true if the brand, model, and color fields of this object are equal to the corresponding fields of the other object; otherwise, the method returns false.

The handout includes some Java files called ClassTestUtils.java and Test01.java, Test02.java, etc.: they are utility files and test programs that use the class Car, and they might not compile or work correctly until you complete the implementation of class Car in the file Car.java. You should read those test programs and try to run them, but you must not modify them.

When you are done, submit the modified file Car.java on DTU Autolab.

02 - Video Game Monsters#

You are helping in the development of a video game where the player faces some monsters. Each monster has a name and a maximum of 100 health points; the player can either hit or burn a monster, and the monster can heal and recover health points.

Your task is to edit the file Monster.java and define a class named Monster. The requirements are the following.

  • When defining the class Monster you can can declare all the fields you like, but they must be all private.

  • The class Monster must have a public constructor that takes the monster’s name (as a String), and creates a corresponding monster object with the maximum health points (100).

  • Monster objects must have the following method:

    public String getDescription()
    

    which returns a summary with the monster’s name, and current health points. For example, a monster called “Horrorface” having 10 health points should return the string: "Horrorface (health: 10)"

  • Monster objects must have the following method:

    public int getHealth()
    

    which returns the current health points of the monster. The health points must be between 0 and 100.

  • Monster objects must have the following method:

    public boolean isDead()
    

    which returns true if the monster has 0 health points, and false otherwise.

  • Monster objects must have the following method:

    public void heal(int points)
    

    which adds the given number of points to the monster’s health, up to the monster’s maximum health points (100).

  • Monster objects must have the following method:

    public void hit(int damage)
    

    which reduces the monster’s health points by the given damage.

  • Monster objects must have the following method:

    public void burn(int damage)
    

    which reduces the monster’s health points by the given damage.

The handout includes some Java files called ClassTestUtils.java and Test01.java, etc.: they are utility files and test programs that use the class Monster, and they might not compile or work correctly until you complete the implementation of class Monster in the file Monster.java. You should read those test programs and try to run them, but you must not modify them. You should also run ./grade and read its reports to see the expected output of the test programs.

When you are done, submit the modified file Monster.java on DTU Autolab.

03 - Vending Machines#

Edit the file VendingMachine.java provided in the handout, and implement a class named VendingMachine that represents a vending machine. The vending machine has a location, and holds a number of coffee and chocolate servings, and an amount of cash. It can serve beverages according to the following list:

  • coffee: costs 10, and consumes 1 serving of coffee;

  • chocolate: costs 20, and consumes 1 serving of chocolate;

  • Wiener melange: costs 30, and consumes 1 serving of coffee and 1 serving of chocolate.

The requirements for the class VendingMachine are the following.

  • When defining the class VendingMachine you can can declare all the fields you like, but they must be all private.

  • The class VendingMachine must have a public constructor that takes the machines’s location (as a String) and the number of coffee and chocolate servings it holds (as integers). A newly-created vending machine must hold no cash. For instance, the constructor should allow us to create a vending machine object with 10 servings of coffee and 12 servings of chocolate, as follows:

    var m = new VendingMachine("DTU Building 324", 10, 12);
    
  • VendingMachine objects must have the following method:

    public String description()
    

    which returns a summary of the machine status. For instance, calling m.description() on the vending machine m created above must return:

    Machine @ DTU building 324 (coffee: 10; chocolate: 12; cash: 0)
    
  • VendingMachine objects must have the following method:

    public void putCash(int amount)
    

    which increases the cash held in a vending machine object by the given amount.

  • VendingMachine objects must have the following method:

    public boolean serveCoffee()
    
    public boolean serveChocolate()
    
    public boolean serveWienerMelange()
    

    These methods must return true if the vending machine contains enough cash and coffee/chocolate servings for the requested beverage, according to the list above; in this case, the method must also correspondingly reduce the amount of coffee/chocolate and cash held by the vending machine. Otherwise (i.e., if the vending machine does not have enough coffee/chocolate or cash), the methods must just return false without changing the machine status.

  • VendingMachine objects must have the following method:

    public int retrieveCash()
    

    which returns the amount of cash currently held by the machine. After this method is called, the machine must contain zero cash.

The handout includes some Java files called TestUtils.java, ClassTestUtils.java and Test01.java, Test02.java, etc.: they are utility files and test programs that use the class Monster, and they might not compile or work correctly until you complete the implementation of class VendingMachine in the file VendingMachine.java. You should read those test programs and try to run them, but you must not modify them. You should also run ./grade and read its reports to see the expected output of the test programs.

When you are done, submit the modified file VendingMachine.java on DTU Autolab.