In previous articles, you already learned how to declare your own full-fledged classes with fields and methods. This is serious progress, well done!
But now I have to tell you an unpleasant truth. We didn't declare our classes correctly!
Why?
At first sight, the following class doesn't have any mistakes:
public class Cat {
public String name;
public int age;
public int weight;
public Cat(String name, int age, int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
public Cat() {
}
public void sayMeow() {
System.out.println("Meow!");
}
}
But it does. Imagine you're sitting at work and write this Cat
class to represent cats. And then you go home.
While you're gone, another programmer arrives at work. He creates his own Main
class, where he begins to use the Cat
class you wrote.
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "";
cat.age = -1000;
cat.weight = 0;
}
}
It doesn't matter why he did it and how it happened (maybe the guy's tired or didn't get enough sleep).
Something else matters: our current Cat
class allows fields to be assigned absolutely insane values. As a result, the program has objects with an invalid state (such as this cat that is -1000 years old).
So what error did we make when declaring our class?
We exposed our class's data.
The name, age and weight fields are public. They can be accessed anywhere in the program: simply create a Cat
object and any programmer has direct access to its data through the dot (.
) operator
Cat cat = new Cat();
cat.name = "";
Here we are directly accessing the name field and setting its value.
We need to somehow protect our data from improper external interference.
What do we need to do that?
First, all instance variables (fields) must be marked with the private modifier. Private is the strictest access modifier in Java. Once you do this, the fields of the Cat
class will not be accessible outside the class.
public class Cat {
private String name;
private int age;
private int weight;
public Cat(String name, int age, int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
public Cat() {
}
public void sayMeow() {
System.out.println("Meow!");
}
}
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "";//error! The Cat class's name field is private!
}
}
The compiler sees this and immediately generates an error.
Now the fields are sort of protected. But it turns out that we've shut down access perhaps too tightly: you can't get an existing cat's weight in the program, even if you need to.
This is also not an option. As it is, our class is essentially unusable.
Ideally, we need to allow some sort of limited access:
- Other programmers should be able to create
Cat
objects - They should be able to read data from existing objects (for example, get the name or age of an existing cat)
- It should also be possible to assign field values. But in doing so, only valid values should be allowed. Our objects should be protected from invalid values (e.g. age = -1000, etc.).
That's a decent list of requirements!
In reality, all this is easily achieved with special methods called getters and setters.
These names come from "get" (i.e. "method for getting the value of a field") and "set" (i.e. "method for setting the value of a field”).
Let's see how they look in our Cat
class:
public class Cat {
private String name;
private int age;
private int weight;
public Cat(String name, int age, int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
public Cat() {
}
public void sayMeow() {
System.out.println("Meow!");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
As you can see, they look pretty simple :) Their names often consist of "get"/"set" plus the name of relevant field.
For example, the getWeight()
method returns the value of the weight field for the object it is called on.
Here's how it looks in a program:
public class Main {
public static void main(String[] args) {
Cat smudge = new Cat("Smudge", 5, 4);
String smudgeName = smudge.getName();
int smudgeAge = smudge.getAge();
int smudgeWeight = smudge.getWeight();
System.out.println("Cat's name: " + smudgeName);
System.out.println("Cat's age: " + smudgeAge);
System.out.println("Cat's weight: " + smudgeWeight);
}
}
Console output:
Cat's name: Smudge
Cat's age: 5
Cat's weight: 4
Now another class (Main
) can access the Cat
fields, but only through getters. Note that getters have the public access modifier, i.e. they are available from anywhere in the program.
But what about assigning values? This is what setter methods are for
public void setName(String name) {
this.name = name;
}
As you can see, they are also simple. We call the setName()
method on a Cat
object, pass a string as an argument, and the string is assigned to the object's name field.
public class Main {
public static void main(String[] args) {
Cat smudge = new Cat("Smudge", 5, 4);
System.out.println("Cat's original name: " + smudge.getName());
smudge.setName("Mr. Smudge");
System.out.println("Cat's new name: " + smudge.getName());
}
}
Here we're using both getters and setters. First, we use a getter to get and display the cat's original name. Then, we use a setter to assign a new name ("Mr. Smudge"). And then we use the getter once again to get the name (to check if it really changed).
Console output:
Cat's original name: Smudge
Cat's new name: Mr. Smudge
So what's the difference? We can still assign invalid values to fields even if we have setters:
public class Main {
public static void main(String[] args) {
Cat smudge = new Cat("Smudge", 5, 4);
smudge.setAge(-1000);
System.out.println("Smudge's age: " + smudge.getAge());
}
}
Console output:
Smudge's age: -1000 years
The difference is that a setter is a full-fledged method. And unlike a field, a method lets you write the verification logic necessary to prevent unacceptable values. For example, you can easily prevent a negative number from being assigned as an age:
public void setAge(int age) {
if (age >= 0) {
this.age = age;
} else {
System.out.println("Error! Age can't be negative!");
}
}
And now our code works correctly!
public class Main {
public static void main(String[] args) {
Cat smudge = new Cat("Smudge", 5, 4);
smudge.setAge(-1000);
System.out.println("Smudge's age: " + smudge.getAge());
}
}
Console output:
Error! Age can't be negative!
Smudge's age: 5 years
Inside the setter, we created a restriction that protected us from the attempt to set invalid data. Smudge's age wasn't changed.
You should always create getters and setters. Even if there are no restrictions on what values your fields can take, these helper methods will do no harm.
Imagine the following situation: you and your colleagues are writing a program together. You create a Cat
class with public fields. All the programmers are using them however they want. And then one fine day you realize: "Crap, sooner or later someone might accidentally assign a negative number to the weight! We need to create setters and make all the fields private!"
You do just that, and instantly break all the code written by your colleagues. After all, they've already written a bunch of code that accesses the Cat
fields directly.
cat.name = "Behemoth";
And now the fields are private and the compiler spews a bunch of errors!
cat.name = "Behemoth";//error! The Cat class's name field is private!
In this case, it would be better to hide the fields and create getter and setters from the very beginning. All your colleagues would have used them. And if you belatedly realized you needed to somehow restrict the field values, you could have just written the check inside the setter. And nobody's code would be broken.
Of course, if you want access to a field to just be "read only", you can create only a getter for it.
Only methods should be available externally (i.e. outside your class). Data should be hidden.
We could make a comparison to a mobile phone. Imagine that instead of the usual enclosed mobile phone, you were given a phone with an open case, with all sorts of protruding wires, circuits, etc. But the phone works: if you try really hard and poke the circuits, you might even be able to make a call. But you'll probably just break it.
Instead, the manufacturer gives you an interface: the user simply enters the correct digits, presses the green call button, and the call begins. She doesn't care what happens inside with the circuits and wires, or how they get their job done.
In this example, the company limits access to the phone's "insides" (data) and exposes only an interface (methods). As a result, the user gets what she wanted (the ability to make a call) and certainly won't break anything inside.
Was published on CodeGym blog .