
by Theo Visagie
Kotlin's :: Operator — What It Actually Does
A no-nonsense look at Kotlin member references, from the basics to the patterns you'll actually use in real code.
Kotlin’s :: Operator — What It Actually Does
The :: operator in Kotlin lets you point at a function, property, or constructor without calling it. You get back a reference you can pass around, store, or invoke later.
It’s not a fancy feature you need to know about to be productive in Kotlin. But it does make certain patterns significantly cleaner, and once you see it in the wild, you’ll start spotting where it applies.
The Basics
The simplest form is an instance method reference:
class Greeter {
fun sayHello(name: String) = "Hello, $name!"
}
val g = Greeter()
val ref = g::sayHello // a reference, not a call
ref("Alice") // "Hello, Alice!"
That’s it. g::sayHello gives you a (String) -> String that you can assign to a variable, pass as an argument, or store somewhere. It’s equivalent to { name -> g.sayHello(name) }, just shorter and with no lambda overhead.
Bound vs Unbound
There are two flavors, and mixing them up is the most common source of bugs.
Bound references are tied to a specific instance:
val calc1 = Calculator()
calc1.name = "One"
val calc2 = Calculator()
calc2.name = "Two"
val add1 = calc1::add // always uses calc1
val add2 = calc2::add // always uses calc2
add1(5, 3) will always print “One is adding 5 + 3”. The instance is baked in.
Unbound references are tied to a class, not an instance. When you call them, the instance becomes the first parameter:
val addRef = Calculator::add // unbound
addRef(calc1, 5, 3) // (5, 3) are the original params, calc1 is 'this'
addRef(calc2, 5, 3) // same, different instance
The rule of thumb: bound references take the method’s original parameters. Unbound references take one extra — the instance.
Properties Are Not Functions
This trips people up because property references look similar but behave differently:
val person = Person("Alice", 30)
val nameRef = person::name // not person::name()
nameRef.get() // "Alice"
nameRef.set("Bob") // now person.name == "Bob"
Mutable properties (var) give you KMutableProperty0<T> with get() and set(). Read-only properties (val) give you just KProperty0<T> with get(). You can’t call them like functions — you call .get().
This matters because you might want a reference to the property rather than the current value:
val currentValue = person.name // "Alice" — a snapshot
val propertyRef = person::name // a live reference
person.name = "Bob"
println(currentValue) // still "Alice"
println(propertyRef.get()) // "Bob"
Constructors
::ClassName references the primary constructor:
data class Book(val title: String, val author: String, val pages: Int)
val maker = ::Book // (String, String, Int) -> Book
maker("Dune", "Frank Herbert", 688)
If the class has secondary constructors, ::ClassName resolves to the one that matches the call site’s signature — or you get a compilation error if it’s ambiguous. The primary constructor is the default choice.
Where It Actually Shines
Collection Operations
This is by far the most common use case. You’ll see it everywhere in Kotlin codebases:
val students = listOf(
Student("Alice", 85, passing = true),
Student("Bob", 42, passing = false),
Student("Charlie", 78, passing = true),
)
val names = students.map(Student::name)
val passing = students.filter(Student::isPassing)
Student::name is an unbound reference. When used with map, each student becomes the implicit this for the call, so it desugars to student -> student.name. Same pattern with filter — Student::isPassing becomes student -> student.isPassing().
Before member references, the idiomatic Kotlin equivalent was:
students.map { it.name }
students.filter { it.isPassing }
Both work. The member reference form is shorter, avoids the it ambiguity (especially with nested map calls), and compiles to essentially the same bytecode.
Event Handlers and Callbacks
If you’re wiring up event handlers, member references are cleaner than lambdas that just delegate:
class FormHandler {
fun onSave() { /* ... */ }
fun onCancel() { /* ... */ }
fun onNameChange(newName: String) { /* ... */ }
}
val handler = FormHandler()
saveButton.setOnClickListener(handler::onSave)
cancelButton.setOnClickListener(handler::onCancel)
nameField.setOnChange(handler::onNameChange)
Versus the lambda version:
saveButton.setOnClickListener { handler.onSave() }
cancelButton.setOnClickListener { handler.onCancel() }
nameField.setOnChange { newName -> handler.onNameChange(newName) }
The member reference version is shorter, has no closure allocation, and reads like a direct connection between the event and the handler. No “look, there’s a lambda that calls something” to parse.
Top-Level Functions
Even top-level functions can be referenced:
fun greet(name: String) = "Hi, $name!"
val names = listOf("Alice", "Bob")
names.forEach(::println)
names.map(::greet)
::println and ::greet work because they’re just named functions like any other member. The :: operator doesn’t care where the function is defined.
Extension Functions
Extension functions are members of the receiver type, so they reference the same way:
fun String.isPalindrome(): Boolean = this == this.reversed()
fun Int.isEven(): Boolean = this % 2 == 0
val phrases = listOf("racecar", "hello", "madam")
val palindromes = phrases.filter(String::isPalindrome)
val numbers = listOf(1, 2, 3, 4, 5)
val evens = numbers.filter(Int::isEven)
Again, the unbound reference plus the collection function does the heavy lifting — each element becomes the this for the extension call.
Gotchas
Don’t confuse x::foo() with x::foo. The () calls the function. Without it, you get a reference. This seems obvious until you’re at 2 AM and it isn’t.
Unbound references with functions that take parameters. If Dog.bark() takes no args, Dog::bark is (Dog) -> Unit. If Dog.eat(food: String) takes one arg, Dog::eat is (Dog, String) -> Unit. The instance is always the first parameter of the function type.
Property references are their own type. They’re not function types. KProperty0<T> has .get(), KMutableProperty0<T> has .get() and .set(). You can’t treat them as () -> T or (T) -> Unit directly.
:: doesn’t work with local functions. Only class members, object members, extension functions, and top-level functions. You can’t reference a function defined inside a fun main().
When to Reach for It
Use member references when:
- Passing a method as a callback or handler
- Using higher-order collection functions (
map,filter,groupBy) - You’re writing a lambda that just delegates to a method with no transformation
- You want to avoid closure allocation for performance-sensitive code
Don’t use them when:
- You need to transform parameters or combine them with other logic (use a lambda)
- You need to reference something that isn’t a named member (top-level functions, extension functions, and constructors are fine — but local functions aren’t)
- You’re already inside a lambda and adding
::makes it harder to read
TL;DR
:: points at a named member without calling it. Bound references (instance::method) keep the instance locked in. Unbound references (Class::method) accept the instance as the first parameter. They’re most useful for collection operations and event handlers, where they make code shorter, clearer, and a bit faster.