Our main topic today is a Java feature called "Interfaces". This topic is closely related to our discussion of object-oriented design from the preceding lectures. It is also, unfortunately, tangentially related to the topic of the next few classes, Java User interface mechanisms. That is, while this topic "Interfaces" and the next topic "User Interfaces" share part of their name, the connection between the two topics is not very strong.
As we hinted, however, there is a strong connecting between Interfaces and object-oriented design.
In our discussion of design, we saw that an important goal was to hide the details of how the behavior of a class was implemented by keeping its instance variables private and then defining public methods that provided the ability to change or determine a limited set of aspects of the object's state.
The collection of operations/methods that can be used from outside a class to interact with an object of the class is called the interface to the class. This explains why the topics "Interfaces" and "User Interfaces" have related names. "User Interfaces" are the mechanisms through which users can interact with the state of an entire program. "Interfaces" are the mechanisms through which one component (object) within a program can interact with another.
The interface of objects of a Java class can be concisely described by listing all of its methods and describing their parameters. That is why we ask you to include such informations in your designs.
Your designs are (quite deliberately) not actual Java code. It turns out, however, to be quite useful to write a description of a classes interface in a form that is interpretable as Java code by the computer. Accordingly, Java's rules of syntax permit such a specification.
To illustrate the use of this new Java mechanism we will use the Tshirt class from our laundry sorter as an example.
The form of an interface specification is quite simple:
public interface Laundry { ... }
For example, here is an interface specification for the properties of the Tshirts used in our laundry sorter program.
// A piece of laundry of an arbitrary shape. public interface Laundry { // Move the laundry relative to its current position // Parameter: // xOffset - distance to move horizontally // yOffset - distance to move vertically public void move( double xOffset, double yOffset ); // Move the laundry to a specific position: // x - x coordinate // y - y coordinate public void moveTo( double x, double y ); // Remove the laundry from the display public void hide (); // Return true if the point is in the laundry // Parameters: // pt - the point to test for containment public boolean contains( Location pt); // Return the sum of the redness, blueness, // and greenness of the laundry item. public int colorValue( ); }
With this interface included in our program, we can now construct a more advanced version of the laundry sorter.
We hope that you noticed that, thanks to the use of a class definition, we had to change very little when we converted our initial laundry program (where the laundry items were just FilledRects) into our current, fancy Tshirt version. Basically, the only change required was that the declaration:
FilledRect item;had to be replaced by a declaration of the form:
Tshirt item;and to change the constructor used.
The code that manipulated the laundry (i.e. onMouseDrag, etc.) remained unchanged. We didn't have a precise way of explaining why at the time, but now we do. FilledRects and Tshirts provide a similar interface. That is, they both provide all the methods we need for this program (including, move, moveTo, contains, etc.)
Interfaces let us write even more flexible code.
Suppose we have two classes of laundry defined. Tshirts (as before) and something else like Pants. If they both contains all the methods listed in our Laundry interface, we can explicitly tell Java this in the headers of the class declarations:
public class Tshirt implements Laundry ...
So long as we restrict ourselves to the methods defined by the Laundry interface, it doesn't matter what type of laundry we come up with in the future (maybe we want to add Shorts when it gets warmer), we can continue to use any program that uses the Laundry interface without changing it!
To tell Java that we want to use objects that implement the Laundry interface in our laundry sorter, we can replace the declaration of the item variable in the window controller with a declaration of the form:
Laundry item;
When we write a declaration, remember that we are telling Java what sort of object the name may be associated with. The declaration "Laundry item" tells Java that item may be associated with an object of any class that implements the Laundry interface.
So, Java will allows us to say either:
item = new Tshirt( ... );or
item = new Pants( ... );
On the other hand, java will not allow us to say:
item = new Laundry( ... );
This makes a lot of sense. In the specification of the interface Laundry, we did not include a constructor. That is because in order to construct an object, we need to know the details of how its state will be represented. The whole point of an interface, however, is to focus on the interface through which the state can be manipulated while ignoring the details of how the state is represented.
On the other hand, given that Java knows that "item" will only be associated with things that implement the Laundry interface, it will let us write commands like:
item.moveTo(...);
Note that Java will not allow us to assign any object with the right methods to the "item" variable. In particular, even if the methods in the Laundry interface were a subset of those provided by FilledRects, we could not say:
item = new FilledRect( ... );since the declaration of the class FilledRect does not say:
public class FilledRect implements Laundry { }
Demo: Laundry Sorter using interfaces
Also, note that we can use interface names as type names in parameter declarations.
As your labs become more complicated and your knowledge of the syntax of Java grows, you will discover that the type of errors you spend most of your time dealing with will change. The problems caused by errors involving the rules of Java grammar will decrease and the problems caused by subtle logical difference between what you told the computer to do and what you meant to tell it will increase.
The process of removing such logical errors is called debugging. In recognition of its growing importance we want to give you three tips on how to approach it. The first is a way to avoid complicated debugging problems in the first place, the second is a deep, guiding principle that us useful in debugging, and the third is a simple but very handy practical technique.
Compile and test your code after adding small parts. Never wait until you have typed in several long methods before you start testing. This will make finding syntax errors easier, and often can give clues to potential logical errors while the problem is still reasonably small. If you do this often, you will know what you have changed recently, which helps make the next item easier.
The minute your program misfunctions, you will probably have some idea what is wrong. Suppose for example, that while you are working on Boxball, you start out by trying to write the code for the WindowController extension first (saving the Box and Ball for later). You would eventually reach a point where the game rectangle, the "drop" line and the level buttons were all drawn on the screen and the code to make the "easy", "medium" and "hard" buttons move the drop line was written. Suppose, however, that when you click on the buttons, the line does not move.
You might attack the problem by carefully rereading the code you have written. This is a good idea, but it is often ineffective. When we write incorrect code, the error often springs from a basic misunderstanding of how our instructions will be interpreted. When we reread them, we stick with our misunderstanding and conclude that they should work as we had intended. In order to escape our misunderstandings, we have to uncover irrefutable proof that some small section of our code is just not doing what we expected. Then, we will have to admit that something about our understanding was wrong.
The trick is to find the small section of code that will force one to recognize the mistake quickly.
In the case of the Boxball example, there are several sorts of mistakes that could cause the problem.
This is too many possibilities. As long as there are so many possibilities, the debugger will tend, while looking at the code related to one possibility, to overlook the error and assume on of the other possibilities must be at fault. Narrowing things down to a single possibility is generally the best way to force oneself to admit a misunderstanding.
This brings us to the simple technique...
Consider the BoxBall problem again. How can we narrow the search for the error down from several possibilities to just one? The answer is to try to rule out one possibility at a time.
In this example, the easiest possibility to eliminate is the third -- that the correct "move" instruction is being executed but the wrong object is being moved. How can we tell if this is the case? One approach is to add another line to our program which a) will produce some visible output, and b) will be executed exactly when the move is executed. Item (b) can usually be accomplished by putting the two lines right next to one another in the program.
Producing visible output isn't too hard either. We could construct a FilledRect or a Text. An even easier thing to do, however is to simply ask Java to print a line of text. Java has a separate window from the "canvas" in which such text appears. Each line displayed is just added to the bottom of the window. You don't have to specify a Location or a canvas.
The syntax of the command used to display such text is:
System.out.println("Move the line");
To make this concrete, consider the following (very incorrect) attempt to write the button handling code.
public void onMouseClick(Location point) //checks to see if the click is inside the court if (court.contains (point)); // Make sure the user clicked above the starting line if (point.getY() < clickHeight); // create a ball // Check if the user clicked on a button if (hard.contains(point)); clickHeight = HARD_HEIGHT; startingLine.moveTo (COURT_LEFT, clickHeight); if (medium.contains(point)); clickHeight = MEDIUM_HEIGHT; startingLine.moveTo (COURT_LEFT, clickHeight); if (easy.contains(point)); clickHeight = EASY_HEIGHT; startingLine.moveTo (COURT_LEFT, clickHeight);
To begin to track down the problem with this program, we could add a simple "println" to the if that is supposed to handle a click on the "HARD" button:
if (hard.contains(point)); System.out.println("Moving to HARD level"); clickHeight = HARD_HEIGHT; startingLine.moveTo (COURT_LEFT, clickHeight);
and then run the program and click on the HARD button to see what happens.
If you do this, you will discover that a new window appears on the screen containing the message "Moving to HARD level". This is proof that the program did get to the move instruction as desired. Now, we get to work on eliminating other possibilities.
For example, the value of clickHeight might be wrong. we can investigate this option by chaning the println to:
System.out.println("Moving to height " + clickHeight);Now, the message displayed will include the value of clickHeight.
Be prepared to use this trick while working on upcoming labs. It can be quite helpful.