Lesson 10 - Getters and setters in Java
In the previous exercise, Solved tasks for OOP in Java lesson 9, we've practiced our knowledge from previous lessons.
In the previous lesson, Solved tasks for OOP in Java lesson 9, we learned about static class members in Java. In today's tutorial, we're going to look at getter and setter methods.
Getters and setters
We often want to have control over how an object field is being changed from outside of the class. We'd like to set the field as read-only or react to its changes somehow. Let's create a new project (name it GetSet) and add the following Student class which will represent a student in a database.
public class Student { public String name; public boolean male; public int age; public boolean fullAged; public Student(String name, boolean male, int age) { this.name = name; this.male = male; this.age = age; fullAged = true; if (age < 18) { fullAged = false; } } @Override public String toString() { String iAmFullAged = "I'm"; if (!fullAged) { iAmFullAged = "I'm not"; } String gender = "male"; if (!male) { gender = "female"; } return String.format("I'm %s, %s. I'm %d years old and %s of age.", name, gender, age, iAmFullAged); } }
The class is very simple, the student has a name, gender, and age. The
fullAged field is set according to the age and provides a more
comfortable determination whether the student is of age from different places in
the system. We use a boolean
value to store the gender, true
indicates that he's male. The constructor will determine whether the student is
of age or not. The toString() method has been altered to suit our needs. In a
real world situation, it'd probably just return the student's name. Let's create
a student using the constructor:
{JAVA_OOP}
public class Student {
public String name;
public boolean male;
public int age;
public boolean fullAged;
public Student(String name, boolean male, int age) {
this.name = name;
this.male = male;
this.age = age;
fullAged = true;
if (age < 18) {
fullAged = false;
}
}
@Override
public String toString() {
String iAmFullAged = "I'm";
if (!fullAged) {
iAmFullAged = "I'm not";
}
String gender = "male";
if (!male) {
gender = "female";
}
return String.format("I'm %s, %s. I'm %d years old and %s of age.", name, gender, age, iAmFullAged);
}
}
{JAVA_MAIN_BLOCK}
Student s = new Student("Peter Brown", true, 20);
System.out.println(s);
{/JAVA_MAIN_BLOCK}
{/JAVA_OOP}
The output:
Console application
I'm Peter Brown, male. I'm 20 years old and I'm of age.
Everything looks nice, but the fields can be re-written. We can break the object like this, so it'll no longer function properly (i.e. would have an inconsistent internal state):
{JAVA_OOP}
public class Student {
public String name;
public boolean male;
public int age;
public boolean fullAged;
public Student(String name, boolean male, int age) {
this.name = name;
this.male = male;
this.age = age;
fullAged = true;
if (age < 18) {
fullAged = false;
}
}
@Override
public String toString() {
String iAmFullAged = "I'm";
if (!fullAged) {
iAmFullAged = "I'm not";
}
String gender = "male";
if (!male) {
gender = "female";
}
return String.format("I'm %s, %s. I'm %d years old and %s of age.", name, gender, age, iAmFullAged);
}
}
{JAVA_MAIN_BLOCK}
Student s = new Student("Peter Brown", true, 20);
s.age = 15;
s.male = false;
System.out.println(s);
{/JAVA_MAIN_BLOCK}
{/JAVA_OOP}
The output:
Console application
I'm Peter Brown, female. I'm 15 years old and I'm of age.
Certainly, we would want fullAged to be updated when the age is
changed. Aside from that, there is no need to allow e.g. a gender to be changed.
However, we want to keep the properties accessible for reading, so we can't make
them private
. In earlier lessons of our course, we've used get
methods to read private fields. We would name them something like getAge() and
so on. In Java, we declare all the fields which should be accessible from the
outside as private and we implement similar methods to make them accessible. We
name them as getFieldName() for reading and setFieldName() for writing. For
boolean fields, we usually name getters as isFieldName(). The class would now
look something like this:
public class Student { private String name; private boolean male; private int age; private boolean fullAged; public Student(String name, boolean male, int age) { this.name = name; this.male = male; setAge(age); } public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isMale() { return male; } public int getAge() { return age; } public void setAge(int age) { this.age = age; // updating whether student is of age fullAged = true; if (age < 18) { fullAged = false; } } public boolean isFullAged() { return fullAged; } @Override public String toString() { String iAmFullAged = "I'm"; if (!fullAged) { iAmFullAged = "I'm not"; } String gender = "male"; if (!male) { gender = "female"; } return String.format("I'm %s, %s. I'm %d years old and %s of age.", name, gender, age, iAmFullAged); } }
The methods just returning a value are very simple. In the method setting the
age is some more logic, since we have to reconsider the fullAged
field. We made sure we can't set variables in any other way than what we want.
We now control all the field changes and can work with them as needed. Our
design must prevent all unwanted changes of the internal state that would cause
an object to malfunction. Notice the change in the constructor where we set the
age using the setAge() method.
Methods for returning values are called getters and methods for setting values are called setters. We could potentially add an editStudent() method to edit the other fields sort of like the constructor. Essentially, the student's name, age, and other fields would be updated using this method. We could also validate the values being set there since we would be able to handle all attempts to change certain values in one place.
The question now is, what are the benefits of the name field being private when we can both write it and read it? We now got 2 methods for accessing it which only occupies place in the code. And isn't it also slower?
We really wrote that right, or at least as Java programmers usually do that. Java performs many optimizations before compiling so if the methods are as simple they only return a value, the method is compiled as a direct access to the memory. SO they are as fast as we used plain attributes (considering there is no other logic in the getter).
IDE (NetBeans) can generate getters and setters automatically for us, there is no need to write them over and over again. The only this we need to do is to click on the variable with the right mouse button and choose Redactor -> Encapsulate fields.
In the next dialog, we specify for which fields we want to generate getters and setters for. We won't generate setters for fullyAged and male fields, making them read-only. There is no sense in changing the gender at all (if really needed, there would be some special method to avoid doing that accidentally). We confirm the dialog.
Compiler optimizations solved the performance issue which would be caused by calling the methods otherwise. We also bypassed manual writing methods manually by generating them automatically. The question still is, what are the benefits?
The main reason is some kind of standardization. We don't have to think about whether the property we want to access has a getter or it's a public field, we just always call the method starting with get* on the instance, resp. set* if we want to change it.
Another benefit is that we can decide later that we want to make some field as read-only, we just remove the setter. We don't have to create a getter and change the field access modifier, which would change the class interface and break existing code using it.
From now on, we'll generate getters and setters for all fields which should be accessible from the outside. There will be almost no public fields in our classes anymore.
Let's try the code that broke the object's inner state once more:
{JAVA_OOP}
public class Student {
private String name;
private boolean male;
private int age;
private boolean fullAged;
public Student(String name, boolean male, int age) {
this.name = name;
this.male = male;
setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isMale() {
return male;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
// updating whether student is of age
fullAged = true;
if (age < 18) {
fullAged = false;
}
}
public boolean isFullAged() {
return fullAged;
}
@Override
public String toString() {
String iAmFullAged = "I'm";
if (!fullAged) {
iAmFullAged = "I'm not";
}
String gender = "male";
if (!male) {
gender = "female";
}
return String.format("I'm %s, %s. I'm %d years old and %s of age.", name, gender, age, iAmFullAged);
}
}
{JAVA_MAIN_BLOCK}
Student s = new Student("Peter Brown", true, 20);
s.setAge(15);
// s.setMale(false); // This line has to be commented out since it's no longer possible to change the gender
System.out.println(s);
{/JAVA_MAIN_BLOCK}
{/JAVA_OOP}
And the output is correct now:
Console application
I'm Peter Brown, male. I'm 15 years old and I'm not of age.
In the next lesson, ArrayList in Java, we'll learn about lists in Java.