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" * 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 variablefoo
emphasised in yellow, and if you hover the mouse pointer over it, you will see a pop-up explaining that the variablefoo
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 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
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, andprivate
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:
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
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 ofthis
account;withdraw
, for removing a given amount from the balance ofthis
account. This method must return the actual amount withdrawn from the account, ensuring that the balance does not become negative: e.g.,withdraw
ing 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 fromthis
account, and deposit it to a destination account. This method must return the actual amount being transferred, with the same constraints explained forwithdraw
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 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
acc1
becomes negative, andthe “transfer” is not counted in the total number of operations performed on
acc1
noracc2
.
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
BankAccount
objects using its constructor;after a
BankAccount
object is created, itsowner
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
, anddescription
.
The fields
number
,balance
, andoperations
now 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
public
orprivate
. This is good programming practice, and familiarising with it will be very useful.If a class field, or constructor, or method has no
public
norprivate
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 frompublic
); however, it is usually better to useprivate
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:
The general notion of programming interface;
Restricting (encapsulating) the fields and methods of a class by making them
private
orpublic
. 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
Car
you can can declare all the fields you like, but they must be allprivate
.The class
Car
must have apublic
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()
(usingc
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 thebrand
,model
,numberPlate
, andcolor
fields ofthis
object are equal to the corresponding fields of theother
object; otherwise, the method returnsfalse
.Car
objects must have the following method:public boolean isAlike(Car other)
When called, the method returns
true
if thebrand
,model
, andcolor
fields ofthis
object are equal to the corresponding fields of theother
object; 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
Monster
you can can declare all the fields you like, but they must be allprivate
.The class
Monster
must have apublic
constructor that takes the monster’s name (as aString
), 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, andfalse
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 allprivate
.The class
VendingMachine
must have apublic
constructor that takes the machines’s location (as aString
) and the number of coffee and chocolate servings it holds (asint
egers). 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 machinem
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 returnfalse
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.