7. Classes and Objects
Chapter 0111 2
What’s the Point?
*
*
Source code examples from this chapter and associated videos are available on GitHub.
There are multiple ways to organize a program, and the code we’ve written to this point could be described as procedural programming. In procedural programming, we organize our code into methods that perform specific tasks, and we think of a program as a series of tasks that need to be performed in a specific order. Another popular way to think about software design is object-oriented programming (OOP). In this chapter, we’ll start dip our toes into OOP by learning about classes and objects, which are the basic building blocks of OOP.
7.1. OOP Basics
There’s nothing wrong with procedural programming—and, in fact, OOP is a form of procedural programming—but it can get cumbersome as a program grows in size and complexity. And, it isn’t always an intuitive way to plan our programs.
Object-oriented programming lets us think about programs the way we think about the world around us. Instead of thinking about a program as a bunch of tasks, we can think of it as a bunch of objects that interact with each other. As an example, the old-school video game Pac-Man has the main character (an object) moving around a maze (another object), and avoiding four ghosts (yep, four more objects).

If we decide to create Pac-Man, we’ll still be using methods, but they won’t be the basic building blocks of our programs. Instead, we’ll be organizing our programs into classes, which are blueprints—or recipes—for creating objects. A class defines the attributes and behaviors of the objects—and those are the variables and methods we’ll write to create our programs.
7.1.1. Class Diagrams
In OOP, our planning for a program starts by deciding what objects—and therefore, what classes—we’ll need in order to implement the functionality we want. To help organize our thinking, we’ll use a tool called a class diagram, which gives a visual representation of the classes and their relationships in our program. The format programmers use for class diagrams is called Unified Modeling Language (UM)). The UML standards are extensive, and we’ll be using a simplified version in this course.
Consider a program that tracks inventory at a comic book shop. A simple class to represent one comic book might look like this:

The top section of the diagram has the name of the class, which is ComicBook
.
The next section is for the attributes of the class—the information the class will store. This example has attributes for the title of the comic book, the issue number, and the price.
The bottom section is for the behaviors of the class, which are the actions the class can perform (or that we can do with the class). With a comic book, we might sell it to a customer, buy it from a customer, or print a receipt.
We’ll learn more about the symbols and conventions of UML as we go along, but for now the important part is that we can use class diagrams to help us plan our programs—and to talk about OOP concepts without bogging down on code details.
Your Canvas course includes access to Lucid (Whiteboard), which is a free online tool you can use to create UML diagrams. If you create your account through the link in the left-hand navbar in Canvas, you’ll have access to the premium features for free. |
7.2. Encapsulation
Object-oriented programming is all about creating objects that can interact with each other.
Since the objects will be interacting, we need to think about how to keep them from interfering with each other in ways we don’t want.
If we have a ComicBook
class in a program used by a shop, we don’t want some other class to change attributes in a way that disrupts the program—like setting the price to zero, or making the issue number a negative number, for example.
To prevent this kind of tampering, whether it’s intentional or accidental, OOP relies on a concept called encapsulation. Encapsulation of a class means that attributes are hidden from the outside world, and only the behaviors of the class can access and change them. For another analogy, consider a vending machine. We can’t just reach in and grab a candy bar; we have to pay and use the buttons on the machine. In this analogy, the snacks are encapsulated and we can only access them by using a behavior, like "buy candy".
Another way to think of encapsulation is the way we interact with other people in social situations. When we encounter a stranger, they don’t automatically know our name and phone number; they have to ask us for that information. We’ve encapsulated our personal information, and we only share it when and how we choose to.
In C#, encapsulation is not a strict requirement, and our code will still work if we don’t use it. But it’s a best practice—and an important one—so we will encapsulate all of our classes in this course. |
7.3. Defining and Using a Class
We’ll look at a program to keep rudimentary swimming pool maintenance records; for a single day’s data, we’ll have a class called PoolRecord
.

PoolRecord
classTo implement this class in code, we’ll start with a class header. The class header always follows the same pattern: an access modifier, the keyword class
, and the name of the class.
The class header is followed by a code block, enclosed in curly braces.
PoolRecord.cs
. A class header and code block.1
2
3
4
5
6
public class WeatherRecord
{
// class code goes here
}
- Access modifier
-
The
public
keyword means that the class can be accessed from any other class. Though this is technically optional, we should always usepublic
for now. class
keyword-
The keyword that tells Java we’re defining a class. It’s required. We’ll eventually be able to create different kinds of classes and OOP structures, but for now we’re just creating regular classes.
- Class name (or identifier)
-
The name of the class, which should be a noun that describes the object the class represents—and is singular, so there’s no s at the end. The identifier should start with a capital, with the first letter of each word capitalized (like
WeatherRecord
). This is similar to the camelCase naming convention we’ve been using for variables, and it’s the same convention we’ve been using for methods; it’s called PascalCase.
The class code block is where we define the different components that make up the class, which we call the instance members. For now, we’ll focus on two types of instance members: fields and methods.
7.3.1. Fields
Fields are the implementation of the attributes of the class.
They are also known as instance variables because they are similar to the variables we’ve been using in our programs, but their scope is the object created from the class, not the method where they’re defined.
A field is unique to the object; if we make two objects from our WeatherRecord
class, each object will have its own date, high temperature, and average temperature.
Fields are declared like our other variables, but they are encapsulated using the private
access modifier.
This means that the fields can only be accessed and changed by the methods of the class, not by other classes—which controls how the data is used and prevents accidental or invalid changes.
Since a class will compile and run even if we leave off the private access modifier, it’s easy to forget to use it. But don’t worry, I’ll help you remember by taking huge points off your assignments if you don’t make your fields private . As I’ve mentioned, you’re not really doing OOP if you don’t encapsulate your fields, and we’re learning OOP here.
|
WeatherRecord.cs
. Fields added to the WeatherRecord
class.1
2
3
4
5
6
public class WeatherRecord
{
private string date;
private int highTemp;
private int avgTemp;
}
In our original class diagram, we indicated that the fields were private by using a -
in front of the field name.


You might remember from the section on variable scope in Chapter 0011 that using global variables is terrible, and every time we create a global variable, a puppy loses its favorite toy. And these fields look an awful lot like global variables. But fields in a class are not global variables; they’re instance variables, and that’s a good thing. The fields are encapsulated, so they can only be accessed and changed by the methods of the class—which is also good thing. And the fields are unique to each object, so we can have multiple objects with different values for the fields—which is yet another a good thing. And so no puppies' toys will be harmed as long as we use private fields correctly.
7.3.2. Methods
Ensuring that our fields are private
is the first step in encapsulating our class, but it’s not the only step.
We also need to create methods that can access and change the fields—otherwise, the fields are useless.
So far, our methods have included the keyword static
; we’ll learn more about that shortly, but when we make methods for an OOP class, we’ll leave off that static
keyword.
These nonstatic methods are called instance methods, and they are otherwise very similar to the static
methods we’ve been using.
Though there are exceptions, most of these instance methods will be public
, so they can be accessed from other classes.
Remember, the foundation of encapsulation is having private
fields and public
methods to permit interactions with that data.
In broad terms, we can categorize instance methods into two types: accessor methods and mutator methods.
Accessor Methods
Accessor methods give access to the fields of the class, but they don’t change the fields.
Think of them as "read only" methods, and often all they do is return the value of a field.
Java naming conventions specify that accessor methods should start with get
and then the name of the field they access, formatted in camelCase.
Because of that convention, another name for accessor methods is getters.
1
2
3
4
public int getHighTemperature()
{
return highTemperature;
}
The return type of an accessor method is the same as the type of the field it accesses; in this case highTemperature
is an int
, so the return type of our getter is int
.
A getter allows other classes to be able to read the value of a field; if they don’t need to know the value, we just don’t write a getter for that field. But read-only access usually does no harm, so often we’ll have getters for all of our fields.
WeatherRecord.cs
. Getters added to the WeatherRecord
class. 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class WeatherRecord
{
private string date;
private int highTemp;
private int avgTemp;
public string getDate()
{
return date;
}
public int getHighTemp()
{
return highTemp;
}
public int getAvgTemp()
{
return avgTemp;
}
}
Mutator Methods
Mutator methods change the fields of the class.
Though they sometimes return a value, their primary purpose is to change the value of a field and they often have a void
return type.
As we’re getting the hang of this OOP thing, we’ll create a lot of mutator methods that are just setters--methods that set the value of a field.
The naming convention for setters is to start with set
and then the name of the field they change, formatted in camelCase; they usually have a void return type.
1
2
3
4
public void setHighTemperature(int temp)
{
highTemperature = temp;
}
The parameter of a setter is the same type as the field it changes; in this case highTemperature
is an int
, so the parameter of our setter is also an int
.
All this method does is accept a new value and assign it to the field.
Choosing to write setters isn’t quite as straightforward as with getters, where there’s generally no harm in exposing read-only access to everything. But we really should only write setters for fields that we want to be able to change from outside the class.
A rule of thumb for beginners: create getters for all of your fields when you first write your class, and then add setters only as you need them. Because this is sometimes tricky for beginners to determine, I don’t deduct points for writing unnecessary setters—but sometimes my directions will explicitly tell you not to write a setter for a field, and I do deduct for that. |
If you’re paaying attention to what we’re doing here, you might be thinking that these setters really just give public access to the fields, which seems to go against the whole idea of encapsulation.
That’s true for now, but only because we’re starting with some basic examples.
As we get the hang of this OOP stuff, we’ll write more complex methods that can control how fields are changed—for example, by using if
statements checkto check that a new value is valid and won’t break anything.
But for now, this is just another one of those frustrating rules that you just have to follow because your teacher is unreasonable.
WeatherRecord.cs
. Setters added to the WeatherRecord
class, and comments identifying the parts. 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
public class WeatherRecord
{
// Fields
private string date;
private int highTemperature;
private double averageWindSpeed;
// Getters
public string getDate()
{
return date;
}
public int getHighTemperature()
{
return highTemperature;
}
public double getAverageWindSpeed()
{
return averageWindSpeed;
}
// Setters and Mutators
public void setDate(string date)
{
this.date = date;
}
public void setHighTemperature(int highTemperature)
{
this.highTemperature = highTemperature;
}
public void setAverageWindSpeed(double averageWindSpeed)
{
this.averageWindSpeed = averageWindSpeed;
}
}
Sometimes mutator methods don’t follow the exact pattern and purpose of setters (simply setting a field’s value).
For example, a method might perform a series of calculations and changes to multiple fields, or it might change a field based on the value of another field.
These methods are still mutators, and we might even still refer to them as setters, but they don’t always follow the setFieldName
naming convention.
7.3.3. Using the Class
As we’ve learned, defining a class establishes a blueprint; to make use of a class in a program, we need to use that blueprint to create an object. We can as many objects from a class as we need, and each object is known as an instance of the class. And creating an instance is called instantiating a class.
To create our first objects, we use the same two steps we’ve been using to create variables: a declaration statement and an assignment statement. The declaration is still a data type and an _identifier, but in this case the data type is the name of the class:
1
WeatherRecord day1;
This creates a variable called day1
that will point to (or reference) the memory location where our object will be stored.
The identifier follows the same rules we learned for primitive variables: a descriptive name typed in camelCase (with a lowercase first letter).
In this case, the day1
object is going to maintain the record for the first day of our weather tracking.
The assignment statement works the same, but what we’re assigning looks a lot different.
We’ll use the new
keyword to allocate memory, and then we’ll call a constructor.
1
2
WeatherRecord day1;
day1 = new WeatherRecord();
We’re soon going to spend time learning about constructors, but here are the takeaways for now: the identifier is exactly the same as the class name, and it’s followed by parentheses.
We’ve already learned that parentheses in C# always means we’re referring to a method. A constructor is a special method called when instantiating an object. |
Just like with variables, we often combine those two statements into one line of code:
1
WeatherRecord day1 = new WeatherRecord();
Now that we have an object, we can call its instance methods using dot notation, which means we put the object name (not the class name!), followed by a dot, followed by the method call:
1
2
3
WeatherRecord day1 = new WeatherRecord();
day1.setHighTemperature(87);
Console.WriteLine("High temperature on day 1:" + day1.getHighTemperature());
In this example, we’re setting the highTemperature
field of day1
to 87 degrees, and then we’re retrieving the high temperature and outputting the returned value.
This is a good test of the set and get methods for the highTemperature
field.
It’s easy for beginners to forget to use that dot notation. To see why it’s necessary, consider the following example.
1
2
3
4
WeatherRecord day1 = new WeatherRecord();
WeatherRecord day2 = new WeatherRecord();
day1.setHighTemperature(87);
If we left off the day1.
part of the call, the compiler would not know which setHighTemperature()
method to use, day1
or day2
.
And even when we only have one instance, the compiler needs to know where to find that method, so the dot notation is required every time we call an instance method.
Object Classes vs. Driver Classes
Ok, time for another convention that seems only intended to be nitpicky and pointless, but is important and is expected on assignments in this course. OOP nerds value keeping the different parts of our programs compartmentalized, and that includes separating the class definition from the code that uses the class. A class we define for use as object can be called an object class or a user-defined class.
The code that uses the object class should be in its own class, and is often called a driver class.
The driver class contains the Main()
method, which is the entry point for the program.
A driver class actually goes by several different names.
Some people call it a main class because, well, it’s the class with the Main()
method; I don’t hear that term a lot, but it is out there.
I often use the terms demo class or test class because, as learners, we’re often making a class just to try a specific concept or skill, and the only thing our program really does is show that the object class is working.
And in those cases, we often see "test" or "demo"; so a driver class for our WeatherRecord
object class might be called WeatherRecordDemo
or TestWeatherRecord
, or something similar.
The point here is that, if we’ve created an object class called WeatherRecord
, we’re not going to put our Main()
method in that same class/file.
We’re going to make a separate class—a driver, or demo class, or test class.
I don’t much care what term you use, as long as it’s a separate class.
Your pitchforks are already sharpened, but here’s the part where you light your torches.
All of your input and output should be in the driver class.
That is, you generally can’t have any Write()
, WriteLine()
, or ReadLine() statements in your object classes.
My examples always demonstrate this separation of concerns, so you’ll have plenty of examples of what I mean.
Why can’t we put input/output in our object classes? * To "decouple" the UI from the business logic or guts of our program. This makes our code reusable in a variety of projects, such as web pages, mobile apps, and GUI applications—none of which are friendly to console input and output. Look up MVC and MVVM for all kinds of information about that. * To keep our code more readable by keeping the parts clearly identifiable. * Because I just don’t care much about input and output. I care about the classes you create, so I want to look at (and grade) that work separately. If your input and output don’t work but your object class looks good, you’re still going to get a good grade—if I’m able to separate out those mistakes.
Unfortunately, this is one of those things that boils down to, "because I said so" and "you’ll thank me later." Sorry, I can’t do much better than that for now.
WeatherRecordDemo.cs
. A driver class to demonstrate the WeatherRecord
class. 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
public class WeatherRecordDemo
{
public static void Main()
{
// Instantiate two objects
WeatherRecord day1 = new WeatherRecord();
WeatherRecord day2 = new WeatherRecord();
// Set field values for both instances
day1.setDate("2024-10-01");
day1.setHighTemperature(87);
day1.setAverageWindSpeed(1.5);
day2.setDate("2024-10-02");
day2.setHighTemperature(75);
day2.setAverageWindSpeed(8.25);
// Output field values for both instances
Console.WriteLine("Date: " + day1.getDate());
Console.WriteLine("High Temperature: " + day1.getHighTemperature());
Console.WriteLine("Average Wind Speed: " + day1.getAverageWindSpeed());
Console.WriteLine("------------------------------");
Console.WriteLine("Date: " + day2.getDate());
Console.WriteLine("High Temperature: " + day2.getHighTemperature());
Console.WriteLine("Average Wind Speed: " + day2.getAverageWindSpeed());
}
}
The driver class above creates two instances of the WeatherRecord
class, uses each setter, then outputs the return from each getter.
This ensures that instance variables are independent of each other and all instance methods work correctly.
In general, I ask students to create at least two instances of each class they are demonstrating.
The Lab Assignments in Canvas can be completed using what we’ve covered to this point. You might choose to complete that work now, then move onto the next section—which you’ll need for the Programming Project. |
7.4. Constructors
When we instantiate a new object, the syntax includes a call to a method, immediately following the new
keyword:
WeatherRecord day1 = new WeatherRecord();
This is a call to a special method called a constructor.
A constructor runs when an object is instantiated, and it’s used to set up the object with any initial values or behaviors.
A constructor’s primary job is to initialize the fields of the object—to give each instance variable a value.
If we don’t write a constructor, the compiler will create one for us; it’s called a default constructor, and it will set all fields to their default values.
For example, numeric fields like int
and double
will be set to zero, and string
fields will be set to null
.
We’ve been using setters to change those initial values to what we want, but we can also write our own constructors to set those values when the object is created.
Constructors are a special kind of method, so their syntax is a little different from other methods.
A constructor is always public, it has no return type (not even void
), and its name is the same as the class name.
WeatherRecord
class.1
2
3
4
public WeatherRecord()
{
// code to initialize fields goes here
}
The most important job of a constructor is setting values for each field of the object. As a beginner, our rule of thumb is to just make a simple assignment statement for each field.
1
2
3
4
5
6
public WeatherRecord()
{
this.date = "2025-01-01";
this.highTemperature = 0;
this.averageWindSpeed = 0.0;
}
Since our WeatherRecord
class has three fields, we’ve got three assignment statements in our constructor.
We can initialize those fields to any value we want, but we should choose values that make sense for the object; whatever we put there will be the default values that each object gets when it is instantiated.
Constructors should be written at the top of the class, before the fields and methods.
This constructor is called a parameterless constructor because it doesn’t have any parameters in the parentheses. It’s technically not a default constructor, because we wrote it ourselves rather than letting the compiler do it, but so many people call it a default constructor that the term is used more often than parameterless constructor. |
Constructors can also have parameters, which allows us to pass values to the constructor when we instantiate an object. This is useful when we want to set the initial values of the fields to something specific, rather than the default values. We add parameters to our constructor just like we do with any other method, by listing the data type and identifier in the parentheses.
1
2
3
4
5
6
public WeatherRecord(string date)
{
this.date = date;
this.highTemperature = 0;
this.averageWindSpeed = 0.0;
}
To use this constructor, we pass a string
value when we instantiate the object:
WeatherRecord day1 = new WeatherRecord("1998-01-25");
There are a couple of important things to note about this example:
-
This constructor only has one parameter but it still has three assignment statements. All fields need values, so if we don’t have a parameter to get a field’s value, we need to set it to a default value.
-
The parameter has the same name as the field:
date
. This is a common practice, but it’s potentially confusing. And it also violates guidance I gave you when we learned about variable scope.In this case, the parameter is a local variable to the constructor, and it’s shadowing the field. Our assignment statement needs to be carefully written:
this.date
refers to the field, anddate
refers to the parameter.
We can also overload constructors, which means we can have multiple constructors with different parameters—just like we can with any other method. That can include having a parameterless constructor and one or more constructors with parameters, or having multiple constructors with different numbers of parameters.
To see a complete example of the WeatherRecord class with constructors, fields, and methods, as well as a driver class to demonstrate it, visit the_Source code examples from this chapter and associated videos are available on GitHub repository for this chapter.
|
7.4.1. Constructors and Encapsulation
Constructors allow us to be stricter with our encapsulation since now we don’t have to have setters to put data into our objects. We can provide a constructor to accept all the data the object needs, decide if we give access to change a field after the object has been instantiated.
For example, if we’re making a bank account object, we’d need to provide an account number when we create the account, but we probably shouldn’t allow the account number to be changed after the account is created. In that case, our constructor would accept the account number, but we wouldn’t provide a setter for the account number.
7.5. static
Methods and Constants
Since we first started learning about methods, we’ve been using the static
keyword as part of those definitions.
And though you’re probably using top-level statements in your programs, we’ve also seen the static
keyword in front of the Main()
method when we create a program the "old fashioned" way.
However, we haven’t had enough context to understand what that keyword means.
With objects, we’ve learned about instance members, which are the fields and methods that belong to an object.
Fields are instance variables, which means that each object has its own copy of the field that can be changed without affecting other objects.
Instance methods are the code that an object can run, and they can access and change the fields of the object.
Instance members are defined without using the static
keyword, so we refer to them as nonstatic members.
When we use the static
keyword, we’re creating a class member--a field or method that belongs to the class itself, not to any object created from the class.
Put another way, a class member is shared by all objects created from the class, and it can be accessed without even creating an object.
Console
is a class that includes the Write()
and WriteLine()
methods we’ve been using, and they are class methods declared with the static
keyword.
Since they’re static
, we can call them without creating a Console
object:
Console.WriteLine("Hello, World!");
If WriteLine()
was an instance method (without the static
keyword), we’d have to create a Console
object before we could call it:
Console myConsole = new Console(); // Don't do this! myConsole.WriteLine("Hello, World!"); // It's not necessary!
That would be a pain, so we’re glad that WriteLine()
is static
.
In C#, when we create a constant (using the const
keyword), that constant is implicitly (automatically) static, so we don’t include the static
keyword.
When we put a constant in an object class, we’re creating a value that is shared by all objects created from the class.
If we have a savings account class, a common example when learning OOP, we might have a constant for the interest rate.
This would mean that every savings account would earn the same interest rate, which is often how banks work.
SavingsAccount.cs
. A class with a static
constant. 1
2
3
4
5
6
7
8
9
10
11
12
public class SavingsAccount
{
public const double INTEREST_RATE = 0.02;
// other fields and methods go here. See the repository for the complete code.
public void AddInterest()
{
balance += balance * INTEREST_RATE;
}
}
Check Yourself Before You Wreck Yourself (on the assignments)
Chapter Review Questions
Sample answers provided in the Liner Notes.