Arrays |
Note that the chapter on arrays from the Java version of our text is available on-line at
http://eventfuljava.cs.williams.edu/chapters/Bruce_chapter14.pdf
It goes into much more
detail than we can here.
Java has a class ArrayLIst<T> that is similar to Grace's lists. However, it also has a more primitive data structure, arrays, that form the building blocks of ArrayList. Here we are going to focus on arrays as it will provide you good intuition on the complexity of list operations. You can look up information about ArrayList is the documentation on Java libraries.
Arrays differ from Grace's list in several important ways. The most important is that arrays have a fixed size that is determined when the array is created. Another is that the operations on an array are limited. You can ask for an element at a given location, update an element at a location, or ask the array for the number of slots it has. However, more complex operations must be hand-coded by the programmer. Finally, the elements of an array are indexed starting at 0 rather than 1 like Grace.
Array types and expressions in Java use a special syntax. If T is a type then T[] is the type of arrays holding elements of type T. Similarly, a new array of T of size n is constructed by writing new T[n]. Thus we can declare and initialize an array a of double with 20 slots by writing:
double[] a = new double[20]
We access the elements of an array using a similar notation to refer to individual elements. Thus we can obtain the values of the first three elements of the array a by writing a[0], a[1], and a[2]. (Remember that we start numbering the array elements at 0, not 1.)
We can update the elements using a similar notation:
a[0] = 7.47
(We note here that Grace also allows programmers to access and update list elements using a similar notation. Where we wrote b.at(5), we could obtain the same value using b[5]. Where we updated a value by writing b.at(3)put(3.12), we could also have written b[3] := 3.12. However, we preferred the more traditional method request style because it made it clear that these accesses were all via method requests.)
Because you must allocate all the space for an array when you create it, very often the number of active elements in an array is smaller than the total number of slots. If a is an array, then a.length will return the total number of slots in the array. Thus if a.length returns 20, then the array has elements a[0] through a[19] (because we start counting at 0, the last slot is always at one less than the length). By the way, notice that there are no open and closed parentheses after the method request of length. That is because length is a public instance variable of the array class. We think that is terrible style (if you want an instance variable to be public then you should write a method that returns its value - in Grace when you declare a variable to be readable, the system automatically generates such a method for you - and if you declare it to be public the system generates both "getter" and "setter" methods.
The fact that length returns the number of slots as opposed to the number in use makes the programmer's task more complicated. For example, think back to the song class in our Simon program in Grace. Here is a copy of the first part of my song class (omitting some of the methods, for example the play method).
class song.from(buttonList: List<NoisyButton>) -> Song { // list of notes (buttons) in song var noteList: List<NoisyButton> // which note the user is about to play var currentNote: Number // create a new song newSong // randomly select one of the buttons method randomNote -> NoisyButton is confidential { // randomly pick a button from buttonList -- code elided } // create a new song with one note method newSong -> Done { noteList := list.with<NoisyButton>(randomNote) currentNote := 1 } // Add a note to the song method addNote -> Done { noteList.add(randomNote) currentNote := 1 } // what button should user play next? method expectedButton -> NoisyButton { noteList.at(currentNote) } // Move on to next note in song method advanceToNext -> Done { currentNote := currentNote + 1 } // Has last note been played? method atEnd -> Boolean { currentNote == noteList.size }
Let's see how we would have to write this in Java:
public class Song { // the maximum possible length of a song private static final int MAX_SONG = 100; // array of notes (buttons) in song private NoisyButton[] theSong; // which note the user is about to play private int currentNote; // the length of the current song private int length; private ButtonPanel theButtons; // "notes" that can be selected for song // creates an initial song of one note. public Song(ButtonPanel someButtons) { // save the parameter in an instance variable theButtons = someButtons; // create a new short song theSong = new NoisyButton[MAX_SONG]; makeNewSong(); } // creates a new song of one note. public void makeNewSong() { length = 0; current = 0; addNote(); } // adds a note to the end of the song. public void addNote() { if (length < MAX_SONG) { theSong[length] = theButtons.getRandomButton(); length++; current = 0; } } // returns the current note in the song. public NoisyButton getExpected() { return theSong[current]; } // moves to the next note of the song. public void next() { if (current < MAX_SONG - 1) current++; } // returns whether currently at the end of the existing song. public boolean atEnd() { return (current == length - 1); } }
Let's make some comparisons between the code. We start with those directly relevant to arrays and lists.
In the Java version, the array theSong was declared as an array of NoisyButton. The program also includes the constant MAX_SONG which will be the size of the array. It also includes the declarations of the two instance variables currentNote and length, referring to where we are in the song and how many notes are currently in the song.
In the Grace version, we declare theSong as a list of NoisyButton, but don't need a constant for the maximum size of the list or a variable length for the current length of the song. We can always as theSong for its size if we want to know how many elements are currently in the song. Similarly, we do not have to specify a maximum size of a song as the list will expand to be as large as is needed.
In Java the array is created in the constructor. It has size MAX_SONG, then makeNewSong is requested to initialize the song to have exactly one note. Looking at the method makeNewSong, we see that it sets length and current to 0 (remember the 0th element is the first in an array) and then calls addNote to add the first note and bump up the length by one.
In Grace, the class initialization code in the class body calls newSong, which creates a new array with one button in it and then sets currrentNote to 1 (the first element in a list).
The Java constructor takes someButtons as a parameter. This can be used to select random buttons for the song, just as the class parameter buttonList permits the selection of random buttons (the code doing the selection is omitted in both cases). A major difference between Java and Grace is that the parameter to the constructor in Java is only visible in the constructor code, just as parameters are for methods. As a result if a constructor parameter is needed in other methods, then it will need to be stored in an instance variable. Thus we see the extra code in the constructor to save someButtons in the instance variable theButtons. This is not necessary in Grace, because the parameter to the class is visible throughout the entire class. Thus there is no instance variable needed to store the parameter buttonList in Grace. All class parameters are visible throughout their class definition.
Notice that we did not need to store the constructor parameters in our Tshirt example because they were only used in the body of the constructor. Only declare instance variables to store constructor parameters if they are needed in other methods of the class.
As an aside, some programmers like to use the same name for the constructor parameter as for the instance variable. Java allows you to do that if you prefix the name of the instance variable by this. Thus if we had used theButtons as the name of the parameter to the constructor then we could initialize the instance variable by writing this.theButton = theButtons. The occurrence of theButtons on the right side of the assignment corresponds to the parameter because it is the nearest declaration, while writing this.theButton makes it clear that the programmer is referring to the theButton the is part of the object, and parameters are not part of an object in Java.
Let's now look at addNote. In Java, it must first check that there is space for the new element, add it in the first empty slot (do you see why theSong[length] is the first empty slot?), increase length, and then reset current to 0. In Grace, we can use the add method to add the new note to the end of the list, and reset currentNote to 1 (the beginning slot in the list). We do not have to worry about whether their is space for it (there always will be), or change length (as noteList.size will always keep track of it).
The other corresponding methods are pretty similar to each other. However, note that advanceToNext in Java requires a test to ensure that we are not off the end of the array, while atEnd's test for the end is a bit more intuitive in Grace because the last element is at noteList.size.
Hopefully this comparison helped you understand the differences between lists in Grace and arrays in Java. But let's now take a look at how we write some of the methods for lists in Grace that are not available in Java for arrays.
Let's look first at adding a new element to an array. For simplicity, let's assume that we are working with an array seq of type SomeThing and instance variable size keeps track of the number of elements in seq that are in use. We assume that size <= seq.length.
Adding a new element to the end of seq is pretty straightforward:
public void addLast(SomeThing newElt) { if (size < seq. length) { seq[size] = newElt; size++; }
However, adding an element to the beginning of a list is much trickier. To add the new element we need to make a space by shifting all of the other elements over by 1.
public void addFirst(SomeThing newElt) { if (size < seq. length) { // shift over existing elements for (int index = size-1; index >= 0; index--) { seq[index+1] = seq[index] } seq[0] = newElt; size++; }
Exercise Why do we shift the last element to the right first, rather than shifting the element at 0 first?
Exercise Explain in detail what would happen if you made the horrible mistake of replacing the +1 in the assignment statement in the forloop by.
seq[index++] = seq[index]
Explain why these horrible things happened and write on the board 100 times "I will never use ++ in the middle of an expression!".
To polish these skills let's look back at the DrawingList program in Grace and the corresponding program DrawingArray in Java. Here is a link to the Grace program. Take a quick look over it to refresh your memory and now we'll take a look at the equivalent Java program.
import java.awt.*; import javax.swing.*; import objectdraw.*; public class Drawing extends WindowController { // max number of objects to be displayed private static final int SIZE = 100; private static final int MAX_OBJECTS = 20; // menus for shape, color, and command private JComboBox<String> shapeChoice; private JComboBox<String> colorChoice; private JComboBox<String> commandChoice; // Array of objects on screen. private DrawableInterface[] shapes = new DrawableInterface[MAX_OBJECTS]; // number of objects on screen private int numShapes = 0; // item currently selected for dragging private DrawableInterface selected; // mouse loc'n when last handled mouse private Location lastPoint; /** * Set up GUI components for program */ public void begin() { // create panel to hold choice buttons JPanel menuPanel = new JPanel(); // menu for selecting or adding commandChoice = new JComboBox<String>(); commandChoice.addItem("Add new item"); commandChoice.addItem ("Recolor item"); commandChoice.addItem("Move item"); commandChoice.addItem("Delete item"); menuPanel.add(commandChoice); // Set up menu for shapes shapeChoice = new JComboBox<String>(); shapeChoice.addItem("Circle"); shapeChoice.addItem("Square"); menuPanel.add(shapeChoice); // Set up menu for colors colorChoice = new JComboBox<String>(); colorChoice.addItem("Red"); colorChoice.addItem("Green"); colorChoice.addItem("Blue"); menuPanel.add(colorChoice); // Add the panel to screen getContentPane().add(menuPanel, BorderLayout.SOUTH); validate(); } /** * When the user clicks in the canvas, check the settings of the command * menu to determine what action to take. */ public void onMousePress(Location point) { selected = null; // indicate nothing currently selected Object buttonLabel = commandChoice.getSelectedItem(); if (buttonLabel.equals("Add new item")) { addNew(point); } else if (buttonLabel.equals("Recolor item")) { recolorShapeAt (point); } else if (buttonLabel.equals("Move item")) { selectShapeAt(point); } else { deleteShapeAt(point); } } /** * This method implements the "Add new item" command. Add new geometric * shape where clicked. Type and color of object is determined by the * settings of the color and shape menus. */ private void addNew(Location point) { // only add if still room for more objects if (numShapes < MAX_OBJECTS) { Location centeredLocation = new Location (point.getX() - SIZE/2, point.getY() - SIZE/2); Object shapeString = shapeChoice.getSelectedItem(); DrawableInterface newShape; // create new object to be shape chosen if (shapeString.equals("Square")) { newShape = new FilledRect(centeredLocation, SIZE, SIZE, canvas); } else { newShape = new FilledOval(centeredLocation, SIZE, SIZE, canvas); } newShape.setColor(getSelectedColor()); shapes[numShapes] = newShape; numShapes++; } } /** * @return the color corresponding to the string selected in the color menu. */ private Color getSelectedColor() { Color objectColor; // local variable - color of object // get color showing in the color menu Object colorString = colorChoice.getSelectedItem(); // set objectColor to chosen color if (colorString.equals("Red")) { objectColor = Color.RED; } else if (colorString.equals("Green")) { objectColor = Color.GREEN; } else { objectColor = Color.BLUE; } return objectColor; } /** * Change the color of the item the user clicks on. * This implements the "Recolor Item" command. */ private void recolorShapeAt(Location point) { int selectIndex = getIndexOf(point); if (selectIndex != -1) { shapes[selectIndex].setColor(getSelectedColor()); } } /** * @param point location of interest * @return the index of the last element of shapes containing point * return -1 if no element of shapes contains point */ private int getIndexOf(Location point) { // Walk the array until we find the selected shape for (int selectIndex = numShapes - 1; selectIndex >= 0; selectIndex--) { if (shapes[selectIndex].contains(point)) { return selectIndex; } } return -1; } /** * Remove top-most geometric item clicked in. If didn't click in any * then don't do anything. * @param point location where user clicked. */ private void deleteShapeAt(Location point) { int selectIndex = getIndexOf(point); // if point is in one of the objects, delete it if (selectIndex != -1) { shapes[selectIndex].removeFromCanvas(); removeEltWithIndex(selectIndex); shapes[numShapes] = null; } } /** * remove shapes[index] by moving later elements back. * @param index: element to be removed */ private void removeEltWithIndex(int index) { for (int objNum = index; objNum < numShapes - 1; objNum++) { shapes[objNum] = shapes[objNum + 1]; } numShapes--; } /** * Set select to indicate top-most geometric item clicked in, and * send it to front of screen. If didn't click in any then * set select to null. * Update lastPoint to last place clicked so can drag it * @param point: where user clicked */ private void selectShapeAt(Location point) { int selectIndex = getIndexOf(point); // Remember which shape is selected so onMouseDrag can move it. if (selectIndex != -1) { selected = shapes[selectIndex]; lastPoint = point; // Move the selected object to the front of the display // and to the end of the array if it is not already there if (selectIndex != numShapes-1) { selected.sendToFront(); removeEltWithIndex(selectIndex); shapes[numShapes] = selected; numShapes++; } } } /** * If something was selected then drag it and remember where left off */ public void onMouseDrag(Location point) { if (selected != null) { selected.move( point.getX() - lastPoint.getX(), point.getY() - lastPoint.getY()); lastPoint = point; } } public static void main(String[] args) { new Drawing().startController(400,400); } }
Let's talk through this program. For now, I'm going to skip the GUI component set up, which is handled in the begin method. Instead just trust that it also creates and installs three pop-up menus (called JComboBoxes in Java) at the bottom of the window, below the canvas.
The identifier shapes is declared as an instance variable that holds values of type DrawableInterface, an interface that is satisfied by all of the graphical objects in the objectdraw library. It is initialized to hold 20 objects. When the array is created the 20 slots all hold the value null, which represents an uninitialized value. You can test a value to see if it is null (e.g., shapes[4] == null), but if you send a method request to null, you will get a null pointer error, which generally happens when you forget to initialize a value.
Other instance variables keep track of the number of shapes on the screen (numShapes), recall the item currently selected for dragging (if any), and the location where the mouse was when the last event was triggered (lastPoint.
The onMousePress method determines what action is to be taken by checking the item selected in the commandChoice test field. The actual actions taken are the responsibility of the methods addNew. recolorShapeAt, selectShapeAt, and deleteShapeAt.
The method addNew creates a new geometric object on the screen, guided by the values selected in the pop-up menus. Based on what is showing on the shapeChoice menu, it will create a square or circle. It will then set its color according to the value returned by method getSelectedColor(), which returns the color corresponding to the string showing on the colorChoice menu. Once the object has been created and its color set, it is added to the shapes array at the end, and numShapes is incremented.
The method getIndexOf is a helper method (and is hence declared to be private). It determines the index of the top-most element in shapes that contains a point. It is very similar to the corresponding Grace method indexOf. They differ only in the syntax of the for loop.
We now have several private methods to help us write the methods to change color, delete, and move objects. Method getSelectedColor just grabs the text showing on the color menu and using an if-else-if-else returns the corresponding color. We'll talk about getting strings from menus later.
The method getIndexOf is a helper method (and is hence declared to be private). It determines the index of the top-most element in shapes that contains a point. It is very similar to the corresponding Grace method indexOf. They differ only in the syntax of the for loop.
Method recolorShapeAt uses getIndexOf to determine what object was clicked on. If there was on object clicked on then its color is changed. Method deleteShapeAt is similar, but with one twist. The selected element is removed from the canvas, but now we must remove it from the array. Unfortunately, there is no remove method for arrays (in fact, only length is defined). Thus we have to write our won. This is done in method removeEltWithIndex. The idea here is that we start with the element one to the right of the one we wish to remove and shove the elements to the left by one slot. Hence the assignment shapes[objNum] = shapes[objNum + 1]; that goes through all the elements from index to the end. After all the elements have been shifted, we cut back the number of elements by 1.
Method selectShapeAt finds the object selected, moves it to the front of the screen and then moves it in the array to the last element (which makes it the top element in terms of searches). The element is moved to the last element of the array by first removing it from the array (leaving the array one element shorter) and then adding it as the new last element (which is easy) and bumping up numShapes back to the original value.
Method onMouseDrag is as before, except that the name of the method to shift an element is move rather than moveBy.
Arrays |