Combinar várias coleções em uma única coleção lógica?

110

Suponha que eu tenho um número constante de coleções (por exemplo, 3 ArrayLists) como membros de uma classe. Agora, quero expor todos os elementos a outras classes para que possam simplesmente iterar sobre todos os elementos (de preferência, somente leitura). Estou usando coleções de goiaba e me pergunto como poderia usar iteráveis ​​/ iteradores de goiaba para gerar uma visão lógica nas coleções internas sem fazer cópias temporárias.

Newgre
fonte
^^ Link quebrado. Acho que ele estava apontando para este método no Goiaba Javadoc
RustyTheBoyRobot

Respostas:

113

Com o Guava, você pode usar o Iterables.concat(Iterable<T> ...), ele cria uma visualização ao vivo de todos os iteráveis, concatenados em um (se você alterar os iteráveis, a versão concatenada também muda). Em seguida, envolva o iterável concatenado com Iterables.unmodifiableIterable(Iterable<T>)(eu não tinha visto o requisito somente leitura antes).

Dos Iterables.concat( .. )JavaDocs:

Combina vários iteráveis ​​em um único iterável. O iterável retornado possui um iterador que percorre os elementos de cada iterável nas entradas. Os iteradores de entrada não são pesquisados ​​até que sejam necessários. O iterador do iterável retornado é compatível remove() quando o iterador de entrada correspondente também.

Embora isso não diga explicitamente que se trata de uma visualização ao vivo, a última frase implica que é (suportar o Iterator.remove()método apenas se o iterador de apoio oferecer suporte, não é possível, a menos que use uma visualização ao vivo)

Código de amostra:

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);

Resultado:

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


Editar:

Por solicitação de Damian, aqui está um método semelhante que retorna uma visualização de coleção ao 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
fonte
Como evito que o usuário remova elementos? Existe uma maneira mais agradável do que agrupar as listas em listas não modificáveis?
newgre
2
@jn_ é só embrulharIterables.unmodifiableIterable(iterable)
Sean Patrick Floyd
2
E as coleções? Iterables.concatproduz um Iterable, não Collection. Eu precisaria de uma Collectionvisão.
Nowaker
@Damian a única característica útil disso seria ter um método size () agregado. Todos os outros métodos na interface de coleção teriam semântica indefinida (adicionar etc) ou desempenho péssimo (contém etc).
Sean Patrick Floyd
2
@Sean, sim - size()é o que eu preciso. add()lançar uma exceção é bom - eu não me importo com esse método. A API de coleções está quebrada e ninguém pode fazer nada a respeito. Collection.add(), Iterator.remove(), Blá.
Nowaker
101

Soluções simples do Java 8 usando a Stream.

Número constante

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

Uma solução:

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

Outra solução:

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

Número variável

Supondo private Collection<Collection<T>> cs:

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

Se você estiver usando pelo menos Java 8, veja minha outra resposta .

Se você já usa o Google Guava, veja a resposta de Sean Patrick Floyd .

Se você estiver travado no Java 7 e não quiser incluir o Google Guava, poderá escrever o seu próprio (somente leitura) Iterables.concat()usando no máximo Iterablee 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 variável

@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
fonte
1

Você poderia criar um novo Liste addAll()de seus outros Listpara ele. Em seguida, retorne uma lista não modificável com Collections.unmodifiableList().

Qwerky
fonte
3
Isso criaria uma nova coleção temporária que é potencialmente muito cara
newgre
6
Caro como , os objetos subjacentes nas listas não são copiados e ArrayListapenas alocam o espaço e as chamadas System.arraycopy()sob o capô. Não pode ser muito mais eficiente do que isso.
Qwerky
8
Como copiar uma coleção inteira para cada iteração não é caro? Além disso, você pode ficar melhor do que isso, veja a resposta de Seans.
newgre
Ele também usa uma implementação nativa para copiar a memória, não itera através do array.
Qwerky
1
Bem, se ele está copiando o array, certamente é um algoritmo O (n) que não é escalonado e tem a mesma complexidade de uma iteração no array uma vez. Suponha que cada lista contenha um milhão de elementos, então preciso copiar alguns milhões de elementos, apenas para iterar sobre eles. Péssima ideia.
newgre
0

Aqui está minha solução para isso:

EDIT - código alterado um pouco

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
fonte
Sua implementação inverte as funções de hasNext()e next(). O primeiro muda o estado do seu iterador, enquanto o segundo não. Deveria ser o contrário. Chamar next()sem chamar hasNext()sempre renderá null. Chamar hasNext()sem chamar next()jogará elementos fora. Você next()também não joga NoSuchElementException, mas retorna null.
xehpuk