Scala - Generic Classes



This chapter takes you through the concept of generic classes in Scala programming. You can write flexible and reusable code using parameterizing types. So you can create classes, methods, and traits that work with any data type.

Generic Classes

Generic classes are classes that take a type parameter. You can create classes that can operate on objects of various types with compile-time type safety.

A generic class is defined with one or more type parameters. These type parameters are used within square brackets after the class name.

Syntax

The syntax of the generic class is -

class ClassName[T] {
   // class body using type T
}

Example

The following example shows a generic class in Scala programming -

class Container[T](value: T) {
   def getValue: T = value
}

object Demo {
   def main(args: Array[String]): Unit = {
      val intContainer = new Container(42)
      val stringContainer = new Container("Hello, Scala!")

      println(intContainer.getValue)    // 42
      println(stringContainer.getValue) // Hello, Scala!
   }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

42 
Hello, Scala!

In the example, Container class is defined as a generic class with a type parameter T. Demo object creates instances of Container with different types (Int and String). It accesses the stored values using the getValue method.

Generic Methods

Generic method is a method that takes type parameters. These methods can be defined within generic and non-generic classes.

Syntax

The syntax of the generic method is -

def methodName[T](param: T): ReturnType = {
   // method body using type T
}

Example

The following example shows a generic method in Scala programming -

object Demo {
   def printValue[T](value: T): Unit = {
      println(s"Value: $value")
   }

   def main(args: Array[String]): Unit = {
      printValue(42)             // Output: Value: 123
      printValue("Hello, Scala!") // Output: Value: Hello, Scala!
      printValue(3.14)            // Output: Value: 3.14
   }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

Value: 42
Value: Hello, Scala!
Value: 3.14

In the example, the printValue method is defined as a generic method with type parameter T. The method takes a value of type T and prints it. Demo object calls the printValue method with different types (Int, String, and Double).

Multiple Type Parameters

Both Generic classes and methods can have multiple type parameters. So there can be more complex and flexible type definitions.

Syntax

The syntax of multiple type parameter is -

class ClassName[T, U] {
   // class body using types T and U
}

Example

The following example shows a generic class with multiple type parameters in Scala -

class Pair[T, U](val first: T, val second: U) {
   def getFirst: T = first
   def getSecond: U = second
}

object Demo {
   def main(args: Array[String]): Unit = {
      val intStringPair = new Pair[Int, String](42, "Scala")
      val stringDoublePair = new Pair[String, Double]("Pi", 3.14)
      println(intStringPair.getFirst)  // 42
      println(intStringPair.getSecond) // Scala
      println(stringDoublePair.getFirst)  // Pi
      println(stringDoublePair.getSecond) // 3.14
   }
} 

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

42
Scala
Pi
3.14

In the example, the Pair class is defined as a generic class with two type parameters T and U. The class has a constructor that takes two values of types T and U. It also has two methods: getFirst and getSecond that return the respective values. Demo object creates instances of the Pair class with different types (Int, String, and Double). Then it calls the getFirst and getSecond methods.

Bounded Type Parameters

Bounded type parameters restrict the types that can be used as arguments for a type parameter. Upper bounds and lower bounds are used to enforce these restrictions.

Syntax

The syntax of bounded type parameter is -

def methodName[T <: UpperBound](param: T): ReturnType = {
  // Method definition
}

def methodName[T >: LowerBound](param: T): ReturnType = {
  // Method definition
}

Example

The following example shows the use of bounded type parameters in Scala programming -

class Upper
class Sub extends Upper

class Container[T <: Upper](value: T) {
   def getValue: T = value
}

object Demo {
   def main(args: Array[String]): Unit = {
      val upperContainer = new Container[Upper](new Upper)
      val subContainer = new Container[Sub](new Sub)

      println(upperContainer.getValue)
      println(subContainer.getValue)
   }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

Upper@<hashcode>
Sub@<hashcode>

In the example, Container class uses an upper-bounded type parameter T <: Upper. It restricts the type parameter to be Upper and its subclasses. Demo object creates instances of Container with Upper and Sub types and how to access the stored values.

Covariant and Contravariant Type Parameters

Covariance and contravariance control how subtyping between complex types relates to subtyping between their component types. When a type parameter is covariant, you can replace the generic class with a subclass. When a type parameter is contravariant, you can replace the generic class with a superclass.

Syntax

The syntax of covariance and contravariance type parameters is -

class Covariant[+T] {
   // class body using type T
}

class Contravariant[-T] {
   // class body using type T
}

Example

The following example shows covariant and contravariant type parameters in Scala programming -

class Animal
class Dog extends Animal

class Covariant[+T](val value: T)
class Contravariant[-T](value: T)

object Demo {
   def main(args: Array[String]): Unit = {
      val covariant: Covariant[Animal] = new Covariant[Dog](new Dog)
      val contravariant: Contravariant[Dog] = new Contravariant[Animal](new Animal)

      println(covariant.value)
   }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

Dog@<hashcode>

In the example, the Covariant class uses a covariant type parameter +T. So you can use a Covariant[Animal] to hold a Covariant[Dog]. The Contravariant class uses a contravariant type parameter -T. So, you can use a Contravariant[Dog] to hold a Contravariant[Animal]. Demo object shows how these relationships work.

Generic Classes Summary

  • You can define classes with type parameters. It provides flexibility and type safety.
  • Generic methods have their own type parameters and can be defined within generic or non-generic classes.
  • You can also define multiple type parameters for complex and flexible type definitions.
  • Bounded type parameters restrict the types. It can be used as arguments for type constraints.
  • Covariant and contravariant type parameters control subtyping relationships between complex types and their component types.
Advertisements