Classes in Java

Java is built around object-oriented programming. Object-oriented programming is built on the idea of representing collections of data as “objects” to be interacted with. Similar objects are described by a single class.

In this module, we’ll show some examples for how to create and use a Java class.


Contents


Note: This unit covers pre-requisite material for CS 3140, and should already be understood by students before taking CS 3140. However, because of the heavy emphasis on Object-Oriented Programming on CS 3140, this material is included in the online textbook.

Class

A class is a description of how a particular “type” (or classification) of objects are interacted with and behave. In Java, we write a Java class as a .java file. This .java file describes the data and methods the class has, as well as defining constructors for instantiating objects. It’s important to remember that a class is a pattern, or blueprint, used to describe a classification of objects, but is not itself and object.

In Java, when we create a class, we are defining our own data type. In the same way we can create String and ArrayList objects in Java, we can create objects of our own custom data type.

Parts of a class

Example: the following shows a partial example of a simple class for modeling Pez Dispensers:

public class PezDispenser {
    //fields
    private String characterName;
    private final int capacity;
    private int count;
    
    //constructor
    public PezDispenser(String characterName, int capacity) {
        this.characterName = characterName;
        this.capacity = capacity;
        this.count = 0;
    }
    
    //methods
    public void load() {
        count = capacity;
    }
    
    public boolean dispense() {
        if (count == 0) {
            return false;
        }
        count--;
        return true;
    }
}

Here, we see a field for storing the name of the character (characterName) of the PezDispenser. We also see fields for tracking the maximum number of candies in the PezDispenser (capacity) and the current number of candies (count).

However, without creating an instance of a Pez Dispenser, we cannot interact with this code.

Instance

An instance is a particular example of a class. For example, if I bought 3 Pez Dispensers, I would have 3 instances of the class PezDispenser.

In a well-designed class, instances of the same class: 1) Each have their own state, separate from other instances 2) Share the same behavior with other instances

Constructors

Constructors are special methods used to create instances of a class. For example, in the above PezDispenser class, the constructor is:

    public PezDispenser(String characterName, int capacity) {
        this.characterName = characterName;
        this.capacity = capacity;
        this.count = 0;
    }

This method is invoked in a special way. For example, let’s say I wanted to make a Superman Pez Dispenser with a capacity of 12. I would, in the code that uses the PezDispenser class, write:

public class PezDemo {
    public static void main(String[] args) {
        PezDispenser superman = new PezDispenser("Superman", 12);
        ...//do something with superman
    }
}

Now we have an instance of the class PezDispenser, referenced by the variable superman. If we also wanted a second, Batman Pez Dispenser, we could augment this code:

public class PezDemo {
    public static void main(String[] args) {
        PezDispenser superman = new PezDispenser("Superman", 12);
        PezDispenser batman = new PezDispenser("Batman", 12);
        ...//do something with superman and Batman
    }
}

This gives us two separate instances.

State

The state of an object is the description of the current status of an instance (not a class!). In this way, all instances have their own state. Typically, the state of an object is described that the value of its fields.

Fields

In the above class, the fields of the class PezDispenser are:

public class PezDispenser {
    private String characterName;
    private final int capacity;
    private int count;
    
    ...
}

Note that it is not required to list all fields at the top of the class. However, it is convention to do so. Following this convention ensures that classes are more easily readable and predictable.

The state of the two instances are as follows:

  • superman - characterName = "Superman", capacity = 12, count = 0
  • batman - characterName = "Batman", capacity = 12, count = 0

Think of each of these objects as having a set of their own 3 variables, one String and two ints. Technically, for both objects capacity is not a variable but a final, or immutable value, but the key is that both values of capacity are stored in separate memory locations.

Behaviors

While each instance of a class has its own state, the instances share the same behavior. For example, the way that I load or dispense from a Superman Pez Dispenser is going to be the same as the way I load and dispense a Batman PezDispenser.

Methods

Typically, behaviors of a class are described by their methods (non-static functions). In the above class, the fields of the class PezDispenser are:

public class PezDispenser {
    ... 
    
    public void load() {
        count = capacity;
    }

    public boolean dispense() {
        if (count == 0) {
            return false;
        }
        count--;
        return true;
    }
}

These methods describe the two behaviors we can do on a Pez Dispenser. That is, we can load or dispense from a Pez Dispenser. These methods are invoked by calling the method on a specific instance using the dot operator. For example:

public class PezDemo {
    public static void main(String[] args) {
        PezDispenser superman = new PezDispenser("Superman", 12);
        PezDispenser batman = new PezDispenser("Batman", 12);
        superman.load();
        ...
    }
}

In the last line of this method, superman.load();, we are saying:

Call the load method on the superman instance of PezDispenser.

Before we call this method, are values are:

  • superman - characterName = "Superman", capacity = 12, count = 0
  • batman - characterName = "Batman", capacity = 12, count = 0

However, after calling superman.load(), the values of our two instances are:

  • superman - characterName = "Superman", capacity = 12, count = 12
  • batman - characterName = "Batman", capacity = 12, count = 0

You’ll notice that only the value of count for superman changed. The value of count for batman did not change, because we didn’t act on the batman instance. This is because both instances have a separate state. And while we changed the state of superman, we did not change the state of batman.

However, if we also wanted to load the batman Pez Dispenser, it would be as simple as calling batman.load(). That is, invoking the same behavior on the different instance.

Encapsulation

Why is this useful?

The key benefit of the tools we looked at so far is encapsulation. That is, separating how something is used from how it works. And we interact with the idea of encapsulation all the time!

For example, if you want to use a toaster, you plug it in, add bread and push down the lever. You don’t have to think about:

  • What is the resistance on the coils that superheat to toast the bread?
  • What is the power draw of the toaster from the wall?
  • What type of wires are necessary to ensure the toaster is safe to use for it’s given power draw?
  • How is the plug manufactured and attached to the toaster?
  • How is does the system know when the lever is pushed down vs. when it isn’t pushed down?
  • How is the time kept so that the toast pops up at the right time?

You don’t have to answer these questions, but whoever designed and built the toaster did! These questions have answer, but you don’t need them to use the toaster.

This is valuable in design because it allows us to create classes that are simple to use, and hides any implementation details from the user.

Public and Private

In this way, we want to ensure that the user of a class only interacts with classes in intended ways. For example, you may notice that I made all three fields of Pez Dispenser private, but I made the methods public. These values describe how encapsulation will be enforced. Specifically:

  • public - this field/method/constructor can be accessed via any other class in a given project
    • For example, PezDemo, which is outside the PezDispenser class, can call the public methods and the constructor on PezDispenser
  • private - this field is hidden from all other classes in a given project. For example, PezDemo cannot access the fields in the class directly. This means that PezDemo can only set the value of count through usages of the load and dispense methods.

This limitation is a good thing, because it prevents the PezDispenser object from being used incorrectly. For example, if we had count as public, wwe could modify it directly, with something like:

public class PezDemo {
    public static void main(String[] args) {
        PezDispenser superman = new PezDispenser("Superman", 12);
        PezDispenser batman = new PezDispenser("Batman", 12);
        superman.load();
        batman.count = 7;
        ...
    }
}

While this may seem convenient, the problem is that it allows a client (someone using the class) to perform meaningless and incorrect operations.

public class PezDemo {
    public static void main(String[] args) {
        PezDispenser batman = new PezDispenser("Batman", 12);
        batman.count = 99;
        batman.load();
    }
}

In the above case, we set the Batman PezDispensers count value to 99, which is much larger than the capacity. Then, when we load the PezDispenser, it results in count decreasing to 12. This weird behavior could cause confusion or a bug down the line.

As such, by making count be private, it becomes syntatically illegal to use batman.count (or superman.count), as the field is hidden.

Other Class Content

The following units cover common programming tools/techniques for creating classes and using objects.

Getters and setters

Of course, we may still want to see what the value of count is, or we may want to allow the client to set the value of count, but within certain parameters (i.e., between 0 and capacity inclusive).

Overloaded Constructors

You can have multiple constructor methods for a single class. For example, with our PezDispenser class, we could have a secondary constructor that defaults the value of capacity to 12:

public class PezDispenser {
    //fields
    private String characterName;
    private final int capacity;
    private int count;
    
    //constructor
    public PezDispenser(String characterName, int capacity) {
        this.characterName = characterName;
        this.capacity = capacity;
        this.count = 0;
    }
    
    public PezDispenser(String characterName) {
      this.characterName = characterName;
      this.capacity = 12;
      this.count = 0;
    }
    
    ...
}

Additionally, if we wanted to make PezDispensers that we can set the initial count value, we could add a three argument constructor:

public class PezDispenser {
    //fields
    private String characterName;
    private final int capacity;
    private int count;
    
    //constructor
    public PezDispenser(String characterName, int capacity, int count) {
        if (capacity < count || count < 0) {
            throw new IllegalArgumentException("count must be non-negative and no greater than capacity");
        }
        this.characterName = characterName;
        this.capacity = capacity;
        this.count = count;
    }
    ...

Here, we have to handle some basic error checking to ensure that the value of count makes sense given the capacity.

But…this is all very redundant, yes? Three constructors that are nearly identical, and simply set default values in two of them. As such, we can shorten these three constructors using the this constructor:

public class PezDispenser {
    //fields
    private String characterName;
    private final int capacity;
    private int count;
    
    public static final int DEFAULT_CAPACITY = 12;
    public static final int DEFAULT_COUNT = 0;
    
    //constructor
    public PezDispenser(String characterName, int capacity, int count) {
        if (capacity < count || count < 0) {
            throw new IllegalArgumentException("count must be non-negative and no greater than capacity");
        }
        this.characterName = characterName;
        this.capacity = capacity;
        this.count = count;
    }
    
    public PezDispenser(String characterName, int capacity) {
        this(characterName, capacity, DEFAULT_COUNT);
    }
    
    public PezDispenser(String characterName) {
        this(characterName, DEFAULT_CAPACITY, DEFAULT_COUNT);
    }
    
    ...
}

Here, we have one primary constructor, and two constructors that simply pass the inputs (along with any default values) to the primary constructor. In this way, we are still only setting the initial field values in one constructor, but allowing the flexibility to call the constructor with different numbers of arguments, depending on what the user needs. Be aware that when using the this constructor in this way, the this call must be the first line of the constructor.

Destructive vs. non-destructive methods

Some methods in objects may be either destructive or non-destructive. A destructive method is one that, when the method is called, could result in the state of the object changing. For example, the PezDispenser methods load and dispense are destructive methods. Note that we call dispense destructive even if a particular call (namely, when count is already equal to zero) would not result in the object changing. In general, setter methods are always considered destructive (since that is the point of a setter).

Non-destructive methods are methods that do not change the state of the object they are called on. Typically, methods like toString and hashCode should always be non-destructive, as should getter methods. In some classes, there will be no destructive methods at all! For example, the Java String class has no destructive methods. This is because the contents of a String are final, or immutable, meaning they can’t be changed. Take for example the following code:

    String s = "Hello World!";
    s.toUppercase();
    System.out.println(s);

What would you expect to print here? Well, you might think what will print is HELLO WORLD!, but you would be incorrect! While the method toUppercase returns the String "HELLO WORLD", it doesn’t change the value of s. That’s because the existing String s cannot be changed after it is created. If you want to change the String s to be all uppercase, you would have to reassign the variable s to a new String.

    String s = "Hello World!";
    s = s.toUppercase(); //reassign the value of s
    System.out.println(s);

The important difference here is that the String variable s is now actually referencing to a different String entirely. That is, the new String "HELLO WORLD" is stored in memory in a completely different location than the old String "Hello World". As a note, after this code executes, the old String location will eventually be released back to memory to be reallocated as needed.

There are a few Java keywords worth mentioning as it relates to class construction.

Static vs. non-static

Unless explicitly stated to be static, all methods are non-static. For example, everything, except the constants we created (DEFAULT_CAPACITY and DEFAULT_COUNT) is non-static. So what does that mean?

  • non-static - belongs to a specific instance of a class.
  • static - belongs to the class as a whole.

As an example, consider the following small Java class:

public class Example {
  private int x;
  private static int sum = 0;

  public Example(int x) {
    this.x = x;
    sum += x;
  }

  public int getX() {
    return x;
  }
  
  public static int getSum() {
      return sum;
  }
}

Let’s say we run the following code:

public class ExampleDemo {
    public static void main(String[] args) {
        Example example1 = new Example(1);
        Example example2 = new Example(2);
        Example example3 = new Example(3);
        System.out.println(Example.getSum());
    }
}

The above code will print 6. That’s because whenever we create a new instance of Example, we are adding that Example object’s x value to sum. Additionally, note that when we call getSum, we aren’t using an instance of Example, but rather we are calling getSum on the class Example. This is what we mean by a static method not belonging to any one instance. It can be invoked by the class name. There are a number of static methods you have like used before:

  Math.sqrt(5);
  Integer.parseInt(input);
  Arrays.toString(myArray);
  Collections.sort(myList);

Here, we are calling the method we want on the class. We don’t make an instance of the classes Math, Integer, Arrays, or Collections.

By contrast, non-static method calls will look like:

  String s = myString.toUpperCase();
  String input = scanner.nextLine();
  myList.sort();
  superman.load();

Here, we are calling methods on an instance of a class, specifically an instance of String, Scanner, List, and PezDispenser.

When in doubt, you should generally make your methods non-static. This is especially true if you are create a datatype that you are going to use multiple times in your program.

Mutability and final

By default, every variable you create in Java is mutable, which means “it can change”. However, when we create objects, there may be variables that we don’t want to change. The final keyword gives us a syntactic way to make a variable immutable.

For instance, consider our PezDispenser fields:

public class PezDispenser {
    //fields
    private String characterName;
    private final int capacity;
    private int count;
    
    ...
}

You’ll notice that the variable capacity is final. What this means is that, after the constructor is completed, the value of capacity can never be changed. That is, the value of capacity is immutable. On the other hand, count is not stated as final, and is able to change. In fact, it’s necessary for count to be able to change in order to mimic the behavior of a PezDispenser (since loading and dispensing from a Pez Dispenser changes the number of candies inside).

It’s worth noting that I left characterName as mutable. Do I have a good reason for this? Not really! And, as Brian Goetz states:

“Just as it is a good practice to make all fields private unless they need greater visibility, it is a good practice to make all fields final unless they need to be mutable.” -― Brian Goetz, Java Concurrency in Practice

In this case, it would make sense for me to make characterName final unless I am envisioning something like a PezDispenser where you can change the character head on top. If I do want to be able to change the character head on top, then I wouldn’t make the value final.

Note that when a value is final, we cannot have a setter method to modify it, since modifying a final variable outside of the constructor is explicitly prohibited.


Next submodule: