Module 12, Part 2: Managing Java Projects: Packages, JAR Files, Build Tools#

In this second part of Module 12 we explore some features and tools of the Java programming language and ecosystem that are particularly useful when working on larger projects, where we may need to organise many classes and/or reuse classes from other projects. These features and tools are:

Important

  • These new topics are not part of the final exam of this course. They are addressed because they clarify some aspects that may pop up during daily usage of Java, even for small programs. Moreover, these topics usually become very important as soon as one tries to work with existing Java programs created by experienced developers.

  • The weekly assessments for this module are besed on a past exam paper, and are based on topics we have addressed in previous modules: you should work on them during the lab and submit them on DTU Autolab as usual, since they are provided to train your programming skills!

Java Packages#

Until now we have written Java programs that consist of a small number of .java files in a single directory. Keeping a few files in a directory works well for programs of limited size (which are the focus of this course) — but the approach has two drawbacks:

  • if a program grows functionality and in size, the number of its .java files may grow as well, and they may become chaotic and difficult to manage;

  • it may be hard to reuse classes written for one program in other programs. For example, we may have a program (maybe written by someone else) that defines a class called Utils with various handy static methods, and we may want to reuse that class in another program — but if that other program also happens to define a different class called Utils, then we would need to rename one of the two classes and update the code accordingly.

To address this issue, Java allows us to organise class definitions by grouping them in packages. We have already encountered packages: e.g. when discussing how to create and use a Scanner object we have briefly seen that when we write:

var s = new java.util.Scanner(System.in);

we are creating a new object of the class Scanner contained in the Java package java.util, which is part of the Java API. Technically, java.util.Scanner is the fully-qualified name of the class (i.e. the package name followed by the class name).

To organise our code in packages, we have to consider three main aspects:

  1. We must declare at the beginning of each .java file the name of the package that “owns” the file, by writing e.g.:

    package my.cool.code
    

    Package names are hierarchical, and the different levels of the hierarchy are separated by dots: e.g. if a file called Utils.java begins with the line “package my.cool.code” above, then its code is organised with:

    • a main package called my, which contains

      • a sub-package called cool, which contains

        • a sub-package called code, which contains

          • any class defined in the file Utils.java.

  2. We must save the files in a hierarchy of directories that matches the package hierarchy. For example, if a file called Utils.java begins with the line package my.cool.code, then the file must be saved inside nested subdirectories called my, cool, and code:

    my                        (directory)
    └── cool                  (directory)
        └── code              (directory)
            └── Utils.java    (Java file)
    
  3. Finally, we need to consider how the classes defined in our .java file can be accessed:

    • A class definition like class Utils { ... } (i.e. a class definition that is not explicitly declared public) means that the class Utils is package-private, i.e. it can only be accessed by code contained in the same package my.cool.code. Similarly, fields and methods defined without public nor private modifiers are package-private, hence they can be only accessed by code contained in the same package.

    • Instead, a class definition like public class Utils { ... } can be accessed from everywhere (including code belonging to other packages). Similarly, public fields and methods can be accessed from any package.

      Important

      A .java file can define at most one public class, and the name of the public class must coincide with the name of the file. For instance, a public class Utils { ... } must be defined in a file called Utils.java.

The reason why Java enforces the restriction 2. and 3. above is that they can help working with large code bases: if a programmer is looking for the definition of a class a.b.c.ClassName, then the programmer can find the class definition inside a directory hierarchy that matches the package a.b.c — and moreover, if the class ClassName is public, then the programmer knows that its code can be found in a file called ClassName.java.

This is illustrated in Example 73 below.

Example 73 (A simple hierarchy of packages)

Suppose we need to write a program that manages the DTU Pet Clinic, using pet classes similar to Example 63, with the following requirements:

  • the class Pet and the derived classes Dog and Cat must be public and must belong to a package called dk.dtu.pets;

  • the main method of the program must belong to a public class called Main in the package dk.dtu.petclinic.

To obtain this, we must:

  • save the public classes in separate .java files with a name that matches the class name;

  • write the desired package name at the beginning of each file .java file, and

  • place each .java file under a subdirectory that matches its package name.

As a result, the files and directories are organised as follows:

dk                       (directory)
└── dtu                  (directory)
    ├── pets             (directory)
    │   ├── Pet.java     (Java source code file)
    │   ├── Dog.java     (Java source code file)
    │   └── Cat.java     (Java source code file)
    │
    └── petclinic        (directory)
        └── Main.java    (Java source code file)

You can download these source files here: dtu-pet-clinic.zip

To explore the source code, you can download and unzip the file above, and open the resulting folder dtu-pet-clinic with Visual Studio Code (e.g. by launching Visual Studio Code and clicking on File -> Open Folder…).

Note

In the ZIP file above, the file Main.java in the package dk.dtu.petclinic needs to use the classes Pet, Cat, and Dog, which belongs to a different package dk.dtu.pets. To do that, the code in Main.java refers to the fully-qualified class names, e.g. dk.dtu.pets.Pet. (Just like we have been using e.g. the fully-qualified name java.util.Scanner to use the class Scanner contained in the package java.util.) The file Main.java is shown below.

 1package dk.dtu.petclinic;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        System.out.println("Welcome to the DTU Pet Clinic!");
 6
 7        var pets = new dk.dtu.pets.Pet[] { new dk.dtu.pets.Dog("Dr Wolf"),
 8                                           new dk.dtu.pets.Dog("Mr Coyote"),
 9                                           new dk.dtu.pets.Cat("Prof Los"),
10                                           new dk.dtu.pets.Cat("Dr Puma")   };
11
12        System.out.println("Current list of pets:");
13        for (var p: pets) {
14            System.out.println("    - " + p.getDescription());
15        }
16    }
17}

To compile and run the program in the ZIP file above, you can open a terminal in the unzipped folder, and execute the following commands.

  1. Compile all Java files in all the various subdirectories, to generate a .class file for each .java file (as previously explained):

    javac dk/dtu/pets/*.java
    
    javac dk/dtu/petclinic/*.java
    

    After the compilation, the files and directory structure will look as follows (the new .class files are highlighted)

    dk                       (directory)
    └── dtu                  (directory)
        ├── pets             (directory)
        │   ├── Pet.java     (Java source code file)
        │   ├── Pet.class    (Java class file)
        │   ├── Dog.java     (Java source code file)
        │   ├── Dog.class    (Java class file)
        │   ├── Cat.java     (Java source code file)
        │   └── Cat.class    (Java class file)
        │
        └── petclinic        (directory)
            ├── Main.java    (Java source code file)
            └── Main.class   (Java class file)
    
  2. Run the program, by asking Java to execute the static method main of the class Main in the package dk.dtu.petclinic:

    java dk.dtu.petclinic.Main
    

Importing Classes from Other Packages#

When a class from another package is used often, it can be convenient to import the class instead of repeating its fully-qualified name over and over again. This is illustrated in Example 74 below.

Example 74 (Importing the Pet class)

In Example 73 we have seen that the file Main.java in the package dk.dtu.petclinic uses e.g. the fully-qualified class name dk.dtu.pets.Dog to refer to a class in another package. The file Main.java can instead import the class dk.dtu.pets.Dog: after being imported, the class can be simply referred as Dog. The same can be done for the classes Pet and Cat.

The modified file Main.java using import is shown below. (The differences with the file Main.java in Example 73 are highlighted)

 1package dk.dtu.petclinic;
 2
 3import dk.dtu.pets.Pet;
 4import dk.dtu.pets.Dog;
 5import dk.dtu.pets.Cat;
 6
 7public class Main {
 8    public static void main(String[] args) {
 9        System.out.println("Welcome to the DTU Pet Clinic!");
10
11        var pets = new Pet[] { new Dog("Dr Wolf"),
12                               new Dog("Mr Coyote"),
13                               new Cat("Prof Los"),
14                               new Cat("Dr Puma") };
15
16        System.out.println("Current list of pets:");
17        for (var p: pets) {
18            System.out.println("    - " + p.getDescription());
19        }
20    }
21}

Protected Fields (and Methods)#

Until now we have seen that, when defining a class, we can decide the visibility of a field or method by using the optional modifiers public or private:

  • a field/method defined with the private modifier is only accessible by code written in the same class;

  • a field/method defined without modifiers is package-private, hence it is only accessible by code within the same package;

  • a field/method defined with the public modifier is accessible by code written in any package.

However, there may be cases where none of these three options fits our needs:

  • we may want to protect a field/method from arbitrary “external” access (so we cannot make it public), and

  • we may want to access the field/method from the subclasses (so we cannot make it private), and

  • such subclasses might be defined in other packages (so we cannot make the field/method package-private).

In these cases, we can make the field/method protected, i.e. only accessible by:

  • code belonging to the same package, and

  • the owner class and its subclasses.

Note

The protected modifier is typically relevant for larger Java applications, and it is not crucial for this course. You can experiment with the protected modifier by trying Exercise 35 below.

Distributing Java Classes and Programs as JAR Files#

When we compile Java source code, the Java compiler javac generates .class files that can be used without the original source code, on any computer architecture and operating system supported by Java: for instance, we can compile a .java file into a .class file on Linux, and that .class file can be used e.g. on MacOS (with an Intel or AMD or Apple Silicon CPU) or Windows (with an Intel or AMD or ARM CPU). This is possible because .class files are loaded and interpreted by the java program, which in turn implements the Java Virtual Machine (JVM) and is available on many operating systems and CPU architectures.

In principle we could distribute our Java projects by distributing such .class files directly — but in practice, this can be cumbersome: each Java class is compiled into a separate .class file, so large Java projects may have many .class files.

For this reason, the Java specification defines a standard way to package and distribute classes and programs: the JAR (Java ARchive) file format — which is just a ZIP file that contains:

  • a “Manifest file” with information about the contents of the archive;

  • .class files organised in a directory structure that matches their packages (as previously explained);

  • other relevant files, if necessary.

This is illustrated in Example 75, Example 76, and Example 77 below.

Example 75 (Creating a JAR file)

Take the ZIP file provided in Example 73 (dtu-pet-clinic.zip): we now see how to generate a JAR file containing the compiled .class files (while in Example 76 below we will use that JAR file to develop a new program).

First, it is convenient to compile all .java files into .class files stored in a separate directory, called e.g. build. To do this, you can open a terminal in the folder dtu-pet-clinic extracted from the ZIP file above, and run the javac compiler with the option -d build:

javac -d build dk/dtu/pets/*.java
javac -d build dk/dtu/petclinic/*.java

After these commands, the contents of the directory dtu-pet-clinic should look as follows: (notice that the compiled .class files are now inside the directory build, and they are arranged in subdirectories that match the directory structure of the source files)

dtu-pet-clinic                   (directory extracted from the ZIP file)
├── dk                           (directory containing the Java source code)
│   └── dtu                      (directory)
│       ├── pets                 (directory)
│       │   ├── Pet.java         (Java source code file)
│       │   ├── Dog.java         (Java source code file)
│       │   └── Cat.java         (Java source code file)
│       │
│       └── petclinic            (directory)
│           └── Main.java        (Java source code file)
│
└── build
    └── dk                       (directory contaning compiled .class files)
        └── dtu                  (directory)
            ├── pets             (directory)
            │   ├── Pet.class    (Java class file)
            │   ├── Dog.class    (Java class file)
            │   └── Cat.class    (Java class file)
            │
            └── petclinic        (directory)
                └── Main.class   (Java class file)

We can now create a JAR file called e.g. PetClinic.jar based on the contents of the directory build, with the following command (which also specifies which class contains the main method):

jar --create --file PetClinic.jar --main-class dk.dtu.petclinic.Main -C build .

After executing this command, you should see a new file called PetClinic.jar containing all the .class files above: we discuss how to use it in Example 76 below.

Example 76 (Using a JAR file)

This is a follow-up to Example 75 above, and the starting point is the file PetClinic.jar created there.

The file PetClinic.jar can be easily distributed to reuse the Java classes it contains. For instance, we can run the DTU Pet Clinic program contained in PetClinic.jar by executing in a console (from within the same directory containing the file):

java -jar PetClinic.jar

We can also develop new Java code that uses the classes saved in the JAR file PetClinic.jar. For example, consider the code below, that defines a class Kangaroo extending dk.dtu.pets.Pet, with a simple main method for testing:

class Kangaroo extends dk.dtu.pets.Pet {
    public Kangaroo(String name) {
        super(name, "kangaroo");
    }

    public static void main(String args[]) {
        var k = new Kangaroo("Clara");

        System.out.println("Pet description: " + k.getDescription());
    }
}

If you save the code above in a file called e.g. Kangaroo.java, and you try to compile and run it by executing java Kangaroo.java, you will get an error like “package dk.dtu.pets does not exist”: this is because Java does not know where to find the package dk.dtu.pets and the classes it contains.

However, we can tell Java to look for such package and classes using the option -classpath (or -cp for short) with the file PetClinic.jar created above. If PetClinic.jar and Kangaroo.java are in the same directory, we can execute (from within that directory):

java -classpath PetClinic.jar Kangaroo.java

And now the program will compile and run, using the class dk.dtu.pets.Pet provided by the JAR file PetClinic.jar. The output will be:

Pet description: Clara (kangaroo)

Example 77 (Using JAR files provided by other Java projects)

This is a follow-up to Example 75 (starting from the file PetClinic.jar) and Example 76: we now discuss how we can improve the small program in this last example by using a collection of Java classes (i.e. a library) that is not part of the standard Java API.

Suppose we want to add some coloured output to the file Kangaroo.java developed in Example 76. The Java API does not provide any class nor method to produce coloured output on the console — but there is a popular Java library called Jansi that provides such facilities.

To use the Jansi library, we first need to download its JAR file: you can find the JAR file for Jansi version 2.4.0 on its download page. Please save the file jansi-2.4.0.jar in the directory that also contains the files PetClinic.jar and Kangaroo.java.

The, we can modify the file Kangaroo.java (from Example 76) as follows, using the Jansi library according to its API documentation.

import org.fusesource.jansi.AnsiConsole;
import org.fusesource.jansi.Ansi;

class Kangaroo extends dk.dtu.pets.Pet {
    public Kangaroo(String name) {
        super(name, "kangaroo");
    }

    public static void main(String args[]) {
        var k = new Kangaroo("Clara");

        AnsiConsole.systemInstall();
        var message = Ansi.ansi().fgGreen().a("Pet description: ")
                        .bgRed().fgBrightYellow().bold().a(k.getDescription())
                        .fgDefault().bgDefault();
        System.out.println(message);
        AnsiConsole.systemUninstall();
    }
}

We can now compile and run the revised file Kangaroo.java using both JAR files PetClinic.java and jansi-2.4.0.jar: (NOTE: this is done slightly differently on Windows, compared to macOS and Linux)

java -classpath "PetClinic.jar;jansi-2.4.0.jar" Kangaroo.java

and the program should produce coloured output on the console.

Note

  • The Java API is made available via a collection of JAR files containing many packages and classes (including the ones we have used thus far, like e.g. java.util.Scanner). Those JAR files are part of Java installation and they are automatically used when we compile or run Java programs, without need to load them with the -classpath command-line option (as we had to do for loading PetClinic.jar and jansi-2.4.0 in Example 76 and Example 77).

  • The Java library includes a package called java.lang whose classes are automatically imported. This is why we can write e.g. System.out.println("...") without importing the class System, or we can create and throw an object of the class ArithmeticException without importing the class ArithmeticException itself: both classes System and ArithmeticException are part of the package java.lang, hence they are automatically imported in our Java programs. Correspondingly, writing e.g. System.out.println("...") is equivalent to writing the more verbose java.lang.System.out.println("...").

Java Build Tools#

The steps described in the previous sections and examples (compiling multiple Java files with the correct options, creating JAR files, downloading JAR files when needed, remembering to specify -classpath…) become very repetitive and time-consuming when developing Java programs. For this reason, the Java ecosystem includes various build tools that automate these steps, and let programmers focus more on designing and writing their Java code.

These build tools can be installed and used in a terminal — and editors like Visual Studio Code provide additional facilities to use build tools through a more intuitive point-and-click interface.

These tools are outside the scope of this course — but if you are curious, here are two introductions to two very popular build tools for Java projects:

Concluding Remarks#

You should now have an understanding of the following topics:

Important

  • As mentioned in the opening, these topics are not part of the final exam of this course. They are presented here because they usually become very important as soon as one tries to work on other people’s code, or steps beyond the scope of introductory programs.

  • Most of the tasks described in this module can be automated using build tools (like Maven) and editors (like Visual Studio Code) and other advanced Java development environments (like Eclipse or IntelliJ Idea). Still, it it is important to have an intuition of what is happening “behind the scenes” to understand how to address possible issues. For instance, an error reporting that a package used in your code “does not exist” might mean that you need to download the relevant JAR file(s) — or you may need to configure Maven or Eclipse or IntelliJ Idea to do that for you.

References and Further Readings#

You are invited to read the following sections of the reference book:

  • Section 3.3 - “Packages”

  • Section 9.1 - “Crating Subclasses” - “The protected Modifier”

More details about Java packages are available in this tutorial:

Here are more details on how to compile multiple Java files in various directories:

Finally, this tutorial discusses how to create JAR files.

Exercises#

Note

These exercises are not part of the course assessment. They are provided to help you self-test your understanding.

Exercise 34 (Experimenting with Java packages)

To experiment with the package structure in Java, you may take one of the previous assessments and rearrange their classes into packages with the corresponding directory structure, as we did for Pet, Dog, and Cat in Example 73.

For example, you could take the solution to 04 - Video Game Monsters, Part 5 (either your own solution, or the one provided by the teacher) and adapt it by:

  • making all monster classes public (so they must be placed in their own file), and

  • grouping all monster classes in a package called dk.dtu.game.monsters, and

  • placing the class GameUtils in a package called dk.dtu.game.utils.

After you reorganise the classes in this way, you can try revising some of the tests in the handout of 04 - Video Game Monsters, Part 5, changing their code to make it compile and run using the new package names. For instance, you may need to import the class dk.dtu.game.monsters.Monster

Exercise 35 (Experimenting with protected fields)

To see the “protected” modifier in action, you can try the following experiment:

  1. take the ZIP file provided in Example 73 (dtu-pet-clinic.zip);

  2. modify the field name of the class dk.dtu.pets.Pet to be protected instead of private;

  3. define e.g. a class Porcupine that extends Pet, but belongs to the package dk.dtu.petclinic (so, Porcupine and Pet belong to different packages);

  4. modify the code of Porcupine and Main to observe that:

    • in the code written inside the class dk.dtu.petclinic.Porcupine, you can directly read and modify the value of the field this.name. However,

    • inside the static method dk.dtu.petclinic.Main.main, if p is an object of the class Pet (or one of its subclasses), then you cannot access the field p.name.

Lab and Weekly Assessments#

During the lab you can try the exercises above or begin your work on the weekly assessments.

Important

For this Module, you can find the weekly assessments in a sample exam paper:

  1. open the course page on DTU Learn (https://learn.inside.dtu.dk/d2l/home/215298);

  2. navigate to Contents → Sample Exam Papers;

  3. select the Sample Exam 01 PDF file.