0

My son got a programming problem to create a generic array of things in Java. You can only pass the initial capacity in as a parameter to the constructor. You can't use an Object array. This is ridiculously hard. The question at How to create a generic array in Java? has some answers, but none of them work and I have a new account and can't comment on that thread.

The solution we came up with was to do lazy allocation of the array. So you have:

public class ArraySet<E> {
    private E[] array;
    private int size;
    private int capacity;
    private int start_capacity;
    private Class<E> baseclass;
    
    public ArraySet(int orig_capacity) {
        start_capacity = orig_capacity;
    }
    
    public boolean add(E e) {
    if (capacity == size) {
        if (capacity == 0) {
            capacity = start_capacity;
            array = (E[]) Array.newInstance(e.getClass(), capacity);
        } else {
            capacity *= 2;
            E[] newarray = (E[]) Array.newInstance(e.getClass(),
                               capacity);
            System.arraycopy(array, 0, newarray, 0, size);
            array = newarray;
        }
    }
    // Add the element ot the array...

The only trouble I can see with this is if you pass in different types to add() you will get bad results. But other than this, I can't see a way to do it.

I suppose I could save off the class of "e" and make sure anything added is that class.

Is there anything else I am missing? This seems too simple that everyone in the other thread missed it, but it seems to work.

  • If you're doing what I think you're doing, it's impossible. As you say, passing a sub-type of `E` in the `add()` method is going to produce intermittent type errors. Using `new Object[]` is the correct solution (c.f. Joshua Block *Effective Java*). Your son's instructor doesn't know what they are doing. – markspace Apr 11 '23 at 17:05
  • You can only add E or children of E, so it is okay. You could use `Arrays.copyOf`. – Joop Eggen Apr 11 '23 at 17:12
  • `e.getClass()` yep, that's a bad idea. `Object[]` it is. – njzk2 Apr 11 '23 at 17:21
  • 1
    (not to mention that `e.getClass()` will throw an NPE once in a while :) ) – njzk2 Apr 11 '23 at 17:23

3 Answers3

2

A correct solution would be to use new Object[] and cast. If that isn't what your son's instructor had in mind then it isn't possible to solve. A first pass at a solution:

public class ArraySet<E> {
    private E[] array;
    private int size;
    
    public ArraySet(int orig_capacity) {
        array = (E[]) new Object[orig_capacity];
    }
    
    public boolean add(E e) {
      if (array.length == size) {
        array = Arrays.copyOf( array, array.length *2 );
      }
      array[size++] = e;
    }

Not syntax checked.

markspace
  • 10,621
  • 3
  • 25
  • 39
0

Your solution doesn't work. Here, trivial test case to make it fail:

var set = new ArraySet<Number>();
set.add(5); // adds an integer
set.add(10.0); // adds a double

This code should be completely fine. Both integers and doubles are, after all, subtypes of number. But that will fail, with an ArrayStoreException, because you end up making a Integer[] under the hood which will throw that exception when you try to shove a Double in there.

This is ridiculously hard.

That's a really funny way of saying "this is impossible". Because it is impossible, not 'hard'. Unless you classify impossible as a particularly egregious case of 'hard', which, I guess, why not :)

The reason it is impossible is that java erases this stuff. If what you wanted was possible, then someone would have written a reifiable version of ArrayList 15 years ago (Reifiable is, amongst other things: You can ask the ArrayList at runtime what its component type is).

Conclusion

You / your boy misunderstood the assignment. That, or, the teacher is clueless. It's possible.

So what should you do?

The java community at large knows exactly how to do this, and it is with an Object array. Here is the source code of java.util.ArrayList which is:

  • Exactly the functionality you seem to have been asked to implement.
  • Is, outside of java.lang, the most used java class on the planet.
  • As a general rule, the openjdk team knows what they are doing.

They use Object[]. Because it's the right way to do this. E[]? Fundamentally wrong - it's not possible to make those, at best you can ugly-cast an Object[] to it, but it's still an Object[] - just cast to a type it really is not, with the compiler warnings that go along with doing stuff that doesn't really make sense.

What does the assignment want you to do?

Who knows - as I said, you misunderstood, or the teacher is clueless / this is one of those socratesian projects whose aim is for you to learn that this doesn't work, maybe.

One option, but this is strictly worse, it has no benefits and quite a few downsides, is to require the code that constructs one of these ArraySet objects to pass in the type. It looks like this:

class ArraySet<E> {
  E[] data;

  public ArraySet(Class<E> type, int capacity) {
    this.data = Array.newInstance(type, capacity);
  }

  // ....
}

But this has quite a few downsides:

  • The caller now needs to explicitly pass in stuff. They HAVE to write:
ArraySet<String> myStrings = new ArraySet<>(String.class, 20);

... for absolutely no good reason whatsoever. This code isn't any faster and it's not any 'safer' unless the user of your library is in the habit of intentionally ignoring compiler warnings. Which gets us to a simple adage: Ooooh, trust me, programming is hard and morons are incredibly inventive. You cannot stop an idiot from creating bugs in this way. This is a very counterproductive lesson if that is the stated reason for trying to do it this way.

  • Class<E> cannot, itself, convey generics. If you want to create an ArraySet<String> that doesn't matter much. But imagine you are trying to make an ArraySet<FileSystem<Path>> for example. That simply doesn't work. FileSystem<Path>.class isn't a thing (class objects cannot convey generics).

So, you've made it harder to use your arrayset, and completely eliminated a sizable batch of use cases (all cases where the element type itself also contains generics), for absolutely no relevant gain of any sort.

You now have a strong appeal to authority (openjdk team itself uses an Object[]), as well as 2 strong objective reasons (needlessly complicated to call the constructor, eliminates significant use cases for no gain).

If this teacher explains their bizarre thoughts (or, the assignment was misunderstood), if you could report back to sate my curiosity I would appreciate it :)

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
-2
import java.lang.reflect.Array;

public class ArraySet<E> {
    private E[] array;
    private int size;
    private int capacity;
    private int start_capacity;
    private Class<E> baseclass;

    public ArraySet(int orig_capacity) {
        start_capacity = orig_capacity;
    }

    public boolean add(E e) {
        if (e == null) {
            throw new IllegalArgumentException("Null values are not allowed.");
        }
        if (baseclass == null) {
            baseclass = (Class<E>) e.getClass();
        } else if (!baseclass.isInstance(e)) {
            throw new IllegalArgumentException("Element type does not match the expected type.");
        }

        if (capacity == size) {
            if (capacity == 0) {
                capacity = start_capacity;
                array = (E[]) Array.newInstance(baseclass, capacity);
            } else {
                capacity *= 2;
                E[] newarray = (E[]) Array.newInstance(baseclass, capacity);
                System.arraycopy(array, 0, newarray, 0, size);
                array = newarray;
            }
        }
        // Add the element to the array...
        array[size++] = e;
        return true;
    }
}
LSDeva
  • 444
  • 4
  • 8
  • a bit of explanation would go a long way. Your IAE seems to forbid adding a subtype as first object (only), is that on purpose? – njzk2 Apr 11 '23 at 17:23