Java Tutorials Made Easy banner

 

Section 1 - Abstraction Defined

 

Abstraction is the principle of separating the details of an implementation from its user interface. A client is given access to an interface or abstract class that specifies all the client needs to know for utilization. Since the objects which implement the interface or abstract class have different implementation details, different results are obtained when the client uses different objects even though the client interface and code is the same.

 

Section 2 - Abstraction Demonstrated using Shapes

 

Suppose a client needs to display the attributes of various shapes and their area and perimeter. The first attempt may look something like the following:

public static void displayCircle(Circle c)
{
    System.out.println("Circle" + c.getRadius() + c.area()
                       + c.perimeter();
}

public static void displayRectangle(Rectangle r)
{
    System.out.println("Rectangle" + r.getLength() + r.getWidth()
                       + r.area() + r.perimeter();
}

 

This is a poor approach for several reasons. Firstly, it is not scalable. For each new shape that is created, the client needs to write a new method to display it. Secondly, the client is given access to implementation details it does not need. The client does not need to know that a Rectangle consists of length and width. The client only needs to display Rectangle attributes.

An abstraction for all shapes can be defined that specifies that all shapes shall provide area and perimeter. The Shape interface defines this abstraction.

public interface Shape
{
    double area();
    double perimeter();
}

 

The toString method specified by the Object class can be overridden to display the type of shape and the shape’s attributes. Therefore, the displayCircle and displayRectangle methods can be replaced by the displayShape method.

public static void displayShape(Shape s)
{
    System.out.println(s + " area= " + s.area() +
                       " perimeter= " + s.perimeter());
}

 

In order for a shape to display its type, an accessor can be specified in the Shape interface. This accessor is overridden by each subclass returning a string corresponding to the type of shape. In order to support the Object class’s equals method, a default matchType method is provided to compare the type of two Shape implementations using the accessor.

public interface Shape
{
    String getType();
    default boolean matchType(Shape s)
    {
        return getType().equals(s.getType());
    }
    double area();
    double perimeter();
}

 

Implementations for the Shape interface are subclasses that override the getTypearea, and perimeter methods, but contain different implementation details. A circle, a rectangle, and a triangle are all shapes for which area and perimeter can be calculated but in different ways. A circle’s area and perimeter are calculated using its radius. A rectangle’s area and perimeter are calculated using its length and width. A triangle’s area and perimeter are calculated using its base and height.

The Circle class is shown below. It contains field radius that is initialized in its constructor. The overridden getType method returns the string “Circle”. The overridden area and perimeter methods compute area and perimeter, respectively, based on field radius. The overridden toString method calls getType to display the type of shape, then displays its radius. The overridden equals method calls the default matchType method to check that the object being compared is a Circle, then checks its radius.

class Circle implements Shape
{
    private double radius;

    public Circle(double r)
    {
        radius = r;
    }

    @Override public String getType() { return "Circle"; }

    @Override public double area()
    {
        return Math.PI * radius * radius;
    }

    @Override public double perimeter()
    {
        return 2.0 * Math.PI * radius;
    }

    @Override public String toString()
    {
        return getType() + " radius = " + radius;
    }

    @Override public boolean equals(Object s)
    {
        boolean result = matchType((Shape)s);
        if (result)
        {
            Circle c = (Circle)s;
            result = radius == c.radius;
        }
        return result;
    }
}

 

The Rectangle class is shown below. It contains fields length and width that are initialized in its constructor. The overridden getType method returns the string “Rectangle”. The overridden area and perimeter methods compute area and perimeter, respectively, based on fields length and width. The overridden toString and equals method utilize fields length and width to display and compare rectangles, respectively.

class Rectangle implements Shape
{
    private double length;
    private double width;

    public Rectangle(double l, double w)
    {
        length = l;
        width = w;
    }

    @Override public String getType() { return "Rectangle"; }

    @Override public double area() { return length * width; }

    @Override public double perimeter()
    {
        return 2.0 * (length + width);
    }

    @Override public String toString()
    {
        return getType() + " length = " + length
             + " width = " + width;
    }

    @Override public boolean equals(Object s)
    {
        boolean result = matchType((Shape)s);
        if (result)
        {
            Rectangle r = (Rectangle)s;
            result = length == r.length && width == r.width;
        }
        return result;
    }
}

 

The Triangle class is shown below. It contains fields base and height that are initialized in its constructor. The overridden getType method returns the string “Triangle”. The overridden area and perimeter methods compute area and perimeter, respectively, based on fields base and height. The overridden toString and equals method utilize fields base and height to display and compare triangles, respectively.

class Triangle implements Shape
{
    private double base;
    private double height;

    public Triangle(double b, double h)
    {
        base = b;
        height = h;
    }

    @Override public String getType() { return "Triangle"; }

    @Override public double area() { return base * height * 0.5; }

    @Override public double perimeter()
    {
        double hypotSquared = Math.pow(base,2.0) +
                              Math.pow(height,2.0);
        double hypot = Math.pow(hypotSquared, 0.5);
        return base + height + hypot;
    }

    @Override public String toString()
    {
        return getType() + " base = " + base
                         + " height = " + height;
    }

    @Override public boolean equals(Object s)
    {
        boolean result = matchType((Shape)s);
        if (result)
        {
            Triangle t = (Triangle)s;
            result = base == t.base && height == t.height;
        }
        return result;
    }
}

 

A square is a specific kind of rectangle whose length and width are equal. Therefore the Square class can extend the Rectangle class and provide a single value to Rectangle constructor.  The overridden getType method returns the string “Square”. The toString and equals methods of the Rectangle class will work correctly for the Square class because the Square class’s implementation of getType will be called for Square objects.

class Square extends Rectangle
{
    public Square(double s)
    {
        super(s, s);
    }

    @Override public String getType() { return "Square"; }
}

 

The client can create CircleRectangleSquare, and Triangle objects

Circle    c = new Circle(5.0);
Rectangle r = new Rectangle(5.0,5.0);
Square    s = new Square(5.0);
Triangle  t = new Triangle(5.0,2.0);

 and call the displayShape method to display each shape’s attributes and its area and perimeter. The attributes will consist of radius if the Shape is a Circlelength and width if the Shape is a Rectangle or a Square, and base and height if the Shape is a Triangle.

displayShape(c);
displayShape(r);
displayShape(s);
displayShape(t);

OUTPUT:

Circle radius = 5.0 area= 78.53981633974483 perimeter= 31.41592653589793
Rectangle length = 5.0 width = 5.0 area= 25.0 perimeter= 20.0
Square length = 5.0 width = 5.0 area= 25.0 perimeter= 20.0
Triangle base = 5.0 height = 2.0 area= 5.0 perimeter= 12.385164807134505

 

Suppose the client also needs to compare two shapes. The client can pass references to two Shape implementations as method parameters. All the client needs to know is that a Shape has an equals method that can be called with another Shape as its parameter. The implementation details, such as matchType and the appropriate fields are abstracted from the client and handled by the equals method.

public static void compareShapes(Shape s1, Shape s2)
{
    System.out.println(s1 + " equals " + s2 + " ? " + s1.equals(s2));
}

 

The comparison of a 5 x 5 rectangle to a 5 x 5 square returns false because the matchType method of the Square class returns “Square” instead of “Rectangle”. The comparison of a 5 x 5 rectangle to a 4 x 2 rectangle returns false because the length and width attributes are different. The comparison of two circles with radius 5 returns true. The comparison of a circle and a triangle returns false.

compareShapes(r, s);
compareShapes(r, new Rectangle(4.0, 2.0));
compareShapes(c, new Circle(5.0));
compareShapes(c, t);

OUTPUT:

Rectangle length = 5.0 width = 5.0 equals Square length = 5.0 width = 5.0 ? false
Rectangle length = 5.0 width = 5.0 equals Rectangle length = 4.0 width = 2.0 ? false
Circle radius = 5.0 equals Circle radius = 5.0 ? true
Circle radius = 5.0 equals Triangle base = 5.0 height = 2.0 ? false

 

The complete program is shown below.

 

Listing 1 - Shape.java

public interface Shape
{
    String getType();
    default boolean matchType(Shape s)
    {
        return getType().equals(s.getType());
    }
    double area();
    double perimeter();
}

class Circle implements Shape
{
    private double radius;

    public Circle(double r)
    {
        radius = r;
    }

    @Override public String getType() { return "Circle"; }

    @Override public double area()
    {
        return Math.PI * radius * radius;
    }

    @Override public double perimeter()
    {
        return 2.0 * Math.PI * radius;
    }

    @Override public String toString()
    {
        return getType() + " radius = " + radius;
    }

    @Override public boolean equals(Object s)
    {
        boolean result = matchType((Shape)s);
        if (result)
        {
            Circle c = (Circle)s;
            result = radius == c.radius;
        }
        return result;
    }
}

class Rectangle implements Shape
{
    private double length;
    private double width;

    public Rectangle(double l, double w)
    {
        length = l;
        width = w;
    }

    @Override public String getType() { return "Rectangle"; }

    @Override public double area() { return length * width; }

    @Override public double perimeter()
    {
        return 2.0 * (length + width);
    }

    @Override public String toString()
    {
        return getType() + " length = " + length
               + " width = " + width;
    }

    @Override public boolean equals(Object s)
    {
        boolean result = matchType((Shape)s);
        if (result)
        {
            Rectangle r = (Rectangle)s;
            result = length == r.length && width == r.width;
        }
        return result;
    }
}

class Square extends Rectangle
{
    public Square(double s)
    {
        super(s, s);
    }

    @Override public String getType() { return "Square"; }
}

class Triangle implements Shape
{
    private double base;
    private double height;

    public Triangle(double b, double h)
    {
        base = b;
        height = h;
    }

    @Override public String getType() { return "Triangle"; }

    @Override public double area() { return base * height * 0.5; }

    @Override public double perimeter()
    {
        double hypotSquared = Math.pow(base,2.0) +
                              Math.pow(height,2.0);
        double hypot = Math.pow(hypotSquared, 0.5);
        return base + height + hypot;
    }

    @Override public String toString()
    {
        return getType() + " base = " + base
               + " height = " + height; }

    @Override public boolean equals(Object s)
    {
        boolean result = matchType((Shape)s);
        if (result)
        {
            Triangle t = (Triangle)s;
            result = base == t.base && height == t.height;
        }
        return result;
    }
}

class ShapeClient
{
    public static void displayShape(Shape s)
    {
        System.out.println(s + " area= " + s.area() +
                           " perimeter= " + s.perimeter());
    }

    public static void compareShapes(Shape s1, Shape s2)
    {
        System.out.println(s1 + " equals " + s2 + " ? " +
                           s1.equals(s2));
    }

    public static void main(String[] args)
    {
        Circle c = new Circle(5.0);
        Rectangle r = new Rectangle(5.0,5.0);
        Square s = new Square(5.0);
        Triangle t = new Triangle(5.0,2.0);

        displayShape(c);
        displayShape(r);
        displayShape(s);
        displayShape(t);

        compareShapes(r, s);
        compareShapes(r, new Rectangle(4.0, 2.0));
        compareShapes(c, new Circle(5.0));
        compareShapes(c, t);
    }
}

 

Section 3 - Polymorphic References

 

The displayShape method in the previous example accepts a reference to a Shape. The program in Listing 1 passes references to CircleRectangleSquare, and Triangle objects as the argument to displayShapeCircleRectangleSquare, and Triangle are all subclasses of the Shape class.

In Java, a superclass variable can refer to an object of one of its subclasses. Such a reference is called a polymorphic reference. Polymorphic references can be passed into and returned from a method.This is possible because all fields and methods needed for proper operation of the superclass are present in the subclass. The Circle class contains the toStringarea, and perimeter methods, so a reference to a Circle object can be passed as the argument to the displayShape method which expects a reference to a Shape.

 

Section 4 - Polymorphism

 

The Java Virtual Machine (JVM) calls the version of a method defined in an object, not the version specified by its reference. When a reference is polymorphic and its method is overridden in a subclass, the version of the method defined in the subclass is called instead of the version defined in the superclass.

When the program in Listing 1 calls the displayShape method with a polymorphic reference to a Circle object, π times the square of the radius is returned. When the displayShape method with a polymorphic reference to a Rectangle object, the length times the width is returned.

This capability is called polymorphism. Polymorphism means literally “many forms”. The area method takes the form of Circle.areaRectangle.area, and Triangle.area based on the object referred to by reference variable shape.

Polymorphism is determined at runtime, not compile time. When the displayShape method is compiled, it is not possible to know whether to call Circle.areaRectangle.area, or Triangle.area

since the type of the object at reference variable shape is unknown. When the in Listing 1 is run, however, the JVM knows that the first call to displayShape passes a reference to a Circle object into reference variable shape, and therefore calls Circle.area. The second call to displayShape passes a reference to a Rectangle object into reference variable shape, and therefore calls Rectangle.area, and so on.

Consider the following classes. Class A defines method M1. Class B extends class A and overrides method M1. Class C extends class B, but does not override method M1.

class A
{
    void M1() { System.out.println("A.M1"); }
}

class B extends A
{
    void M1() { System.out.println("B.M1"); }
}

class C extends B {}

 

The main method of class ABCDDriver creates an A object, a B object, and a C object. Reference variable b is polymorphic, since it is of type A while referring to an object of subclass B. Reference variables a and c are non-polymorphic since the type of the variable is the same as the type of the object. Method M1 accepts a reference variable of type A. The first call to M1 passes a variable of type A which refers to an object of type A. Since the object is of type A, the JVM invokes the version of M1 defined in class A and “A.M1” is displayed. The second call to M1 passes a variable of type A which refers to an object of type B. Since the object is of type B and class B overrides method M1, the JVM invokes the version of M1 defined in class B and “B.M1” is displayed. The third call to M1 passes a variable of type C which refers to an object of type C. Since the object is of type C but class C has not overridden method M1, the JVM invokes the version of M1 defined in class B and “B.M1” is displayed.

public class ABCDDriver
{
    public static void callM1(A a)
    {
        a.M1();
    }

    public static void main(String[] args)
    {
        A a = new A();
        A b = new B();
        C c = new C();

        callM1(a);
        callM1(b);
        callM1(c);
    }
}

OUTPUT:

A.M1
B.M1
B.M1

 

Section 5 - Factories

 

The main method in Listing 1 hard codes four shape objects of various types. Often the number,  types, and attributes of objects are not known at compile time. Sometimes this information is read from a file, a database, or over a network connection. A factory is a construct that creates different objects based on some arbitrary input. In Java, a factory is typically a static method that returns a polymorphic reference to one of several objects that can be created based on arbitrary input.

The following is an example of a shape factory with factory method makeInstance. For simplicity, the method’s input is String parameter s, but input is often obtained from a file, database, or network connection. The input needs to specify which subclass object to instantiate and the value of any non-zero fields. The name of the class to instantiate is used as the value of a switch statement. For example, a call to makeInstance with string “Circle 5.0” will traverse switch case “Circle”, instantiate a Circle with radius = 5, and assign the reference to variable shape. Variable shape is initialized to null, so that if the subclass name is incorrect and no object is created, the caller can test for nullness and report an error.

public static Shape makeInstance(String s)
{
    Shape shape = null;
    StringTokenizer tokens = new StringTokenizer(s);
    String token = tokens.nextToken();

    switch (token) {
        case "Circle":
            shape = new Circle(
              Double.parseDouble(tokens.nextToken()));
            break;

        case "Rectangle":
            shape = new Rectangle(
              Double.parseDouble(tokens.nextToken()),
              Double.parseDouble(tokens.nextToken()));
            break;

        case "Square":
            shape = new Square(
              Double.parseDouble(tokens.nextToken()));
              break;

        case "Triangle":
            shape = new Triangle(
              Double.parseDouble(tokens.nextToken()),
              Double.parseDouble(tokens.nextToken()));
             break;
    }
    return shape;
}

 

 

The main method below contains a string named data that specifies the types and attributes of five different shapes and one invalid shape (Rhombus). The string is tokenized in a loop that obtains the data necessary to create a single shape. Then the makeInstance method is called with the single line of string data, and a reference to a Shape subclass object is returned. If the reference is non-null (and the object is valid), the ShapeClient class’s displayShape method is called to display the shape. Otherwise, an error is reported.

public static void main(String[] args)
{
    String data =
    "Circle 5.0\nRectangle 5.0 5.0\nSquare 5.0\nTriangle 5.0 2.0\nRhombus";
    StringTokenizer tokens = new StringTokenizer(data,"\n");

    while (tokens.hasMoreTokens())
    {
        Shape s = makeInstance(tokens.nextToken());
        if (s != null)
            ShapeClient.displayShape(s);
        else
            System.out.println("Invalid shape");
    }
}

OUTPUT:

Circle radius = 5.0 area= 78.53981633974483 perimeter= 31.41592653589793
Rectangle length = 5.0 width = 5.0 area= 25.0 perimeter= 20.0
Square length = 5.0 width = 5.0 area= 25.0 perimeter= 20.0
Triangle base = 5.0 height = 2.0 area= 5.0 perimeter= 12.385164807134505
Invalid shape

 

The complete program is shown below.

 

Listing 2 - ShapeFactory.java

import java.util.StringTokenizer;
public class ShapeFactory
{
    public static Shape makeInstance(String s)
    {
        Shape shape = null;
        StringTokenizer tokens = new StringTokenizer(s);
        String token = tokens.nextToken();

        switch (token) {
            case "Circle":
                 shape = new Circle(
                   Double.parseDouble(tokens.nextToken()));
                 break;

            case "Rectangle":
                 shape = new Rectangle(
                   Double.parseDouble(tokens.nextToken()),
                   Double.parseDouble(tokens.nextToken()));
                 break;

            case "Square":
                 shape = new Square(
                   Double.parseDouble(tokens.nextToken()));
                 break;

            case "Triangle":
                 shape = new Triangle(
                   Double.parseDouble(tokens.nextToken()),
                   Double.parseDouble(tokens.nextToken()));
                 break;
        }
        return shape;
    }

    public static void main(String[] args)
    {
        String data =
   "Circle 5.0\nRectangle 5.0 5.0\nSquare 5.0\nTriangle 5.0 2.0\nRhombus";
        StringTokenizer tokens = new StringTokenizer(data,"\n");

        while (tokens.hasMoreTokens())
        {
            Shape s = makeInstance(tokens.nextToken());
            if (s != null)
                ShapeClient.displayShape(s);
            else
                System.out.println("Invalid shape");
        }
    }
}

 

Section 6 - Polymorphism Demonstrated using Vehicles

 

The Vehicle interface is an abstraction for a vehicle that specifies adding and using fuel and obtaining the amount of fuel remaining. The type of fuel is not known.

public interface Vehicle
{
    void addFuel(double fuel);
    void useFuel(double distance);
    double getFuelRemaining();
}

 

This abstraction can be further subdivided into human-powered vehicles in which the fuel is calories and gasoline-powered vehicles in which the fuel is gasoline. These abstractions can fully define the addFuel and getFuelRemaining methods.

abstract class HumanPoweredVehicle implements Vehicle
{
    protected double calories;

    @Override public void addFuel(double fuel) { calories += fuel; }

    @Override public double getFuelRemaining() {return calories;}
}

abstract class GasolinePoweredVehicle implements Vehicle
{
    protected double gasoline;

    @Override public void addFuel(double fuel) { gasoline += fuel; }

    @Override public double getFuelRemaining() {return gasoline;};
}

 

A bicycle is a human-powered vehicle that uses 2000 calories per mile traveled.

class Bicycle extends HumanPoweredVehicle
{
    private final double CALORIES_PER_MILE = 2000.0;

    @Override public void useFuel(double distance) {
        calories -= distance * CALORIES_PER_MILE;
    }
}

 

An automobile is a gasoline-powered vehicle that uses 1 gallon of gasoline per 30 miles traveled.

class Automobile extends GasolinePoweredVehicle
{
    private final double GALLONS_PER_MILE = 1/30.0;

    @Override public void useFuel(double distance) {
        gasoline -= distance * GALLONS_PER_MILE;
    }
}

 

A motor boat is a gasoline-powered vehicle that uses 1 gallon of gasoline per 5 nautical miles traveled.

class MotorBoat extends GasolinePoweredVehicle
{
    private final double GALLONS_PER_NAUTICAL_MILE = 1/5.0;

    @Override public void useFuel(double distance) {
        gasoline -= distance * GALLONS_PER_NAUTICAL_MILE;
    }
}

 

The VehicleClient defines method processVehicle that accepts a polymorphic reference to a Vehicle, the amount of fuel to add, and the amount of fuel to use. If the object referred to by reference variable Vehicle is a subclass of HumanPoweredVehicle, then HumanPoweredVehicle.addFuel and HumanPoweredVehicle.getFuelRemaining are called.

If the object referred to by reference variable Vehicle is a subclass of GasolinePoweredVehicle, then GasolinePoweredVehicle.addFuel and GasolinePoweredVehicle.getFuelRemaining are called. Method Bicycle.useFuel, Automobile.useFuel, or MotorBoat.useFuel is called if the object referenced by variable Vehicle is a Bicycle, Automobile, or MotorBoat, respectively.

 

public static void processVehicle(Vehicle v, double add, double use)
{
    v.addFuel(add);
    v.useFuel(use);

    System.out.println("Fuel remaining = " + v.getFuelRemaining());
}

 

The main method in the vehicle client creates a bicycle, an automobile, and a boat, each referenced by a polymorphic variable of type Vehicle. ProcessVehicle is then called to add 4000 calories to the bicycle, travel 1 mile, and display the remaining calories, add 2 gallons of gas to the automobile, travel 30 miles, and display the remaining gasoline, then add 2 gallons of gas to the motor boat, travel 5 miles, and display the remaining gasoline.

public static void main(String[] args)
{
    Vehicle bicycle = new Bicycle();
    Vehicle car = new Automobile();
    Vehicle boat = new MotorBoat();

    processVehicle(bicycle, 4000, 1);
    processVehicle(car, 2, 30);
    processVehicle(boat, 2, 5);
}

OUTPUT:

Fuel remaining = 2000.0
Fuel remaining = 1.0
Fuel remaining = 1.0

 

The complete program is shown below.

 

Listing 3 - Vehicles.java

public interface Vehicle
{
    void addFuel(double fuel);
    void useFuel(double distance);

    double getFuelRemaining();
}

abstract class HumanPoweredVehicle implements Vehicle
{
    protected double calories;

    @Override public void addFuel(double fuel) { calories += fuel; }

    @Override public double getFuelRemaining() {return calories;}
}

abstract class GasolinePoweredVehicle implements Vehicle
{
    protected double gasoline;

    @Override public void addFuel(double fuel) { gasoline += fuel; }

    @Override public double getFuelRemaining() {return gasoline;};
}

class Bicycle extends HumanPoweredVehicle
{
    private final double CALORIES_PER_MILE = 2000.0;

    @Override public void useFuel(double distance) {
        calories -= distance * CALORIES_PER_MILE;
    }
}

class Automobile extends GasolinePoweredVehicle
{

    private final double GALLONS_PER_MILE = 1/30.0;

    @Override public void useFuel(double distance) {
        gasoline -= distance * GALLONS_PER_MILE;
    }
}

class MotorBoat extends GasolinePoweredVehicle
{
    private final double GALLONS_PER_NAUTICAL_MILE = 1/5.0;

    @Override public void useFuel(double distance) {
        gasoline -= distance * GALLONS_PER_NAUTICAL_MILE;
    }
}

class VehicleClient
{
    public static void processVehicle(Vehicle v, double add,
                                      double use)
    {
        v.addFuel(add);
        v.useFuel(use);

        System.out.println("Fuel remaining = " +
                           v.getFuelRemaining());
    }

    public static void main(String[] args)
    {
        Vehicle bicycle = new Bicycle();
        Vehicle car = new Automobile();
        Vehicle boat = new MotorBoat();

        processVehicle(bicycle, 4000, 1);
        processVehicle(car, 2, 30);
        processVehicle(boat, 2, 5);
    }
}