Class and Object
Learning goals
- You understand the relationship between a class and an object, and how a class serves as a template for creating objects.
- You can define your own class and its members: attributes, methods, and constructors.
- You understand the lifecycle of objects and know how to use objects as parts of a program.
- You understand how the
thisreference works in object methods. - You know what the
staticmodifier means for class members.
Class
The first step in object-oriented programming is defining a class using the class keyword. A class can be thought of as a blueprint or template from which objects are created. A class specifies what data an object contains (attributes) and what it can do (methods). Attributes and methods defined in a class are also called class members.
Let's do a small thought experiment: consider building houses. From a single architectural blueprint, many buildings can be constructed. They would have the same structure because they are built according to the same design, but each building would still have its own state: a different owner, color, interior, and so on. The blueprint can be compared to a class in object-oriented programming, while the buildings are the objects created from it. The name of a class describes what an object is. Therefore, if we created a class for buildings, a suitable name would be Building.
Note that in Java, class names are conventionally written starting with an uppercase letter.
Let's begin by defining an empty Building class and gradually expand it.
class Building {
// Inside the class we define the structure
// that objects created from this class will follow.
}
Objects (instances) are created from a class using the new keyword. This allocates memory for the object, selects and executes an appropriate constructor, and returns a reference to the newly created object. We store this reference in a variable so that we can access the object through it. The class name serves as the variable's type and tells the compiler what kind of object must exist at the referenced memory location.
void main() {
// The expression 'new Building()' creates an object
// and returns a reference to it.
// We store this reference in the variable 'building'.
Building building = new Building();
}
note
A reference variable and an object are two different things. A reference variable is like an arrow that can point to an object. However, a reference variable does not have to point to anything, in which case its value is null.
An object may also be created without assigning its reference to a variable. However, if there are no references pointing to it, the object becomes inaccessible and is automatically marked as garbage.
Multiple reference variables may refer to the same object, but a single reference variable can point to only one object at a time.
Attributes
An attribute is a variable defined inside a class but outside any methods. It represents a property, characteristic, or state of an object. Objects created from a class always contain the attributes defined in that class. While a class defines what information objects can have, attributes store the concrete values for each individual object. Like variables in general, attributes may be primitive data types or references. The same naming conventions apply to them. Note that variables declared inside methods are not attributes; they are local variables. Local variables are not part of an object's state.
Let's add a few attributes to our Building class.
public class Building {
// These variables are attributes of the object.
// Every building has an owner and a color, but
// the values do not have to be the same for all objects.
private String owner;
// An attribute can be given a default value.
// In this case, the default color is blue.
private String color = "blue";
}
Attributes differ from local variables because their visibility can be controlled using access modifiers. Local variables only exist and are visible within a method during its execution. Attributes, on the other hand, exist for the entire lifetime of the object and are visible to all members within the class. Attributes can also be made visible outside the object, although this is generally considered a poor design choice. We will return to access modifiers later.
note
A local variable may have the same name as an attribute, in which case the local variable shadows the attribute. When an attribute and a local variable share the same name, expressions refer to the local variable by default. Even in this case, methods can still access the attribute using the this reference, which we will discuss shortly.
An attribute may be assigned a default value in the class definition. Objects created from that class then receive the same initial value for their corresponding attribute. If no default value is provided, the attribute's value can be set when the object is created, later through methods, or left uninitialized.
public class Building {
// Object attribute with a default value.
private String color = "blue";
public void print() {
// This local variable shadows the attribute.
String color = "red";
// Prints "red".
// The identifier 'color' refers to the local variable.
IO.println(color);
// Prints "blue".
// The object's attribute can be accessed using 'this'.
IO.println(this.color);
}
}
Methods
Subroutines defined inside a class are called methods. While an attribute represents the internal state of an object, a method can be described as the object's ability to perform some action. One distinctive feature of Java is that all subroutines are always contained within some class. Perhaps for this reason, Java developers commonly refer to all subroutines as methods.
Method definitions do not differ syntactically from other subroutines, and the same naming conventions apply. Methods can also be overloaded. Like attributes, their visibility outside the class can be controlled using access modifiers.
As with any subroutine, a method should perform the task indicated by its name. Large tasks should generally be divided into smaller methods.
To call an object's method, we need both the object and a reference to it. Let's add a couple of simple methods to our Building class for manipulating its state. Methods like these are often called accessor methods.
public class Building {
private String owner;
private String color;
// Object method that receives a string and stores
// it in the 'color' attribute.
public void setColor(String color) {
// The parameter and attribute have the same name,
// so we use the 'this' reference.
this.color = color;
}
// Object method that returns the value of the
// 'color' attribute to the caller.
public String getColor() {
return this.color;
}
}
We could also add similar methods for the class's other attribute. Later in this chapter, we will discuss more sensible ways of managing object state, but for now simple get and set methods are sufficient.
The this Reference
The keyword this refers to the object itself. It acts as a reference to "this object" in whose context the code is currently executing.
In previous examples, we used the this reference to read the object's attributes:
public class Building {
private String color;
// ...
public String getColor() {
return this.color;
}
}
This reference is automatically available whenever an object's method is called.
When a method is invoked, this is automatically set to refer to the object whose method was called, and it cannot be changed. Through this reference, the method can access the correct object's state and other methods.
Inside object methods, writing this is only required when another member with the same name exists within the current scope, such as a local variable. Therefore, we do not have to prefix every attribute or method call with this; the compiler can determine it automatically if no ambiguity exists.
In some programming languages, a similar reference is called self.
Let's look at a few examples.
public class Building {
private String owner;
private String color;
public void setColor(String color) {
// We use 'this' because the parameter and
// attribute have the same name.
this.color = color;
}
public String getColor() {
// Here 'this' is optional.
// There are no other variables named 'color'
// in this scope.
return this.color;
}
}
Inside methods, we can use this just like any other reference variable.
For example, we can pass it as a parameter to another method. This is not necessary when calling the object's own methods, but it allows us to pass a reference to the object to methods outside the class.
public class Building {
private String owner;
private String color;
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
// Object method
public String beautify() {
// Pass this object to a method in another class.
return Formatter.formatNicely(this);
}
}
Constructor
A constructor is a special method of a class that is used to initialize the state of a new object when it is created. The constructor's name is always the same as the class name and begins with an uppercase letter. This differs from the naming style typically used for other methods. A constructor does not have a return type. Instead, it always returns a reference to the newly constructed object.
Every class must have at least one constructor. So far, however, we have been creating objects without defining one ourselves. This works in Java because if a class contains no constructors at all, the compiler automatically creates a parameterless constructor. This automatically generated default constructor contains no statements. However, it is only generated if the class does not define any constructor of its own.
Because constructors share the same name as the class, defining multiple constructors with different parameters is a form of method overloading. The compiler chooses the appropriate constructor automatically based on the arguments provided during object creation.
Let's temporarily remove the methods from the Building class and examine what happens when objects are created.
public class Building {
private String owner;
private String color;
}
In this case, the object is created using the compiler-generated default constructor because no constructor has been defined. We can also define an equivalent constructor ourselves:
public class Building {
private String owner;
private String color;
// Parameterless constructor equivalent
// to the default constructor.
public Building() {
// Here we could initialize the
// object's state if desired.
}
}
Up to this point, we have been creating objects without proper initialization. This is generally not good practice.
A newly created building currently has neither an owner nor a color. The values of these attributes are null.
Depending on the object and its intended use, such values may be acceptable. However, if a building exists, it should probably have some owner and color.
A better approach is for the object to be usable immediately after creation. We could create an object directly in the correct staty by defining a constructor that accepts the information needed to initialize the object's state as parameters and properly initializes its attributes.
Let's add a constructor to the Building class that receives the owner and color as parameters and uses them to initialize the object's attributes. This way, we do not have to set the attributes separately after the object is created.
We can also remove the parameterless constructor so that objects can no longer be created in an invalid initial state.
public class Building {
private String owner;
private String color;
public Building(String owner, String color) {
// Initialize the object's state using the parameters.
// Note the use of 'this', since the parameter names
// are the same as the attribute names.
this.owner = owner;
this.color = color;
}
}
A couple of observations:
We cannot create a building using only one parameter, for example
Building building2 = new Building("University of Jyväskylä");
because the constructor requires two parameters.
Similarly, we cannot create a building without parameters
Building building3 = new Building();
because the default constructor no longer exists.
Let's add a somewhat more advanced constructor that accepts another object of the same class and copies its state into the newly created object. Such a constructor is often called a copy constructor.
public class Building {
private String owner;
private String color;
public Building(String owner, String color) {
this.owner = owner;
this.color = color;
}
// Constructor that copies another Building object.
public Building(Building source) {
this.owner = source.owner;
this.color = source.color;
}
}
We can also use the this keyword inside a constructor similarly to a method call if we want to delegate object creation to another constructor of the same class. This often helps avoid code duplication.
Let's modify the class so that one constructor uses another constructor to initialize the attributes.
public class Building {
private String owner;
private String color;
public Building(String owner, String color) {
this.owner = owner;
this.color = color;
}
public Building(Building source) {
// Delegate construction to the constructor above.
this(source.owner, source.color);
}
}
Create a class called Clock with the integer attributes minutes and hours.
Add the methods setMinutes and setHours to the class. These methods take the minutes and hours to be set as parameters.
The value of minutes must be between 0 and 59; otherwise, an error message should be printed and the time should remain unchanged. Similarly, the value of hours must be between 0 and 23; otherwise, an error message should be printed.
Also create a method called displayTime that prints the time in the format "HH:MM".
You can test the functionality of the class using the provided main program.
Create a class called Timer with the integer attributes minutes and seconds.
Add the methods addMinutes and addSeconds to the class. These methods take the number of minutes and seconds to be added to the timer as parameters. Also add a method called toString that returns the timer's minutes and seconds as a string.
The number of minutes can be any non-negative value, but the number of seconds must always be between 0 and 59. If the seconds exceed this range, they must be converted into minutes. You can convert seconds into minutes and remaining seconds as follows:
int seconds = 75; // Example of value in parameter
// This gives the amount of minutes to be added
int minutesToAdd = (this.seconds + seconds) / 60;
int remainingSeconds = (this.seconds + seconds) % 60;
You can test the functionality of the class using the provided main program.
Choose a real-world object or concept and create your own simple class for it.
Add the following to the class:
- At least two attributes that are appropriate for the class.
- At least two methods that modify or examine the state of the object in some way.
- At least two different constructors that initialize the object to a sensible state.
What constitutes a sensible state depends on the object you choose. You may use the examples from the course material as inspiration when coming up with attributes and methods.
Create an object and experiment with changing its state in the main program. Print the object's information both before and after modifying its state.
Static
Class members can be defined as belonging to the class itself rather than to individual objects by using the static modifier. Such members are called class attributes and class methods.
The word static can be slightly misleading. In this context, it does not mean that the member is permanent or immutable. Throughout this material, however, we use the term static to refer to class members.
Attributes and methods normally belong to an object. Object attributes and methods can access the object's state. A class attribute, on the other hand, is not part of any object's state. There is only one copy of it, shared by all objects of the class. If one object changes the value of a class attribute, the change is visible to all objects of the same class.
Similarly, class methods are shared by all objects of a class. They do not belong to any particular object and therefore cannot directly access an object's state.
Objects may call static methods of their class, but doing so does not provide access to the object's state.
Since static methods do not belong to any object, the this reference cannot be used inside them.
To call an object method, we need an object and a reference to it. A static class method, however, can be called directly through the class without creating an object. Although static methods can also be invoked through object references, doing so still does not grant access to any object's state.
Let's start with a class that has no static members.
public class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getName() {
return firstName + " " + lastName;
}
}
Because getName is an object method, we must first create an object. We cannot call it statically using something like Person.getName().
Now let's add a class attribute called adultAgeLimit, which represents the age of adulthood for all people. We will also add a method called isAdult that checks whether a given age qualifies as an adult.
Notice that this method is not associated with any particular object. It simply evaluates the given age.
public class Person {
private String firstName;
private String lastName;
private static int adultAgeLimit = 18;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getName() {
return firstName + " " + lastName;
}
public static boolean isAdult(int age) {
return age >= adultAgeLimit;
}
}
We now have a class method, isAdult, that can be called without creating an object.
We could call this through an object, but that's unecessary and compiler warns about it.
Static members are useful when we want functionality that does not belong to any single object but instead relates to the class as a whole.
Static members can also be accessed from object methods. For example, we can check if person is adult
// An object method can use a static variable.
public boolean isPersonAdult() {
return this.age >= adultAgeLimit;
}
For clarity, it is often a good idea to use the class name even inside the class itself
Person.adultAgeLimit.
This makes it easier to distinguish static members from object members.
The previous example is also a warning: the adulthood limit can be modified from anywhere that has access to the Person class. Using this kind of global shared state is generally considered a poor design choice.
One example of a static method that we use frequently is IO.println().
This is a static method of IO class
and it is convenient because we do not need to create a separate object every time we want to print something.
Modify the example Person class so that every created person is automatically assigned a unique identification number, which is an integer.
The first person created should receive the number 1, the next person should receive the number 2, and so on.
The number must not be assigned from outside the object, for example by calling one of its methods.
Also implement a method called getNumber that returns the identification number of a particular object as an integer. No other methods need to be added.
Tip
You will need static class members in this exercise.
Before you begin, consider the following questions:
- When does an object receive its number?
- How does an object know what its number should be?
- What information is shared among all objects, and what information is specific to each object?
Object Lifecycle
During program execution, instances of classes are created; these instances are called objects. To access an object, we create a reference variable for it. During compilation, Java verifies that the variable type is compatible with the object being created and then stores a reference to the newly created object in that variable.
When an object is created, Java allocates space for it in the JVM's heap memory. When there are no longer any references to the object, it becomes eligible for destruction. In Java, programmers do not need to manually allocate or free memory. The memory occupied by discarded objects is eventually reclaimed by Java's automatic garbage collector.
Let's walk through an object's entire lifecycle using examples.
To create objects, we first need a class. We'll once again use a Person class and add a few simple methods for modifying the object's state.
class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getName() {
return firstName + " " + lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
In the main program above, objects are created in several different ways while demonstrating how references behave.
Once objects have been created, we can inspect and modify their state during program execution.
void main() {
Person p1 = new Person("Joni", "Mäkinen");
IO.println(p1.getName());
p1.setLastName("Korhonen");
IO.println(p1.getName());
}
Let's finally examine the end of an object's lifecycle: its destruction. When an object no longer has any references pointing to it, it becomes garbage, meaning that Java's automatic garbage collection mechanism may eventually remove it from memory and reclaim the space it occupies.
void main() {
// Create two objects.
Person p1 = new Person("Joni", "Mäkinen");
Person p2 = new Person("Anna", "Korhonen");
// p1 is currently the only reference to the first object.
// If we assign another reference to p1 or set it to null,
// the object becomes eligible for garbage collection,
// because no references to it remain.
p1 = null;
// When the method ends, all local variables created
// inside it (p1 and p2) are destroyed.
// If there are no other references to the objects,
// they too become eligible for garbage collection.
}
We will not study Java's garbage collector or memory management in great detail in this course. For the purposes of this course, it is sufficient to understand when an object becomes garbage and can be destroyed. Once no references to an object exist, it becomes eligible for garbage collection, and Java may later reclaim the memory it occupies. If you would like to explore the topic in more detail, you can start by reading more about heap memory and memory allocation here, and about Java garbage collection from here, both of which provide relatively accessible introductions to the subject.
Create a class called Phone with the attributes brand (a string) and batteryLevel (an integer representing the battery charge as a percentage between 0 and 100).
Add the following methods to the class:
-
sendMessage(String person, String message): prints a message in the format:"Sending a message to <person>: <message>"Sending a message reduces the battery level by 5 percentage points. -
call(String person, int minutes): prints a message in the format:"Calling <person>, duration: <minutes> minutes"Making a call reduces the battery level by 1 percentage point per minute. -
charge(int percentage): increases the battery level by the given amount, but the battery level cannot exceed 100%. -
printInfo(): prints the phone's brand and battery level in the format:"The battery level of the <brand> phone is <battery>%."
Replace the parts enclosed in angle brackets with the appropriate attribute or parameter values.
The battery level cannot drop below 0%.
If the battery level is 0%, messages cannot be sent and calls cannot be made. Print:
"Battery empty. Cannot send message."
or
"Battery empty. Cannot make a call."
Test your application by creating a Phone object, sending a message, making a call, charging the battery, and printing the phone's information.
Optional additional exercise: if battery runs out
Modify the sendMessage and call methods so that if the battery level is not sufficient to send the entire message or complete the entire call, the message is sent or the call is made only for as long as the battery lasts.
If the battery runs out during a message or call, print how much of the message was successfully sent or how long the call lasted before the battery was depleted. For example:
"Battery empty. You managed to send 60% of the message.""The battery ran out after 3 minutes."
Implement a class called Book that keeps track of individual books while also maintaining global library statistics.
Create the following variables for the class:
Instance variables:
String title: The title of the book.String author: The author of the book.boolean isBorrowed: Indicates whether the book is currently on loan.
Class variables (static):
static int totalBooks: The total number of books that have been created.static int borrowedBooks: The number of books that are currently on loan.
The constructor should take the title and author as parameters. Whenever a new book is created, the totalBooks variable should be increased by one.
Create the instance methods borrow() and returnBook(). These methods change the book's isBorrowed status and update the static borrowedBooks counter.
Create a static method printStatistics() that prints the library statistics in the following format:
"Library contains X books, of which Y are currently on loan."
A main program is provided that you can use to test your implementation.