The depths of Java: API leak exposed through covariance
A simplified example of package-level encapsulation
Here’s roughly how jOOQ models SQL tables. The (overly simplified) API:
package org.jooq; /** * A table in a database */ public interface Table { /** * Join two tables */ Table join(Table table); }
And two (overly simplified) implementation classes:
package org.jooq.impl; import org.jooq.Table; /** * Base implementation */ abstract class AbstractTable implements Table { @Override public Table join(Table table) { return null; } } /** * Custom implementation, publicly exposed to client code */ public class CustomTable extends AbstractTable { }
How the internal API is exposed
Let’s assume that the internal API does some tricks with covariance:
abstract class AbstractTable implements Table, InteralStuff { // Note, this method returns AbstractTable, as it might // prove to be convenient to expose some internal API // facts within the internal API itself @Override public AbstractTable join(Table table) { return null; } /** * Some internal API method, also package private */ void doThings() {} void doMoreThings() { // Use the internal API join(this).doThings(); } }
This looks all safe at the first sight, but is it? AbstractTable is package-private, but CustomTable extends it and inherits all of its API, including the covariant method override of “AbstractTable join(Table)”. What does that result in? Check out the following piece of client code
package org.jooq.test; import org.jooq.Table; import org.jooq.impl.CustomTable; public class Test { public static void main(String[] args) { Table joined = new CustomTable(); // This works, no knowledge of AbstractTable exposed to the compiler Table table1 = new CustomTable(); Table join1 = table1.join(joined); // This works, even if join exposes AbstractTable CustomTable table2 = new CustomTable(); Table join2 = table2.join(joined); // This doesn't work. The type AbstractTable is not visible Table join3 = table2.join(joined).join(joined); // ^^^^^^^^^^^^^^^^^^^ This cannot be dereferenced // ... so hide these implementation details again // The API flaw can be circumvented with casting Table join4 = ((Table) table2.join(joined)).join(joined); } }
Conclusion
Tampering with visibilities in class hierarchies can be dangerous. Beware of the fact that API methods declared in interfaces are always public, regardless of any covariant implementations that involve non-public artefacts. This can be quite annoying for API users when not properly dealt with by API designers.
Fixed in the next version of jOOQ
Reference: The depths of Java: API leak exposed through covariance from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog.