0Pricing
Scala for Backend Engineering & Functional Programming · Lesson

Generics and Type Parameters

Understand how to write generic classes and methods to create reusable components with type safety.

Generics and Type Parameters is a free Scala for Backend Engineering & Functional Programming lesson on CoddyKit — lesson 1 of 3. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Scala for Backend Engineering & Functional Programming learning path, one of 3 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What are Generics?

Imagine you need to create a container that can hold any type of item, like a String, an Int, or a custom object. Without generics, you'd either write a separate container for each type, leading to repetitive code, or use a very broad type like Any.

Generics allow you to write flexible, reusable code that works with various data types while still providing type safety at compile time. It's like a blueprint where you can plug in different materials later!

The `Any` Type Trap

Using Any as a placeholder for types can lead to problems. While it allows you to store anything, you lose the specific type information. This means you often need to cast the item back to its original type, which is error-prone and can cause runtime crashes if the cast is incorrect.

Try running this example and consider the potential issues:

class ItemHolder(val item: Any)

object Main {
  def main(args: Array[String]): Unit = {
    val stringHolder = new ItemHolder("Hello Scala")
    val numberHolder = new ItemHolder(123)

    // We need to cast, and it's unsafe if we get it wrong
    val retrievedString = stringHolder.item.asInstanceOf[String]
    println(s"Retrieved String: $retrievedString")

    // If we uncomment the line below, it compiles but will crash at runtime!
    // val wrongType = numberHolder.item.asInstanceOf[String]
    // println(s"Wrong Type: $wrongType")
  }
}

Introducing Type Parameters

Generics solve the Any type trap by introducing type parameters. These are like placeholders for actual types that you specify when you create an instance of a class or call a method.

In Scala, type parameters are defined using square brackets ([]) after the class or method name, typically using single uppercase letters like T, A, B, etc. For example, class Box[T] declares a generic class Box that takes one type parameter T.

Building a Generic Box

Let's create a simple generic class called Box that can hold any type of content. Notice how T is used throughout the class definition to represent the type of the content.

This allows us to create boxes for strings, integers, or any other type without writing separate classes for each.

class Box[T](val content: T) {
  def get: T = content
  override def toString: String = s"Box($content)"
}

object Main {
  def main(args: Array[String]): Unit = {
    val stringBox = new Box("Scala is fun!")
    val intBox = new Box(42)
    val booleanBox = new Box(true)

    println(stringBox)
    println(intBox)
    println(booleanBox)
  }
}

Type Safety with Generics

The biggest advantage of generics is compile-time type safety. When you create a Box[String], the compiler knows it should only contain strings. If you try to put an integer into it or retrieve it as an integer, the compiler will catch the error immediately.

This prevents many common runtime errors that occur with less type-safe approaches like using Any.

class Box[T](val content: T) {
  def get: T = content
}

object Main {
  def main(args: Array[String]): Unit = {
    val myStringBox: Box[String] = new Box("CoddyKit")
    val myIntBox: Box[Int] = new Box(100)

    // This works, type is String
    val s: String = myStringBox.get
    println(s"String from box: $s")

    // This works, type is Int
    val i: Int = myIntBox.get
    println(s"Int from box: $i")

    // The compiler prevents this line from compiling:
    // val wrong: String = myIntBox.get // Type mismatch error!
  }
}

Generics for Methods Too!

Just like classes, individual methods can also be generic. A generic method can take type parameters that are local to that method, allowing it to operate on different types without the entire class needing to be generic.

This is useful when you have a specific operation that can apply to various types, but the class itself isn't a generic container.

A Flexible `printPair` Method

Here's an example of a generic method printPair. It takes two type parameters, A and B, which allows it to print a pair of any two types. Notice how the type parameters are declared right after the method name.

This makes the method highly reusable for different combinations of data.

object Util {
  def printPair[A, B](first: A, second: B): Unit = {
    println(s"Pair: ($first, $second)")
    println(s"Type of first: ${first.getClass.getName}")
    println(s"Type of second: ${second.getClass.getName}")
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    Util.printPair("Hello", 123)
    Util.printPair(true, List(1, 2, 3))
    Util.printPair(4.5, 'C')
  }
}

Smart Type Inference

One of Scala's powerful features is its type inference. When you use generic classes or methods, you often don't need to explicitly specify the type parameters. Scala's compiler is smart enough to figure them out based on the arguments you provide.

This makes your code more concise and easier to read without sacrificing type safety.

class Container[T](val item: T) {
  def getItem: T = item
}

object Main {
  def main(args: Array[String]): Unit = {
    // Scala infers T as String
    val stringContainer = new Container("Inferred String")
    println(s"Content: ${stringContainer.getItem}, Type: ${stringContainer.getItem.getClass.getName}")

    // Scala infers T as Double
    val doubleContainer = new Container(3.14)
    println(s"Content: ${doubleContainer.getItem}, Type: ${doubleContainer.getItem.getClass.getName}")

    // You can also specify explicitly, but often not needed
    val explicitIntContainer: Container[Int] = new Container(500)
    println(s"Content: ${explicitIntContainer.getItem}, Type: ${explicitIntContainer.getItem.getClass.getName}")
  }
}

Restricting Generic Types with Bounds

Sometimes, you want a generic class or method to work only with types that have certain capabilities. For example, a method that adds numbers should only accept numeric types.

Type bounds allow you to restrict the types that can be used as type parameters. An upper bound (<:) means the type parameter must be a subtype of a given type. For example, [T <: Animal] means T must be Animal or a subtype of Animal.

class Animal { def speak(): String = "..." }
class Dog extends Animal { override def speak(): String = "Woof!" }
class Cat extends Animal { override def speak(): String = "Meow!" }

// This box can only hold types that are Animal or its subtypes
class AnimalShelter[T <: Animal](val animal: T) {
  def getAnimalSound: String = animal.speak()
}

object Main {
  def main(args: Array[String]): Unit = {
    val dogShelter = new AnimalShelter(new Dog())
    println(s"Dog says: ${dogShelter.getAnimalSound}")

    val catShelter = new AnimalShelter(new Cat())
    println(s"Cat says: ${catShelter.getAnimalSound}")

    // This would NOT compile because Car is not an Animal:
    // class Car
    // val carShelter = new AnimalShelter(new Car()) 
  }
}

Generic Class Challenge

Time to check your understanding of generics!

Read the following statements carefully and select all that are true regarding generic classes and methods in Scala.

Generics: Key Takeaways

In this lesson, we explored Scala's generics, a powerful feature for writing flexible and type-safe code.

  • Generics allow classes and methods to operate on different types.
  • They use type parameters (e.g., [T]) as placeholders for actual types.
  • Generics provide compile-time type safety, preventing runtime errors.
  • Scala's type inference often makes generic code concise.
  • Type bounds (e.g., <:) can restrict type parameters to specific subtypes.

By using generics, you can write reusable components that are robust and adaptable to various data types, making your Scala code more powerful and less prone to errors.

Frequently asked questions

Is the “Generics and Type Parameters” lesson free?

Yes — the full text of “Generics and Type Parameters” is free to read here on the web, and the Scala for Backend Engineering & Functional Programming course includes 3 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Scala for Backend Engineering & Functional Programming course, upgrade to CoddyKit PRO.

What will I learn in “Generics and Type Parameters”?

Understand how to write generic classes and methods to create reusable components with type safety. You practise Scala for Backend Engineering & Functional Programming with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Scala for Backend Engineering & Functional Programming?

No prior experience is required. Scala for Backend Engineering & Functional Programming on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 3, so you can start here or from the beginning and move at your own pace.

How long does the “Generics and Type Parameters” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Scala for Backend Engineering & Functional Programming lesson?

Yes. Every Scala for Backend Engineering & Functional Programming lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Generics and Type Parameters
  2. Variance: Covariance & Contravariance
  3. Type Classes and Implicits
← Back to Scala for Backend Engineering & Functional Programming