Classes¶
Congratulations! If you’ve reached this point in the tutorials, you should have good understanding of the basics of the Shadow language: variables, operators, making choices, loops, methods, and arrays. Now, it’s time to move on to classes, objects, and interfaces – three crucial topics. Similar to Java, Shadow is object-oriented. However, before we dive into creating and using objects, we must first understand Shadow classes.
Whenever we write a program, we always start by creating and naming the class that serves as the container for our code. So far, we’ve been writing the majority of our code directly inside the main()
method to test out the language basics. In the Methods tutorial, we introduced the idea of methods as a way to break down long segments of code into useful, named chunks. However, methods are always part of an object, which can also contain data.
We want to keep data organized so that only the methods that need to access the data can do so. To define a group of data and the methods that can access it, we write classes which are the blueprints for objects. Just as we started with just the main()
method and showed how we could add additional methods, we’re going to show you how you can write different classes to create objects that can hold different data and interact with each other.
In order to explain the concepts behind classes and objects, we’ll be analyzing an extended example of the Otter
class. An object is a collection of data and methods to manipulate that data. Sometimes Shadow objects model objects in the physical word, as in this case where Otter
objects could model real otters. In other cases, a Shadow object could represent something more abstract like a collection of messages or a particular shade of blue.
Before we go on, take a moment to read through the program below to get familiar with it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | class Otter
{
/* These are the private member variables
* of the Otter class.
*/
get String name;
get set String habitat;
get set boolean mate;
get set int age;
/* This is the contructor for the Otter class,
* which initializes its private members.
*/
public create(String newName, String newHabitat, int age)
{
name = newName;
habitat = newHabitat;
this:age = age;
mate = false;
}
/* Implementation of the set method
* for the member variable mate.
*/
public set mate(boolean value) => ()
{
if (value and age > 2)
{
mate = true;
}
else
{
mate = false;
}
}
/* This method returns an int
* representing the number of fish caught.
*/
public goFishing() => (int)
{
int fish;
if (mate)
{
fish = 10;
}
else
{
fish = 5;
}
return fish;
}
}
|
Member variables¶
The first thing to note about the Otter
class is its member variables (also called fields) seen on Lines 6-9. Member variables represent a class’s data. For example, imagine you’re looking at a real, live otter. How would you describe or define the animal? Does it have a name? What’s its habitat? Does it have a mate? What’s its age? Each piece of information can be stored as specific member variable in the Otter
class. The name
member is a String
variable, mate
is a boolean
, age
is an int
, and habitat
is a String
as well. There’s no limit to how many member variables a class can have, and there’s no minimum requirement. A class doesn’t need to have member variables at all.
Aside from the modifiers get
and set
, declaring a member variable is the same as declaring any local variable. Unlike local variables, which only exist for the lifetime of a method, these variables exist inside of an object for its lifetime.
Note
Unlike local variables, member variables must be declared with an explicit type, instead of var
.
Member variables cannot be accessed by code written in a different class. If you’re familiar with other object-oriented languages, it’s as if all member variables in Shadow are declared private
, although this keyword should not be used explicitly for Shadow member variables.
Since these variables can only be directly accessed by code in the same class, we use the get
and set
modifiers to allow other classes to read or write the value of these private member variables through properties, explained below.
Constructors and objects¶
It is important to understand that the Otter
class is not an Otter
object. Rather, it’s a template or blueprint that describes the attributes, features, and actions of an Otter
. Writing the class doesn’t create any Otter
objects, but we can create an object, or an instance, of the Otter
class. This object will have its own name, habitat, age, and will either be mated or not. Otter
objects will all have the same member variables, but the values of those member variables will vary from one object to another. Otters, like people, are unique, after all.
To create objects and specify the initial values for member variables, we use a constructor. A constructor is a special kind of method in a Shadow class. There’s an example of a constructor on lines Lines 14-20 of our example Otter
class. We use constructors to create a new object by passing in the appropriate values. The general method header for a constructor is as follows:
public create(parameters)
The name of every constructor in Shadow is create
. Most constructors are public
, allowing code in other classes to create a new object of the class. The number and type of parameters will vary, depending on the class. Before we get into the body of the constructor, let’s go over the basic syntax for creating an object in Shadow, which is similar to how arrays are created.
A constructor is used to initialize an object, not to give back information. Unlike normal methods, a constructor has no return types, not even => ()
.
Note
Many object-oriented languages use the name of the class for constructors, but Shadow uses create
.
Creating an object¶
Objects can be created in any method. Some classes have a main()
method where a program can start, and other’s don’t. Although the Otter
class doesn’t have a main()
method, we could, for example, create an Otter
object in a method of another class. Sometimes, we will call a class that tests objects of other classes a driver class.
We could imagine a driver class OtterDriver
that has a main()
method. Inside this main()
method, the following code could create an Otter
object:
Otter olive = Otter:create("Olive", "river", 6);
The first part of this line, Otter olive
, declares a variable of type Otter
to point at the object we’re about to create. The type must match the kind of object we’re creating. The name of our variable is olive
, which follows the same naming conventions we discussed in the Variables tutorial.
The expression on the right of the equals sign invokes the Otter
constructor, creating an Otter
object. Inside the parentheses we see three literal values, the name ("Olive"
), habitat ("river"
), and age (6
) that the constructor requires as parameters. Looking back at the Otter
class, you can see in the constructor parameter list that it requires two String
variables and an int
, in that order.
The constructor body¶
Now that you know how to create an object, let’s examine Lines 16-19 of the Otter
class to see how the constructor works . The goal of a constructor is to initialize the object’s member variables. On Line 16, the newName
parameter giving the name of the Otter
is stored into the name
member variable. Given the driver code above, "Olive"
will be stored into name
.
What happens if the parameter name is the same as the member variable name? Although this is legal in Shadow, the parameter name will hide the name of the member variable. Line 18 shows a way to resolve this problem. Both the member variable and the parameter have the same name, age
. Although the code would compile if you said age = age;
, it would have no effect, since it would set the value of the parameter to itself. By putting this:
before the name of a member variable, you can specify that you’re referring to the member variable inside the current object. The keyword this
is a Shadow reserved word that refers to whatever object the code is currently inside of.
Line 19 demonstrates that not all member variables need to be initialized using parameter values. The member variable mate
is always initialized to false
, as we assume that an Otter
object does not have a mate when it’s first created.
Note
We also could have set the field mate
equal to false
at Line 8 where the variable is initially declared.
Overloaded constructors¶
Just like other methods in Shadow, constructors can be overloaded. This means that one class can have more than one constructor, as long as each one’s parameter list varies in type or number from the others. Consider this additional constructor for the Otter
class:
public create(String newName, String newHabitat)
{
name = newName;
habitat = newHabitat;
age = 0;
mate = false;
}
The only difference is this overloaded constructor does not take in an int
representing age. It sets the member variable age
to 0
when the object is created. This constructor is legal because it has a different number of parameters.
If this constructor is part of the Otter
class, someone could create an Otter
object with the following code:
Otter oliver = Otter:create("Oliver", "ocean");
Now oliver
is an Otter
whose age
member is 0
. Both olive
and oliver
are still Otter
objects containing the same kinds of data, even though they were created by invoking different constructors.
Default constructors¶
A default constructor is a constructor that takes in no parameters. Objects created by default constructors are assigned default values for all of their member variables.
If you don’t write any constructors for your class, the Shadow compiler creates a default one behind the scenes, initializing all variables with the following default values for commonly used primitive types:
int
:0
double
:0.0
boolean
:false
code
:'\0'
Note that this default constructor is created for you only if no other constructor is defined in the class. However, what happens to reference (non-primitive) member variables? If the member variable is nullable
, the compiler will initialize it to null
, but if it isn’t, the compiler will give an error because you haven’t said what this member variable will be initialized to and there’s no suitable default.
If you get this compiler error, you could mark all reference variable types as nullable
, but doing so would increase the number of nullable
references, which is a bad programming practice. If there are reasonable defaults for these member variables, you can easily initialize the individual member variables outside of any constructor.
For example, if one of your member variables is String something;
, to avoid using nullable
and still use the default constructor, you could simply write String something = "";
Constructor chaining¶
Constructor chaining is another feature of constructors that helps eliminate repeated code. Using the keyword this, you can invoke an existing constructor from another constructor of that class. Doing so makes it easy to provide a range of different constructors to programmers who will use your class. These constructors will often call other constructors that that take more parameters, supplying them with default parameter values.
The following code shows the original constructor for the Otter
class with two more added:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public create(String newName, String newHabitat, int age)
{
name = newName;
habitat = newHabitat;
this:age = age;
mate = false;
}
public create(String newName, String newHabitat)
{
this(newName, newHabitat, 0);
}
public create(String newName)
{
this(newName, "unknown");
}
|
Now, consider the following code from a test program:
Otter jasmine = Otter:create("Jasmine");
Console.printLine(jasmine->name);
Otter harrison = Otter:create("Harrison", "pond");
Notice that we create jasmine
using that constructor that takes only one parameter, the name. How do the other member variables get instantiated? Look at Line 16 above. Using the this
keyword, it invokes the previous constructor using the name that was passed in ("Jasmine"
) along with a value for habitat
("unknown"
) as parameters. Control then flows to the constructor that takes two String
values as parameters. If there hadn’t been such a constructor, we would have gotten a compiler error. In this constructor, there is yet another example of constructor chaining. The two String
values passed in, along with the value 0
, are sent as parameters to the original constructor where the member variables are initialized.
Finally, look at the second Otter
object, harrison
. Here, we have invoked the constructor that takes two String
values, which also includes a call using this
. The member variable age
is set to 0
.
Note
It’s permitted to write other code inside a constructor after invoking another constructor using this
, but the constructor invocation must be the first statement.
Properties¶
Let’s return to the original Otter
class and consider the get
and set
keywords.
Because all member variables in Shadow are private, how can other classes access or change these values? It would be tedious to write accessors (methods that return the value of a member variable) and mutators (methods that change the value of a member variable) for every single member variable. Instead, Shadow provides a tool called properties. Properties are similar to method calls, but they use the arrow operator (->
) instead of parentheses with arguments.
In order to see how properties work, take a look at Line 6 of the original Otter
class:
get String name;
Here the get
keyword modifies the member variable name
, creating an accessor property that we can use in our OtterDriver
program, part of which is shown below:
1 2 3 4 5 6 | Otter olive = Otter:create("Olive", "river", 6);
Console.printLine(olive->name # " lives in a " # olive->habitat);
olive->mate = true;
Console.printLine(olive->name # " has a mate: " # olive->mate);
Console.printLine(olive->name # " just caught " # olive.goFishing() # " fish!");
|
The program output is below:
Olive lives in a river
Olive has a mate: true
Olive just caught 10 fish!
In Line 2 of the driver program we see olive->name
, which returns the value of the member variable name
("Olive"
). The same applies for olive->habitat
. If either name
or habitat
hadn’t had get
in their declaration, we would’ve needed to write accessor methods for both in order to retrieve their values in OtterDriver
.
Additionally, the set
keyword can be used to create a mutator property. Line 4 states olive->mate = true;
. If no set
mutator method was defined in the program, the member variable mate
would simply have been changed to true
.
Most of the time, simply applying the get
or set
modifier to a member variable is fine, but it’s possible to customize what happens if you want something more complicated than retrieving or storing a value. For example, in the Otter
class, a condition must be met before mate
can be set to true
:
1 2 3 4 5 6 7 8 9 10 11 | public set mate(boolean value) => ()
{
if(value and age > 2)
{
mate = true;
}
else
{
mate = false;
}
}
|
In order for the property to work correctly, the method header is critical. The syntax is as follows:
public set memberName(parameter of member type) => ()
In the Otter
class, the member variable name is mate
and the type is boolean
, as reflected in the method header. Now, mate
will only be set to true
if the Otter
object has an age greater than 2. As you can see in the console output from OtterDriver
, olive
is 6, so she did find a mate.
Note
This method and indeed all properties can also be called directly as methods (since that’s what they are, under the covers), but property syntax is easier to read.
Normal methods¶
Beyond constructors and properties, a class can have any number of other methods, as discussed in the previous Methods tutorial.
Recall that the Otter
class has a method called goFishing()
, repeated below:
public goFishing() => (int)
{
int fish;
if (mate)
{
fish = 10;
}
else
{
fish = 5;
}
return fish;
}
The method takes in no parameters and returns an int
representing the number of fish caught. If the Otter
object the method is called on has a mate, it catches twice the number of fish. As seen in Line 6 of the OtterDriver
class, we call this method using normal method syntax:
objectName.methodName(parameters);
Note
The object’s name must always be used when calling a method on a different object. However, if we want to call a method on the same object as the method we’re currently in, the objectName.
can be left off (or can be replaced with this.
).
Packages¶
Packages in Shadow are a means of organizing groups of classes that are commonly used together. A normal Shadow class should be written in its own file, whose name ends with .shadow
. These files should be put in the same folder if they’re in the same package.
A few packages are worth mentioning because they’re used all the time. For example, the shadow:standard
package contains essential classes, interfaces, singletons, and exceptions (to be explained in later tutorials) needed for any Shadow program. These types do not need to be explicitly imported because the compiler will do so automatically. Other built-in Shadow packages are listed below (as described in the Shadow API).
Package
shadow:io
contains fundamental types used for input and output, both for the console and for file and path manipulation.Package
shadow:natives
contains classes and exceptions used to interact with C code.Package
shadow:utility
contains basic data structures and utility classes that are useful in many different kinds of programs.
While these are packages fundamental to the Shadow language, what if you wanted to create your own package? For example, you might be wondering what package the sample programs for these tutorials are in. If not specified in the class header, classes are stored in the default
package. Now that we’ve introduced packages, we’ll put all future programs in a tutorials
package.
First, we’ll create a folder called tutorials
. Inside this folder we can have multiple other folders to hold different classes. For example, inside the tutorials
folder, we could make a folder called variables
. Inside this folder, we could put all the programs we have relating to variable examples, making it a subpackage of the tutorials
package. But how do we designate the package in class headings?
Let’s pretend we made a class called VariableClass
. Instead of the class header saying class VariableClass
, we should now write class tutorials:variables@VariableClass
.
The package name is tutorials:variables
(exactly matching the folder names), and the class name is VariableClass
. The class name must appear after the @
symbol.
It’s always a good idea to put your code into packages to stay organized. From now on, packages will be incorporated into our example programs.
If classes are in the same package, they can refer to each other’s names without using an import
statement. However, if you want to use outside classes (with the exception of those in shadow:standard
), one approach is to use an import
statement at the top of your program. We have seen this many times, since most of our code has needed to use the Console
singleton from the shadow:io
package:
import shadow:io@Console;
Using an import
statement is the most common and convenient way of referring to a class from another package, but it’s also possible to use its fully qualifed name. If you don’t use an import
statement, you can refer to a class using its whole name, including packages. Thus, you could type out shadow:io@Console
everytime you wanted to refer to Console
. This approach is sometimes required when you need to import two different classes that have the same name.