- Type: Design proposal
- Authors: Roman Elizarov
- Contributors: Simon Ogorodnik, Mikhail Zarechensky, Marat Ahin, Alexandr Udalov, Vsevolod Tolstopyatov
- Status: Proposed, in public review
- Issue: KT-11968 Research and prototype namespace-based solution for statics and static extensions
- Discussion: KEEP-348
- Introduction
- Detailed Design
- Static members grammar
- Static members and scope
- Static objects
- Constant static properties
- Intended use of static properties and functions
- Static initialization
- Static body scope
- Static declarations resolution and import
- Static objects resolution and import
- Statics visibility
- Statics and inheritance
- Extension functions as static members
- Static extensions
- Static extensions on generic classes
- Extensions on static objects
- Static extensions resolution and import
- Static extensions vs static members
- Static extensions as members
- Static extensions vs extensions as static members
- Code style
- JVM ABI
- Statics in other languages
- Potential future improvements
- Open issues
This proposal introduces the concepts of static members, static extensions, and static objects to the Kotlin programming language. The main motivations behind this proposal are:
- Enable static extensions of 3rd party classes, which is the top most voted feature in Kotlin.
- Provide a lighter-weight mechanism for defining static members that do not access instance variable as an alternative to companion objects in many use-cases.
- Simplify interoperability with JVM statics that Kotlin compiler has to support anyway — more concise and clear usage of Java frameworks that rely on statics, easier Java to Kotlin migration.
It is a non-goal of this proposal to deprecate or to completely replace companion objects. More details on the future of companion objects can be found in the statics and the future role of companion objects section.
All in all, the new concepts are designed to address the following major problems in the language design:
- KT-11968 Adding statically accessible members to an existing Java class via extensions
- KT-15595 Optimize away unused and empty private companion object
- KT-16872 JvmStatic annotation on companion object functions creates an extra method
- KT-16900 Allow 'const val' in classes and interfaces
This proposal also opens a road to potential future extensions of the language that need some ability to extend classes as opposed to extension of instances, namely:
- KT-43871 Collection literals
- KT-45587 Tuples (structural literals and structural types)
- KT-49392 Implement static interface (companion/static requirements)
OPEN ISSUE: This proposal has a major open issue on what kind of declaration syntax to use for static members: static sections or static modifiers. See Static section vs static modifier for details on their pros and cons. The text of the proposal is written showing both alternatives side by side.
Kotlin programming language has a concept of top-level functions that are analogous to static functions in languages
like Java and C#. In particular, top-level functions don’t have any this
reference in their scope.
However, inside the Kotlin classes the closest thing Kotlin has are members inside companion objects.
Consider the following example of a Color
class for an RGB color:
class Color(val rgb: Int) {
companion object {
fun fromRGB(r: Int, g: Int, b: Int): Color { /* impl */ }
val RED = Color(0xff0000)
// other declarations
}
}
The above declarations does more than declaring Color.fromRGB
and Color.RED
. It also declares a
Color.Companion
object which does not add any additional value to this particular usage of companion object
and creates additional complications:
- Every function in the companion object has
this
reference to the companion object, which usually is not used in any meaningful way. - Companion object can be extended using
fun Color.Companion.extFunction()
syntax but only if the class had explicitly declared the companion object, which makes extending 3rd party classes without a companion object impossible. - Companion object gets compiled into a separate class on Kotlin/JVM even though it does not usually carry any state.
- You cannot access
Color.fromRGB
from Java directly, you’ll need to add@JvmStatic
to those declaration, which creates additional byte code on JVM. - All members of companion object have to be grouped together in the declaration of the class, which sometimes is not convenient in larger classes.
In this proposal we introduce a concept of a static members as a direct replacement for the majority of today’s usage for companion object.
The example code can be converted into the following:
Option: Static section syntax.
class Color(val rgb: Int) {
static {
fun fromRGB(r: Int, g: Int, b: Int): Color { /* impl */ }
val RED = Color(0xff0000)
// other declarations
}
}
Declarations inside a static section are called static members.
A class can have several static sections, thus in larger classes static members can be grouped according to their function next to the corresponding instance members if needed.
Unlike companion objects, static sections cannot be named and cannot have their own visibility modifiers, which also makes them easier to understand and to use in practice.
Option: Static modifier syntax.
class Color(val rgb: Int) {
static fun fromRGB(r: Int, g: Int, b: Int): Color { /* impl */ }
static val RED = Color(0xff0000)
}
Declarations with a static modifier are called static members.
Static members, regardless of the syntax that is used to declare them, address all shortcomings of companion objects that were listed above:
- Static members do not have any
this
reference similar to top-level functions. - Static members can be introduced outside the lexical scope of the class using
static extension syntax
fun Color.static.extFunction()
. - Static members compile directly into the original class, without a need to generate a separate class on Kotlin/JVM.
- A static
Color.fromRGB
function can be accessed as a static member from Java directly. - Static members can be mixed together with instance members, so they can be grouped in the code according to their intended purpose.
Unlike companion objects, static members do not introduce any kind of stable object reference. They only expand the static scope of a class. It enables writing extensions for static scope of any class. For example, a use-case from KT-11968 (Adding statically accessible members to an existing Java class via extensions) can be written like this using a new static extension syntax:
val Intent.static.SCHEME_SMS: String
get() = "sms"
Here, Intent
is a 3rd-party Java class. With this declaration, one can write Intent.SCHEME_SMS
to get a string
"sms"
.
Kotlin has object
declarations, which are used for multiple purposes. One of the current uses for an object
declaration is to group a number of related declarations under a common namespace. For example, the Kotlin standard
library has the following declaration:
object Delegates {
fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
// other declarations
}
The goal of this object is to ensure that call-sites for the declarations inside it, like notNull()
, are prefixed with
a namespace Delegates
and look like Delegates.notNull()
. However, just like a companion object, this kind of object
usage introduces a superfluous Delegates
instance that works as this
inside of the notNull
function without being
actually used. A new static object feature allows for such grouping to be performed without introducing
an object instance:
static object Delegates {
fun <T : Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar()
// other declarations
}
The members of such a static object
work similarly to top-level functions. They don't have this
inside of them and
get compiled directly to static functions on JVM.
The rationale for rolling out the static object
feature at the same moment when static declarations are introduced
is that if static declarations become available without static objects, it will immediately cause the
"static utility class" boilerplate pattern to appear in the Kotlin code. That is, developers would work around the
absence of static objects by declaring a class with only static members inside to replicate the effect a
static object for grouping multiple functions or properties under a single namespace. This pattern will be hard
to get rid of later, since it would introduce unnecessary type of the class, in addition to the static methods
themselves.
The main alternative to this design proposal with statics is to gradually improve and fix the design of companion objects in Kotlin to address all of their shortcomings while doubling down on the companion object concept. That means things like getting companion objects lighter-weight, getting rid of a required stable instance, and thus allowing extensions on any classes by postulating that every class has a light-weight instance-less companion object. The advantage of this approach is that we will not have to teach existing and novice Kotlin developers a new concept of statics, and it keeps the spirit of "everything is an object in Kotlin".
There is a major shortcoming to this approach. In the end, when the improvement process is over, we'll have a feature called a "companion object" in Kotlin that will work and behave very much like statics in all the other major and popular programming languages (see Statics in other languages), but under a different name, without offering any tangible benefits in conciseness or in reducing error-proneness of the resulting code. Basically, it is going to be "inventing a new name just for the sake of inventing a new name".
This approach is not consistent with Kotlin's pragmatic design philosophy that is explicitly focused on keeping the language familiar and easy to learn to a wide range of developers with diverse backgrounds.
The section goes into the details of the design.
There are two syntactic choices for static member declaration: static section and static modifier. Both of them are explained in the separate sections below.
From the standpoint of Kotlin grammar, static section gets added
to classMemberDeclaration
and, grammatically, looks very much like init
section,
but has classBody
block inside of it:
classBody : '{' classMemberDeclarations '}' ;
classMemberDeclarations : (classMemberDeclaration semis?)* ;
classMemberDeclaration
: declaration
| companionObject
| anonymousInitializer
| secondaryConstructor
| staticSection // New class member declaration variant
;
staticSection : 'static' classBody ;
There are the following additional semantic restrictions on static sections:
- Static section is not allowed directly inside any kind of
object
declaration (including companion object), but is allowed inside a nested class or an interface of an object. - Static section is not allowed inside another
static
section. - Static section is not allowed inside a local class or interface.
- Static section is not allowed inside an inner class.
Rationale: static sections inside objects or other static sections don't bring additional value, since those containers with their contents are already conceptually static. The restrictions on placing statics inside local and inner classes simply extend existing restrictions on static declarations (like named objects) inside them.
Static sections can appear inside a class
or an interface
. All the declarations inside a static sections are said
to be static members of the corresponding class or interface.
Despite grammar of the static section listing classBody
as the static section contents,
only the following declarations are allowed directly inside the static section:
- Function and property declarations — called static functions and static properties.
- Anonymous initializers (
init { ... }
sections) — called static initializers.
Other declarations (including class
, interface
, object
, and typealias
declarations) are not allowed
inside a static section.
Rationale:
class
,interface
, andobject
declarations inside a static section make no sense, as such declarations are already static by default and do not have access to an outer class instance. They should be written directly inside the corresponding class or interface, not inside its static section.typealias
declarations are currently allowed only on top-level and lifting this restriction is outside the scope of this proposal.
Static members cannot have operator
modifier.
See Static operators for potential lifting of this restriction in the future.
There can be multiple static sections inside a class or an interface. This way, in larger classes, it becomes possible to group static declarations based on their functionality. For example, private helper static methods can be situated somewhere close to the place where they are used.
A new static
modifier gets introduced for the purpose of marking static members of classes and interfaces.
There are the following semantic restrictions on the placement of static modifiers:
- Static modifier is only allowed on:
- Function and property declarations — such declarations are called static functions and static properties.
- Anonymous initializers (
init { ... }
sections) — such section are called static initializers.
- Static modifier is not allowed on other declarations (including
class
,interface
, andtypealias
declarations).- Static modifier on
object
declaration is used to denote Static objects and have a different semantics from a static declaration on functions and properties.
- Static modifier on
Rationale: static modifier on a
class
,interface
declarations make no sense, as such declarations are already static by default and do not have access to an outer class instance.typealias
declarations are currently allowed only on top-level and lifting this restriction is outside the scope of this proposal.
- Static modifiers on members (functions and properties) are not allowed directly inside any kind of
object
declaration (including companion object), but are allowed inside a nested class or an interface of an object.- Note that
static object
is allowed inside other objects, see Static objects.
- Note that
- Static modifiers are not allowed inside a local class or interface.
- Static modifiers are not allowed inside an inner class.
- Static members cannot have
operator
modifier.- See Static operators for potential lifting of this restriction in the future.
Static declarations get placed into the same declaration scope as the instance member declarations in the containing class or interface. Thus, unlike companion objects, it is illegal to declare static members with names that conflict with the instance members of the class or interface, yet it is Ok to properly overload functions. Also, it is illegal for a static member to hide instance members that are inherited from any superclasses or superinterface.
As explained in Statics and inheritance, the static declarations themselves are not inherited, so no hiding happens between statics with the same signature in related classes.
For example:
Option: Static section syntax.
class Outer {
val x = 0 // member property
fun foo(x: Int) {} // member function
static {
val x = 0 // ERROR: Conflicting declaration
fun toString() = "Outer" // ERROR: Hides inherited member from Any
fun foo(x: Int) {} // ERROR: Conflicting overload
fun foo(x: Any) {} // OK: A proper overload
}
}
Option: Static modifier syntax.
class Outer {
val x = 0 // member property
fun foo(x: Int) {} // member function
static val x = 0 // ERROR: Conflicting declaration
static fun toString() = "Outer" // ERROR: Hides inherited member from Any
static fun foo(x: Int) {} // ERROR: Conflicting overload
static fun foo(x: Any) {} // OK: A proper overload
}
The object declaration supports a new static
modifier. Such an object is said to be a static object and is
referred to, as usual, by its name in the code. Unlike regular objects, static objects cannot be referenced as a
value or as a type by themselves (see Static scope projection reference for
discussion on potentially allowing it in the future).
Static objects can contain directly inside them the following declarations:
- Function and property declarations — called static functions and static properties.
- Anonymous initializers (
init { ... }
sections) — called static initializers. - Nested object declarations (both static and non-static ones), but companion object is not allowed.
- Nested classes (but no inner classes).
- Nested interfaces.
Declarations inside static objects have no this
reference.
static object Namespace {
val property = 42 // static property
fun doSomething() { // static function
property // OK: Can refer to static property in the same scope
this // ERROR: Static objects have no instance
}
}
fun main() {
Namespace.doSomething() // OK
val x = Namespace // ERROR: Cannot reference static object as value
val y: Namespace? = null // ERROR: Cannot reference static object as type
}
Static objects cannot implement interfaces nor extend classes. Their only intended purpose is to serve as a namespace grouping related functions.
With static object having no value, nor type, nor identity, their ability to extend objects and implement interface would create a weird exception where the super class or interface implementation will be getting access to
this
reference, while the methods of the static objects themselves have no access tothis
. However, in the future, if needed, we may introduce a separate concept of Static interfaces and static overrides.
The key use-case for static objects is to group a number of declarations with short names, such that they will be used
at their call sites with a qualified call syntax of <ObjectName>.<declarationName>
. This will aid in autocompletion
such declarations in IDE by typing the name of the containing object first to discover all the members inside.
Declaration with longer, non-ambiguous names should continue to be declared on the top-level in packages.
Static objects do not have value and do not extend Any
. Still, to future-proof them, they are forbidden from declaring
any functions with the same Kotlin or platform signatures that are declared in the kotlin.Any
class. For example:
static object Namespace {
fun toString() = "Namespace" // Error: not allowed to use the signatures matching members of `kotlin.Any`
}
The mental model for the concept of static object
is that it is very much like a regular named object,
but all its methods are static and do not use object instance in any way, so there is no stable reference
to this object, as it is not needed. Another way to look at it, is that static
modifier for an object
has
a similar effect as a value
modifier for a class
— it is an indicator that a stable reference
is not needed.
Static object is a performance-oriented tweak to the concept of a named object. It need not be taught
to beginner Kotlin developers. When used properly, one does not need to understand what difference
static
modifier for a named object
makes. Beginner Kotlin developers can simply ignore static
modifier
before object
and still make sense of the code.
An early prototype of this design used a
namespace
keyword instead ofstatic object
. Static object alternatives and namespaces section goes into more details on alternatives forstatic object
design and the rationale for picking the design proposed here.
Static properties can have const
modifier similarly to top-level and object properties.
Note: On JVM adding/removing
const
modifier is a binary incompatible change. See Constant static properties on JVM.
The primary use of static function and properties is to provide utilities that are intended to be used in the scope of the corresponding class or interface without the need have an instance of the corresponding class or shall be qualified by the name of their containing class.
This is especially relevant for creation functions and constants that cannot be put on the top-level
due to their short, ambiguous names. For example, it would be a bad style to declare a top-level property
called empty
(as this short name might be appropriate for any container and does specify which one it is about),
but MyContainer.empty
is non-ambiguous and can be declared as a static member without introduction
of a superfluous companion object whose instance is not needed:
Option: Static section syntax.
class MyContainer {
static {
val empty: MyContainer = MyContainer()
}
}
Option: Static modifier syntax.
class MyContainer {
static val empty: MyContainer = MyContainer()
}
Static properties can have initializers, classes and interfaces can have static initialization sections with code. Together they form static initialization code. This code gets executed, from top to bottom, in the program order, when the containing class or interface gets initialized.
The rules that define when the corresponding class gets initialized are currently platform-specific.
When a class or interface has a static section as well as a companion object, then the initialization of the
companion object instance also happens in the program order of the companion object declaration with respect to
its placement in the source code. For example, the following code
prints 1
, 2
, 3
, 4
, 5
in order when the class Example
gets loaded:
Option: Static section syntax.
class Example {
static {
val x = run { println("1") }
init { println("2") }
}
companion object {
val y = run { println("3") }
}
static {
init { println("4") }
val z = run { println("5") }
}
}
Option: Static modifier syntax.
class Example {
static val x = run { println("1") }
static init { println("2") }
companion object {
val y = run { println("3") }
}
static init { println("4") }
static val z = run { println("5") }
}
Rationale: mixing of companion objects with statics is expected to be rare in practice, mostly driven by migration. The proposed program-order-initialization rule is easier to remember and gives developer flexibility in addressing the specific needs of initialization order in their code.
As it is usual for initialization in Kotlin, the process of static initialization may have cycles, leading to unspecified and platform-depended behavior when attempting to access not-yet-initialized values. Kotlin compiler may attempt to detect and report initialization cycles as compile-time warnings or errors, but is not strictly required to do so.
The body scope of the static function, static property getters/setter, and static init section
does not have this
reference and behaves similarly to a top-level declaration.
However, all the static declarations inside a class, interface, or (static) object can refer to all the declarations in the scope of their containing class, interface, or object by their unqualified name (see Static members and scope). For example:
Option: Static section syntax.
class Outer {
static {
val staticVal = "OK"
}
interface NestedInterface
class NestedClass : NestedInterface
object NestedObject
static {
fun staticFunction() {
this // ERROR: Unresolved
val ic = NestedClass() // OK
ic as NestedInterface // OK
NestedObject // OK
staticVal // OK: it does not matter that it is in a different static section, it is still the same scope
}
}
}
Option: Static modifier syntax.
class Outer {
static val staticVal = "OK"
interface NestedInterface
class NestedClass : NestedInterface
object NestedObject
static fun staticFunction() {
this // ERROR: Unresolved
val ic = NestedClass() // OK
ic as NestedInterface // OK
NestedObject // OK
staticVal // OK
}
}
The following declarations of classes and interfaces are collectively called static declarations:
- Static function and property declarations — static member declarations.
- Nested classes, interfaces, and objects (but not inner classes).
The rule of thumb, is that static declarations do not have access to their containing scope via
this
instance reference and cannot be accessed, themselves, using the reference to an instance of the scope. They are accessed statically in the corresponding scope or via the name of the corresponding scope.
The following basic rules drive the resolution of static declarations in Kotlin:
- Static declarations are available by their unqualified name from the code that is lexically contained inside the corresponding class or interface.
- Static declarations are NOT available automatically in the extensions of the corresponding classes or in places where an instance of the corresponding class is otherwise available as a receiver in the context.
- Static declarations are available with a qualified name of
<ClassName>.<unqalifiedName>
. - In case of ambiguity between a reference to a static declaration and a declaration from a companion object,
the reference to a static declaration wins. Companion object members can be always accessed using a name of the
companion object (e.g.
<ClassName>.Companion.<unqalifiedName>
) if needed. - Static declarations can be imported by a regular import directive either individually or using a
*
wildcard.
These rules consistently extend the current approach to resolution of existing static declarations in Kotlin (nested classes, interfaces, and objects) to the newly introduced concept of static members.
Example:
Option: Static section syntax.
package color
class Color(val rgb: Int) {
static {
val RED = Color(0xff0000) // static member of `Color` class
}
fun foo() {
RED // OK: can use simple unqualified name here
}
class NestedClass {
fun bar() {
foo() // ERROR: cannot access instance members without `this` reference
RED // Ok, can access static declarations via a simple name
}
}
}
fun Color.bar() {
RED // ERROR: not lexically inside `Color`, statics are not available by simple name
Color.RED // OK: via a qualified name
}
Option: Static modifier syntax.
package color
class Color(val rgb: Int) {
static val RED = Color(0xff0000) // static member of `Color` class
fun foo() {
RED // OK: can use simple unqualified name here
}
class NestedClass {
fun bar() {
foo() // ERROR: cannot access instance members without `this` reference
RED // Ok, can access static declarations via a simple name
}
}
}
fun Color.bar() {
RED // ERROR: not lexically inside `Color`, statics are not available by simple name
Color.RED // OK: via a qualified name
}
Now, in another file:
package user
import color.Color.* // import all static declarations inside Color
fun baz() {
RED // OK: was imported using wildcard
}
Note, that static members behave like nested classes, interface, and objects with respect to import and not like
companion object members. In the above example, if Color.RED
was declared in the companion object, then it
will not be accessible via import color.Color.*
, but would require import color.Color.Companion.RED
without
ability to import everything using *
, because star import is not supported for singleton objects.
All declarations inside a static object
are considered to be static declarations and behave just like
static declarations in classes and interfaces as explained above. The difference between them and regular (non-static)
object declarations is that it is possible to use star import. For example:
package com.example
static object Namespace {
fun doSomething() {
/* does something */
this // ERROR: Not available
}
}
In another file:
package user
import com.example.Namespace.*
fun main() {
doSomething() // OK: imported from a static object `Namespace` via star import
}
This means, that removing a
static
modifier to an object declaration is not a source-compatible change, as it can break some previously compiling code. Adding astatic
modifier is not a source compatible change either, becausestatic objects
lacks identity and cannot be referenced as explained in the section on Static objects. Neither change is binary compatible, too. See Migration of objects to statics section for further discussion.
Option: Static section syntax. Unlike companion objects, static sections themselves cannot have any visibility modifiers.
Static members of classes and interfaces are public
by default, just like regular instance members.
Their visibility can be reduced to internal
or private
with a similar effect as for instance members.
Internal static members are accessible only from the same module, while private members are accessible only
from the lexical scope of the containing class or interface.
Private static declarations are utilities that intended to be used only in the lexical scope of the corresponding class or interface.
Static members cannot have
protected
visibility.
Static declarations are not inherited. They can only be accessed using the name of the class or interface they are directly contained within.
However, class scope is linked to the scope of its parent classes in Kotlin, so inside the class it is possible to access existing static declarations (nested classes, interfaces, objects, companion object members, and Java statics) from its superclasses (but not from its superinterfaces) using a short name.
We do not plan to make it possible to access newly introduced static members (properties and functions) through such a parent class scope linking mechanism and, as a separate research, will see if it is feasible and desirable to deprecate such linking altogether in the future. See Deprecate superclass scope linking section for details.
For example:
Option: Static section syntax.
open class BaseClass {
object ObjectX // existing static declaration
static {
val x = 1 // static member
}
}
interface BaseInterface {
object ObjectY // existing static declaration
static {
val y = 2 // static member
}
}
class DerivedClass : BaseClass(), BaseInterface {
object ObjectZ // existing static declaration
static {
val z = 3 // static member
}
fun foo() {
ObjectX // OK, BUT: from a linked scope of a superclass `BaseClass`, might be deprecated in the future
ObjectY // ERROR: superinterface scope is not linked
ObjectZ // OK: from the scope for this class
x // ERROR: Static member is not available through a linked scope
y // ERROR: superinterface scope is not linked
z // OK: from the scope for this class
}
}
fun test() {
DerivedClass.z // OK: Declared in `DerivedClass`
DerivedClass.x // ERROR: Not declared in `DerivedClass`, not inherited
BaseClass.x // OK: Declared in `BaseClass`
}
Option: Static modifier syntax.
open class BaseClass {
object ObjectX // existing static declaration
static val x = 1 // static member
}
interface BaseInterface {
object ObjectY // existing static declaration
static val y = 2 // static member
}
class DerivedClass : BaseClass(), BaseInterface {
object ObjectZ // existing static declaration
static val z = 3 // static member
fun foo() {
ObjectX // OK, BUT: from a linked scope of a superclass `BaseClass`, might be deprecated in the future
ObjectY // ERROR: superinterface scope is not linked
ObjectZ // OK: from the scope for this class
x // ERROR: Static member is not available through a linked scope
y // ERROR: superinterface scope is not linked
z // OK: from the scope for this class
}
}
fun test() {
DerivedClass.z // OK: Declared in `DerivedClass`
DerivedClass.x // ERROR: Not declared in `DerivedClass`, not inherited
BaseClass.x // OK: Declared in `BaseClass`
}
Rationale: Statics can be always imported explicitly if needed. The linking of superclass scope (but not from superinterface) was a design decision that is inspired by the way in which statics are inherited from superclasses (but not from superinterfaces) in Java, which is a legacy design decision stemming from the fact, that Java interfaces did not have static initially. However, statics are resolved statically and any kind of their inheritance (even if it is called linking) is confusing, because it does not really take the actual run-time type into account.
Extension functions can be declared as static members, similarly to extensions that can be declared as instance members. Such extensions can be imported and/or resolved according to general rules on static declarations. Unlike extensions declared as instance members, extensions declared as static members have only a single extension receiver, and they are statically dispatched. They don't have a dispatch receiver.
For example:
Option: Static section syntax.
class Color(val rgb: Int) {
static {
// Extension declared in static section
private fun Int.toHexByte(index: Int) =
// `this` is a reference to an extension receiver of type `Int`
(this ushr (index * 8) and 0xff).toString(16).padStart(2, '0')
}
fun toString() = "(r=${rgb.toHexByte(2)}, g=${rgb.toHexByte(1)}, b=${rgb.toHexByte(0)}"
}
Option: Static modifier syntax.
class Color(val rgb: Int) {
// Extension declared with static modifier
private static fun Int.toHexByte(index: Int) =
// `this` is a reference to an extension receiver of type `Int`
(this ushr (index * 8) and 0xff).toString(16).padStart(2, '0')
}
fun toString() = "(r=${rgb.toHexByte(2)}, g=${rgb.toHexByte(1)}, b=${rgb.toHexByte(0)}"
}
Such extensions are useful as they allow to limit extension's scope to one class. Unlike extensions declared as instance members, extensions declared as static members can be used from inside other static declarations of the corresponding class. For example:
Option: Static section syntax.
class Outer {
static {
fun Int.extensionAsStaticMember() { /* implementation */ }
}
fun Int.extensionAsInstanceMember() { /* implementation */ }
object Nested { // a static declaration inside an Outer class
fun test() {
1.extensionAsStaticMember() // OK: Available in the static context
2.extensionAsInstanceMember() // ERROR: Not available in the static context
}
}
}
Option: Static modifier syntax.
class Outer {
static fun Int.extensionAsStaticMember() { /* implementation */ }
fun Int.extensionAsInstanceMember() { /* implementation */ }
object Nested { // a static declaration inside an Outer class
fun test() {
1.extensionAsStaticMember() // OK: Available in the static context
2.extensionAsInstanceMember() // ERROR: Not available in the static context
}
}
}
Static extensions are declared similarly to regular extension functions with <ClassName>.static
as the
receiver type. While regular extensions are declared with <ClassName>
as the receiver type and are somewhat analogous to
putting a member into class scope, static extensions are declared with <ClassName>.static
as the receiver type,
as they are somewhat analogous to putting a member into a static scope of the class.
With static section syntax for static members, the syntax for static extensions is designed to be mnemonic with respect to how static section is declared inside a class, analogous to putting a member into a
static {}
section inside the class.
The static
here is a soft keyword. See Static soft keyword ambiguity section
for implications on backwards compatibility.
The body for the static extension can use static members of the corresponding class by their short name. This is similar to a traditional extension that puts a reference to the class into scope, making all the instance members from the corresponding class available by their short name. Here the scope contains a phantom static implicit receiver of the corresponding class, making all the static members of the corresponding class available by their short name.
fun Color.static.parse(s: String): Color { // static extension
RED // OK: unqualified `Color.RED` reference to a static member works here
this // ERROR: Unresolved: static extension has no real receiver
rgb // ERROR: Unresolved: instance members of the `Color` class are not present in its phantom static receiver
// ...
}
The phantom static implicit receiver is added as a phantom receiver to the receiver chain with the same priority as regular receiver for the regular extension function would have been added. See the receivers section in the specification for details.
The reference to static scope <ClassName>.static
cannot be used by itself as a value or as type.
See Static scope projection reference for
discussion on potentially allowing it in the future.
Static extensions cannot have operator
modifier.
See Static operators for potential lifting of this restriction in the future.
Static extensions are defined on a class, not on a specific type. That means, that when the generic class is being extended only the class name is being specified. For example:
class Box<T>(val value: T)
fun <T> Box.static.of(value: T): Box<T> = Box(value)
// ^^^ It is an error to write Box<T> here
Static extensions can be declared for Static objects in exactly the same way
as Static extensions for classes and interfaces using <StaticObjectName>.static
as receiver.
They follow all the same rules as other static extensions.
For example:
static object Namespace {
// static member
fun doSomething() {}
}
// extension on static object
fun Namespace.static.ext() {
this // ERROR: It has no receiver
doSometing() // OK: Via phantom static implicit receiver
}
Rationale: the requirement to have an explicit
.static
modifier makes it explicit for the reader of this extension function that it has nothis
receiver in scope without having to find the declaration of the object that is being extended to verify that it is a static object. It will also aid with Migration of objects to statics as the extensions on the objects that are being migrated to static objects can clearly and independently migrate from being regular extensions to static extensions.
Static extensions can be called in several ways. One way is using making a qualified call with
<ClassName>.<extensionName>
as in, for example, Color.parse(s)
. Similarly to the regular extensions, the
corresponding static extensions has to be imported in order for this call to be resolved.
For example, a package ext
declares extension on color.Color
:
package ext
import color.Color // import `Color` to use it by short name
// Declares `parse` in `ext` package as a static extension on `color.Color`
fun Color.static.parse(s: String): Color { /* implementation */ }
Now, a package users
imports both color.Color
and ext.parse
in order to use it:
package user
import color.Color // import `Color` to use it by short name
import ext.parse // import `parse` static extension to resolve it
val yellow = Color.parse("yellow") // OK: Call resolves
Static extensions can be called by their short name from the scope of the corresponding classes when imported, for example:
package color
import ext.parse // import `parse` static extension to resolve it
class Color(val rgb: Int) {
fun test() {
parse("test") // OK: Resolves to static extension call
}
}
Static extensions on a class can be also called by their short name from the scope of other static extensions on the same class. For example:
package ext2
import color.Color // import `Color` to use it by short name
import ext.parse // import `parse` static extension to resolve it
fun Color.static.anotherExtension() {
parse("test") // OK: Resolves to static extension call
}
The call to the static extension of a class C
, unlike the call to the static member, does not cause the
class C
to be initialized. For example, given the following class declaration.
Option: Static section syntax.
class C {
static {
val property = println("initialized")
}
}
Option: Static modifier syntax.
class C {
static val property = println("initialized")
}
The following code produces output as given in the comments:
fun C.static.foo() = println("foo")
fun main() {
C.foo() // Prints foo
C.property // Prints initialized
}
Unlike static members, static extensions can be called by their short name only in two contexts as explained in the above Static extensions resolution and import section:
- From inside the lexical scope of the corresponding class.
- From inside the lexical scope of another static extension on the corresponding class.
While static members can be imported anywhere to be used by their short name, static extensions do not have such a
capability. In other contexts static extensions are used only via <ClassName>.<unqualifiedName>
syntax.
For example:
Option: Static section syntax.
package exmp
class Example {
static {
fun staticMember() {}
}
}
fun Example.static.staticExtension() {}
Option: Static modifier syntax.
package exmp
class Example {
static fun staticMember() {}
}
fun Example.static.staticExtension() {}
In another package we must import exmp.Example
in order to make unqualified class name Example
available
first, then we can import a static member and a static extension:
package test
import exmp.Example
import exmp.Example.staticMember
import exmp.staticExtension
fun test() {
Example.staticMember() // OK
Example.staticExtension() // OK
staticMember() // OK, because of import exmp.Example.staticMember
staticExtension() // ERROR
}
It does no matter what kind of import is used — using star imports leads to the same results.
If Static scope projection reference support is added to this design in the future, then scope functions could be used bring in static scope into arbitrary places in the code, thus allowing for usage of static extensions by their short name in those places of the code.
Static extensions can be declared as members similarly to how
regular extensions can be declared as members.
Such extensions have a dispatch receiver on the corresponding class in scope as a member,
giving them access to the members of the corresponding class, and a phantom static implicit receiver
as a static extension. For example, the following code declares a static
extension property on Color
as a member of class Widget
:
class Widget {
private val backgroundColor: Color = initializeBackgroundColor()
// static extension property on Color as a member in Widget
val Color.static.background: Color
get() = backgroundColor
}
With such a declaration, any code in the scope of Widget
can refer to the widget's background color as
Color.background
, which creates a DSL for uniform access to all colors via Color.<xxx>
references in code.
One of the most confusing aspects of adding statics to Kotlin is that there will be to similarly looking, yet different concepts Static extensions and Extension functions as static members.
For example:
Option: Static section syntax.
class Example {
static {
fun Int.ext1() { // extension declared as static member
this // has type Int
}
}
fun Int.static.ext2() { // static extension declared as member
this // has type Example
}
}
Option: Static modifier syntax.
The confusion is more apparent with the static modifier syntax, which makes their declaration very much alike:
class Example {
static fun Int.ext1() { // extension declared as static member
this // has type Int
}
fun Int.static.ext2() { // static extension declared as member
this // has type Example
}
}
To summarize the difference between them:
-
Extension declared as static member (
ext1
) operates on an instance of the class it extends (this: Int
in the example) and has no reference to the outer class (Example
) in its scope. It can be used only from the lexical scope of the containing class or its static extensions, or if imported (viaimport Example.ext1
), using a qualified call on an instance of a class it extends (intInstance.ext1()
). -
Static extension declared as member (
ext2
) operates in the context of an instance of an outer class (this: Example
) and has no reference to an instance of the class its static scope it extends (Int
). It can be uesd only when the instance of the outer class (Example
) is in scope using qualified call on a class it statically extends (Int.ext2()
).
We expect that introduction of statics and static extensions in Kotlin will have a major impact on the style of Kotlin libraries, as well as on the style of the Kotlin standard library itself.
Statics will replace companion objects in most use-cases. The majority of uses of companion object in Kotlin libraries will be migrated to statics. Statics will become the new default that is being taught and promoted to Kotlin developers as a means to structure their declarations and to add class-related utilities.
The actual design for the migration mechanism is TBD. See Migration of objects to statics for details.
The remaining use-cases for companion objects are DSLs that want to attach a custom instance implementing
a certain interface (or extending a certain class) to the classes that are declared with that DSL. For example,
a database-related DSL might want to be structured in a way where a User
is both a class, representing the type
of the user entities with its properties, and an instance that can be used in expressions like query(User)
.
The Kotlin standard library and Kotlin coroutines have one such DSL where elements of the
CoroutineContext
, like a
Job
,
serve both as a type and as a value, so you can write concise and clear code like:
val currentJob: Job = coroutineContext[Job]
// ^^^ type ^^^ value of companion object
These are all sophisticated use-cases, so the concept of a companion object will be moved into advanced section of teaching materials and can be omitted from the curriculum for beginner developers entirely.
In the future, Static interfaces and static overrides might replace the companion objects entirely.
It is now customary to complement interfaces, like List
from the standard library, with top-level functions
that provide instances of these interfaces, like emptyList()
and listOf()
that all return List
instances.
Semantically, these functions are tightly related to the corresponding interface, but syntactically they are
not tied to the interface List
itself.
With statics, it becomes possible to declare static extension function List.empty()
instead of
emptyList()
,
List.of()
instead of
listOf()
,
etc. The advantage of such transition is that it becomes easier to discover
all List
-related top-level functions just by typing List.
and using IDE autocompletion. Moreover,
3rd party library can add their domain-specific functions as static extensions so that they
will be available with List.xxx
syntax as well.
In effect, the implicit knowledge that the functions like emptyList()
and listOf()
are related to the List
interface will become explicitly represented in the syntax, as those all those functions will be declared
as static extensions of the List
interface.
This explicit connection will also improve documentation, as it will become possible to automatically group all
the List
-related static functions and static extensions into a single section in the docs.
Migration from the top-level functions to the corresponding static extensions can be performed using the
regular deprecation and replacement. However, migration of the standard library functions like emptyList()
to
static extensions like List.empty()
will have to be performed with care. They are so popular in the existing
Kotlin code, that even giving deprecation warning on them will be too disruptive. We will need
some new mechanism to make this transition smoother, see KT-54106
Provide API for perpetual soft-deprecation and endorsing uses of more appropriate API.
The other common Kotlin pattern are so-called constructor functions. Using the List
example again, there
is a List(size: Int, init: (index: Int) -> T): List<T>
constructor function for the List
interface. Today, its connection to the List
interface is only apparent from
its name and return type and is not explicit in the language.
If support for static operators is added in the future, it will become possible to declare it
as a static extension operator fun <T> List.static.invoke(...)
, thus preserving the call-site List(...)
usage,
but making it explicitly connected to the List
interface.
Authors of classes will have a choice of writing static members or static extensions for their classes.
For example, an author of the Color
class can declare fun Color.static.parse(s: String): Color
function
as a static extension, or they can declare it as a static member. There will not be much difference for
the users of Color
class, in either case users are calling the function as Color.parse(s)
.
This is similar to a stylistic choice between an instance member and an extension:
- Declare a static member when the function need to access the private implementation details of the class or otherwise constitutes a core part of its API that cannot be conceptually declared as an extension by a 3rd party. For example, when the implementation, even though relying only on public API, is tied to non-documented internal implementation details that are subject to change in the future updates.
- Declare a static extension when the function is a utility that is provided on top of the core API of the class and which anyone could have written themselves if it was not provided by the original author of the class.
This sections describes the compilation strategy on JVM in details.
On JVM all static class and interface members are compiled as the static methods on JVM, including Extension functions as static members. For example:
Option: Static section syntax.
class Outer {
static {
val x = 0
fun foo(x: Int) {}
fun Int.ext() {}
}
}
Option: Static modifier syntax.
class Outer {
static val x = 0
static fun foo(x: Int) {}
static fun Int.ext() {}
}
Get compiled as the following equivalent Java code:
public class Outer {
private static int x = 0;
public static int getX() { return x; }
public static void foo(int x) {}
public static ext(int $this$ext) {}
}
Static initialization sections are compiled into the corresponding static initialization method on JVM, preserving the program order of all static initializers, including companion object initialization.
Note, that JVM 1.8 supports private static interface members, even though the Java language supports them starting only from Java 9, so all Kotlin static members can be compiled for all supported JVM targets.
On JVM Static objects are compiled as utility classes with static members. For example:
static object Namespace {
val property = 42
fun doSomething() {}
}
Get compiled as the following equivalent Java code:
public class Namespace {
private static int property = 42;
public static int getProperty() { return property; }
public static void doSomething() {}
}
Note, that similarly to Kotlin file-classes (class XxxKt
that is produced when compiling top-level declaration
from Kotlin file Xxx.kt
), we will not generate private constructor in those utility classes for static objects.
So, it would be possible to create an instance of static object class from Java, but the instance will be of no
practical use, as its methods are static and the type of class itself is not used anywhere.
Initialization sections of static objects are compiled into the corresponding static initialization method on JVM, preserving the program order of all static initializers.
In Kotlin, the reference to the static object and the type of the class to which the methods are compiled
cannot exist, but it can accidentally get exposed to Kotlin code via Java, which sees Kotlin static objects
as regular classes. In this case Kotlin compiler will report an error — the same
error that happens when Kotlin file-classes XxxKt
get exposed to Kotlin compiler via Java. For example:
// Java code
public class Expose {
// an instance of Kotlin static object class
public static Namespace NAMESPACE = new Namespace();
}
// Kotlin code
fun main() {
Expose.NAMESPACE // ERROR: Cannot access class 'Namespace'
}
On JVM constant static properties of classes, interface, and static objects get compiled directly into public static fields without the corresponding getter. For example:
Option: Static section syntax.
class Color(val rgb: Int) {
static {
const val BITS = 24
}
}
Option: Static modifier syntax.
class Color(val rgb: Int) {
static const val BITS = 24
}
Get compiled as the following equivalent Java code:
public class Color {
// constructor and fields are omitted
public static final int BITS = 24;
}
Note: It means that adding/removing Kotlin
const
modifier is not a binary compatible change on JVM.
On JVM Static extensions get compiled as the corresponding static or member functions of the containing file class or class (depending on the placement in the source code). The phantom static implicit receiver is not represented in the JVM function signature in any way. They entirely look like functions without any receiver.
The static extensions are supposed to have short names, as their effective namespace is confined to the
static scope of the class they extend. So, in order to reduce the incidence of JVM platform declaration
clashes and to make stack-traces easier to read, the default JVM name of the static extension is defined as
the short name of the class being extended, $
(dollar sign), and the function name.
For example:
// In file Parsing.kt
fun Color.static.parse(s: String): Color = TODO()
Get compiled as the following equivalent Java code:
public class ParsingKt {
public static Color Color$parse(String s) { /*...*/ }
}
If the static extensions need to be used a part of the public Java API, then the @JvmName
annotation can be used,
when applicable, to specify the desired JVM name. For example:
// In file Parsing.kt
@JvmName("parseColor")
fun Color.static.parse(s: String): Color = TODO()
Get compiled as the following equivalent Java code:
public class ParsingKt {
public static Color parseColor(String s) { /*...*/ }
}
Static extensions as members receive the same treatment. For example:
class Widget {
val Color.static.background: Color
get() = TODO()
}
Get compiled as the following equivalent Java code:
public class Widget {
public Color getColor$background() { /*...*/ }
}
Here, the compound JVM name Color$background
of this static extension gets further processed to produce the JVM name
of the getter method. Other kinds of JVM name mangling, like the mangling scheme used for functions with inline or
value class parameter types, would likewise receive the compound name of the static extension as an input.
Alternative schemes for the names of static extensions on JVM are discussed in Mangling scheme for static extensions on JVM.
Static members of classes, interfaces, and static objects interact with JVM-specific annotations
from the kotlin.jvm
package as explained below. The only special cases are the following:
@JvmName
is supported, including on static interface members (note: not supported on interface instance members).@JvmOverloads
is supported, including on static interface members (note: not supported on interface instance members).@JvmStatic
is not supported, as they are already compiled to static members.@Transient
is not supported, as static properties are not a part of serialized instance state.
Other JVM-specific annotations are supported normally on static members just like on regular instance members and
top-level declarations, including @JvmField
, @JvmSynthetic
, @JvmSuppressWildcards
, @JvmWildcard
,
@Synchronized
, @Volatile
, @Strictfp
, @Throws
.
The overwhelming majority of popular and growing languages support some form of static declarations. However, there are differences in how static methods are represented syntactically and how the calls to static methods are dispatched.
We say that the static method dispatch is always static if the call to a static method in a specific call-site is resolved at a compile-time or link-time to a single declaration. We say that static methods support dynamic dispatch if some forms of static methods calls can call different static method declarations depending on the runtime class that the code operates with.
C++, C# and Java use static
modifier keyword to designate static members of classes (and interfaces in Java and C#).
They are always dispatched statically using the call-site syntax of ClassName.method()
in C# and Java
and ClassName::method()
in C++.
C# 11 (.NET 7) has introduced
static virtual members in interfaces
which allow static declarations to be overridden and their calls can be dynamically dispatched in generic methods
using T.method()
call-site syntax when T
is a type parameter.
See Static interfaces and static overrides
for potential introduction of a similar feature in Kotlin in the future.
JS/TS uses static
modifier keyword to designate static members of classes.
In JS/TS static methods are reified as members of the corresponding class object, so they can be overridden
in subclasses and their calls can be dynamically dispatched when the reference to the class object is passed at runtime
using cls.method()
call-site syntax where cls
can be any expression that provides the reference to the target class.
The target class for cls.method()
call can be retrieved via ClassName
syntax for statically known class or
via obj.constructor
syntax for a run-time object.
Python uses @classmethod
and @staticmethod
decorators to declare static class members, with the only difference that
@classmethod
also receives a reference to the corresponding Python class as its first parameter.
Both kinds of static methods become members of the class objects and can be called dynamically
in a way that is somewhat similar to JS/TS using cls.method()
syntax.
Swift uses static
and class
modifier keywords to designate static members of classes and protocols.
Calls to static members of classes are statically dispatched when ClassName.method()
syntax is used.
Swift class
methods (only in classes) can be overridden in subclasses and their calls are dynamically dispatched
when type.method()
syntax is used. Type references can be retrieved via ClassName.Type
syntax for a
statically known class or via type(of: obj)
for a run-time object.
Swift protocols can only declare static
methods which serve as requirements for the classes that
adopt the corresponding protocols (both class
and static
methods in an adopting class meet the requirement),
and they are dynamically dispatched using type.method()
syntax.
There are no statically dispatched protocol members in Swift. There is no call-site syntax of ProtocolName.method()
.
Rust distinguishes static and instance members by the presence of the first &self
/self
parameter in a method
declaration. Calls to static members in Rust are statically dispatched using ClassName::method()
syntax.
Static methods of Rust traits are dispatched dynamically.
There are no statically dispatched trait members in Rust. There is no call-site syntax of TraitName::method()
.
This section gives an overview of potential future extensions that have narrower uses are not likely to be implemented in the initial release, but can be added later if needed. It does not go into the details of their design.
In the initial design presented in this KEEP, the <ClassName>.static
syntax is only used
to declare Static extensions on classes and interface, and
Extensions on static objects.
<ClassName>.static
syntax can be extended in the
future with value semantics to represent both the value and its own type.
We'll call it static scope projection since it refers to static declarations from class, interface, or static object.
As a value, static scope projection can be used to bring the static scope of other classes, interfaces, or objects into the code block using scope functions, similarly to wildcard import, but with local effect.
For example:
fun test() {
with(Color.static) { // bring static scope using a scope function
RED // can access static member of Color via a simple name
}
}
A static scope projection value does not have a stable identity and is internally implemented as a special-purpose value class whose scope contains all the static declarations from the scope of the corresponding class.
The value of the static projection <ClassName>.static
it the only member of its own type <ClassName>.static
that is a direct subtype of Any
. The types of different static scope projections are always unrelated despite
relations of the corresponding classes. For example, even if class B
is subclass of class A
, the B.static
type
is not a subtype of A.static
type.
In the initial design presented in this KEEP, static methods cannot be overridden and cannot be dispatched dynamically, static objects cannot implement interfaces. This can be changed by introducing a concept of a static interface. The prerequisite of this feature includes support for Static scope projection reference. For example:
static interface Parseable<T> {
fun parse(s: String): T // static interface with `parse` function
}
Unlike regular interfaces, that establish requirements for instance methods, static interfaces will establish requirements for static methods and could be implemented by the static methods of classes, interfaces, or static objects. For example:
Option: Static section syntax.
class Color(val rgb: Int) : Parseable<Color> {
static {
override fun parse(s: String): Color { /* impl */ }
}
}
Option: Static modifier syntax.
class Color(val rgb: Int) : Parseable<Color> {
static override fun parse(s: String): Color { /* impl */ }
}
When a class implements a static interface its instances do not implement the static interface, but its static scope projection does. For example:
fun main() {
val parser: Parseable<Color> = Color.static // OK
val color = parser.parse("red") // OK
println(color is Parseable<Color>) // Prints "false"
}
Introduction of static interfaces and static overrides might allow for a complete replacement of companion objects
and their eventual deprecation. In this case, instead of Color.static
syntax for
Static scope projection reference we might allow the name of the class itself,
like Color
, to be used as a reference to the static scope in expressions. For example, as was discussed
in Statics and the future role of companion objects section,
as DSL for database access that needs a User
class to be usable both as a type and in expressions like query(User)
could declare a static interface that is required for all database entity classes to implement and accept an instance
of this static interface in its query
function.
In this initial design presented in this KEEP, the static methods and static extensions cannot have an operator
modifier.
However, some operators can have interesting use-cases when declared as static members or static extensions.
For example, as shown in the Constructor functions section, it
is worthwhile to allow static operator fun invoke(...)
in the future.
Other Kotlin features in the future might be totally reliant on static operators for their functioning.
For example, KT-43871 collection literals
can use static operator fun buildCollection(...)
declared on a type as a static member or a static extension to
figure out how the instance of the corresponding type shall be constructed from the collection literal syntax,
depending on the context in which the corresponding literal is used. For example:
val list: List<Int> = [1, 2, 3] // calls List.buildCollection(...) operator function
val set: Set<Int> = [1, 2, 3] // calls Set.buildCollection(...) operator function
This section lists open issues in the design and discusses alternatives for some of the design decisions.
The major open issue of this design is the declaration syntax for static members of classes and interface. There are two choices: static sections and static modifier. The following summary lists benefits of each of the choices:
Option: Static section syntax.
Benefits of static section syntax:
- It is easy to migrate existing companion object declarations to statics in cases when the companion object
instance was not really needed by simply replacing
companion object
withstatic
. The concept of static section should be easy to learn for existing Kotlin developers. - Currently established practice of grouping all statics together that is enforced by the companion object
concept will continue to be softly enforced with
static {}
section, yet providing some additional flexibility for large classes that can declare multiple static sections. - With
static {}
section syntax for static members declaration of static extension asfun SomeClass.static.ext()
becomes mnemonic. You can picture it in your mind as placing the extension into the static section of the corresponding class. - With static sections it becomes easier to make sense out of more complicated declarations like Extension functions as static members.
Disadvantages of static section syntax:
- Single static declarations become more visually verbose as they have to be wrapped into the static section, and all static declaration bodies acquire an additional level of indentation.
- Static section syntax is different from the statics in other languages, so it would put a learning barrier for Kotlin developers coming from other languages.
In order to see how the syntax of static methods and extensions work together with static section syntax, here is the showcase of various possible combinations:
class Example {
fun AnotherClass.foo1() {} // Instance extension for another class declared as member
fun AnotherClass.static.foo2() {} // Static extension for another class declared as member
static {
fun AnotherClass.foo3() {} // Instance extension for another class declared as static member
fun AnotherClass.static.foo4() {} // Static extension for another class declared as static member
fun foo5() {} // Static method
}
}
Option: Static modifier syntax.
Benefits of static modifier syntax:
- It makes statics in Kotlin work very much statics in other languages.
- The syntax of static modifier is more concise for occasional static declarations and consumes less horizontal space for writing their bodies.
Disadvantages of static modifier syntax:
- Migration from
companion object
to static modifiers will change every line of code that has static method declarations and their bodies due to change in indentation. The concept of statics will be very different from the concept of companion objects that Kotlin developers know and use nowadays. - The language will cease to enforce the placement of all the statics together, leading to move varied codebases with statics scattered around.
- The distinction between static methods
static fun foo()
and static extensionsfun SomeClass.static.ext()
becomes harder to discern and to visualize. It could be even more confusing in other combinations. For example, the distinction betweenstatic fun AnotherClass.ext()
(extension for another class declared as static member) andfun AnotherClass.static.ext()
(static extension for another class) will be harder to explain and to articulate in words, as they look and read very much alike.
In order to see how the syntax of static methods and extensions work together with static modifier syntax, here is the showcase of various possible combinations:
class Example {
fun AnotherClass.foo1() {} // Instance extension for another class declared as member
fun AnotherClass.static.foo2() {} // Static extension for another class declared as member
static fun AnotherClass.foo3() {} // Instance extension for another class declared as static member
static fun AnotherClass.static.foo4() {} // Static extension for another class declared as static member
static fun foo5() {} // Static method
}
Not an option: Support both static section and static modifier syntax.
Note, that supporting both static section and static modifier syntax is not an option, especially in the initial support of statics in Kotlin. This would move the choice of syntax to the coding style, which would make both the problem of picking a more suitable syntax and the problem of learning the Kotlin language even more complicated. We'll have to make once choice of syntax and accept all its disadvantages for the sake of having a consistent syntax across Kotlin codebases.
The design of callable references to static members via ::member
or ClassName::member
syntax is to be specified in
this KEEP later, but they should definitely become available with the initial release of statics.
One thing to note is that statics will solve another long-standing problems with companion objects related to references, see KT-9315 "A companion object should be able to make property references of containing class that are not prefixed by classname and not ambiguous".
In particular, the following code (adapted from the above issue) shall work with statics out-of-the-box:
Option: Static section syntax.
class Outer(val one: String, val two: String) {
static {
fun createMappings(): List<String> =
setOf(::one, ::two).map { it.name } // WORKS! No need to write Outer::one, Outer::two
}
}
Option: Static modifier syntax.
class Outer(val one: String, val two: String) {
static fun createMappings(): List<String> =
setOf(::one, ::two).map { it.name } // WORKS! No need to write Outer::one, Outer::two
}
Usage of static
soft keyword in the syntax of Static extensions creates the following
ambiguity:
class Example {
class static {} // a valid declaration of a nested class
}
// Currently parsed as extension on `Example.static` class.
fun Example.static.ext() {}
The detailed design on how to deal with this ambiguity is TBD. Initially, the compiler will have to parse this
code as it was parsed before, which will complicate the implementation of Example.static
construct as it'll require
the extra resolution step. We'll need to develop some kind of deprecation cycle to remove this ambiguity.
The reasonable approach to such deprecation is to deprecate all nested and inner classes, interfaces, and objects
with the name static
.
We expect a large number of regular objects and companion objects to be migrated to statics. This is easy in cases where the
corresponding objects were not part of the stable public API. However, change from companion object to
statics is not binary compatible, so it is not an option for stable libraries API. The same story is for
migration from regular object
declaration that are used as a namespace to static object
declarations
(e.g. Delegates
from the standard library).
So, we'll need to design a mechanism to perform such migration in gradual way. The most obvious approach it to
add some kind of @ToBeMigratedToStatics
declaration for objects (including companion objects) that has the
following effects:
- It makes it deprecated to use the value and the type of the corresponding object for any other purpose as directly calling its members.
- On JVM, it will add static bridges to all the members of such an object as if they were marked with
@JvmStatic
, with an important difference that the code will actually move into the static method, while the instance method will be retained for binary compatibility as a bridge. - On JVM, it will compile all new call calling such members to call the corresponding static method, so that when the members are made truly static, this code still links and works.
- It will allow importing members of migrating companion objects via
import ClassName.member
syntax, instead ofimport ClassName.Companion.member
syntax, the latter syntax being deprecated for companion objects under migration.
This way, the libraries can first mark their to-be-migrated object with this new annotation, then give plenty of time to their users to recompile their code, and then replace the objects with statics. The transition time depends on the library compatibility policy. In practice, it means that the Kotlin standard library will be forever in such migration mode for its old companion object declarations.
Support for static members, static extensions, and static objects in Kotlin reflection will have to be designed later and added to this document.
In Kotlin, class scope is linked to the scope of its parent classes in Kotlin, so inside the class it is possible to access existing static declarations (nested classes, interfaces, objects, companion object members, and Java statics) from its superclasses (but not from its superinterfaces) using a short name. For example:
open class Parent {
class Nested
}
class Derived : Parent() {
val x = Nested() // Works using short name of Nested
}
We should research how often this feature in used in practice and consider deprecating this feature.
The fix during deprecation will be to explicitly import the class in need via import Parent.Nested
or
to use its fully qualified name in code. IDE can be made smart enough to automatically add the corresponding
import when the short name is typed.
Static extensions on JVM section gives one scheme for mangling the name of static extensions so that it is readable nicely on JVM. It is not clear-cut decision on which scheme to choose, so here is a larger list of options. They vary in the separator that is used (or an absence thereof) and in the order of components. As example, we'll show JVM names for the following static extensions:
val Color.static.background: Color // Color.background
fun Color.static.parse(s: String): Color // Color.parse
fun <T> Box.static.of(value: T): Box<T> // Box.of
Alternative | Scheme | Color.background on JVM |
Color.parse on JVM |
Box.of on JVM |
---|---|---|---|---|
0 (proposed) | Class$name | getColor$background |
Color$parse |
Box$of |
1 | name$Class | getBackground$Color |
parse$Color |
of$Box |
2 | lower(Class)upper(name) | getColorBackground |
colorParse |
boxOf |
3 | nameClass | getBackgroundColor |
parseColor |
ofBox |
4 | Class_name | getColor_background |
Color_parse |
Box_of |
5 | Class::name | getColor::background |
Color::parse |
Box::of |
ABI for non-JVM platforms will have to be designed and added to this document. Kotlin/JVM platform provides the strictest backwards and forwards compatibility guarantees, as well as seamless two-way interoperability, hence the JVM ABI is taken care of first.
The static object
syntax from Static objects section of this proposal is one of its most
controversial parts with a number of alternative, but it is the result of a pragmatic compromise.
An early prototype of this design used a namespace
keyword instead of static object
and attempted to base the whole terminology around the concept of a namespace. So, static members were namespace members
and static extensions were namespace extensions. And here in lies the problem: static objects feature is a fringe part
of this design proposal. The feature that every Kotlin developer will use are static members, some will also use static
extensions, and only a few will use static objects. So, a good design has to get the most important,
the most actively used part of the proposal right — static members. However, it would be inappropriate
to call them "namespace members" in Kotlin. An overwhelming majority of popular programming languages calls
them static members, see statics in other languages. It is not in Kotlin design
philosophy to invent a new name for a long-established concept, even if that new name might somehow better
reflect the concept.
So, having made the decision to call static members what everyone calls the — static members, we come to the hard choice of picking the name for the concept of static objects. Now, remember that static objects are going to be a fringe, rarely used feature. Inventing a totally new name for the rarely used feature is not an option. To minimize the number of concepts in the language, it has to reuse the concept of statics.
So, static it is, but static what? The default way of grouping a bunch of functions and, optionally, some state
in Kotlin is through an object. In Kotlin, you can allocate an anonymous object using object {}
expression
or declare a named object using object XXX {}
declaration. In both cases, you get a thing that posses a
stable reference as many things in Kotlin do. But not all things in Kotlin posses a stable reference. Instances of
numbers in Kotlin and instances of other value classes are objects in Kotlin, but they don't have a stable
reference. So, having a stable reference might initially seem to be an integral part of being an object, but it is not.
This observation reflects the pragmatic approach to choosing defaults in Kotlin. It is easier to see in comparison with a hypothetical programming language that puts performance above pragmatics as the cornerstone of its design philosophy. In this hypothetical performance-centered language every feature that adds any kind of runtime costs would require developer to write extra code to acknowledge and be explicit about such cost. For example, a provision of stable reference for objects incurs costs, so in that hypothetical language objects would not have any stable reference by default, unless explicitly requested. However, in Kotlin defaults are chosen pragmatically, to reflect the most common usage for writing application-level code.
In Kotlin design philosophy application developers are primarily concerned with the business logic of their code and
performance is a secondary concern, if a concern at all. For performance-sensitive code Kotlin provides additional
opt-in capabilities to be used by experts who write performance-sensitive code. One such performance-oriented
capability is a value class
, and another such performance-oriented capability is a static object
.
They take away performance-affecting "provides a stable reference" feature of classes and objects respectively
for those cases where you can do away without it. If you take legal Kotlin code and replace value class
with class
and static object
with object
the code will mostly compile and work as before
(maybe with few non-essential differences), but will lose some efficiency.
It is perfectly fine if developer who wants to combine several functions into a common namespace uses object XXX {}
for that purpose. If they know about static objects they can gain a bit of additional efficiency by declaring
their functions inside a static object XXX {}
, but there is not much harm is they don't. That is another reason while
static objects do not deserve a separate concept on their own.
However, there are two feasible alternatives to static objects explained in details below: Anonymous static sections and Local packages.
We can consider an option of using static XXX {}
instead of a static object XXX {}
and name them "named static sections" instead of "static objects". So, the example from the
Static objects section is going to look like this:
static Namespace {
val property = 42 // static property
fun doSomething() { // static function
property // OK: Can refer to static property in the same scope
this // ERROR: Static objects have no instance
}
}
Advantages:
- Named static sections play nicely together with static section syntax.
static
is more concise thanstatic object
, so it is going to be easier to promote and teach it as a default solution for grouping functions together.
Disadvantages:
- It requires
static
to become a hard keyword on par withfun
,class
,interface
, etc. It is a more disruptive and more breaking change. - It breaks the spirit of Kotlin design that tries to minimize the number of core concepts, opting to modify them
instead. Kotlin does not have
enum
, butenum class
, Kotlin does not haverecord
butdata class
, etc.
Another alternative for the static object
concept is a "local package". That is, we can add package XXX {}
syntax
instead of static object XXX {}
. So, the example from the
Static objects section is going to look like this:
package Namespace {
val property = 42 // static property
fun doSomething() { // static function
property // OK: Can refer to static property in the same scope
this // ERROR: Static objects have no instance
}
}
The package and the static object are really similar — they contain the same kind of static declarations, they are imported in a similar way, etc.
Advantages:
- Reuses the existing concept of a packages as a way to group static declarations in a common namespace.
package
is more concise thanstatic object
, so it is going to be easier to promote and teach it as a default solution for grouping functions together.
Disadvantages:
- It breaks the naming conventions that are already established for packages and objects: lowercase for packages and camelcase for objects.
- It does not highlight an important difference that declarations from top-level packages are supposed to
be used by their short name as
<declarationName>
, while declaration from local packages are designed to be used in the qualified form of<LocalPackageName>.<declarationName>
. - It might cause confusion on JVM, as local packages will not be represented by packages on JVM.
- It adds an ambiguity to the grammar when a file without a package starts with a local package without body,
e.g. when the whole file consists of
package XXX
. This ambiguity is not hard to resolve in the favor of treating it as a package name declaration for a file, but it is an ambiguity nonetheless.