Comparing extends/super with out/in of Kotlin in Java generics

Keywords: Java Programming

Welcome to my blog: songjhh's blog

In Java generics, there is a concept called wildcard upper and lower bounded wildcard.

  • Extended T >: refers to the Upper Bounded Wildcards.
  • <? Super T>: Refers to Lower Bounded Wildcards.

Relatively in Kotlin generics, there are out and in keywords

Next, I'll use the example of job assignment to explain what problems it can be used to solve and what improvements Kotlin has made compared to Java.

Problem Solved

There are four entities: Employee, Manager, DevManager and WorkStation.

Their relationship is as follows:

@Data
public class Employee {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
}

@Data
public class Manager extends Employee {
    private Integer level;
    public Manager(String name) {
        super(name);
    }
}

@Data
public class DevManager extends Manager {
    private String language;
    public DevManager(String name) {
        super(name);
    }
}

One of the workstations can sit on an employee, where generics are used to abstract the employee:

@Data
public class WorkStation<T> {
    private T employee;
    public WorkStation(T employee) {
        this.employee = employee;
    }
}

Logically, a manager's job is also an employee's job, but is that really the case?

// Create a manager position
WorkStation<Manager> managerWorkStation = new WorkStation<>(new Manager("John"));
// Assignment of Manager's Position to Employee's Position
WorkStation<Employ> employWorkStation = managerWorkStation; // error

But incompatible types: WorkStation < Manager > cannot be converted to WorkStation < Employee > are reported here, meaning that the two types cannot be converted to each other. Although Manager inherits Employee, there is no inheritance relationship between the two types of jobs, so it is not possible to pass the reference of manager's jobs directly to employees'jobs.

The reason for this is that Java's parameter type is invariant, and the upper and lower bounds of wildcards are designed to circumvent this problem.

ps: Variable is an important cornerstone in computer programming, especially in object-oriented programming. It can help programmers find many errors in the testing phase. There is no discussion here.

Bounded Wildcards

To help understand and remember, before we talk about the upper and lower bounds of wildcards, let's talk about the PECS principle.

PECS stands for producer-extends, consumer-super

From: Effective Java Third Edition - Item 31

Here is a mnemonic in the section Effective Java Third Edition on how to use bounded wildcards to improve API flexibility.

Simply put, producers use <? Extends T> and consumers use <? Super T>, where producers refer to objects that can be read and consumers refer to objects that can be written. These two concepts are explained in detail below.

Upper bound wildcards

Or follow the example above, where the upper bound wildcard <? Extends T> is used to obtain a reference to the manager's position.

// Create a manager position
WorkStation<Manager> managerWorkStation = new WorkStation<>(new Manager("John"));
// Assign a reference to a manager's job to a job inherited from an employee's object
WorkStation<? extends Employee> exWorkStation = managerWorkStation;

As you can see, using the upper bound wildcards, we associate managers'and employees' jobs, which greatly increases the flexibility of Java generics.

But the PECS principle is introduced above. It points out that upper bound wildcards are only suitable for producers. Let me show you how to understand this sentence.

WorkStation<Manager> managerWorkStation = new WorkStation<>(new Manager("John"));
WorkStation<? extends Employee> exWorkStation = managerWorkStation;

// Only it and its base class can be obtained
Object a = exWorkStation.getEmployee();
Employee b = exWorkStation.getEmployee();
DevManager d = exWorkStation.getEmployee(); // error

// Not Storable
exWorkStation.setEmployee(new Employee("Sam")); // error, incompatible types: Manager cannot be coverted to capture#1 of ? extends Employee
exWorkStation.setEmployee(new DevManager("James")); // error, incompatible types: DevManager cannot be coverted to capture#1 of ? extends Employee

As can be seen from the example above, using upper bound wildcards can only use get() method to extract the occupancy type and its base class, but can no longer use set() method to store objects in the workplace, so the upper bound wildcards are only suitable for producers.

The reason is also well understood, because the compiler only knows that the workstation is sitting on an Employee object or its derivative class, but does not know which object it is (the compiler uses capture 1 to mark the placeholder, which means that Employee and its subclasses are captured here), so it is impossible to judge whether the workstation can match the workstation:?

  • The person sitting on exWorkStation must be an employee, so Employee can be removed.
  • exWorkStation may be the Manager's job, so there's no problem accessing TestManager here. But the problem is that it may also be DevManager's job, so TestManager can't sit in this job, the editor can't judge, so the upper wildcard can't use the set() method.

In short, the upper bound wildcard Upper Bounded Wildcards makes the parameter type covariant.

Lower bound wildcards

Contrary to the upper-bound wildcard, the lower-bound wildcard <? Super T> is suitable for storing objects in scenarios.

WorkStation<? super Manager> supWorkStation = new WorkStation<>(new Manager("James"));

// You can store it and its subclasses
supWorkStation.setEmployee(new DevManager("Sam"));
supWorkStation.setEmployee(new Manager("Sam"));
supWorkStation.setEmployee(new Employee("Sam")); // error

// Only the base class of all classes - Object can be obtained
Object o = supWorkStation.getEmployee();
Employee e = supWorkStation.getEmployee(); // error
Manager e = supWorkStation.getEmployee(); // error
DevManager e = supWorkStation.getEmployee(); // error

// It can only be safely and strongly converted to it and its base classes
Employee employee = (Employee) o;
Manager manager = (Manager) o;

WorkStation<? super Manager> w = new WorkStation<>(new Manager("Sam"));
// ClassCastException: Manager cannot be cast to DevManager
DevManager devManager = (DevManager) w.getEmployee();

As you can see from the example above, using lower bound wildcards, you can store Manager and its subclasses in the set() method, but you can only get the base class Object objects of all classes in the get() method. With strong rollover, it can only be forced into Manager and its base class, and if strong rollover to a subclass of Manager, it may report a ClassCastException runtime exception.

Because it is convenient to store and take out data, lower bound wildcards are suitable for consumers.

The reason for this is simply that the lower wildcard marks the position of at least Manager, so there is no problem sitting here either in DevManager or TestManager.

This is called contravariance.

What is it like in Kotlin's world?

It's the Java world that uses wildcard upper and lower bounds to feel that generics don't change, so what about Kotlin?

val managerWorkStation: WorkStation<Manager> = WorkStation(Manager("John"))
val station: WorkStation<Employee> = managerWorkStation // error, type mismatch

This shows that generics are also limited in Kotlin. Compared with <? Extends T > and <? Super T > provided by Java, Kotlin provides out and in keywords.

In Kotlin, out equals <? Extends T> and in equals <? Super T>. Here's how to use it.

out keyword:

val managerWorkStation: WorkStation<Manager> = WorkStation(Manager("John"))
val outStation: WorkStation<out Employee> = managerWorkStation

// Only it and its base class can be obtained
val a: Any = outStation.employee
val b: Employee = outStation.employee
val c: Employee = managerWorkStation.employee
val d: DevManager = managerWorkStation.employee // error, type mismatch

// Not Storable
outStation.employee = DevManager("Sam") // Setter for 'employee' is removed by type projection

in keyword:

val inStation: WorkStation<in Manager> = WorkStation()

// You can store it and its subclasses
inStation.employee = Manager("James")
inStation.employee = DevManager("James")
inStation.employee = Employee("James") // error, type mismatch

// You can only get Any
val any: Any? = inStation.employee

// It can only be safely and strongly converted to it and its base classes
val employee: Employee = any as Employee
val manager:Manager = any as Manager

As can be seen from the above two examples, Kotlin and Java are very similar, but the relevant keywords are different. But after all, Kotlin is supposed to solve Java, so what's the difference?

The similarities and differences between Kotlin and Java

Usage change

In Java, upper and lower bound wildcards can only be used in parameters, attributes, variables or return values, not in generic declarations, so they are called usage variants.

The above Kotlin example also uses usage variants, known as type projection.

So both Java and Kotlin provide usage variants.

Declarations vary from place to place

But the difference is that Kotlin also provides declarative variations that Java does not have.

As the name implies, the out and in variant keywords provided by Kotlin can also be used for generic declarations.

public interface Collection<out E> : Iterable<E> {
    ...
}

// Error, you can only use val here, not var.
class Source<out T>(var t: T) {
    ...
}

Setting out at the declaration makes Collection < Number > safely used as the parent class of Collection < Int > in Kotlin, but after E is marked out, E can only be output but not written.

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 has the type Double, which is a subtype of Number
    // Therefore, we can assign x to variables of type Comparable < Double>.
    val y: Comparable<Double> = x
}

After Comparable sets in at the declaration, x can be compared with Number or its subclasses.

summary

These are the contents of Java and Kotlin about generic variants, in which Kotlin compares Java and adds the way of declaring variants.

Java Java sample code Kotlin sample code
Usage change void example(List<? extends Number> list) fun example(list: List<out Number>)
Usage Inversion void example(List<? super Integer>) fun example(list: List<in Int>)
Declarations vary from place to place - interface Collection<out E> : Iterable<E>
Statement reversal - interface Comparable<in T>

To help memory, PECS principles are cited above: producer-extends, consumer-super.

Finally, here are some comments on wildcards in Effective Java - 31 | Use bounded wildcards to increase API flexibilty:

  • If an input parameter is both a producer and a consumer, then wildcard types will do you no good.

    If the input parameters are both producers and consumers, then wildcards are not a good choice for you.

  • Do not use bounded wildcard types as return types, if the user of a class has to think about wildcard types, there is probably something wrong with its API.

    Do not use bounded wildcards as your return type. If the user of the class has to consider the wildcard type, the API of the class may be wrong.

  • If a type parameter appears only once in a method declaration, replace it with a wildcard.

    If the type parameter appears only once in the method declaration, it can be replaced by wildcards.

Thank you for reading.

Posted by zman on Sat, 16 Mar 2019 10:09:28 -0700