I’m trying to gather feedback on an idea that was discussed in an Android Slack group and to see if the community thinks it’s worth me raising a KEEP for it.
The problem is that, in some cases, automatic SAM conversion can lead to subtle bugs and unintended behaviour.
The typical example is a register/unregister API, something that is often seen in older Android APIs.
Take for instance the pair of methods on the View class in Android:
public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
ListenerInfo li = getListenerInfo();
if (li.mOnLayoutChangeListeners == null) {
li.mOnLayoutChangeListeners = new ArrayList<OnLayoutChangeListener>();
}
if (!li.mOnLayoutChangeListeners.contains(listener)) {
li.mOnLayoutChangeListeners.add(listener);
}
}
public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) {
ListenerInfo li = mListenerInfo;
if (li == null || li.mOnLayoutChangeListeners == null) {
return;
}
li.mOnLayoutChangeListeners.remove(listener);
}
and the SAM interface:
public interface OnLayoutChangeListener {
void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom);
}
If a caller tries to use this API in the following manner:
class SomeActivity: Activity() {
private val listener = { v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int ->
// something
}
private val view get() = findViewById<View>(android.R.id.content)
override fun onStart() {
super.onStart()
view.addOnLayoutChangeListener(listener)
}
override fun onStop() {
super.onStop()
view.removeOnLayoutChangeListener(listener)
}
}
They will introduce a bug. The problem is obviously that, internally, the method adds/removes an object from some data structure, and since there is an implicit SAM conversion at the call site of addOnLayoutChangeListener and removeOnLayoutChangeListener, the objects are not the same, therefore the listener is never removed.
There is a similar bug when an API internally holds a weak reference to the passed object. The bug there is that the weak reference is almost immediately cleared since nothing holds on to the SAM object if it was declared in a similar manner above.
What I propose is a new annotation, @RejectSam or similar, where API declare sites can provide hints to the compiler that an automatic SAM conversion might introduce subtle bugs and should be avoided. For the example above, this would be:
public void addOnLayoutChangeListener(@RejectSam OnLayoutChangeListener listener) {
// ....
}
public void removeOnLayoutChangeListener(@RejectSam OnLayoutChangeListener listener) {
// ....
}
This could then cause either a warning or even a compiler error at the call site if an automatic SAM conversion happens.