Skip to main content

How to reuse your Java classes

Posted by hellofadude on December 18, 2013 at 11:24 AM PST

There are mainly two ways by which one may reuse classes in Java. The first is by way of composition. Composition provides a way to compose your classes from objects of existing classes, essentially making use of the objects' functionality as opposed to its form.

The second method is by what we call inheritance, which describes how one may derive a new class as a type of an existing class. With inheritance you make use of not only the functionality of an existing class, but more importantly its form.

Composition

To make use of composition in your class, for non-primitives, you simply create a reference to that object; however for primitives you must define them directly.

Here is an example of a class using composition:-

class Dog {
    Dog() {
        System.out.println( "Dog()" );
    }
}
public class Kennel {
    private int i;
    private double d;
    private String Dog1, Dog2, Dog3, Dog4, Dog5, Dog6;
    private Dog poddle = new Dog();

    public String toString() {
        return
           "Dog1 = " + Dog1 + " " +
           "Dog1 = " + Dog1 + " " +
           "Dog1 = " + Dog1 + " " +
           "Dog1 = " + Dog1 + " " +
           "Dog1 = " + Dog1 + " " +
           "Dog1 = " + Dog1 + " " + "\n" +
           "i = " + i + " " + "d = " + d;
     }
    public static void main(String[] args) {
        Kennel kn = new Kennel();
        System.out.print(kn);
    }
}
/* Output
Dog()
Dog1 = null Dog1 = null Dog1 = null Dog1 = null Dog1 = null Dog1 = null
i = 0 d = 0.0
*//

The Kennel class in this example is composed using primitives as well as non-primitives like the String and Dog object. Primitives that are fields in a class are automatically initialised to zero as can be gleaned from the output, while object references are initialised to null. As you may observe it is possible to print a null reference without throwing an exception, however an attempt to call a method on an uninitialised variable will induce the relevant compiler error. Further, take note of the special toString() method in the Kennel class which comes as standard in every non-primitive object and is useful in special situations when the compiler wants a string but has only an object.

Composition is generally best suited to those situations where you require the functionality of an existing class inside of your new class but not its interface. In other words, composition allows you to embed an object in order to use it to implement features in your new class. To achieve this, you simply embed private objects of an existing class inside your new class.

Inheritance

Inheritance on the other hand, is a way of taking one class and deriving from it, another class of the same type. The relationship between the two classes can be described as one class being like another or a type of another class. For instance, consider the relationship between a vehicle and a car. We could say, a car is a type of vehicle and our reasoning would make sense to most people. This is essentially how inheritance is designed to work.

In Java, all classes you create implicitly inherit from the standard root class Object, from which all objects are ultimately derived. To make use of inheritance, you add the extends keyword followed by the name of the base class:-

class Pet {
    private String s = "Pet";
    public void append(String a) { s += a; }
    public void cuddle() { append(" cuddle()");  }      
    public void run() { append("run()");    }
    public void Jump() { append(" jump()");    }
    public String toString() { return s; }
    public static void main(String[] args) {
        Pet p = new Pet();
        p.cuddle(); p.run(); p.Jump();
        System.out.print(p);
    }
  
}

public class Cat extends Pet {  
    //change a method  
    public void Jump() {
        append(" Pet.jump()");
        super.Jump();
    }
    //add new members to interface
    public void purr() {
        append(" purr()");
    }
    public static void main(String[] args) {
        Cat c = new Cat();
        c.cuddle();
        c.run();
        c.Jump();
        c.purr();
        System.out.println(c);
        System.out.println("Test base class:");
        Pet.main(args);
    }
}
/* Output
Pet cuddle() run() Pet.jump() jump() purr()
Test base class:
Pet cuddle() run() jump()
*//

In the above example, the Cat class extends or otherwise inherits the members of the Pet class and adds new members as you can see with the purr() method.

It is also possible to observe within this example, how both classes include a main() method which is a way to allow for the easy testing of your classes. It is possible to include a main() method in every one of the classes in your program without breaking any rule. In such situations only the main() for the class invoked from the command line will be called though in this case, the Pet.main() method is invoked from the main() of the Cat class.

Inheritance is clearly demonstrated in this example because the Cat class, by making use of the extends keyword, ensures Cat objects automatically get all the methods defined in the Pet class - even if not explicitly defined - which to all intents is considered to be its base class. This explains why the Cat class is able to casually make a call to the append() method of its base class from within the jump() and purr() methods

The example also demonstrates how to modify a method already defined in the base class, within the derived class as is demonstrated by the jump() method. The super keyword is used to call the base class version of the jump() method from within the same method in the Cat class.

Further, it should be noted how all methods within the Pet class are declared as public access. This is important because the Pet class defaults to package access as is expected of any class for which you do not specify any access specifier and failure to make the methods public might make them inaccessible to inheriting classes from other packages.

Base class initialisation

On the subject of inheritance, it is worth noting that when you create an object of a derived class, it automatically contains a sub-object of the base class within it. This is possible because Java automatically inserts a call to the base class constructor from within the derived class constructor. Here is an example that demonstrates my meaning:-

 class Hat {
    Hat() { System.out.println("Hat constructor"); }
}

class Fedora extends Hat {
    Fedora() { System.out.println("Fedora constructor"); }
}
public class Stetson extends Fedora {
    Stetson() { System.out.println("Stetson constructor"); }
    public static void main(String[] args) {
        Stetson st = new Stetson();
    }
}
/* Output
Hat constructor
Fedora constructor
Stetson constructor
*//

The preceding example demonstrates how, when you use inheritance, the base class is always initialised before the derived class constructors are able to access it. In other words construction happens inside-out from the base class. This is how Java ensures the base-class subobject is initialised correctly and avoids many of the problems experienced in other programming languages. Note also that this example uses the default no args constructor. If however, your base class included only constructors with arguments, you would be forced in those circumstances to make an explicit call to the base class constructor using the super keyword. In addition, the call to the base class would have to be the first thing you do in the derived class constructor.

Upcasting

Upcasting is a term that refers to the direction of movement from a derived type to a base type up the inheritance chain. It is based on the way class inheritance diagrams have traditionally been drawn i.e. with the root or base class at the top of the page, and extending downward.

Upcasting is very relevant to inheritance, the most important aspect of which is the relationship expressed between the new class and the base class, which can be summarised by saying "the new class is a type of this existing class".

It is normally considered a safe operation because you are going from a more specific type to a more general type with very little chance of compromising your data. The derived class is said to be a superset of the base class and consequently might contain more methods than the base class, but it must contain at least the methods in the base class. Consider the following example:-

  class Instrument {
    public void play() {}
    static void tune(Instrument i) {
      i.play;
    }
  }

  public class Piano extends Instrument {
    public static void main(String[] args) {
       Piano grandPiano = new Piano();
       Instrument.tune(grandpiano); //upcasting
    }
  }

The interesting thing about this example is that the tune() method, which accepts an Instrument object reference is passed a Piano object reference in the main(). How is this possible? You might well observe that an object of the Piano class is also a type of Instrument by virtue of the fact that it inherits all the methods of the Instrument class, and therefore has access to the tune() method of its base class.

The act of converting a Piano object into an Instrument reference is what we refer to as upcasting. Understanding when you might need to upcast in your code allows you to make an informed decision about whether to use composition or inheritance when designing your classes. If you will ever need to upcast in your program, then inheritance is probably necessary, but if not, you should consider carefully whether you need inheritance as it is only really useful when there is a clear need for it.

Finally, it should be well understood that inheritance has some implication for how Java loads and initialises your classes particularly when dealing with statics;

Combining composition and inheritance

More often, one may combine both composition and inheritance to derive a more complex type. The following example demonstrates the use of both methods:-

class Beverage {
    Beverage(int i) {
        System.out.println("Beverage constructor");
    }
}
class OrangeJuice extends Beverage {
    OrangeJuice(int i) {
        super(i);
        System.out.println("OrangeJuice constructor");
    }
}
class Beer extends Beverage {
    Beer(int i) {
        super(i);
        System.out.println("Beer constructor");      
    }
}
class Food {
    Food(int i) {
        System.out.println("Food constructor");      
    }
}
class Potatoes extends Food {
    Potatoes(int i) {
        super(i);
        System.out.println("Potato constructor");
    }
}
class Chicken extends Food {
    Chicken(int i) {
        super(i);
        System.out.println("Chicken constructor");
    }
}
class Dessert {
    Dessert(int i) {
        System.out.println("Dessert constructor");
    }
}
class Cake extends Dessert {
    Cake(int i) {
        super(i);
        System.out.println("Cake constructor");
    }
}
class Dinner {
    Dinner(int i) {
        System.out.println("Dinner constructor");
    }
}
public class Meal extends Dinner {
    private OrangeJuice oj;
    private Beer br;
    private Potatoes pt;
    private Chicken chk;
    private Cake ck;
    public Meal(int i) {
        super(i + 1);
        oj = new OrangeJuice(i + 1);
        br = new Beer(i + 2);
        pt = new Potatoes(i + 10);
        chk = new Chicken(i + 1);
        ck = new Cake(i + 1);
    }  
    public static void main(String[] args) {
        Meal ml = new Meal(1);
    }

}
/* Output
Dinner constructor
Beverage constructor
OrangeJuice constructor
Beverage constructor
Beer constructor
Food constructor
Potato constructor
Food constructor
Chicken constructor
Dessert constructor
Cake constructor
*//

The Meal class is constructed using both composition and inheritance and the example further demonstrates the importance of calling the base class constructor as the first thing you do in the derived class constructor using the super keyword. This is particularly necessary when you are working with constructors that accept any number of arguments.

Summary

In this post, we have discussed the important subject of code reuse in Java as a mechanism by which one may quickly and easily add functionality to the classes one creates. We have also distinguished between two main ways by which this possible - composition and inheritance. Finally, we looked briefly at the manifestation of the inheritance mechanism in Java and its effects within constructor initialisation, upcasting and in the loading and initialisation of objects.

contact me @ kaseosime@btinternet.com