skip to content

Explain SpEL collection selection and projection. What do the .?[] and .![] operators do?

level: middleimportance: should knowfreq 45%

answer

  1. .?[] = selection/filter
  2. .![] = projection/map
  3. .^[] first, .$[] last match
  4. #this = current element
  5. Maps -> key/value on Map.Entry

basics

~10 s

Selection .?[expr] filters a collection, keeping elements where the boolean expr is true. Projection .![expr] transforms each element, producing a new collection of the results. Inside both, #this is the current element.

solid answer

~40 s

SpEL has two collection operators. **Selection** `collection.?[booleanExpr]` returns a new collection containing only elements for which the expression is true — like a filter/where. Variants `.^[...]` and `.$[...]` return just the first or last matching element. **Projection** `collection.![expr]` evaluates the expression for every element and returns a new collection of the results — like map/select. Inside both operators, `#this` refers to the current element being evaluated (you often access its properties directly, e.g. `#this.age` or just `age`). They work on arrays, `Collection`s, and even `Map`s (where each element is a `Map.Entry`, so you use `key`/`value`). Selection and projection can be chained, e.g. `users.?[age > 18].![name]` selects adults then projects their names. This is heavily used in `@Value` to derive lists and in security/cache expressions.

code

java · 19 lines
java
@Component
class Report {
    record User(String name, int age, boolean active) {}

    @Bean
    List<User> users() {
        return List.of(new User("Ann", 34, true),
                       new User("Bob", 17, false),
                       new User("Cy", 40, true));
    }

    // Selection then projection: names of active adults
    @Value("#{users.?[age >= 18 and active].![name]}")
    private List<String> activeAdultNames; // -> [Ann, Cy]

    // First match only
    @Value("#{users.^[age > 30].name}")
    private String firstOver30; // -> Ann
}

go deeper

for a junior

Recognize .?[] filters and .![] transforms; may not recall first/last variants.

for a middle

Correctly use both, know #this, chain filter-then-map, and know Map key/value handling.

for a senior

Explain return types, .^[]/.$[] single-match semantics, null/safe-navigation edge cases, and when to prefer streams over SpEL.

for a principal

Set guidance limiting SpEL collection logic in security/cache annotations for readability and testability, favoring extracted methods.

## The two operators SpEL provides first-class operators for working over collections without writing loops. ### Selection — `.?[ ... ]` (filter) Syntax: `collection.?[booleanExpression]`. It iterates the source and returns a **new collection** of the elements for which the boolean expression evaluates to `true`. - `list.?[age > 30]` → all elements whose `age` property is > 30. - **First match**: `list.^[age > 30]` → only the *first* matching element (single value, not a collection). - **Last match**: `list.$[age > 30]` → only the *last* matching element. ### Projection — `.![ ... ]` (map/transform) Syntax: `collection.![expression]`. It evaluates `expression` for every element and returns a **new collection** of those results. - `list.![name]` → a list of each element's `name`. - `list.![name + '!' ]` → transformed strings. ## `#this` — the current element Inside `.?[]` and `.![]`, the evaluation root is temporarily each element, so `#this` is the current item. You can write `#this.age` or, because property access falls through to the current object, simply `age`. Use explicit `#this` when you need the whole element (e.g. `.![#this]`) or to disambiguate. ## Works on Maps too When the source is a `Map`, each 'element' is a `Map.Entry`, so inside the brackets you use `key` and `value`: - `map.?[value > 10]` → a Map of entries whose value > 10. - `map.![key + '=' + value]` → a list of formatted strings. ## Chaining Because each operator returns a new collection, you can chain them: `members.?[active].![email]` → emails of active members. Order matters: filter first to reduce the set, then project. ## Result types - Selection over a `List`/`Collection` yields a `List`; over a `Set` yields a `Set`; over an array yields an array (of the same component type); over a `Map` yields a `Map`. - Projection always yields a `List` (or array for array input). ## Gotchas - **NPEs / missing property**: if an element lacks the property or is null, evaluation throws `SpelEvaluationException` unless you use safe navigation (`#this?.name`). - **`.^[]`/`.$[]` return null** when nothing matches (single-value selection), whereas `.?[]` returns an empty collection. - **`#this` vs `#root`**: inside the brackets `#this` is the element; `#root` still points at the original evaluation root object, which is useful to reference outside data while filtering. - **Performance / readability**: these are handy in configuration but become unreadable when nested deeply — prefer Java streams in real code. ## When to use Deriving small lists/filters in `@Value`, `@PreAuthorize` (e.g. checking a collection of authorities), or `@Cacheable` conditions. Avoid heavy logic here; SpEL isn't debuggable like Java.

  • How do selection/projection behave on a Map?
    Each element is treated as a Map.Entry, so inside the brackets you reference key and value. Selection returns a filtered Map; projection returns a List of the computed expressions, e.g. map.![key + '=' + value].
  • What does #this mean inside .?[] and .![]?
    It is the current element being evaluated. Property names inside the brackets resolve against that element, and #this gives you the whole element object (e.g. .![#this] or #this.name).

saying these in an interview costs you the question

  • Swapping the meanings: saying .![] filters and .?[] maps
  • Claiming #this refers to the whole collection
  • Thinking .^[] returns a collection (it returns the single first match)
  • Not knowing Maps expose key/value inside the brackets

context