Core Java

The depths of Java: API leak exposed through covariance

Java can be very tricky some times, especially in API design. Let’s have a look at a very interesting showcase. jOOQ strongly separates API from implementation. All API is in the org.jooq package, and public. Most implementation is in the org.jooq.impl package and package-private. Only factories and some dedicated base implementations are public. This allows for very powerful package-level encapsulation, exposing mostly only interfaces to jOOQ users.

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.

Lukas Eder

Lukas is a Java and SQL enthusiast developer. He created the Data Geekery GmbH. He is the creator of jOOQ, a comprehensive SQL library for Java, and he is blogging mostly about these three topics: Java, SQL and jOOQ.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button