This question involves adding default methods to the GenericSet
interface that you designed in question b401.
Note: if you had trouble completing question b401, you have two choices:
-
You could attempt this question by starting with the question b401 sample solution;
-
You could add default methods to the
IntSet
interface of question 8a61, rather than to its generic version.
Let's suppose that our GenericSet
interface has become popular, and that we now regret not having included some additional methods when we designed the interface. We'll go through how to use default methods to fill these omissions.
-
Add a
default
method to theGenericSet
interface calledaddAll
. TheaddAll
method havevoid
return type, and should take an array,items
, of typeE
as its single parameter, whereE
is the generic parameter associated withGenericSet
(useInteger
orint
instead ofE
if you are working onIntSet
rather thanGenericSet
).For each element
item
initems
, theaddAll
method should use theadd
method to additems
to the set. -
In a
Demo
class, write a main method that creates aMemoryEfficientGenericSet
of integers and aSpeedEfficientGenericSet
of integers, creates some small arrays of integers, and usesaddAll
to add the arrays of integers to each of the sets. Run yourmain
method to check that it behaves appropriately.Now change your
main
method so that it creates some large arrays of integers, and usesaddAll
to add the large arrays of integers to each of the sets.(You could look at
Demo.java
from the sample solution for some example code here; seesolutions/code/tutorialquestions/question336b/Demo.java
.)You will probably find that
addAll
takes an excessively long time to add a large array of integers to aMemoryEfficientGenericSet
. Think about why this is. -
The default implementation of
addAll
works for any set, but as demonstrated in step 2 is not efficient forMemoryEfficientGenericSet
s. OverrideaddAll
inMemoryEfficientGenericSet
with an implementation that does not invoke the defaultaddAll
implementation, but instead uses aHashSet
local variable to keep track of the contents of the set. This hash set should be initialized with the contents of theMemoryEfficientGenericSet
. Then, for each element ofitems
(the parameter toaddAll
), you should first check whether the element is in the hash set. If it is, you should move on to the next element; otherwise you should add it directly to the elements of theMemoryEfficientGenericSet
(without going through theadd
method), and also add it to the hash set of elements that are known to be in the set.You should find that, with this specific implementation of
addAll
, the code in yourmain
method behaves in an efficient manner. Think hard to make sure you understand why this is the case. -
Add one more default method to
GenericSet
. This method should be calledasUnmodifiableSet
. It should take no parameters, and should return aGenericSet<E>
. The generic set that is returned should be a version of the original set where any methods that could cause the set to be modified are replaced with methods that throw anUnsupportedOperationException
.There are two ways you could approach this. The simple, but somewhat verbose way, is to make a new class,
UnmodifiableGenericSet
that implements theGenericSet<E>
interface. This class should have a single field of typeGenericSet<E>
representing the set for which an unmodifiable version is being created; this field could be calledwrapped
. The new class should implement all of the required methods ofGenericSet<E>
. The non-mutating methods should be implemented by delegation towrapped
: invoking the corresponding method, with the same parameters (if any) onwrapped
, and returning the result (if any) returned by said method. The mutating methods should be implemented by simply throwing anUnsupportedOperationException
.Recall from question 85bb) that we avoided the need for writing a fully separate iterator class by using an inner class. It is not possible for an interface to have an inner class (it can have a nested class, but not an inner class; read online about nested classes to understand the difference).
However, we can use an anonymous inner class in the implementation of
asUnmodifiableSet
, by having the body ofasUnmodifiableSet
look like this:return new GenericSet<E>() { @Override public void add(E item) { throw new UnsupportedOperationException("Attempt to add to an unmodifiable set."); } ... /* Implementations of other methods */ ... };
This returns an instance of a new class that implements the
GenericSet<E>
interface, implementing its methods according to the method implementations given between the{
and}
followingnew GenericSet<E>
.If you follow this approach, your anonymous inner class will need to call methods of the
GenericSet<E>
that it is defined inside. This can be achieved by using the syntaxGenericSet.this
. So, for example, in a method inside the anonymous inner class,isEmpty()
would call theisEmpty
method of the anonymous inner class, whileGenericSet.this.isEmpty()
would call theisEmpty
method of the object implementing the outerGenericSet<E>
interface.Try both implementation approaches -- using an explicit
UnmodifiableGenericSet
class and an anonymous inner class, and see which you prefer. Beyond the amount of code you have to write, can you see any pros and cons of the two approaches? -
Adapt your
main
method so that it usesasUnmodifiableSet
to create unmodifiable versions of some sets, and check that the unmodifiable sets behave just like the sets that they wrap if non-mutator methods are called, and that appropriate exceptions are thrown if mutator methods are called. Notice that, due to the use of a default method, you can work with unmodifiable versions ofMemoryEfficientGenericSet
s andSpeedEfficientGenericSet
s without having had to modify these classes.