add
or delete
operation was performed
on a set, a new array was constructed representing the way the
changed set is stored in the computer. This new array replaced the
array within the object. But it remained the same object, since any
variable referring to that object would refer to the same place in the
computer though with its internal (invisible outside the set class)
array changed.
An alternative way of dealing with this would be for the add
and delete
operations to create entirely new objects
representing the change to the set. In this case, the object representing
the old set would remain in place unchanged. In Java terms, the operations
are represented by methods add()
and delete()
which return sets. Previously these methods had return type void
so they were called as statements on their own, with their effect being to
change the object the call was attached to. Here is an implementation
with the methods returning new set objects:
class IntSet7 { // Constructive Set class private int[] array; public IntSet7() { array = new int[0]; } private IntSet7(int[] a) { array = a; } public IntSet7 delete(int n) { int i; for(i=0; i<array.length&&array[i]<n; i++) {} if(i<array.length&&array[i]==n) { int[] array1 = new int[array.length-1]; for(int j=0; j<i; j++) array1[j]=array[j]; for(i++;i<array.length; i++) array1[i-1]=array[i]; return new IntSet7(array1); } return this; } public IntSet7 add(int n) { int i; for(i=0; i<array.length&&array[i]<n; i++) {} if(i==array.length||array[i]>n) { int[] array1 = new int[array.length+1]; for(int j=0; j<i; j++) array1[j]=array[j]; array1[i]=n; for(int j=array.length; j>i; j--) array1[j]=array[j-1]; return new IntSet7(array1); } return this; } public boolean member(int n) { int from=0, to=array.length; int mid=(from+to)/2; while(from!=to&&array[mid]!=n) { if(array[mid]>n) to=mid; else from=mid+1; mid=(from+to)/2; } return from!=to; } public String toString() { int i; String str="{"; if(array.length>0) { str+=array[0]; for(i=1;i<array.length;i++) str+=","+array[i]; } return str+"}"; } }The code here is similar to the previous code, but rather than replacing the array in the object in the methods
add
and delete
a new object is created and returned.
Because the new object needs to be created with the new array
inside it, a private constructor is necessary which returns a set object
with the array object given as its argument inside. Since this
constructor is declared as private
it can only be used
inside the class IntSet7
, where it is used in just the
two places, in method delete
and in method add
.
It would not be appropriate to make this constructor public since it
might then be called with its argument an array that is not in
ascending numerical order, which would result in the creation of a
set object with its internal data not in the correct format which is
relied on by the other methods.
Note that if a call is made to delete
a number not
in the set, or to add
a number which is already in
the set, the method returns this
. The keyword
this
in a method means "the object to which this method
is attached" (so this
can't be used in static
methods since when they are called they are not attached to objects).
s.add(3);on its own (where
s
is a variable of type IntSet5
or one of the other set types) would have changed the set represented by
the variable s
to make it become one which includes the number 3.
In this new code,
s.add(3);on its own, where
s
is a variable of type IntSet7
is meaningless (although permitted as legal Java). The reason
for this is that it doesn't change s
but returns a new
set object, but nothing is done with that set object. The call
s.add(3)
only makes sense when used as part of another
statement, either assigned to a variable or as an argument to another
method call (or it could be used to represent an object on which another
method call is made).
If we have variables s1
and s2
of type
IntSet6
, suppose s1
represents the set
{1,2,5,8}. Then the call
s2=s1.add(3);will cause
s2
to represent the set
{1,2,3,5,8} while s1
still
represents the set {1,2,5,8}. The situation
following this statement is:
The add
method is referred to as a constructive
method because it constructs a new object with a new data structure.
In contrast, the method add
in the previous examples
can be referred to as destructive, since it has its effect
by changing the data structure of the object it is attached to, and
hence destroying the old value.
If a class has no destructive methods, objects of that class are
referred to as immutable. This means that once an object
of that type is created, its internal structure can never be changed.
If the internal structure of an object can be changed by the call of
some method (or if it has fields declared as public
but
not final
so they can be assigned new values directly),
the object is referred to as being mutable.
Note that since the add
and delete
methods
can return this
, a call to them can result in an object
being aliased. For example, if s1
represents the set
{1,2,5,8} and we call
s2=s1.add(5);since 5 is already in the set represented by
s1
the method
returns this
i.e. a reference to s1
so the
situation following the call is:
However, aliasing will only ever have an effect if there are destructive
methods that can be called on the aliased object. If not, there is no
way in which a change on the object referred to by one alias can effect
the (same) object referred to by the other alias, because there is no
way it can be changed at all.
Axioms
If we have an abstract data type, we need to ensure the code that
implements it not only gives methods with the correct signatures
but also that those methods behave correctly. So far we have simply
assumed an understanding of what sets mean, but if we wish to be more
rigorous we can specify the behaviour of an abstract data type using
mathematical rules called axioms.
The axioms that define the correct behaviour of any type that
implements the concept of a set are quite simple. Firstly, for
any i
, i
is not a member of
a newly created set. Secondly, for any i
and
set s
, i
is a member of the set
created by calling add(i)
on s
.
Thirdly, for any i
and
set s
, i
is a not a member of the set
created by calling delete(i)
on s
.
Fourthly, for any i
and j
and set s
, j
is a member of the
set obtained by adding i
to s
if
and only if j
is a member of s
providing i
is not equal to j
.
Fifthly, for any i
and j
and set s
, j
is a member of the
set obtained by deleting i
from s
if
and only if j
is a member of s
providing i
is not equal to j
.
Or formally:
1.
(new set()).member(i) == false
2. s.add(i).member(i) == true
3. s.delete(i).member(i) == false
4. s.add(i).member(j) == s.member(j) if i!=j
5. s.delete(i).member(j) == s.member(j) if i!=j
So long as a class provides the methods member
, add
and delete
and they behave such that the above rules are
always obeyed, the class is a correct implementation of the concept of
a set. Using these axioms, providing we follow the chain of method
calls used to create a set, we can always prove whether a particular number
is a member of that set or not.
Last modified: 9 February 2003