CSE143 Notes for Friday, 4/9/21

I began by discussing the concept of interfaces. I started with a nonprogramming example. I mentioned that you can plug in many kinds of devices to a modern TV including a cable box, a DVD player, a VCR, a game console, etc. It doesn't make sense to have specific plugs for each kind of device. Instead these different devices are usually each HDMI-compliant source devices. This is an example of the interface concept in use. In fact, the "I" of HDMI stands for "Interface" (High-Definition Multimedia Interface). This provides a good example of where being less specific makes something more flexible. Instead of having a place to plug in a cable box and only a cable box, it is better to have a place to plug in any HDMI-compliant device (anything that satisfies the interface).

Then I used jGRASP to look at a programming example. I pointed out that I had available to us the ArrayIntList class that we talked about in the first week of the quarter. Soon we will discuss a different way of implementing such a list using a linked structure that we will refer to as a LinkedIntList. For now the only important thing to understand is that it is a second way of implementing a list. I purposely designed the two classes to have very similar methods. They each have:

The point is that these classes are similar in terms of what they can do by they are very different in how they do it.

To underscore the similarity, I wrote the following client code that does parallel operations on two different lists, adding three values, removing one, and printing it before and after the remove:

        public class ListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                list1.add(18);
                list1.add(27);
		list1.add(93);
		System.out.println(list1);
		list1.remove(1);
                System.out.println(list1);

                LinkedIntList list2 = new LinkedIntList();
                list2.add(18);
                list2.add(27);
		list2.add(93);
		System.out.println(list2);
		list2.remove(1);
                System.out.println(list2);
            }
        }
The program produced the following output:

        [18, 27, 93]
        [18, 93]
        [18, 27, 93]
        [18, 93]
As expected, the two kinds of list behave the same way. I pointed out that in CSE142 we tried to emphasize the idea that you shouldn't have redundant code like this. So ideally we'd like to move this code into a method. As a first attempt, I said:

        public class ListClient {
            public static void main(String[] args) {
                ArrayIntList list1 = new ArrayIntList();
                processList(list1);

                LinkedIntList list2 = new LinkedIntList();
                processList(list2);
            }

            public static void processList(ArrayIntList list) {
                list.add(18);
                list.add(27);
		list.add(93);
		System.out.println(list);
		list.remove(1);
                System.out.println(list);
            }
        }
This is obviously a better way to write this program, but it didn't compile. jGRASP highlighted the second call on processList and said:

        File: /Users/reges/143/ListClient.java  [line: 4]
        Error: processList(ArrayIntList) in ListClient cannot be applied to (LinkedIntList)
The error indicates that the method processList takes an ArrayIntList as an argument and that it cannot be applied to a call that passes a LinkedIntList as a parameter. So I tried changing the parameter type to LinkedIntList and then it produced an error for the other call.

The point is that we want to be able to think of these lists as being the same thing. In computer science we try to use abstraction to find what is common between these two classes even though we recognize that there are things that are quite different about the two. We would imagine an "integer list" abstraction of which these are two possible implementations. They're both the same in the sense that they provide basic "integer list" functionality like an appending add. But they are different in the sense that they are implemented quite differently (one using an array and the other using a linked list).

With Java, we have actual language support for this concept. Not only can we talk abstractly about an "integer list" abstraction, we can actually define it using what is known in Java as an "interface".

In an interface, we want to specify that certain behaviors exist without saying how they are implemented. We want to specify the "what" part without specifying the "how" part. So I went to the ArrayIntList class and deleted all of its comments and all of the method bodies. That left me with this:

        public class ArrayIntList {
            public int size()
            public int get(int index)
            public String toString()
            public int indexOf(int value)
            public void add(int value)
            public void add(int index, int value)
            public void remove(int index)
        }
To turn this into an interface, I had to change the name to something new. I decided to call it IntList. I also had to change the word "class" to "interface". The method have no curly braces because I deleted those lines of code. I replaced each of those with a semicolon. That left me with:

        public interface IntList {
            public int size();
            public int get(int index);
            public String toString();
            public int indexOf(int value);
            public void add(int value);
            public void add(int index, int value);
            public void remove(int index);
        }
This is how you define an interface. In place of the method bodies, we have a semicolon to indicate, "The implementation isn't given." You can think of an interface as being like a hollow radio. It has all of the knobs and buttons that are used to control the radio, but it has none of the "innards" that make it work.

So I went back to our code and changed the header for the processList method to use the interface instead:

        public static void processList(IntList list) {
Unfortunately, this led to two errors. Both of the calls now failed with messages like this:

        File: /Users/reges/143/ListClient.java  [line: 4]
        Error: processList(IntList) in ListClient cannot be applied to (ArrayIntList)
That seems a bit odd, because both ArrayIntList and LinkedIntList have the methods mentioned in the IntList interface. The explanation is that Java requires classes to explicitly state what interfaces they implement. So we had to modify the two classes to include this notation:

        public class ArrayIntList implements IntList {
             ...
        }
        
        public class LinkedIntList implements IntList {
             ...
        }
With this change, the code compiled and executed properly.

I then tried creating an instance of the IntList class:

        IntList list = new IntList();
This produced an error. Interfaces cannot be instantiated because they are incomplete.

I then asked people to think about the types of these objects. Consider our variable list1 from main:

        ArrayIntList list1 = new ArrayIntList();
This describes an object of type ArrayIntList, but it also describes an object of type IntList. That's why the method call works. The object is of more than one type. This idea is important in object oriented programming because Java objects can typically fill many roles. This is related to the notion of type because each role is defined by a new type. Interfaces are a way to define a new role, a new type. By saying that ArrayIntList and LinkedIntList both implement the IntList interface, we say that they both fill that role. So given an ArrayIntList, we can say that it is of type ArrayIntList, but we can also say that it is of type IntList. Similarly, a LinkedIntList is of type LinkedIntList and of type IntList.

As I have mentioned before, it is a good idea to use interfaces when defining the types of variables. I changed our main method to use type IntList for the variables instead of listing the individual types:

        public static void main(String[] args) {
            IntList list1 = new ArrayIntList();
            processList(list1);

            IntList list2 = new LinkedIntList();
            processList(list2);
        }
These variables are more flexible than the old variables. The variable list1, for example, can now refer to any IntList object, not just one of type ArrayIntList. In fact, list1 can even store a reference to other kinds of objects, as long as they implement the IntList interface.

Then I switched to talking about the next programming assignment. I first gave an example that involved constructing an array of objects. I used the example of constructing an array of Point objects. The Point class is part of the java.awt package that is used for graphics. It was also a primary example in the CSE142 class.

I began with this code:

        import java.awt.*;
        
        public class PointArray {
            public static void main(String[] args) {
                Point[] points;
            }
        }
I asked what kind of objects this program creates. The answer is that it doesn't create any objects. It defines a variable called points that is of type Point[], which means that it is capable of storing a reference to an array of Point objects. But if we want an actual array or some actual Point objects, we have to explicitly construct them.

So I added code to construct the array:

        Point[] points = new Point[5];
We used jGRASP to see what the program is doing and we saw that it constructs the array, but not any Point objects. When you work with an array of objects, you have to construct not just the array, but also every individual object.

Java initializes the array to the zero-equivalent for the type, which in the case of an array of objects means that Java initializes each array element to null:

           +--+     +---------+---------+---------+---------+---------+
    points | -+-->  |    /    |    /    |    /    |    /    |    /    |
           +--+     +---------+---------+---------+---------+---------+
                        [0]     [1]       [2]       [3]       [4]
It is a common convention to use a slash ("/") to represent null. To fill up this array, we had to write a loop that constructed individual Point objects that were stored in the array:

        for (int i = 0; i < points.length; i++) {
            points[i] = new Point(i, 2 * i + 1);
        }
This constructed 5 different Point objects:
                     +-------+ +-------+ +-------+ +-------+ +-------+
                     | x = 0 | | x = 1 | | x = 2 | | x = 3 | | x = 4 |
                     | y = 1 | | y = 3 | | y = 5 | | y = 7 | | y = 9 |
                     +-------+ +-------+ +-------+ +-------+ +-------+
                         ^         ^         ^         ^         ^
                         |         |         |         |         |
           +--+     +----+----+----+----+----+----+----+----+----+----+
    points | -+-->  |    *    |    *    |    *    |    *    |    *    |
           +--+     +---------+---------+---------+---------+---------+
                        [0]       [1]       [2]       [3]       [4]
I then asked how we could print each of the Point objects with a println. Someone mentioned that we could use a foreach loop:

        for (Point p : points) {
            System.out.println(p);
        }
This produced the following output:

        java.awt.Point[x=0,y=1]
        java.awt.Point[x=1,y=3]
        java.awt.Point[x=2,y=5]
        java.awt.Point[x=3,y=7]
        java.awt.Point[x=4,y=9]
Then I started discussing the programming assignment. The assignment involves simulating guitar strings and musical instruments built from those strings. Each instrument will be stored in its own class. You will be provided with a class called GuitarLite that has just two guitar strings and you will define a class called Guitar37 that has 37 strings.

I asked people to consider the issue of making our code more general. If we know that we are going to want to use different objects as our guitar, then how do we structure our code so that we can make minimal changes to the code? We don't want to write all of our code for one kind of guitar and then find that it doesn't work for another kind of guitar.

Someone mentioned that this is a good place to introduce an interface. To do so, we have to think about what are the behaviors we expect of a guitar object. We expect the guitar to have these methods:

This can be turned into an interface:

        public interface Guitar {
            public void playNote(int pitch);
            public boolean hasString(char string);
            public void pluck(char string);
            public double sample();
            public void tic();
            public int time();
        }
I mentioned that the first part of the assignment involves implement a GuitarString class that simulates a string that can be plucked. The writeup gives you all of the information about how the simulation works.

You are provided with a GuitarLite object that has two strings. Once you have a working GuitarString class, you can use the GuitarLite object to write some client code. I said it would be instructive for us to write some client code. I began by constructing a GuitarLite object:

        Guitar g = new GuitarLite();
Notice that the variable is of type Guitar using the interface. We only need to specify the specific type of Guitar object when we construct it.

The Guitar interface has methods for playing the guitar. You can play a note using the playNote method. The notes are specified using a chromatic scale where concert-A has the value 12. But you have to give more instructions to the guitar object than just to play the note. Telling it to play the note will pluck an appropriate string. But there are two important methods called sample and tic. The sample method returns the current sound information that we send to the sound card and the tic method advances the simulation. Most often you'll do these two things together, calling sample to play the sound and then calling tic. But they are distinct operations, so it's best to have them in separate methods. For example, you might want to mute the program so you might call tic to move time forward without calling sample.

This simulation involves sending information to the sound card at a rapid rate. We are using a class called StdAudio that has a constant called SAMPLE_RATE. It indicates that we are sampling 44,100 times a second. So that means we have to call sample and tic thousands of times just to get a fraction of a second of audio.

Then I mentioned that these Guitar objects also have a different interface for playing notes. Each Guitar object is allowed to introduce a mapping of characters to notes. For the GuitarLite object, the mapping is from "a" and "c" to concert-A and concert-C. The Guitar37 class has a more complex mapping that allows you to use the computer's keyboard to play it like a piano. I opened up a client program that is called GuitarHero. This is being provided to you. It is set up to play the GuitarLite guitar. I compiled it and ran it and I showed that it was able to play those two notes as I would hit the keys "a" and "c". Then I changed this line of code:

        Guitar g = new GuitarLite();
to be:
        Guitar g = new Guitar37();
and then I was able to play 37 different keys. I played a bad version of "Three Blind Mice." I spent the rest of the time discussing details about the homework that are included in the assignment writeup.


Stuart Reges
Last modified: Fri Apr 9 13:25:03 PDT 2021