刘耀文

刘耀文

java开发者
github

What exactly is a self-referential type?

Self-bounded types may sound complicated, but they are actually used to prevent subclasses from messing up type parameters. Let's say we have a generic class A that can accept any type of parameter:

class A<T> {
    T property;
    void setProperty(T t) { property = t; }
    T getProperty() { return property; }
}

Here, A can be used with any type of parameter, such as A<Integer> or A<String>. If we have a class B that wants to inherit from A, but only wants to fix the type parameter in A to B, we would typically write it like this:

class B extends A<B> {}

This pattern is called the curiously recurring generic pattern. In simple terms, B inherits from A, and A uses B as the type parameter.

What's the problem?#

The problem is that this approach does not enforce the use of B itself as the type parameter. For example, someone could write it like this:

class C {}

class B extends A<C> {}

In this case, B inherits from A, but uses C as the type parameter, completely breaking the rule we want.

Solution with self-bounded types#

To solve this problem, we can use self-bounded types:

class A<T extends A<T>> {}

With this change, the generic parameter T is forced to be a class that extends A<T>. In other words, when a subclass inherits from A, it must pass itself as the type parameter.

So now:

class C {}

class B extends A<C> {} // Compilation error

The above code will fail to compile because C does not extend A<C>. But the following code is valid:

class B extends A<B> {} // Compilation successful

This way, we ensure that when B inherits from A, the type parameter can only be B itself.

Practical application of self-bounded types: MyBatis-Plus Wrapper#

MyBatis-Plus is a popular ORM framework, and Wrapper is a commonly used utility class in it for building query conditions. The Wrapper class and its subclasses (such as QueryWrapper, UpdateWrapper) extensively use self-bounded types in their implementation to ensure that the return type matches the caller, thus enabling a fluent API.

Definition of Wrapper#

Let's take a look at the simplified definition of the AbstractWrapper class:

public abstract class AbstractWrapper<T, R, This extends AbstractWrapper<T, R, This>> {
    // Assume there are some fields and methods
    
    public This eq(R column, Object val) {
        // Implementation logic
        return (This) this;
    }
    
    public This like(R column, Object val) {
        // Implementation logic
        return (This) this;
    }
    
    // Other condition methods...
}

In this definition, This is a self-bounded type parameter that extends AbstractWrapper itself. This means that any class that inherits from AbstractWrapper must pass itself as the type parameter This. This ensures that the return type of chained method calls is still the concrete subclass type, rather than the base class type.

Implementation of QueryWrapper#

Now let's take a look at the simplified implementation of QueryWrapper:

public class QueryWrapper<T> extends AbstractWrapper<T, String, QueryWrapper<T>> {
    // Additional query conditions can be defined here
}

QueryWrapper inherits from AbstractWrapper and passes QueryWrapper<T> as the third generic parameter This. This means that when we call methods like eq or like, the return type is still QueryWrapper<T>, ensuring the consistency of the fluent API.

Example code#

Let's say we have a User table and want to build query conditions using QueryWrapper:

QueryWrapper<User> query = new QueryWrapper<>();
query.eq("name", "John")
     .like("email", "gmail.com");
     
// Execute the SQL query
List<User> users = userMapper.selectList(query);

In the above code, each eq and like method returns a QueryWrapper<User> object, allowing us to continue chaining other condition methods.

Benefits of self-bounded types#

In this design, the benefits of self-bounded types are evident:

  1. Type safety: It avoids returning incorrect types. For example, it ensures that the eq method of QueryWrapper still returns QueryWrapper instead of its superclass AbstractWrapper.
  2. Chained method calls: By using self-bounded types, the design ensures the coherence of method chaining, making the code more concise and intuitive.

In summary#

Self-bounded types are a technique that forces a class to use itself as a type parameter when inheriting, avoiding errors in type passing. Although this approach may seem convoluted, it is very useful in scenarios where strict control of types is required. In practical development, frameworks like MyBatis-Plus achieve more elegant and type-safe API designs by using self-bounded types.

This article is synchronized and updated to xLog by Mix Space.
The original link is https://me.liuyaowen.club/posts/default/20240821and1


Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.