1. Records, Interfaces, Enumerations, Sealed Classes

1.1. Records

Records are useful in Java because they provide a simple and compact way to define and use data objects with a fixed number of attributes.

1.1.1. Quiz: Which statements are true for Records? ⭐

Given the following Java Record:

record Candy( String name, int calories ) { }

Which of the following statements is true?

  1. The name and age of a candy cannot be changed after the object is created.

  2. The attributes of a Candy object are accessed via getter methods.

  3. Candy cannot have static methods.

  4. The equals() and hashCode() methods must be implemented manually.

  5. The toString() method of Candy returns a JSON-like string.

1.1.2. Quiz: Records with static variables ⭐

Given the following Java Record:

record Candy( String name, int calories ) {
  public static String id = UUID.randomUUID().toString();
}

Does the record compile?

Is the following a valid alternative notation?

record Candy( String name, int calories ) {
  public static String id;
  Candy {
    id = UUID.randomUUID().toString();
  }
}

1.1.3. Record Patterns (NEW) ⭐

Daredevil pirates not only have a keen sense of prey, but also a fondness for exotic pets. So far, one application represents pets in two records:

record MischiefMonkey( String name, boolean isMutinous ) { }
record FeistyParrot( String name, String favoritePhrase, boolean isMutinous ) { }

The modeling has proven to be rigid as new animal types are added and properties change. Therefore, the data from the records should be converted into java.util.Properties objects. Properties is a special associative store that associates one string with another string. The method setProperty(String key, String value) sets a new key-value pair into the Properties object.

Task:

  • Write a method Properties convertToProperties(Object) to convert pets of type MischiefMonkey and FeistyParrot into Properties objects.

  • Pets that are mutinous (state isMutinous) should be ignored and result in an empty Properties object with no entries.

  • The method should be able to be called with null or unknown types and return an empty Properties object in those cases.

Examples (output shows the toString() representation of Properties):

  • new MischiefMonkey( "Jack", true ){}

  • new FeistyParrot( "Captain Squawk", "Avast, ye scallywags!", false ){favoritePhrase=Avast, ye scallywags!, name=Captain Squawk}

  • new MischiefMonkey( "Barbossa", false ){name=Barbossa}

  • new FeistyParrot( "Polly", "Pieces of eight!", true ){}

  • new FeistyParrot( "Marauder", "Walk the plank!", false ){favoritePhrase=Walk the plank!, name=Marauder}

1.2. Interfaces

Abstract classes are still classes with all their capabilities: instance variables, constructors, methods, different visibilities. Often a simpler form of specification is sufficient, and for this Java provides interfaces. They do not have instance variables, but can have constants, abstract methods, static methods, and default methods—​an instance variable is a way of storing something that belongs to a class, not to the interface.

1.2.1. Compare consumption of electrical devices ⭐

Every electrical device has a power output, which is measured in watts.

Task Part 1:

  1. Declare in ElectronicDevice a private int instance variable watt, and generate with the development environment Setter/Getter.

  2. Add a toString() method that returns something like the following: "ElectronicDevice[watt=12kW]". Some subclasses had already overridden toString(); they should then include a super.toString() in their toString() methods.

Task Part 2:

  1. Write a new class ElectronicDeviceWattComparator that implements the interface java.util.Comparator<ElectronicDevice>.

  2. Let the compare(…​) method define an ordering of electrical devices, where an electrical device is "smaller" if it consumes less power.

  3. Put a println(…​) in your own compare(…​) method for a better understanding, so that you can see which objects are compared.

Example:

ElectronicDevice ea1 = new Radio(); ea1.setWatt( 200 );
ElectronicDevice ea2 = new Radio(); ea2.setWatt( 20 );
Comparator<ElectronicDevice> c = new ElectronicDeviceWattComparator();
System.out.println( c.compare(ea1, ea2) );
System.out.println( c.compare(ea2, ea1) );

Objective of the exercise: ElectronicDeviceWattComparator as an implementation of the Comparator interface, as shown in the UML diagram:

ElectronicDeviceWattComparator UML
Figure 1. UML diagram

1.2.2. Find electronic devices with the highest power consumption ⭐

The java.util.Collections class has a static method that returns the largest element of a collection (the generics in the angle brackets have been removed for simplicity):

static T max( Collection coll, Comparator comp )

The following must therefore be passed to the max(…​) method

  1. a Collection implementation like ArrayList and

  2. a Comparator implementation. Here we can use our ElectronicDeviceWattComparator.

Task:

  • Put a method findMostPowerConsumingElectronicDevice() in the ship that returns the device with the highest consumption.

Example:

  • The following program returns the output 12000.

    Radio grannysRadio = new Radio();
    grannysRadio.volumeUp();
    grannysRadio.setWatt( 12_000 );
    
    TV grandpasTv = new TV();
    grandpasTv.setWatt( 1000 );
    
    Ship ship = new Ship();
    ship.load( grannysRadio );
    ship.load( grandpasTv );
    System.out.println( ship.findMostPowerConsumingElectronicDevice().getWatt() );

1.2.3. Use Comparator interface for sorting ⭐

If you want to sort objects of a list, you can use the sort(…​) method on List implementations. It is important to tell the sort(…​) method when one object is "smaller" than another. For this purpose, our ElectronicDeviceWattComparator can be used; it is a prerequisite for objects that you want to sort — the signature void sort(Comparator<…​> c) already reveals this.

Task:

  • Call sort(…​) in load(…​) of the ship object after adding it to its data structure, to always have an internal sorted list after adding.

1.2.4. Static and default methods in interfaces ⭐⭐⭐

Interfaces can contain static methods and serve as factory methods, that is, they can provide instances of classes that implement that interface.

Task:

  1. Create an interface Distance.

  2. Set two static methods Distance ofMeter(int value) and Distance ofKilometer(int value) in Distance, which return a new object of type Distance.

  3. Set in Distance an abstract method int meter(). What must be implemented?

  4. Set a default method int kilometer() in the interface Distance.

Example in use:

Distance oneKm = Distance.ofKilometer( 1 );
System.out.printf( "1 km = %d km, %d m%n", oneKm.kilometer(), oneKm.meter() );

Distance moreMeter = Distance.ofMeter( 12345 );
System.out.printf( "12345 m = %d km, %d m", moreMeter.kilometer(), moreMeter.meter() );

1.2.5. Delete selected elements with Predicate ⭐⭐

If we want to make ships energy efficient, we need to remove all devices with excessive power consumption.

The List method removeIf(Predicate<…​>filter) deletes all elements that satisfy a predicate. The ArrayList class is an implementation of the List interface, so the method is available on an ArrayList.

For example, if we want to delete from a List<String> all empty strings, we can call removeIf(new IsStringEmpty()) on the list, where IsStringEmpty is declared as follows:

class IsStringEmpty implements Predicate<String> {
  @Override public boolean test( String t ) {
    return t.trim().isEmpty();
  }
}
Find electronic devices with the highest power consumption

Task:

  • Set in the ship a new method removePowerConsumingElectronicDevices() that deletes all devices with power consumption greater than a self-selected constant MAXIMUM_POWER_COMSUMPTION.

1.3. Enumeration types (enum)

Enumeration types (enum) represent closed sets and are powerful in Java; they not only allow additional object and class variables, new private constructors, but they can also implement interfaces, override methods, and they have some built-in methods. The upcoming exercises address these nice features.

1.3.1. Enumeration for candy ⭐

Captain CiaoCiao wants to appeal to a younger crowd of buyers and is experimenting with candy instead of rum in his lab.

Enumeration for candy

Task:

  • Declare an enumeration CandyType with constants for

    • caramels

    • chocolate

    • gummies

    • licorice

    • lollipops

    • Chewing Gums

    • Cotton Candy

  • Respect the usual naming convention.

  • Users should be able to enter a candy from the console. The appropriate enum object should be retrieved for the input, case-insensitivity should not matter. Add a new static method Optional<CandyType> fromName(String input) in the enumeration type CandyType for the conversion from a String to an enumeration element of type CandyType. The method must not throw exceptions due to incorrect input; unknown names will result in an Optional.empty().

CandyType Enum UML
Figure 2. UML diagram of enumeration type (without method)

1.3.2. Deliver random candies ⭐

Captain CiaoCiao launches his food tour and always chooses random candies.

Deliver random candies

Task:

  • Give the enumeration type CandyType a method random() that returns a random candy.

    System.out.println( CandyType.random() ); // e.g., CHOCOLATE
    System.out.println( CandyType.random() ); // e.g., LOLLIPOPS
CandyType Enum random UML
Figure 3. UML diagram of the enumeration type with static methods

1.3.3. Tagging candy with addictive value ⭐⭐

We know that sweets are addictive, some more, some less.

Task:

  • Associate an addictive value (int) with each enumerated element from CandyType:

    • Caramels: 9

    • Chocolate: 5

    • gummies: 4

    • Licorice: 3

    • Lollipops: 2

    • Chewing Gums: 3

    • Cotton Candy: 1

    To store the addiction value, use a constructor in enum. The addiction value should be provided by a new non-static method addictiveQuality().

  • Since Captain CiaoCiao wants to achieve a dependency towards candy with a higher addiction factor, a new CandyType method next() shall return the candy with the next higher addiction. Lollipops have two potential successors, here the selection shall randomly go to Chewing Gums and Licorice. Caramels have no successor, and it stays with Caramels.

Examples:

  • CandyType.COTTON_CANDY.next() is LOLLIPOPS.

  • CandyType.LOLLIPOPS.next() is e.g. LICORICE.

  • CandyType.LOLLIPOPS.next() is e.g. CHEWING_GUMS.

  • CandyType.CARAMELS.next() is CARAMELS.

AddictiveQualityCandy Enum UML
Figure 4. UML diagram of enumeration type

1.3.4. Interface implementations via an enum ⭐⭐

An enum type can implement interfaces, but cannot extend classes.

Given an interface Distance:

interface Distance {
  double distance( double x1, double y1, double x2, double y2 );
  double distance( double x1, double y1, double z1, double x2, double y2, double z2 );
}

Task:

  • Take the interface Distance into your project.

  • Declare an enumeration type Distances that implements Distance with exactly one enumeration element EUCLIDEAN:

    enum Distances implements Distance {
      EUCLIDEAN
    }

    If you now need a Distance implementation for the Euclidean distance, you can get it using Distances.EUCLIDEAN.

  • Add the implementation that the Euclidean distance of two points is calculated; remember, for a 2D point:

    Math.sqrt( (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) )
  • Extend the enumeration type Distances with another enumeration element MANHATTAN so that there are two constants EUCLIDEAN and MANHATTAN.
    The Manhattan distance is formed by the sum of the absolute differences of the single coordinates, so for a 2D point Math.abs(x1 - x2) + Math.abs(y1 - y2).