O Java 8 oferece uma boa maneira de repetir um valor ou função?

118

Em muitas outras línguas, por exemplo. Haskell, é fácil repetir um valor ou função várias vezes, por exemplo. para obter uma lista de 8 cópias do valor 1:

take 8 (repeat 1)

mas ainda não encontrei isso no Java 8. Essa função existe no JDK do Java 8?

Ou, alternativamente, algo equivalente a um intervalo como

[1..8]

Pareceria um substituto óbvio para uma instrução detalhada em Java como

for (int i = 1; i <= 8; i++) {
    System.out.println(i);
}

ter algo como

Range.from(1, 8).forEach(i -> System.out.println(i))

embora este exemplo particular não pareça muito mais conciso na verdade ... mas espero que seja mais legível.

Graeme Moss
fonte
2
Você estudou a API Streams ? Essa deve ser sua melhor aposta no que diz respeito ao JDK. Ele tem uma função de alcance , é o que descobri até agora.
Marko Topolnik
1
@MarkoTopolnik A classe Streams foi removida (mais precisamente ela foi dividida entre várias outras classes e alguns métodos foram completamente removidos).
assylias
3
Você chama um loop for prolixo! É uma coisa boa você não estar por perto na época do Cobol. Foram necessárias mais de 10 declarações declarativas em Cobol para exibir números crescentes. Os jovens de hoje não apreciam o quão bom eles têm.
Gilbert Le Blanc
1
@GilbertLeBlanc verbosity não tem nada a ver com isso. Loops não são combináveis, Streams sim. Os loops levam à repetição inevitável, enquanto os Streams permitem a reutilização. Como tais, Streams são uma abstração quantitativamente melhor do que loops e devem ser preferidos.
Alain O'Dea
2
@GilbertLeBlanc e tivemos que programar descalços, na neve.
Dawood ibn Kareem

Respostas:

155

Para este exemplo específico, você poderia fazer:

IntStream.rangeClosed(1, 8)
         .forEach(System.out::println);

Se precisar de uma etapa diferente de 1, você pode usar uma função de mapeamento, por exemplo, para uma etapa de 2:

IntStream.rangeClosed(1, 8)
         .map(i -> 2 * i - 1)
         .forEach(System.out::println);

Ou crie uma iteração personalizada e limite o tamanho da iteração:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);
assilias
fonte
4
Os fechamentos transformarão completamente o código Java, para melhor. Ansioso por esse dia ...
Marko Topolnik
1
@jwenting Realmente depende - normalmente com coisas de GUI (Swing ou JavaFX), que remove um monte de boiler plate devido a classes anônimas.
assylias
8
@jwenting Para qualquer pessoa com experiência em FP, o código que gira em torno de funções de ordem superior é uma vitória pura. Para qualquer pessoa sem essa experiência, é hora de atualizar suas habilidades - ou arriscar ser deixado para trás na poeira.
Marko Topolnik
2
@MarkoTopolnik Você pode querer usar uma versão um pouco mais recente do javadoc (você está apontando para a compilação 78, a última é a compilação 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel
1
@GraemeMoss Você ainda poderia usar o mesmo pattern ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) mas confunde IMO e nesse caso um loop parece indicado.
assylias
65

Aqui está outra técnica que usei outro dia:

Collections.nCopies(8, 1)
           .stream()
           .forEach(i -> System.out.println(i));

A Collections.nCopieschamada cria um Listcontendo ncópias de qualquer valor que você fornece. Nesse caso, é o Integervalor 1 na caixa . É claro que, na verdade, ele não cria uma lista com nelementos; ele cria uma lista "virtualizada" que contém apenas o valor e o comprimento, e qualquer chamada para getdentro do intervalo apenas retorna o valor. onCopies método existe desde que o Collections Framework foi introduzido no JDK 1.2. Obviamente, a capacidade de criar um fluxo a partir de seu resultado foi adicionada ao Java SE 8.

Grande coisa, outra maneira de fazer a mesma coisa com aproximadamente o mesmo número de linhas.

No entanto, esta técnica é mais rápido do que o IntStream.generatee IntStream.iteratese aproxima e, surpreendentemente, é também mais rápido do que a IntStream.rangeabordagem.

Pois iteratee generateo resultado talvez não seja muito surpreendente. A estrutura de fluxos (na verdade, os divisores para esses fluxos) é construída na suposição de que os lambdas irão potencialmente gerar valores diferentes a cada vez e que eles irão gerar um número ilimitado de resultados. Isso torna a divisão paralela particularmente difícil. O iteratemétodo também é problemático para este caso porque cada chamada requer o resultado da anterior. Portanto, os fluxos usando generatee iteratenão funcionam muito bem para gerar constantes repetidas.

O desempenho relativamente baixo de rangeé surpreendente. Isso também é virtualizado, de modo que nem todos os elementos existem realmente na memória e o tamanho é conhecido desde o início. Isso deve resultar em um divisor de fácil paralelização rápido e fácil. Mas, surpreendentemente, não foi muito bem. Talvez a razão seja que rangetem que calcular um valor para cada elemento do intervalo e então chamar uma função sobre ele. Mas essa função simplesmente ignora sua entrada e retorna uma constante, então estou surpreso que isso não seja embutido e eliminado.

A Collections.nCopiestécnica tem que fazer boxing / unboxing para lidar com os valores, já que não existem especializações primitivas de List. Como o valor é o mesmo todas as vezes, é basicamente embalado uma vez e essa caixa é compartilhada por todas as ncópias. Eu suspeito que o boxing / unboxing é altamente otimizado, até mesmo intrinsecado, e pode ser bem embutido.

Aqui está o código:

    public static final int LIMIT = 500_000_000;
    public static final long VALUE = 3L;

    public long range() {
        return
            LongStream.range(0, LIMIT)
                .parallel()
                .map(i -> VALUE)
                .map(i -> i % 73 % 13)
                .sum();
}

    public long ncopies() {
        return
            Collections.nCopies(LIMIT, VALUE)
                .parallelStream()
                .mapToLong(i -> i)
                .map(i -> i % 73 % 13)
                .sum();
}

E aqui estão os resultados do JMH: (2.8 GHz Core2Duo)

Benchmark                    Mode   Samples         Mean   Mean error    Units
c.s.q.SO18532488.ncopies    thrpt         5        7.547        2.904    ops/s
c.s.q.SO18532488.range      thrpt         5        0.317        0.064    ops/s

Existe uma grande variação na versão ncopies, mas no geral parece confortavelmente 20x mais rápida do que a versão range. (Eu estaria bastante disposto a acreditar que fiz algo errado, no entanto.)

Estou surpreso com o quão bem a nCopiestécnica funciona. Internamente, ele não faz muita coisa especial, com o fluxo da lista virtualizada simplesmente sendo implementado usando IntStream.range! Eu esperava que fosse necessário criar um divisor especializado para fazer isso funcionar rápido, mas já parece estar muito bom.

Stuart Marks
fonte
6
Desenvolvedores menos experientes podem ficar confusos ou ter problemas quando aprendem que nCopiesnada realmente copia e todas as "cópias" apontam para aquele único objeto. É sempre seguro se esse objeto for imutável , como um primitivo em caixa neste exemplo. Você alude a isso em sua declaração "boxed once", mas pode ser bom chamar explicitamente as advertências aqui, porque esse comportamento não é específico para a caixa automática.
William Price
1
Isso LongStream.rangesignifica que é significativamente mais lento do que IntStream.range? Portanto, é uma boa coisa que a ideia de não oferecer um IntStream(mas usar LongStreampara todos os tipos inteiros) tenha sido abandonada. Observe que, para o caso de uso sequencial, não há razão para usar stream: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));faz o mesmo, Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));mas pode ser ainda mais eficienteCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger
1
@Holger, esses testes foram realizados no perfil de tipo limpo, portanto, não estão relacionados ao mundo real. Provavelmente tem LongStream.rangepior desempenho, porque tem dois mapas com LongFunctiondentro, enquanto ncopiestem três mapas com IntFunction, ToLongFunctione LongFunction, portanto, todos os lambdas são monomórficos. Executar este teste no perfil de tipo pré-poluído (que é mais próximo do caso do mundo real) mostra que ncopiesé 1,5x mais lento.
Tagir Valeev
1
Otimização prematura FTW
Rafael Bugajewski,
1
Para fins de integridade, seria bom ver um benchmark que comparasse essas duas técnicas a um forloop antigo . Embora sua solução seja mais rápida do que o Streamcódigo, meu palpite é que um forloop superaria qualquer um deles por uma margem significativa.
typeracer 01 de
35

Para ser completo, e também porque não pude evitar :)

Gerar uma sequência limitada de constantes é bastante próximo ao que você veria em Haskell, apenas com detalhamento de nível Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);
clstrfsck
fonte
() -> 1geraria apenas 1's, é isso intencional? Portanto, a saída seria 1 1 1 1 1 1 1 1.
Christian Ullenboom
4
Sim, de acordo com o primeiro exemplo Haskell do OP take 8 (repeat 1). Assylias praticamente cobriu todos os outros casos.
clstrfsck
3
Stream<T>também possui um generatemétodo genérico para obter um fluxo infinito de algum outro tipo, que pode ser limitado da mesma maneira.
zstewart
11

Uma vez que uma função de repetição é definida em algum lugar como

public static BiConsumer<Integer, Runnable> repeat = (n, f) -> {
    for (int i = 1; i <= n; i++)
        f.run();
};

Você pode usá-lo de vez em quando desta forma, por exemplo:

repeat.accept(8, () -> System.out.println("Yes"));

Para obter e equivalente ao de Haskell

take 8 (repeat 1)

Você poderia escrever

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));
Hartmut P.
fonte
2
Este é incrível. No entanto, eu o modifiquei para fornecer o número da iteração de volta, alterando o Runnablepara Function<Integer, ?>e usando f.apply(i).
Fons
0

Esta é minha solução para implementar a função de tempos. Sou um júnior, então admito que não seria o ideal, ficaria feliz em saber se isso não é uma boa ideia por qualquer motivo.

public static <T extends Object, R extends Void> R times(int count, Function<T, R> f, T t) {
    while (count > 0) {
        f.apply(t);
        count--;
    }
    return null;
}

Aqui estão alguns exemplos de uso:

Function<String, Void> greet = greeting -> {
    System.out.println(greeting);
    return null;
};

times(3, greet, "Hello World!");
JH
fonte