Generics is mainly a compiler feature. In java (at least the versions up to this moment, January 2021) it is even more so, because after the compiler had it’s go with it, it largely discards the generic information, and the runtime, the JVM, does not really care about generics and does not use it. The role of generics is to keep the programs type-safe. The compiler will check that a use of a method, field, or class complies with its definition, and if the use does not, the compiler will produce an error, to prevent the programmer from doing unsafe things.

1. Generics, the fine print

Generics were introduced in Java 5.

Generics help keeping the language safer by giving the compiler more ways to reason about the code and reject it when it is bad. That is what a strictly typed language is all about.

With generics also auto-boxing was introduced. The concept was borrowed from C# and the dot-net framework. Auto-boxing is auto-magically converting/wrapping an int into an Integer (auto-box) when a reference type is needed and backwards when the reference type Integer is given and an int is needed (auto-unboxing). This makes primitive types to play 'nicely' with generics to some degree.

Generics also helps avoiding code duplication, because you can have a something of anything like a List of Student without any code duplication or any code generation under the hood, to create specialized classes per anything type.[1] So this List exists once and is just as applicable to Student as is to Donkey. This applies to reference types, not yet to primitive types. That may come in a future version of Java under the banner of project valhalla .

A strong point of Java has always been the preservation of investment (in code). What worked once should still work. So code that was legal (and compiled) in the Java 1.1 era should still run, even if used today in its binary format. Heck, in closed source stuff that might be the only form you have, but your investment is preserved because of this backwards compatibility.

This strong point has a flip side. Pré Java 5 does not understand generics, which would break the backwards compatibility. That is why the generic info, after being used by the compiler for correctness verification is then thrown away by the same compiler. Little if anything of the generic information is preserved for the run time. As a matter of fact, the JVM has no notion of generics, nor does it need it to function. This has all kind of consequences.

In modern Java code, in particular since Java 8 with Functional interfaces and Streams, generics are a must, because the provide safety in the one hand and the required flexibility in the other, required to make the Java 8 features possible at all.

1.1. Raw types, pre-Java 5

The pre-java 5 days did not have generics. All types that have been generified since then still exist in their original, nowadays called raw form. From the above you can infer that this is what the JVM sees and uses.

Using a map in pre Java 5 times
   Map studentsMap = new HashMap(); (1)

   studentsMap.put( Integer.valueOf( 313581 ) ,new Student( "Potter","Harry" ) );

   // some time later
   Student harry = (Student) studentsMap.get(Integer.valueOf( 313581 ) ); (2)
1 The map has no clue about of the types of key or value, so Object is the best it can do.
Also note that you had to turn the int into an Integer object manually, because auto-boxing did not exist.
2 Retrieving the object from a map is again wrapping the int and also cast the object back to the expected type.

In the above pieces of code all kinds of things can go wrong. You could put any pair of objects of any type in the map and the compiler had no way of checking what you did made sense. In a Hogwarts narative: You might have put a benign wizard in your map, only to get haunted by a Dementor on retrieval.

1.2. Generic types, the modern way.

example lists and maps
   List<String> l1 = new ArrayList<>();
   List<Student> stuList = new ArrayList<>();
   Map<String,BiFunction<Double,Integer,Integer>> operatorMap;

The thing is that the compiler is very finicky. It should be, very precise and meticulous. As an example, say you have a simple person hierarchy: Person is extended by Student.

some regular people in lists.
class Person{}
class Student extends Person{}

// in some code
    void m(){
      Person paul = ....
      Student serena = ...
      List<Person> pl;
      List<Student> sl;

    }

The compiler will allow you to do pl.add(serena), and sl.add(serena) because serena is a Student and by extension a Person, but paul can’t be added to sl, because as far as the compiler is concerned, paul is not a Student and 'would not fit'.

What the compiler tries to achieve is that if you define a collection to contain students, then it promises that only students will go in and will ever come out. It does that by checking and not accepting any potential violation of the promise.

By sticking to this promise, the objects you can receive will be of the correct type and you can do what the type allows, like method calls etc.

Exercise Generic Box

Exercise 1: Box of things
All your classes must be in the package boxes

Before you learnt that generics exist, you had to get by. You needed classes that store information for you and let you retrieve it, too. So after a semester of Java, you have a bunch of classes in which you store stuff. For example, you have the class IntegerBox:

public class IntegerBox {
  private Integer value;

  public void putIntegerInBox(Integer anInt) {
    value = anInt;
  }
  public Integer getInteger() {
    return value;
  }
}

This little box class is pretty straight-forward. It has an Integer field at which you store the Integer that you put in the box, and you have a getter (getInteger) and a setter (putIntegerInBox) method to store and retrieve your Integers.

The trouble is, this box can’t deal with Double. Or String. Or anything that is not an Integer. So calling myBox.putIntegerInBox("Text") will not work. That means that for every new kind of variable, you need a new class. Quickly, your list of classes grows: IntegerBox, StringBox, DoubleBox, ListBox, StudentBox, BoxBox…​ this is very inefficient.

This is where you and Java generics come in:

  • Create a class named Box that can take any type using a type parameter.

  • Inside that class, have a private final field that stores the same any type.

  • Constructor that takes the same any type

  • Create getter method for the class named: get.

Test your implementation by using this main method.
public static void main(String[] args) {

   Box<Integer> iBox = new Box<Integer>(5);
   Box<String> sBox = new Box<String>("Some Text");


   System.out.println("My integerBox contains: "+ iBox.get());
   System.out.println("My stringBox contains: "+ sBox.get());
}

To make the exercise complete, also test and implement Box.equals(Object o)` and hashCode().

Next create a class named Pair:

  • that can take two different type parameters

  • store them in two separate fields (P and Q)

  • constructor that takes both types

  • getters for both types (getP and getQ)

  • Also test and implement equals and hashCode.

Test your implementation by using this main method.
public static void main(String[] args) {

   Pair<String,Integer> box = new Pair<>("Test", 4);


   System.out.println("My String contains: "+ box.getP());
   System.out.println("My Integer contains: "+ box.getQ());
}

1.3. Cheating the compiler

You can trick the compiler into breaking its promises. In the end, of course the compiler is not the victim, but the program is. It will generate a ClassCastException on retrieval of the non-fitting object. And as programs go when they generate uncaught exceptions: they will be aborted by the JVM.

Sneaky code. Do not do this at home. You are only fooling yourselves.
public static void main( String[] args ) {
        List<Student> sl = new ArrayList<Student>();
        sl.add( new Student( "Hermione" )); (1)
        List ol = (List) sl;                (2)
        ol.add( new Person("Voldemort") );  (3)

        // somewhere else in the scenario
        Student hermione = sl.get( 0 );       (4)
        Student lilly = sl.get( 1 );        (5)
        System.out.println( "lilly = " + lilly ); (6)
    }
1 accepted.
2 give the compiler a raw alias.
3 so it will gladly accept this. You cheated!
4 no sweat, our heroine pops out,
5 but the other object will make your program fail at this statement with a ClassCastException.
6 Statement never reached because your program will have been terminated by now.

1.4. Type token.

towerguard Sometimes you have a problem, which can only be solved by explicitly providing the generic information to the code at run-time. This use is called 'providing a type-token'.
An example of such use is the method Collections.checkedList( <List<E>, Class<E> type ),
whose usage example is List<Student> studList = Collections.checkedList( students, Student.class ).
The type token will play the role of a sentinel or guard, because all class object have a cast method that check if the cast is legal, and if it is, applies the cast to the incoming object; if not it will throw a ClassCastException. This will prevent even the sneakiest code, including code predating Java 5 and the code above, to sneak in an none-student object. This guard will also throw an exception, and actually the same one, but now at the side of the culprit, because the exception will be thrown at insertion. You should not do this as normal usage, because the check on every insertion goes with a cost (CPU cycles), but it can be helpful to detect problems caused by some naughty code, because now a violation against the promise will result in an exception at run-time, a ClassCastException to be exact at the moment of insertion, so any sneaky code can be caught red-handed, leaving a nice big stack trace as a trail for all to see.

There are other uses of type-tokens, which you may see later in the course.

2. Wildcard and bounds.

Read Horstmann section 8.8.

With the definition in the previous paragraphs, you are losing some flexibility, compared to raw types. In particular, you may want to insert (and retrieve) sub types of a class into or retrieve from a collection.

Using wildcards and bounds will give back some of the flexibility while still upholding type-safety. The wildcard has been given the character symbol '?', the question mark.

2.1. Unbound wildcard.

The unbound wildcard is something that should be used sparingly. The best example in the new definition of the Class type. In pre -5 times that was Class someClass that is a raw class, nowadays it is Class<?> someClass which actually sounds as a class of any kind. There is little difference but the compiler will be nicer to you. There are some corner cases where you can’t avoid it and then the form using the wildcard is the correct choice.

2.2. Upper bound or Covariant

acceptablesupers
Figure 2. Hierarchy of shapes, assume mathematical shapes, so a line has no width and hence no surface area. Nor does an arc.
Quiz: Can you name the super types of circle in the class diagram above?

Shape, Object, Serializable, ArcLike, Surface, and Circle itself.

When you intend to write code with high flexibility, in particular when you use inheritance as in extends and implements, then there are a few rules to apply to your generic code. It all has to do with what a container, defined with generic types should accept or will produce.

We will refer to the picture above. We have two scenarios:

  1. A class that makes or supplies shapes. Imagine reading the shape definitions from a drawing file with vector graphics.

  2. A class that should accept shapes, like a canvas on which you can draw.

The supplier or Producer as it is called in the documentation, could be some collection such as a list of any of the types in the class diagram: Object, Serializable (that is probably how the Objects were read from disk), but the most likely and useful would be the Shape type.

Passing things to be drawn to the canvas.
  interface Canvas {
    void draw( List<Shape> shapes ); (1)
  }
1 as far as the draw method is concerned, the list produces the shapes to be drawn.

Now imagine that you have a list of circles: List<Circle> circles = new ArrayList<>(). Then the above definition of the Canvas.draw() method forbids you to do canvas.draw( circles );, which is quite counter-intuitive, because the class diagram and the code says that a Circle is a Shape. The compiler will say: 'incompatible types: List<Circle> cannot be converted to List<Shape>'.

This is because, although a Circle is a sub-class of Shape, a List<Circle> is NOT a sub-class of List<Shape>. This is not a Java feature but a problem addressed with Type Theory which is beyond the topics of this course.
To amend this problem, you must change the definition of the draw method so that it accepts shapes but also any derived type. You do that by adding the wild-card which expresses anything (within limits or bounds).

Flexible shape that also accepts lists of sub-classes of Shape.
  interface Canvas {
    void draw( List<? extends Shape> shapes ); (1)
  }
1 This accepts Shape but also anything that is a sub type of Shape, so Circles, Triangles, Lines etc.

This makes the draw method more flexible and reusable, because the draw method can draw any shape now. Type theory calls this kind of relation between Shape and List of Shape Covariant. They both develop in the same direction, so semantically a List<? extends Shape> is covariant with Circle extends Shape. You could say, both extend down and therefor there is an upper bound, Shape in the example.

2.3. Lower bound or Contra variant

Now imagine that you have a method in some other class that wants to collect Shapes in a collection like a List. In the documentation such list, which should accept objects, would be called a Consumer.

Passing a container that should receive/consume collected shapes.
  class Collector {
    // objects are added to be added to receiver or consumer
    void collect( List<Circle> receiver ){ (1)
      Circle c =...
      receiver.add( c ); (2)
    }
  }
1 Too strict
2 Problematic line when widening 1 with extends.

Again, the compiler will complain if you call collect with your list of Circles as input. Use case: you want to tally up the total area covered by circles.

This time it says the same thing on the call statement collector.collect(circles). Let’s apply the same solution as above. If we define the collect method like void collect( List<? extends Circle> receiver ), the compiler will not complain on this line, but on the line receiver.add( c ); with a nasty curse:

Compiler complaint.
incompatible types: Circle cannot be converted to CAP#1
  where CAP#1 is a fresh type variable:
    CAP#1 extends Shape from capture of ? extends Shape

So extends is not the way to go. You could infer that the list should be defined such that it accepts the Circle but also any type that circle extends or, from the stand point of the circle, a super type of Circle. You are actually saying that circle and any of its super types will be acceptable to the receiver. Circle is the lower bound in the example.

Super, that does it.
class Collector {
    // objects are added to receiver
      void collect( List<? super Circle> receiver ){ (1)
        Circle c =...
        receiver.add( c ); (2)
      }
}
1 super here makes any List of any super type of Circle acceptable to
2 the add operation.

The List<? super Circle> in the above example is now contra variant with Circle.

2.4. Bounds explained.

The wild card construct with extends or super is called a bounded wild card.

  • With extends, which defines the upper-bound, the boundary is the top most type of the acceptable types, so that that type defines what can be done with it and all its derivatives, like draw().
    extends puts a upper bound on the element type.

  • With super, which defines the lower-bound, the bound is the bottom-most type of the acceptable types. It defines the receiver or consumer of the type. Because that lower bound is the sub-most type of the inheritance hierarchy it is it and all above it, which says that any consumer (receiver) of such super-type will accept the sub-most or lowest type but also any of the super types.
    super puts a bound on what a consumer like a collection, set or list should accept.

All this bounds stuff is about the direction of the operation, get or put. You can get from a Producer and put into a Consumer. The extends bound puts a constraint on the element that are to be produced, the super puts a constraint what the receiver or consumer of the element should accept.
There is a mnemonic for this: P.E.C.S., which says Producer, use Extends, Consumer use Super.
Note that put and get both come with other names. For instance, put is sometimes called add, push, or accept, according to the naming convention in its context. Same for get, which may answer to the name pull, pop, take, remove, etc.

Extends and covariance is easiest to understand. Contra-variance less so.
As an example for super, have a look at the Consumer functional interface. There is a default helper method given that specifies that the Consumer named after in the andThen(..) method must be of type Consumer<? super T>, saying that said consumer must accept Ts, but may also accept any super type of T. And again any consumer that accepts T or any of its super types will do. You will see this pattern throughout the java.util.function package where a default followup method such as andThen or compose is specified.

The last paragraph makes the point why Functional interfaces and all other goodies introduced in Java 8 heavily rely on generics.

Exercise Shapefilter

Shape Filter

Shape Filters The shapes project template is in your repository.

In package shapes.basic you have multiple shapes, among which are:

  • Disk which extends Circle. Disk contains an image.

  • Sphere which extends Disk. Sphere creates a 3D like image of a sphere or Ball.

The essential detail here is that Disk and Sphere are sub classes of Circle. There is no need to change anything in this package. Just check if you understand the inheritance hierarchy and the use of interfaces and abstract classes.

Shape filter This part is about generics. Remember the PECS acronym. In the ShapeManager test-driven develop two methods:

  • void addShapes(List<…​> shapesToAdd) Make sure to use the proper list definition with generics. The idea is that each list of shapes (e.g. a list of Circles, a list of Disks, a list of Shapes can be processed).

  • boolean collectShapesOfType( Collection<…​> consumer, Class<…​> type ) This method takes two arguments, (1) a list (like a list) in which you can put shapes and (2) the type of shape you want to filter. For example, you’re passing an empty list that is filled with all Circles.

The learning goal in the exercise is to get the generics write, almost a fill in the dots in the methods above. Of course the declaration must be correct on both call-site (where you call the method) and at the method site.

Using raw types is certainly wrong.

Generics and arrays, including varargs are not an optimal combination, because you can’t create generic arrays. When you pass a generic varargs array to a method, the compiler may generate a warning along the line of Possible heap pollution for vararg type Y for a method declared as static <Y extends Comparable<? super Y>> Y[] minmax( Y…​ a ). Only put a @SafeVarargs annotation on a method using varargs, after when you have ensured yourselves that the use of the generic parameter in the method does not throw a ClassCastException.

By the same token, don’t fool yourselves by putting @SuppressWarnings("unchecked") in all places where you see a warning flag saying you are using a raw type. Correct the use of the type or at least use the wildcard like List<?>.

3. Self use in Generic definitions

Generics is not only applicable for collections, although that aspect of the standard Java library is a big customer of this language feature. But it can be used in other places too, as you have seen in the functional interfaces which define the shape of lambda expressions.

You may have come across a generic type definition that looks a bit weird as in, the type is used in its own definition. An example of that is the definition of the Enum class, which the abstract super class of all enums:

Enum class definition, a bit of a mouth-full.
public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
}

This definition puts bounds or constraints on the type E, which must hence be a sub type (sub class) of Enum itself.

The type declaring keywords in Java are class, interface, and since Java 5 enum. Java 14 introduced record, which looks a lot like enum in the technique of its definition. It makes an enum and a record final, but they can implement interfaces.

Another one is in the use of Comparable, like in the String class, it says among others String implements Comparable<String>, to make Strings comparable to each other, but not to other types. Again a bound or constraint of types, in the example the parameter of the int compareTo(T) method declared in Comparable.

Let us make a simple and silly but instructive example: A zoo. We want it to be such that a class inherits all the traits of its super, and we want to be able to use it in a fluent style of programming.

zoo
Figure 3. Mini zoo

In our programs we want to express things like donald.fly().swim().walk(), which is a common sequence of operations for ducks. We want each of the methods in the chain to return the duck, so that Donald can do his operations in any order he likes.

This is also understood by the IDE, which will show completions fitting for the duck when the object is a Duck. For the Bird class on the other hand, the IDE will not complete with swim(), and for the Penguin not with fly().

To make this work, you need to make your classes as you could say self-aware. It means that the class in the hierarchy defines a method called self() whose return type is that of the class, not one of the super types. The self-use in the generics declaration makes this work, in combination with the fact that each leaf class [2] has a self() method which, through convincing the compiler with generics, returns an object of the correct type. In the definition of each leaf class, the leaf class itself is the type parameter, like in Penguin implements Bird<Penguin>, Swimmer<Penguin> etc. In particular, it is not parameterized any further such as Penguin<P>, because they themselves are the desired final type or leaf types.

If you want a non-leaf (abstract) class or interface, your have to further use the self reference in the generic definition.

EBird is an extendable Bird
public interface EBird<EB extends EBird<EB>>  extends Flyer<EB>, Walker<EB>{

     EB afterBurner(){
         System.out.println("VROOOAAAR");
         return self();
     }
}
Thunderbird is it’s child.
public class ThunderBird implements EBird<ThunderBird>{
    public static void main( String[] args ) {
        ThunderBird tb = new ThunderBird();
        tb.fly().move().walk().fly().brood().fly().afterBurner();
    }
}
LeafFarket A leaf class is a concrete class at the bottom of the type hierarchy, which is no longer generic on self. In the current implementation you can’t extend it easily. In java enum and record types are also all leaf classes, that cannot be extended.

The code below shows what all this looks like for Bird (providing self), Flyer, and Duck:

Animals can move and are self aware.
package zoo;

public abstract interface Animal<A extends Animal<A>> {

    default A move() {
        System.out.println( "twitch " );
        return self();
    }

    default A brood() {
        System.out.println( "cosy with my offspring " );
        return self();
    }

    // makes object self aware.
    default A self() {
        return (A) this;
    }

}

The common trait of animals is that they can move. It is in the name. Even coral can move, if not very far.

Flyer adds flying functionality.
package zoo;

public interface Flyer<F extends Flyer<F>> extends Animal<F> {

    default F fly() {
        System.out.println( "swoosh" );
        return self();
    }
}
Ducks are Animals with many traits, all inherited, and little work. You might think ducks are lazy. 🦆 😊
package zoo;

public class Duck implements  Flyer<Duck>, Swimmer<Duck>, Walker<Duck> {

}
you know it is just a model, think movies …​

duckling The Duck class above is the complete concrete leaf class, nothing has been left out. Everything is in the supers (the interfaces in this case), generics is just the thread that stitches the Duck together.

This all provides the most benefit in Java 8 and up, because from Java 8 interfaces can have default and static methods, thereby implementing things, based on the abstract methods the implementing class must provide, as is the case with self(). And even self() is generified out by way of convincing the compiler that the default implementation does the trick. Minimal code with maximum effect.

The techniques described here are heavily used in the AssertJ library. The details of this SELF use can be found here .

4. Testing generics

As explained, generics is mainly used at the source code level and compiler to check the program for correct use of the declared types and methods. As such, testing of the generic aspect of your code is done when your compiler is happy with it AND you have expressed your real intent. Sometimes the last part is the trickiest.

On the other hand generics allows you to create a super type, ( super class or interface ), that pulls most of the work, as the little zoo example already hinted towards.

make it super

Superman shield In the zoo example you go from the abstract (super type animal) and interfaces (Flyer and the lot) to the concrete Penguin and Duck. For a student that studies Software Design, in particular Design Patterns, that smells like a potential application of the Abstract Factory Pattern. Indeed, the case of testing is one.

If you are designing such a super type with concrete methods (in the abstract class or default or static methods in the interface) you will want to develop this in a super test driven way. Always remember that the most important rule in TDD is tiny steps, to prevent falling off the track.

You don’t need a (public) constructor.
You will notice that the IntegerRange and LongRange classes in the exercise below have no public constructor. That is fine. The style is similar to what LocalDate and friends in the java.time package do. It can be quite useful to not have a public constructor.

  • For one, the static factory methods can have names, which allow you to choose something meaningful, if this factory is special. Or it can have a short name, as you have seen in the Fraction exercise. Or 'of' in the example classes and in e.g. LocalDate.

  • It also provides more control over the created instances.

  • It prevents subclassing, if the only constructors you have are private ones.

To support the case: You will see that the public constructors of the Wrapper classes such as Integer and Long are already deprecated since Java 9 and are about to be removed in some future version of Java, as is evident for the @Deprecated annotations since java 16. That could very well be in Java 17. We’ll see.

from the java.lang.Integer source, i.e. Java 16
    /**
     * Constructs a newly allocated {@code Integer} object that
     * represents the specified {@code int} value.
     *
     * @param   value   the value to be represented by the
     *                  {@code Integer} object.
     *
     * @deprecated
     * It is rarely appropriate to use this constructor. The static factory
     * {@link #valueOf(int)} is generally a better choice, as it is
     * likely to yield significantly better space and time performance.
     */
    @Deprecated(since="9", forRemoval = true) ) (1)
    public Integer(int value) {
        this.value = value;
    }
1 Announced forRemoval since java 16.

Exercise Generic Ranges part 1

Generic Ranges Part 1

A range is a mathematical concept with quite a few applications in software systems.

  • Availability of a person or resource according to some agenda.

  • Booking resources like rooms in hotels or renting cars in a holiday.

  • Computing prices of an arrangement or rent when it crosses different price-zones, like top-season or low-season.

  • Computer memory inside the computer is a resource to be shared. It also can’t overlap in the normal case, in particular not for data segments that are written by a program

The examples are often related to date or time, and are the most common use. In the physical 3D world, the simple 1 dimensional linear model of ranges will not always be sufficient.

A range is some segment between two points. These points could be points on a number line or on a timeline. Initially it looks like a simple concept, in particular a number line of say long, but in most physical cases, the dimension of a point differ substantially of that of the distance between two points. To make the case, you will find it natural to express a point in time as 1995-Aug-12 but you would wonder what exactly 6 * 25 August would mean.

This aspect is modeled in the Range interface, the type of the Points is P, the dimension between them with D. The mnemonic way to remember is P for point in time, D for Distance. The Range get the letter R.

An example of an (important) operation is expressed as a Functional interface called MeasureBetween, which provides the shape of a lambda expression. (P a, P b) → D, which says that the method computes the distance D between two Points a and b.

range classes
Figure 4. Class diagram of the ranges project

Operations.

  • Compute the length of a Range as type Distance.

  • Compute the distance between two points.

  • Join and union to join two ranges producing a new Range.

  • Intersection of two ranges, returning the overlap as Range.

  • Divide into parts, such that a range is chopped up into 1, 2 or three parts (Ranges), dependent on the relative position of the operands in the operation. Useful to make reservations from available date or time ranges.

All tests use a limited set of test values, basically just points, a, b, c, d, e, and f. These are the same point as used in the diagrams. A requirement to those points is that they are in ascending order a < b < c < d < e < f.

The ranges project is meant as a reusable library to define some ranges and operations on ranges. The most important operation in this case is to compute the overlap between to ranges.

rangesoperations
Figure 5. Operations between ranges

The contained case is interesting one, because it can be used to determine if an availability matches a request. For instance you want a 3 day holiday and the rooms that you can book must have a free range that contains your request. If you then book, the free range is then cut up into a (optional) free part before, your booking, and an (optional) free part after.

rangecutter
Figure 6. The cutter cuts red and blue and takes green as a whole

The intersection operation in the ranges package is called Range.intersectWith( Range other ) and is defined and implemented in the Range interface as a default method. Because the Range interface is defined as a Generic type, the implementing classes can use the intersectWith method without change.

The same applies to methods boolean overlaps(Range other) and D overlap(Range other) which test if there is an overlap and measure the length of such overlap.

The Range intersectWith(Range other) can be used to find conflicting ranges, but the information you gain by this method is typically the same as boolean overlaps(Range other). In a planning scenario you typically want to avoid overlaps. You would not like to find other guests plundering your minibar.

The final operation, D overlap(R other) can be used to compute prices. Use case: prices vary per season, and the length of an overlap of a reservation with seasons determines the price of the parts.

Completely in line with the Duck example in the theory, we want maximum reuse, or as much as possible work done in the Range super type, so that a specialization has little or no work to do.

Let us start simple and show what an integer range could look like when we get the design done. The IntegerRange is also the first leaf class in our design.

IntegerRange is a Range of Integer
public class IntegerRange implements Range<IntegerRange, Integer, Integer> {

    final Integer start;
    final Integer end;

    private IntegerRange( Integer[] p ) { (1)
        this.start = p[ 0 ];
        this.end = p[ 1 ];
    }

    @Override
    public Integer start() {
        return start;
    }

    @Override
    public Integer end() {
        return end;
    }

    @Override
    public BiFunction<Integer, Integer, Integer> meter() { (2)
        return ( a, b ) -> b - a;
    }

    @Override
    public int hashCode() {
        return rangeHashCode();
    }

    @Override
    @SuppressWarnings( "EqualsWhichDoesntCheckParameterClass" )
    public boolean equals( Object obj ) { (3)
        return rangeEquals( obj );
    }

    @Override
    public String toString() {
        return rangeToString();
    }

    @Override
    public IntegerRange between( Integer start, Integer end ) { (4)
        return IntegerRange.of( start, end );
    }

    public static of(Integer start, Integer end) {
        return new IntegerRange(  Range.minmax( start, end ) ); (5)
    }
}
1 Private helper constructor that takes an array of two T’s
2 This method returns a function in the form of a lambda expression
3 The interface defines helper methods for equals and hashcode. Because you cannot overwrite methods of a class in an interface, we use this trick to avoid duplicate work or copy-and-waste-errors.
4 Factory helper that can be used by the default methods in the Range interface
5 Using helper method that ensures that parameters are passed in correct order.
IntegerRange as Java 16+ feature, less boilerplate.
public record IntegerRange( Integer start, Integer end) implements
        Range<IntegerRange, Integer, Integer> {

    @Override
    public BiFunction<Integer, Integer, Integer> meter() {
        return ( a, b ) -> b - a;
    }

    @Override
    public IntegerRange between( Integer start, Integer end ) {
        return new IntegerRange( start, end );
    }

    @Override
    public Integer zero() {
        return 0;
    }
}

When you want to enable java preview features with projects that use testeasypom, set the java.release to the version you want to use, and add a build section to tell the compiler to turn on the preview-features.

   <java.release>16</java.release>
.
.

  <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerArguments>--enable-preview</compilerArguments>
                    <compilerArgs>
                        <arg>--enable-preview</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
  </build>
The Range interface class header
/**
 * Range with start and end and some operations.
 *
 * @param <R> self referencing range type.
 * @param <P> the demarcation type of start and end point.
 * @param <D> the distance Dimension between start and end points.
 */
 public interface Range<R extends Range<R,P,D>,
                        P extends Comparable<? super P>,
                        D extends Comparable<? super D>
                        >
        extends Comparable<R>, Serializable {
// code omitted
}

The interface definition may look a bit daunting but it reads as:
a range which has subtypes R, and has two Comparable endpoints of type P, with distance of type D and is itself Comparable with something of it’s own type [blue]`R`. The comparison is done by comparing the starting boundaries, a and c in the drawings above.

The type parameters are a bit special, because generic ranges may have endpoints of a different type than the distance between them. Compare to two locations on earth. The location is expressed as lat-long which is a two-dimensional value, but the distance is expressed as a length in meters, kilometers, or miles.
Same for time and dates. A date is a position on some scale with an arbitrary origin, like the Gregorian Calendar, but the 'distance' is not of type date, but of some kind of time-duration such or Period of Duration. If you ask a Duration its 'length', you need to specify the unit, such as ChronoUnit.DAYS, ChronoUnit.HOURS or ChronoUnit.NANOS.

The definition of the leaf class LocalDateTimeRange therefore looks like this:

public class LocalDateTimeRange implements Range<LocalDateRange, LocalDateTime, Duration> {
  // code omitted
}

The ChronoUnit allows the range to be measured in any of the unit types defined in that enum, from nano seconds to centuries, with days somewhere in between. Not all units are applicable all of the time. HOURS and DAYS are precise, but MONTHS and YEARS are not, because they vary in length.

The interesting or special methods defined in the interface may also need some explanation.

Between. Note that a is included, b is excluded in the range a-b
 /**
   * Is a midpoint between a (inclusive) and b (exclusive).
   *
   * @param a first point
   * @param b second
   * @param inBetween point
   * @return true if inBetween is at or after a and strictly before b.
   */
  default boolean isBetween( P a, P b, P inBetween ) {
      return a.compareTo( inBetween ) <= 0 && inBetween.compareTo( b ) < 0;
  }
Do two ranges have points in common?
/**
 * Does this range overlap with another one.
 *
 * @param other to check
 * @return true on overlap with other
 */
default boolean overlaps( R other ) {
    // code left as exercise
}
The getLength() method evaluates the method returned by the meter() method implemented in the above class.
/**
 * Get the length of this range in some unit. The effective operation is
 * (end - start), but since we do not know how to subtract the sub-types,
 * that is left to the implementer. The exception thrown on incompatibility
 * of range and unit is also left to the implementer.
 *
 * @param unit of measurement
 * @return the length
 * @throws RuntimeException when the unit and this range are not compatible
 */
default long getLength( D unit ) {
    return meter().apply( this.start(), this.end() );
}

The default method above uses the abstract method below.

The measurement device is a BiFunction<P,P,D> that is applied in the above method.
    /**
     * Return the 'device' to measure the distance between two points Compute the distance in dimension D.
     * The implementer has the choice to return a class, lambda expression, or a (static) method reference.
     * The lambda has two inputs of type P: a and b.
     *
     * @return the distance meter lambda or BiFunction Implementation.
     */
    BiFunction<P,P,D> meter();
}
One of methods that makes the Range interface and its sub-classes worth their keep.
/**
 * Compute the length that this and an other range overlap.
 *
 * @param other range
 * @param unit of measurement
 * @return the length of the overlap as a number of 'long' units
 */
default D overlap( R other ) {
   // sorry, this is your work. And 0 is wrong most of the time.
}
punchthrough
Figure 7. The punch only punches if it has a full hit
The punching on test stage as shown in figure The punch only punches if it has a full hit
   Stream<Range<R, T, U>> result = range.punchThrough( punch ); (1)
   assertThat( result )
        .as( "punch " + range + " with " + punch )
        .containsExactlyElementsOf( expectedParts );
1 Note that the punch operation takes a single Range punch but produces a Stream that contains
  • Either the 'green' range as a whole, in case the punch 'missed', or more formally is not completely contained in the target,

  • part(s) of the green range and the punch as a whole,

  • or only the punch if it completely knocks out and thereby replaces the green range. In this last case the punch and target completely overlap.

From the above you can infer that the resulting Stream from the punchThrough operation has at least one (1) element and at most three (3).

Whenever a method produces a Stream from an element of a Stream with a single operation, flatMap is your friend.

Parameterized Tests
Many of the tests in the Ranges project are parameterized. Test driven in this instance means adding a row of test data to the data source, or un-comment a row.

In this first part you need to complete the Range and RangeTest in the given project using the IntegerRange as leaf class.

Range types expected at the end of this part:

Range Point Distance

IntegerRange

Integer

Integer

5. Design for Extension and Reusing Generics Tests.

Initial test situation

rangetest The Range interface can be nicely implemented into leaf classes such as IntegerRange, which you already implemented in the exercise above. It can also be implemented by time based ranges such as LocalDateRange and LocalDateTimeRange, and InstantRange. The first two may sound familiar, but actually the third is the precise one. The other two deal with the quirks of daylight saving time, which is a political invention to make our getting out of bed early in April hard.
You invested a lot into the RangesTest test classes, with IntegerRange as the first concrete of leaf class on the Range type tree. Now we want to preserve the investment. The trick is to again use generics and 'leaf' classes, but now on the testing side of the project.

The idea is in the class diagram below.

generictestclasses
Figure 8. Generics on both sides of the business/test divide.

The cyan leafs and the green leaf classes are the pairs of business and test classes. We kept the data provider and the test class as separate classes, because we think the responsibilities are different. However, an approach where the test-leaf-class provides the data would be just as valid.

You are going do a little bit of copying, starting from the RangeTest class.

The approach will take a little perseverance, because before you can make your code better, you have to break some.

We want to turn a concrete test class, with concrete data, testing against a leaf class, into a generic test class. We start by copying the class, RangeTest to RangeTestBase. Because the type name no longer ends in Test, it is no Longer considered a test class by itself. It will serve as the root or super of a set of leaf test-classes. These leave’s names do end in Test, but will do only minimal configuration. A bit like the duck earlier in this part. They will also make explicit what kind of Range they test by using concrete classes at the places of the type parameters. As example IntegerRangTest is declared as public class IntegerRangeTest extends RangeTestBase<IntegerRange, Integer, Integer>.

Readability is key. That also applies to tests and test data. We use a, b, c, and d in the images and therefor want to stick to those same "names" in the tests, so that a test value record "ab,cd" is easily understood as two disjunct ranges and "ac,bd" as two overlapping ones. That would be hard with array indices. Our first approach was to use a map to do the translation, but creating the map is a lot of typing even with Map.of(…​). So we use an array of test data and index it with this trick, where the key is a String, in which only the first character is significant.

    */
    Integer getPoint( String key ) {
        return pointsA[ key.charAt( 0 ) - 'a' ]; (1)
    }
1 We know that the keys will be adjacent and start with a, so character -'a' produces the integer index into the array.

In the first part of the Generic Range exercise all tests use a limited set of test values, basically just points, a, b, c, d, e, and f. The only requirement to those points is that they are in ascending order a < b < c < d < e < f. In RangeTestBase we replace the use of concrete test values A, B, C, etc. into method calls on some data generic factory, but instantiated for our concrete test case, Integer. The Generic Helper abstract class RangeTestDataFactory has methods to lookup points and to create ranges from String specifications. one method to create a range: R createRange( P start, P end );.

The test-leaf, that is the IntegerRangeTest creates its helper specialization, the RangeTestDataFactory, as an anonymous inner class, just like in the class diagram above.

For IntegerRangeTest it and it’s inner class looks like the class below. You only see trivial methods that configure the RangeTestDataFactory, which is the actual name of the Generic Helper class.

IntegerRangeTest class, the whole of it.
package io.github.sebivenlo.ranges;

import java.util.Map;
public class IntegerRangeTest extends RangeTestBase<IntegerRange, Integer, Integer> {

    RangeTestDataFactory<IntegerRange, Integer, Integer> daf; (1)
    Integer[] points={ 42, 51, 55, 1023, 1610, 2840  };

    @Override
    RangeTestDataFactory<IntegerRange, Integer, Integer> helper() {
        if ( null == daf ) {                                  (2)
            daf = new RangeTestDataFactory<>( points ) { (3)
                @Override
                IntegerRange createRange( Integer start, Integer end ) {
                    return IntegerRange.of( start, end );
                }
                @Override
                Integer distance( Integer a, Integer b ) {
                    return b - a;
                }
            };
        }
        return daf;                                           (4)
    }
}
1 Cache instance.
2 Test if we have a cached value.
3 Start of anonymous inner class.
4 Return the cache.

In the above code we cache the result of the first invocation of helper(), to avoid creating a new instance on each call. It speeds things up a bit and is less wasteful on object allocation.

Exercise Generic Ranges 2: Adding tests and Leafs

Generic Ranges Part 2

Plan We are going to refactor our tests, so that we get tests for new leaf classes almost for free. See the explanation in the part above.

The idea is that you separate the test data from the tests. Doing this in a generic way allows you to specify the tests in terms of the generic types, R, P, and D. Let this be mnemonic for Range between Points at a Distance.

There will a test base class RangeTestBase, generic with the type tokens R,P and D. The Leaf-Test class then only has to provide a test data provider or factory. The RangeTestBase class defines a helper() method that returns an instance of a RangeTestDataFactory, that has to be configured for the specific test leaf class.

The test data factory is of course defined as a generic type, an abstract class to be precise.

To do that follow the plan described in the text above:

First define the RangeTestDataFactory abstract class. Actually, it is already in the project. This is an abstract class, because that allows us to 'pull' the test helper methods into this factory, so that the data massaging to get the types right can be done in that class. The important factory methods are createRange, and distance, both abstract. The factory also expects a map of String to P: Map<String,P>, that allows it to create points, ranges, and distances with the given abstract methods, so that it can produce the test values that are required (and actually already are) in the tests. Remember we started by copying the complete test class for the IntegerRange as a starting point.

Important members of RangeTestDataFactory.
public abstract class RangeTestDataFactory<R,P,D > { (1)

    protected final p[] points;  (2)

    /**
     * Remember the type of the class under test.
     *
     */
    protected RangeTestDataFactory( P[] points ) {
        this.points = points;
    }

    /**
     * Lookup up a point from the spec.
     * @param key for lookup
     * @return the mapped value.
     */
    P lookupPoint( String key ) {
        return points.get( key );
    }

    /**
     * Create a test range,`P a()`,
     *
     * @param start or range
     * @param end   of range
     * @return the range
     */
    abstract R createRange( P start, P end );   (3)

    abstract D distance(P a, P b);              (4)
    // remainder left out
}
1 Simple type tokens are enough.
2 Point lookup, to be provided by leaf configuration.
3 and
4 helper methods to be provided by the factory leaf (inner) class.

The factory leaf classes have to implement the abstract methods and pass a map of points via the constructor. In all classes they have been implemented using an anonymous inner class. For LocalDateRange that inner class definition looks like the code below, the other inner classes look similar enough. Again you see that the abstract methods are trivially implemented. The construction used is very much like the typical way an Iterable<T> class returns the requited Iterator<T>.

We have chosen Duration as the type for distance with LocalDate instead of the more 'natural' Period, because we think it is useful to have the demarcation points of a range to be comparable, so you can tell early and late apart. Period is NOT comparable, because it cannot keep that promise in all cases, because of the invention of daylight saving, where you loose and hour of sleep in Spring and gain one when you don’t need it in the autumn. That’s politics for you. And even without daylight saving, the idea of leap year and leap day also throw some fly in the ointment.
LocalDataRangeTest inner class extending RangeTestFactory
   RangeTestDataFactory<LocalDate, Duration, LocalDateRange> daf;

   static final ChronoUnit UNIT = ChronoUnit.DAYS;

   static final LocalDate A = LocalDate.EPOCH.plus( 12, UNIT );

   static Map<String, LocalDate> dateMap = Map.of(     (1)
           "a", A,
           "b", A.plus( 5, UNIT ),
           "c", A.plus( 7, UNIT ),
           "d", A.plus( 14, UNIT ),
           "e", A.plus( 21, UNIT ),
           "f", A.plus( 28, UNIT )
   );

   @Override
   RangeTestDataFactory< LocalDateRange, LocalDate, Duration> helper() {
       if ( null == daf ) {
           daf = new RangeTestDataFactory<>( dateMap ) { (2)
               @Override
               LocalDateRange createRange( LocalDate start, LocalDate end ) {
                   return LocalDateRange.of( start, end );
               }
               @Override
               Duration distance( LocalDate a, LocalDate b ) {
                   return Duration.between( a, b );
               }
           };
       }
       return daf;
   }
   // public static void main( String[] args ) {
   //      new LocalDataRangeTest(); (3)
   //  }
1 Data set for LocalDateRangeTest.
2 Start of anonymous inner class defining Test Factory leaf class, at the same time Passing the map to the super class, i.e. RangeTestDataFactory.
3 Main that helped to show what went wrong.

In the test data you see the use of a ChronoUnit. I have run into trouble when I selected some value that looked useful, like ChronoUnit.YEARS, but was called back from that choice by the Runtime, because not all values defined in ChronoUnit are allowed in all cases. Because the error was covered by a lot of testing output, I added a tiny main method, which you can see commented out above. That revealed that Duration choked on the wrong choice of ChronoUnit. That is a tip in disguise: You can simply add a main to a test class, to see what happens when you run it as a normal class.

That is why I choose DAYS with LocalDate ranges, and HOURS with LocalDateTimeRanges. For instance YEARS does not work.

Your task:

  • Copy your class RangeTest to RangeTestBase, and make it generic by adding the following declaration:

public abstract class RangeTestBase<
              R extends Range<R, P, D>,
              P extends Comparable<? super P>,
              D extends Comparable<? super D>
              >
               {
                // class left out
}
  • Give RangeTestBase one abstract method abstract RangeTestDataFactory<R, P, D> helper();, that returns a data factory.

  • Adapt all tests so that any use of Integer as Point is replaced by generic P, any use of Integer as distance by generic D, and any IntegerRange by the generic R.

  • Create an IntegerRangeTest as given above. It is a leaf class of RangeTestBase and only configures the helper with the anonymous inner class Implementation of the RangeTestDataFactory.

  • Now make sure that the test results are the same as before with the simple RangeTest.
    You may want to disable the whole RangeTest class by putting an @Disabled annotation on the class, so that you can see only one class doing its work.
    In this way you are effectively testing the new test. You can of course also inspect the code coverage, to see if indeed all code of the target business class is put through it’s paces.

Next you apply this to create more range leaf classes. Start with LongRange: public class LongRange implements Range<LongRange, Long, Long>.

First create the test class: LongRangeTest. Mold it after the pattern in IntegerRangeTest. Copy and replace Integer with Long works in most cases.
Then implement LongRange. Here too most is prescribed by the Range interface.

  • Implement the two fields as final, add a private constructor that takes an array of long and uses the first two values,

  • create the public static of( Long start, long end ) method and in it use the private constructor like so: return new LongRange( Range.minmax( start, end ) );.
    The static helper in Range P[] Range.minmax(P,P) returns an array with two elements, minimum value first, to help ensure that the least value lands at the start of a Range.

  • Implement the methods that are defined abstract in Range. They might already have been generated by the IDE, with bodies throwing a UnsupportedOperationException. Replace the body with the correct return expression. All abstract methods are non-void.

  • Override the methods equals, hashCode and toString inherited from Object. You can’t override a method of a concrete class in an interface, you must do it here.
    But most work is already done in Range, because these three methods only use the fields. For all said methods there is a equivalent rangeXXX, which you can use to do the work. In case of equals, do it like this:

    @Override
    @SuppressWarnings( "EqualsWhichDoesntCheckParameterClass" )
    public boolean equals( Object obj ) {
        return rangeEquals( obj );
    }

Make sure everything still works for both classes and their tests.

Then continue with:

  • Instant (point in time). For 'distance' use Duration.

  • LocalDateRange. For 'distance' use Duration.

    • Test for a getDays() method that tells the length in days. Implement getDays.

    • Test for an isBefore(LocalDate) method, which can tell if the whole range is before a date. Implement isBefore.
      Similar for isAfter.

  • LocalDateTimeRange. For 'distance' use Duration.

    • Add isBefore and isAfter similar to LocalDateRange.

You get 67 tests per implemented range, maybe a few more if the range has some specialized needs, like LocalDateRange. Do not forget to override the methods equals, hashCode, and toString, or some of the tests will fail.

More useful ranges to come.

  • DoubleRange. For 'distance' use Double.

  • BigIntegerRange. For 'distance' use BigInteger.

  • BigDecimalRange. For 'distance' use BigDecimal.

    • For BigDecimal you may run into the issue that the overlap test method tells that 0 and 0E27 (ZERO at some large exponent) are not equal. You can either suppress the test by overriding it in the leaf test class and effectively silence it by commenting out the super call, or, better, improving the distance method in the test leaf class and the meter method in the BigIntegerRange class. The later will then look like:

if two points are equal, the return ZERO distance.
    @Override
    public BiFunction<BigDecimal, BigDecimal, BigDecimal> meter() {
        return ( a, b ) -> {
            if ( a.equals( b ) ) {
                return BigDecimal.ZERO;
            }
            return b.subtract( a );
        };
    }

If you still not satisfied, find other comparable 'points', define their distance function and implement those too. Tests first of course.

Our version gives us 486 successful tests with a coverage of 100%. YMMV.

Range types expected at the end of this part:

Range Point Distance

LongRange

Long

Long

InstantRange

Instant

Duration

LocalDateRange

LocalDate

Duration

LocalDateTimeRange

LocalDateTime

Duration

By completing the above exercise you will have learned that there can be reuse in tests as well, you only need to know what tricks to apply.

More useful ranges to come, but first back to some explanation.

Reading

  • Horstmann, Ed 11, chapter 8, Generic Programming section 8.1-8.8

Slides

[TIP] The generic ranges exercise had a third part, but that has been dropped and may be part of a future week.

Exercises in this part


1. actually a specialized type is the C++ approach, not better or worse, but different
2. those at the bottom in the class diagram tree