What Value classes is?
Value classes are a mechanism in Scala to avoid allocating runtime objects. This is accomplished through the definition of new AnyVal subclasses. The following shows a very minimal value class definition:
case class UserId(id: Int) extends AnyVal
As you can see in the above section, For a class to be a value class, it must have exactly one parameter and it must have nothing inside it except defs. Furthermore, no other class can extend a value class, and a value class cannot redefine equals or hashCode. To define a value class, make it a subclass of AnyVal, and put val keyword before the one parameter. This is how you can recognize or create value classes in Scala.
The need for Value Classes?
Fundamentally a value class is one that wraps around a very simple type or very simple value like Int, Boolean etc. What that means is that at compile time you see the value class and you use the value class but at the time bytecode could get generated you are actually using the underlying simple type. So that means your instances of the wrapper classes creator which means less initialization so that increase performance and less memory usage because there is no instance of the wrapper classes.
Value classes are mostly used for performance optimization and memory optimization. You can think of many of this classes are being your Scala typical primitive like classes Int, Boolean, Double etc. Use case where you would want to and where you could apply value classes is for tiny types. Let’s look at a couple of example of how that can be applied.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class User(id: Int, name: String) | |
case class Tweet(id: Int, content: String) | |
object Test extends App{ | |
val user = User(1, "Kunal") | |
val tweet = Tweet(11, "Scala Rocks") | |
println(s"User ID :- ${user.id}") | |
println(s"Tweet Content:- ${tweet.content}") | |
} |
In the above example, we have a very simple data model where we have User and Tweet. Note that we’re using Int for two types that are really unrelated a user ID and tweeter ID are completely different so we might want to differentiate it. Rest of this nothing is special in the above code.
Now, what happens if I want to use the concept of tiny types. Let’s go through one more example where we are using tiny types.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class UserId(id: Int) | |
case class Username(name: String) | |
case class TweetId(id: Int) | |
case class TweetContent(content: String) | |
case class User(id: UserId, name: Username) | |
case class Tweet(id: TweetId, content: TweetContent) | |
object Test extends App { | |
val user = User(UserId(1), Username("kunal")) | |
val tweet = Tweet(TweetId(11), TweetContent("ABC")) | |
println("userID :-" + user.id) | |
println("tweetID:-" + tweet.content) | |
} |
Now, what happens when I used the concept of java types to use more specific classes for the user ID and username which means now I can’t use any number directly and I can’t mix a tweet ID with a user ID because the compiler is helping us to verify that. So, that is why we created four classes here to wrap around very simple Int and String values. So User and Tweet takes tiny types instead of Int or String values directly.
Let’s generate a bytecode for User.scala with the command:
> javap -p User
Now, If you look at the bytecode that is generated by this as we would expect and we would see the user has two fields and those fields are the type of UserId and UserName which means every time I create a User that time I’m also creating instances of those two classes as well. This is where Value Classes comes into the picture.
Let’s go through the one last example where we are using value classes:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class UserId(id: Int) extends AnyVal | |
case class Username(name: String) extends AnyVal | |
case class TweetId(id: Int) extends AnyVal | |
case class TweetContent(content: String) extends AnyVal | |
case class User(id: UserId, name: Username) | |
case class Tweet(id: TweetId, content: TweetContent) | |
object Test extends App { | |
val user = User(UserId(1), Username("kunal")) | |
val tweet = Tweet(TweetId(10), TweetContent("ABC")) | |
println(s"userID :- ${user.id}") | |
println(s"tweetID:- ${tweet.content}") | |
} |
What we have done on the above example is we just took those tiny types and made them value classes by extending AnyVal they are perfect candidates because they all wrap around a single simple value. So, the four classes that we created we just changed that into Value classes. But when you will look into the bytecode which is generated this time you will get the difference.
The above code is self-descriptive so am hoping that you are well aware of value classes. Where and how to use it.