Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin returns same object from Factory method

Tags:

android

kotlin

I'm playing with Kotlin and found interesting behavior. So lets say i want to have some kind of a Factory :

internal interface SomeStupidInterface {
    companion object FACTORY {
       fun createNew(): ChangeListener {
            val time = System.currentTimeMillis()
            return ChangeListener { element -> Log.e("J2KO", "time " + time) }
        }

        fun createTheSame(): ChangeListener {
            return ChangeListener { element -> Log.e("J2KO", "time " + System.currentTimeMillis()) }
        }
    }

    fun notifyChanged()
}

where ChangeListener defined in java file:

interface ChangeListener {
    void notifyChange(Object element);
}

And then I try to use it from Java like so:

ChangeListener a = SomeStupidInterface.FACTORY.createNew();
ChangeListener b = SomeStupidInterface.FACTORY.createNew();
ChangeListener c = SomeStupidInterface.FACTORY.createTheSame();
ChangeListener d = SomeStupidInterface.FACTORY.createTheSame();
Log.e("J2KO", "createNew a == b -> " + (a == b));
Log.e("J2KO", "createTheSame c == d -> " + (c == d));

The results are:

createNew: a == b -> false
createTheSame: c == d -> true

I can understand why createNew returns new objects due to closure. But why I'm receiving the same instance from createTheSame method?

P.S. I know that code above is not idiomatic :)

like image 728
j2ko Avatar asked May 31 '17 17:05

j2ko


2 Answers

This has to do with performance. Creating less objects obviously is better for performance, so that is what Kotlin tries to do.

For each lambda, Kotlin generates a class that implements the proper interface. So for example the following Kotlin code:

fun create() : () -> Unit {
  return { println("Hello, World!") }
}

corresponds with something like:

Function0 create() {
  return create$1.INSTANCE;
}

final class create$1 implements Function0 {

  static final create$1 INSTANCE = new create$1();

  void invoke() {
    System.out.println("Hello, World!");
  }
} 

You can see here that the same instance is always returned.


If you reference a variable that is outside of the lamdba scope however, this won't work: there is no way for the singleton instance to access that variable.

fun create(text: String) : () -> Unit {
  return { println(text) }
}

Instead, for each invocation of create, a new instance of the class needs to be instantiated which has access to the text variable:

Function0 create(String text) {
  return new create$1(text);
}

final class create$1 implements Function0 {

  final String text;

  create$1(String text) {
    this.text = text;
  }

  void invoke() {
    System.out.println(text);
  }
} 

That is why your a and b instances are the same, but c and d are not.

like image 77
nhaarman Avatar answered Sep 19 '22 14:09

nhaarman


First note: your example code doesn't work as is: the interface has to be written in Java in order to be available for use with SAM constructors.

As for the actual question, you've already touched on why this behavior is happening. Lambdas (in this case, the SAM constructors) are compiled to anonymous classes (unless they're inlined). If they capture any outside variables, then for every invocation, a new instance of the anonymous class will be created. Otherwise, since they don't have to have any state, only a single instance will back every invocation of the lambda. I suppose this is for performance reasons, if nothing else. (Credit to the Kotlin in Action book for the information in this paragraph.)

If you want to return a new instance every time without capturing any variables, you can use the full object notation:

fun createNotQUiteTheSame(): ChangeListener {
    return object : ChangeListener {
        override fun notifyChanged(element: Any?) {
            println("time " + System.currentTimeMillis())
        }
    }
}

Calling the above function multiple times will return different instances for each call. Interestingly, IntelliJ will suggest converting this to the original SAM conversion syntax instead:

fun createNotQUiteTheSame(): ChangeListener {
    return ChangeListener { println("time " + System.currentTimeMillis()) }
}

Which, as you've already found out, returns the same instance every time.

I suppose this conversion is offered because comparing whether these stateless instances are equal is very much an edge case. If you need to be able to do comparison between the instances that are returned, you're probably best off with the full object notation. Then you can even add some additional state to each listener, in the form of an id for example.

like image 21
zsmb13 Avatar answered Sep 18 '22 14:09

zsmb13