Item 23: Avoid shadowing type parameters

It is possible to define property and parameters with the same name due to shadowing. Local parameter shadows outer scope property. There is no warning because such a situation is not uncommon and is rather visible for developers:

  1. class Forest(val name: String) {
  2. fun addTree(name: String) {
  3. // ...
  4. }
  5. }

On the other hand, the same can happen when we shadow class type parameter with a function type parameter. Such a situation is less visible and can lead to serious problems. This mistake is often done by developers not understanding well how generics work.

  1. interface Tree
  2. class Birch: Tree
  3. class Spruce: Tree
  4. class Forest<T: Tree> {
  5. fun <T: Tree> addTree(tree: T) {
  6. // ...
  7. }
  8. }

The problem is that now Forest and addTree type parameters are independent of each other:

  1. val forest = Forest<Birch>()
  2. forest.addTree(Birch())
  3. forest.addTree(Spruce())

Such situation is rarely desired and might be confusing. One solution is that addTree should use the class type parameter T:

  1. class Forest<T: Tree> {
  2. fun addTree(tree: T) {
  3. // ...
  4. }
  5. }
  6. // Usage
  7. val forest = Forest<Birch>()
  8. forest.addTree(Birch())
  9. forest.addTree(Spruce()) // ERROR, type mismatch

If we need to introduce a new type parameter, it is better to name it differently. Note that it can be constrained to be a subtype of the other type parameter:

  1. class Forest<T: Tree> {
  2. fun <ST: T> addTree(tree: ST) {
  3. // ...
  4. }
  5. }

Summary

Avoid shadowing type parameters, and be careful when you see that type parameter is shadowed. Unlike for other kinds of parameters, it is not intuitive and might be highly confusing.