An Introduction to Generics in Java – Part 6
This is a continuation of an introductory discussion on Generics, previous parts of which can be found here.
In the last article we were discussing about recursive bounds on type parameters. We saw how recursive bound helped us to reuse the vehicle comparison logic. At the end of that article, I suggested that a possible type mixing may occur when we are not careful enough. Today we will see an example of this.
The mixing can occur if someone mistakenly creates a subclass of Vehicle in the following way:
/** * Definition of Vehicle */ public abstract class Vehicle<E extends Vehicle<E>> implements Comparable<E> { // other methods and properties public int compareTo(E vehicle) { // method implementation } } /** * Definition of Bus */ public class Bus extends Vehicle<Bus> {} /** * BiCycle, new subtype of Vehicle */ public class BiCycle extends Vehicle<Bus> {} /** * Now this class’s compareTo method will take a Bus type * as its argument. As a result, you will not be able to compare * a BiCycle with another Bicycle, but with a Bus. */ cycle.compareTo(anotherCycle); // This will generate a compile time error cycle.compareTo(bus); // but you will be able to do this without any error
This type of mix up does not occur with Enums because JVM takes care of subclassing and creating instances for enum types, but if we use this style in our code then we have to be careful.
Let’s talk about another interesting application of recursive bounds. Consider the following class:
public class MyClass { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public MyClass() {} public String getAttrib1() { return attrib1; } public void setAttrib1(String attrib1) { this.attrib1 = attrib1; } public String getAttrib2() { return attrib2; } public void setAttrib2(String attrib2) { this.attrib2 = attrib2; } public String getAttrib3() { return attrib3; } public void setAttrib3(String attrib3) { this.attrib3 = attrib3; } public String getAttrib4() { return attrib4; } public void setAttrib4(String attrib4) { this.attrib4 = attrib4; } public String getAttrib5() { return attrib5; } public void setAttrib5(String attrib5) { this.attrib5 = attrib5; } }
If we want to create an instance of this class, then we can do this:
MyClass mc = new MyClass(); mc.setAttrib1("Attribute 1"); mc.setAttrib2("Attribute 2");
The above code creates an instance of the class and initializes the properties. If we could use Method Chaining here, then we could have written:
MyClass mc = new MyClass().setAttrib1("Attribute 1") .setAttrib2("Attribute 2");
which obviously looks much better than the first version. However, to enable this type of method chaining, we need to modify MyClass in the following way:
public class MyClass { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public MyClass() {} public String getAttrib1() { return attrib1; } public MyClass setAttrib1(String attrib1) { this.attrib1 = attrib1; return this; } public String getAttrib2() { return attrib2; } public MyClass setAttrib2(String attrib2) { this.attrib2 = attrib2; return this; } public String getAttrib3() { return attrib3; } public MyClass setAttrib3(String attrib3) { this.attrib3 = attrib3; return this; } public String getAttrib4() { return attrib4; } public MyClass setAttrib4(String attrib4) { this.attrib4 = attrib4; return this; } public String getAttrib5() { return attrib5; } public MyClass setAttrib5(String attrib5) { this.attrib5 = attrib5; return this; } }
and then we will be able to use method chaining for instances of this class. However, if we want to use method chaining where inheritance is involved, things kind of get messy:
public abstract class Parent { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public Parent() {} public String getAttrib1() { return attrib1; } public Parent setAttrib1(String attrib1) { this.attrib1 = attrib1; return this; } public String getAttrib2() { return attrib2; } public Parent setAttrib2(String attrib2) { this.attrib2 = attrib2; return this; } public String getAttrib3() { return attrib3; } public Parent setAttrib3(String attrib3) { this.attrib3 = attrib3; return this; } public String getAttrib4() { return attrib4; } public Parent setAttrib4(String attrib4) { this.attrib4 = attrib4; return this; } public String getAttrib5() { return attrib5; } public Parent setAttrib5(String attrib5) { this.attrib5 = attrib5; return this; } } public class Child extends Parent { private String attrib6; private String attrib7; public Child() {} public String getAttrib6() { return attrib6; } public Child setAttrib6(String attrib6) { this.attrib6 = attrib6; return this; } public String getAttrib7() { return attrib7; } public Child setAttrib7(String attrib7) { this.attrib7 = attrib7; return this; } } /** * Now try using method chaining for instances of Child * in the following way, you will get compile time errors. */ Child c = new Child().setAttrib1("Attribute 1").setAttrib6("Attribute 6");
The reason for this is that even though Child inherits all the setters from its parent, the return type of all those setter methods are of type Parent, not Child. So the first setter will return reference of type Parent, calling setAttrib6 on which will result in compilation error, because it does not have any such method.
We can resolve this problem by introducing a generic type parameter on Parent and defining a recursive bound on it. All of its children will pass themselves as type argument when they extend from it, ensuring that the setter methods will return references of its type:
public abstract class Parent<T extends Parent<T>> { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public Parent() { } public String getAttrib1() { return attrib1; } @SuppressWarnings("unchecked") public T setAttrib1(String attrib1) { this.attrib1 = attrib1; return (T) this; } public String getAttrib2() { return attrib2; } @SuppressWarnings("unchecked") public T setAttrib2(String attrib2) { this.attrib2 = attrib2; return (T) this; } public String getAttrib3() { return attrib3; } @SuppressWarnings("unchecked") public T setAttrib3(String attrib3) { this.attrib3 = attrib3; return (T) this; } public String getAttrib4() { return attrib4; } @SuppressWarnings("unchecked") public T setAttrib4(String attrib4) { this.attrib4 = attrib4; return (T) this; } public String getAttrib5() { return attrib5; } @SuppressWarnings("unchecked") public T setAttrib5(String attrib5) { this.attrib5 = attrib5; return (T) this; } } public class Child extends Parent<Child> { private String attrib6; private String attrib7; public String getAttrib6() { return attrib6; } public Child setAttrib6(String attrib6) { this.attrib6 = attrib6; return this; } public String getAttrib7() { return attrib7; } public Child setAttrib7(String attrib7) { this.attrib7 = attrib7; return this; } }
Notice that we have to explicitly cast this to type T because compiler does not know whether or not this conversion is possible, even though it is because T by definition is bounded by Parent<T>. Also since we are casting an object reference to T, an unchecked warning will be issued by the compiler. To suppress this we used @SuppressWarnings(“unchecked”) above the setters.
With the above modifications, it’s perfectly valid to do this:
Child c = new Child().setAttrib1("Attribute 1") .setAttrib6("Attribute 6");
When writing method setters this way, we should be careful as to not to use recursive bounds for any other purposes, like to access children’s states from parent, because that will expose parent to the internal details of its subclasses and will eventually break the encapsulation.
With this post I finish the basic introduction to Generics. There are so many things that I did not discuss in this series, because I believe they are beyond the introductory level.
Until next time.
Reference: | An Introduction to Generics in Java – Part 6 from our JCG partner Sayem Ahmed at the Codesod blog. |