Generics & Bounds
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala
3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). The same programs and error demos were also compiled with
3.9.0 (the current LTS) and 3.3.8 (the previous LTS); the one difference, in the handling of the _ wildcard, is
called out where it occurs.
A generic class, trait or method has one or more type parameters: placeholders for types that the caller chooses. They
let one definition (Stack[A], Repository[K, V]) work for many element types while the compiler still checks every use.
A bound restricts which types a parameter may take, and so also what the code inside the definition may do with a value
of that type. This page covers the parameters and bounds themselves; how the variance annotations +A and -A make
Route[Path] a Route[Segment] is the subject of Variance, how the
compiler fills in type arguments is covered in Type Inference, and
type parameters that are themselves type constructors (F[_]) are in
Type Lambdas & Higher Kinds. The examples use the
trailhead scenario, a toolkit that plans hikes on a network of mountain trails. For the other JVM languages see
Java: Generics and
Kotlin: Generics & Variance.
Generic classes and traits
Type parameters go in square brackets after the name of a class, trait or enum. Scala uses [] where Java and Kotlin use
<>, which frees < and > to be ordinary operators. By convention a lone parameter is A (Scala’s standard library) or
T, and several parameters are K, V, A, B, or the initials of their roles.
//> using scala 3.10.0
package trailhead
case class Trail(name: String, lengthKm: Double, ascentM: Int)
// A generic class: A is chosen at each use
class Stack[A](private val items: List[A] = Nil):
def push(a: A): Stack[A] = Stack(a :: items)
def pop: (A, Stack[A]) = (items.head, Stack(items.tail))
def isEmpty: Boolean = items.isEmpty
def size: Int = items.size
override def toString = items.mkString("Stack(", ", ", ")")
// A generic trait with two parameters
trait Repository[K, V]:
def find(key: K): Option[V]
def all: List[V]
// A subclass can fix one parameter and keep the other open
class InMemory[V](data: Map[String, V]) extends Repository[String, V]:
def find(key: String): Option[V] = data.get(key)
def all: List[V] = data.values.toList
// A polymorphic method: the type parameter belongs to the method
def twice[A](a: A): List[A] = List(a, a)
def pairUp[A, B](a: A, b: B): (A, B) = (a, b)
def firstOr[A](xs: List[A], default: A): A = xs.headOption.getOrElse(default)
@main def run(): Unit =
val s1 = Stack[Int]() // explicit type argument
val s2 = s1.push(1).push(2) // A = Int stays fixed
println(s2)
println(s2.pop._1)
val trails = Stack(List(Trail("Ridge", 8.5, 900))) // A inferred: List[Trail]
println(trails.size)
val repo = InMemory(Map("Ridge" -> Trail("Ridge", 8.5, 900)))
println(repo.find("Ridge"))
println(repo.find("Pass"))
println(twice("x"))
println(twice[Double](1)) // explicit: the Int literal becomes a Double
println(pairUp(1, "a"))
println(firstOr(List[Int](), 7))
Stack(2, 1)
2
1
Some(Trail(Ridge,8.5,900))
None
List(x, x)
List(1.0, 1.0)
(1,a)
7
Source: Scala 3 Book: Generics
The rules the example shows:
-
A type parameter belongs to the declaration that lists it. In
Stack[A],Ais in scope in the whole class body. Thepushmethod does not declare a type parameter of its own, sos1.push("x")on aStack[Int]is a type error. -
A class’s type arguments are inferred from the constructor arguments (
Stack(List(...))is aStack[List[Trail]]) andnewis not needed to create an instance (see Classes & Constructors). When the arguments say nothing, as withStack(), you write the argument yourself:Stack[Int](). -
A polymorphic method has its own parameters. The compiler infers them from the arguments (
twice("x")istwice[String]), and you may give them explicitly, as intwice[Double](1), which also decides how the literal1is typed. -
A subclass chooses what to do with each parameter of its parent.
InMemory[V]fixesKtoStringand keepsVopen, soRepository[String, Trail]andInMemory[Trail]describe the same service.
Without a bound, a type parameter has the bounds >: Nothing <: Any, so inside the definition the only things you can do
with an A are the ones every object supports: store it, pass it on, compare it with ==, and call toString, hashCode
and the other methods of Any. Bounds are how you ask for more.
Upper bounds
An upper bound A <: B says that A must be B or a subtype of B. Inside the definition the compiler then knows that
every A has the members of B.
//> using scala 3.10.0
package trailhead
sealed trait Segment:
def km: Double
case class Path(km: Double) extends Segment
case class Ridge(km: Double) extends Segment
case class Hut(km: Double, beds: Int) extends Segment
// Upper bound: every A must be a Segment, so km is available
def totalKm[A <: Segment](xs: List[A]): Double = xs.map(_.km).sum
// The bound keeps the precise type: the result is an A, not just a Segment
def longest[A <: Segment](xs: List[A]): A = xs.maxBy(_.km)
@main def run(): Unit =
val paths = List(Path(2.0), Path(5.0))
println(totalKm(paths))
val l = longest(paths) // Path, not Segment
println(l)
7.0
Path(5.0)
Source: Tour of Scala: Upper Type Bounds
The second method is the point of using a bound rather than an ordinary parameter. def longest(xs: List[Segment]): Segment
accepts the same argument but returns a Segment, forcing the caller to match on it to recover a Path. With
A <: Segment the result type is whatever the caller passed in. A type argument that violates the bound is rejected at the
call, with the error code E057:
//> using scala 3.10.0
sealed trait Segment:
def km: Double
case class Path(km: Double) extends Segment
def totalKm[A <: Segment](xs: List[A]): Double = xs.map(_.km).sum
@main def run(): Unit =
println(totalKm[String](Nil))
./C.scala:11:19
Type argument String does not conform to upper bound Segment
println(totalKm[String](Nil))
^
Source: Tour of Scala: Upper Type Bounds
When the type argument is inferred rather than written, the same mistake is reported at the offending argument:
./C.scala:11:24
Found: ("a" : String)
Required: Segment
println(totalKm(List("a", "b")))
^^^
Lower bounds
A lower bound B >: A says that B must be A or a supertype of A. It looks odd until you meet a covariant class. A
Route[+A] that lets you add a segment must not take an A, because A is covariant (it only appears in output
positions). Taking an A as input is an error:
//> using scala 3.10.0
class Route[+A](val segments: List[A]):
def prepend(b: A): Route[A] = Route(b :: segments)
./H.scala:3:15
covariant type A occurs in contravariant position in type A of parameter b
def prepend(b: A): Route[A] = Route(b :: segments)
^^^^
Source: Tour of Scala: Lower Type Bounds
The way out is a new type parameter that is allowed to be broader than A, and a result that is typed with it:
//> using scala 3.10.0
package trailhead
sealed trait Segment:
def km: Double
case class Path(km: Double) extends Segment
case class Ridge(km: Double) extends Segment
case class Hut(km: Double, beds: Int) extends Segment
class Route[+A](val segments: List[A]):
def prepend[B >: A](b: B): Route[B] = Route(b :: segments)
@main def run(): Unit =
val paths = List(Path(2.0), Path(5.0))
val r = Route(paths).prepend(Hut(0.5, 12))
println(r.segments)
List(Hut(0.5,12), Path(2.0), Path(5.0))
Source: Tour of Scala: Lower Type Bounds
What the compiler infers for B depends on the argument. The types of three calls on a Route[Path], revealed with the
Nothing-assignment technique of Type Inference:
| Call | Result type |
|---|---|
|
|
|
|
|
|
Adding a Path keeps the narrow type, adding a Hut widens to the common parent, and adding something unrelated widens
to a union type (see Union & Intersection Types). The
List class makes the same move in its own +:, :: and prepended methods, which is why 1 :: List("a") is a
List[String | Int]. If you want to refuse unrelated elements, combine the lower bound with an upper bound.
Combining bounds
A parameter can have a lower and an upper bound together, written in that order. Bounds can also use the compound types of Scala 3.
//> using scala 3.10.0
package trailhead
sealed trait Segment:
def km: Double
case class Path(km: Double) extends Segment
case class Ridge(km: Double) extends Segment
case class Hut(km: Double, beds: Int) extends Segment
// A context bound: an Ordering[A] must exist
def bestOf[A: Ordering](xs: List[A]): A = xs.max
// An upper bound and a context bound together
def widest[A <: CharSequence: Ordering](xs: List[A]): A = xs.max
// Both bounds on one parameter: a Segment that is at least a Path
def describe[A >: Path <: Segment](a: A): String = s"${a.km} km"
// F-bounded: A can be compared with its own kind
def largest[A <: Comparable[A]](xs: List[A]): A = xs.reduce((l, r) => if l.compareTo(r) >= 0 then l else r)
// An intersection as the upper bound
def arity[A <: Segment & Product](a: A): Int = a.productArity
// A union as the upper bound
def shout[A <: Int | String](a: A): String = a.toString.toUpperCase
// Singleton keeps the literal type
def literal[A <: Singleton](a: A): A = a
@main def run(): Unit =
println(bestOf(List(3, 9, 4)))
println(widest(List("ab", "abc")))
println(describe(Path(1.0)))
println(describe(Ridge(2.0)))
println(largest(List("alpha", "gamma", "beta")))
println(arity(Hut(1.0, 4)))
println(shout(1))
println(shout("a"))
println(literal(42))
println(1 :: List("a"))
9
abc
1.0 km
2.0 km
gamma
2
1
A
42
List(1, a)
The forms that can appear, with an example of each:
| Form | Meaning |
|---|---|
|
Both bounds: |
|
An F-bounded (self-referential) bound: an |
|
An intersection as bound: an |
|
A union as bound, which has no Java or Kotlin equivalent. |
|
|
|
|
|
A context bound, shorthand for an extra |
Source: Scala 3 Book: Generics; the singleton bound is in the Scala API: scala.Singleton
Bounds versus context bounds
A bound constrains the inheritance of the type: A <: Segment needs A to extend Segment. A context bound
A: Ordering constrains something else, the existence of a type-class instance Ordering[A] that the compiler finds
at the call site. The two compose: A <: CharSequence: Ordering is both. Prefer a context bound when the capability
comes from outside the type, as with Ordering[Int], because Int cannot be made to extend a trait you invent, and
prefer an upper bound when the type already carries the operations. The details are in
Context Bounds & Deferred Givens and
Type Classes.
Wildcards
A wildcard stands for an unknown type argument at a use site: List[?] is a list of something. It can carry its own
bounds, and these are the same as Java’s ? extends and ? super:
| Scala | Java | Kotlin |
|---|---|---|
|
|
|
|
|
|
|
|
|
//> using scala 3.10.0
def sizes(xs: List[?]): Int = xs.size
def paths(cs: Seq[? <: CharSequence]): List[Int] = cs.map(_.length).toList
def sink(xs: java.util.List[? >: Integer]): Unit = xs.add(1)
@main def run(): Unit =
println(sizes(List(1, "a", 2.0)))
println(paths(List("ab", StringBuilder("cde"))))
val l = java.util.ArrayList[Number]()
sink(l)
println(l)
3
List(2, 3)
[1]
Wildcards are used less in Scala than in Java, because Scala’s collections are covariant (a Seq[Path] is already a
Seq[Segment]) and a method can introduce a type parameter when it needs one. They matter for Java generics, which are
invariant and rely on wildcards to express variance, and for mutable Java collections like java.util.List. The sink
method above accepts a java.util.List[Number], java.util.List[Integer] and java.util.List[Object], which is exactly
the Java List<? super Integer>. In the opposite direction, Seq[? <: CharSequence] is, for a covariant Seq, the same
as Seq[CharSequence].
The _ spelling is deprecated
Scala 2 and early Scala 3 wrote List[_], Seq[_ <: CharSequence]. The _ was reassigned in Scala 3.4: it will in future be
the placeholder for an anonymous type parameter (like f(_) for a lambda), which is why wildcards moved to ?. Under 3.9.0
and 3.10.0 the old spelling still compiles with a warning:
[warn] ./D.scala:7:18
[warn] `_` is deprecated for wildcard arguments of types: use `?` instead
[warn] This construct can be rewritten automatically under -rewrite -source 3.4-migration.
[warn] def old(xs: List[_]): Int = xs.size
[warn] ^
Compiled with -source:future, the same line is an error. The compiler flags -rewrite -source 3.4-migration rewrite
the code. On 3.3.8 there is no warning, because _ has not been deprecated yet in the 3.3 LTS line, and ? is accepted
there too, so code that is written with ? compiles on every Scala 3 version. Scala 2 does not know ?, so cross-built
code must keep _ until it drops Scala 2.
Existential types are gone
Scala 2 also had existential types spelled forSome: List[T] forSome { type T }. Scala 3 removed them. A wildcard
covers the common cases, and the rest are expressed with a dependent type or a type member:
./G.scala:2:21
Existential types are no longer supported -
use a wildcard or dependent type instead
def old(xs: List[T] forSome { type T }): Int = xs.size
^^^^^^^
Evidence: =:= and <:<
Sometimes a method makes sense only for some type arguments of its class. A Leg[A] might have a sum only when A is
Int. A bound on the method cannot express that, because the class parameter is already fixed. The solution is an
evidence parameter: a given instance of the type A =:= B (A equals B) or A <:< B (A conforms to B). The
compiler creates one when the relation holds and reports an error when it does not, so the method simply does not
apply to the wrong types.
//> using scala 3.10.0
package trailhead
// A method that exists only when the type argument allows it
class Leg[A](val items: List[A]):
def sum(using ev: A =:= Int): Int = items.map(ev).sum
def flatten[B](using ev: A <:< List[B]): List[B] = items.flatMap(ev)
def widen[B](using ev: A <:< B): List[B] = items.map(ev)
def sameType[A, B](using A =:= B): String = "same"
def subtype[A, B](using A <:< B): String = "subtype"
@main def run(): Unit =
println(Leg(List(1, 2, 3)).sum)
println(Leg(List(List(1), List(2, 3))).flatten)
println(Leg(List("a", "b")).widen[Any])
println(sameType[Int, Int])
println(subtype[String, CharSequence])
println(subtype[List[String], Seq[CharSequence]])
// The standard library uses the same idea
println(Option(Option(3)).flatten)
println(List((1, "a"), (2, "b")).toMap)
6
List(1, 2, 3)
List(a, b)
same
subtype
subtype
Some(3)
Map(1 -> a, 2 -> b)
Source: Scala API: =:= and Scala API: <:<
Both relations are classes with an apply, so the evidence ev is also a function: items.map(ev) converts an A to an
Int, and that conversion is how the body gets to use the Int operations. Calling sum on a Leg[String] gives:
./E.scala:6:34
Cannot prove that String =:= Int.
println(Leg(List("a", "b")).sum)
^
Option.flatten and Iterable.toMap in the standard library are written this way: flatten needs
A <:< Option[B], and toMap needs A <:< (K, V). Scala 2 spelled the parameter (implicit ev: A =:= Int); the Scala 3 form uses using, and
the old form still compiles on 3.10.0.
Erasure and what generics cannot do
The JVM does not keep type arguments at run time. List[Int] and List[String] are both List after compilation. This
erasure is the same as in Java and Kotlin and it has the same consequences, which Scala handles in its own way.
//> using scala 3.10.0
import scala.reflect.ClassTag
def isListOfInt(x: Any): Boolean = x match
case _: List[Int] => true
case _ => false
def make[A: ClassTag](n: Int): Array[A] = new Array[A](n)
@main def run(): Unit =
println(isListOfInt(List("not", "ints")))
println(make[Int](3).toList)
println(List(1).getClass == List("a").getClass)
[warn] ./F.scala:5:8
[warn] the type test for List[Int] cannot be checked at runtime because its type arguments can't be determined from Any
[warn] case _: List[Int] => true
[warn] ^
true
List(0, 0, 0)
true
The compiler warns that case _: List[Int] is unchecked, and the program then answers true for a list of strings. The
two runtime classes are the same. In the same way a type parameter cannot be tested or instantiated:
//> using scala 3.10.0
def create[A](): A = new A()
def check[A](x: Any): Boolean = x.isInstanceOf[A]
./F.scala:2:26
A does not have a constructor
def create[A](): A = new A()
^
The second definition compiles with a warning ("the type test for A cannot be checked at runtime because it refers to an
abstract type member or type parameter"), and check[String](1) returns true. The remedies are:
-
ClassTag[A](a context bound[A: ClassTag]) carries the runtime class: it is what letsmake[Int](3)build anArray[Int], since arrays are the one place where the JVM does keep the element type. -
A type class, as in
def create[A](using f: Factory[A]), replacesnew A()with a call to a factory that the caller or the type provides. This is the Scala way to construct things generically. -
A
TypeTestor a sealed hierarchy makes a pattern match checkable (see Pattern Matching). -
A match on the shape that is checkable, such as
case _: List[?], followed by a check of the elements.
Primitive type arguments are not a problem: List[Int] is allowed, and the compiler boxes the elements when the generic
code needs objects, which is the same cost that Java pays for List<Integer>. Value classes and opaque types can avoid the
boxing of your own wrapper types (see
Opaque Types & Value Classes).
Java and Kotlin compared
| Topic | Scala 3 | Java | Kotlin |
|---|---|---|---|
Syntax |
|
|
|
Upper bound |
|
|
|
Several bounds |
|
|
|
Lower bound on a parameter |
|
not available (only on wildcards: |
not available |
Wildcards |
|
|
|
Declaration-site variance |
|
none |
|
Primitive type argument |
allowed ( |
not allowed ( |
allowed ( |
Runtime type tests |
erased; |
erased; |
erased; |
Higher-kinded parameters |
|
none |
none |
Source: The Java Tutorials: Bounded Type Parameters, The Java Tutorials: Wildcards and Kotlin: Generics
Three differences are worth singling out. Scala has declaration-site variance, so a class says once that it is covariant
instead of every user writing ? extends; Kotlin has the same, Java does not. Scala has lower bounds on ordinary type
parameters (B >: A), which is what makes covariant collections with prepend-style methods possible; Java and Kotlin
have only the wildcard forms. And Scala has no raw types: a class that takes a type parameter must be given one or be
written with ?.
Where the books are outdated
-
List[_]andSeq[_ <: Foo]are the pre-3.4 spelling. Scala 3 writesList[?]andSeq[? <: Foo], and deprecates_. Many Scala 2 articles and Stack Overflow answers still use it. -
Existential types written with
forSomeno longer exist in Scala 3. -
Evidence parameters are shown as
(implicit ev: A =:= Int)in Scala 2 books; in Scala 3 they are(using ev: A =:= Int). The older form still compiles. -
The Tour of Scala examples use
class Stack[A]withnew Stack[Int]; Scala 3 allowsStack[Int]()withoutnew. -
The type-class style
[A: Ordering]replaces(implicit ord: Ordering[A])in newer code; See Context Bounds & Deferred Givens. -
Articles that discuss type projections (
Outer#Inner) as a general feature are out of date: Scala 3 supports them only whenOuteris a class.
Scala 3.9 LTS versus 3.10
-
Generic classes, polymorphic methods, bounds,
?wildcards,=:=/<:<evidence andClassTagbehave the same on 3.3.8, 3.9.0 and 3.10.0: the programs and error demos above gave identical output. -
The
_wildcard deprecation warning appears on 3.9.0 and 3.10.0 (it was introduced for Scala 3.4) and not on 3.3.8, and it is an error under-source:futureon 3.10.0.