Classes Core Concepts
Fear not…
There are definitely quite a few concepts at play here.
Don’t feel like this all needs to make sense immediately! It definitely takes time!
The best way to learn is to watch the recommended videos, and do the activities in lectures alongside us.
There is a reason we have semi-flipped lectures for this course:
- The hands-on experience is what helps, and we are giving this opportunity.
- It will take some time, but keep pushing yourself!
- Don’t be afraid to muck around with the code snippets, this is important!
- Struggling is part of learning!
- You’re going to be hooked once it clicks!
Make the most of the help clinics and practical in-lecture activities.
First of all, what happens if we don’t have classes?
What will our code look like?
The example Repl below shows what is meant to be rather simplistic:
- First, we are representing two people in our program:
- Bob Jones is 18
- Jenny Jones is 20
- Second, we are printing a message depending on whether they have the same surname or not.
- Note: we are using
.equals()
instead of==
to compare two strings! More on that later.
- Note: we are using
How do we visualise this code?
When the program runs, all data it uses needs to be somewhere in memory.
We can visualise the data stored in memory as follows:
Note: This is a little bit of an oversimplification:
- The
String
data type is actually a reference type, rather than a primitive type. - We will discuss the differences of primitive vs reference types later.
- But of all the reference types, the
String
is quite forgiving for us to imagine it as if it’s a primitive type.
So, what’s the problem?
- In our program, we want to represent the idea of people.
- We decide (as part of our design) that every person should have the following attributes:
- A first name,
- A last name, and
- An age.
- For every person we wish to represent (e.g., Bob and Jenny), they need to have their own copy of each of the above attributes.
- Since there isn’t a readily available data type that conveniently represents a person, we would need to use a large number of individual variables.
- We end up with six variables in memory (two people, each with three attributes)!
- But conceptually, we only want to represent two things (Bob and Jenny).
- There is no relationship between these six variables, despite three of them conceptually representing one person and the other three conceptually representing another person.
- Ideally, we want just two variables:
- One to represent Bob, and
- Another to represent Jenny.
- The data type of each of these variables should be of a
Person
type.
Custom data types (i.e., classes) to the rescue!
The concept of classes allows us to define our own custom data types.
This is the fundamental concept underpinning OOP!
Using Java, OOP allows us to represent Bob and Jenny as follows:
Conceptually, this makes much more sense:
- We have a single top-level (custom) data type, i.e.
Person
. - We can create instances of this
Person
data type.- Each instance will represent one person.
- We want to represent two people, so we make two
Person
instances:- The
bob
variable name refers to the instance representing Bob, and - The
jenny
variable name refers to the instance representing Jenny.
- The
- Both
bob
andjenny
are of typePerson
:- They are two instances of the same type.
- That type will be defined by a class (i.e., a blueprint).
- Therefore, classes will allow us to define our own custom data types.
- Each instance of the
Person
class will have:- A
firstName
field (of typeString
), and - A
lastName
field (of typeString
), and - An
age
field (of typeint
).
- A
- Take note of the different levels of abstraction:
- At the top level, we have the
Person
abstraction.- This is absolutely liberating!
- When we want to think in terms of Jenny, we don’t have to think of “the pieces” she’s made up of, i.e.:
- We don’t think of: “Jenny first name, Jenny last name, Jenny age”.
- We just think of: “Jenny”!
- Going a level lower:
- Should we care about the inner details of a particular person (i.e., the fields of a particular instance), no worries.
- We can easily retrieve that information.
- At the top level, we have the
- Notice how there are two different kinds of memory:
- The actual
Person
instances are stored on the heap, while - Only references to the instances are stored on the stack.
- i.e.
bob
is actually a reference to aPerson
instance.bob
is not an instance per se. The actual instance is located in longer-term heap memory. - Similar thing with
jenny
. - The placement of the instance in the heap is the work of the
new
keyword.
- i.e.
- The actual
The corresponding code snippet is below:
Here’s how the corresponding blueprint (i.e., the Person
class) looks:
Get your hands dirty!
Edit the above code:
- Create more
Person
instances in themain()
method.- Then call the
introduceSelf()
method on the new instances.
- Then call the
- Edit the
introduceSelf()
method insidePerson.java
to say something differently.
Instance fields (also called instance variables)
The example above demonstrated the following:
- Custom data types are composed from fields:
- i.e. the
Person
type is composed offirstName
,lastName
, andage
fields.
- i.e. the
- Each instance of that data type will have its own copy for those fields.
- In other words, they are instance fields.
- An instance field is a field (variable) whose value belongs to a particular instance.
- i.e.
"Nasser"
is the value of thefirstName
field of a particularPerson
instance representing Nasser. - A different
Person
instance will have their own corresponding field, with the potential of having a different value (but doesn’t have to be different—there’s nothing stopping two people having the same values for their first name!).
- i.e.
Constructors
We now understand:
- What a class is,
- e.g.
Person
.
- e.g.
- What instances of that class are,
- e.g. the data pointed to by the
bob
andjenny
references.
- e.g. the data pointed to by the
- What instance fields are,
- e.g.
firstName
,lastName
, andage
.
- e.g.
The magical thing that connects everything together, is the constructor!
The constructor of the class is invoked using the new
keyword:
- This creates an instance of the class,
- In the illustration below, we can see a “blank” instance on the left being created.
- In Java, the instance will always be stored on the heap,
- The statements inside the constructor are then executed:
- In most cases, this is usually to serve the purpose of initialising the fields by copying them over from the constructor parameters.
- If the fields of the instance are not explicitly initialised, they get default values. However, it’s better practice to explicitly initialise all the fields rather than rely on default values!
- The memory address (i.e. reference) of where the instance is place is returned by the constructor call.
- This allows us to save the reference into a variable (e.g.
bob
).
- This allows us to save the reference into a variable (e.g.
The constructor is denoted with the below:
Caution!
Some video explanations on the web are misleading.
For example, do not follow this explanation!
- The fields and methods should not be
static
. - You’ll find out why later.
The following is a good video on constructors.
For the purposes of this course, the fields in the Book
class should be explicitly private
.
Initialising instance fields
In the examples above, we saw that one of the important roles of the constructor is to initialise the fields as the instance is being constructed.
- If the class does not explicitly provide a constructor, one will be provided by the compiler:
- This is known as the default constructor, and
- It has no parameters, and
- It has no statements its body.
- Therefore, this will leave all fields uninitialised.
- It is good practice to provide at least one explicit constructor.
- How many you provide depends on what makes sense (more on this later), and it is pretty much a design decision.
- When you explicitly provide at least one constructor:
- This enables you to initialise the fields in a meaningful manner for the instance being created,
- This alone might help you avoid future bugs in your code!
- The default constructor will no longer be automatically added in by the compiler.
- If you add your own zero-parameter constructor, it’s not strictly speaking the default constructor. It’s just a constructor with no parameters.
- This enables you to initialise the fields in a meaningful manner for the instance being created,
- You can add as many constructors as you want:
- Each constructor must have a unique permutation of parameter types (so that the compiler doesn’t get confused).
- You can use any of the constructors to create an instance of that type.
The following example includes three different classes:
Colour1
- Doesn’t provide any explicit constructor, so Java provides a default constructor.
- All the fields get initialised to 255 in their respective declaration statements.
Colour2
- Provides exactly one constructor, with no parameters.
- All the fields get initialised to 5 in this constructor.
- The default constructor is not created by the Java compiler, as a constructor is explicitly defined.
Colour3
- Two fields are initialised to 100 in their declarations, while the third remains uninitialised in the declaration.
- There are three different constructors:
- The first constructor takes zero parameters:
- It initialises all three fields to 255.
- The second constructor takes one parameter:
- The
red
field is initialised to the parameter value, - The
green
field is already initialised in the declaration (to 100), - The
blue
field is not explicitly initialised, but gets a default value (of0
).
- The
- The third constructor takes three parameters:
- All fields get initialised to their respective parameters.
- The first constructor takes zero parameters:
- The default constructor is not created by the Java compiler.
Instance methods
Imagine the following bit of code:
bob.introduceSelf();
This is known as invoking an instance method (in this case introduceSelf()
) on an instance (in this case, that referred to by the bob
reference variable).
We can visualise it like this:
The key points are:
- In order to invoke an instance method, we must provide an instance to be used as the context for that instance method to execute.
bob
is a variable that stores a reference to thePerson
instance stored in the heap.- i.e.
0x810
happens to be the memory location where the instance is stored. - But the
bob
variable itself is in the stack (since the variable is declared inside themain()
method).
- i.e.
- Our
Person
class is a blueprint:- It doesn’t contain any values for the instance fields. It only templates that
Person
instances will have those fields. - We can think of the actual code/logic for the method(s) and constructor(s) as also being on this blueprint.
- It doesn’t contain any values for the instance fields. It only templates that
- The instances themselves can be thought of as a piece of paper:
- Each instance is its own piece of paper.
- The piece of paper stores the values of the instance fields for the specific instance.
- When we want to invoke an instance method on an instance, we can imagine it being “attached” to the blueprint of the class type it belongs to.
- In this case, the instance is a
Person
type. - The instance is then “attached” to the
Person
blueprint (class). - The
introduceSelf()
method is then executed on the context of this attached instance.
- In this case, the instance is a
- Notice that the instance already exists:
- We know that the instance was created at some stage, after the constructor was invoked.
- Any time we call the constructor, an instance gets created.
- We only need to invoke one of the constructors, and once, in order to make an instance.
- So the constructor is really only ever needed for the construction of the instance.
- This means, we do not really want to “go back into” the constructor again (in the context of that instance). This doesn’t really make sense.
- However, of course it’s still allowed for an instance method to call the constructor. This will just create another instance (so it’s not like it’s going into the constructor with the context of the instance that called the constructor).
- We know that the instance was created at some stage, after the constructor was invoked.
- When invoking an instance method on an instance, it can access all the fields declared in the blueprint
- This is regardless of whether the fields are declared as
private
orpublic
(known as access modifiers). - More on access modifiers later.
- This is regardless of whether the fields are declared as
- Instance methods are similar to functions as you know them.
- In Java, we tend to say “methods” (rather than “functions”) – especially in the context of it operating on instances.
- These methods can take parameter(s), in similar fashion to constructors:
-
The following method does not take any parameters:
public void introduce() { ... }
-
The following method takes exactly one
String
as parameter:public void greet(Sting name) { ... }
-
- These methods can also
return
a value:-
The type will be specified in the method’s signature, just before the method name:
public int pickNumber() { ... }
must return anint
. -
If it doesn’t return any value, it will have
void
for the return type:public void introduce() { ... }
doesn’t return anything.
-
Method signatures
When calling a method (whether we wrote the method ourselves, or someone else), we need to understand how to use it.
We can say that each method has a protocol that defines how to use it, sort of like a contract, that we need to obey to correctly use it.
To know how to use a method, we need to know:
- What inputs it expects (i.e. parameter types and in what order to provide them),
- What output it will give us (i.e. return type),
- The name of the method, and
- Whether it is an instance method or a
static
method (more on this later).
Getter and setter methods
- Although we didn’t dwell on it too much so far, you may have noticed our insistence on declaring fields as
private
.- This promotes an important pillar of OOP, that of encapsulation.
- This essentially hides details of the class.
- In most cases, only the class itself needs to know its “inner workings”.
- We don’t like one class (e.g.
A
) knowing the inner workings of another class (e.g.B
):- If class
A
can access the inner logic of classB
, we are introducing “coupling”, - Should the inner working of class
B
change, then classA
will “break”. - However, if the inner working of class
B
was always hidden (andprivate
to classB
only), then there’s no scope for future breakage of classA
as it was never accessing the things that got changed.
- If class
- So, this begs an important question:
- How can another class access important information (such as the first name of a particular person) without having direct access to the (e.g.
firstName
) field itself?
- How can another class access important information (such as the first name of a particular person) without having direct access to the (e.g.
- Fortunately, we can provide setters and/or getters as needed:
- A “getter” is an instance method that allows us to read (i.e. “get”) a value from an instance.
- e.g.
getFirstName()
will tell us what the person instance’s first name value is.
- e.g.
- A “setter” is an instance method that allows us to write/update (i.e. “set”) a value for a particular instance.
- e.g.
setFirstName()
will allow us to overwrite the first name to something else.
- e.g.
- A “getter” is an instance method that allows us to read (i.e. “get”) a value from an instance.
- Note: It’s not necessarily that getters and setters have to map to particular fields. The getters and setters are provided more for the conceptual design aspects of what we want to represent and be possible to action on instances of that class. For example, we could easily have a
getInitials()
getter method, even if there is no field called “initials
” inside thePerson
class. Rather, we could just recreate the initials by taking the first letters from each of thefirstName
andlastName
fields.
Get your hands dirty!
Edit the above code:
- Add a
public String getInitials()
method for thePerson
class.- It should return the first letter of first name and last name, with full-stops:
- e.g.
jenny.getInitials()
would return"J. J."
. - Hint: You might find the Java API documentation for
String
useful.
- e.g.
- Test it out by calling it in the
main()
method, printing the result.
- It should return the first letter of first name and last name, with full-stops:
- While keeping the current
Person
constructor, define another:- It takes only two
String
parameters: one for the first name, and one for the last name. - Remember, it should still initialise the
age
field to follow good programming practices.- What would be a good default age when one isn’t specified?
- Test it out by using it in the
main()
method by creating a new instance, callingintroduceSelf()
, callinghaveBirthday()
, and observing the output.
- It takes only two
- Define a
public boolean isAdult() { ... }
method for thePerson
class.- It returns
true
if the instance represents someone at least 18 years old. - Test it out by using it in the
main()
method with instances that are over, under and equal to 18.
- It returns
What is this
?
- We can think of
this
as simply being a “pronoun” that allows us to explicitly refer to the instance that we are in the context of. - It is therefore a special keyword that may only be used inside:
- Constructors, or
- Instance methods.
- It works exactly the same in either a constructor or an instance method. It will always be contextualised to an instance of the class type.
- In the case of being in the constructor,
this
refers to the instance that is “in the process of being instantiated”. - In the case of an ordinary instance method (including getters and setters),
this
refers to an already-existing instance.
- In the case of being in the constructor,
Example inside a constructor
Imagine if our constructor looked like this:
public Person(String firstName, String lastName, int age) {
...
this.lastName = lastName;
...
}
If you were to write “lastName
”, how will the compiler know if you mean the parameter versus the field? Both have the same name!
- If you mean to refer to the field, you need to explicitly state this by saying
this.lastName
. - If you mean to refer to a local variable (such as the parameter), you must omit
this
and just saylastName
.
We can visualise this line of code as follows:
In this regard, using the “this
” keyword is mandatory, since there is a local variable (i.e. the parameter with the same name) that shadows (i.e. hides) the instance field with the same name.
Example inside a method
The exact same logic applies inside ordinary instance methods.
Imagine the following instance method:
public void introduceSelf() {
...
System.out.println(lastName);
...
System.out.println(this.lastName);
...
}
Since there is no local variable (or parameter) inside the introduceSelf()
method with the same name as the lastName
field, there will be no confusion as to what lastName
refers to. In this case, using “this
” is completely optional.
We can visualise this code as follows:
Passing instances as parameters to instance methods
Consider the following example, where an instance is passed as a method parameter:
public class Person {
...
public void meet(Person other) {
System.out.println(this.firstName + ", meet " + other.firstName);
this.introduceSelf();
other.introduceSelf();
}
}
We would then call this method as follows:
public static void main(String[] args) {
Person bob = new Person("Bob", "Jones", 18);
Person jenny = new Person("Jenny", "Jones", 20);
bob.meet(jenny);
}
The runnable code snippet of the above example is shown here:
We can visualise this code as follows:
What’s happening here?
- Two
Person
instances (bob
andjenny
) are created in themain()
method. - We then call
bob.meet(jenny);
- We leave the
main()
method, and go to themeet()
method. meet()
is an instance method, invoke in the context of thebob
instance. Notice how thebob
instance is “paper-clipped” to the blueprint, just like any other instance method call.- The
meet()
method expects a parameter (i.e.Person other
). Thejenny
instance is passed in as the parameter. - In effect, we have the following:
this
inside themeet()
method maps to thebob
instance declared inmain()
, andother
inside themeet()
method maps to thejenny
instance declared inmain()
.- The exact-same
bob
andjenny
instances exist – they are not copied or anything like that! - So inside the
meet()
method, you just need to be aware of which instance do you want to work with. You have to refer to them asthis
orother
. - In this particular example, both are instances of the
Person
class. So we can access their fields directly (even if they are declaredprivate
).
- Notice the
this
“field” on thePerson
blueprint. We can refer tothis
from any instance method or constructor, and it always refers to the “paper-clipped” instance (i.e. the instance we are “in the context of” when invoking an instance method or constructor). - If we say
this
inside themeet()
method, we are referring to the entirePerson
instance that represents Bob. - If we say
other
inside themeet()
method, we are referring to the entirePerson
instance that represents Jenny. - If we say
this.age
inside themeet()
method, we get 18. - If we say
other.age
inside themeet()
method, we get 20. - If we say
this.firstName
inside themeet()
method, we get “Bob”. - If we say
other.firstName
inside themeet()
method, we get “Jenny”. - If we say
this.introduceSelf()
inside themeet()
method, we are calling theintroduceSelf()
instance method on the context of the instance representing Bob. - If we say
other.introduceSelf()
inside themeet()
method, we are calling theintroduceSelf()
instance method on the context of the instance representing Jenny.
Important: Although we are operating on the instances representing Bob and Jenny, we cannot refer to the variable names
bob
andjenny
while inside themeet()
method! Those variable names were declared in themain()
method, and so are not in range of themeet()
method (later we will talk about “scope”). This is why we have to usethis
andother
, as that’s what they are referred to from the context of themeet()
method (because, remember, the logic inside thePerson
class was written in a generalised manner—without knowing the specifics of what instances would be created). - We leave the
Encapsulation
True story
A few years ago, I needed a new desktop system. I shopped around to see what systems were available, but the pre-built systems were either too expensive or didn’t have the specs I wanted.
So, I decided to build my own system up by buying the individual components separately and constructing my dream system.
Being a novice hardware guy without any training, of course I relied on YouTube.
Believe it or not, the computer worked! (for a few months). But it eventually had problems.
I returned it to the shop, and the technician opens it up and sees all these messy wires inside the computer. With a “what an amateur” look on his face, he smirks “I can see you built it yourself”.
Moral of the story?
Don’t abuse encapsulation, by using it to cover up your messy internal code.
When writing code, always keep it nice and tidy, and easy to figure out what’s happening.
If someone ever needs to look at your code (and they will!), don’t let them hate (or laugh at) you.
In hardware, we often buy components and plug them into our system. To support this, the components need to conform to a common interface so they can communicate between each other.
When you plug a new component in, you don’t need to know its internal details. You know what it can do, but not how it does it.
This is encapsulation: it hides implementation details.
We can apply the same principle in software development:
- When you reuse other libraries (like printing to the console), you don’t know (or even want to care) how it does it. Just as long as it does it, we’re happy.
- When you write your own code, you often want to construct separate components for the different aspects of your code. It makes it easier for you (and others) to know where to look when something needs fixing: you go straight to the component you suspect.
In OOP, there are many ways we see encapsulation in action. The most common ways are:
- Hiding internal details: When it comes to methods, they naturally hide the internal details of how they operate:
- All the logic written inside a method, is only visible to that method.
- All the local variables (and parameters) declared inside that method, are only accessible to that method.
- This is very helpful for us as programmers, as it allows us to “trust” that a method will “do its thing” without worrying about details.
- A method signature is precisely this:
- It explains the “protocols of using” a given method, without us needing to worry about the details.
- In addition to this benefit, composing code into smaller methods allows us to reuse logic without repeating ourselves!
- This not only saves us time, but it reduces the chance of introducing new errors.
- Look at the following code. Even without knowing what’s happening inside the methods, you can probably figure out what should be happening:
Person jordan = new Person("Michael Jordan", 23); Person nasser = new Person("Nasser", 24); jordan.sayHello(); // "Hi, my name is Michael Jordan. My favourite number is 23" nasser.sayHello(); // "Hi, my name is Nasser. My favourite number is 24" nasser.changeMind(); // only operates on the nasser instance jordan.sayHello(); // "Hi, my name is Michael Jordan. My favourite number is 23" nasser.sayHello(); // "Hi, my name is Nasser. My favourite number is 83"
“I don’t care how it works, I just want to know what it does.”
- Hiding the components entirely: If hiding internal details of methods is so useful, what about completely hiding the existence of some methods? Don’t even let “outsiders” know that these methods even exist! In fact, while we’re at it, why not even hide fields, and even entire classes altogether?
- We achieve this using a special modifier: the
private
keyword (as opposed topublic
). - Later on, we will learn about the
protected
keyword also. - By declaring a field or method as
private
, this means it cannot be accessed outside the class. - This is very useful:
- It encourages us to write cleaner code by defining more methods, but keeping them “hidden” outside the class. This makes our class appear cleaner in that we aren’t exposing more fields and methods than is necessary. The less code that can be accessed outside the class, the less that is needed to know how to use the class.
“These methods and fields are needed for this class, but no one else needs to trouble themselves wondering what they do. I’ll just hide them.”
- It allows us to protect the class, by ensuring no one misuses the class or directly modifies fields in a manner that’s not correct.
“I don’t want anyone messing with how my class works. I’ll completely hide the bits what I don’t want them to use, to restrict what they can do with my class.”
- Here’s an example of how we might implement a
Car
:public class Car { private void activateStarterMotor() { ... } private void turnCrankShaft() { ... } private void fireSparkPlugs() { ... } private ArrayList<Fuse> fuses; public Car() { ... } public void turnKeyOn() { activateStarterMotor(); // OK, within Car turnCrankShaft(); // OK, within Car fireSparkPlugs(); // OK, within Car } }
Main
is “outside” theCar
class, so it cannot access what’sprivate
to theCar
:public class Main { public static void main(String[] args) { Car car = new Car(); // This is OK, as it is functionality exposed to be used publicly: car.turnKeyOn(); // Error: this is private functionality, intended only for the Car class to use: car.fireSparkPlugs(); } }
- We achieve this using a special modifier: the
Instance versus static
: fields and methods
We have seen what instance fields and instance methods are, so let’s quickly recap them before we introduce static
fields and static
methods.
Instance fields
- An instance field is a variable that belongs to an instance of the class it’s declared in.
- Each and every instance of that class will have its own value for that field.
-
We see this with the
Person
example:- Each instance of a
Person
will have its own values for thefirstName
,lastName
, andage
fields. - Different instances of the
Person
class can potentially have different values for these fields.
- Each instance of a
Instance methods
- An instance method is a method that operates in the context of a particular instance.
- Such methods cannot be used, unless you invoke it on an instance.
- We sometimes explicitly specify the instance to invoke on:
- Outside of the
Person
class, we explicitly call thegetAge()
instance method on thebob
instance:public static void main(String[] args) { Person bob = ... int age = bob.getAge(); // }
- From inside the
Person
class, we explicitly call thegetAge()
instance method onthis
instance:public void introduceSelf() { int myAge = this.getAge(); ... }
- Outside of the
- At other times, we implicitly call an instance method, on which case it operates on
this
instance:public void introduceSelf() { int myAge = getAge(); ... }
static
fields
- A
static
field (sometimes also called “class field”) is a variable that belongs to the class it is declared in. - There will only ever be one value for that static field, and it gets shared with all instances of that class.
- Unlike instance fields,
static
fields can therefore exist even without an instance of that class! static
fields are useful when we need to have a single “shared” value for a field regardless of instances.
Here’s an example where we might want to use a static
field…
Imagine for our Person
example, we would like to also have an automatically-generated id
as an instance field. Namely, when an instance of a Person
is created, that instance will be assigned a unique identifier without us needing to provide it as a parameter input.
We want our instances to end up looking something like this:
Here, we see that each instance has a unique id
instance field.
Below is the code that will help us achieve this:
Of particular interest, is the Person
class where we have:
public class Person {
private int id;
private static int nextID = 0;
...
}
We see that id
is just an ordinary instance field, just like age
.
But we also have this nextID
field, declared as being static
.
In effect, this is how we can visualise our class:
You’ll notice that the nextID
field is not in the instance field blueprint section, and is instead an actual variable in a different section of the Person
class. It’s an actual real int
variable, and we have immediately initialised it to zero! It exists, even before any Person
instances are created!
Before we create an instance, let’s first look at the constructor:
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.id = nextID;
nextID++;
}
The first few lines we have seen many times:
- We are initialising the instance fields (
firstName
,lastName
, andage
) to the corresponding values passed in as parameters to the constructor.
The next line is actually the exact-same logic:
- We are initialising an instance field (
id
). - The only difference is that it’s not being assigned a value that was passed in as a parameter.
- Instead, it is being assigned a value from the
static nextID
field.
Finally, we have the final line that increments the value of nextID
.
Now, let’s create our first instance by calling the constructor:
In creating the Jenny instance above, the constructor reads the current value of nextID
(currently 0
) and assigns it to the instance’s id
field. So Jenny’s id
is 0
. We then increment nextID
, and so it becomes 1
.
Next, we create another instance by again calling the constructor:
In creating the Bob instance above, the constructor reads the current value of nextID
(currently 1
) and assigns it to the instance’s id
field. So Bob’s id
is 1
. We then increment nextID
, and so it becomes 2
.
This is what our Person
class blueprint looks like after having created two instances:
We have seen how the state of the nextID
field persisted across all instances. This is what we mean when we say that static
fields “belong” to the class, as opposed to any instance.
static
methods
- A
static
method (sometimes also called “class method”) is a method that *belongs to the class it’s declared in. - Unlike instance methods,
static
methods do not need an instance of that class for them to be invoked. static
methods are very useful when it doesn’t make sense for a method to operate on the context of any particular instance.
Here’s an example where we might want to use a static
method…
Imagine for our Person
example, we would like to have the following methods:
getPopulation()
:- What is the population (i.e. how many
Person
instances have been created)?
- What is the population (i.e. how many
getOldestPerson()
:- Who is the oldest
Person
?
- Who is the oldest
getOldestAge()
:- What is the age of the oldest
Person
?
- What is the age of the oldest
In deciding if it a method should be static
or not, ask yourself a simple question:
- Should calling the method depend on a particular instance?
If the answer is “yes”, then it sounds like it’s an instance method.
If the answer is “no”, then it sounds like it’s a static
method.
The three methods above therefore make sense to be static
. We shouldn’t need to call any of those methods on particular instances for them to make sense.
Here’s an example making use of static
methods:
And here’s how we can visualise key parts of this code:
Of particular interest, is the Person
class where we have:
- On the left-side of the class, we have instance fields and instance methods as usual:
- This part of the class must operate in the context of a particular instance.
- On the right-side of the class, we have the
static
fields andstatic
methods:- This part of the class can operate happily without the context of any instance.
- These fields and methods belong to the class itself, not the instances.
- Since the
static
methods belong to the class and don’t need instances for them to be used, this means it’s not allowed for astatic
method to access any of the instance fields or instance methods.- This also means we cannot use the
this
keyword inside astatic
method, because we aren’t inside the context of an instance! - While inside a
static
method, we can only access otherstatic
methods andstatic
fields.
- This also means we cannot use the
- However, we are more than welcome to use the
static
fields and methods while we are inside an instance method:- An example is the
haveBirthday()
instance method accessing thestatic
fields. It can also call thestatic
methods if it needs to. - There’s nothing wrong if a particular
Person
instance wants to know who is theoldestPerson
, or see what the population is by callinggetPopulation()
.
- An example is the
Notice also the syntax of invoking a static
method: we use the name of the class:
int population = Person.getPopulation();
String oldest = Person.getOldestPerson();
...
You might have also noticed we have two static
methods inside the Main
class!
Get your hands dirty!
Edit the above code:
- This will require us to refactor the
Person
class, by changing thestatic
fields andstatic
methods in it. - Delete the
oldestAge
field. There will be some compilation errors. - Change the
oldestPerson
field such that it is of aPerson
type instead ofString
type. There will be some more compilation errors. - See what we are trying to do? Instead of having two separate fields to know who’s the oldest person, why not just have a single field (of type
Person
), and get all the same information we need (their name and age) from that instead! - Fix all the compilation errors you get. This will include changing the return type of the
static
methods, and where you used to check for age, etc, these need to be updated to take into account thatoldestPerson
is now of aPerson
type. - Once you fixed the compilation errors, run the code. Opps… Now we have runtime errors!
- Follow the trace for the
NullPointerException
that gets shown. Which line of the code is causing this problem? Fix it by adding the necessary checks, to make sure that theoldestPerson
is notnull
. There might be more than one situation you need to fix this. - You might also need to rewrite the the
printStatus()
method in theMain
class. It’s printing some weird numbers for thePerson.getOldestPerson()
—this is the reference value for whoever happens to be the oldestPerson
instance. Change this so that it actually prints out their full name. - Opps… another
NullPointerException
? Why is that happening? There’s no oldest person as there’s no one in the population! Add a safety check, and give a meaningful message when that happens. - By the way, another thing you can do, is override the
toString()
method for thePerson
class. You can tell it by default that theString
representataion of aPerson
instance (such as when you print it out) will default to giving you their name, or whatever you want, and in whatever format you want.