Notes on Arrays of Objects
Just as we want to look at collections of primitive data types (int, double, etc.) to manipulate them, we want to do the same thing with Objects.
We define arrays of Objects just as we define arrays of primitive data types:
Type [] variable = new Type[#];
where Type is the class name, and # has to be an integer telling the size of the array that we want, e.g.,
Fraction [] list = new Fraction[10];
creates an array of 10 Fractions.
But the storage of arrays of Objects is different because Objects can have different sizes which are not necessarily multiples of two. I.e., all of the primitive data types have as their size either 1, 2, 4, or 8 bytes. So when we make the following declaration:
int [] list = new int[40];
40*4 = 160 bytes of memory are allocated for the array list. The address of list[i] is then computed as (address of list) + 4*i. But in the binary number system, multiplication by 4 is just a left shift of two bits. So the calculation of the address of list[i] is not “expensive” for the compiler. If, however, the size of the Object was 22 bytes, then if one allocates a contiguous block of data for the array, then the calculation of list[i] would be (address of list) + 22*i, and this is “expensive” for the compiler to calculate.
So the above declaration of
Fraction[] list = new Fraction[10];
would allocate 40 bytes in today’s computers, 4 bytes for each of the 10 addresses. (Some of the computers today allocate 8 bytes for a memory address, so they would allocate 80 bytes.) So the array of list would look like:
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Where initially, each value is 0, signifying the null address. I.e., the compiler has the memory management system allocate an address for each element of the array list, but no data elements have been allocated for the individual members of the array of Fractions.
If we than have the following two lines of code:
List[0] = new Fraction(1,4);
List[3] = new Fraction(1,3);
Then the actual storage needed for each Fraction Object is allocated by the memory management system, so we would not have:
1 , 4
|
||
0 |
||
0 |
||
1 , 3 |
||
0 |
||
0 |
||
0 |
||
0 |
||
0 |
||
0 |
Note that list[1] above has the null address as its value, so if one used the code of
list[1].add(list[0]);
One would get a run-time error of null-pointer exception. If this exception is not “caught”, the program will terminate at the execution of this line of code.
Another way to understand why the storage for an array is done like this is to look at array of Strings (Remember that String is an Class). Suppose we have the following declarations:
String [] list = new String[10];
list[0] = “First One”;
list[1] = “Another one”;
list[2] = “This is larger than the first two”;
Then, the memory would look like:
First one
Another one |
|||||||||||
|
|||||||||||
This is larger than the first two |
|||||||||||
0 |
|||||||||||
0 |
|||||||||||
0 |
|||||||||||
0 |
|||||||||||
0 |
|||||||||||
0 |
|||||||||||
0 |
If, however, one tried to directly to allocate memory for the array as contiguous memory, one would look like:
F |
i |
r |
s |
t |
|
o |
n |
e |
/0 |
A |
n |
o |
t |
h |
e |
r |
|
o |
n |
e |
/0 |
T |
h |
i |
s |
|
i |
s |
|
l |
. |
. |
. |
|
|
|
|
|
Where the “/0” means the end-of-string character (which is stored as 8 bits of 0 in most implementations.
Later, if I change
list[0] = “Change it to something else”;
If I tried to put the new value of list[0] directly in memory, I would need to move all of the successive ones (list[1],list[2], etc.) back to make more room for the larger list[0]. However, by allocating the memory as an array of addresses as above, then the new assignment of list[0] would lead to:
Change it to something else |
||||||||||||
Another one |
||||||||||||
This is larger than the first two |
||||||||||||
0 |
||||||||||||
0 |
||||||||||||
0 |
||||||||||||
0 |
||||||||||||
0 |
||||||||||||
0 |
||||||||||||
0 |
I.e., the memory allocated to “First one” would be freed up and new memory would be allocated to store the value of “Change it to something else”. (It would be the responsibility of the garbage collector to recognize that the memory allocated to “First one” is no longer reachable and send it back to the memory management system.)
Suppose with my example with Fraction above, I give the assignment of
list [7] = new Fraction(4,3);
Then the array of Fractions would look like:
1 , 4
|
||
0 |
||
0
1 , 3 |
||
|
||
0 |
||
0 |
||
0 |
||
4 , 3 |
||
0 |
||
0 |
Similarly like what happened to the array of Strings above, if we now execute the statement:
list[7] = list[3];
we would get:
1 , 4
|
||
0 |
||
0
1 , 3 |
||
|
||
|
||
0 |
||
4 , 3 |
||
|
||
0 |
||
0 |
The memory allocated to 4,3 would have to be picked up by the garbage collector.
By the way, this use of a table having an address to actually find the data is referred to as a Lookup Table, which are very common in computer science.
We would like to sort them in the same way as we sort primitive data types, like int. To do this, we need to modify the comparisons (Remember that in arrays of Objects, the assignments are done by changing addresses.)
In SelectionSort, if we have an array of ints:
int [] number = new int[20];
Then the comparison is done by:
if (number[i] < number[min])
We modify it to be
list[i].compareTo(list[min]) < 0)
Where the compareTo refers to the comparison that one implements for
the class when one implements Comparable. (See page 654 of the Gittleman
text for a discussion of this.) Remember
that compareTo
returns a negative number if the element passed is smaller than the object we
are comparing to. In this case, the
element passed is list[min] and the element compared to is list[i].
In the insertionSort, which is discussed in Gittleman starting on page 370, the main comparison is:
while( item[i] > item[j])
Note that in the text (e.g., page 376), this is written as while(current > item[j]), but current is just item[i]. For an array of Objects, this is modified to be:
while(list[i].compareTo(list[j]) > 0)
Otherwise, the sorts are exactly the same.
Thus, for every class, if we want to sort an array of objects of the class, we must define a comparison of objects in the class. The way this is done in Java is to have the class implement the Comparable Interface, and write a compareTo method in the class. For example, on page 654-656, Gittleman writes a compareTo for names by first comparing the last name, and if they are equal, then the first name. The first and last names are compared via the compareTo of strings.
Note that in Java, the allocation of memory for two-dimensional arrays is the same type of allocation as for an array of Objects. This is true even for primitive data types.
In C (and C++), COBOL, FORTRAN, and BASIC, for example, when one declares a two-dimensional array of ints, e.g., (this depends on the language, but I’ll use that of C):
int [][] table = new int[3][5];
The memory management system will allocate 80 (4*5*4) bytes of contiguous memory. It will store the two dimensional array as row major. This means it first stores the first row of the table, then the second row, etc: (table[0][0],table[0][1],table[0][2],table[0][3],table[0][4],table[1][0],table[1][1]…)
where table[i][j] means the element in the ith row and jth column.
The address of table[i][j] then is calculated as
address of table + i*4*5 + j*4
The 4*5 is because each row has 4 elements, each of 4 bytes.
However, in Java, one allocates a table of addresses, just as for an array of objects. I.e.,
int[][] table = new int[3][5];
in Java means
int[][] table;
table = new int[3][];
table[0] = new int[5];
table[1] = new int[5];
table[2] = new int[5];
which is equivalent to:
int[][] table;
table = new int[3][];
for(int i = 0; i < 3; i++)
table[i] = new int[5];
So, symbolically, we have the following allocation and references in memory.
table
|
|||||||||||
|
|||||||||||
Each of the 15 boxes, represents the storage of an int. A similar allocation of memory is for two dimensional arrays of Objects, except that the contents of each of the boxes is an address (instead of data for ints), pointing to where the actual object can be found.
Note that one can have irregular dimensional two dimensional arrays in Java. That is to say, the following can be done in Java:
int [][] table;
table = new int[3][];
table[0] = new int[4];
table[1] = new int[3];
table[2] = new int[5];
Thus, row 0 has 4 elements, row 1 has 3, and row 2 has 5 elements.