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:

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.

Example 30 (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 is a

    • the other has type int, and its name is b

  • the static method returns a result of type int (as specified after the keyword static)

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, while 21 * 2 evaluates to 42. Therefore, the static method call becomes max(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 parameters a and b;

  • Now, the body of the static method max is executed:

    • as mentioned in the previous item, the formal parameters a and b get the values passed with the call, i.e., 3 and 42, respectively. Intuitively, this is akin to placing the variable declarations var a = 3 and var 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 to a and b by the call (i.e. 3 and 42) using a conditional expression. Therefore, the local variable result 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.

Example 31 (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, named n;

  • returns a value of type int;

  • in its body, computes the sum of all int values between 1 and n, 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.

Example 32 (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 of sum (lines 20–28), and

  • to 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 write Utils.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:

  1. 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 called main (which is the starting point when our program is executed);

  2. 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, then javac will generate one class file (with extension .class) for each class defined in your Java files;

  3. Execute the main method of the program, by running java followed by the name of the class that contains the static method main.

Example 33 and Fig. 17 below illustrate how a Java program can be organised in multiple files, compiled, and executed.

Example 33 (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:

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
Java compilation and execution workflow

Fig. 17 A diagram of the Java compilation and execution workflow, based on the scenario illustrated in Example 33. The Java source code files (TwoSums.java and Utils.java) are compiled using javac, producing one Java bytecode file for each class (TwoSums.class and Utils.class); the class files are then executed using java, and the running program can then e.g. interact with the console to read keyboard inputs or print outputs on screen.#

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 compiler javac), 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

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).

  1. A static method called monthNumber that:

    • takes one argument (of type String) representing the name of a month;

    • returns an integer 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.

  2. A static method called daysInMonth that:

    • takes two arguments (of type int) representing a month number and a year, respectively;

    • returns an integer 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 (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.