Edit

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.


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:

Edit

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:

Edit

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.
  • Both bob and jenny are of type Person:
    • 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 type String), and
    • A lastName field (of type String), and
    • An age field (of type int).
  • 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.
  • 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 a Person 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.

The corresponding code snippet is below:


Here’s how the corresponding blueprint (i.e., the Person class) looks:

Edit


Edit Get your hands dirty!

Edit the above code:

  • Create more Person instances in the main() method.
    • Then call the introduceSelf() method on the new instances.
  • Edit the introduceSelf() method inside Person.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 of firstName, lastName, and age fields.
  • 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 the firstName field of a particular Person 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!).




Constructors

We now understand:

  • What a class is,
    • e.g. Person.
  • What instances of that class are,
    • e.g. the data pointed to by the bob and jenny references.
  • What instance fields are,
    • e.g. firstName, lastName, and age.

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).

The constructor is denoted with the Edit below:


Edit

Edit 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.
  • 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 (of 0).
      • The third constructor takes three parameters:
        • All fields get initialised to their respective 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:

Edit

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 the Person 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 the main() method).
  • 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.
  • 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.
  • 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).
  • 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 or public (known as access modifiers).
    • More on access modifiers later.
  • 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 an int.

      • 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:

  1. What inputs it expects (i.e. parameter types and in what order to provide them),
  2. What output it will give us (i.e. return type),
  3. The name of the method, and
  4. 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 class B, we are introducing “coupling”,
      • Should the inner working of class B change, then class A will “break”.
      • However, if the inner working of class B was always hidden (and private to class B only), then there’s no scope for future breakage of class A as it was never accessing the things that got changed.
  • 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?
  • 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.
    • 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.
  • 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 the Person class. Rather, we could just recreate the initials by taking the first letters from each of the firstName and lastName fields.


Edit Get your hands dirty!

Edit the above code:

  • Add a public String getInitials() method for the Person class.
    • It should return the first letter of first name and last name, with full-stops:
    • Test it out by calling it in the main() method, printing the result.
  • 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, calling introduceSelf(), calling haveBirthday(), and observing the output.
  • Define a public boolean isAdult() { ... } method for the Person 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.




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.


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 say lastName.

We can visualise this line of code as follows:

Edit

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:

Edit




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:

Edit

What’s happening here?

  • Two Person instances (bob and jenny) are created in the main() method.
  • We then call bob.meet(jenny);
    • We leave the main() method, and go to the meet() method.
    • meet() is an instance method, invoke in the context of the bob instance. Notice how the bob instance is “paper-clipped” to the blueprint, just like any other instance method call.
    • The meet() method expects a parameter (i.e. Person other). The jenny instance is passed in as the parameter.
    • In effect, we have the following:
      • this inside the meet() method maps to the bob instance declared in main(), and
      • other inside the meet() method maps to the jenny instance declared in main().
      • The exact-same bob and jenny 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 as this or other.
      • In this particular example, both are instances of the Person class. So we can access their fields directly (even if they are declared private).
    • Notice the this “field” on the Person blueprint. We can refer to this 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 the meet() method, we are referring to the entire Person instance that represents Bob.
    • If we say other inside the meet() method, we are referring to the entire Person instance that represents Jenny.
    • If we say this.age inside the meet() method, we get 18.
    • If we say other.age inside the meet() method, we get 20.
    • If we say this.firstName inside the meet() method, we get “Bob”.
    • If we say other.firstName inside the meet() method, we get “Jenny”.
    • If we say this.introduceSelf() inside the meet() method, we are calling the introduceSelf() instance method on the context of the instance representing Bob.
    • If we say other.introduceSelf() inside the meet() method, we are calling the introduceSelf() 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 and jenny while inside the meet() method! Those variable names were declared in the main() method, and so are not in range of the meet() method (later we will talk about “scope”). This is why we have to use this and other, as that’s what they are referred to from the context of the meet() method (because, remember, the logic inside the Person class was written in a generalised manner—without knowing the specifics of what instances would be created).




Encapsulation

Edit    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.

Edit


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 to public).
    • 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” the Car class, so it cannot access what’s private to the Car:
          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();     
              }
          }
        




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:

    Edit

    • Each instance of a Person will have its own values for the firstName, lastName, and age fields.
    • Different instances of the Person class can potentially have different values for these fields.

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 the getAge() instance method on the bob instance:
        public static void main(String[] args) {
            Person bob = ...
            int age = bob.getAge();    // 
        }
      
    • From inside the Person class, we explicitly call the getAge() instance method on this instance:
        public void introduceSelf() {
            int myAge = this.getAge();
            ...
        }
      
  • At other times, we implicitly call an instance method, on which case it operates on this instance:
      public void introduceSelf() {
          int myAge = getAge();
          ...
      }
    

    Edit



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:

Edit

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:

Edit

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, and age) 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:

Edit

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:

Edit

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:

Edit

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)?
  • getOldestPerson():
    • Who is the oldest Person?
  • getOldestAge():
    • What is the age of the oldest Person?

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:

Edit

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 and static 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 a static method to access any of the instance fields or instance methods.
      • This also means we cannot use the this keyword inside a static method, because we aren’t inside the context of an instance!
      • While inside a static method, we can only access other static methods and static fields.
    • 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 the static fields. It can also call the static methods if it needs to.
      • There’s nothing wrong if a particular Person instance wants to know who is the oldestPerson, or see what the population is by calling getPopulation().

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!


Edit Get your hands dirty!

Edit the above code:

  • This will require us to refactor the Person class, by changing the static fields and static methods in it.
  • Delete the oldestAge field. There will be some compilation errors.
  • Change the oldestPerson field such that it is of a Person type instead of String 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 that oldestPerson is now of a Person 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 the oldestPerson is not null. There might be more than one situation you need to fix this.
  • You might also need to rewrite the the printStatus() method in the Main class. It’s printing some weird numbers for the Person.getOldestPerson()—this is the reference value for whoever happens to be the oldest Person 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 the Person class. You can tell it by default that the String representataion of a Person 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.