eclipse-collections icon indicating copy to clipboard operation
eclipse-collections copied to clipboard

Implement boxing wrappers for primitive collections.

Open motlin opened this issue 2 years ago • 15 comments

Sometimes, one may want to pass a primitive collection into a method that takes a JCF interface. In cases like this, users must transform the primitive collection into an object collection to proceed.

public static void printAll(List<?> list)
{
    System.out.println("list = " + Iterate.makeString(list, ", "));
}


public static void main(String[] args)
{
    MutableIntList list = IntLists.mutable.with(1, 2, 3);
    printAll(list.collect(Integer::valueOf));
}

One option would be to make our primitive collections implement JCF interfaces. Fastutils does this; for example its IntArrayList implements List<Integer>

The downside is that it's very easy to accidentally cause boxing by using the primitive collection inside an enhanced for-loop.

A compromise that I believe would be pragmatic would be to implement a boxing wrappers for primitive collections, and a new method boxed(). If it existed, the example above would become:

    printAll(list.boxed());

The boxed() method would take O(1) time and space since it returns a wrapper.

To prove the idea, I wrote one wrapper. If we like the idea, we'd need to code generate it for all the primitives, and expand the functionality from just lists to sets and maps.

public class BoxedMutableIntList
        extends AbstractMutableList<Integer>
        implements MutableList<Integer>, RandomAccess
{
    private final MutableIntList delegate;

    public BoxedMutableIntList(MutableIntList delegate)
    {
        this.delegate = Objects.requireNonNull(delegate);
    }

    @Override
    public int size()
    {
        return this.delegate.size();
    }

    @Override
    public boolean isEmpty()
    {
        return this.delegate.isEmpty();
    }

    @Override
    public boolean contains(Object o)
    {
        return o instanceof Integer && this.delegate.contains((Integer) o);
    }

    @Override
    public boolean add(Integer integer)
    {
        return this.delegate.add(integer.intValue());
    }

    @Override
    public boolean remove(Object o)
    {
        return o instanceof Integer && this.delegate.remove((Integer) o);
    }

    @Override
    public boolean addAll(int index, Collection<? extends Integer> c)
    {
        return this.delegate.addAllAtIndex(index, c.stream().mapToInt(Integer::intValue).toArray());
    }

    @Override
    public void clear()
    {
        this.delegate.clear();
    }

    @Override
    public Integer get(int index)
    {
        return this.delegate.get(index);
    }

    @Override
    public Integer set(int index, Integer element)
    {
        return this.delegate.set(index, element.intValue());
    }

    @Override
    public void add(int index, Integer element)
    {
        this.delegate.addAtIndex(index, element.intValue());
    }

    @Override
    public Integer remove(int index)
    {
        return this.delegate.removeAtIndex(index);
    }

    @Override
    public int indexOf(Object o)
    {
        return o instanceof Integer ? this.delegate.indexOf((Integer) o) : -1;
    }

    @Override
    public int lastIndexOf(Object o)
    {
        return o instanceof Integer ? this.delegate.lastIndexOf((Integer) o) : -1;
    }

    @Override
    public MutableList<Integer> subList(int fromIndex, int toIndex)
    {
        return this.delegate.subList(fromIndex, toIndex).boxed();
    }
}

motlin avatar Oct 18 '22 17:10 motlin

Plain JCF or EC List?

mohrezaei avatar Oct 18 '22 17:10 mohrezaei

I was thinking plain JCF when I wrote up the issue. But I'm looking at what it would take to make boxed() return MutableList instead of List, and it looks only marginally more effort.

motlin avatar Oct 18 '22 17:10 motlin

Updated the example wrapper to implement MutableList. For some reason, I had to implement just one more method, addAll.

motlin avatar Oct 18 '22 20:10 motlin

If we do this, are we then going to have endless enhancements to optimize the other methods on there? (It's unclear to me why sublist was overridden, for example).

mohrezaei avatar Oct 18 '22 22:10 mohrezaei

I implemented sublist() just to get the wrapper compiling, by delegating to the primitive sublist and then boxing that. I realized afterwards that it won't work anyway because the implementation of IntArrayList is:

    @Override
    public MutableIntList subList(int fromIndex, int toIndex)
    {
        throw new UnsupportedOperationException("subList not yet implemented!");
    }

But if someone implements a proper sublist, then sublist(start, end).boxed() could start working.

In the code I pasted, few of the implementations are there for optimization. I didn't start by extending AbstractList or AbstractMutableList so it's possible I could delete a few more methods. I think I can get rid of indexOf and lastIndexOf for example. I'll try to minimize it again and update.

motlin avatar Oct 18 '22 22:10 motlin

confused... my copy of AbstractMutableList has this:

    @Override
    public MutableList<T> subList(int fromIndex, int toIndex)
    {
        return new SubList<>(this, fromIndex, toIndex);
    }

why is that not compiling?

mohrezaei avatar Oct 18 '22 22:10 mohrezaei

I see what you mean. I had started with List and AbstractList which don't give an implementation of subList(). It's actually easier to implement MutableList than List. Here's the new smaller implementation.

public class BoxedMutableIntList
        extends AbstractMutableList<Integer>
        implements MutableList<Integer>, RandomAccess
{
    private final MutableIntList delegate;

    public BoxedMutableIntList(MutableIntList delegate)
    {
        this.delegate = Objects.requireNonNull(delegate);
    }

    @Override
    public int size()
    {
        return this.delegate.size();
    }

    @Override
    public boolean add(Integer integer)
    {
        return this.delegate.add(integer.intValue());
    }

    @Override
    public boolean addAll(int index, Collection<? extends Integer> c)
    {
        return this.delegate.addAllAtIndex(index, c.stream().mapToInt(Integer::intValue).toArray());
    }

    @Override
    public void clear()
    {
        this.delegate.clear();
    }

    @Override
    public Integer get(int index)
    {
        return this.delegate.get(index);
    }

    @Override
    public Integer set(int index, Integer element)
    {
        return this.delegate.set(index, element.intValue());
    }

    @Override
    public void add(int index, Integer element)
    {
        this.delegate.addAtIndex(index, element.intValue());
    }

    @Override
    public Integer remove(int index)
    {
        return this.delegate.removeAtIndex(index);
    }
}

motlin avatar Oct 18 '22 22:10 motlin

That looks good to me, so long as there is a clear comment that says something to effect of "this is for scenarios where optimization is not a consideration and therefore no further methods will be optimized".

mohrezaei avatar Oct 18 '22 22:10 mohrezaei

I could help out with this.

goldbal330 avatar Jan 17 '23 17:01 goldbal330

There is no c.stream().mapToFloat Hence, this method can't be implemented for float:

@Override public boolean addAll(int index, Collection<? extends Float> c) { return this.delegate.addAllAtIndex(index, c.stream().mapToFloat(Float::floatValue).toArray()); }

Any suggestions to avoid creating an intermediate List that will be flattened into array?

goldbal330 avatar Jan 25 '23 21:01 goldbal330

Any suggestions to avoid creating an intermediate List that will be flattened into array?

The only efficient way I can think of is to add a bunch of zeros at that index (the same size as the collection), then set the actual values by iterating through the collection. If the index is at the end, adding zeros should be skipped.

mohrezaei avatar Jan 26 '23 10:01 mohrezaei

As far as I can tell, it's not been brought up: why not two options (i.e. .boxCopy() and .boxView()) where one does all of the boxing at once and the other is a wrapper? This allows the former to be used in the case that the collection should be moved into/owned by some consumer, and the latter for when a helper is using the collection sparsely. I'm not familiar with optimizing collections operations on the JVM so please forgive me if I'm missing something.

s5bug avatar Jan 01 '24 03:01 s5bug

@s5bug If you really want a boxed collection, we already have a decent way to perform that with collect().

MutableList<Integer> boxed = IntLists.mutable.with(1, 2, 3).collect(each -> each);

Sorry for the delay - please let me know what you think!

motlin avatar Jan 08 '24 22:01 motlin

This doesn't seem to exist for Maps, in which my specific use-case is ObjectLongMap<UUID>. (And this is only for copies and not for views, right?)

s5bug avatar Jan 08 '24 23:01 s5bug

That's a really good point about maps - I don't think we have any form of collect() or any other method that will created object Maps.

Once we do support boxed, a pattern like this will be possible.

ObjectLongMap<UUID> map = ...;
MapIterable<UUID, Long> boxed = map.boxed();
MutableMap<UUID, Long> objectMap = Maps.mutable.withMapIterable(boxed);

That's still an extra step. The idea of separate methods for boxed views and copies is a nice one and I'm going to keep it in mind. I'm curious if other collections frameworks have both.

motlin avatar Jan 09 '24 00:01 motlin