[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: the benefits of immutability



Pascal Costanza wrote:
 > Of course, I expect Jeanette to think about what she does and write
 > the following code:
 >
 > public boolean equals(Object that) {
 >   if (that == null) return false;
 >
 >   if ((this.class != that.class) &&
 >        this.class.isAssignableFrom(that.class))
 >     return that.equals(this);
 >
 >   return super.equals(that) &&
 >              (that instance of ColoredCircle ?
 >                 this.color == ((ColoredCircle)that).color : true);
 > }

This will not run as written due to a number of syntax errors.  I
assume Jeannette is supposed to write something along the lines of:

    public boolean equals(Object that) {
        if (that == null) return false;

        final Class thisClass = this.getClass();
        final Class thatClass = that.getClass();

        if ( !thisClass.equals(thatClass) &&
            thisClass.isAssignableFrom(thatClass))
            return that.equals(this);

        return super.equals(that) &&
            (that instanceof ColoredCircle ?
             this.color.equals(((ColoredCircle) that).color) : true);
    }

I also assume that Barbara was supposed to write something similar for
her Circle class:

    public boolean equals(Object that) {
        if (that == null) return false;

        final Class thisClass = this.getClass();
        final Class thatClass = that.getClass();

        if ( !thisClass.equals(thatClass) &&
            thisClass.isAssignableFrom(thatClass))
            return that.equals(this);

        return that instanceof Circle ?
             this.radius == ((Circle)that).radius : true;
    }


Consider this:

   Circle xx = new ColoredCircle(3, Color.red);
   Circle yy = new Circle(3);
   Circle zz = new ColoredCircle(3, Color.blue);

In the above example, xx equals yy, and yy equals zz.  Is that
correct?

To say that Jeannette must be smart is to partially misplace the
blame.  The smart implementation of 'equals' that Jeannette is
supposed to come up with relies on the fact that Barbara implemented
her 'equals' method in a particular fashion.  This means that if
Barbara expects subclasses of Circle to behave correctly, she *must*
document the way in which Circle implements 'equals'.  This breaks
encapsulation.

 > I think the really issue here is that Java doesn't have
 > multi-methods. Both tricks I have introduced in this example are
 > hand-coded simulations of multiple dispatch.

I need a little more convincing that this is indeed the root of the
problem.  Even if Java did have multi-methods, it would just take
fewer lines of code to write a non-transitive 'equals' method.  I'm
all for shorter code, but that doesn't address the crux of the matter,
now does it?  The crux of the matter is, no matter how smart Jeannette
is, Barbara's job is *hard*.  It is non-trivial to write an externally
facing class that allows subclassing.

Vadim


P.S.  In case anyone cares to see a specific example of
how the suggested implementation fails to meet the transitivity
requirement:


$ find . -name \*.java -print -exec cat {} \;
./Circle.java
public class Circle {
    private int radius;

    /**
     *@pre radius > 0
     **/
    public Circle(int radius) {
        if ( radius <=0 ) { throw new IllegalArgumentException(); }
        this.radius = radius;
    }

    public int getRadius() {
        return radius;
    }

    public boolean equals(Object that) {
        if (that == null) return false;

        final Class thisClass = this.getClass();
        final Class thatClass = that.getClass();

        if ( !thisClass.equals(thatClass) &&
            thisClass.isAssignableFrom(thatClass))
            return that.equals(this);

        return that instanceof Circle ?
             this.radius == ((Circle)that).radius : true;
    }
}
./ColoredCircle.java
import java.awt.Color;

public class ColoredCircle extends Circle {
    private Color color;

    public ColoredCircle(int radius, Color color) {
        super(radius);
        this.color = color;
    }

    public Color getColor() {
        return color;
    }


    public boolean equals(Object that) {
        if (that == null) return false;

        final Class thisClass = this.getClass();
        final Class thatClass = that.getClass();

        if ( !thisClass.equals(thatClass) &&
            thisClass.isAssignableFrom(thatClass))
            return that.equals(this);

        return super.equals(that) &&
            (that instanceof ColoredCircle ?
             this.color.equals(((ColoredCircle) that).color) : true);
    }
}
./Main.java
import java.awt.Color;

public final class Main {

    public final static void main(String[] args) {
        Circle xx = new ColoredCircle(3, Color.red);
        Circle yy = new Circle(3);
        Circle zz = new ColoredCircle(3, Color.blue);

        report(xx, "xx", yy, "yy");
        report(yy, "yy", zz, "zz");
        report(xx, "xx", zz, "zz");
    }

    private static void report(Circle c1, String c1name,
                               Circle c2, String c2name) {

        if ( c1.equals(c2) ) {
            System.out.println(c1name + " equals " + c2name);
        } else {
            System.out.println(c1name + " doesn't equal " + c2name);
        }
    }
}
$ javac *.java
$ java -cp . Main
xx equals yy
yy equals zz
xx doesn't equal zz