Sep 23: Finish Image Processing. Arrays. Interfaces and Polymorphism
Code discussed in lecture
The part of these notes on arrays is a modified version of notes from CS 5. I will go over the highlights in class, but the details will be here for later study.
Arrays
You can think of an array like a table. You give an index and get or store an element. For example, here is a table of winners of the Preakness (the middle of the three races in horse racing's Triple Crown) in the 1950s, indexed by the year of the race:
Year (index) | Winner (element) |
---|---|
1950 | Hill Prince |
1951 | Bold |
1952 | Blue Man |
1953 | Native Dancer |
1954 | Hasty Road |
1955 | Nashua |
1956 | Fabius |
1957 | Bold Ruler |
1958 | Tim Tam |
1959 | Royal Orbit |
There is one difference between this table and arrays in Java: in Java, array indices always start at 0. So array indices always go as 0, 1, 2, 3, 4, … . (Java is different from languages such as Pascal, where you can select a lower bound other than 0. In C/C++, indices always start at 0, just as in Java.)
Then again, just because indices start at 0, that doesn't mean you always have to use all the entries starting from 0. If you'd rather start from 1, go ahead. The cost is just the wasted space of the 0-indexed entry.
Although this example has character strings (the names of the winning ponies) as its entries, we can store any type in the array, as long as all entries are declared as the same type. So for starters, our array entries will be something simpler, such as integers.
To declare an array of 10 int
s you have to do two steps:
- Create a variable that can hold a reference to an array of the proper type.
- Create the actual array by using
new
.
Here,
-
int
gives the type of each array element. The empty square brackets say that we have a reference to an array. Think of the whole notationint []
as saying "array ofint
s." (By the way, it doesn't matter how much space you leave betweenint
and[]
, so you could writeint[] bozo
if you wanted to.) -
bozo
gives the name of the variable that stores a reference to the array. -
int[10]
says how many elements are in the array.
bozo[0]
,
bozo[1]
, bozo[2]
, …,
bozo[9]
. Note that An array of n elements has elements indexed from 0 to n–1, only. An array of n elements does not have an element with index n.It will soon become natural to start counting from 0.
You can think of array indices as being like subscripts. Our array
bozo
is like having bozo
0,
bozo
1, bozo
2,
…, bozo
9.
Technically, since an array is an object, we should not say that
bozo
is an array; we should say that bozo
is
a reference to an array. That would become incredibly
tedious, however, and so we shall sometimes just say that
bozo
is an array. But you should understand that arrays
are always objects, and that when we declare a variable to be
an array, it's really a reference to an array.
You can make arrays of other sizes and of other types. To make an
array blap
of 30 double
s:
Array elements can be of any type. Even objects. Even other arrays!
Our first example of an array is in ShortList.java. We have a 10-element array
named list
. The first for-loop assigns the value
i+1
to list[i]
for i
= 0, 1,
…, 9. For example, list[5]
is assigned the
value 6.
Note how we set up the first for-loop:
N
-element array. We start at 0 and continue as long as
the index is strictly less than N
. As
soon as it hits N
, we are out of bounds of the legal
array indices, so we must stop. In other words, the loop test i
<= N
would be wrong, because within the loop body, we would be
trying to access list[N]
, and no such array entry exists.
An attempt to access list[N]
(or any other array element
out of range) would cause an
ArrayIndexOutOfBoundsException
error at run time. (This
situation shows another advantage of Java over C/C++, which would
happily make the reference, returning garbage or overwriting other
data or code).
Remember:
The legal indices into an array of n elements are integers in the range 0, 1, …, n–1. There is no element with index n. No indices are allowed to be negative, and no indices are allowed to be greater than or equal to n.The most common paradigm for processing each element of an n-element array has a for-loop with the header
for (int i = 0; i < n; i++) and the body uses the variablei
to index into the array.
The second for-loop prints out the values in the array in increasing
order of the index. That is, it prints in order list[0]
,
list[1]
, list[2]
, up through
list[9]
.
Java 5.0 added another way to access every element of an array. The third loop in ShortList.java shows it:
element
in this example, takes on each
value in the array (list
in this example). In each
iteration of the foreach-loop, the loop variable takes on the next
value in the array.
Foreach-loops have the advantage that you cannot make an indexing error. You don't have to worry about indices being too large or too small. There are a few disadvantages to foreach-loops, however:
- You don't have access to the index within an iteration, so if
you need to do something that depends on the index, you don't
have it.
- Foreach-loops access the array elements in only one order: at
index 0, index 1, index 2, and so on. If you want to access
the array elements in any other order, you cannot use a
foreach-loop.
- If you are using only some, but not all, of the array elements, a foreach-loop will access array elements that you're not using. Depending on what you do with the array elements, severe problems could result.
The fourth for-loop prints out the values in decreasing order of the
index: list[9]
, list[8]
,
list[7]
, down through list[0]
. Note how the
loop index i
decreases in this loop. We could not have
used a foreach-loop for this job.
The fifth for-loop prints out the array in decreasing order again, but
using a slightly different method. The loop index i
increases, but we use a more complex expression, N-i-1
,
in the array index. When i
is 0, N-i-1
is
9. When i
is 1, N-i-1
is 8, and so forth.
Finally, when i
is 9, N-i-1
is 0. This
for-loop points out that the array index in a program can be any
expression that evaluates to an integer in the correct range of
indices.
There is an alternate way to create an array, using an initializer list.
Instead of calling new
, we put a list of numbers in curly
braces on the right side of the assignment statement. The Java
compiler counts the number of items in the list, verifies that they
are all of type int
, and creates an array of that length
with the first item assigned to list2[0]
, the second to
list2[1]
, etc. Only valid when you declare the array!
Note that when you do this that you don't necessarily know the length of
the array. But the program shows how to deal with this: list2.length
gives the length of the array list2
.
Multidimensional arrays
In a multidimensional array, you can think of the array entries as themselves being arrays. For example, suppose we have the declaration
Rect
is a class.
The usual way to think of box
is as a 2-dimensional array with
6 rows and 8 columns:
Note that the entry in row i
and column j
is
denoted box[i][j]
. In some other languages, you might
write box[i,j]
, but not in Java (also not in C/C++).
You must always use a separate set of brackets for each index:
box[i][j]
.
A more accurate picture notes that we really have an array of 6 entries,
each of which is an array of 8 entries.
Therefore box[i]
is a 1D array of references to Rect
.
The picture is:
A consequence of the fact that 2D arrays are really arrays of arrays
is that the number of rows is bozo.length
and the number of
columns is bozo[0].length
. (Any valid subscript could be
substituted for 0.)
Note that so far all of the references in box
are null.
After we assign a Rect
object to each reference the picture would
look like:
You can also use initializers with 2D arrays. The short assignment wants you to use a 3x3 matrix of weights. It suggests that one matrix that you should test is:
float [][] edges = {{-1.0f, -1.0f, -1.0f},
{-1.0f, 8.0f, -1.0f},
{-1.0f, -1.0f, -1.0f}};
This is an array with three arrays of floats (one per row), each of which contains three floats.
Code for Image Manipulation
Let's look at some code now.
pickAndShow
does what you did by hand
in SA-0. It is static, so called on class name. Uses a Picture
constructor
that takes a file name. copy
creates a copy by using a constructor that takes a
Picture
and returns a new copy of it.
What about increaseRed
? It uses getPixels
defined in
SimplePicture
. What comes back is an array of Pixel
.
increaseRed
uses a special foreach form of the for
statement to go throught all of the pixels one at a time. As we suspected, it
sets the red on each pixel to twice its current value. Note the use of method calls
on the pixel to get and set the amount of red in the pixel. Note that we can use
methods from the Pixel
class without ever looking at the code.
The two decreaseRed
methods are very similar to increaseRed
.
They get the amount of red, reduce it (by half for the first version, but a given
amount for the second), and setRed
to the new value. Howver, the first
uses a while
loop and the second a conventional for
loop to go through the array. Barb Ericson is getting pedagogical and demonstrating
various options.
Negate is also similar to increaseRed
,
but it creates a new Color
object. Color
is a Java
library type used to represent and manipulate colors. As we suspected, it
gets each color from the pixel. If a color value is c this method sets it
to (255 - c).
flip
does what we thought it did. But instead of getting an array
of pixels it needs to access each one individually. It uses getPixel
to do this. It first creates an new "empty" image of the correct size, which it
calls target
. It gets a pixel from the source, the corresponding
one from the target, and sets the target's color to the source's color.
The outer for
loop here has two
initializations and two updates, one value increasing while the other is decreasing
for the x values. The two y values in the inner for
loop are kind of overkill.
srcY
and trgY
are always equal! Could have used a single y
variable for both.
Look at compose
. Code similar to flip
, in
that for every pixel in the source it gets the corresponding target pixel
in the target and sets the color
of the target to the color of the source. But the way that it finds corresponding
pixels is different. Note that it does an
if
test to make sure that we are still in the picture before updating
the pixel, so don't go out of bounds. blueScreen
has a very similar structure,
but it puts the test for going out of bounds in the for loop tests rather than an
if (more efficient). Also, it only copies if the amount of blue is less that the
total amount of red and green.
scale
is interesting. Demo. How do you scale a picture? Create
an empty picture of the right size, and for each pixel in the new picture find the
"corresponding" pixel in the old and copy it. Go through target in size 1 steps,
original in size 1/factor
steps. Note +=
.
Polymorphism and Interfaces
Polymorphism comes from Greek words for "many" and "shape". So a polymorphic variable is one that can hold multiple types. It is easy to see why Simula found this idea helpful. In simulation you may have many objects, perhaps representing animals. It would be useful to have an array that could hold any animal type. The first object in the array might be a fish, the second a frog, the third an antelope, etc. For the simulation it would be useful to be able to go through the array and tell each animal to move. The fish would swim, the frog would hop, the antelope would run, etc. So we want to be able to have an array of type Animal. Fish, Frog, Antelope, etc. would be subtypes of Animal. They should be able to do anything that a generic Animal can do, perhaps plus other things as well.
Java gives us two ways to achieve this type of polymorphism. The first is to create an interface. That is what we will consider for the rest of this lecture. The second is inheritance, which we will see next class.
An interface is like a class where the method headers (name, return type,
parameters) are given, but there are no bodies that implement these
methods. We will see a large number
of interfaces this term, because interfaces are ideal for specifying an
Abstract Data Types (ADTs). An ADT consists of some data and a collection of
operations that can be performed on that data. "A collection of operations" is
exactly what an interface defines. We will see interfaces List
,
Set
, Map
, and many others.
Let's look at a simple interface and how it can be used.
The file GeomShape.java
contains such an interface. It looks at first like a class
definition, but instead of using the word class
in the
header, it uses the word interface
. The other difference
is that the methods are not implemented. Instead of a method body,
each one has a semicolon after the method header. This makes them
abstract methods. An interface can have only
abstract methods and constants.
The classes Circle.java and
Rectangle.java both
implement the GeomShape
interface. Note
that the the first line of the definition of both classes ends with
implements GeomShape
. (There could be a list of
interface names, separated by commas.) The
implements
tag is a promise to include a definition for
every abstract method in the interface. We say that
the implementing class implements every method in the interface. It
is an error to fail to implement any method from the interface, and
the compiler will complain. (This is a slight lie - we will see later
that classes can be abstract. But it is true about any class that can
create objects.)
An implements
tag is a promise to implement all
the methods of the interface, but the class may include even
more. For example, both the Circle
and
Rectangle
class contain constructors and toString
.
Moreover, the
Rectangle
class has a flip
method; the
Circle
class does not, but that's just to show you that the
two classes need not implement the same exact set of methods, as long
as each implements at least the methods in the interface (which are
the methods it has promised to implement).
The advantage is that a variable (including a formal parameter) can be
declared using the interface name as if it were a class name.
As long as the object actually referenced by the variable is
from a class that implements the interface, it's OK. For
example, we can declare a variable to be a reference to a
GeomShape
and have it actually be a reference to a
Circle
, a reference to a Rectangle
, or for that
matter a reference to any class that implements
GeomShape
. We can call any of the methods specified in
the interface on the object. We know that the methods specified in
the interface are defined for the object, because that's exactly what
the implements
tag tells us.
The program GeomShapeDriver.java
demonstrates our interface in action. The main
method
creates an array shapes
that contains references to
GeomShape
objects. shapes[0]
refers to
a Circle
and shapes[1]
refers to a
Rectangle
. In the for
loop the methods
areaOf
, move
, and scale
are
called on shapes[0]
the first time through the loop and on
shapes[1]
the second time through the loop.
Java uses the actual type of the object
referenced by shapes[i]
to decide at run time which
areaOf
, move
, and scale
methods to call.
Deciding at run time which method is actually called is known as
dynamic binding. It is a feature of
polymorphism: the ability of a single variable to reference
more than one type of object. It can be quite powerful.
Let's revisit what is happening here, because it is important and also
very cool. We have declared shapes
as an array of type
reference to GeomShape
. Except that there's no such thing as a
GeomShape
object. GeomShape
is a
placeholder for any class that implements the scale
,
move
, and areaOf
methods that appear in the
GeomShape
interface declaration. When we
actually create an object that implements GeomShape
, say
a Circle
, we may assign the reference to that object to a
place in shapes
.
The truly remarkable part happens when we call a method with
shapes[i]
as the reference to the left of the dot, e.g.,
shapes[i].areaOf()
. At compile time, we have no idea from
which class the object will actually be. Perhaps shapes[i]
references a Circle
, but perhaps shapes[i]
references a Rectangle
. We don't know at compile time. And
it doesn't matter! The decision as to which areaOf
method gets called—the one in Circle
or the one in
Rectangle
—is based entirely on what kind of object
shapes[i]
references at the time the call is made.
That's dynamic binding.
An addtional observation: there is an if-statement that tests if
shapes[i]
is of type Rectangle
. That is
what the operator instanceof
tests. There is a call to
flip
inside this if-statement. To make this
call, we have to cast the variable shapes[i]
to be a
reference to a Rectangle
object, because
GeomShape
does not include a flip
method.
Therefore, there is no reason for the compiler to treat as legal a
call of flip
on an object that implements
GeomShape
. We know that shapes[i]
references a Rectangle
at that moment, and so we are willing
to assume responsibility for the call to shape
being
legal. In order to convince the compiler that the call is legal,
however, we cast shapes[i]
to a reference to
Rectangle
. The compiler then follows President Reagan's
maxim: "Trust but verify." Because it is doing something that may be
unsafe, it puts in a runtime test to make sure that shapes[i]
really refers to a Rectangle
before it allows the call
to flip
.
What would have happened if we had tried to cast shapes[i]
to
a reference to Rectangle
when it actually was a reference to
some other kind of object (such as a Circle
)? We would get
a run-time error known as a ClassCastException
, and our
program would crash at that point. You can see this behavior
yourself: eliminate the if-statement that tests if shapes[i]
is an instance of Rectangle
.