A class in OOP can be defined as a named template in terms of storing and presenting data. A class is, in a sense, a blueprint from which instances are created. In the process of creating instances, class member variables are initialized with values.
In Scala, a syntactic class definition consists of at least two parts: the class keyword followed by a capitalized identifier as the name of the class. Curiously enough, we can define:
class Person
Looking at this example, we might ask some questions: how does such a definition provide an initial state, and how we can create instances? Scala has an advanced compiler that sees approximately this:
class Person() { }Constructor
That is, there are parts in the class definition that Scala allows to omit. Let's keep improving our class:
class Person(name: String)
The name is a formal parameter here. Let's define the primary constructor:
class Person(name: String) {
val theName: String = name
}
In Scala 3 curly braces can be replaced by a single colon, which is especially useful when classes are small:
class Person(name: String):
val theName: String = name
Note that the scope of formal parameters extends to the entire constructor. The class looks bulky, so let's try to improve it further:
class Person(val name: String)
We don't even need a constructor here, Scala will add the required code. It's roughly the equivalent of this Java code:
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
Specifying val before a formal parameter creates a getter in the class, var creates a getter/setter pair. Parameters without val or var are only visible inside the constructor. Note that the class constructor executes all the instructions it contains, setting the initial state of the instance.
But why should var be handled with care? Let's explore this in the next sections.
Instance creation
Armed with a class description, we can create an instance:
val oliverBrown: Person = new Person("Oliver Brown")
To create instances of classes, we used the new keyword followed by the name of the class. We also set the values of the formal parameters. However, in Scala 3 we can drop the new keyword, the result will be the same.
Of course, from the same class definition, we can create different instances:
val emmaDavies: Person = Person("Emma Davies") // Still workingFields
As we could see earlier, definitions can contain fields for storing data in instances: strings, numbers, other classes, etc. We use a dot to access fields:
println(oliverBrown.name) // Oliver Brown
But if we try to change the name the code will not compile:
oliverBrown.name = "Other Name" // compile error: reassignment to val
The reason for it is that the name field is preceded by val in the Person class. Let's try to change that:
class Person(var name: String)
val jessicaEvans: Person = Person("Jessica Evans")
jessicaEvans.name = "John Smith" // OK?
println(jessicaEvans.name) // Jessica Evans expected, got John Smith
Despite the fact that the code is compiled, we can not expect that an instance of the class can change the name. You need to be careful with var.
Fields without val or var are not accessible in client code outside of the class. Here is an example:
class Time(hours: Int, minutes: Int):
val inner = hours * 60 + minutes
val t = Time(1, 30)
t.inner // OK
t.hours // compile error: value hours is not a member of Time
t.minutes // compile error: value minutes is not a member of TimeMethods
The users of our Time class may want to print its contents, such as:
class Time(val hours: Int, val minutes: Int)
val t = Time(1, 30)
// app 1
def printTime1(h: Int, m: Int) = println("time: " + h + ":" + m)
printTime1(t.hours, t.minutes)
// app 2
def printTime2(h: Int, m: Int) = println("time: " + h + "h " + m + "mi")
printTime2(t.hours, t.minutes)
// app 3
// ...
As a result, each user has to implement the print function every time. This can be avoided by adding behavior to the class. Let's try to improve the Time class:
class Time(val hours: Int, val minutes: Int) {
def showWith(prefix: String): String =
prefix + '=' + asString
def asString: String = hours + ":" + minutes
}
// all app
val t = Time(1, 30)
println(t.showWith("the time"))
println(t.asString)
Now the users will be able to use one method!
The method starts with the keyword def, followed by the identifier as its name. Method names are usually verbs, for example, print, show, open, etc. Methods may or may not have formal parameters enclosed in parentheses. Sometimes the parentheses may be omitted. You can call an instance method of a class using a dot.
And, now that we know that the class body is calculated when it is created, what will change if def asString is replaced with val asString?
Access modifiers
Class members (fields and methods) can have access modifiers: private, protected. For example:
class Time(val hours: Int, val minutes: Int) {
private val inner = hours * 60 + minutes
val asString: String = "Time(" + inner + ")"
}
val t = Time(1, 30)
t.inner // compile error: value inner in class Time cannot be accessed in Time
t.asString // OK
The field inner is not available outside of the Time class. If a member doesn't have an access modifier, then it is available everywhere. Members with the protected modifier don't differ from private yet. This will be used in a class inheritance theme. We'll cover it in other topics.
Modifiers can also be used in the constructor itself:
class Time(val hours: Int, val minutes: Int, private val id: String)Conclusion
Great! In this topic, you got acquainted with classes in Scala. Now you know that the class runs all the instructions it contains during initialization. You can describe classes with methods, with mutable and immutable fields. You can also create instances and manage access to class members.