In the lesson about classes, you learned about how to create instance variables to store important data inside each instance of that class, although nothing was covered in too much detail. To review, let’s take a look at the following code example.
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
This class has a constructor that accepts a name and an age and creates a new Dog object, storing this information in the instance variables. Since this wasn’t covered deeply in the last lesson, there are probably a lot of questions that you have. For example, why are the instance variables private when everything else we’ve worked with has been public? Another question could be why we need to include this
when setting these variables?
We are first going to cover the meaning of the this
keyword. There are two ways of using this
. In the examples we’ve given in the past, using this
allows you to refer to the current object that you’re located in. In the example above, there are two name
and two age
variables that you could be referring to. Without including this
(for example: name = name
), Java assumes you are referencing the local variable name both times, meaning that nothing is going to happen to that private String name
variable. In order to store the values of those local variable given to you into the instance variables, you have to include this.
to tell Java that you’re referring to the instance variables in this object, that way the values passed in through the constructor can be properly stored. To make more sense of this, try running the following code on your computer:
public class Test {
private int num; // Default value is 0
public Test (int num) {
System.out.println(this.num); // Will print 0
num = num;
System.out.println(this.num); // Will still print 0
this.num = num;
System.out.println(this.num); // Will finally print 5
}
public static void main(String[] args) {
Test t = new Test(5);
}
}
Using this
helps to avoid confusion between local and global variables. Of course, if none of the variables share the same name, you do not need to include this
when referring to instance variables as Java will easily be able to figure it out.
public class Test {
private int num1; // Default value is 0
public Test (int num2) {
num1 = num2; // this.num1 is optional
System.out.println(num1); // Will print 5
}
public static void main(String[] args) {
Test t = new Test(5);
}
}
Another way to use this
is when you have multiple constructors. In the previous lessons, we have had only one constructor, but it can be useful to have multiple in a similar way to method overloading. This allows the user to pass in whatever information they have when creating a new object, like in the example below:
public Dog() { // Creates a Dog without a name or age
this.name = null;
this.age = 0;
}
public Dog(String name) { // Creates a Dog with only a name
this.name = name;
this.age = 0;
}
public Dog(int age) { // Creates a Dog with only an age
this.name = null;
this.age = age;
}
public Dog(String name, int age) { // Creates a Dog with both name and age
this.name = name;
this.age = age;
}
This gives a lot more flexibility to the clients of this class when they want to create a new Dog
. Of course, there is a lot of repetitive code in having these four different constructors, which can be solved by using this
in the second way. When you are in one constructor, you can simplify the code by calling another constructor by using this( /* *ARGUMENTS* */)
, with whatever arguments that constructor requires inside the parentheses. So, similar to calling a method by typing its name, you can call a constructor from another constructor by using this
. In this example, the first three constructors all call the last constructor, which reduces repetition of code.
public Dog() { // Creates a Dog without a name or age
this(null, 0);
}
public Dog(String name) { // Creates a Dog with only a name
this(name, 0);
}
public Dog(int age) { // Creates a Dog with only an age
this(null, age);
}
public Dog(String name, int age) { // Creates a Dog with both name and age
this.name = name;
this.age = age;
}
Now you still might be wondering why all this data is made private instead of public. This is to prevent outside users of the class from having too much access to it. By keeping this data private, you can ensure that no one is messing with the data and you can choose how much authority to give outside clients. Of course, most of the time, you want to give people some access to this data, which is where accessor and mutator methods come in (also referred to as getters and setters).
In the Dog
class, you can create an accessor method to allow people to get the name and age of a Dog
as well as a mutator method to allow people to change the dog’s name or age. For example:
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
In this way, you control how much access others get by changing these getters and setters. For example, you might not want people to be able to change the age and name, so you could simply remove the setter methods and not worry about it anymore. This process of hiding internal data and providing getters and setters is called encapsulation.
If you’re still not convinced of the benefits of encapsulation, take a look at the following example code:
public class Dog {
public String name;
public int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
// Some code located outside the Dog class
public static void main(String[] args) {
Dog d = new Dog("Foo", 7);
d.name = null;
d.age = -10;
}
In the main method, a Dog
is created with the name “Foo” and age 7. However, because the instance variables of Dog
are public, the client was able to access and change the data by typing d.name
and d.age
. Now, the variable d
has an invalid name and age. However, if Dog
had been following the rules of encapsulation by keeping field variables private, this wouldn’t have been a problem, as the only way to modify the data is through mutator methods, which can be changed to only allow valid values.
public void setName(String name) {
// Prevents a null name being set
if (name == null) {
throw new IllegalArgumentException();
}
this.name = name;
}
public void setAge(int age) {
// Prevents a negative age
if (age < 0) {
throw new IllegalArgumentException();
}
this.age = age;
}
Up to this point, we have only talked about instance variables, data that is specific to each instance of the class. However, what if we wanted to keep track of how many total Dogs
have been created? This would no longer be an instance variable but instead a class variable because it’s not specific to any instance of the Dog
class. We can create this by adding the word static in the variable declaration. With static variables, it can often be okay to have them as public, although using getters and setters with a private static variable can work as well.
public class Dog {
private String name;
private int age;
public static int totalDogs = 0;
public Dog(String name, int age) {
this.name = name;
this.age = age;
totalDogs++;
}
}
// Some code located outside the Dog class
public static void main(String[] args) {
Dog d1 = new Dog("Foo", 7);
Dog d2 = new Dog("Bar", 8);
Dog d3 = new Dog("Baz", 3);
System.out.println(Dog.totalDogs); // Will print 3
}
Every time a new Dog
is created, totalDogs
increases by 1, keeping track of how many total Dogs
have been created. To access a static variable, since it belongs to the class itself, simply type the class name followed by the variable name (in this example, Dog.totalDogs
).
The last keyword you need to know is final. If you want to prevent an instance variable from being changed after it is initialized, you can add final
to it:
public class Dog {
private final String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
Now, the compiler will give you an error if anyone tries to change the value of name
, because once it was initialized in the constructor, the value in that variable became finalized.
When used with the static
keyword, final variables essentially become what are known as constant variables that keep track of values that are applicable to all instances of that class and never change. For example, if each Dog
had a method called bark()
that printed out the exact same message each time, like “Bark!”, to the console, a static final variable would generally be used. When naming these static final variables, you use all uppercase letters and underscores between words.
public class Dog {
private final String name;
private int age;
public static final String DOG_NOISE = "Bark!";
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public void bark() {
System.out.println(DOG_NOISE);
}
}
This makes it very easy if you decide later on that you want to change what noise a Dog
makes (Ex: changing “Bark!” to “Woof!”) because all you have to do is edit the final static variable DOG_NOISE
at the top of the class, whereas if you didn’t have this variable, you would have to go through the class and make changes at every place you had the string “Bark!”.
Lesson Quiz
1. What is the difference between static and final?
2. What is the reason for encapsulation?
Written by Alan Bi
Notice any mistakes? Please email us at learn@teamscode.com so that we can fix any inaccuracies.