Scala Tutorial

Basics

Control Statements

OOP Concepts

Parameterized - Type

Exceptions

Scala Annotation

Methods

String

Scala Packages

Scala Trait

Collections

Scala Options

Miscellaneous Topics

Generic Classes in Scala

In Scala, as in many statically-typed languages, you can create classes that are parameterized by one or more types. These are called generic classes. Generics provide a way to make your code more abstract and reusable while maintaining type safety.

Defining a Generic Class

Let's start with a simple example of a generic class:

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

In this example, Box is a generic class that can hold any type T. The [T] after the class name denotes the type parameter.

Using the Generic Class

You can create instances of this class with any type:

val intBox = new Box[Int](10)
val stringBox = new Box[String]("Hello")

Multiple Type Parameters

A class can also have multiple type parameters:

class Pair[A, B](val first: A, val second: B)

Bounded Type Parameters

You can restrict the types that can be used with your generic class by using bounds. Here are the types of bounds you can use:

  • Upper Bound: Ensures that the type parameter is a subtype of a particular type. This is specified using the <: symbol:
class Box[T <: AnyVal](t: T)

The above Box class now can only be parameterized with types that are subtypes of AnyVal (e.g., Int, Double).

  • Lower Bound: Ensures that the type parameter is a supertype of a particular type. This is specified using the >: symbol:
class Box[T >: String](t: T)

Variance Annotations

In the context of generic types and subtyping, Scala supports variance annotations:

  • Covariant (+): If B is a subtype of A, then Box[B] is a subtype of Box[A].
class Box[+T]
  • Contravariant (-): If B is a subtype of A, then Box[A] is a subtype of Box[B].
class Box[-T]
  • Invariant (default): Box[A] and Box[B] are not related irrespective of the relationship between A and B.

Context Bounds

You can use context bounds if you want to ensure that there exists an implicit value of a certain type:

def sort[T : Ordering](seq: Seq[T]): Seq[T] = {
  seq.sorted
}

This method ensures that there's an implicit Ordering[T] available when the method is invoked, which is required by the sorted method.

Conclusion

Generics in Scala allow developers to create type-safe, reusable, and abstract code. With features like bounded type parameters, variance annotations, and context bounds, Scala provides a rich set of tools to make generics expressive and flexible.

  1. Type parameters in Scala generic classes:

    • Description: Type parameters allow the creation of generic classes, enabling flexibility in handling different data types.
    • Code Example:
      class Box[T](value: T) {
        def getValue: T = value
      }
      
      val intBox = new Box[Int](42)
      val stringBox = new Box[String]("Hello, Scala!")
      
  2. Using generic classes for code reusability in Scala:

    • Description: Generic classes promote code reusability by allowing the creation of classes that can work with various types.
    • Code Example:
      class Pair[A, B](first: A, second: B) {
        def getFirst: A = first
        def getSecond: B = second
      }
      
      val intStringPair = new Pair[Int, String](1, "One")
      val doubleBooleanPair = new Pair[Double, Boolean](3.14, true)
      
  3. Covariance and contravariance in Scala generics:

    • Description: Covariance and contravariance define how subtyping relationships are preserved in generic classes.
    • Code Example (Covariance):
      class Container[+T](value: T) // Covariant
      val stringContainer: Container[String] = new Container("Hello")
      val anyContainer: Container[Any] = stringContainer
      
    • Code Example (Contravariance):
      class Processor[-T](processor: T => Unit) // Contravariant
      val printString: String => Unit = (s: String) => println(s)
      val printAny: Any => Unit = printString
      
  4. Bounds in Scala generic classes:

    • Description: Bounds restrict the types that can be used as type parameters in generic classes.
    • Code Example (Upper Bound):
      class Box[T <: Number](value: T) // T must be a subtype of Number
      val intBox = new Box[Int](42)
      
    • Code Example (Lower Bound):
      class Box[T >: String](value: T) // T must be a supertype of String
      val anyBox = new Box[Any]("Hello")
      
  5. Wildcard types and generic classes in Scala:

    • Description: Wildcard types allow flexibility in specifying unknown types or dealing with generic classes with unspecified type parameters.
    • Code Example (Upper Bounded Wildcard):
      def printBoxValue(box: Box[_ <: Number]): Unit = {
        println(box.getValue)
      }
      
    • Code Example (Lower Bounded Wildcard):
      def processBox(box: Box[_ >: String]): Unit = {
        // Process the box
      }
      
  6. Scala generic classes vs. abstract classes:

    • Description: Generic classes and abstract classes serve different purposes. Generic classes allow parameterization, while abstract classes provide a way to define common behavior for subclasses.
    • Code Example (Generic Class):
      class Box[T](value: T)
      val intBox = new Box[Int](42)
      
    • Code Example (Abstract Class):
      abstract class Shape {
        def area: Double
      }
      
      class Circle(radius: Double) extends Shape {
        override def area: Double = math.Pi * radius * radius
      }
      
  7. Using generic classes with collections in Scala:

    • Description: Generic classes are commonly used with collections to create type-safe and reusable data structures.
    • Code Example:
      val list: List[Int] = List(1, 2, 3, 4, 5)
      val set: Set[String] = Set("apple", "orange", "banana")
      
  8. Variance annotations in Scala generic classes:

    • Description: Variance annotations (+, -, or no annotation) specify how subtyping relationships are preserved in generic classes.
    • Code Example (Covariant):
      class Container[+T](value: T)
      val stringContainer: Container[String] = new Container("Hello")
      val anyContainer: Container[Any] = stringContainer
      
    • Code Example (Contravariant):
      class Processor[-T](processor: T => Unit)
      val printString: String => Unit = (s: String) => println(s)
      val printAny: Any => Unit = printString
      
  9. Inheritance and generic classes in Scala:

    • Description: Generic classes can be inherited, providing an opportunity for specialization or further parameterization.
    • Code Example:
      class Box[T](value: T)
      class NumberBox[T <: Number](value: T) extends Box[T]
      
  10. Scala generic methods vs. generic classes:

    • Description: Generic methods allow parameterization at the method level, offering flexibility for methods with generic behavior.
    • Code Example:
      def printElement[A](element: A): Unit = {
        println(element)
      }
      
  11. Parameterized types and type erasure in Scala:

    • Description: Scala uses type erasure, where generic types are erased at runtime. Parameterized types provide type information during compilation.
    • Code Example:
      def printType[A](value: A): Unit = {
        val typeName = value.getClass.getSimpleName
        println(s"The type of $value is $typeName")
      }