1. Overview
String, a fundamental data type in Scala, is a complex type we can understand as a sequence of characters.
One of the most common tasks with any collection is to check if it contains any elements.
In this tutorial, we’ll learn about several ways to perform these checks efficiently and idiomatically in Scala 3.
2. Using the isEmpty, isBlank or nonEmpty Methods
In the JVM, Scala Strings are implemented as Java Strings; this means we can use the isEmpty method as we do in Java.
Scala also adds the opposite operation as the nonEmpty method. It’s defined in the StringOps class, among other useful extension methods. It returns true if the String contains at least one character and false otherwise. It doesn’t treat white spaces as empty:
scala> val str1: String = "Hello"
scala> val str2: String = ""
scala> val str3: String = " "
scala> println(str1.isEmpty) // false
scala> println(str1.nonEmpty) // true
scala> println(str2.isEmpty) // true
scala> println(str2.nonEmpty) // false
scala> println(str3.isEmpty) // false
scala> println(str3.nonEmpty) // true
The easiest way to treat white space as empty is using the isBlank() method, because it already treats all white-space characters as empty. It has been part of the String class since Java 11 . Alternatively, we can trim the String and call isEmpty or nonEmpty according to our needs.
However, we should remember that all these methods throw a NullPointerException if the reference we use to call is null.
3. Dealing With null
For compatibility with Java, Scala allows null references by default. If we don’t activate Scala 3 explicit null values, we’ll need to decide if they should result in an error or be treated as empty values.
3.1. Creating Custom Extension Methods
*To handle both white-space characters and null Strings as empty Strings, we can use Scala’s extension methods to make our code more readable:*
object EmptyStringExtensions {
extension (str: String)
def isEmptyOrWhitespace: Boolean = str.trim.isEmpty
def isNullOrEmptyOrWhitespace: Boolean = str == null || str.isEmptyOrWhitespace
}
We must import EmptyStringExtensions to use the methods we defined above:
import EmptyStringExtensions._
class EmptyStringUnitTests extends AnyFlatSpec with Matchers {
"isNullOrEmptyOrWhitespace" should "return true for null strings" in {
val str: String = null
str.isNullOrEmptyOrWhitespace.shouldBe(true)
}
it should "return true for empty strings" in {
val str: String = ""
str.isNullOrEmptyOrWhitespace.shouldBe(true)
}
it should "return true for strings with only whitespace" in {
val str: String = " "
str.isNullOrEmptyOrWhitespace.shouldBe(true)
}
it should "return false for non-empty strings" in {
val str: String = "Hello, Scala"
str.isNullOrEmptyOrWhitespace.shouldBe(false)
}
}
This way, we avoid littering our code with sparsed checks for null every time we check whether a String is empty.
3.2. Writing a General Empty Checker
We can use Scala’s generalization of Strings as a sequence of characters and write a generic empty checker, by writing an extension method on a Seq[T] and a specialized version on Seq[Char]:
object EmptySeqExtensions {
extension (objs: Seq[?])
def isNullOrEmpty: Boolean = objs == null || objs.isEmpty
extension(objs: Seq[Char])
def isNullOrEmptyOrWhitespace: Boolean = objs.isNullOrEmpty || objs.forall(_.isWhitespace)
}
The specialized version makes use of the forall method in sequences and the isWhitespace from Char, allowing us to share code across Strings and other kind of sequences:
import EmptySeqExtensions._
class EmptySeqUnitTests extends AnyFlatSpec with Matchers {
"isNullOrEmpty" should "return true for null reference" in {
val seq: Seq[?] = null
seq.isNullOrEmpty.shouldBe(true)
}
it should "return true for empty sequences" in {
val seq: Seq[Any] = Seq.empty
seq.isNullOrEmpty.shouldBe(true)
}
"isNullOrEmptyOrWhitespace" should "return true for null strings" in {
val str: String = null
str.isNullOrEmptyOrWhitespace.shouldBe(true)
}
it should "return true for empty strings" in {
val str: String = ""
str.isNullOrEmptyOrWhitespace.shouldBe(true)
}
it should "return true for strings with only whitespace" in {
val str: String = " "
str.isNullOrEmptyOrWhitespace.shouldBe(true)
}
it should "return false for non-empty strings" in {
val str: String = "Hello, Scala"
str.isNullOrEmptyOrWhitespace.shouldBe(false)
}
}
4. Using Pattern Matching
Alternatively, we can also use pattern matching to implement the extension methods. Which approach is better is a matter of taste:
object EmptyPatternMatchingExtensions {
extension (seq: Seq[?])
def isNullOrEmpty: Boolean = seq match {
case null => true
case Seq() => true
case s => false
}
extension (seq: Seq[Char])
def isNullOrEmptyOrWhitespace: Boolean = seq match {
case null => true
case Seq() => true
case s => s.forall(_.isWhitespace)
}
extension (str: String)
def isNullOrEmptyOrWhitespace: Boolean = str match {
case null => true
case "" => true
case s => s.trim.isEmpty
}
}
Besides, pattern matching explicitly shows whether we treat nulls as empty Strings.
5. Conclusion
In this article, we explored various methods to check if a string is null or empty in Scala. By leveraging Scala’s powerful type system and extension methods, we saw how to create more readable and maintainable code.
Firstly, we examined the built-in methods isEmpty, nonEmpty, and isBlank. We noted their limitations, particularly when dealing with null references.
Secondly, we learned how to use extension methods to address these limitations, ensuring our checks are concise and robust.
We also explored how to generalize the concept of empty checks on sequences and how to use Scala’s pattern matching to rewrite the empty checks.
As usual, the full source code can be found over on GitHub.