Scala/Play, Part 6: Scala Collections for Java Developers - map, filter and find
After working through Option and for-comprehensions, I wanted to handle several policies together. Coming from Java, I already had the vocabulary of streams. Scala made the next step pleasantly direct: call map, filter and find on a List.
The useful distinction is what comes back: transformed values, matching elements, or one optional result. These examples come from my Scala 2.13.18 learning lab. The insurance data is fictional; it represents no company business rules.

One list, three policies
case class PolicySnapshot(
number: String,
basePremium: Int,
claimFreeYears: Int
)
val policies: List[PolicySnapshot] = List(
PolicySnapshot("POL-001", 600, 3),
PolicySnapshot("POL-002", 600, 2),
PolicySnapshot("POL-003", 500, 3)
)
List[PolicySnapshot] tells me the element type. List(...) uses the companion factory syntax we met earlier. The ordinary Scala List is an immutable, ordered linked list: these collection operations leave the original list intact. That does not magically freeze mutable objects someone might put inside another list.
map: turn every policy into a number
val numbers: List[String] =
policies.map((policy: PolicySnapshot) => policy.number)
// List(POL-001, POL-002, POL-003)
The function receives one policy and returns its number. Applying it to all three policies produces three strings, in the same order. The element type changes from PolicySnapshot to String.
I keep the explicit parameter type here so the function is easy to read. Scala can infer it, letting me write policies.map(policy => policy.number). Both expressions do the same work.
An empty input is ordinary too:
val noPolicies = List.empty[PolicySnapshot]
val noNumbers = noPolicies.map(policy => policy.number)
// List()
This result is an empty List[String]. There were no elements to transform, so the function was never called.
filter: use a Boolean to decide what stays
val experiencedPolicies: List[PolicySnapshot] =
policies.filter(
(policy: PolicySnapshot) => policy.claimFreeYears >= 3
)
val experiencedNumbers: List[String] =
experiencedPolicies.map(policy => policy.number)
// List(POL-001, POL-003)
A predicate is simply a function returning Boolean. Here the answers are true, false, true. filter uses those answers to retain POL-001 and POL-003. It returns policies, preserving their order; the subsequent map extracts their numbers.
The comparison that made this click for me was passing the same predicate to map:
val decisions: List[Boolean] =
policies.map(policy => policy.claimFreeYears >= 3)
// List(true, false, true)
map keeps the function's answers as values. filter uses them as inclusion decisions. If I raise the threshold to ten years, filter returns List(), while the original policies list still has three elements.

find: return the first match as an Option
val found: Option[PolicySnapshot] =
policies.find(policy => policy.number == "POL-002")
val missing: Option[PolicySnapshot] =
policies.find(policy => policy.number == "POL-999")
// found: Some(PolicySnapshot(POL-002,600,2))
// missing: None
This is where our earlier Option work pays off. A search can produce one policy or nothing, and its return type says exactly that.
val display: String = found
.map(policy => policy.number)
.getOrElse("Policy not found")
// POL-002
Notice the receiver of map: found is an Option, so this is Option.map. It transforms a present policy into a present string. On missing, the same chain reaches the fallback.
find means first in list order. Searching for at least three claim-free years returns POL-001 and stops, even though POL-003 also qualifies. It neither checks uniqueness nor selects the best policy. Use filter when you need every match.

The Java comparison I keep nearby
For equivalent Java objects with record-style accessors, I would express the same intentions using streams:
var numbers = policies.stream()
.map(policy -> policy.number())
.toList();
var experienced = policies.stream()
.filter(policy -> policy.claimFreeYears() >= 3)
.toList();
var found = policies.stream()
.filter(policy -> policy.number().equals("POL-002"))
.findFirst();
The last Java expression returns Optional. Scala's find returns Option. Scala List.map and List.filter execute eagerly and already return lists; they need no stream() or terminal toList() call. That detail matters when reading a chain: each List operation does its work immediately.
Run the examples
The examples and assertions are in ListLesson.scala at this lesson's commit. From the lab root, run:
sbt "runMain learning.PremiumLesson"
This entry point runs the earlier examples as well as ListLesson. Our run passed assertions for transformed values, retained policies, empty results, present and missing lookups, and the first-match behavior.
I now have three precise questions: what should each element become, which elements should remain, and which element is the first match? Next, I will bring the for-comprehension syntax I enjoyed into List examples.
Reference: Scala 2.13.18 List API. This is language groundwork for our Scala/Play series, whose target stack is Scala 2.13.18, Play 3 and Apache Pekko.