| Home | Resources | Papers | Essays | Short Fiction | About |

Instance Fields & Methods

Table of Contents

Introduction

Recall that objects in Java have both characteristics and behaviors. Within the Java language we can represent characterstics through the use of fields, or variables, and behaviors through the use of methods.

For the following chapter, I take for granted your familiarity with functions in the computer science sense of the term. If you are unfamiliar with functions then I suggest you check out this chapter written for a C class which goes over the basics.

What is an Instance?

An instance refers to the in-memory representation of an object. Take the following class definition:

 1:   public class BankAccount {
 2:     String owner;
 3:     double balance;
 4: 
 5:     BankAccount(String owner, double initialBalance){
 6:         this.owner   = owner;
 7:         this.balance = initialBalance;
 8:     }
 9: 
10:     void deposit(double amount){
11:         balance = balance + amount;
12:     }
13: 
14:     void withdraw(double amount){
15:         balance = balance - amount;
16:     }
17: 
18:     void printBalance(){
19:         System.out.printf("%s's balance: $%.2f%n", owner, balance);
20:     }
21: 
22: }

The this keyword makes explicit the reference to the instance which is being used for the method invocation.

The BankAccount class has two instance fields (owner and balance) and three instance methods (deposit, withdraw, and printBalance). We can create two instances of the class with the following:

1: BankAccount alice = new BankAccount("Alice", 1000.00);
2: BankAccount bob   = new BankAccount("Bob",   500.00);

The variables alice and bob store two distinct instances of the BankAccount class in the form of in-memory objects. We can represent these objects pictorially like so:

                   BankAccount
         (class definition / blueprint)
┌─────────────────────────────────────────────────┐
│  fields:                                        │
│    String owner                                 │
│    double balance                               │
│                                                 │
│  methods:                                       │
│    deposit(double amount)      ◄────────────┐   │
│    withdraw(double amount)     ◄──────────┐ │   │
│    printBalance()              ◄────────┐ │ │   │
└─────────────────────────────────────────┼─┼─┼───┘
               /              \           │ │ │
         new BankAccount()   new BankAccount()│
             /                  \         │ │ │
┌─────────────────────┐   ┌─────────────────────┐
│  BankAccount object │   │  BankAccount object │
│  ─────────────────  │   │  ─────────────────  │
│  owner:   "Alice"   │   │  owner:   "Bob"     │
│  balance: 1000.00   │   │  balance: 500.00    │
│                     │   │                     │
│  deposit()    ──────┼───┼─────────────────────┼─┘
│  withdraw()   ──────┼───┼─────────────────────┼─┘
│  printBalance()─────┼───┼─────────────────────┼─┘
└─────────────────────┘   └─────────────────────┘
        alice                       bob

We can see that there are two BankAccount objects with their own, distinct internal fields. We can also see, however, that the methods defined in the class are all shared by the instances. This is because alice and bob are both instances of the BankAccount class and as such they have the same characteristics (i.e. fields) and the same behaviors (i.e. methods). Characteristics may change from object to object, but behaviors do not (a bank account may have such and such amount, but it should always be able to take in new deposits).

Instance Fields

Fields are the variables tied to a particular class. Instance fields are specifically the fields which are tied to instances of an object, not shared between the entire class type. Unless otherwise specified (with the static keyword), all fields are instance fields. In Listing 1 there are two instance fields owner and balance. Just like how we defined local variables in the past, instance fields have types attached to them. owner is a String, itself an object type, and balance is a primitive double. These fields can be freely referenced within the class. Let's take a look at some code which makes use of the BankAccount class:

1: public class BankUser{
2:    public static void main(String[] args){
3:         BankAccount acc = new BankAccount("Alice", 1000.00);
4:         System.out.printf("acc's owner: %s\n", acc.owner); // Using . to reference instance field
5:     }
6: }

I can compile and run this code to get the following:

1: josephraskind@stargazer:/tmp/instance$ ls
2: BankAccount.java  BankUser.java
3: josephraskind@stargazer:/tmp/instance$ javac BankUser.java; java BankUser
4:   acc's owner: Alice

This only compiles because I have both .java files in the same directory.

How did this work? BankAccount defined an instance field, owner, which was referenced by BankUser in its main method on Line 4. We can access instance fields and methods using the . notation. acc.owner tells the JVM to find the object tied to the acc variable and return the value of the field called owner. Let's see what happens when I have two variables:

1: public class BankUser{
2:    public static void main(String[] args){
3:         BankAccount acc_1 = new BankAccount("Alice", 1000.00);
4:         BankAccount acc_2 = new BankAccount("Bob", 500.00);
5:         System.out.printf("acc_1's owner: %s\n", acc_1.owner);
6:         System.out.printf("acc_2's owner: %s\n", acc_2.owner);
7:     }
8: }

After compiling and running:

1: josephraskind@stargazer:/tmp/instance$ javac BankUser.java; java BankUser
2: acc_1's owner: Alice
3: acc_2's owner: Bob

How are there two different values being printed for the calls to .owner? Again, this has to do with the fact that there are two different instances of BankAccount. acc_1 is tied to Alice and acc_2 is tied to Bob. acc_1 and acc_2 are both BankAccount objects, but they are not the same BankAccount object.

Instance Methods

Classes define behaviors, alongside characteristics. In Java those behaviors are referred to as methods1. Let's update our BankUser to include a call to, or invocation of, the printBalance method:

1: public class BankUser{
2:    public static void main(String[] args){
3:         BankAccount acc_1 = new BankAccount("Alice", 1000.00);
4:         BankAccount acc_2 = new BankAccount("Bob", 500.00);
5:         acc_1.printBalance();
6:         acc_2.printBalance();
7:     }
8: }

After compiling and running:

josephraskind@stargazer:/tmp/instance$ javac BankUser.java; java BankUser
Alice's balance: $1000.00
Bob's balance: $500.00

We've already established that each instance has its own internal values associtated with the fields tied to the class definition, so it makes sense that we see Alice and 1000. But why do those calls to printBalance work? They work because acc_1 is an instance of a BankAccount object and by the class definition it has the printBalance method bound to it. For example, if I wrote acc_1.printBALance instead, we would encounter a compilation error as no method with that name is guaranteed to exist for the BankAccount type.

But how does printBalance "know" the values of owner and balance? Both owner and balance are tied to the instance of a BankAccount class, so by definition there will be an owner and a balance field for each instance. When the printBalance method is invoked it is invoked with context—the context of which instance it was invoked from. Line 5 invokes printBalance with acc_1 which we know contains a BankAccount object whose fields contain the values Alice and 1000; likewise, Line 6 invokes printBalance with acc_2 which we know contains a BankAccount object whose fields contain the values Bob and 500.

Let's see what happens when we call deposit on acc_1, but not on acc_2:

 1: public class BankUser{
 2:    public static void main(String[] args){
 3:         BankAccount acc_1 = new BankAccount("Alice", 1000.00);
 4:         BankAccount acc_2 = new BankAccount("Bob", 500.00);
 5:         acc_1.printBalance();
 6:         acc_2.printBalance();
 7:         acc_1.deposit(500);
 8:         System.out.println("After Deposit:");
 9:         acc_1.printBalance();
10:         acc_2.printBalance();
11:     }
12: }

After compiling and running:

josephraskind@stargazer:/tmp/instance$ javac BankUser.java; java BankUser
Alice's balance: $1000.00
Bob's balance: $500.00
After Deposit:
Alice's balance: $1500.00
Bob's balance: $500.00

We can see that 500 was added to acc_1's 1000, but not acc_2's 500. Why not? Again, this is because acc_1 is a totally distinct instance of BankAccount from acc_2. Even though they both "share code" in the form of method definitions, they do not share the same runtime information in regards to their instance field values.

We will learn later that some fields can be shared. Those fields are called static fields.

Constructors

A constructor is a special function which details how an object will be instantiated. In Listing 1 the constructor is defined between Lines 5 and 8. Constructors are always named after the class and never have a return type. When the new keyword is used to instantiate an object, the constructor associated with that object is invoked. For example, when we write new BankAccount("Charlene", 211000) we are calling the BankAccount constructor. Constructors are primarily used to set up useful contexts for a new object or set instance fields (the BankAccount class does the latter).

When no constructor is defined, a default constructor will be created by the Java compiler. The default constructor will leave all instance fields to their default values.

Constructors are often used to enforce type invariants/contracts. We will learn how we can do so using exceptions.

Encapsulation

OOP came from a desire to attribute greater functionality to programmer-defined types. One such aspect is the ability to encapsulate data by restricting its visibility to prevent, wittingly or unwittingly, malicious actors from ruining a class's assumptions. This is achieved in Java through the use of access modifiers.

Access Modifiers

So far, you've likely seen the access modifier public the most. There are four access modifiers in Java with varying levels of privilege associated with them:

Modifier Class Package Subclass World Description
public Accessible from anywhere
protected Accessible within the same package and by subclasses
(default) Accessible only within the same package (no keyword required)
private Accessible only within the class it is declared in

All classes, fields and methods that do not specify a modifier are assigned "default".

The reason for why this might be desireable can be quickly illustrated with the simple BankAccount class which has been slightly edited:

 1:   public class BankAccount {
 2:     String owner;
 3:     double balance;
 4: 
 5:     BankAccount(String owner, double initialBalance) throws RuntimException{
 6:         this.owner   = owner;
 7:         this.balance = initialBalance;
 8:         if(balance < 0)
 9:             throw new RuntimeException("Cannot have negative balance!");
10:     }
11: 
12:     void deposit(double amount){
13:         balance = balance + amount;
14:     }
15: 
16:     void withdraw(double amount){
17:         balance = balance - amount;
18:     }
19: 
20:     void printBalance(){
21:         System.out.printf("%s's balance: $%.2f%n", owner, balance);
22:     }
23: 
24: }

Now, it is impossible to instantiate the BankAccount class with a negative number for the balance:

1: public class BankUser{
2:     public static void main(String[] args){
3:         BankAccount acc = new BankAccount("Alice", -1000.00);
4:         acc.printBalance();
5:     }
6: }

After compiling and running:

josephraskind@stargazer:/tmp/instance$ javac BankUser.java; java BankUser
Exception in thread "main" java.lang.RuntimeException: Cannot have negative balance!
        at BankAccount.<init>(BankAccount.java:9)
        at BankUser.main(BankUser.java:3)

But since balance is left as a default field, it can easily be manipulated to violate the invariant:

1: public class BankUser{
2:     public static void main(String[] args){
3:         BankAccount acc = new BankAccount("Alice", 1000.00);
4:         acc.balance = -1000.00;
5:         acc.printBalance();
6:     }
7: }

After compiling and running:

josephraskind@stargazer:/tmp/instance$ javac BankUser.java; java BankUser
Alice's balance: $-1000.00

This can easily be solved by flipping balance to private:

1:   public class BankAccount {
2:     String owner;
3:     private double balance;
4:     //...
5: }

After compiling:

josephraskind@stargazer:/tmp/instance$ javac BankUser.java; java BankUser
BankUser.java:4: error: balance has private access in BankAccount
        acc.balance = -1000.00;
           ^
1 error

The compiler tells us that balance is no longer accessible. This is because it is now restricted to being referenced inside of the BankAccount class definition.

Exercises

  1. In your own words, explain the difference between a class and an instance. Using the BankAccount example from the chapter, identify which parts of the code represent the class definition and which parts represent instances.
  2. Compile and run Listing 10. Then add a call to acc_2.deposit(250) after the existing deposit and print both balances again. Does the deposit to acc_2 affect acc_1? Explain why or why not in terms of instances.
  3. Add a withdraw call to Listing 10 that withdraws $2000 from acc_1. What is the resulting balance? Does the BankAccount class prevent this? What would need to change in the class definition to prevent a negative balance from occurring through a withdrawal?
  4. Listing 17 prevents instantiation with a negative balance but Listing 15 shows the invariant can still be broken. Explain in your own words why making balance private solves this problem. What does private prevent that the constructor check alone cannot?
  5. The chapter states that when no constructor is defined, the Java compiler creates a default one. Remove the constructor from BankAccount and attempt to instantiate it with new BankAccount("Alice", 1000.00). What error do you get? Then instantiate it with new BankAccount() and print acc.owner. What value is printed and why?
  6. The chapter states that methods are invoked with context. In your own words explain what this means. When acc_1.printBalance() is called, how does printBalance know to print Alice and 1000.00 rather than Bob and 500.00?
  7. The access modifier table shows four levels of privilege. Rank them from most to least restrictive and give a real-world analogy for each — for example, private is like a personal diary that only you can read.
  8. Add a transfer method to BankAccount that takes another BankAccount object and a double amount as arguments and transfers that amount from the current account to the other. Test it by transferring $200 from acc_1 to acc_2 and printing both balances before and after.
  9. The chapter introduces the . notation for accessing instance fields and methods. Write a program that creates a BankAccount, accesses its owner field directly, calls printBalance, and then attempts to access a field that does not exist (e.g. acc.phone). Record the compiler error and explain what it tells you.
  10. Make both owner and balance private in BankAccount. Now BankUser can no longer access acc.owner directly. Write a method getOwner in BankAccount that returns the value of owner and update BankUser to use it instead. This pattern is called a getter — why might this be preferable to leaving owner as a default field?
  11. The chapter states "a bank account may have such and such amount, but it should always be able to take in new deposits." Give two more real-world examples of objects where characteristics vary between instances but behaviors remain the same. For each, identify at least two fields and two methods.
  12. Create a second class called SavingsAccount that has an additional field interestRate of type double. Add a method applyInterest that multiplies balance by (1 + interestRate). Instantiate one SavingsAccount and one BankAccount and demonstrate that applyInterest cannot be called on a BankAccount object.
  13. The chapter notes that constructors are "always named after the class and never have a return type." Try adding a return type of void to the BankAccount constructor and recompile. What happens? Then try naming the constructor bankAccount (lowercase b) instead. What happens?
  14. Consider the following code snippet:

    BankAccount a = new BankAccount("Alice", 500.00);
    BankAccount b = a;
    b.deposit(200.00);
    a.printBalance();
    

    Before running it, predict what a.printBalance() will print. Then run it and verify. Does the result surprise you? What does this tell you about how object variables work in Java? We will explore this further in the chapter on references.

  15. The BankAccount constructor uses the this keyword on Lines 6 and 7 of Listing 1:

    this.owner   = owner;
    this.balance = initialBalance;
    

    Remove the this keyword from Line 6 so it reads owner = owner and recompile. Does it compile? Run it and call printBalance. What value is printed for the owner and why? Now restore this.owner = owner and explain in your own words what this refers to and why it is necessary when a parameter name matches a field name.

Footnotes:

1

Other languages, like Python and C, call them functions.

Contact: [email protected] | rss feed | Compiled with org-mode | Licensed under CC BY-NC-SA 4.0