Module 4, Part 2: Structured Programming#
In this second part of Module 4 we address how to structure your code for better readability. More specifically, we study how to split the code into static methods, which are a form of “subroutine” in Java. Then, we build upon this concept to see how to further split our code into separate classes, and then into separate files.
Structured Programming and Code Reuse#
At this point, we have seen various elements of the Java programming language, which allow us to organise and run the statements of our programs in 3 ways:
executing statements in sequence (by writing them one after the other);
executing statements conditionally, depending the result of a boolean expression (If-Then-Else Statement, If-Then Statement, Switch Statement);
executing statements repeatedly, depending on the result of a boolean expression (While Statement, Do-While Statement, For Statement).
These constructs give us the basic tools of structured programming and are sufficient for writing any possible algorithm that a computer can execute. (There is even a famous theorem that proves this fact.) However, if we only use these constructs to write our Java programs, may end up writing a lot of duplicated code! This is illustrated in Example 30 below.
(Code duplication)
Consider again Example 27, but now suppose that we need to write a program that lets the user provide two positive values \(n_1\) and \(n_2\), and then computes and displays the sum of all numbers from 1 to \(n_1\), and then from 1 to \(n_2\).
With the elements of Java we have seen thus far, we would end up writing a program like the following:
1class TwoSums {
2 public static void main(String[] args) {
3 var s = new java.util.Scanner(System.in);
4 s.useLocale(java.util.Locale.ENGLISH);
5
6 System.out.println("Please write two positive numbers:");
7 var n1 = s.nextInt();
8 var n2 = s.nextInt();
9
10 var sum1 = 0; // Will be updated during the computation of the sum
11 for (var i = 1; i <= n1; i = i + 1) {
12 sum1 = sum1 + i;
13 }
14
15 var sum2 = 0; // Will be updated during the computation of the sum
16 for (var i = 1; i <= n2; i = i + 1) {
17 sum2 = sum2 + i;
18 }
19
20 System.out.println("Sum of all numbers from 1 to " + n1 + ": " + sum1);
21 System.out.println("Sum of all numbers from 1 to " + n2 + ": " + sum2);
22
23 s.close();
24 }
25}
Observe that the code of the for
loop that computes the sum is repeated twice.
If we had to compute more than two sums, or more complex results, the amount of
duplicated code would increase, and this would become more and more
inconvenient:
duplicated code makes programs longer and harder to understand;
duplicated code makes programs harder to maintain: if a piece of duplicated code contains a bug, then the bug is also duplicated, and must be fixed twice!
What we need to reduce code duplication is generally called a subroutine (or function), i.e. a sub-program that performs a useful task; we also need some way to call a subroutine to make it perform its task. We now discuss three ways to structure Java code for improving code reuse, based on the concept of subroutine:
Splitting Code into Static Methods#
Java allows us to define a form of “subroutine” called “static method” — which is a block of code with an assigned name that can be called (possibly providing some values as arguments). When a static method is called, it executes the Java statements written in its body, and finally produces and returns a result to the caller.
The definition of a static method looks as follows: (try to copy&paste it on the Java shell)
1static int max(int a, int b) {
2 var result = (a > b) ? a : b;
3 return result;
4}
Line 1 above (before the open curly bracket “{
”) is the signature of the
static method, which tells us that:
we are defining a static method whose name is
max
the static method takes two arguments, a.k.a. formal parameters:
one has type
int
, and its name isa
the other has type
int
, and its name isb
the static method returns a result of type
int
(as specified after the keywordstatic
)
The curly brackets {
…}
(lines 1–4) surround the method body, which is
executed when the static method is called.
When we write code that needs to call the static method above, we simply use its name, providing the required arguments (between parentheses, and separated by commas). For instance, on the Java shell we can try:
jshell> var maximum = max(3, 21 * 2)
maximum ==> 42
| created variable maximum : int
When Java executes the static method call max(3, 21 * 2)
above, it proceeds as
follows:
Java evaluates all the call arguments, proceeding from left to right. In this example, the argument
3
is already a value, while21 * 2
evaluates to42
. Therefore, the static method call becomesmax(3, 42)
;Then, the two values 3 and 42 become the actual parameters of the method call, i.e. they are passed to the body of
max
providing a value to its formal parametersa
andb
;Now, the body of the static method
max
is executed:as mentioned in the previous item, the formal parameters
a
andb
get the values passed with the call, i.e., 3 and 42, respectively. Intuitively, this is akin to placing the variable declarationsvar a = 3
andvar b = 42
just before the first line of the method body;on line 2, the method body declares a local variable
result
, computing the maximum between the values assigned toa
andb
by the call (i.e. 3 and 42) using a conditional expression. Therefore, the local variableresult
takes the value 42;on line 3, the method body returns the value of the variable
result
(which is 42) to the caller.
The value returned by the method call (i.e., 42) is finally used to declare the
variable maximum
in the Java shell (as shown by the shell output).
We can define static methods as we please — and they are especially useful for tasks that are performed multiple times in our program, as shown in Example 31 below.
(Avoiding code duplication with static methods)
We can remove the code duplication in Example 30 by defining a static method that computes the summation, and calling it when needed.
For example, we could define a static method named sum
that:
takes one argument of type
int
, namedn
;returns a value of type
int
;in its body, computes the sum of all
int
values between 1 andn
, and returns the result.
With this adaptation, the Java program in Example 30 could
be rewritten as follows, removing the code duplication. (Notice that the static
method sum
is defined on lines 19–25, and is called on lines 10 and 11)
1class TwoSums {
2 public static void main(String[] args) {
3 var s = new java.util.Scanner(System.in);
4 s.useLocale(java.util.Locale.ENGLISH);
5
6 System.out.println("Please write two positive numbers:");
7 var n1 = s.nextInt();
8 var n2 = s.nextInt();
9
10 var sum1 = sum(n1);
11 var sum2 = sum(n2);
12
13 System.out.println("Sum of all numbers from 1 to " + n1 + ": " + sum1);
14 System.out.println("Sum of all numbers from 1 to " + n2 + ": " + sum2);
15
16 s.close();
17 }
18
19 static int sum(int n) {
20 var summation = 0; // Will be updated during the computation of the sum
21 for (var i = 1; i <= n; i = i + 1) {
22 summation = summation + i;
23 }
24 return summation;
25 }
26}
Splitting Code into Separate Classes#
When designing or developing our Java programs, we can use classes to better organise our static methods, as shown in Example 32 below.
(Organising the code in separate classes)
Continuing Example 31, we may want to move our
static method sum
into a different class. The idea is to improve code
readability by distinguishing the class TwoSums
(which contains the main
method of the program) from the class Utils
(where we want to put
miscellaneous utility methods, like sum
and possibly others).
The result of this reorganisation is shown below. Notice that:
there is now a class named
Utils
that contains the definition ofsum
(lines 20–28), andto call the static method
sum
, we now need to specify the name of the class where the static method is defined, i.e. we need to writeUtils.sum(...)
(lines 10 and 11).
1class TwoSums {
2 public static void main(String[] args) {
3 var s = new java.util.Scanner(System.in);
4 s.useLocale(java.util.Locale.ENGLISH);
5
6 System.out.println("Please write two positive numbers:");
7 var n1 = s.nextInt();
8 var n2 = s.nextInt();
9
10 var sum1 = Utils.sum(n1);
11 var sum2 = Utils.sum(n2);
12
13 System.out.println("Sum of all numbers from 1 to " + n1 + ": " + sum1);
14 System.out.println("Sum of all numbers from 1 to " + n2 + ": " + sum2);
15
16 s.close();
17 }
18}
19
20class Utils {
21 static int sum(int n) {
22 var summation = 0; // Will be updated during the computation of the sum
23 for (var i = 1; i <= n; i = i + 1) {
24 summation = summation + i;
25 }
26 return summation;
27 }
28}
Splitting Code into Separate Files#
As a further step to better organise our code, we may decide to split some of
our classes into separate files, with extension .java
. This may make our
programs easier to read and maintain, especially as they grow in size.
However, splitting classes into multiple files also introduces a drawback:
before running a program, we will need to explicitly compile all its .java
files, by using the Java compiler javac
. When we compile a .java
file, we obtain one or more class files — i.e. files with extension
.class
, each one containing an executable representation
(called Java bytecode) of a specific class in the program.
Note
You can think of Java bytecode as something similar to the
machine instructions executed by a computer’s CPU
— e.g., as in BlinkenBits.
However, the Java bytecode is not executed directly by the CPU: instead, it is
interpreted by the java
program, which in turn implements the
Java Virtual Machine (JVM).
We will reprise this topic in more detail
later in the course.
In a nutshell, the procedure for writing and running a Java program now becomes:
Write one or more Java files (with extension
.java
), each one defining one or more classes — with at least one class containing a static method calledmain
(which is the starting point when our program is executed);Compile all the Java files into class files, using the Java compiler
javac
. If your Java files do not contain syntax errors nor type errors, thenjavac
will generate one class file (with extension.class
) for each class defined in your Java files;Execute the
main
method of the program, by runningjava
followed by the name of the class that contains the static methodmain
.
Example 33 and Fig. 17 below illustrate how a Java program can be organised in multiple files, compiled, and executed.
(Organising the code in separate files)
Continuing Example 32, we may want to move
the class Utils
containing our static method sum
in a different file.
The idea is to make our code easier to navigate — especially when it grows in
size (although this example is quite small).
Therefore, we now crate two separate files:
a file called
TwoSums.java
, containing only the classTwoSums
(downloadTwoSums.java
)another file called
Utils.java
, containing only the classUtils
(downloadUtils.java
)
After we place both files in the same directory, we will need to compile them,
before we can run the program TwoSums
. To compile the files, we use the
Java compiler javac
from the console (in the directory where the files
TwoSums.java
and Utils.java
are located):
javac TwoSums.java Utils.java
(If all goes well, you will not see any error or warning message, and you will be back to the console prompt.)
If you have a look at the directory contents now, you will notice that there are
two new files, called TwoSums.class
and Utils.class
: they have been
created by javac
, and contain the executable Java bytecode of the two classes
TwoSums
and Utils
.
We can now run the TwoSums
program that has been compiled and saved in the
file TwoSums.class
. To run the program, we invoke java
by specifying
the name of the class that contains the main
static method.
java TwoSums
The overall prodedure is illustrated in Fig. 17 below. You
will see that the TwoSums
program runs just as it did in
Example 32 above.
Important
Notice that to execute the main
method of the class TwoSums
compiled in the
file TwoSums.class
, we run java
with the class name TwoSums
only — i.e.
we do not write TwoSums.java
nor TwoSums.class
!
Tip
To compile all the .java
files contained in the current directory, without
listing their names one by one, you can execute:
javac *.java
Note
When writing our first Java programs up to this point, we have considered
situations where a Java program fits in single file (called e.g.
Program.java
), and we have been able to run the program directly by using
java
(by executing e.g. java Program.java
on a terminal).
For these single-file programs, java
is automatically following the workflow
illustrated in Fig. 17: under the hood, it is invoking javac
,
producing .class
files, and then running the main
method of the program in
the Java Virtual Machine (JVM); the class files, however, are not saved on disk.
You can try this workflow yourself. Go back to the single-file version of our
program TwoSums.java
(in Example 32),
and execute on the terminal:
javac TwoSums.java
This will create two new bytecode class file called TwoSums.class
and
Utils.java
(just like the ones we compiled in
Example 33). You can then execute the
main
method of the class TwoSums
by running:
java TwoSums
Concluding Remarks#
You should now have an understanding of three main topics:
How to define and call static methods, to make your code better structured and easier to reuse;
How to organise your static methods into separate classes;
How to split your classes into separate files, how to compile your
.java
files into.class
files (using the Java compilerjavac
), and how to execute them.
References and Further Readings#
The notions introduced in this module should allow you to better understand what
was happening, e.g., when you used Math.sqrt(...)
and Math.abs(...)
for solving
06 - Line-Point Distance: you were calling some static methods
defined in a class named Math
, which is part of the Java API. If you are
curious, you can find the documentation of the Math
class and its static methods
here.
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.
05 - Maximum 2#
This is a variation of the assessment 03 - Print the Maximum, but with the program split into separate files.
Edit the file Utils.java
and implement the body of the static method with the
following signature (in the class Utils
):
static int max(int x, int y, int z)
The method takes three arguments x
, y
, and z
(all of type int
). When
called, the method must return the maximum value between those given as arguments.
For instance, if the method is called as Utils.max(1, 42, -3)
, then it must
return 42. The method is used in the
test program contained in the file Maximum.java
, which is also included in the
handout: you should read Maximum.java
and try to run it, but you must not
modify it.
When you are done, submit the modified file Utils.java
on DTU Autolab.
Important
Besides testing your solution by running ./grade
, make sure you are also able
to manually compile this multi-file Java program (using javac
) and then run it
(using java
), as explained above.
Warning
The automatic grading on DTU Autolab includes some additional secret checks that test your submission with additional inputs. After you submit, double-check your grading result on DTU Autolab: if the secret checks fail, then your solution is not correct, and you should fix it and resubmit.
06 - Square 2#
This is a variation of the assessment 09 - Square, but with the program split into separate files.
Edit the file Utils.java
and implement the body of the static method with the
following signature (in the class Utils
):
static String createSquare(int size)
The method takes as argument the size of a square, and must return a String
representing a square of that size, made by using the character #
(like the
square shown in 09 - Square). The method is used in the
test program contained in the file Square.java
, which is also included in the
handout: you should read Square.java
and try to run it, but you must not
modify it.
When you are done, submit the modified file Utils.java
on DTU Autolab.
Important
Besides testing your solution by running ./grade
, make sure you are also able
to manually compile this multi-file Java program (using javac
) and then run it
(using java
), as explained above.
Hint
The string that represents the square might include multiple lines of characters.
To generate the required “newline” that separate one line from the next, you
should call System.lineSeparator()
: it is
a static method provided by the Java API which returns a string containing the
special character(s) that, when printed, are visualised as a newline (you can
read the documentation
here).
For example, you can try this on the Java shell: (note: the content of
the variable twoLines
will be slightly different on Windows)
jshell> var twoLines = "Line 1" + System.lineSeparator() + "Line 2"
twoLines ==> "Line 1\nLine 2"
| created variable twoLines : String
jshell> System.out.println(twoLines)
Line 1
Line 2
Warning
The automatic grading on DTU Autolab includes some additional secret checks that test your submission with additional inputs. After you submit, double-check your grading result on DTU Autolab: if the secret checks fail, then your solution is not correct, and you should fix it and resubmit.
07 - Days in a Month 2#
This is a variation of the assessment 04 - Days in a Month, but with the program split into separate files.
Edit the file Utils.java
and implement the following two static methods
(in the class Utils
).
A static method called
monthNumber
that:takes one argument (of type
String
) representing the name of a month;returns an
int
eger value corresponding to the number of the month — or 0, if the month name is invalid.
The month name can be written using any combination of uppercase and lowercase letters. For instance:
the static method call
Utils.monthNumber("jAnUaRy")
must return 1;the static method call
Utils.monthNumber("aPRil")
must return 4;the static method call
Utils.monthNumber("foobar")
must return 0.
A static method called
daysInMonth
that:takes two arguments (of type
int
) representing a month number and a year, respectively;returns an
int
eger value corresponding to the number of days in the given month in the given year, taking into account leap years. If the month number is invalid, the method must return 0.
For instance:
the static method call
Utils.daysInMonth(1, 2024)
must return 31;the static method call
Utils.daysInMonth(2, 1900)
must return 28;the static method call
Utils.daysInMonth(42, 1733)
must return 0.
These two methods are used in the test program contained in the file
MonthDays.java
, which is also included in the handout: you should read
MonthDays.java
and try to run it, but you must not modify it.
When you are done, submit the modified file Utils.java
on DTU Autolab.
Important
You will need to write the complete definition of the two static methods — i.e., both their signatures and their bodies. Feel free to give any name you like to the formal parameters of the methods.
Besides testing your solution by running
./grade
, make sure you are also able to manually compile this multi-file Java program (usingjavac
) and then run it (usingjava
), as explained above.
Warning
The automatic grading on DTU Autolab includes some additional secret checks that test your submission with additional inputs. After you submit, double-check your grading result on DTU Autolab: if the secret checks fail, then your solution is not correct, and you should fix it and resubmit.