5. Data Structures Review
Like objects, defining a data structure is similar between Python and Java
Most of the differences are syntax related
Note
Although Python code was included for the previous topic defining a Friend class, for brevity and an exercise in
understanding Java, Python is not included here when defining the data structure.
5.1. Contact List Class
With the
Friendclass complete, there needs to be a way to keep track of and manage theFriendobjectsTo do this, a new class called
ContactListwill be createdWhat fields should this have?
A way to keep track of the
Friendsin theContactListAn array will be used here
A count of how many
Friendobjects theContactListcontainsJust an
int
5.1.1. Setting Fields and Writing the Constructor
2import java.util.Arrays;
3import java.util.NoSuchElementException;
4import java.util.Objects;
5
6/**
7 * Class to keep track of a collection of Friend objects. The underlying contained for the class is an array that
8 * will automatically "grow" to accommodate adding beyond the initial capacity.
9 */
10public class ContactList {
11
12 private static final int DEFAULT_CAPACITY = 10;
13 private static final int NOT_FOUND = -1;
14
15 private int size;
16 private Friend[] friends;
17
18 /**
19 * Create an empty ContactList with the array container's capacity being set to the default capacity.
20 */
21 public ContactList() {
22 // Calls the constructor that takes an int as parameter
23 this(DEFAULT_CAPACITY);
24 }
25
26 /**
27 * Create an empty ContactList with the array container's capacity being set to the specified size.
28 *
29 * @param initialCapacity Starting capacity of the fixed length array
30 */
31 public ContactList(int initialCapacity) {
32 size = 0;
33 friends = new Friend[initialCapacity];
34 }
Things to notice
The imports
The two constants
The declaring of the fields
Two constructors
Most of these are ideas one should already be familiar with
The imports are used for functionality described below
One constant,
DEFAULT_CAPACITY, defines the default size an array should have — more on this belowThe other constant,
NOT_FOUND, is used to provide a name to the sentinel value of-1— more detail belowThe declaring of the fields is similar to what was seen in the
Friendclass
Having two constructors is a new idea that was not used in Python
In several programming languages, it is possible to have multiple methods with the same name that take different parameters
This is called overloading
Notice that one constructor takes no parameters and the other takes a single integer
capacityFor now, focus on the second one; the one that takes the parameter
It first sets the
sizefield of theContactListto0since a newContactListis emptyIt then creates a new empty
Friendarray of sizecapacityRemember, arrays have a fixed size
The strategy here is to make an array that is sufficiently large, but only use what is needed
Consider the following example,
The array is size
10Only two
Friendobjects are in theContactListThen only indices
0and1of the array are actually used
The second constructor, the one that takes an integer, is used to create a new
ContacListwith the array of some specified sizeThe first constructor, the one with no parameter, is used to create a new
ContactListwith a default capacityMore precisely, the default capacity set to the class constant
DEFAULT_CAPACITYIt does this with the use of constructor chaining
The use of
this(DEFAULT_CAPACITY)simply calls the constructor that takes a single integer as a parameterIn this example, it ultimately calls
ContactList(10)See the constructor chaining aside for more details
The fact that
DEFAULT_CAPACITYwas set to10in this class is entirely arbitraryFurther, the inclusion of the constructor that takes no parameter is entirely optional
Below is a visualization of a new and empty
ContactList
Example of an empty
ContactListthat was createdContactList contacts = new ContactList(5);.
5.1.2. size and isEmpty
180 public boolean isEmpty() {
181 return size() == 0;
182 }
183
184 public int size() {
185 return size;
186 }
isEmptyreturns abooleanindicating if theContactListis empty or notThe
sizemethod returns the number ofFriendsactually within theContactListRemember, the size of the array and the number of
Friendsin theContactListare different things
5.1.3. add
There is some complexity involved with adding a
Friendto theContactListArrays have a fixed size
The capacity of the array is not the same as the number of
Friendsin the collection
Since the array has a fixed size, it’s not possible to add more
Friendobjects beyond the size of the arrayHowever, it would be ideal if it were possible to continually add
Friendobjects without worrying about the capacityIf more space is needed, a simple solution is
Make a new array that is bigger
Copy over the contents of the old array to the new array
Assign the array field
friendsto reference the new, bigger array
“Expanding” the capacity. No array actually grows, but a new array that is larger is created and the contents of the old array is copied to the new array. The object’s field that references the array is updated to refer to the new larger array.
38 /**
39 * Add a new friend to the ContactList.
40 *
41 * @param friend Friend object to add to the ContactList
42 * @return True if the friend was added successfully, false otherwise
43 */
44 public boolean add(Friend friend) {
45 // If we have run out of space in our array we need to deal with it by making a new array
46 if (size() == friends.length) {
47 friends = expandCapacity(friends);
48 }
49 // Add friend to the next available spot
50 friends[size] = friend;
51 size++;
52 return true;
53 }
54
55 /**
56 * Private helper method for making room in the collection for new Friend objects. This method creates a new Friend
57 * array twice the size of the original (this.friends), copies over the contents of the original Friends array in
58 * order, and then returns the new, bigger friends array.
59 */
60 private Friend[] expandCapacity(Friend[] oldFriends) {
61 // Make a new array of twice the size of the previous
62 Friend[] newFriends = new Friend[oldFriends.length * 2];
63
64 // Copy over the contents of the friends array to the new bigger friends array
65 for (int i = 0; i < oldFriends.length; i++) {
66 newFriends[i] = oldFriends[i];
67 }
68 // Have friends now reference the new friends
69 return newFriends;
70 }
The
expandCapacitymethod gets called automatically by theaddmethod if the array has run out of spaceIf the array had enough room,
expandCapacityis not called
Either way, the
Friendbeing added via theaddmethod will always go to the next available spotNotice that the value in the
sizefield also corresponds to the next available spot in the arrayFor example, if there are
0Friendobjects in theContactList, the next available spot in the array is0
When done, this method returns a
booleanindicating if theaddworked correctlyAlso notice that the
expandCapacitymethod isprivateThis method is important for the inner workings of the
ContactListclassThis method is not something one wants a user of this
ContactListclass to care about
5.1.4. contains and find
75 /**
76 * Check if a given Friend object exists within the collection.
77 *
78 * @param friend Friend object to check if it exists within the collection
79 * @return True if the Friend exists within the collection, false otherwise
80 */
81 public boolean contains(Friend friend) {
82 return find(friend) != NOT_FOUND;
83 }
84
85 /**
86 * Private helper method to find and return the index of a given Friend object within the collection. If no such
87 * Friend exists within the collection, a sentinel value of -1 (NOT_FOUND constant) is returned.
88 *
89 * @param friend Friend to find the index of
90 * @return Index of the Friend within the collection, or -1 (NOT_FOUND constant) if no such Friend exists
91 */
92 private int find(Friend friend) {
93 // Linear search for the friend we are trying to find
94 for (int i = 0; i < size(); i++) {
95 if (Objects.equals(friend, friends[i])) {
96 return i;
97 }
98 }
99 // -1 (NOT_FOUND constant) will signify that we didn't find what we were looking for
100 return NOT_FOUND;
101 }
The
findmethod, which isprivate, is an internal helper method for finding the index of a givenFriendThis method is just a linear search
Mind the use of
Objects.equalswhich is a null safe way to check if two objects are equal via their definedequalsmethod
If no such
Friendobject exists, a special sentinel value of-1is returnedReferred to by the class constant
NOT_FOUNDThis sentinel value has special meaning
Since
-1is not a valid index, it can be used to indicate that the object was not found
The
containsmethod returns abooleanindicating if aFriendobject exists within theContactListThis method makes use of the private
findmethod
5.1.5. indexOf
The
indexOfmethod returns the index of the specifiedFriend, if it exists
106 /**
107 * Return the index of the specified Friend object within the collection. If no such Friend exists within the
108 * collection, a NoSuchElementException is thrown.
109 *
110 * @param friend Friend object to find the index of within the collection
111 * @return Index of the Friend object
112 * @throws NoSuchElementException If no equal Friend object exists within the collection, throw an exception
113 */
114 public int indexOf(Friend friend) {
115 if (!contains(friend)) {
116 throw new NoSuchElementException(Objects.toString(friend));
117 }
118 return find(friend);
119 }
This method checks if the
Friendexists, and if it does not, it throws an exceptionA string representation of the provided
Friendis given to the exception for its messageIt uses
Objects.toStringas it is null safe
This way the exception provides details on what happened
For example, if trying to get the index a
Friendobject that does not existException in thread "main" java.util.NoSuchElementException: Friend(Sammy, Silver, samtheman@yahoo.com)
If it does exist, this method simply delegates the work to the private
findmethod
5.1.6. get
The
getmethod returns theFriendat the specified index
124 /**
125 * Return the Friend object at the specified index. If the provided index is out of bounds an
126 * IndexOutOfBoundsException is thrown.
127 *
128 * @param index Index of the Friend object to be returned
129 * @return Friend object at the specified index
130 * @throws IndexOutOfBoundsException If an invalid index is provided (negative, or too large)
131 */
132 public Friend get(int index) {
133 if (index < 0 || index >= size()) {
134 throw new IndexOutOfBoundsException(index);
135 }
136 return friends[index];
137 }
If the index is out of bounds, an exception is thrown
The invalid index is provided to the exception for its message
Example message from an exception being thrown —
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index out of range: 99
5.1.7. remove
Below is an example of a
removemethod that will remove aFriendfrom theContactListThis method returns a
booleanto indicate if theremovewas successful
142 /**
143 * Removes the specified Friend object from the collection. The removed object is removed from the collection by
144 * being replaced in the collection with the Friend object at the end of the collection. This, remove does not
145 * preserve the order of the Friend objects within the collection.
146 *
147 * @param friend Friend object to remove from the collection
148 * @return True if the object was successfully removed, false otherwise
149 */
150 public boolean remove(Friend friend) {
151 if (!contains(friend)) {
152 return false;
153 }
154 int removeIndex = find(friend);
155 // Although it is possible that the element being removed is replaced with itself, which happens when it is the
156 // last element being removed (when removedIndex is equal to size-1), this will not actually matter since size
157 // is decreased by 1 (size--), meaning the next add would happen at the index removedIndex/size-1, thereby
158 // overwriting that value anyway.
159 friends[removeIndex] = friends[size - 1];
160 // Further, by changing the last element to reference null, we know the data is gone.
161 friends[size - 1] = null;
162 size--;
163 return true;
164 }
Remove first checks if the
Friendobject exists within theContactListIf does not exist,
removereturnsfalse.A string representation of the provided
Friendis given to the exception for its message
To actually remove the
Friend, all that needs to happen is for the program to lose reference to itIn the above example, the array at the index of the
Friendto be removed is set to reference theFriendat the end of the arrayfriends[size - 1]
Once this is done, the array has no reference to the
Friendthat was removedThe array at index
size - 1is set tonullAlthough not necessary, it is not a bad idea to explicitly remove the reference at the end
After the
Friendhas been removed, the size of theContactListneeds to be decreased by1The method then returns
true.
Friend objects are removed by deliberately losing reference to them. After a remove, the array index that
referred to the Friend that was removed now refers to the Friend that was at the end of the ContactList.
5.1.8. clear
Clear out all the
Friendobjects within theContactList
169 /**
170 * Clear the contents of the ContactList object; eliminate all Friends form the ContactList.
171 */
172 public void clear() {
173 friends = new Friend[friends.length];
174 size = 0;
175 }
Here, simply create a new empty array and set the size to
0Since the old array referenced by
friendshas no more reference to it, it get managed by the garbage collectorOne could have gone through the array and set each index to reference
null, but this is easierFurther, setting the size to
0would also be sufficient
5.1.9. toString
A good representation of the collection would be an aggregate of the string representations of the
FriendobjectsHave each
Friendwithin theContactListbe on its own line
One could simply loop over the array and perform several string concatenations
However, due how
Stringobjects work, it ends up being wasteful to continually append to stringsStringobjects are immutableThey cannot be changed once they are created
When appending, a new
Stringobject needs to be created
An alternative to continually appending to a
Stringis aStringBuilder, which eliminates the extra overheadSee the below example of the
ContactListclass’toStringthat makes use of aStringBuilder
190 /**
191 * Create a string representation of the ContactList. The string representation will be an aggregate of the
192 * string representations of each of the Friend objects within the ContactList. Each Friend object's string will
193 * be on its own line. If the ContactList is empty, an empty string is returned.
194 *
195 * @return String representation of the ContactList
196 */
197 public String toString() {
198 StringBuilder builder = new StringBuilder();
199 for (int i = 0; i < size(); i++) {
200 builder.append(friends[i]);
201 builder.append("\n");
202 }
203 return builder.toString();
204 }
Note
In the above example, the instances of Friend objects are not having their .toString() methods called
explicitly. This is unnecessary here since the StringBuilder object’s append method would call it
automatically.
5.1.10. equals and hashCode
209 /**
210 * Checks if two ContactList objects are equal. ContactList objects are considered equal if the contents of their
211 * friends field, an array, are all equal.
212 *
213 * @param o an "object" being compared to
214 * @return True if the two objects are equal, false otherwise
215 */
216 @Override
217 public boolean equals(Object o) {
218 if (this == o) {
219 return true;
220 }
221 if (o == null || getClass() != o.getClass()) {
222 return false;
223 }
224 ContactList that = (ContactList) o;
225 return Arrays.equals(this.friends, 0, this.size(), that.friends, 0, that.size());
226 }
Here,
Arrays.equalsis used to check the equality on the arrayThe alternative would be to loop over the array and check equality on each element within the loop
231 @Override
232 public int hashCode() {
233 int result = Objects.hash(this.size);
234 for (int i = 0; i < this.size(); i++) {
235 result = result * 97 + Objects.hashCode(this.friends[i]);
236 }
237 return result;
238 }
Above is an example of the
hashCodemethod for theContactListclassAlthough clearly more complex than the
Friendclass in the previous topic, it still follows the same basic ideaSum the hash values of all the fields
Although, the summing was delegated to the
Objectshashfunction in theFriendclass
First the
sizevalue is hashedThen the array is iterated over and each element’s hash is included to the running total that is ultimately returned
Also notice the use of the
Objectsclass’hashCodefunctionLike the
Objectsclass’equalsfunction, this is a null safe way to use theFriendclass’hashCode
The value
97is used to scale the result since it is a prime numberThis increases the chance of producing a unique hash value
Warning
The ContactListTest class is provided below, but it will not be covered here. Testing collections can be
difficult for a variety of reasons. The first collection that testing will be discussed with will be the
ArrayStack as it is a simpler collection.
Nevertheless, feel free to explore the ContactListTest below.
5.2. For Next Time
Read Chapter 1 of the text
15 pages
5.2.1. Playing Code
Download and play with
If everything was done correctly, the following code from
PlayingObjectsshould work
1public class PlayingObjects {
2 public static void main(String[] args) {
3 ContactList myFriends = new ContactList(5);
4 myFriends.add(new Friend("Bob", "Smith", "bsmith@gmail.com"));
5 myFriends.add(new Friend("Jane", "Doe", "jdoe@gmail.com"));
6 myFriends.add(new Friend("Clarence", "Cartwrite", "treelover1523@hotmail.com"));
7 myFriends.add(new Friend("Sandy", "Seaside", "boatsboatsboats@yachtclub500.com"));
8 myFriends.add(new Friend("Adam", "Fluffson", "fluffyman28@hotmail.com"));
9 myFriends.add(new Friend("Adrian", "Andrews", "aandrews@hotmail.com"));
10
11 System.out.println("Print after adds and expand");
12 System.out.println(myFriends);
13
14 System.out.println("Print after remove");
15 myFriends.remove(new Friend("Bob", "Smith", "bsmith@gmail.com"));
16 System.out.println(myFriends);
17
18 System.out.println("Print after clear");
19 myFriends.clear();
20 System.out.println(myFriends);
21 }
22}