Java Tutorial 6: Useful Standard Methods, Access Modifiers and Abstract Classes

In the previous tutorial in this series, we looked at inheritance in Java. In this tutorial we continue with inheritance, plus we'll look at some standard methods in Java and a few other interesting odds and ends.

Access Modifiers



Take a look at the following code.


class Device {
    String name;
    
    public Device(String name) {
        this.name = name;
    }
}

class Camera extends Device {
    public Camera(String name) {
        super(name);
    }
}


public class Application {
    public static void main(String[] args) {
        
        Camera camera = new Camera("Camera 1");
        
        System.out.println(camera.name);
    }
}




We create a Device class; then we derive a child Camera class from Device.

Notice that, for ease of reading, we've declared all the classes in the same file. This is legitimate, and in fact you can declare classes practically anywhere in Java. However, a top-level public class (such as Application in this example) can only be defined in a file that has the same name as the class.

The Device class has an instance variable called name of type String which the Camera class inherits. However, nothing stops us accessing the name variable in the main program. This is undesirable; we want data specific to an object to be encapsulated in that object, and inaccessible to outside interference.

The Three Access Modifiers in Java (and the Default Access Level)



If we declare an instance variable of a class with no access modifier, by default it can be accessed from anywhere within the same package. We haven't looked at packages yet, but since we haven't declared any package name for this file, all these classes exist in the (same) default package.

So if we write

class Something {
    String name = "Hello";
}




and create objects from this class, the name variable will be accessible from any method or class within the same package.

We can change that by adding an explicit access modifier

private



The private access modifier means that only the class that a variable or method is declared in can access that variable or method.

In the following code, we've added the private access modifier to the name variable. Now name can't be accessed anywhere except within the Device class itself. It can't even be accessed in the Camera class (although we can set it by calling the public Device constructor using the keyword super).


class Device {
    private String name;
    
    public Device(String name) {
        this.name = name;
    }
}

class Camera extends Device {
    public Camera(String name) {
        
        // Note, at no point do we access
        // Device::name directly; we 
        // CANNOT now write
        // this.name = name
        super(name);
    }
}


public class Application {
    public static void main(String[] args) {
        
        Camera camera = new Camera("Camera 1");
        
        // Won't work
        // System.out.println(camera.name);
    }
}





public



The public keyword is the opposite to private. It means that other classes can freely access the method or variable. You can see examples of this in the above code. The constructors are declared public so that other classes can call them (by creating objects from the classes). The main method is also declared public so that the operating system can run it to start the program.

We can of course also declare variables public. Usually this is bad practice; instance variables should be made private or protected. Also, any methods used only the class itself should be private or protected. However, sometimes you really do what a variable to be accessed from outside the class. Usually this is only the case with final (constant) static (accessible via the class) variables. By convention, these constants are often written in uppercase letters.


class Device {
    private String name;
    
    Device(String name) {
        this.name = name;
    }
}

class Camera extends Device {
    
    // A public class constant
    public final static String DEVICE_CLASS = "CAMERA 1.23";
    
    Camera(String name) {
        super(name);
    }
}


public class Application {
    public static void main(String[] args) {
        
        System.out.println(Camera.DEVICE_CLASS);
    }
}




CAMERA 1.23




The above code stores the "device class" in a constant public static (class) variable.

protected



So what about protected? This keyword allows you to create a method or variable that is accessible within the class itself, but also within all derived classes and any class in the same package (unlike private, which restricts the method or variable to one particular class).

Suppose we change the Device class to make name protected (and we've also moved it into its own file called Device.java).

public class Device {
    protected String name;
    
    Device(String name) {
        this.name = name;
    }
}




The name variable is now not accessible to other classes unless they are in the same package (which usually they won't be -- but more on packages in a future tutorial). But it IS accessible to any derived classes. The following code shows that the Camera child class can access name in the parent class freely.

Camera.java:

public class Camera extends Device {
    
    // A public class constant
    public final static String DEVICE_CLASS = "CAMERA 1.23";
    
    Camera(String name) {
        super(name);
    }
    
    String getName() {
        return name;
    }
    




Access Modifers Summary



In summary: use private as much as possible. The private access modifier will restrict your variable or method to being used within the class where it's defined.

If you can't use private, try to use protected. This will restrict your variable or method to derived classes and classes within the same package.

Finally, when you genuinely want a method (or sometimes a constant variable) to be accessible from anywhere, use public.

If you don't use any of these (which you usually should), the default access level is that any code in the same package can access your variable or method.

Is-A vs Has-A



Sometimes when you want to use functionality from one class in another, you don't know whether to derive a child class from that class, or just use the class to define an instance variable (object) of that class.

In the following example, both the Camera class and the Car class have the functionality of the Machine class. Camera has an instance variable of type Machine, whereas the Car class IS a Machine (inherits from it).


class Machine {
    public void start() {
        System.out.println("Starting!");
    }
}

// Camera "has a" machine.
class Camera {
    Machine machine = new Machine();
    
    public void start() {
        machine.start();
    }
}

// Car "is a" machine.
class Car extends Machine {
    
}

public class Application {
    public static void main(String[] args) {
        
        Camera camera = new Camera();
        Car car = new Car();
        
        camera.start();
        car.start();
        
    }
}




In general, create a child class if your intended child class IS A kind of the parent class. For instance, a car really IS A kind of machine. Also make sure that you have control over the parent class, or that the parent class was intended to be overridden and documented with that in mind. Otherwise you will have a headache if the parent class changes in a future revision.

In all other situations, it is safe to use the HAS A relationship to gain the functionality of a class; just use the class to create an instance variable (or even a variable local to a method).

Upcasting and Downcasting



Wherever you have a child class, polymorphism means you can store it in a variable of the parent class type. This is known as upcasting, since you are implicitly "casting" the child class to the parent class type.

Although overridden methods in the child class CAN then be invoked using the parent class variable, you CANNOT use the parent class variable to call methods that only exist in the child class.

An example will make things clearer.


class Machine {
    public void start() {
        System.out.println("Start!");
    }
    
    public void stop() {
        System.out.println("Machine stopping.");
    }
}

class Camera extends Machine {
    public void snapshot() {
        System.out.println("Snap!");
    }
    
    @Override
    public void stop() {
        System.out.println("Camera stopping");
    }
}

public class Application {
    public static void main(String[] args) {
        
        Camera camera = new Camera();
        
        Machine machine = camera;
        machine.start();
        
        // Note: the camera version of stop() is invoked.
        machine.stop();
        
        // We can't do this
        // machine.snapshot();
        
    }
}




Start!
Camera stopping




How about downcasting? Downcasting is perhaps more common than upcasting, and more the sort of thing you'd actually notice, rather than something that happens implicitly.

Downcasting means that you cast an object stored in a parent type variable to the child type.

Often, for one reason or another, you'll store a child class in a parent class variable and then want to access specific child class functionality. That's when you downcast. Let's take an example.


class Machine {
    public void start() {
        System.out.println("Start!");
    }
    
    public void stop() {
        System.out.println("Machine stopping.");
    }
}

class Camera extends Machine {
    public void snapshot() {
        System.out.println("Snap!");
    }
    
    @Override
    public void stop() {
        System.out.println("Camera stopping");
    }
}

public class Application {
    public static void main(String[] args) {
        
        Machine machine = new Camera();
        
        Camera camera = (Camera)machine;
        
        camera.snapshot();
        
    }
}




Snap!




Here, a Camera object is stored in a Machine type variable. Then later on we decide we'd like to cast it back to a Camera so we can invoke camera-specific methods. We can do this by using "(Camera)" in brackets to cast to the Camera type.

Note that you have to be careful with downcasting. Even if we had in fact stored a Machine in the Machine-type variable, not a Camera, we could still have downcast it to a Camera and tried to access the snapshot() method. Except that the program would then have thrown a 'runtime exception' instead of working as you'd intended.

Object: Standard Methods From the Parent of All Objects



All objects in Java ultimately derive from a class called Object. When you create a new class, even if the class appears to extend no other class, in fact it extends the Object class.

The Object class has some methods which it is often useful to override. Usually you can override these using your IDE; in Eclipse, right click on your class, go to Source->Override/Implement Methods, or go to Source->Generate hashCode() and equals() or Source->Generate toString().

What do all these methods do?

toString()



The toString() method is intended to create a string representation of your class. When you create a new class you will often want to override toString().

Here we override toString() to provide the Machine class with a string representation of itself, which can be called when we want to tell one machine from another.


class Machine {
    private String name;
    
    // The constructor sets the machine's name.
    public Machine(String name) {
        this.name = name;
    }
    
    public String toString() {
        return "This machine is named: " + name;
    }
}



public class Application {
    public static void main(String[] args) {
        
        Machine machine = new Machine("My Machine");
        
        System.out.println(machine.toString());
        
    }
}




equals() and hashCode()



equals() is a method that compares your object with another object and returns true only if it's "equal" to the other object. What do we mean by "equal"?

If you want to check whether two objects are equal in Java, it's often tempting to use the == operator. But in fact this checks whether two references to objects point at the same object or not.

Here's an example.


class Machine {
    private String name;
    
    public Machine(String name) {
        this.name = name;
    }
    
    public String toString() {
        return "Machine name: " + name;
    }
}

public class Application {
    public static void main(String[] args) {
        
        Machine machine1 = new Machine("Big machine");
        Machine machine2 = new Machine("Big machine");
        Machine machine3 = machine2;
        
        if(machine1 == machine2) {
            System.out.println("Machine1 equals Machine2");
        }
        else {
            System.out.println("Machine1 does not equal Machine2");
        }
        
        if(machine2 == machine3) {
            System.out.println("Machine2 equals Machine3");
        }
        else {
            System.out.println("Machine2 does not equal Machine3");
        }
        
    }
}





Machine1 does not equal Machine2
Machine2 equals Machine3




You can see that == only considers two variables to be equal if they literally point to the same object.

However, often we want to know whether two objects are equivalent in terms of what they represent.

For instance, in the above example, machine1 and machine2 are both objects of type Machine; what's more, they both contain exactly the same data --- their name instance variables are set to the same thing, and they contain no other instance variables. They are not the same object, but the two objects are the same in terms of what they do and their inner state.

This is what equals() is for. We override equals() so that it returns true if two different objects are equivalent. In this case, we can do this by returning true if two machines have the same name.

It's easiest just to let your IDE implement equals(). All you have to do is select which instance variables you consider important when testing for equality. At the same time, you can get it to implement the hashCode() method. Ideally you should implement hashCode() whenever you implement equals(). hashCode()'s function is to return an integer which is the same when two objects are equivalent, but different when they are not equivalent.

In Eclipse, you can implement both methods by right clicking, selecting "Source", then "Generate hashCode() and equals()". All you have to do then is to selected which instance variables matter to you when testing for equality. Eclipse will generate an optimised equals() method for you. Don't forget to delete it and regenerate it if you add further important instance variables.

hashCode() is only used if you add your object to a Map (which we haven't covered yet). If you are completely certain that you'll never add your object to a map, you can not worry about it. But it's best if you let your IDE generate it for you.

In the following code we've added automatically the equals() and hashCode() methods, and we use them instead of == to test if our objects are equal. Note that machine1 and machine2 are now considered equal.


class Machine {
    private String name;
    
    public Machine(String name) {
        this.name = name;
    }
    
    public String toString() {
        return "Machine name: " + name;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Machine other = (Machine) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
    
    
}

public class Application {
    public static void main(String[] args) {
        
        Machine machine1 = new Machine("Big machine");
        Machine machine2 = new Machine("Big machine");
        Machine machine3 = machine2;
        
        if(machine1.equals(machine2)) {
            System.out.println("Machine1 equals Machine2");
        }
        else {
            System.out.println("Machine1 does not equal Machine2");
        }
        
        if(machine2.equals(machine3)) {
            System.out.println("Machine2 equals Machine3");
        }
        else {
            System.out.println("Machine2 does not equal Machine3");
        }
        
    }
}




Machine1 equals Machine2
Machine2 equals Machine3




Important note about strings: you should always use equals() and not == when testing if two String objects are equal. Sometimes == will actually work as you'd expect on strings; this is because if you declare two identical String objects in Java, Java will optimise behind the scenes by setting your variables to point at the same String objects. But you should not rely on this; if you want to check if two strings are equal in the sense that they contain the same characters. always use equals() and not ==.

clone()



The clone() method is overridden to provide your objects with a way of creating copies of themselves. A 'copy' here means that the object returned by clone() should be a new object that is equal to the object that creates it in the sense that the equals() method returns true if used to compare the two objects.

An example should make this clear. Note that clone() returns an Object which must be downcast to the right type of object.


class Machine {
    private String name;
    
    public Machine(String name) {
        this.name = name;
    }
    
    public String toString() {
        return "Machine name: " + name;
    }
    
    @Override 
    public Object clone() {
        return new Machine(name);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Machine other = (Machine) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
    
    
}

public class Application {
    public static void main(String[] args) {
        
        Machine machine1 = new Machine("New Machine");
        Machine machine2 = (Machine)machine1.clone();
        
        if(machine1.equals(machine2)) {
            System.out.println("Machine1 equals Machine2");
        }
        else {
            System.out.println("Machine1 does not equal Machine2");
        }
        
        if(machine1 == machine2) {
            System.out.println("machine1 and machine2 point to the same objects");
        }
        else {
            System.out.println("machine1 and machine2 point to different objects");
        }
        
    }
}




Machine1 equals Machine2
machine1 and machine2 point to different objects




Return Type Covariance



In the above example, the clone() method returns an Object, not a Machine. This is the usual behaviour of clone(). However, there is one useful trick that we can use to return an object of type Machine.

In general, an overridden method in Java must have the same 'signature' (same parameters and return type) as the parent method. However, there is one exception to this rule. If a parent method returns an object of type A, the overrridden version may return an object of type B instead, if (and only if) B happens to be a child class of A.

In the above example, clone() returns an object of type Object. However, all objects are children of the built-in Object class, including objects of type Machine. That means we can return a Machine instead in the override if we want. Then we can get rid of the ugly downcast.

This is called return type covariance (presumably because the return type is varying in the same direction as the derived class; we are moving down the object hierarchy).


class Machine {
    private String name;
    
    public Machine(String name) {
        this.name = name;
    }
    
    public String toString() {
        return "Machine name: " + name;
    }
    
    // Make use of return type covariance.
    // This is still a legitimate override.
    @Override 
    public Machine clone() {
        return new Machine(name);
    }   
}

public class Application {
    public static void main(String[] args) {
        
        Machine machine1 = new Machine("New Machine");
        Machine machine2 = machine1.clone();
        
    }
}




Abstract Classes



One final topic for this tutorial. We saw in a previous tutorial that in Java you can use interfaces to force a class to implement certain methods. One class can implement many interfaces. There is another way to force a class to implement certain methods. This is to derive it from an abstract class.

Abstract classes contain abstract methods that must be overridden and implemented in child classes. If a class contains even one abstract method, it can not be instantiated (you can't create objects from it). You can only override it. Once you declare even one abstract method in a class, you must also declare the class abstract.

The advantage of this over using an interface is that you can put implementation in an abstract class that child classes can use. However, of course a class can only inherit from a single abstract class. In Java, there is no multiple inheritance.


// The Machine class is declare abstract
// because it has an abstract method.
// It now can't be instantiated.
abstract class Machine {
    private String name;
    
    public Machine(String name) {
        this.name = name;
    }
    
    public String toString() {
        return "Machine name: " + name;
    }

    // The abstract method must be overridden
    // in child classes, or else they too
    // must be declared abstract and you can't
    // create objects from them.
    abstract public void start();
}

class Car extends Machine {
    public Car(String name) {
        super(name);
    }
    
    @Override
    public void start() {
        System.out.println("Starting the car.");
    }
}

public class Application {
    public static void main(String[] args) {
        
        // We cannot do the following, because
        // Machine is abstract.
        //Machine machine = new Machine("A machine");
        
        Car car = new Car("A car");
        
        car.start();
        
    }
}




Starting the car.