- Type: Design proposal
- Author: Pavel Mikhailovskii
- Status: Proposed
- Prototype: Supported under a flag in 1.3.70 and enabled by default in .gradle.kts. Preview in 1.8.20 with
-language-version 1.9
. Planned to be enabled by default in 2.1. - Related issues: KT-8575, KT-35933, KT-54525, KT-54770
Discussion of this proposal is held in this issue.
While Kotlin allows to access Java synthetic properties, it provides only limited and, until recently,
not fully correct support for obtaining references to such properties using object::property
or class::property
syntax.
This document describes the known bugs and limitations of the current implementation, a proposes a number of steps for improving the situation.
The possibility to access Java synthetic properties the same way as properties defined in Kotlin is widely used in Kotlin/JVM.
For the sake of language consistency, it would make sense to allow to obtain KProperty
references to such properties
similarly to Kotlin properties as well.
Methods that follow the Java conventions for getters and setters (no-argument methods with names starting with get
(or is
for boolean properties) and single-argument methods with names starting with set)
are represented as properties in Kotlin, as described here.
So, properties defined in a Java class
class Widget {
private String name;
private boolean active;
public String getName() {
return name;
}
public void setName(String value) {
name = value;
}
public boolean isActive() {
return active;
}
public boolean isActive(boolean value) {
active = value;
}
}
Can be accessed in Kotlin as follows:
val widget = Widget()
widget.name = "Widget1"
widget.isActive = true
In Kotlin 1.3.70 a possibility to obtain references to such properties was added.
val widget = Widget()
val widgetName = widget::name
widgetName.set("Widget1")
That functionality was never widely advertised, and was only available under a feature flag.
It also wasn't properly documented or covered with tests.
Nonetheless, it was enabled by default in .gradle.kts
(see KT-35933).
Until recently, it didn't work in K2 (see KT-54770); the issue has been
solved in our prototype.
Our review of the implementation added in 1.3.70 uncovered a number of issues:
- Most of the features provided by
kotlin-reflect
, such asKProperty::visibility
worked incorrectly. - Synthetic Java properties weren't included in
KClass.members
. - Synthetic Java properties can be overshadowed by "physical" declarations with the same name.
We are going to consider the found issues and possible ways of solving them below.
As was mentioned above, all features requiring kotlin-reflect
, such as KProperty::visibility
worked incorrectly.
Instead of somehow taking into account the synthetic nature of the property, they tried to access a Java field of the same name.
If no such field existed a run-time exception would be thrown.
In particular, that meant that calling propertyReference.seter(value)
would bypass the original setter method and write
directly to the underlying field!
Possibly, a proper solution to this issue would be to implement proper reflection support.
However, the demand for such a feature is most likely very low, so that we could postpone its implementation.
Supporting reflection would require making some extra design decisions, for example on what should be returned
by such properties as KProperty::annotations
or KProperty.Getter::name
.
As a midterm solution, we propose to forbid kotlin-reflect
-based reflection for synthetic Java properties.
Any invocation of a method or a property requiring kotlin-reflect
would result in an UnsupportedOperationException
.
That solution has been implemented in our prototype.
If a significant number of users asks for full reflection, we'll reconsider this decision.
The fact that synthetic Java properties aren't included in KClass.members
doesn't seem to be an issue.
By design, KClass.members
returns only real members and doesn't include any synthetic ones.
In the future, we could possibly consider introducing a separate property for them, e.g. KClass.syntheticMembers
.
class Jaba {
public boolean isFoo; // (0) physical val
public boolean isFoo() { return true; } // (1) physical method, (2) synthetic val
public boolean clazz() { return true; } // (3) physical method
public class clazz {} // (4) physical class
public String getField() { return ""; } // (5) physical method, (6) synthetic val
public int field = 2; // (7) physical val
public int bar() { return 1; } // (8) physical method
public int bar; // (9) physical val
public int getGetGoo() { return 1; } // (10) physical method, (11) synthetic val
public int getGoo = 2; // (12) physical val
public String getIsBaz() { return "getIsBaz"; } // (15) physical method, (16) synthetic val
public boolean isBaz() { return true; } // (17) physical method, (18) synthetic val
public String isDuh; // (19) physical val
public CharSequence isDuh() { return "42"; } // (20) physical method, (21) synthetic val
}
import kotlin.reflect.KProperty
fun main() {
// Case (a)
Jaba::isFoo // Conflict between physical members (0) and (1)
Jaba::clazz // Conflict between physical members (3) and (4)
Jaba::bar // Conflict between physical members (8) and (9)
// Case (b)
Jaba::field // Resolves to (7). Physical member (7) is preferred over synthetic val (6)
Jaba::getGoo // Resolves to (12). physical val (12) is preferred over synthetic val (11)
// Case (c)
val z: KProperty<Boolean> = Jaba::isFoo // Resolves to (0). The conflict is resolved
// Case (d)
val x: KProperty<String> = Jaba::field // Resolves to (6). A different member is chosen in non-conflicting situation
// Case (e)
val w: KProperty<*> = Jaba::isBaz // Conflict between synthetic members (16) and (18)
// Case (f)
val v: KProperty<String> = Jaba::isBaz // Resolves to (16). The conflict is resolved
val h: KProperty<Boolean> = Jaba::isBaz // Resolves to (18). The conflict is resolved
// Case (g)
val physical: KProperty<CharSequence> = Jaba::isDuh // Resolves to (19)
}
The following resolution rules apply in order:
- Firstly, the candidates are filtered by the expected type. Only the candidates that satisfy the expected type participate in the reference overload resolution. Cases (c), (d), (e), (f), and (g)
- If there is only a single physical candidate. We choose the candidate. Cases (b) and (g)
- If there are multiple physical candidates, the conflict is reported. Case (a)
- If there is only a single synthetic candidate. We choose the candidate. Case (d)
- If there are multiple synthetic candidates, the conflict is reported. Case (e)
Even though the code might look puzzling at a glance (why Jaba::isFoo
results in a conflict, but Jaba::field
doesn't?),
the resolution rules are in fact consistent.
Note that similar rules apply to pure Kotlin. Replace "physical" with "member", and "synthetic" with "extension"
class Kt {
val prop: Int = 42 // (0)
fun prop(): Double = 1.0 // (1)
val memberVsExtension: String = "" // (3)
val physVsTwoExts: String = "" // (5)
val duh: String = "" // (8)
fun duh(): Int = 42 // (9)
}
val Kt.prop: String get() = "extension" // (2)
val Kt.memberVsExtension: Int get() = 42 // (4)
fun Kt.physVsTwoExts(y: String): Int = 42 // (6)
fun Kt.physVsTwoExts(): Double = 1.0 // (7)
val Kt.duh: CharSequence get() = "extension" // (10)
fun main() {
// Case (a)
Kt::prop // Conflict between member members (0) and (1)
// Case (b)
Kt::memberVsExtension // Resolves to (3). Member is preferred over extension
Kt::physVsTwoExts // Resolves to (5). Member is preferred over extensions
// Case (c)
val z: KProperty<Int> = Kt::prop // Resolves to (0). The conflict is resolved
// Case (d)
val x: KProperty<Int> = Kt::memberVsExtension // Resolves to (4). A different member is chosen in non-conflicting situation
// Case (e)
val y: KFunction<*> = Kt::physVsTwoExts // Conflict between extension (6) and (7)
// Case (f)
val w: KFunction<Double> = Kt::physVsTwoExts // Resolves to (7). The conflict is resolved
val h: KFunction<Int> = Kt::physVsTwoExts // Resolves to (6). The conflict is resolved
// Case (g)
val physical: KProperty<CharSequence> = Kt::duh // Resolves to (8)
}
To sum up, we propose the following:
- Make it possible to reference synthetic Java properties using the
::
syntax. - Disable
kotlin-reflect
-dependent features for now; throw anUnsupportedOperationException
with a message making it clear that the limitation applies only to Java synthetic properties. - Do not include synthetic Java properties in
KClass.members
.