1. Special types from the Java class library

The class library includes thousands of types, with a huge number added via the Java Enterprise Frameworks and open-source libraries. Fortunately, you don’t need to know all of these types to write software successfully. Much of Java SE is also very low level and intended more for framework developers.

Some common types are particularly closely related to the language, so even the compiler knows them. We need to understand these so that we can make the most of the language’s capabilities. This chapter is therefore about the absolute superclass Object, which methods are relevant for us, ordering principles, the use of primitive types, and wrapper types (autoboxing).

Prerequisites

  • know the difference between == (identity) and equals(…​) (equivalence).

  • be able to implement equals(…​) and hashCode().

  • implement Comparator and Comparable for ordering criteria

  • understand the working of autoboxing

Data types used in this chapter:

1.1. Absolute superclass java.lang.Object

From Object there are three methods that subclasses usually override: toString(), equals(Object) and hashCode(). While identity is tested with == and !=, equivalence is handled by the equals(Object) method. equals(Object) and hashCode() are always implemented together so that both match each other; for example, if the hash code of two objects is not equal, equals(…​) must also return false and if equals(…​) returns true, the two hash codes must also be equal. There are certain rules to follow when implementing this, which is why the next two tasks focus on equals(Object) and hashCode(). (ObjecthashCode()))

1.1.1. Generate equals(Object) and hashCode() ⭐

Every modern Java development environment can generate various methods, for example toString(), but also equals(Object) and hashCode().

The development environments have slightly different menu items and dialogs. equals(Object)/hashCode() can be created for the three known IDEs as follows:

IntellIJ

In this IDE we press the shortcut Alt+Ins. A list of items that can be generated follows, and equals() and hashCode() is listed below it. If we activate the entry, a dialog opens first where we can select different templates. IntelliJ can generate the methods in different ways. We stay with the default setting and switch to the next dialog with Next. We now select the instance variables that will be used for the equals(…​) method; by default, these are all of them. We press Next. The next step is the same dialog, but now we select the instance variables for the hashCode(…​) method; by default, all are preselected again. We press Next and reach the next dialog, where we may still determine whether the name may be null or not. Since we assume it could be null, we do not select the field, but go directly to Finish.

Eclipse

In Eclipse we put the cursor in the body of the class, activate the context menu, navigate to the menu item Source, and go to Generate hashCode() and equals(). Unlike in IntelliJ, in Eclipse the instance variables are displayed only once and used for both the equals(Object) and hashCode() methods at the same time. Code generation starts with a click on Generate.

NetBeans

Go to the menu item under Source (or activate the context menu in the editor), then select Insert Code; alternatively activate via keyboard Alt+Ins. A small dialog follows, where you can select equals() and hashCode()…​. Also, other things like a setter, getter, constructor, toString() can be generated this way.

Task:

  • Copy the following class into the project:

    public class Person {
      public long id;
      public int age;
      public double income;
      public boolean isDrugLord;
      public String name;
    }
  • Create the methods equals(Object) and hashCode() for the class Person with the IDE.

  • Study the generated methods cautiously.

1.1.2. Existing equals(Object) implementations ⭐⭐

What does the Javadoc have to say, or what do the equals(Object) implementations look like for the following classes?

  • java.awt.Rectangle (module java.desktop)

  • java.lang.String (module java.base)

  • java.lang.StringBuilder (module java.base)

  • java.net.URL (module java.base)

The code for the individual modules can be viewed online from OpenJDK at https://github.com/openjdk/jdk/tree/master/src/; the classes can be found at share/classes.

1.2. Interfaces Comparator and Comparable

A comparison with equals(…​) tells whether two objects are equal, but says nothing about the order, which object is larger or smaller. For this, there are two interfaces in Java:

  • Comparable is implemented by the types that have a natural order, that is, for which there is usually a common ordering criteria. If there are two date values, it is clear which was before and which was after, or if both dates are the same.

  • There is an implementation of the interface Comparator for each ordering criteria. We can sort people by name, but we can also sort by age: that would be two implementations of Comparator.

Comparator Comparable UML
Figure 1. UML diagram of Comparator and Comparable

1.2.1. Handle superheroes

Bonny Brain has been interested in superheroes since she was a child. And there are so many exciting things to know. For Bonny Brain to get answers to her questions, we first need to define the dataset.

Task: Copy the following class declaration into your Java project:[1]

com/tutego/exercise/util/Heroes.java
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

public class Heroes {

  private Heroes() { }

  public record Hero( String name, Heroes.Hero.Sex sex, int yearFirstAppearance ) {
    public enum Sex { MALE, FEMALE }
    public Hero {
      Objects.requireNonNull( name ); Objects.requireNonNull( sex );
    }
  }

  public static class Universe {
    private final String     name;
    private final List<Hero> heroes;

    public Universe( String name, List<Hero> heroes ) {
      this.name   = Objects.requireNonNull( name );
      this.heroes = Objects.requireNonNull( heroes );
    }

    public String name() { return name; }
    public Stream<Hero> heroes() { return heroes.stream(); }
  }

  // https://github.com/fivethirtyeight/data/tree/master/comic-characters
  private static final Hero DEADPOOL = new Hero( "Deadpool (Wade Wilson)", Hero.Sex.MALE, 1991 );
  private static final Hero LANA_LANG = new Hero( "Lana Lang", Hero.Sex.FEMALE, 1950 );
  private static final Hero THOR = new Hero( "Thor (Thor Odinson)", Hero.Sex.MALE, 1950 );
  private static final Hero IRON_MAN = new Hero( "Iron Man (Anthony 'Tony' Stark)", Hero.Sex.MALE, 1963 );
  private static final Hero SPIDERMAN = new Hero( "Spider-Man (Peter Parker)", Hero.Sex.MALE, 1962 );
  private static final Hero WONDER_WOMAN = new Hero( "Wonder Woman (Diana Prince)", Hero.Sex.FEMALE, 1941 );
  private static final Hero CAPTAIN_AMERICA = new Hero( "Captain America (Steven Rogers)", Hero.Sex.MALE, 1941 );
  private static final Hero SUPERMAN = new Hero( "Superman (Clark Kent)", Hero.Sex.MALE, 1938 );
  private static final Hero BATMAN = new Hero( "Batman (Bruce Wayne)", Hero.Sex.MALE, 1939 );

  public static final List<Hero> DC =
      List.of( SUPERMAN, LANA_LANG, WONDER_WOMAN, BATMAN );

  public static final List<Hero> MARVEL =
      List.of( DEADPOOL, CAPTAIN_AMERICA, THOR, IRON_MAN, SPIDERMAN );

  public static final List<Hero> ALL =
      Stream.concat( DC.stream(), MARVEL.stream() ).toList();

  public static final List<Universe> UNIVERSES =
      List.of( new Universe( "DC", DC ), new Universe( "Marvel", MARVEL ) );
}

If you have added the class to your project, you are done with the exercise! The class declaration is a preparation for the next tasks. About the content of the class: Heroes declares the two nested classes Hero and Universe and also collections with heroes. Which Java API is used to initialize the variables and which private variables exist is not relevant for the solution. We will come back to the class Heroes in the section about the Java Stream API. Those interested are welcome to rewrite the classes in records.

1.2.2. Compare Superheroes ⭐⭐

Not all heroes are the same! Some appear earlier or are bald. We can use Comparator objects to individually determine the order between heroes.

Compare Superheroes

Task:

  • First, build a mutable list of all heroes:

    List<Hero> allHeroes = new ArrayList<>( Heroes.ALL );
  • Write a comparator so that heroes are ordered by year of release. Use for implementation:

    1. a local class

    2. an anonymous class

    3. a lambda expression

  • The List interface has a sort(…​) method. Sort the list allHeroes with the new Comparator.

  • Extend the one Comparator so that if the year of publication is the same, it is also compared by name. Evaluate the approach that the Comparator tests several criteria at the same time.

1.2.3. Concatenate hero comparators ⭐

Sorting is often done not only by one criterion, but by many. A typical example is the phone book — if that is still known today …​ First, the entries are sorted by the last name and then, in the case of a group of people with the same last names, by the first name.

Often several criteria are involved in the ordering. We do not have to do the chaining of the Comparator instances ourselves, but we can use the default method thenComparing(…​).

Task:

  • Study the API documentation (or implementation) for the Comparator method thenComparing(Comparator<? super T> other).

  • Some heroes have the same release year.

    1. Write a Comparator implementation that only compares heroes by name.

    2. Write a second Comparator that compares the heroes only by their year of publication.

    3. Sort all heroes in the first criterion by year of publication, then by name. Implement the composite Comparator with thenComparing(…​).

1.2.4. Using a key extractor to easily create a comparator ⭐⭐

A Comparator generally "extracts" key elements and compares them. But doing this the Comparator actually does two things: first, it extracts the relevant information, and second, it compares these extracted values. According to good object-oriented programming, these two steps should be separated. This is the goal of the static comparing*(…​) methods of the Comparator interface. Because these methods are only given a key-extractor, and the comparison of the extracted values is done by the comparing*(…​) methods themselves.

Let’s look at three implementations, and start with the implementation of the comparing(Function) method:

OpenJDK’s implementation from java.util.Comparator.
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
       Function<? super T, ? extends U> keyExtractor)
{
   Objects.requireNonNull(keyExtractor);
   return (Comparator<T> & Serializable)
       (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

Initially, the mandatory null test takes place. Then keyExtractor.apply(…​) gets the value from the first object and the second object. Since both objects have a natural order (they are Comparable), compareTo(…​) returns this order. comparing(Function) returns a Comparator, here as a lambda expression.

The key extractor is a function that returns a value, and exactly this value is compared internally. comparing(Function) can be used if the objects have a natural order. Now, there are different factory methods for Comparator instances that, in addition to comparing Comparable objects, extract selected primitive data types and compare them. Let’s look at the second method comparingInt(ToIntFunction) when two integers are extracted via a ToIntFunction:

OpenJDK’s implementation from java.util.Comparator.
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
   Objects.requireNonNull(keyExtractor);
   return (Comparator<T> & Serializable)
       (c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}

The key extractor extracts an integer value from the objects to be compared and then defers to Integer.compare(…​) to compare these two integers.

Let’s take a look at the last function. It combines a key extractor with a Comparator. This is handy when the objects have no natural order, but a foreign Comparator has to determine the order.

OpenJDK’s implementation from java.util.Comparator
public static <T, U> Comparator<T> comparing(
       Function<? super T, ? extends U> keyExtractor,
       Comparator<? super U> keyComparator)
{
   Objects.requireNonNull(keyExtractor);
   Objects.requireNonNull(keyComparator);
   return (Comparator<T> & Serializable)
       (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                         keyExtractor.apply(c2));
}

First, the key extractor will extract the values for the two objects c1 and c2. Thereafter, the values will go into the compare(…​) method of the passed Comparator instance. The lambda expression returns a new Comparator.

If you compare this with your own Comparator implementations, you will generally do the same, namely, extract the values from two objects and compare them. This is precisely what factory functions do! We just have to specify how a key has to be extracted, and then this key extractor is applied to the two values to be compared.

Vary the previous task as follows:

  1. Use the static method Comparator.comparingInt(ToIntFunction<? super T> keyExtractor) and a lambda expression to create a Comparator to sort the list by years.

  2. For comparison of names, also use a Comparator method that uses a key extractor.

  3. Sort by name and then by age, again using thenComparing(…​). Then change the concatenation method, and use thenComparingInt(…​) instead of thenComparing(…​).

  4. Write a Comparator<Hero>, based on CASE_INSENSITIVE_ORDER from String, for the hero name regardless of case. Call the comparator method comparing(Function, Comparator).

1.2.5. Sort points by distance to center ⭐

Captain CiaoCiao runs his Absolutus Zero-Zero Vodka Distillery at the North Pole. On an imaginary rectangular map, the distillery is located exactly at the zero point. A java.awt.Point is represented by x/y coordinates, which is quite appropriate for storing the location information. Now the question is whether certain locations are closer or further from the distillery.

Sort points by distance to center

Task:

  • Write a comparison comparator PointDistanceToZeroComparator for Point objects. The distance to the zero points is to be used for the comparison. If the distance of a point p1 from the zero point is smaller than the distance of a point p2, let p1 < p2 hold.

  • Build an array of Point objects and sort them using the Arrays method sort(T[] a, Comparator<? super T> c).

Example:

Point[] points = { new Point( 9, 3 ), new Point( 3, 4 ), new Point( 4, 3 ), new Point( 1, 2 ) };
Arrays.sort( points, new PointDistanceToZeroComparator() );
System.out.println( Arrays.toString( points ) );

The output is:

[java.awt.Point[x=1,y=2], java.awt.Point[x=3,y=4], java.awt.Point[x=4,y=3], java.awt.Point[x=9,y=3]]

The class java.awt.Point provides various class and object methods for calculating the distance. See the API documentation for more information.

1.2.6. Find stores nearby ⭐⭐

For the liquors of the Absolutus Zero-Zero Vodka distillery, Bonny Brain builds the distribution channels and plans stores in different locations.

Find stores nearby

Task:

  1. Create a new record Store.

  2. Give the Store two instance variables Point location and String name.

  3. Collect several Store objects in a list.

  4. Write a method List<Store> findStoresAround(Collection<Store> stores, Point center) that returns a list, sorted by distances from the center; at the head of the list are those closest to the distillery.


1. source: https://github.com/fivethirtyeight/data/tree/master/comic-characters