Skip to content

Instantly share code, notes, and snippets.

@manoelcampos
Last active December 22, 2020 13:09
Show Gist options
  • Save manoelcampos/c7e36473fdf1257dc4587dadb93200ce to your computer and use it in GitHub Desktop.
Save manoelcampos/c7e36473fdf1257dc4587dadb93200ce to your computer and use it in GitHub Desktop.
An example showing that it is not possible to implement a generic Comparable class that extends a super class and also ensures a clear code that is type safe and avoids surprising runtime typecast errors.
/**
* Example that shows <b>THE IMPOSSIBILITY</b> to implement {@link Comparable} for a given class
* and subclasses of it, ensuring that the {@code compareTo}
* method will receive an attribute of the current class where it is being
* implemented and not just a object from the super class, and yet providing a clear code.
* <b>Considering the classes {@link Foo} and {@link Bar} (this one a subclass from {@link Foo}).
* Thus, a class {@link Foo} must be compared just to {@link Foo} objects
* (that includes {@link Bar} objects) and a class {@link Bar} must be compared
* just to {@link Bar} objects.</b>
*
* <p>This example implements the classes {@link Foo} and {@link Bar} that are
* generic class just to enable the generic implementation of the {@code compareTo} method.
* The classes in fact can be used ignoring the generic type that is used just internally.
* However, using a generic class without providing a generic type will
* raise a warning such as "unchecked call to compareTo(T) as a member of the raw type"
* when a method that uses the generic type is called (such as the {@code compareTo(T)}).</p>
*
* <p>As can be seen <a href="http://www.angelikalanger.com/GenericsFAQ/FAQSections/ProgrammingIdioms.html">here</a>,
* there are unsafe situations such as when we declare a {@link List} that has to receive just
* Integers but we don't specify the generic type. By this way,
* the List will accept any value. However, other situations
* as the {@code compareTo} method can be sometimes safe.
* In this case, the @SuppressWarnings("unchecked") annotation
* can be used. Despite of that, consider a scenario
* in which a subclass such as {@link Bar} uses generics
* to define the type for the {@code compareTo} parameter.
* When the generic type is not defined
* when declaring an object from {@link Bar}, the compiler will not check
* if a {@link Bar} object is being passed to {@code compareTo}
* or not. It just ensures that a {@link Foo} object is passed,
* despite the {@link Bar#compareTo(Bar)} requires a {@link Bar}.</p>
*
* <p>When the generic type is not informed, the generic methods will
* automatically accept any object defined by the generic type
* bounds. In this example, the class {@link Foo} defines the upper bound
* to be {@link Foo}. By this way a {@link Foo} or any object from its subclasses
* can be passed. The class {@link Bar} works in the same way, the upper
* bound is {@link Bar}.</p>
*
* <p>However several use cases work perfectly, some situations
* will raise runtime exceptions, as you will see if you run this example.
* See <a href="http://www.javapractices.com/topic/TopicAction.do?Id=10">this article</a> for more details.
* </p>
*
* <p>Finally, the only way to implement a generic {@link Comparable} class that
* inherits from other {@link Comparable} class is using composition instead
* of inheritance or always defining the generic type when declaring the objects.
* This last solution may be completely weird and confusing. For instance, the declaration
* {@code Bar<Bar> myBar} will work perfectly, accepting only {@link Bar}
* objects for the {@link Bar#compareTo(Bar)} method, but it is more
* verbose and everyone could ask why there is a generic type {@link Bar}
* for the {@link Bar} class. Why on the Earth does the class have a generic type that is itself?
* It is strange and confusing.</p>
*
* @author Manoel Campos da Silva Filho
*/
@SuppressWarnings("unchecked")
public class GenericComparableInheritanceProblem {
public static void main(String args[]){
/*Notice that the classes Foo e Bar have a generic type,
but they aern't being defined here and they don't have to.
The generic type is just used internally by the compareTo method.*/
Foo foo1 = new Foo(1);
Foo foo2 = new Foo(2);
System.out.println(foo1+" previous id: " + foo1.previousId());
System.out.println(foo2+" previous id: " + foo2.previousId());
System.out.println(foo1+" compared to "+foo1+" based on id: " + foo1.compareTo(foo1));
System.out.println(foo1+" compared to "+foo2+" based on id: " + foo1.compareTo(foo2));
System.out.println(foo2+" compared to "+foo1+" based on id: " + foo2.compareTo(foo1));
System.out.println("");
Bar bar1 = new Bar(1, "OneRawBar");
Bar bar2 = new Bar(2, "AnotherRawBar");
System.out.println(bar1 + " next id: " + bar1.nextId());
System.out.println(bar2 + " next id: " + bar2.nextId());
System.out.println(bar1 + " compared to "+bar1+" based on name: " + bar1.compareTo(bar1));
System.out.println(bar1 + " compared to "+bar2+" based on name: " + bar1.compareTo(bar2));
System.out.println(bar2 + " compared to "+bar1+" based on name: " + bar2.compareTo(bar1));
System.out.println(foo2 + " compared to "+bar1+": " + foo2.compareTo(bar1));
System.out.println();
System.out.flush();
//Compile error due to completely incompatible type. Uncoment and you see.
//foo1.compareTo("This is a String, not a Foo object"); //this line doesn't compile
try{
/*
Runtime error because rawFoo1 is not from class Bar, but it compiles
anyway (with warning).
The only way to be safe is defining the generic type when declaring the object.
See the code after this one.
*/
bar1.compareTo(foo1);
} catch(ClassCastException e){
System.err.printf("Error trying to compare a Bar to a Foo: %s\n\n", e);
}
/*
The compareTo at the code below does not compile because
now the generic type of genericBar1 object was explicitly declared.
By this way, it just accepts comparison with other Bar objects.
However, it is strange to define a generic type Bar for the class Bar.
I don't think anyone will do that.
*/
Bar<Bar> bar3 = new Bar<>(3, "OneReallyGenericBar");
//bar3.compareTo(foo1); //this line doesn't compile
Bar<Bar> bar4 = new Bar<>(4, "AnotherReallyGenericBar");
System.out.println(bar3 + " compared to "+bar4+": " + bar3.compareTo(bar4));
}
}
/**
* A class that implements {@link Comparable} that will be the base
* for implementation of other subclasses such as {@link Foo}.
*
* @param <T> the generic type that defines the class of objects
* that can be compared to, allowing subclasses of this class
* to define different types subtypes for T.
*
* @author Manoel Campos da Silva Filho
*/
class Foo<T extends Foo> implements Comparable<T> {
private final int id;
public Foo(int id){
this.id = id;
}
public int getId(){
return id;
}
public int previousId() {
return id-1;
}
/**
* Compares this Foo object with another Foo one.
*
* @param o the Foo object to be compared to
* @return {@inheritDoc }
*/
@Override
public int compareTo(T o){
return Integer.compare(this.getId(), o.getId());
}
@Override
public String toString(){
return "Foo " + id;
}
}
/**
* A subclass of {@link Foo} that uses a generic type
* to redefine the class of objects that it can be compared to.
*
* @param <T> the generic type that the {@link Bar} class will used
* to allow that the method {@link #compareTo(Bar)} will receive
* just instances of {@link Bar}.
*
* @author Manoel Campos da Silva Filho
*/
class Bar<T extends Bar> extends Foo<T> {
private final String name;
public Bar(int id, String name){
super(id);
this.name = name;
}
public String getName(){
return name;
}
/**
* Just a method that only exists in Bar to show
* how the {@link #compareTo(Bar)} method that receives
* a {@link Bar} will call this specific method without performing
* any typecast.
*
* @return the current {@link #getId() id} + 1
*/
public int nextId(){
return getId()+1;
}
/**
* Compares this {@link Bar} instance with another given one.
* The use of a generic type allows the method to
* receive a {@link Bar} object instead of a {@link Foo} object.
* Accordingly, specific {@link Bar} methods such as {@link #getName()}
* can be called directly without typecast.
*
* @param o the Bar to be compared to
* @return {@inheritDoc}
*/
@Override
public int compareTo(T o){
return this.name.compareTo(o.getName());
}
@Override
public String toString(){
return "Bar " + getId() + " named " + name;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment