Why does `list.filter(String::isEmpty)` work for a `List<String>`, and how does the unbound `::isEmpty`-style reference satisfy `filter`'s expected `(String) -> Boolean` type?
answer
- filter wants (String) -> Boolean
- String::isEmpty is unbound -> (String) -> Boolean exact match
- Zero-arg Boolean member -> (T) -> Boolean reference
- Bound someStr::isEmpty is () -> Boolean -> does not fit filter
- Works for any/none/count/partition too
basics
~10 sString::isEmpty is a function that takes one string and returns true/false. filter calls it once per element, passing each string in. That matches exactly what filter wants.
solid answer
~40 s`filter` on a `List<String>` expects a predicate of type `(String) -> Boolean`. `String.isEmpty()` is a member with zero declared parameters returning `Boolean`, so its unbound reference `String::isEmpty` has type `(String) -> Boolean` — the receiver `String` becomes the single parameter. That is an exact structural match for the predicate slot, so `filter` accepts it and invokes it once per element, feeding each string as the receiver. Compare the bound form `someString::isEmpty`, which is `() -> Boolean` and would **not** fit `filter`. This pattern generalizes: any zero-arg `Boolean`-returning member yields an unbound `(T) -> Boolean` reference perfect for `filter`, `any`, `none`, `count`, or `partition`. Using `String::isNotEmpty` or `String::isBlank` works identically.
code
kotlin · 5 linesval xs = listOf("", "a", "", "bb")
val kept = xs.filter(String::isNotEmpty) // [a, bb]
val emptyCount = xs.count(String::isEmpty) // 2
println(kept)
println(emptyCount)go deeper
Knows filter runs the predicate per element and String::isEmpty returns a Boolean per string.
Explains the structural arity match (String) -> Boolean and why the bound form fails.
Extends the reasoning to any/none/partition and flags the nullable-receiver mismatch.
Discusses how expected-type-driven resolution chooses the unbound form and how variance/nullability of the receiver constrains where such references are usable.
## The expected type `List<T>.filter` has signature `filter(predicate: (T) -> Boolean): List<T>`. For `List<String>`, the predicate must be `(String) -> Boolean`. ## Why the unbound reference fits `isEmpty()` is declared on `CharSequence`/`String` with **no parameters** and returns `Boolean`. Its unbound reference `String::isEmpty` promotes the receiver to the only parameter: ```kotlin val pred: (String) -> Boolean = String::isEmpty listOf("", "a", "").filter(String::isEmpty) // ["", ""] ``` Arity check: declared params (0) + receiver (1) = 1, returning `Boolean` → `(String) -> Boolean`. Exact match. ## The bound form does NOT fit ```kotlin val s = "" val bound: () -> Boolean = s::isEmpty // list.filter(s::isEmpty) // ERROR: expected (String) -> Boolean, found () -> Boolean ``` The bound reference always returns the same answer (it is tied to one fixed string), so it is semantically useless as a per-element predicate and structurally has the wrong arity. ## Generalizing across the collection API Unbound predicate references compose throughout the standard library: ```kotlin val names = listOf("", "Ann", " ") names.any(String::isEmpty) // true names.none(String::isBlank) // false names.count(String::isNotEmpty) // 2 names.partition(String::isBlank) // (matching, nonMatching) ``` For transformations, the same idea applies with `map`: `names.map(String::trim)` uses `(String) -> String`. ## Receiver vs argument intuition The mental model: an unbound reference says "give me the object and I'll run the method on it." Higher-order functions like `filter` are happy to be that caller, supplying each element as the object. A bound reference already chose its object, so it has nothing left to vary per element. ## A subtlety: nullable receivers If the list were `List<String?>`, `filter` expects `(String?) -> Boolean`, and `String::isEmpty` (type `(String) -> Boolean`) would NOT match because the receiver type is non-null. You would need a lambda with a null check instead.
- Would `someString::isEmpty` work as the argument to `filter`? Why or why not?No. It is bound, so its type is `() -> Boolean`, which does not match `filter`'s expected `(String) -> Boolean` — wrong arity and it ignores each element.
- What if the list is `List<String?>`?`String::isEmpty` is `(String) -> Boolean` and will not satisfy `(String?) -> Boolean`. Use a lambda like `{ it.isNullOrEmpty() }` instead.
saying these in an interview costs you the question
- Saying the bound form fits `filter`
- Claiming `String::isEmpty` is `() -> Boolean`
- Not knowing the receiver becomes the predicate parameter
- Assuming it works unchanged for nullable element types
- Thinking `filter` passes the index, not the element