On this page:
Overview
9.1 Combining Searches, Part II
9.2 abstracting Common Method Implementations
9.3 abstracting Common Fields
9.4 Talking About abstract Classes
9.5 Summary
6.8

Lecture 11: Classes that Share Fields and Code

Related files:
  ExamplesSearch.java  

Overview
9.1 Combining Searches, Part II

In the last section we saw how we could combine search queries together using combiner classes like AndQuery and OrQuery. This let us write queries like:

class ExamplesSearch {
ImageData i1 = new ImageData("ucsd cse computer science", "png", 600, 400);
ImageData i2 = new ImageData("data science ai artificial intelligence", "png", 500, 400);
ImageQuery lg1 = new LargerThan(600, 400);
ImageQuery me1 = new MatchesExtension("png");
ImageQuery ck1 = new ContainsKeyword("ucsd");
ImageQuery all3 = new AndQuery(new AndQuery(this.lg1, this.me1), this.ck1);
boolean testAnd(Tester t) {
return t.checkExpect(this.all3.matches(i1), true) &&
t.checkExpect(this.all3.matches(i2), false);
}
}

One thing that gets a bit verbose here is the construction of the combiners. If we make large queries, we end up repeating new AndQuery quite a few times, which is a lot of typing:

class ExamplesSearch {
ImageQuery lg1 = new LargerThan(600, 400);
ImageQuery me1 = new MatchesExtension("png");
ImageQuery ck1 = new ContainsKeyword("ucsd");
ImageQuery ck2 = new ContainsKeyword("data");
ImageQuery ck3 = new ContainsKeyword("science");
ImageQuery all5 = new AndQuery(new AndQuery(this.lg1, this.me1), new AndQuery(this.ck1, new AndQuery(this.ck2, this.ck3)));
}

While this isn’t unusable, it’s often nice to find ways to shorten uses like these. One idea is to have a method, called and, defined on the various ImageQuery classes that takes another ImageQuery and produces a new AndQuery of the two. If that were defined, we could shorten the long line above to:

ImageQuery all5 = this.lg1.and(this.me1).and(this.ck1).and(this.ck2).and(this.ck3);

Subjectively, I find this quite a bit more readable than the example above, as well. So in LargerThan, we’d write:

class LargerThan implements ImageQuery {
...
public ImageQuery and(ImageQuery other) {
return new AndQuery(this, other);
}
}

Of course, just implementing this in one place isn’t sufficient. We need to add this to all the classes that we want to use and on;

class MatchesExtension implements ImageQuery {
...
public ImageQuery and(ImageQuery other) {
return new AndQuery(this, other);
}
}
class ContainsKeyword implements ImageQuery {
...
public ImageQuery and(ImageQuery other) {
return new AndQuery(this, other);
}
}
class AndQuery implements ImageQuery {
...
public ImageQuery and(ImageQuery other) {
return new AndQuery(this, other);
}
}

This still isn’t quite enough to make the above example run.

Do Now!

What error will we get if we put the add method in all of these classes and try to run the example using and above?

Since all the names in the example above have type ImageQuery—the interface type—Java won’t let us use the and method on them. We have to first declare that method as part of the interface:

interface ImageQuery { boolean matches(ImageData id); ImageQuery and(ImageQuery other); }

Now the example above will run, and we can use this idea of the and method to shrink our code that uses the query combiners. To do so, we paid a particular cost – we had to put the and method in each of the ImageQuery-implementing classes. This is a bunch of repeated work. It might be worth it if we have hundreds of lines of code that make use of this to be more readable and usable, but it’s still annoying to repeat code when it is exactly the same across classes.

9.2 abstracting Common Method Implementations

Java, and many other languages that support objects and classes, has a feature for precisely this situation, where multiple classes could share an implementation of a method. Note that this situation is more specific than the situation we identified with interfaces, where multiple classes shared a common method header with different implementations. Here, the method header and implementation—the entire method definition—is the same across these classes.

When this situation comes up, we can declare an abstract class that contains the shared implementations. An abstract class is written like a class with abstract at the beginning, and it lists fields and methods (we’ll just use methods for now). Methods can also be started with abstract, which means they don’t have a shared implementation, or they can be written out normally. Here’s an abstract class that defines the and method as before, and has an abstract definition of the matches method:

Note that we declared that the ImageQuery abstract class implements the ImageQuery interface.

abstract class AQuery implements ImageQuery {
public ImageQuery and(ImageQuery other) {
return new AndQuery(this, other);
}
}

We can now use a new keyword, extends, to make each of the individual query classes get access to the single implementation of and in AQuery, and remove their individual definitions of it. So LargerThan becomes:

class LargerThan extends AQuery {
... just the fields, constructors, and match, no and method ...
}

We’d add the same extends AImageQuery clause to each query class, which allows us to remove the identical copies of the and method, and just use the one in AImageQuery. This lets us write the add method only once, but get its benefits for all the classes that extend the AImageQuery class.

9.3 abstracting Common Fields

Consider the class AndQuery and the related class OrQuery:

class AndQuery extends AQuery {
ImageQuery q1, q2;
AndQuery(ImageQuery q1, ImageQuery q2) {
this.q1 = q1;
this.q2 = q2;
}
public boolean matches(ImageData id) {
return this.q1.matches(id) && this.q2.matches(id);
}
}
class OrQuery extends AQuery {
ImageQuery q1, q2;
OrQuery(ImageQuery q1, ImageQuery q2) {
this.q1 = q1;
this.q2 = q2;
}
public boolean matches(ImageData id) {
return this.q1.matches(id) || this.q2.matches(id);
}
}

These two classes share a lot of code. For example, they have the exact same two field definitions (q1 and q2), and the constructors are identical aside from their names. Similar to the case above, where we identified completely shared method definitions, here we’ve idenfieid completely shared field and constructor definitions. In this case again, we can “abstract out” the shared behavior into an abstract class:

abstract class AComboQuery extends AQuery {
ImageQuery q1, q2;
AComboQuery(ImageQuery q1, ImageQuery q2) {
this.q1 = q1;
this.q2 = q2;
}
}
class AndQuery extends AComboQuery {
AndQuery(ImageQuery q1, ImageQuery q2) {
super(q1, q2);
}
public boolean matches(ImageData id) {
return this.q1.matches(id) && this.q2.matches(id);
}
}
class OrQuery extends AComboQuery {
OrQuery(ImageQuery q1, ImageQuery q2) {
super(q1, q2);
}
public boolean matches(ImageData id) {
return this.q1.matches(id) || this.q2.matches(id);
}
}

We may see other uses of super outside of constructors in the future.

This introduces a new keyword, super. Within constructors, super means “call the constructor of the class that this one extends.” So when the constructor for AndQuery runs, it will immediately call the constructor in the AComboQuery abstract class, since AndQuery extends it. The other new behavior we see here is that all the fields declared in AComboQuery are present on AndQuery and OrQuery objects – this was the point of moving them into a shared abstract class so we could write them once and use them in both places.

9.4 Talking About abstract Classes

Some definitions are useful at this point:

It’s useful to use pictures to describe the class hierarchy. These pictures are distinct from the memory diagrams that include the stack and heap – they talk about the relationship between classes. For this example, we’d use the following class hierarchy diagram to describe it:

This captures the relationships between these classes by drawing an extends arrow between each pair of classes where one extends the other, and an implements arrow when a class implements another.

This picture helps us understand what methods and fields are available on a particular class. For example, if we see a field access like this.q1, and know that this is a reference to an AndQuery object, we can use the diagram to trace the extends relationships to find that the q1 field is defined on its parent class. And in a method call like this.and(someOtherQuery), where this is a reference to an object of type LongerThan, we can trace the extends relationship to find the and method on AQuery.

9.5 Summary

We saw that: