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 succesfully. 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 functionality 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 object 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 object 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 object 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 in 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 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 very carefully.

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. Quiz: Natural order or not? ⭐

If we execute the following in a main(…​) method, what happens? The type Point is from the java.awt package.

String[] strings = { "A", "B", "C" };
Arrays.sort( strings );

Point[] points = {
 new Point( 9, 3 ),
 new Point( 3, 4 ),
 new Point( 4, 3 ),
 new Point( 1, 2 ),
};

Arrays.sort( points );

1.2.2. Handle superheroes

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

Task: Copy the following class declaration into your own 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 static class Hero {

    public enum Sex { MALE, FEMALE }

    public final String name;
    public final Sex    sex;
    public final int    yearFirstAppearance;

    public Hero( String name, Sex sex, int yearFirstAppearance ) {
      this.name = Objects.requireNonNull( name );
      this.sex  = Objects.requireNonNull( sex );
      this.yearFirstAppearance = yearFirstAppearance;
    }

    @Override public String toString() {
      return String.format( "Hero[name=%s, sex=%s, yearFirstAppearance=%s]",
                            name, sex, yearFirstAppearance );
    }
  }

  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.

*Java 8 Backport

The static of(…​) methods of the List interface are new in Java 9. For Java 8 you can write Collections.unmodifiableList( Arrays.asList(e1, e2, …​). The method Collectors.toUnmodifiableList() exists since Java 10; for Java 8 we have to write for example: Collections.unmodifiableList( aStream.collect( Collectors.toList() ) ). And instead of .collect( Collectors.toUnmodifiableList(), since Java 16, toList() is possible on the stream; the result is an unmodifiable list.

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

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.4. Concatenate hero comparators ⭐⭐

Sorting is often done not only by one criteria, but by several. A typical example is the phone book — if that is still known today …​ First, the entries are sorted by last name and then, in the case of a group of people with the same last names, by 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.5. 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. In general, a Comparator extracts crutial elements and compares them. This is the goal of the static comparingXXX(…​) 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 comparingXXX(…​) methods themselves.

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

OpenJDK 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));
}

At the beginning, 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 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 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 of all, the key extractor will extract the values for the two objects c1 and c2. After that, 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 exactly 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.6. 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.

Task:

  • Write a comparison comparator PointDistanceToZeroComparator for Point objects. The distance to the zero point 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.7. 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.

Task:

  1. Create a new class or record Store.

  2. Give the Store two object 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.3. Autoboxing

Autoboxing is a capability of the compiler to pack primitive types into wrapper objects (called boxing) and then unbox them when needed (called unboxing). With autoboxing there are some oddities that you should be aware of, otherwise you will get an unexpected NullPointerException or performance problems. (Autoboxing(Boxing

1.3.1. Quiz: Handling null reference in unboxing ⭐

Unboxing extracts the primitive value from a wrapper object. Since object references can be null, questions arise:

  1. Can a null be converted to 0 in such a way: int i = null?

  2. What will happen?

    Character c = null;
    switch ( c ) { }
  3. Does it make sense to write the following?

    Map<String, Integer> map = new HashMap<>();
    map.put( "number-of-ships", 102 );
    int ships = map.get( "number_of_ships" );

    Research the return of get(…​) of type java.util.Map for the answer.

1.3.2. Quiz: Unboxing surprise ⭐⭐

What are the screen outputs if the following lines were in the main(…​) method?

Integer i11 = 1;
Integer i12 = 1;
System.out.println( i11 == i12 );
System.out.println( i11 <= i12 );
System.out.println( i11 >= i12 );

Integer i21 = 1000;
Integer i22 = 1000;
System.out.println( i21 == i22 );
System.out.println( i21 <= i22 );
System.out.println( i21 >= i22 );

The task is tricky.

Autoboxing calls the valueOf(…​) method. The Javadoc gives a crucial hint.

1.4. Suggested solutions

1.4.1. Generate equals(Object) and hashCode()

Generated equals(Object) method from IntelliJ

Let’s take a look at equals(Object) first:

Generated method equals(Object) from IntelliJ
public boolean equals( Object o ) {
  if ( this == o )
    return true;
  if ( o == null || getClass() != o.getClass() )
    return false;

  Person person = (Person) o;

  if ( id != person.id )
    return false;
  if ( age != person.age )
    return false;
  if ( Double.compare( person.income, income ) != 0 )
    return false;
  if ( isDrugLord != person.isDrugLord )
    return false;
  return name != null ? name.equals( person.name ) : person.name == null;
}

First we test if the incoming object is identical to our own object, then we have a short path, and the answer is true, because of course we are equivalent to ourselves. If the incoming object reference is null or if our own Class object is not identical to the passed Class object, then we do not want to continue the comparison, we can exit with false. By the way, the Class comparison is not the only option, we can also test the type relationships with instanceof, which also includes subtypes in the comparison.

After the initial queries, we continue with the object state tests. We know that the object is in fact a Person, so we introduce a new variable of type Person. Next, we need to compare our own states with those of the person. For integers and for truth values, we can compare directly. The approach here is to compare the values and if they are not equal, exit the equals(…​) method with false, because there is no point in checking any further.

It remains interesting to compare the floating point number and the reference. With floating point numbers we have the problem that there is a NaN (Not-a-Number). And a NaN is not equal to a NaN! This special case is covered by the compare(…​) method of Double. Although the compare(…​) method checks much more, namely the order of two floating-point numbers, we do not need this property, we only compare if the result is not equal to 0; the method returns 0 if two floating-point numbers are equal, otherwise something negative or positive.

The previous object variables were primitive, last we look at the equality test on reference types. Here we delegate to the equals(Object) method of the referenced object. However, we have to consider a special case, namely that name can be null. In the dialog we could select whether an object variable can be null or not; since we selected null as a possibility, a special query is added. A nice alternative would be:

return Objects.equals( name, person.name );
Generated equals(Object) method from Eclipse

Let’s move on to Eclipse:

Generated method equals(Object) from Eclipse
public boolean equals( Object obj ) {
  if ( this == obj )
    return true;
  if ( obj == null )
    return false;
  if ( getClass() != obj.getClass() )
    return false;
  Person other = (Person) obj;
  if ( age != other.age )
    return false;
  if ( id != other.id )
    return false;
  if ( Double.doubleToLongBits( income ) !=
       Double.doubleToLongBits( other.income ) )
    return false;
  if ( isDrugLord != other.isDrugLord )
    return false;
  if ( name == null ) {
    if ( other.name != null )
      return false;
  }
  else if ( !name.equals( other.name ) )
    return false;
  return true;
}

In Eclipse, the query is essentially the same, but the code is slightly longer. This is partly because the checks on null and on the Class objects take place in two individual case distinctions and have not been combined. The checks on the name on null are also somewhat more extensive. The only real difference is when comparing floating point numbers. Here Eclipse takes a slightly different approach. A double is the same size as a long with 64 bits. The double method doubleToLongBits(double) queries the bit pattern as long. Then, if we have two integers, we can compare them like any other integer. This solves the problem with the NaN.

Generated method hashCode() from IntelliJ

Let’s take a look at hashCode().

generated hashCode() method of IntelliJ
public int hashCode() {
  int result;
  long temp;
  result = (int) (id ^ (id >>> 32));
  result = 31 * result + age;
  temp = Double.doubleToLongBits( income );
  result = 31 * result + (int) (temp ^ (temp >>> 32));
  result = 31 * result + (isDrugLord ? 1 : 0);
  result = 31 * result + (name != null ? name.hashCode() : 0);
  return result;
}

The job of the method is to bring the values of the various object variables together algorithmically to produce an int. In the best case, the hash code changes when any object variable changes. So the desire is to get a completely different hash code for a changed bit of some object variable.

When setting up the calculation, the IntelliJ and Eclipse development environments use a special pattern. After the initialization of result the calculation continues with result = 31 * result + hashCode, where hashCode is the hashcode to be added in each step.[2] How the hash code is composed now for int, long, double and a reference type shows exactly the code. Since the hashcode is an int, age, which is also an int, is added directly. In the case of a truth value, IntelliJ uses either 1 or 0. In the case of a long integer, first the upper 32 bits, then the lower 32 bits are combined using an XOR operation. The same happens in principle with a double floating point number, here the bit pattern is used, similar to what the equals(…​) method uses for comparison.

Generated method hashCode() from Eclipse

Eclipse takes a similar approach:

generated hashCode() method of Eclipse
public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + age;
  result = prime * result + (int) (id ^ (id >>> 32));
  long temp;
  temp = Double.doubleToLongBits( income );
  result = prime * result + (int) (temp ^ (temp >>> 32));
  result = prime * result + (isDrugLord ? 1231 : 1237);
  result = prime * result + ((name == null) ? 0 : name.hashCode());
  return result;
}

There are two minor differences between Eclipse and IntelliJ.

  • First, the magic value 31 is pulled out as a variable. This is handy if we want to change the number later. If we have a large number of object variables, we may need to adjust the prime, otherwise multiplication may result in large numbers running out of the value range, so the first object variables are not considered at all. We have to reduce the prime number in such a case.

  • The second difference is the use of a different number for truth values: Eclipse uses 1231 = 0b10011001111 and 1237 = 0b100110101.

Overriding the hashCode() method by hand, some developers fall back to the static Objects method hash(Object... values):

public int hashCode() {
  return Objects.hash( age, income, isDrugLord, name );
}

This is short and elegant, but you should rather refrain from this, because first of all the method has to do a boxing for primitive values, and secondly an array always must be created for the vararg and later swept away by the automatic garbage collection. For an immutable object, however, this makes it easy to calculate the hash code once and then cache it.

1.4.2. Existing equals(Object) implementations

java.awt.Rectangle (module java.desktop)

From the Javadoc you can read what equals(…​) does: "The result is true if and only if the argument is not null and is a Rectangle object that has the same upper-left corner, width, and height as this Rectangle." You can see this even better in the source code of the implementation:

OpenJDK implementation of the equals(…​) method from java.awt.Rectangle.
public boolean equals(Object obj) {
    if (obj instanceof Rectangle) {
        Rectangle r = (Rectangle)obj;
        return ((x == r.x) &&
                (y == r.y) &&
                (width == r.width) &&
                (height == r.height));
    }
    return super.equals(obj);
}

The method equals(Object obj) from the superclass Object has the parameter type Object for obj, which the overriding class must of course adopt. The idea: In general, you may also ask an apple whether it is equivalent to a pear, but the answer must always be false. This is how the class Rectangle does it. The instanceof test first checks whether the object type is also Rectangle, which includes subclasses. Only then does it proceed with the actual checks, and the x and y position as well as height and width of its own object are compared with the other rectangle. If it is not a Rectangle, then the superclass should deal with the comparison. Usually we find in the equals(…​) implementation a query on null or on this, this is saved here and moved to the superclass.

java.lang.String (module java.base)

The String class has an equals(…​) method, and the Java API documentation describes how to implement it:

Compares this string to the specified object. The result is true if and only if the argument is not null and is a String object that represents the same sequence of characters as this object.

We don’t need to look further into the implementation.

java.lang.StringBuilder (module java.base)

The Java documentation does not show an overridden equals(…​) method for StringBuilder, and the entry under Methods declared in class java.lang.Object shows that an equals(…​) method comes from Object. The class documentation also states:

StringBuilder implements Comparable but does not override equals. Thus, the natural ordering of StringBuilder is inconsistent with equals. Care should be exercised if StringBuilder objects are used as keys in a SortedMap or elements in a SortedSet. See Comparable, SortedMap, or SortedSet for more information.

The consequence is that we get into trouble when we put a StringBuilder into a data structure, because the vast majority of data structures use the equals(…​) method.

java.net.URL (module java.base)

Of all the equals(…​) methods in the Java library, the one for the URL class might be the weirdest. The Java documentation explains why:

Two URL objects are equal if they have the same protocol, reference equivalent hosts, have the same port number on the host, and the same file and fragment of the file. Two hosts are considered equivalent if both host names can be resolved into the same IP addresses; else if either host name can’t be resolved, the host names must be equal without regard to case; or both host names equal to null. Since hosts comparison requires name resolution, this operation is a blocking operation. Note: The defined behavior for equals is known to be inconsistent with virtual hosting in HTTP.

This has far-reaching consequences. First of all, the equals(…​) method can take a very long time to execute, namely when a network access has to be made. Even in the best case, when everything is in an internal cache, the performance is still worse than when a few object variables are compared. Furthermore, network access is always required, which means that if the computer is disconnected from the network, the equals(…​) method cannot respond. Another problem is that the equals(…​) method can suddenly give a different answer. What may not have been the same before can suddenly be the same.

For this reason, it is unusual for URL objects to be held in data structures such as lists or sets, because the standard implementations almost always fall back on the equals(…​) method. The Java library provides a second class URI that implements equals(…​) as a simple comparison of object variables, i.e. Schema, Fragment, Path, Query, etc.

1.4.3. Quiz: Natural order or not?

This is followed by an

Exception in thread "main" java.lang.ClassCastException: class java.awt.Point cannot be cast to class java.lang.Comparable (java.awt.Point is in module java.desktop of loader 'bootstrap'; java.lang.Comparable is in module java.base of loader 'bootstrap')

The reason: The method sort(Object[]) from java.util.Arrays assumes that the objects have a natural order and therefore implement the Comparable interface. However, Point does not do that.

If we want to compare unknown objects with each other, we have to give the sort(…​) method a special object that respects the sort criterion. This is the task of a Comparator object.

1.4.4. Compare Superheroes

com/tutego/exercise/util/HeroComparators.java
// local class
class YearFirstAppearanceComparator implements Comparator<Heroes.Hero> {
  @Override public int compare( Heroes.Hero h1, Heroes.Hero h2 ) {
    return Integer.compare( h1.yearFirstAppearance, h2.yearFirstAppearance );
  }
}

// inner anonymous class
Comparator<Heroes.Hero> innerClassComparator = new Comparator<>() {
  @Override public int compare( Heroes.Hero h1, Heroes.Hero h2 ) {
    return Integer.compare( h1.yearFirstAppearance, h2.yearFirstAppearance );
  }
};

// Lambda expression
Comparator<Heroes.Hero> lambdaComparator =
    (h1, h2) -> Integer.compare( h1.yearFirstAppearance, h2.yearFirstAppearance );

// Comparator with 2 criteria
Comparator<Heroes.Hero> combinedComparator = ( h1, h2 ) -> {
  int yearComparison = Integer.compare( h1.yearFirstAppearance, h2.yearFirstAppearance );
  return (yearComparison != 0) ? yearComparison : h1.name.compareTo( h2.name );
};

List<Heroes.Hero> allHeroes = new ArrayList<>( Heroes.ALL );
allHeroes.sort( new YearFirstAppearanceComparator() );
allHeroes.sort( innerClassComparator );
allHeroes.sort( lambdaComparator );
allHeroes.sort( combinedComparator );

The Comparator implementations rely on Integer.compare(..) to compare the two integers, which is implemented like this:

OpenJDK implementation of Integer.compare(int, int) method.
public static int compare(int x, int y) {
  return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

In order for Comparator instances to do more than a comparison, there are two approaches. First, they can completely realize the logic itself, as in the proposed solution, or two existing Comparator objects can be concatenated, which is the topic of the next task.

The merging of the logic is shown in the proposed solution: First the year is compared, and if the years are equal, compare(…​) returns 0, then the second criterion must be considered, the name. name is a String and implements Comparable for a natural order, so compareTo(…​) must then make the decision.

1.4.5. Concatenate hero comparators

com/tutego/exercise/util/HeroCombinedComparators.java
Comparator<Heroes.Hero> nameComparator =
    (h1, h2) -> h1.name.compareTo( h2.name );

Comparator<Heroes.Hero> yearComparator =
    (h1, h2) -> Integer.compare( h1.yearFirstAppearance, h2.yearFirstAppearance );

Comparator<Heroes.Hero> combinedComparator = yearComparator.thenComparing( nameComparator );

List<Heroes.Hero> allHeroes = new ArrayList<>( Heroes.ALL );
allHeroes.sort( combinedComparator );
System.out.println( allHeroes );

To better understand how it works, a look at the thenComparing(…​) implementation from the OpenJDK:

OpenJDK implementation from java.util.Comparator.
default Comparator<T> thenComparing(Comparator<? super T> other) {
   Objects.requireNonNull(other);
   return (Comparator<T> & Serializable) (c1, c2) -> {
       int res = compare(c1, c2);
       return (res != 0) ? res : other.compare(c1, c2);
   };
}

The focus is on the return statement with the lambda expression, which thus returns a Comparator. The implementation first calls its own compare(…​) method, because its own Comparator goes first and checks first. If the comparison is not 0, then our compare(…​) method can return the result directly. However, if the result was equal to 0, then our own compare(…​) method says that the two objects are equal, and then we have to proceed to the second Comparator from the parameter passed to the thenComparing(Comparator) method. This second Comparator must then decide what to do next.

1.4.6. Using a key extractor to easily create a comparator

Building new Comparator objects using static methods and the key extractor is a bit unfamiliar at the beginning. You also have to get used to chaining Comparator objects. The assignment introduces the implementation extensively, so that the solution results from it:

com/tutego/exercise/util/HeroKeyExtractorComparators.java
Comparator<Heroes.Hero> nameComparator =
    Comparator.comparing( h -> h.name );

Comparator<Heroes.Hero> yearComparator =
    Comparator.comparingInt( h -> h.yearFirstAppearance );

Comparator<Heroes.Hero> combinedComparator1 =
    yearComparator.thenComparing( nameComparator );

Comparator<Heroes.Hero> combinedComparator2 =
    nameComparator.thenComparingInt( h -> h.yearFirstAppearance );

Comparator<Heroes.Hero> insensitiveNameComparator =
    Comparator.comparing( h -> h.name, String.CASE_INSENSITIVE_ORDER );

1.4.7. Sort points by distance to center

com/tutego/exercise/util/PointComparatorDemo.java
class PointDistanceToZeroComparator implements Comparator<Point> {
  @Override
  public int compare( Point p1, Point p2 ) {
    double distanceToZeroPoint1 = p1.distanceSq( 0, 0 );
    double distanceToZeroPoint2 = p2.distanceSq( 0, 0 );
    return Double.compare( distanceToZeroPoint1, distanceToZeroPoint2 );
  }
}

The sort(…​) method expects a Comparator from us. The Comparator is a functional interface with a single abstract method compare(…​).

Since Comparator is a generic data type, we need to specify a type argument, and that is our Point. To implement the Comparator interface, we write a class PointDistanceToZeroComparator. It implements compare(Point p1, Point p2). In the body of the method, we first calculate the distance of the first point to the zero point and then the second distance to the zero point. Strictly speaking, we don’t really calculate the distance, but the distance squared, but that’s perfectly fine for the comparison and even a bit faster, because the root isn’t necessary.

With the two distances, all we need to do is return the appropriate value — negative, positive, or 0 — for the compare(…​) method. To do this, we fall back on the Double.compare(…​) three-way comparison function.

We can pass the Comparator as a second argument to sort(…​) like this:

Arrays.sort( points, new PointDistanceToZeroComparator() );

1.4.8. Find stores nearby

com/tutego/exercise/oop/StoreFinder.java
class Store {
  String name;
  Point  location;

  Store( String name, int x, int y ) {
    this.name = name;
    this.location = new Point( x, y );
  }

  @Override
  public String toString() {
    return String.format( "Store [name=%s, location=%s]", name, location );
  }
}

public class StoreFinder {

  static List<Store> findStoresAround( Collection<Store> stores, Point center ) {
    List<Store> result = new ArrayList<>( stores );

    class StoreDistanceComparator implements Comparator<Store> {
      @Override
      public int compare( Store s1, Store s2 ) {
        double dist1ToCenter = s1.location.distance( center );
        double dist2ToCenter = s2.location.distance( center );
        return Double.compare( dist1ToCenter, dist2ToCenter );
      }
    }

    result.sort( new StoreDistanceComparator() );
    return result;
  }

  public static void main( String[] args ) {
    Store s1 = new Store( "ALDI", 10, 10 );
    Store s2 = new Store( "LIDL", 90, 80 );
    Store s3 = new Store( "REWE", 51, 51 );
    List<Store> list = Arrays.asList( s1, s2, s3 );
    System.out.println( list );
    List<Store> around = findStoresAround( list, new Point( 50, 50 ) );
    System.out.println( around );
  }
}

First, we model the Store class with the two object variables name and location. For practical reasons, we give the class a parameterized constructor that takes the name, as well as the x-y coordiate, and transfers it to the internal states. The toString() method returns a representation with the name and location.

The next class StoreFinder has the desired findStoresAround(…​) method and a main(…​) method with a small demonstration.

The actual findStoresAround(…​) method uses a special trick to make the implementation a bit more compact. First of all, the general solution: we work with a List and a Comparator, so we can use the Comparator object to sort the list. The Comparator only has to use the distance between the points as an ordering criterion. Here we can make it easy for ourselves, because the java.awt.Point class has a distance(…​) method, with which we can easily calculate the distance of points.

In the first step of findStoresAround(…​) we put the Store objects from the Collection into an ArrayList. This is a requirement for sorting, because a Collection cannot be sorted, only a list can. Furthermore, the assignment does not explicitly say that we are allowed to modify the Collection as well. Therefore, we want to be conservative here and not modify the incoming Collection, which may not be possible if the Collection is immutable.

Now we take advantage of the special feature of the Java language that you can declare classes inside methods. We call such a feature local inner classes. The advantage of local inner classes is that they can access (explicitly or implicitly final) variables from the environment. Our Comparator needs this, because if it is to judge which of the two Store objects is closer to the center, a comparison must take place from the first Store to the center and then from the second Store to the center. If you don’t want to work with the local inner classes, you would have to give the Comparator implementation a parameterized constructor so that you can get the center into the Comparator implementation from the outside.

The inside of the compare(…​) method calculates the distance of the two stores to the center. This results in two floating point numbers, which we again translate into something negative, positive or 0 using the familiar Double.compare(…​) method. If we have the Comparator, we can use it to sort our list and return the list.

1.4.9. Quiz: Handling null reference in unboxing

The first statement does not compile. What you can compile, however, is the following code:

Integer int1 = null;
int int2 = int1;
int i = (integer) null;

During execution, though, there is a NullPointerException. The reason lies in the unboxing. The Java compiler will automatically call appropriate methods to retrieve the primitive value from the wrapper object. In the case of an Integer object, the compiler will automatically put into the bytecode a call to the intValue() method.

A NullPointerException also exists in the switch example. The Java compiler will automatically unbox and call charValue() — this will not succeed at runtime. switch has no special treatment on a wrapper, which can be null.

A Map is an associative store that connects key-value pairs. The get(…​) method has the property that it returns null if there is no associated value to the key. Our example is just such a case. Under a wrong key, we query the associative store, there is no associated value, so the return is null. The compiler will automatically unbox and call the intValue() method, resulting in a NullPointerException at runtime. The solution to this is to explicitly include a check for null in the code whenever null expresses a missing return. So safer would be:

Integer maybeShips = map.get( "number_of_ships" );
if ( maybeShips != null ) {
  int ships = maybeShips;
}

To summarize: There is no automatic conversion from null to 0 or false.

1.4.10. Quiz: Unboxing surprise

The output of the program is noted in the comment.

Integer i11 = 1;
Integer i12 = 1;
System.out.println( i11 == i12 );  // true
System.out.println( i11 <= i12 );  // true
System.out.println( i11 >= i12 );  // true

Integer i21 = 1000;
Integer i22 = 1000;
System.out.println( i21 == i22 );  // false
System.out.println( i21 <= i22 );  // true
System.out.println( i21 >= i22 );  // true

It is surprising at first glance that in one case the == operator produces true and then suddenly false. There are three things to know:

  1. The relational operators <, >, <=, >= force the values to be taken from the wrapper objects and the comparison to be made. However, the == comparison operator does not perform unboxing, but merely compares two references and consequently makes an identity comparison. That is, the code does not compare the numbers 1 and 1 or 1000 and 1000, but compares the references of the two Integer objects respectively. The question is: Why are the two Integer objects identical for 1 and not for 1000? We will come to that now …​

  2. The compiler does not build wrapper objects with new, but goes to the factory method valueOf(…​).

  3. The Integer class internally uses a cache for all Integer objects in the value range from -128 to +127. The valueOf(…​) method consequently returns already existing Integer objects for exactly the integers from the mentioned value range. Everything outside the value range is always built and returned as a new object. Thus, calling valueOf(1000) results in two different Integer objects, which are of course not identical.


1. source: https://github.com/fivethirtyeight/data/tree/master/comic-characters
2. 31 is a prime number, which gives a good spread of bits. The fact that ultimately 31 is used so often in the Java library for the hashcodes is also due to the authors' gut feeling, as documented by https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622.