☰ Contents Classes that Share Behavior

The Pict Library, Revisited

Prof: Are you finding some of this interface stuff a bit familiar? Does it remind you of something we were working with all the way back at the beginning?
Student: Yeah… when I was writing something like isPng.and(isBig) it felt very similar to writing tree.beside(sun). The Pict library we have been using, that uses interfaces?
Prof: It sure does! You’re right that it is similar, here is an example of using Pict like you just mentioned.
Student: I see, so when I use dot.beside(square), that is using a default method inside of the Pict interface?
Prof: Yep.
Student: Ohhh, and there must be a record called Beside that implements Pict. It’s all coming together now.
Prof: Here, I’ll write out a section of the Pict library so we can talk through it. I trimmed it down a little; the whole thing is in Pict.java.
interface Pict {
    int width();
    int height();

    default Pict beside(Pict other) { return new Beside(this, other); }
    default Pict above(Pict other) { return new Above(this, other); }
    // ... more default methods ...

    void draw(Graphics2D g, int x, int y);
}

record Circle(int radius, Color color) implements Pict {
    // ... plus constructors that accept a color name like "red" ...
    public int width() { return 2 * this.radius; }
    public int height() { return 2 * this.radius; }
    public void draw(Graphics2D g, int x, int y) {
        g.setColor(this.color);
        g.fillOval(x, y, 2 * this.radius, 2 * this.radius);
    }
}

record Beside(Pict left, Pict right) implements Pict {
    public int width() { return this.left.width() + this.right.width(); }
    public int height() { return Math.max(this.left.height(), this.right.height()); }
    public void draw(Graphics2D g, int x, int y) {
        int centerY = this.height() / 2;
        this.left.draw(g, x, y + centerY - this.left.height() / 2);
        this.right.draw(g, x + this.left.width(), y + centerY - this.right.height() / 2);
    }
}
Student: Ok, so Pict is an interface. And you gave it a width and height method. So all cases where we implement Pict will have a width and height as well.
Prof: Correct, what else do you notice?
Student: You used default just like we did with ImageQuery, but instead of using it for And, Not, Or, it’s being used for beside and above. Although I am a bit confused about the Graphics2D part? What is a Graphics2D? What are all these lines that say g mean?
Prof: For now let’s just treat Graphics2D as our way of actually drawing pixels on the screen. Pict describes what we want to draw, and Graphics2D is part of a library that handles the details of actually drawing it.
Student: So you are hiding things from me again?
Prof: Funnily enough, the creators of Graphics2D actually hid all this from me! When I wrote Pict, I didn’t have to think too much about how Graphics2D works. It abstracted away those details so I could focus on implementing all the shapes and relationships that I wanted Pict to have. Programmers rely on the work of others, especially when working on very complicated projects. We have also been using assertEquals() the same way, as code someone else wrote that we trust to do its job.
Student: This is a lot to keep track of.
Prof: Let’s just focus on Pict a bit more, and just treat the Graphics2D g section of the draw method as a canvas with a paint brush: when you hand it something, it draws it for you. We only use it inside draw, and only the simple records like Circle call Graphics2D methods directly. The records are what we are focusing on. They hold the data saying “a circle with a given radius and color” or “a circle, then a square beside it”. Pict is the type that lets all of them be used interchangeably. Notice that Beside holds two Picts, in the same way And held two ImageQuerys. The drawing part is just what allows us to see and verify the result visually, but all the data is defined by us.

Practice

Look at the draw method of Circle and the draw method of Beside above. Which statement is true?
  1. Beside.draw paints pixels by calling g.fillOval and g.fillRect directly.
    Beside never calls a Graphics2D method itself. It only calls draw on left and right.
  2. Circle.draw calls Graphics2D methods to paint itself, while Beside.draw hands the same g to the draw of its two parts, at different positions.
  3. A Circle stores its own pixels, so draw just copies them onto g.
    Circle holds only a radius and a color. The record is data, and draw is where it gets painted.
  4. Beside does not need a draw method, because the Pict interface supplies one.
    Every record that implements Pict must supply draw, because the interface declares it with no default.

Above is the vertical version of Beside. This is the draw method of Above from the Pict library, with one piece missing. The top picture is drawn at the top, so the bottom picture must start just below it. Which expression belongs in place of /* ??? */, the y argument of the second draw call?

record Above(Pict top, Pict bottom) implements Pict {
    public int width() { return Math.max(this.top.width(), this.bottom.width()); }
    public int height() { return this.top.height() + this.bottom.height(); }
    public void draw(Graphics2D g, int x, int y) {
        int centerX = this.width() / 2;
        this.top.draw(g, x + centerX - this.top.width() / 2, y);
        this.bottom.draw(g, x + centerX - this.bottom.width() / 2, /* ??? */);
    }
}
  1. y
    That is the same y as the top picture, so the bottom picture would be drawn on top of it.
  2. this.top.height()
    That ignores y, so the pictures would only line up when the Above is drawn at the very top of the canvas.
  3. y + this.top.height()
    The bottom picture starts where the top one ends: the starting y, plus the height of the top picture.
  4. y + this.bottom.height()
    That moves down by the height of the bottom picture itself, which leaves a gap the size of the bottom picture, and the top picture’s height never counts.