1. Writing your own classes

Of course, we have already written various classes so far. But these classes so far had only static fields and methods, and we have not yet utilized our own classes to build instances. Objects themselves, of course, we have already built, from classes of the Java standard library. We now want to extend this knowledge. We would like to write classes and make instances of our own types.

This chapter is about electronic consumer devices, and most of the exercises build on each other. We will first build simple electrical devices such as radios and televisions; later we will realize abstractions and collect these electrical devices on a ship. And when Captain CiaoCiao and Bonny Brain go on vacation, everything must be turned off nicely. In this way, the topics of associations and inheritance can be practiced. As a reminder, an association is what we refer to as a "has" or "knows" relationship; an inheritance is an "is-a-kind-of" relationship.

Prerequisites

  • be able to create new classes

  • be able to set instance variables in classes

  • be able to implement methods

  • know visibilities private, package visible, and public

  • be able to use static methods and fields

  • being able to declare and use enumeration types

  • know how to implement simple and overloaded constructors

  • know types of associations

  • be able to implement 1:1 association

  • be able to use a List

  • be able to use delegation of operations

  • be able to implement inheritance relations with extends

  • be able to override methods

  • understand the two meanings of super

  • be able to use dynamic binding

  • be able to use and declare interfaces

  • use default methods in interfaces

Data types used in this chapter:

1.1. Class declaration and object properties

For a new type, we write a new class in Java. Let’s create a few classes in this section and give the classes instance variables and methods.

1.1.1. Declare radio with instance variables and a main program ⭐

The first type of our collection of electrical devices is a radio. Radio has a state that we want to store.

Task:

  • Create a new class Radio.

  • Provide the radio with the following instance variables:

    • isOn, is the radio on or off?

    • volume, how loud does the radio play music?

  • What types of variables are useful? Make sure that the instance variables are not static!

  • Write an additional class Application, which creates a Radio object in its main(…​) method. Assign and access the variables for testing.

Consider the naming conventions: Classes start with an uppercase letter and variables and methods with a lowercase letter; only constants are in uppercase.

1.1.2. Implementing methods of a radio ⭐

Methods are to be put into the new class radio so that the object "can" do something.

Task:

  • Add the following non-static methods:

    • void volumeUp()/void volumeDown(): change the instance variable volume by 1 and -1 respectively. The volume may only be in the range of 0 to 100.

    • void on()/ void off()/ boolean isOn(): access the instance variable isOn; it is okay if a method is named like an instance variable. The on()/off() methods are supposed to print messages like "on"/"off" on the screen.

    • public String toString(): It should return information about the internal state as a string, where the string should take a form like Radio[volume=2, is on].

  • In the main(…​) method of the Application class, the object methods of the radio can be tested like this, for example:

    excerpt from Application.java
    Radio grandmasOldRadio = new Radio();
    System.out.println( grandmasOldRadio.isOn() );     // false
    grandmasOldRadio.on();
    System.out.println( grandmasOldRadio.isOn() );     // true
    System.out.println( grandmasOldRadio.volume );     // 0
    grandmasOldRadio.volumeUp();
    grandmasOldRadio.volumeUp();
    grandmasOldRadio.volumeDown();
    grandmasOldRadio.volumeUp();
    System.out.println( grandmasOldRadio.volume );     // 2
    System.out.println( grandmasOldRadio.toString() ); // Radio[volume=2, is on]
    System.out.println( grandmasOldRadio );            // Radio[volume=2, is on]
    grandmasOldRadio.off();

1.1.3. Private Parts: Make instance variables private ⭐

The private details of implementation must not be public so that you can change the insides at any time.

Task:

  • Make all instance variables from Radio private.

  • Decide if the methods can be made public.

  • Are there any internal methods that should be private?

1.1.4. Create setters and getters ⭐

In the Java world, getters and setters are commonly utilized to define properties. These properties can be automatically accessed by many frameworks through their corresponding getter and setter methods.

Task:

  1. Add a new private double instance variable frequency to the Radio so that you can set the frequency.

  2. Modify the toString() method so that it takes the frequency into account.

  3. Writing these setters and getters is often tedious, so either generate them automatically using an IDE or use tools to put them in the bytecode automatically. Generate setters and getters for frequency using the IDE.

  4. If you want to implement read-only operations and prevent properties from being modified externally, use getters without setters. If a variable is final, only getters will work. Generate only one getter for the state volume.

Setters and getters are an important naming convention. If a property XXX is of type boolean, the prefix is generally isXXX(), not getXXX(). So, our existing method isOn() is also a getter.

1.2. Static variables methods

Class variables and static methods often lead to confusion for novice programmers. Yet, it is simple: either state is stored within individual objects or within the class object itself. If we have different objects with instance variables, the object methods can access the individual properties. A static method can only access class variables without explicitly specifying an object.

1.2.1. Convert station names to frequencies ⭐

So far, the radio has only instance variables and instance methods. Let’s add a static method that has no link to a concrete radio object.

Task:

  • Implement a static method double stationNameToFrequency(String) in the class Radio that maps a station as a string to a frequency (for example, the well-known pirate radio station "Walking the Plank" has the frequency 98.3).

  • If null is passed to the method, then the result shall be 0.0. Furthermore, for unknown station names, the return should be 0.0.

Example:

  • In the main program, we can write:

    System.out.println( Radio.stationNameToFrequency( "Walking the Plank" ) ); // 98.3

String comparisons with stations can be implemented with switch-case or with equals(…​).

1.2.2. Write log output with a tracer class ⭐

You use loggers to log program output and be able to trace it later—much like Captain CiaoCiao records in his logbook everything that happened on the seas, in the ports, and within the crew.

Tracer

Task:

  1. Create a new class Tracer.

  2. Add a static method void trace(String) that prints a passed string on the screen.

  3. Extend the program with two static methods on() and off(), which remember in an internal state whether trace(String) leads to output or not. At the beginning, the tracer shall be switched off.

  4. Optional: Add a method trace(String format, Object... args) that internally calls System.out.printf(format, args) when tracing is turned on.

Example:

We can then use the class like this:

Tracer.on();
Tracer.trace( "Start" );
int i = 2;
Tracer.off();
Tracer.trace( "i = " + i );
//  Tracer.trace( "i = %d", i );
Tracer.on();
Tracer.trace( "End" );

The expected output is:

Start
End
Tracer static UML
Figure 1. UML diagram with static properties

1.3. Simple enumerations

Enumerations are closed sets built in Java using the keyword enum.

1.3.1. Give radio an AM-FM modulation ⭐

Modulation is vital in radio transmissions; there is AM (amplitude modulation) and FM (frequency modulation)[1].

Task:

  • Declare a new enumeration type Modulation with the values AM and FM in its own file.

  • Add a private instance variable Modulation modulation to Radio where the radio keeps the modulation.

  • Set the Modulation via a new Radio method void setModulation(Modulation modulation), a getter can also exist.

  • Customize the toString() method in Radio.

1.3.2. Set valid start and end frequency for modulation ⭐

For broadcasting, three frequency ranges (called frequency bands) are classified, which encode over AM:

  • Longwave: 148.5 kHz to 283.5 kHz

  • Mediumwave: 526.5 kHz to 1606.5 kHz

  • Shortwave: shortwave broadcasting uses several bands between 3.2 MHz and 26.1 MHz.

Coded over FM:

  • Ultra-shortwave (FM): 87.5 MHz to 108 MHz.

Task:

  • Add two new private instance variables:

    • minFrequency

    • maxFrequency

  • When calling setModulation(Modulation), the instance variables minFrequency and maxFrequency are to be set to their minimum and maximum value ranges, namely for AM 148.5 kHz to 26.1 MHz and for FM 87.5 MHz to 108 MHz.

1.4. Constructors

Constructors are special initialization routines that are automatically called by the virtual machine when an object is created. We often use constructors to assign states when creating objects, which we then store in the object.

1.4.1. Writing radio constructors ⭐

So far, our radio has had only a default constructor generated by the compiler. Let’s replace it with our own constructors:

Task:

  • Write a constructor for the class Radio so that you can initialize a radio with a frequency (double). But you should still be able to create radios with the parameterless constructor!

  • Alternatively, a Radio object should be able to be initialized with a station name (as String) (use stationNameToFrequency(..) internally for this). The station name is not stored, only the frequency.

  • How can we use the constructor delegation with this(…​)?

Example: We should be able to construct radios in the following way:

Radio r1 = new Radio();
Radio r2 = new Radio( 102. );
Radio r3 = new Radio( "BFBS" );

1.4.2. Implement copy constructor ⭐

If an object of the same type is assumed to be a template in the constructor of a class, we refer to this as a copy constructor.

Task:

  • Implement a copy constructor for Radio.

1.4.3. Realize factory methods ⭐

In addition to constructors, some classes provide an alternative variant to create objects, called factory methods. The following applies:

  • Constructors exist in principle, but they are private, and consequently, outsiders cannot create instances.

  • So that objects can be built, there are static methods, which internally call the constructor and return the instance.

Task:

  1. Create a new class TreasureChest for a treasure chest.

  2. A treasure chest can contain gold doubloons and gemstones; create two public final instance variables int goldDoubloonWeight and int gemstoneWeight. So, the object is immutable, the states cannot be changed later. Getters are not necessary.

  3. Write four static factory methods that return a TreasureChest object:

    • TreasureChest newInstance()

    • TreasureChest newInstanceWithGoldDoubloonWeight(int)

    • TreasureChest newInstanceWithGemstoneWeight(int)

    • TreasureChest newInstanceWithGoldDoubloonAndGemstoneWeight(int, int)

Where would be the problem here with a usual constructor?

1.5. Associations

An association is a dynamic connection between two or more objects. We can characterize association in several ways:

  • Does only one side know the other, or do both sides know each other?

  • Is the lifetime of an object tied to the lifetime of an object?

  • With how many other objects does an object have an association? Is there a connection to only one other object or several? For 1:n or n:m relationships, we need containers, like arrays or dynamic data structures like the java.util.ArrayList.

1.5.1. Connect monitor tube with TV ⭐

So far, we have had one electrical appliance: a radio. It’s time to add a second electrical device and includes a 1:1 association.

Task:

  • Create a new class TV.

  • The TV should get methods on()/off() which write short messages to the console (an instance variable is not necessary for the example for now).

  • Create a TV in the main(…​) method of Application.

  • Create a new class MonitorTube.

    • The MonitorTube shall also get on()/off() methods with console messages.

  • A TV shall reference a MonitorTube via a private instance variable. How can this be done in Java?

    • Implement a unidirectional relationship between the TV and the monitor tube. About the life cycle: when the TV is built, the monitor tube should also be created, you don’t need to be able to replace the monitor tube.

  • When the TV is switched on/off, the monitor tube should also be switched on/off.

  • Optional: How can we implement a bidirectional relationship? There might be a problem?

In the end, this relation should be implemented:

TV has MonitorTube UML
Figure 2. UML diagram of the directed association.

1.5.2. Add radios with a 1:n association to the ship ⭐⭐

Captain CiaoCiao owns a whole fleet of ships, and they can take cargo. In the beginning, Captain CiaoCiao only wants to load radios onto his ship.

Add radio to ship

Task:

  1. Create a new class Ship (without main(…​) method).

  2. To enable Ship to hold radios, we use the data structure java.util.ArrayList. As private instance variable, we include in Ship:

    ArrayList<Radio> radios = new ArrayList<Radio>();
  3. Write a new load(…​) method in Ship to allow the ship to hold a radio.

  4. Build two ships in the main(…​) method of Application.

  5. Assign multiple radios to a ship in the main(…​) method.

  6. Write a Ship method int countDevicesSwitchedOn() that returns how many radios are switched on. Attention: It is not about the total number of radios on the ship, but about the number of radios switched on!

  7. Optional: Give the ship also a toString() method.

  8. What do we need to do if the ship also wants to load other electrical devices, such as ice machines or televisions?

Implementation goal: A ship references radios.

Ship has Radio UML
Figure 3. UML diagram of the 1:n association.

1.6. Inheritance

Inheritance models an is-a-type-of relationship and ties two types together very directly. The modeling is critical to form groups of related things.

1.6.1. Introduce abstraction into electrical devices via inheritance ⭐

Until now, radios and televisions have been disconnected. But there is one thing they have in common: they are all electronic consumption devices.

Task:

  1. Create a new class ElectronicDevice for electronic devices.

  2. Derive the class Radio from the class ElectronicDevice—leave TV out of it for now.

  3. Pull into the superclass the common features of the potential electrical devices.

  4. Write a new class IceMachine which is also an electrical device.

Nowadays, a development environment can automatically move properties into the superclass via refactoring; find out how.

Task Objective: implement the following inheritance relationship.

Radio IceMachine is a ElectronicDevice UML
Figure 4. UML diagram of the inheritance relationship

1.6.2. Determine number of switched on electrical devices ⭐

Inheritance can be used to declare a parameter with a supertype, which then addresses a whole group of types with it, namely all subtypes as well.

Task:

  • Declare a new static method in the class ElectronicDevice:

    public static int numberOfElectronicDevicesSwitchedOn( ElectronicDevice... devices ) {
      // Returns the number of switched on devices,
      // that were passed to the method
    }

Example:

  • If, for example, r1 and r2 are two radios that are turned on, and ice is an ice machine that is turned off, main(…​) may say, for example:

    int switchedOn =
      ElectronicDevice.numberOfElectronicDevicesSwitchedOn( r1, ice, r2 );
    System.out.println( switchedOn ); // 2

1.6.3. Ship should hold any electronic device ⭐

So far, the ship can only store the type Radio. Now, general electrical devices should be stored.

Task:

  1. Change the type of the dynamic data structure from Radio to ElectronicDevice:

    private ArrayList<ElectronicDevice> devices =
        new ArrayList<ElectronicDevice>();
  2. Furthermore, the method to add electrical devices has to change—why?

Task objective: ships store all types of electrical devices.

Ship has ElectronicDevice UML
Figure 5. UML diagram

1.6.4. Take working radios on the ship ⭐

When radios are taken onto the ship, a message should appear on the console. Moreover, Captain CiaoCiao dislikes taking broken radios onto the ship.

Task:

  • When a radio is passed to the load(…​) addition method, it should be checked if it has volume 0; in that case, it should not be included in the data structure.

  • When a radio is added, the console output should be: Remember to pay license fee!.

1.6.5. Solve equivalence test with pattern variable ⭐

Java 14 introduces "pattern matching for instanceof" which can shorten code nicely.

Task:

  • Given is an older class Toaster with an equals(…​) method for a test for equivalence:

    com/tutego/exercise/oop/Toaster.java
    public class Toaster {
      int capacity;
      boolean stainless;
      boolean extraLarge;
    
      @Override public boolean equals( Object o ) {
        if ( !(o instanceof Toaster) ) return false;
    
        Toaster toaster = (Toaster) o;
        return    capacity == toaster.capacity
               && stainless == toaster.stainless && extraLarge == toaster.extraLarge;
      }
    
      @Override public int hashCode() {
        return Objects.hash( capacity, stainless, extraLarge );
      }
    }
  • Rewrite the equals(…​) method to use instanceof with a pattern variable.

1.6.6. Fire alarm does not go off: Overriding methods ⭐

Fire is something Captain CiaoCiao doesn’t need on his ships at all. If there is a fire, it must be extinguished as soon as possible.

Task:

  • Implement a class Firebox for a fire detector as a subclass of ElectronicDevice.

  • Fire detectors should always be switched on after creation.

  • The off() method should be implemented with an empty body or with console output so that a fire detector cannot be turned off.

Example:

  • Task target: an overridden off() method that does not change the isOn state. This can be tested like this:

    Firebox fb = new Firebox();
    System.out.println( fb.isOn() );  // true
    fb.off();
    System.out.println( fb.isOn() );  // true
Firebox is ElectronicDevice
Figure 6. UML diagram

1.6.7. toString() override ⭐

Give ElectronicDevice and Radio their own toString() method.

1.6.8. Calling the methods of the superclass ⭐⭐

A radio has on()/off() methods, and the TV class also already has on()/off() methods. However, the TV is not yet an ElectronicDevice. The reason is that the TV needs special treatment because of the monitor tube.

If TV also extends the class ElectronicDevice, a TV, therefore, overwrites the methods of the superclass ElectronicDevice. But a problem arises:

  • If we omit the two methods, the tube would not be turned off, but the TV would pass as an electrical device when inherited.

  • If we leave the methods in the class, only the tube will be turned off, but the device is no longer switched on or off. After all, this state was managed by the superclass via the on()/off() methods.

Task:

  • Fix the problem that a TV is an ElectronicDevice, but the monitor tube is turned on/off.

1.7. Polymorphism and dynamic binding

A central feature of object-oriented programming languages is the dynamic resolution of method calls. This form of calls can be decided thereby not at compile-time, but at runtime, if the object type is known.

1.7.1. Holiday! Switch off all devices ⭐

Before Captain CiaoCiao lies down in the hammock with a Tropical Storm cocktail and enjoys his vacation, all electrical appliances on the ship must be turned off.

Holiday Switch off all devices

Task:

  • Implement a method holiday() in the ship class that turns off all the electrical devices in the list.

    public void holiday() {
      // call off() for all elements in the data structure
    }
  • The main(…​) method of Application may contain, for example:

    Radio bedroomRadio = new Radio();
    bedroomRadio.volumeUp();
    Radio cabooseRadio = new Radio();
    cabooseRadio.volumeUp();
    TV mainTv = new TV();
    Radio crRadio = new Radio();
    Firebox alarm = new Firebox();
    Ship ship = new Ship();
    ship.load( bedroomRadio );
    ship.load( cabooseRadio );
    ship.load( mainTv );
    ship.load( crRadio );
    ship.load( alarm );
    ship.holiday();

1.7.2. The Big Move (NEW) ⭐

The fearless Captain CiaoCiao has decided to abandon his old ship and move onto a fresh barge. Since not all of his fellow pirates are capable of reading, a graphical loading list should be created with little pictures representing the various items.

Task:

  • Copy the following class AsciiArt as a nested class into the class Ship:

    public static class AsciiArt {
      public static final String RADIO = " .-.\n|o.o|\n|:_:|";
      public static final String BIG_TV = """
          .---..--------------------------------------..---.
          |   ||.------------------------------------.||   |
          |.-.|||                                    |||.-.|
          | o |||                                    ||| o |
          |`-'|||                                    |||`-'|
          |.-.|||                                    |||.-.|
          | O |||                                    ||| O |
          |`-'||`------------------------------------'||`-'|
          `---'`--------------------------------------'`---'
                 _||_                            _||_
                /____\\                          /____\\""";
      public static final String TV = " \\  /\n _\\/_\n|    |\n|____|";
      public static final String SOCKET = """
                ____
           ____|    \\
          (____|     `._____
           ____|       _|___
          (____|     .'
               |____/""";
    }
  • Implement a new method printLoadingList() in Ship that implements the following rules:

    • If the device is a Radio and the wattage is positive, a radio will be printed to the screen by accessing AsciiArt.RADIO. Broken radios have 0 watts and must not be printed.

    • If the device is a TV and the wattage is above 10,000, the image of a big TV (AsciiArt.BIG_TV) will be printed.

    • If the device is a TV (regardless of the wattage), the image of a normal TV (AsciiArt.TV) is printed.

    • If none of the above cases apply, the image of a socket (AsciiArt.SOCKET) will be printed.

  • Solve the task with the language feature Pattern Matching for switch.

1.8. Abstract classes and abstract methods

Abstract classes are something strange at first sight: What to do with classes you can’t make objects of? And what about abstract methods? A class without implemented methods has nothing to offer, after all!

Both concepts are crucial. Supertype and subtype always have a contract; a subtype must have at least what a supertype specifies and must not break semantics. If a superclass is abstract or if methods are abstract, the subclasses from which objects can be built promise to provide that functionality.

1.8.1. TimerTask as an example for an abstract class ⭐⭐

Captain CiaoCiao records each robbery by video and analyzes the sequences in the debriefing. However, the finest 8K quality videos quickly fill the hard drive. He wants to determine in time whether he needs to buy a new hard drive.

TimerTask as an example for an abstract class

A java.util.Timer can perform tasks repeatedly. To achieve this, the Timer class has a schedule(…​) method for adding a task. The task is of type java.util.TimerTask.

Task:

  • Write a subclass of TimerTask that prints a message on the screen whenever the number of free bytes on the file system falls below a certain limit (e.g., less than 1000 MB).

    Returns the free bytes:

    long freeDiskSpace = java.io.File.listRoots()[0].getFreeSpace();
  • The Timer should execute this TimerTask every 2 seconds.

FreeDiskSpaceTimerTask UML
Figure 7. UML diagram of TimerTask and its own subclass.

Bonus: Integrate a message in the tray with:

import javax.swing.*;
import java.awt.*;
import java.awt.TrayIcon.MessageType;
import java.net.URL;
try {
  String url =
    "https://cdn4.iconfinder.com/data/icons/common-toolbar/36/Save-16.png";
  ImageIcon icon = new ImageIcon( new URL( url ) );
  TrayIcon trayIcon = new TrayIcon( icon.getImage() );
  SystemTray.getSystemTray().add( trayIcon );

  trayIcon.displayMessage( "Warning", "Hard drive full", MessageType.INFO );
}
catch ( Exception e ) { e.printStackTrace(); }

1. The two modulation methods, AM and FM, differ in how one uses a carrier frequency to encode a signal to be transmitted. Wikipedia provides an overview at https://en.wikipedia.org/wiki/Modulation