¿Combinar varias colecciones en una única colección lógica?

110

Supongamos que tengo un número constante de colecciones (por ejemplo, 3 ArrayLists) como miembros de una clase. Ahora, quiero exponer todos los elementos a otras clases para que simplemente puedan iterar sobre todos los elementos (idealmente, solo lectura). Estoy usando colecciones de guayaba y me pregunto cómo podría usar iterables / iteradores de guayaba para generar una vista lógica de las colecciones internas sin hacer copias temporales.

Newgre
fuente
^^ Enlace roto. Creo que estaba señalando este método en el Javadoc de Guava
RustyTheBoyRobot

Respuestas:

113

Con Guava, puede usar Iterables.concat(Iterable<T> ...), crea una vista en vivo de todos los iterables, concatenados en uno (si cambia los iterables, la versión concatenada también cambia). Luego envuelva el iterable concatenado con Iterables.unmodifiableIterable(Iterable<T>)(no había visto antes el requisito de solo lectura).

Desde Iterables.concat( .. )JavaDocs:

Combina varios iterables en uno solo. El iterable devuelto tiene un iterador que atraviesa los elementos de cada iterable en las entradas. Los iteradores de entrada no se sondean hasta que son necesarios. El iterador del iterable devuelto admite remove() cuando el iterador de entrada correspondiente lo admite.

Si bien esto no dice explícitamente que se trata de una vista en vivo, la última oración implica que lo es (admitir el Iterator.remove()método solo si el iterador de respaldo lo admite no es posible a menos que se use una vista en vivo)

Código de muestra:

final List<Integer> first  = Lists.newArrayList(1, 2, 3);
final List<Integer> second = Lists.newArrayList(4, 5, 6);
final List<Integer> third  = Lists.newArrayList(7, 8, 9);
final Iterable<Integer> all =
    Iterables.unmodifiableIterable(
        Iterables.concat(first, second, third));
System.out.println(all);
third.add(9999999);
System.out.println(all);

Salida:

[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 9999999]


Editar:

Por solicitud de Damian, aquí hay un método similar que devuelve una vista de colección en vivo

public final class CollectionsX {

    static class JoinedCollectionView<E> implements Collection<E> {

        private final Collection<? extends E>[] items;

        public JoinedCollectionView(final Collection<? extends E>[] items) {
            this.items = items;
        }

        @Override
        public boolean addAll(final Collection<? extends E> c) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void clear() {
            for (final Collection<? extends E> coll : items) {
                coll.clear();
            }
        }

        @Override
        public boolean contains(final Object o) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean containsAll(final Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean isEmpty() {
            return !iterator().hasNext();
        }

        @Override
        public Iterator<E> iterator() {
            return Iterables.concat(items).iterator();
        }

        @Override
        public boolean remove(final Object o) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean removeAll(final Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean retainAll(final Collection<?> c) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int size() {
            int ct = 0;
            for (final Collection<? extends E> coll : items) {
                ct += coll.size();
            }
            return ct;
        }

        @Override
        public Object[] toArray() {
            throw new UnsupportedOperationException();
        }

        @Override
        public <T> T[] toArray(T[] a) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean add(E e) {
            throw new UnsupportedOperationException();
        }

    }

    /**
     * Returns a live aggregated collection view of the collections passed in.
     * <p>
     * All methods except {@link Collection#size()}, {@link Collection#clear()},
     * {@link Collection#isEmpty()} and {@link Iterable#iterator()}
     *  throw {@link UnsupportedOperationException} in the returned Collection.
     * <p>
     * None of the above methods is thread safe (nor would there be an easy way
     * of making them).
     */
    public static <T> Collection<T> combine(
        final Collection<? extends T>... items) {
        return new JoinedCollectionView<T>(items);
    }

    private CollectionsX() {
    }

}
Sean Patrick Floyd
fuente
¿Cómo puedo evitar que el usuario elimine elementos? ¿Hay una forma mejor de envolver las listas en listas no modificables?
Newgre
2
@jn_ simplemente envuélvaloIterables.unmodifiableIterable(iterable)
Sean Patrick Floyd
2
¿Y las colecciones? Iterables.concatproduce un Iterable, no Collection. Necesitaría una Collectionvista.
Nowaker
@Damian, la única característica útil de eso sería tener un método de tamaño agregado (). Todos los demás métodos en la interfaz de Colección tendrían semántica indefinida (agregar, etc.) o un rendimiento miserable (contiene, etc.).
Sean Patrick Floyd
2
@Sean, sí, size()es lo que necesito. add()lanzar una excepción es bueno, no me importa este método. La API de colecciones está rota y nadie puede hacer nada al respecto. Collection.add(), Iterator.remove()bla.
Nowaker
101

Soluciones simples de Java 8 que utilizan un Stream.

Número constante

Asumiendo private Collection<T> c, c2, c3.

Una solución:

public Stream<T> stream() {
    return Stream.concat(Stream.concat(c.stream(), c2.stream()), c3.stream());
}

Otra solución:

public Stream<T> stream() {
    return Stream.of(c, c2, c3).flatMap(Collection::stream);
}

Número variable

Asumiendo private Collection<Collection<T>> cs:

public Stream<T> stream() {
    return cs.stream().flatMap(Collection::stream);
}
xehpuk
fuente
10

Si está usando al menos Java 8, vea mi otra respuesta .

Si ya está utilizando Google Guava, consulte la respuesta de Sean Patrick Floyd .

Si está atascado en Java 7 y no desea incluir Google Guava, puede escribir el suyo propio (solo lectura) Iterables.concat()usando no más de Iterabley Iterator:

Número constante

public static <E> Iterable<E> concat(final Iterable<? extends E> iterable1,
                                     final Iterable<? extends E> iterable2) {
    return new Iterable<E>() {
        @Override
        public Iterator<E> iterator() {
            return new Iterator<E>() {
                final Iterator<? extends E> iterator1 = iterable1.iterator();
                final Iterator<? extends E> iterator2 = iterable2.iterator();

                @Override
                public boolean hasNext() {
                    return iterator1.hasNext() || iterator2.hasNext();
                }

                @Override
                public E next() {
                    return iterator1.hasNext() ? iterator1.next() : iterator2.next();
                }
            };
        }
    };
}

Número variable

@SafeVarargs
public static <E> Iterable<E> concat(final Iterable<? extends E>... iterables) {
    return concat(Arrays.asList(iterables));
}

public static <E> Iterable<E> concat(final Iterable<Iterable<? extends E>> iterables) {
    return new Iterable<E>() {
        final Iterator<Iterable<? extends E>> iterablesIterator = iterables.iterator();

        @Override
        public Iterator<E> iterator() {
            return !iterablesIterator.hasNext() ? Collections.emptyIterator()
                                                : new Iterator<E>() {
                Iterator<? extends E> iterableIterator = nextIterator();

                @Override
                public boolean hasNext() {
                    return iterableIterator.hasNext();
                }

                @Override
                public E next() {
                    final E next = iterableIterator.next();
                    findNext();
                    return next;
                }

                Iterator<? extends E> nextIterator() {
                    return iterablesIterator.next().iterator();
                }

                Iterator<E> findNext() {
                    while (!iterableIterator.hasNext()) {
                        if (!iterablesIterator.hasNext()) {
                            break;
                        }
                        iterableIterator = nextIterator();
                    }
                    return this;
                }
            }.findNext();
        }
    };
}
xehpuk
fuente
1

Podrías crear uno nuevo Listy addAll()de tus otros Listal mismo. Luego devuelve una lista no modificable con Collections.unmodifiableList().

Qwerky
fuente
3
Eso crearía una nueva colección temporal que es potencialmente bastante cara
newgre
6
Caro forma , los objetos subyacentes en las listas no se copian y ArrayListsimplemente asigna el espacio y las llamadas System.arraycopy()bajo el capó. No puede ser mucho más eficiente que eso.
Qwerky
8
¿Por qué no es caro copiar una colección completa para cada iteración ? Además, puede ser mejor que eso, vea la respuesta de Seans.
Newgre
También usa una implementación nativa para copiar la memoria, no itera a través de la matriz.
Qwerky
1
Bueno, si está copiando la matriz, ciertamente es un algoritmo O (n) que no se escala y tiene la misma complejidad que iterar sobre la matriz una vez. Supongamos que cada lista contiene un millón de elementos, luego necesito copiar algunos millones de elementos, solo para iterar sobre ellos. Mala idea.
Newgre
0

Aquí está mi solución para eso:

EDITAR - código cambiado un poco

public static <E> Iterable<E> concat(final Iterable<? extends E> list1, Iterable<? extends E> list2)
{
    return new Iterable<E>()
    {
        public Iterator<E> iterator()
        {
            return new Iterator<E>()
            {
                protected Iterator<? extends E> listIterator = list1.iterator();
                protected Boolean checkedHasNext;
                protected E nextValue;
                private boolean startTheSecond;

                public void theNext()
                {
                    if (listIterator.hasNext())
                    {
                        checkedHasNext = true;
                        nextValue = listIterator.next();
                    }
                    else if (startTheSecond)
                        checkedHasNext = false;
                    else
                    {
                        startTheSecond = true;
                        listIterator = list2.iterator();
                        theNext();
                    }
                }

                public boolean hasNext()
                {
                    if (checkedHasNext == null)
                        theNext();
                    return checkedHasNext;
                }

                public E next()
                {
                    if (!hasNext())
                        throw new NoSuchElementException();
                    checkedHasNext = null;
                    return nextValue;

                }

                public void remove()
                {
                    listIterator.remove();
                }
            };
        }
    };
}
chmouel kalifa
fuente
Tu implementación cambia los roles de hasNext()y next(). El primero cambia el estado de su iterador mientras que el segundo no lo hace. Debería ser de otra manera. Llamar next()sin llamar hasNext()siempre cederá null. Llamar hasNext()sin llamar next()arrojará elementos. Tu next()tampoco lanza NoSuchElementException, sino que regresa null.
xehpuk