4. 0100 - Classes and Objects
What’s the Point?
-
Understand the basics of object-oriented programming (OOP).
-
Use a class diagram to plan a program.
-
Create a class in accordance with OOP principles.
-
Create a driver class and use objects.
-
Define and use constructors.
-
Understand the role of
static
in Java.
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. This course is all about object-oriented programming (OOP), but I want students to have a handful of basic coding skills before we dive into OOP. In this chapter, we’ll start our transition to OOP by learning about classes and objects, which are the basic building blocks of OOP.
4.1. OOP Basics
Until now, we have been organizing our programs into methods, which is a procedural programming approach. We’ve been thinking of a program as a collection of tasks that need to be performed in a specific order. There’s nothing wrong with this approach, 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.
Now we’re ready to start our transition to object-oriented programming (OOP), which 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.
4.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 UML (Unified Modeling Language). The UML standards are extensive, but we’ll be using a simplified version in this course.
Consider a program to handle orders at a restaurant. A simple class to represent one person’s order might look like this:

The top section of the diagram has the name of the class, which is Order
.
The next section is for the attributes of the class—the information the class will store. This example has attributes for the server’s name, the table number, the items ordered, and the total 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 an order, we might send it to the kitchen, print the bill, or close the bill.
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. |
4.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 an Order
class in a program used by a restaurant, we don’t want some other class to change attributes in a way that disrupts the program—like changing the entree selection to something that’s not on the menu, or setting the price to 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. As an analogy, consider the counter at a fast food restaurant. We can’t just reach over and grab a handful of fries; we have to ask the employee behind the counter to get them for us. In this analogy, the food is encapsulated and we can only access it by using a behavior, like "order food".
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 Java, 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. In fact, I would argue that if we don’t encapsulate our classes, we’re not really doing object-oriented programming. And that’s what we’re here to learn.
4.3. Defining and Using a Class
We’ll look at a program to keep rudimentary weather records; for a single day’s weather data, we’ll have a class called WeatherRecord
.

WeatherRecord
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.
WeatherRecord.java
. A class header and code block.1
2
3
4
5
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 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. To begin with, we’ll focus on two types of instance members: fields and methods.
4.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.java
. Fields added to the WeatherRecord
class.1
2
3
4
5
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 they’re a good thing. The fields are encapsulated, so they can only be accessed and changed by the methods of the class—which is a good thing. And the fields are unique to each object, so we can have multiple objects with different values for the fields—which is also a good thing. And so no puppies' toys will be harmed as long as we use private fields correctly.
4.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
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.java
. Getters added to the WeatherRecord
class. 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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.
As 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 don’t know enough Java yet to do anything about it. As we learn more about Java, we’ll be able to write more complex methods that can control how fields are changed—for example, by checking the new value to make sure it’s valid and won’t break anything. But for now, this is just another one of those frustrating rules that you just have to follow until you know enough to understand it.
WeatherRecord.java
. 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
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.
4.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 a lot of 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 Java 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:
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);
System.out.println("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
.
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 parts of our programs compartmentalized, and that includes separating the class definition and the code that uses the class. A class definition goes in its own file, which must have a filename exactly match the name of the class (with .java as an extension)--and that one’s not a convention, that’s a syntax rule for the compiler. 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 file, 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 and file.
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 print()
or println()
statements, any dialog popups (if you know how to use JOptionPane
or similar), or any Scanner
input calls 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.java
. 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
public class WeatherRecordDemo {
public static void main(String[] args) {
// 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
System.out.println("Date: " + day1.getDate());
System.out.println("High Temperature: " + day1.getHighTemperature());
System.out.println("Average Wind Speed: " + day1.getAverageWindSpeed());
System.out.println("------------------------------");
System.out.println("Date: " + day2.getDate());
System.out.println("High Temperature: " + day2.getHighTemperature());
System.out.println("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. |
4.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.
A definition for a constructor for the WeatherRecord
class would look like this:
WeatherRecord
class.1
2
3
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
public WeatherRecord() {
date = "2025-01-01";
highTemperature = 0;
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
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.
|
4.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.
4.5. static
Constants and Methods
Since our first Hello World
program, we’ve been using the static
keyword to create methods—beginning with the main()
method that is the starting point for every Java program.
However, we haven’t had enough context to understand what that keyword means.
We’ve learned about instance members, which are the fields and methods that belong to a class, and instance members, which are the fields and methods that belong to an object.
Fields are instance members, 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 creating an object.
System
is a class that includes the print() and println() methods we’ve been using, and those methods are static
.
Since they’re static
, we can call them without creating a System
object:
System.out.println("Hello, World!");
If println()
were an instance method, we’d have to create a System
object before we could call it:
System mySystem = new System();
mySystem.println("Hello, World!");
That would be a pain, so we’re glad that println()
is static
.
The print() and println() are slightly more complicated than that, since they’re actually instance methods of the PrintStream class, which is a class that System.out is an instance of. But for our purposes, we can think of them as static methods of the System class. You don’t need to know that right now, but if I don’t mention it then somebody will claim I’m an idiot. I probably am an idiot, but not for this reason.
|
Throughout our learning, we’ll learn more about using static
in our programs and classes, but an important one to known about for now is the static
constant.
When we put a static
constant in an object class (which would also use the keyword final
), 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 static final double INTEREST_RATE
constant. This would mean that every savings account would earn the same interest rate, which is often how banks work.
SavingsAccount.java
. A class with a static
constant. 1
2
3
4
5
6
7
8
9
10
public class SavingsAccount {
private static final 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)
Can you answer these questions?
Sample answers provided in Stuff That’s Tacked On The End.