Algorithms and Data Structures in an Object-Oriented Framework (“ADSOOF”)
ArrayLists in Java, and an Introduction to Generics
ArrayList
You have seen arrays in Java, where for any type, for example
Thing
, adding []
to the end of the type
creates an array type, so Thing[]
is the type
“array of Thing
”. Then, if i
is
an integer value, and a
a variable of type
Thing[]
, we can change the value of the
i
th component of the array referred to by
a
using the assignment a[i]=expr
where expr
evaluates to a value of type Thing
and the component is
set to that value. Any usage of a[i]
in an
expression gives the current value of the i
th component
of the array which a
is currently referring to.
ArrayLists work in a similar way. The class ArrayList
occurs
in the package java.util
, so if it is used in a file, there
has to be an import statement for it.
The type ArrayList<Thing>
is the type “arrayList of Thing
”, and in general
ArrayList<X>
is the type
“arrayList of X
” where X
can be any type except a primitive type. So
ArrayList<int>
and ArrayList<char>
are not allowed, this is where the equivalent
wrapper classes need to
be used as you can have ArrayList<Integer>
and
ArrayList<Character>
. Then, if
a
is of type ArrayList<Thing>
, the
statement a.set(i,expr)
is equivalent to the
previous a[i]=expr
when a
referred to an
array. The method call a.get(i)
is the
equivalent of a[i]
in an expression. So note that,
unlike arrays, there are no special symbols for referring to the components
of an arrayList, it is done using ordinary method calls. Note that
a.get(i)=expr
does not make sense as
a.get(i)
is a method call, and you cannot assign
a value to a method call.
Note the characters <
and >
are used here
as if they are a sort of bracket (sometimes they are called “angled
brackets”). They are the same characters that are used to
mean “less than” and “greater than”, and you will have seen them used in
this way for comparing numerical values in Java, but this is a completely
different use. Note also that what goes inside the <
and
>
pair must be a type name. Don't confuse it with
the [
and ]
characters (sometimes called “square
brackets”) which come after a type name and with nothing between
to form a new type name, and which otherwise have an integer value between
them. When an array is being created, it is done with new
followed by a type name followed by [
then there is the integer
value giving the size of the array and the closing ]
. When
you are referring to a component of an array, you use the name of a
variable which refers to the array, followed by [
, followed
by an integer expression to give the index, followed by ]
.
But you cannot use the <
and >
brackets with an
integer value between them to create an arrayList of a particular size, and
you cannot use them with an integer value between them to refer to a particular
location in an arrayList object.
What is happening is that ArrayList
is a generic type.
To form a full type, it must be combined with another type, and the
angled bracket notation is how this combining is done. If we have
a full type consisting of ArrayList
combined with another type,
that other type is referred to as the element type of the full type
(the term “base type” has also been used for it, but that term is now used
with a different meaning in the context of inheritance).
In the next
set of notes, you will see another example of a generic type. The other way you will see angled
brackets being used is described below,
where they enclose the list of type variables for a generic method.
You may also see elsewhere a generic type such as ArrayList
used
without being combined with another type. This is referred to as a “raw type”.
It is permitted in Java, and has its own rules for correct use, because generics
were only introduced in Java version 5, and the raw types represent how
older versions of Java handled arrayLists and similar types. So that code written
for the old versions of Java can still be run under the new version of Java,
raw types are still allowed. However, we will not use them in this module, and
will not discuss them further.
Here is a static method which takes an arrayList of Integer
s
and two integer values and destructively changes all occurrences of the first
integer value to the second in the arrayList:
public static void change(ArrayList<Integer> a,int m,int n)
{
for(int i=0; i<a.size(); i++)
if(a.get(i).equals(m))
a.set(i,n);
}
You can compare this to the equivalent method for arrays which we
saw previously:
public static void change(int[] a,int m,int n) { for(int i=0; i<a.length; i++) if(a[i]==m) a[i]=n; }In the arrayList example,
a.get(i).equals(m)
calls
get(i)
on a
to get the value
of the i
th component of the arrayList referred to
by a
, then equals(m)
is called on
that component to see if it stores a value equal to m
.
The method equals
can be called on an object of type
Integer
, it takes an argument of type Integer
and returns true
if the argument stores an integer of the
same value as its own integer, false
otherwise. With the
int
variable as the argument, what actually happens is that
m
is “boxed” into the equivalent Integer
object. Also in a.set(i,n)
the int
value
n
is boxed into the equivalent Integer
object.
You can see also that the number of components in an arrayList
referenced by variable a
is given by the method call
a.size()
, which is the equivalent of a.length
for an array.
In the array example, a[i]==m
is used to test whether the element at position
i
in the array referred to by the variable a
is equal to the value
stored in the variable m
. We have seen the distinction between ==
and the equals
method
already
in the context of Java's class String
. As noted
there, the method equals
should always be used to test if two objects are equal,
because the ==
only tests if two references to objects are aliases (that is,
referring to the same object). With primitive values, the symbol ==
is used
for equality testing, because variables that store primitive values store the actual value
rather than a reference to an object. In fact, equals
cannot be used, because
a method cannot be called on a primitive value, and since a
here is of type
int[]
then a[i]
is of type int
, which is a primitive
type.
Here is a method which copies an arrayList of integers:
public static ArrayList<Integer> copy(ArrayList<Integer> a) // BEWARE - this is not a good example! { ArrayList<Integer> b = makeArrayListInt(a.size()); for(int i=0; i<a.size(); i++) b.set(i,a.get(i)); return b; }This is equivalent to the method for copying arrays:
public static int[] copy(int[] a) { int[] b = new int[a.length]; for(int i=0; i<a.length; i++) b[i]=a[i]; return b; }So you can use
ArrayList<Integer>
as a type
for arguments to methods and for the return type of methods, just as
you can use int[]
in these ways. Note that in our example
b.set(i,a.get(i))
is equivalent to
{ int n=a.get(i); b.set(i,n); }that is, it gets the
i
th component of a
and
sets the i
th component of b
to it.
In this example, makeArrayListInt(a.size())
is used as
the equivalent of new int[a.length]
, that is, it
creates a new arrayList with the same number of components as a
but with those components set to 0
. However, Java does not
provide us with this method, because arrayLists aren't intended to work
like this, unlike arrays you don't have to create one of a fixed size
and then put things into it. That is why the method above for copying
arrayLists was commented as “not a good example”, a better way of doing it is
given below. But if you wanted to run
the poor example, here is the code for the static method
makeArrayListInt
:
public static ArrayList<Integer> makeArrayListInt(int n) { ArrayList<Integer> a = new ArrayList<Integer>(); for(int i=0; i<n; i++) a.add(0); return a; }This works using some of the additional flexibility arrayLists have over arrays which we describe below.
You can find the code for this and the other examples from this set of
notes in the code index
for this section.
The file UseArrayLists1.java gives
a destructive change method, and a copy method as given above, together
with a main
method which enables it to be demonstrated.
Code for finding whether an item is in an arrayList follows very closely
code for finding whether an item is in an array, with a.get(i)
replacing a[i]
. Here is how you could write a method to find
if an integer is in an arrayList of integers:
public static boolean isIn(ArrayList<Integer> a, int n) { int i=0; for(; i<a.size(); i++) if(a.get(i).equals(n)) return true; return false; }Again, boxing occurs automatically to enable an
int
value to be
compared with an Integer
value. But actually you would not need
to write this method, because there is a built-in method for arrayLists which
does it for you. If a
refers to an arrayList, then
a.contains(n)
returns true
if n
is in the arrayList, and false
otherwise.
Similarly, code for finding the biggest item in an arrayList is very similar to what we saw for arrays:
public static Integer biggest(ArrayList<Integer> a) { Integer biggestSoFar=a.get(0); for(int i=1; i<a.size(); i++) { if(a.get(i)>biggestSoFar) biggestSoFar=a.get(i); } return biggestSoFar; }The algorithm for finding the biggest integer in an indexed collection of integers is the same, whether that collection is an array as we saw previously, or an arrayList as seen now. The code differs because of the different way of accessing items at position
i
. In this code for biggest
with an arrayList argument,
unboxing is used to enable Integer
values to be compared
using the >
operation. An alternative way of writing the
comparison would be a.get(i).compareTo(biggestSoFar)>0
.
Java also provides a way of doing this automatically using its library code.
If a
refers to an arrayList of integers, then
Collections.max(a)
will return its highest value integer.
Note this is a static method in Java's library class Collections
,
which is why it is called in this way. A full explanation of how this
method max
can be used requires understanding of further aspects
of Java, which will be covered later.
Once an array object has been created, what each individual component
refers to can be changed, but the size of the array cannot be changed,
as we saw previously. ArrayList objects,
however, can have their size changed. If a
refers to
an object of type ArrayList<Thing>
, then the
statement a.add(expr)
where expr
evaluates to a value of type Thing
causes an extra
component to be added to the end of the arrayList object referred to
by a
which is set to refer to the object returned by
the evaluation of expr
. The value returned by
a.size()
is increased by 1, and the value returned
by a.get(a.size()-1)
with the new value of a.size()
is the value returned by expr
. This is a destructive
change, so any variable which aliases a
will see the
same change in the object it refers to as it is the same object.
So in our code for makeArrayListInt
above, the statement
ArrayList<Integer> a = new ArrayList<Integer>();declares a variable of type
ArrayList<Integer>
called
a
, and sets it to a new arrayList of integers whose
size is initially 0. Then every time a.add(0)
is called
in the loop, an extra component is added to this arrayList object,
every component added referring to an Integer
of value
0
.
In fact, there was no need for our
However, there is actually no need to write a copy method if we just want
an arrayList which is identical in elements to another arrayList. The class
copy
method to create
an initial arrayList of the same size as the argument arrayList. It
could just have created an initial arrayList of size 0, and added the
new components to make the copy:
public static ArrayList<Integer> copy(ArrayList<Integer> a)
{
ArrayList<Integer> b = new ArrayList<Integer>();
for(int i=0; i<a.size(); i++)
b.add(a.get(i));
return b;
}
The code for this is found in the file
UseArrayLists2.java.
The usual way of programming with arrayLists is to do what is
done here, create an arrayList of size zero, then add what is required to
make the arrayList which is needed.
ArrayList
provides a constructor which takes an
arrayList as an argument and constructs a new arrayList which is the same
length as the arrayList argument and for every i
from 0 to
one less than the size of the arrayList, the i
th component of
the new arrayList is an alias of the i
th component of the
argument arrayList. This is called a copy constructor. So, for example
ArrayList<Integer> b = new ArrayList<Integer>(a);
declares variable b
of type ArrayList<Integer>
and sets it to refer to a new ArrayList<Integer>
object
which has the same components as the ArrayList<Integer>
object referred to be a
. It's important to note that the
components within the arrayList are not copied, this is referred to as
a shallow copy. Since objects of type Integer
are
immutable, this is not an issue in this case, but it would be if we had
an arrayList of mutable objects. Since the element type of an arrayList
could be any object type, then it could be a mutable type such as
DrinksMachine
as covered
previously.
If a
refers to an arrayList of DrinksMachine
objects
of size 4, then the result of
ArrayList<DrinksMachine> b = new ArrayList<DrinksMachine>(a);could be illustrated diagrammatically as:
Code which uses a constructor to make a shallow copy of an arrayList
and then uses the destructive change method can be found in
UseArrayLists3.java.
Note that although there is a constructor for the type ArrayList
which takes an argument of type int
, using it does not
create a new arrayList of a particular length, it actually creates an arrayList
of length 0 just as the zero-argument constructor does. The int
argument is actually to do with the internal structure of an arrayList, which
you need not be concerned with at this point. So in this way arrayLists
work differently from arrays: with arrays you always create an array of a
particular size with its elements intialised to 0 or null
,
but this cannot be done with arrayLists, instead the usual technique is to
create an arrayList of length 0 and expand it by adding new elements.
It is also possible to make a copy of an arrayList using the
clone
method. Declaring a variable b
of
type ArrayList<Integer>
and setting it to a copy
of the arrayList referenced by a
is done by:
ArrayList<Integer> b = (ArrayList<Integer>)a.clone();Note it is necessary to use type casting here because the return type of the
clone
method is declared as Object
even
though the actual type of the object returned will be the same type
as the actual type of the object cloned. The Java 5 compiler will also
give you a warning that an “unsafe” operation has been used, though it will
compile correctly. For this reason (and other associated with the details
of the way it works), though clone
exists in Java as a way
of copying objects, it should be avoided if there are other ways of copying
an object. The use of clone
is demonstrated in
UseArrayLists2a.java.
You can find the complete documentation for the class ArrayList
here.
As we have previously noted with the official Java documentation, it is complex
because it has to cover every aspect of the class, which includes plenty of things
you have not covered yet. For instance, there's a large section at the beginning
of the documentation noting that the implementation is not “synchronised”, and then
stating what to do “If multiple threads access an ArrayList
instance
concurrently”. Concurrency and multiple threads is not something we shall cover
at all in this course, you will need to become much more advanced Java programmers
before this becomes as issue so you can ignore this section, but the problem when you
are just starting out and faced with complex documentation like this is that it's
hard for you to know what aspects you can safely ignore because they go beyond
what you need to know. However, from the documentation you can see that there
is a variety of methods provided in class ArrayList
which perform
useful operations changing both the size and content of the ArrayList
object they are called on, so making arrayLists much more flexible than arrays.
The class is named as ArrayList<E>
where E
stands
for whichever element type you use. So the method get
, which is
the method we described above, is documented as having argument type int
and return type E
meaning that if get
is called on
an object of type ArrayList<Integer>
it returns an
Integer
, if it is called on an object of type
ArrayList<String>
it returns a String
, and so on.
add
and remove
in class ArrayList
Above we saw the single-argument add
method which adds
its argument to the end of the arrayList object it is called on, but there
is also a two-argument add
which takes an integer and an object
of the element type of the arrayList and adds the object at the position in the
arrayList specified by the integer. The object which was at this position and
all the objects in higher-indexed positions are moved up one place, and the size
of the array increases by one. So this is a different thing from set
which also takes an integer representing a position and an object of the base
type, but replaces the object at the given position, so this object is no
longer in the arrayList, and the size of the arrayList remains the same. Note that
set
does actually have a return value, given as type E
,
the element type of the arrayList object it is called on. Where we have used it above,
we have done nothing with the value it returns, so used it as if it has
return type void
. This is fine, mostly this is how set
is used. But the value it returns is the object that was replaced, just in case
that was something you needed to keep a reference to. There are also two
remove
methods. One takes an argument of type int
and
removes the item from the arrayList it was called on at the position given by
the int
argument, moving everything following it down one place
in the arrayList and causing the arrayList's size to be reduced by one.
The class ArrayList
has two single-argument methods called
remove
. We saw a method called remove
previously with arrays,
which was static and constructive, so a call remove(a,n)
where a
referred to an array of integers and n
held an integer returned a reference to a new array which was like the
one referenced by a
but had the first occurrence of
n
removed and everything following it moved down one place.
The remove
methods in class ArrayList
are not
static and are destructive, so a.remove(n)
changes the
arrayList referred to by a
to remove an item,
all items in the arrayList following it are moved down one place,
and the size of the arrayList is reduced by one. However, if
n
is of type int
, the call
a.remove(n)
removes the item in the component of the
arrayList indexed by n
, otherwise it removes the first
item in the arrayList which is equal to n
. So
method remove
in class ArrayList
is like
method removePos
we saw
previously with arrays when it
has an argument of type int
, and like the previous
remove
otherwise. This is why we said there are
two different single-argument methods called remove
in
class ArrayList
: which one is used depends on the type
of the argument. The situation where methods have the same name
but can be distinguished by the number and/or the types of their
arguments in known as overloading.
Note that when the method remove
“removes the first
item in the arrayList which is equal to n
”, what this
means is as defined by the method equals
. To put it more
formally, a.remove(n)
removes the object at position i
in the arrayList referred to by a
where i
is the
lowest value such that a.get(i).equals(n)
. The way the method
equals
works depends on the class of the object it is called on,
this is something we will discuss in more detail
later.
You might wonder how a.remove(n)
works when a
refers to an arrayList of integers. However, remember the element type
of an arrayList of integers must be Integer
, not int
.
So if a
refers to an arrayList of integers, then n
must be of type Integer
to have the effect of removing an
item equal to n
rather than the item at position n
.
Note that both remove
methods in class arrayList
have return types. The method with argument of type int
which removes the item at the position given by its argument returns
a reference to the item that was removed, and throws an
IndexOutOfBoundsException
if the int
argument
is a value below 0
or equal to or great than the size of
the arrayList it is called on. Otherwise, a call of remove
on an arrayList returns true
if the item given as an
argument can be found and is removed, and it returns false
if no item equal to the argument can be found.
There is some code to demonstrate the two different remove
methods in UseArrayLists12.java
and UseArrayLists13.java. In the
second of these, the element type of the ArrayList
is
String
, so it is clear which one of the overloaded
methods is meant. It is the one which removes a particular String
wherever it is when the argument is a String
object, it is the
one which removes whatever String
object is at a particular
position when the argument is an int
value giving that position.
In the first of these, however, as the element type of the ArrayList
is Integer
, which method is meant depends on whether the argument
is of type int
or of type Integer
.
As with the two-argument add
method, you should be careful to
remember that when you are using the method remove
with an
int
argument giving a particular position it changes the
position of following elements in the arrayList. So, a.add(n,t)
has the effect of putting what t
refers to into position
n
in the arrayList referenced by a
, but what was at
position n
gets moved up to position n+1
, what
was at position n+1
gets moved up to position n+2
and so on. In a similar way, a.remove(n)
when n
is of type int
means that what was at position n
is removed from the arrayList referenced by a
, but what was
at position position n+1
gets moved down to position
n
, what was at position n+2
gets moved down to
position n+1
and so on.
The class ArrayList
also has methods
indexOf
and lastIndexOf
, which work like
the position
methods we gave
previously, except again they
are not static, so a.indexOf(n)
gives the index of the
first occurrence of an item equal to n
in the arrayList
referenced by a
, while a.lastIndexOf(n)
gives
the position of the last occurrence; both of these return -1
if no
item equal to n
can be found in the arrayList.
ArrayList
arguments
The class ArrayList
gives us some of the methods we would
most likely need to use on arrayLists, but if there is not something there
already to do a task we require, we need to write our own method.
We cannot add methods to the class ArrayList
as that class is
already given to us as part of Java, so instead we can write methods
which take an ArrayList
object as their argument. Note the
difference between a method which is in class ArrayList
,
so is called on an ArrayList
object, and a method which
is in another class and takes an ArrayList
object as an
argument. When a method is called on an object, the reference to the object
(usually a variable of its type) is followed by the .
character, then the name of the method and any arguments it takes within
(
and )
. When an object is passed to a method
as an argument, the reference to it is one of the things which is
between the (
and )
. So far we have only seen
writing our own static
methods, but
later
we shall see also writing our own instance methods which are called on objects.
You have already seen a static method which takes an arrayList of integers and two integers, and changes all occurrences of one integer argument to the other. Now here is a method which takes an arrayList of strings and two strings and changes all occurrences of the first string to the second:
public static void change(ArrayList<String> a,String m,String n) { for(int i=0; i<a.size(); i++) if(a.get(i).equals(m)) a.set(i,n); }As you can see, it is identical to the method that deals with arrayLists of integers, except for the type
String
rather than
Integer
. It seems wasteful that we should have to write
a separate method called change
identical to other methods
every time we want a method to do this destructive change on an arrayList
for a different element type. We shall show below how this is unnecessary.
First, however, let us consider another operation, this time we want
to take an arrayList and two integers, and insert the second integer
after every occurrence of the first. So if the array is the one that
can be represented by [2,5,8,3,2,4,2,7]
and the numbers
are 2
and 10
, the result will be the array
[2,10,5,8,3,2,10,4,2,10,7]
, that is, 10
has been added
after every 2
. With arrayLists, we could do the change either
constructively or destructively. Here is a destructive method
to perform the operation:
public static void addAfter(ArrayList<Integer> a,Integer m,Integer n) // Adds n after every occurrence of m in a { for(int i=0; i<a.size(); i++) if(a.get(i).equals(m)) a.add(i+1,n); }Here a call
addAfter(a,p,q)
will add q
after
every p
in a
, and the arrayList referred to by
a
and by any other variable which aliases it will be
changed. We have used the type Integer
for its arguments,
but it is passed int
values they will be converted into
Integer
values automatically.
Now here is a constructive method performing the same operation:
public static ArrayList<Integer> addAfter(ArrayList<Integer> a,int m,int n) // Adds n after every occurrence of n in a constructively { ArrayList<Integer> b = new ArrayList<Integer>(); for(int i=0; i<a.size(); i++) { b.add(a.get(i)); if(a.get(i).equals(m)) b.add(n); } return b }Here,
addAfter(a,p,q)
does not change the arrayList referred to
by a
, but constructs a new one with the desired change and
returns it, so a1=addAfter(a,p,q)
will leave the arrayList
referred to by a
unchanged but set a1
(assuming
it is declared of type ArrayList<Integer>
) to refer to
the new one.
There is a problem with the destructive version. Suppose we want to add
a 2
after every occurrence of 2
, so that starting with [2,5,8,3,2,4,2,7]
we change it to [2,2,5,8,3,2,2,4,2,2,7]
.
If we call the method with the call addAfter(a,p,q)
where
a
refers to the arrayList written [2,5,8,3,2,4,2,7]
and p
and q
are set to 2
. You will
find the code runs for much longer than you'd expect and terminates with
an error message. The reason for this is that our code does not take
into account the case where both integer arguments are the same value. In
our example, having added a 2
after the first 2
,
it looks at the next integer and finding it is a 2
adds
another 2
after it. Then it does the same and in fact keeps on adding
2
and expanding the size of the arrayList until it exceeds
the maximum size the system can handle. The error message you will get here is:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceIf you get an error message like this, unless you are doing something which you know for sure involves creating a huge arrayList or some other collection type, you should conclude your code somehow gets into an infinite loop which just carries on taking more and more store until it reaches the system limit. Always look for that infinite loop rather than ask “how can I increase the amount of store the system allows me to use?”. The other thing to learn here is always check special cases, and the case where two arguments are the same where normally they'd be expected to be different is a good example. It's best that your code takes into account every possible special case, don't just assume “Oh, that will never really happen, so I don't need to bother adjusting the code to deal with it”. Here is code for the destructive
addAfter
method which deals properly
with the two integer arguments being the same and gives the result you
would expect:
public static void addAfter(ArrayList<Integer> a,int m,int n) // Adds n after every occurrence of m in a { for(int i=0; i<a.size(); i++) if(a.get(i).equals(m)) { a.add(i+1,n); i++; } }You can see that when a new integer is added to the arrayList, the variable
i
is increased by 1 in addition to its normal increase by 1 in
the update part of the for loop. This is a good illustration of the way in
which for loops do not always execute their body a fixed number of times,
it can vary if the body contains code which can alter the value of the
variables used in the test and update part of the for loop.
There is an issue we need to consider in our first version of a constructive
method for the operation addAfter
. In this case, it is simply
a matter of good coding practice, it is not something that would cause it to
give the wrong or an unexpected result.
The code we gave calls a.get(i)
twice in
circumstances where it is always going to return the same value. If you
find your code calls a method twice with the same arguments and you know
nothing will change by calling the method and nothing in between the
two calls will change the result returned, it's better to call it once,
record that value using a variable, and then refer to that variable each
time you want the result of the method call. This gives the following
revised code for constructive addAfter
:
public static ArrayList<Integer> addAfter(ArrayList<Integer> a,Integer m,Integer n) // Adds n after every occurrence of m in a constructively. { ArrayList<Integer> b = new ArrayList<Integer>(); for(int i=0; i<a.size(); i++) { Integer k=a.get(i); b.add(k); if(k.equals(m)) b.add(n); } return b; }So here the variable of type
Integer
called k
holds the value of a.get(i)
. The reason for doing this is
that if it took a significant amount of calculation to find the value
of a.get(i)
, we would then avoid doing this calculation twice.
Another way of avoiding multiple calls of the get
method is to use the
“for-each” loop construct. That would result in the code being:
public static ArrayList<Integer> addAfter(ArrayList<Integer> a,Integer m,Integer n) // Adds n after every occurrence of m in a constructively. // Uses for-each loop { ArrayList<Integer> b = new ArrayList<Integer>(); for(Integer k : a) { b.add(k); if(k.equals(m)) b.add(n); } return b; }We will look at the for-each loop later in the context of Java's
Iterator
type. For consistency, we will continue to
use loops over arrayLists involving an explicit numerical index and the get
method in example code, but once you are used to the concept of arrayLists, it would be
better to use the for-each loop when it is appropriate.
The file UseArrayLists4.java
shows the first destructive version of the addAfter
method, and
the file UseArrayLists5.java
shows the amended version.
The file UseArrayLists6.java
shows the first constructive version of the addAfter
method, and
the file UseArrayLists7.java
shows the amended version.
The file UseArrayLists14.java
shows an example of a static method which uses the remove
method with an int
argument called on an arrayList. It
destructively removes all integers which occur one position before a
particular integer in an arrayList of integers. It is programmed carefully
so that the integer argument itself is destructively removed if it occurs
before itself in the arrayList. For example, with
[2,3,4,5,6,7,4,1,4,8,9,4,4,5,2]
and the removal of all integers
which occur before a 4
, the result should be
[2,4,5,6,4,4,8,4,5,2]
because although the 4
s
are not normally removed, the second to last 4
occurs before
another 4
so it should be removed. The code uses a
while
-loop rather than a for
-loop, so that the
index variable is not increased by one when an integer is removed. This
is because when this happens, what was at position i+1
moves down to position i
. In the file
UseArrayLists15.java
which performs the operation constructively, a for
-loop
which increases the index variable by one each time round the loop is
used because here we do not have to take into account any destructive
effect of our code on the arrayList argument as we process it.
Finally, here's how to get round the problem of having to write a
separate method for each element type. Here's the amended version of
constructive addAfter
for dealing with arrayLists of
strings rather than arrayLists of integers:
public static ArrayList<String> addAfter(ArrayList<String> a,String m,String n) // Adds n after every occurrence of m in a constructively. { ArrayList<String> b = new ArrayList<String>(); for(int i=0; i<a.size(); i++) { String k=a.get(i); b.add(k); if(k.equals(m)) b.add(n); } return b; }You can find this in the file UseArrayLists8.java. As you can see, the only way it differs is the use of
String
rather than Integer
. The version of Java introduced in 2004,
called Java 5, introduced the idea of type variables to deal with
situations like this. When a method is written, any type variables it
has come in a list before the return type, starting with the symbol
<
and ending with the symbol >
. Here
is the generic version of constructive addAfter
:
public static <T> ArrayList<T> addAfter(ArrayList<T> a,T m,T n) // Adds n after every occurrence of m in a constructively. { ArrayList<T> b = new ArrayList<T>(); for(int i=0; i<a.size(); i++) { T k=a.get(i); b.add(k); if(k.equals(m)) b.add(n); } return b; }The type variable here is
T
. When the method is called,
Java works out what the type variable is from the arguments so
calling b=addAfter(a,p,q)
will set T
to
Integer
if a
and b
are of type
ArrayList<Integer>
and p
and q
are of type Integer
. If a
and b
are of
type ArrayList<String>
and p
and q
are of type String
, it will set T
to
String
. The generic version of the addAfter
method, together with code that shows it used for both an arrayList
of integers and an arrayList of strings can be found in the file
UseArrayLists10.java.
In the code for the generic version of addAfter
you can
see that the type variable may be used in the arguments to the method,
the return type, and also to declare local variables, either as a
type on its own or as the element type for ArrayList
.
However, we can assume nothing about objects whose type is given
by the type variable. We can call the method equals
on
them, as we do on the example where we have k.equals(m))
where
k
and m
are declared as of type T
where T
is a type variable. This is because the method
equals
can be called on objects of any type. We
could not call a method on these variables of a sort which can only
be called on objects of certain types. Later we shall see how type
variables may be specified in a way which enables a wider range of
methods to be called on them by using
bounded generic types.
Going back to methods to take an arrayList and two values, and change all occurrences of one value to the other in the arrayList, here is a destructive generic version:
public static <T> void destChange(ArrayList<T> a,T m,T n) { for(int i=0; i<a.size(); i++) if(a.get(i).equals(m)) a.set(i,n); }This goes through the arrayList, and if the component of it indexed by
i
is equal to the argument m
changes it
to n
by calling a.set(i,n)
.
Here is a constructive version:
public static <T> ArrayList<T> constChange(ArrayList<T> a,T m,T n) { ArrayList<T> b = new ArrayList<T>(); for(int i=0; i<a.size(); i++) { T k = a.get(i); if(k.equals(m)) b.add(n); else b.add(k); } return b; }This works by starting with variable
b
set to refer to
an arrayList of size 0, then going through the components of a
one at a time in order, adding them to the arrayList referred to by
b
, except in the case where the component is equal to the
argument given by m
in which case the value given by the
argument n
is added to the new arrayList. When this process
is completed, the new arrayList is returned as the result of the method.
These methods can be found in a file called
ArrayListOps.java.
If a static method is declared as public
in one class
file, a call to it may be made in another class file by attaching the
call to the name of the class the method is in. Examples of this are
shown in the file
UseArrayList11.java.
For example, ArrayListOps.destReverse(a)
calls a
destructive reverse method, which takes a reference to an arrayList
object and changes the object so that its components are placed in the
reverse of their original order. The call ArrayListOps.destReverse(a)
causes this to be done to the arrayList object referenced by the variable
a
.
Previously, we saw a method which searched an array of strings to see if a particular string was in it, and noted that it was almost identical to a method which searched an array of integers to see if a particular integers was in it. We noted then that generics would offer a way to avoid rewriting the method for every element type, and here is how it is done:
public static <T> boolean isIn(T[] a,T item) { for(int i=0; i<a.length; i++) { if(a[i].equals(item)) return true; } return false; }Note, however, that though this method can be called with its first argument an array of strings (type
String[]
) and its second argument
of type String
, it cannot be called with its first argument of
type int[]
and its second argument of type int[]
,
this is because int
is a primitive type, not an object type.
It can be called using the wrapper type Integer
, so with
its first argument of tyype Integer[]
and its second argument
of type Integer
. The method, and support code showing its
use with an array of strings and an array of integers can be found in the
file GenericArrayTest.java.
Last modified: 26 September 2019