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”); orlaunch 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" * 2will 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 variablefooemphasised in yellow, and if you hover the mouse pointer over it, you will see a pop-up explaining that the variablefoois 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 classString.
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
ShopItemby 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:
publicfields and methods that can be accessed by any other part of a program, andprivatefields 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:
how to implement the bank account class without encapsulation, and what are the consequences; and
how to implement the bank account class with encapsulation.
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
BankAccountmust store the bank account number, owner, and balance;The class
BankAccountmust provide a constructor to create an empty bank account with a given number for a given owner;The class
BankAccountmust provide methods for performing the following operations on its objects:deposit, for adding a given amount to the balance ofthisaccount;withdraw, for removing a given amount from the balance ofthisaccount. 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 fromthisaccount, and deposit it to a destination account. This method must return the actual amount being transferred, with the same constraints explained forwithdrawabove.
Each object of the class
BankAccountmust also remember how many operations (deposit, withdraw, transfer) have been performed in total since its creation.Finally, the class
BankAccountmust provide a method returning a description (as aString) 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
acc1becomes negative, andthe “transfer” is not counted in the total number of operations performed on
acc1noracc2.
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 modifierpublic. This means that:any part of a Java program can create
BankAccountobjects using its constructor;after a
BankAccountobject is created, itsownerfield 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, anddescription.
The fields
number,balance, andoperationsnow have the modifierprivate. This means that those fields can only be read and written by Java code that is written inside the classBankAccount.
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
publicorprivate. This is good programming practice, and familiarising with it will be very useful.If a class field, or constructor, or method has no
publicnorprivatemodifier, 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 frompublic); however, it is usually better to useprivateas 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:
The general notion of programming interface;
Restricting (encapsulating) the fields and methods of a class by making them
privateorpublic. This has the effect of shaping and restricting the programming interface offered by the class.
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
For each assessment, you can download the corresponding handout and submit your solution on DTU Autolab: https://autolab.compute.dtu.dk/courses/02312-E24.
For details on how to use Autolab and the assessment handouts, and how to submit your solutions, please read these instructions.
If you have troubles, you can get help from the teacher and TAs.
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
Caryou can can declare all the fields you like, but they must be allprivate.The class
Carmust have apublicconstructor 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");
Carobjects must have the following methods:public String getBrand()
public String getModel()
public String getNumberPlate()
public String getColor()
When called on a
Carobject, these methods must return, respectively, the brand, model, number plate, and colour used to construct the object.Carobjects must have the following method:public String description()
When called, the method returns a string with the car information. For instance, calling
c.description()(usingcfrom the previous point) must return a string of the following form:FIAT Topolino, blue [EZ 13623]
Carobjects must have the following method:public boolean equals(Car other)
When called, the method returns
trueif thebrand,model,numberPlate, andcolorfields ofthisobject are equal to the corresponding fields of theotherobject; otherwise, the method returnsfalse.Carobjects must have the following method:public boolean isAlike(Car other)
When called, the method returns
trueif thebrand,model, andcolorfields ofthisobject are equal to the corresponding fields of theotherobject; otherwise, the method returnsfalse.
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
Monsteryou can can declare all the fields you like, but they must be allprivate.The class
Monstermust have apublicconstructor that takes the monster’s name (as aString), and creates a corresponding monster object with the maximum health points (100).Monsterobjects 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)"Monsterobjects 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.
Monsterobjects must have the following method:public boolean isDead()
which returns
trueif the monster has 0 health points, andfalseotherwise.Monsterobjects must have the following method:public void heal(int points)
which adds the given number of
pointsto the monster’s health, up to the monster’s maximum health points (100).Monsterobjects must have the following method:public void hit(int damage)
which reduces the monster’s health points by the given
damage.Monsterobjects 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
VendingMachineyou can can declare all the fields you like, but they must be allprivate.The class
VendingMachinemust have apublicconstructor that takes the machines’s location (as aString) and the number of coffee and chocolate servings it holds (asintegers). 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);
VendingMachineobjects must have the following method:public String description()
which returns a summary of the machine status. For instance, calling
m.description()on the vending machinemcreated above must return:Machine @ DTU building 324 (coffee: 10; chocolate: 12; cash: 0)
VendingMachineobjects must have the following method:public void putCash(int amount)
which increases the cash held in a vending machine object by the given
amount.VendingMachineobjects must have the following method:public boolean serveCoffee()
public boolean serveChocolate()
public boolean serveWienerMelange()
These methods must return
trueif 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 returnfalsewithout changing the machine status.VendingMachineobjects 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.