Java SE 8 Documentation
Exercises

"Strategies"

There is a type of interfaces that make it possible to choose how a given algorithm should behave in run time. Such interfaces are called strategy or policy. In that case, a class of algorithms is characterized by an interface and all the concrete implementations are going to be the algorithms themselves. In result, it is enough to refer to the class (interface) itself only at the place of use, and then the actual implementation may be specified later on, at a different section of the program.

For example:

interface BinaryIntFunction {
    int apply(int x, int y);
}

class Add implements BinaryIntFunction {
    @Override
    public int apply(int x, int y) {
        return (x + y);
    }
}

class Subtract implements BinaryIntFunction {
    @Override
    public int apply(int x, int y) {
        return (x - y);
    }
}

class Multiply implements BinaryIntFunction {
    @Override
    public int apply(int x, int y) {
        return (x * y);
    }
}

class Context {
    private final BinaryIntFunction bif;

    public Context(BinaryIntFunction bif) {
        this.bif = bif;
    }

    public int compute(int x, int y) {
        return this.bif.apply(x, y);
    }
}

class Strategies {
    public static void main(String[] args) {
        Context context;
        int x, y;

        x = Integer.parseInt(args[0]);
        y = Integer.parseInt(args[1]);
        context = new Context(new Add());
        System.out.println("Add: " + context.compute(x, y));

        context = new Context(new Subtract());
        System.out.println("Subtract: " + context.compute(x, y));

        context = new Context(new Multiply());
        System.out.println("Multiply: " + context.compute(x, y));
    }
}

In that regard, another useful similar interface is the java.util.Comparator:

interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

Any method that implements interface can be used to define an "ad hoc" ordering for the elements of the type T. There are certain methods that take objects in their parameter list therefore they allow the ordering for type T to be overridden temporarily. It could also make the method work for types that do not have an ordering associated by default.

An example of that is the sort() method of the java.util.Collections class, where lists can be sorted by a Comparator:

static <T> void sort(List<T> list, Comparator<? super T> c);

In this signature, the super sets a constraint on the T type variable such that the T type or any of its super classes must implement the java.util.Comparator interface.

Here is an example on how to use that:

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class NumericStringComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        int x, y;
        try {
            x = Integer.parseInt(s1);
            y = Integer.parseInt(s2);
            return (x - y);
        }
        catch (NumberFormatException e) {
            return s1.compareTo(s2);
        }
    }

    @Override
    public boolean equals(Object obj) {
        return (obj instanceof NumericStringComparator);
    }
}

public class Comp {
    public static void main(String args[]) {
        List<String> values = Arrays.asList(args);
        Collections.sort(values);
        System.out.println("values = " + values);
        Collections.sort(values, new NumericStringComparator());
        System.out.println("values = " + values);
    }
}

Abstract Classes

When classes are derived, their base class always defines a common interface for the descendants, and it builds a common set of fields and methods that are applicable to all of them — since every object of the derived class is an object for the base class as well due to the subtyping relation.

Though note that it is not always necessary to define all the methods in the common base, sometimes it is better to just leave them unimplemented. Such classes that do not have all their methods implemented are called abstract one, which can be declared by adding the abstract modifier for both the definition of the class and the affected methods. The compiler will forbid using the abstract modifier together with the private, final, and static modifiers as they would make method overriding impossible, which does not make much sense.

Because abstract classes do not have all their methods defined, they cannot be instantiated.

On deriving from an abstract class, it is not required to implement all the missing (that is, abstract) method bodies, but then the derived class must stay abstract.

Here is an example for that:

abstract class Shape {
    protected boolean symmetric;

    public Shape(boolean symmetric) {
        this.symmetric = symmetric;
    }

    public abstract double circumference();
    public abstract double area();

    public boolean isSymmetric() {
        return symmetric;
    }

    public void print() {
        System.out.println("A: " + area());
        System.out.println("C: " + circumference());
    }
}

class Circle extends Shape {
    private static final double PI = 3.1415;
    private double r = 1.0;

    public Circle() {
        super(true);
    }

    @Override
    public double circumference() {
        return (2 * r * PI);
    }

    @Override
    public double area() {
        return (r * r) * PI;
    }
}

class Rectangle extends Shape {
    private double a = 1.0, b = 1.0;

    public Rectangle() {
        super(true);
    }

    @Override
    public double circumference() {
        return 2 * (a + b);
    }

    @Override
    public double area() {
        return a * b;
    }
}

An example of application:

public class Main {
    public static void printArea(Shape shape) {
        System.out.println(shape.area());
    }

    public static void main(String[] args) {
        Shape s = new Circle(); // c.f.: static-dynamic type
        printArea(s);
    }
}

Comparison of Abstract Classes and Interfaces

Due to their nature of use, there may be a parallel drawn between abstract classes and interfaces. Neither of them may be instantiated, they may have methods without implementation. But abstract classes may have fields that are not class-level ones as well as they do not have to be declared final, and they may have public, protected, and private methods with bodies. On the contrary, all the fields must be public, static, and final in an interface, and all methods must be public. In addition to that, abstract classes are still classes, so they may have only a single base, while it is possible to implement many interfaces at the same time.

So it might be a question which one of them should be chosen during the development of programs.

  • Abstract classes should be preferred when:

    • we want to share code between more, tightly coupled classes.

    • we want to reuse fields and methods among the instances of the derived classes, and we need a more restrictive visibility than public.

    • we do not only want to work with class-level and final fields. Abstract classes make it possible to define methods that access and modify the state of the objects.

  • Interfaces should be preferred when:

    • we want to ensure that random, loosely coupled classes provide the same functionality, for example, all their objects can be compared (see java.lang.Comparable).

    • we want describe the behavior of a given data type, and it does not count who and how will implement that.

    • we need multiple inheritance.

An example for abstract classes from the Java standard libraries is java.util.AbstractMap, which is a component of Java Collections. The classes that are derived from this one (such as java.util.HashMap, java.util.TreeMap and so on) reuse many of its common methods, such as get(), put(), isEmpty(), containsKey(), and containsValue().

With regard to the java.util.HashMap class, note that it is also an example of implementing a number of interfaces like java.io.Serializable, java.lang.Cloneable, and java.util.Map. By only looking at the list of implemented interfaces, we can immediately observe that the instances of the java.util.HashMap class can be cloned (copied), turned into a sequence of bytes, or they can act finite maps. We could also note there that this class is derived from java.util.AbstractMap — so there may be classes (and they occur quite often) that are derived from an abstract class, while they implement multiple interfaces.

Enumerations

Enumerations in the Java language are values that are declared by the enum keyword. They refer to special types where it is possible to restrict the set of elements to a list of predefined constants. According to that, values of such types can be only one of those constants. A typical example of its use is the representation of the day of the week, or the cardinal directions. Those constants — similarly to the class-level fields with the final modifier — have identifiers made of all-uppercase letters. The enumeration itself is introduced by the enum keyword like as follows:

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

enum values are recommended to use when a predefined set of constants is needed.

public class EnumDemo {
    Day day;

    public EnumDemo(Day day) {
        this.day = day;
    }

    public void tellItLikeItIs() {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;

            case FRIDAY:
                System.out.println("Fridays are better.");
                break;

            case SATURDAY: case SUNDAY:
                System.out.println("Weekends are best.");
                break;

            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }

    public static void main(String[] args) {
        EnumDemo firstDay = new EnumDemo(Day.MONDAY);
        firstDay.tellItLikeItIs();
        EnumDemo thirdDay = new EnumDemo(Day.WEDNESDAY);
        thirdDay.tellItLikeItIs();
        EnumDemo fifthDay = new EnumDemo(Day.FRIDAY);
        fifthDay.tellItLikeItIs();
        EnumDemo sixthDay = new EnumDemo(Day.SATURDAY);
        sixthDay.tellItLikeItIs();
        EnumDemo seventhDay = new EnumDemo(Day.SUNDAY);
        seventhDay.tellItLikeItIs();
    }
}

Note that enum actually declares a class. This means that it is possible to place fields and methods in the body of the definition, like it was a class. In addition to that, the compiler will automatically add certain methods to the enum classes. For example, every enum has a class-level values() method that can return all the constants defined in the class in the same order as they have been enumerated in the source. This method could be then nicely exploited in a for each construct, where the elements of the enum is traversed.

for (Planet p : Planet.values()) {
    System.out.printf("Your weight on %s is %f.\n", p, p.surfaceWeight(mass));
}

Every enumeration also automatically becomes the descendant of the java.lang.Enum class. As a consequence, because classes can only have a single base, enum definitions cannot have anything other than that. The language syntax also demands that the constants must be listed first, then followed by the fields and methods. On definition of extra fields and methods they have to be enclosed by a semicolon. The constructor for enum types cannot be public or protected, and they could not be called directly.

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // kilogram
    private final double radius; // meter

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    private double mass() { return mass; }
    private double radius() { return radius; }

    // gravitational constant  (m^3 kg^{-1} s^{-2})
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }

    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }

        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / EARTH.surfaceGravity();

        for (Planet p : Planet.values()) {
           System.out.printf("Your weight on %s is %f.\n", p,
             p.surfaceWeight(mass));
        }
    }
}

Exercises

  • Modify the implementation of the previous stack calculator in a way that all applicable binary operators are stored in a Map<String,BinaryIntFunction> data structure, so always the corresponding operator is chosen during the evaluation of an expression. If the given operator is not found in the map, raise an error with the message "Unknown operator".

  • Define an abstract class with the name Prism in order to represent mathematical prisms. Let there be a field that stores the height of the prism and an abstract method that tells the base area, and another (non-abstract) method that computes the prism's volume using the base area and height.

  • Derive the Cylinder and Cube classes from Prism by implementing the abstract method.

    In addition to that, override the toString() method so it could return a textual representation that corresponds to the actual type.

    For Cylinder, it should look like this:

    Cylinder: (height = 10, radius = 5)

    For Cube:

    Cube: (side = 4)
  • Define the Scalable interface that contains a method named scale() and a field named defaultScale. The method has an optional parameter that if not specified must be the default scale, and it scales the actual object.

  • Implement the Scalable interface with the Prism classes and all of its descendants.

  • Implement the standard Comparable interface — that is, an ordering — for all the descendants of the Prism class. The objects should be compared by their volumes. The correct behavior of the implementation may be verified by the Collections.sort() method.

  • With the help of the Comparator interface, create a special ordering for the Prism values that compares objects by their base areas. The correct behavior of the implementation may be verified by the Collections.sort() method.

  • Modify the implementation of the previous Date class so the months are represented with an enumeration, and therefore only valid months might be allowed.

Related sources
Front page
Back