Czy Java 8 zapewnia dobry sposób na powtórzenie wartości lub funkcji?

118

W wielu innych językach, np. Haskell, łatwo jest wielokrotnie powtórzyć wartość lub funkcję, np. aby otrzymać listę 8 kopii wartości 1:

take 8 (repeat 1)

ale nie znalazłem tego jeszcze w Javie 8. Czy jest taka funkcja w JDK Java 8?

Lub alternatywnie coś równoważnego z zakresem takim jak

[1..8]

Wydawałoby się, że jest oczywistym zamiennikiem pełnej instrukcji w Javie

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

mieć coś takiego

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

chociaż ten konkretny przykład nie wygląda właściwie na bardziej zwięzły ... ale miejmy nadzieję, że jest bardziej czytelny.

Graeme Moss
źródło
2
Czy znasz interfejs Streams API ? To powinno być najlepszym rozwiązaniem, jeśli chodzi o JDK. Ma funkcję zakresu , to właśnie znalazłem do tej pory.
Marko Topolnik
1
@MarkoTopolnik Klasa Streams została usunięta (a dokładniej została podzielona na kilka innych klas, a niektóre metody zostały całkowicie usunięte).
assylias
3
Wołasz pętlę for verbose! To dobrze, że nie było cię w pobliżu w czasach Cobola. W Cobolu wyświetlenie rosnących liczb zajęło ponad 10 deklaratywnych stwierdzeń. Młodzi ludzie w dzisiejszych czasach nie doceniają tego, jak dobrze to mają.
Gilbert Le Blanc
1
@GilbertLeBlanc gadatliwość nie ma z tym nic wspólnego. Pętli nie można komponować, strumienie są. Pętle prowadzą do nieuniknionych powtórzeń, podczas gdy strumienie pozwalają na ponowne wykorzystanie. W związku z tym strumienie są ilościowo lepszą abstrakcją niż pętle i powinny być preferowane.
Alain O'Dea
2
@GilbertLeBlanc i musieliśmy kodować boso, na śniegu.
Dawood ibn Kareem

Odpowiedzi:

155

W tym konkretnym przykładzie możesz zrobić:

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

Jeśli potrzebujesz kroku innego niż 1, możesz użyć funkcji mapowania, na przykład dla kroku 2:

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

Lub utwórz niestandardową iterację i ogranicz jej rozmiar:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);
asylias
źródło
4
Zamknięcia całkowicie przekształcą kod Javy na lepsze. Nie mogę się doczekać tego dnia ...
Marko Topolnik
1
@jwenting To naprawdę zależy - zazwyczaj z GUI (Swing lub JavaFX), które usuwa wiele kotłów z powodu anonimowych klas.
assylias
8
@jwenting Dla każdego, kto ma doświadczenie w FP, kod, który obraca się wokół funkcji wyższego rzędu, to czysta korzyść. Dla każdego, kto nie ma tego doświadczenia, czas ulepszyć swoje umiejętności - lub zaryzykować pozostawienie w tyle.
Marko Topolnik
2
@MarkoTopolnik Możesz użyć nieco nowszej wersji javadoc (wskazujesz na kompilację 78, najnowszą jest kompilacja 105: download.java.net/lambda/b105/docs/api/java/util/stream/ ... )
Mark Rotteveel
1
@GraemeMoss Można nadal używać tego samego wzorca ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());), ale IMO wprowadza w błąd i w takim przypadku pętla wydaje się wskazywać.
assylias
65

Oto inna technika, z którą spotkałem się tamtego dnia:

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

Collections.nCopiesWezwanie tworzy Listzawierających nkopie cokolwiek wartość podać. W tym przypadku jest to Integerwartość w ramce 1. Oczywiście w rzeczywistości nie tworzy listy z nelementami; tworzy „zwirtualizowaną” listę, która zawiera tylko wartość i długość, a każde wywołanie getw zakresie po prostu zwraca wartość. Ta nCopiesmetoda istnieje od czasu wprowadzenia struktury kolekcji w JDK 1.2. Oczywiście możliwość tworzenia strumienia z jego wyniku została dodana w Javie SE 8.

Wielka sprawa, inny sposób na zrobienie tego samego w tej samej liczbie linii.

Jednak technika ta jest szybsza niż IntStream.generatei IntStream.iteratepodejść, i zaskakująco, ale także szybciej niż IntStream.rangepodejścia.

Dla iteratea generatewynik nie jest może zbyt zaskakujące. Struktura strumieni (tak naprawdę Spliteratory dla tych strumieni) jest zbudowana przy założeniu, że lambdy będą potencjalnie generować różne wartości za każdym razem i będą generować nieograniczoną liczbę wyników. To sprawia, że ​​równoległe rozłupywanie jest szczególnie trudne. iterateMetoda jest również problematyczne w tym przypadku, ponieważ każde wywołanie wymaga wynik poprzedniego. Zatem strumienie używające generatei iteratenie radzą sobie zbyt dobrze z generowaniem powtarzających się stałych.

rangeZaskakujące jest stosunkowo słabe działanie programu . To również jest zwirtualizowane, więc w rzeczywistości nie wszystkie elementy istnieją w pamięci, a rozmiar jest znany z góry. Powinno to zapewnić szybki i łatwy równolegle rozdzielacz. Ale, co zaskakujące, nie wyszło zbyt dobrze. Być może powodem jest to, że rangemusi obliczyć wartość dla każdego elementu zakresu, a następnie wywołać na nim funkcję. Ale ta funkcja po prostu ignoruje swoje dane wejściowe i zwraca stałą, więc jestem zaskoczony, że nie jest ona wstawiana i zabijana.

Collections.nCopiesTechnika musi zrobić boks / unboxing w celu obsługi wartości, ponieważ nie istnieją prymitywne specjalizacje List. Ponieważ wartość jest za każdym razem taka sama , w zasadzie jest ona zapakowana raz i to pudełko jest wspólne dla wszystkich nkopii. Podejrzewam, że boxing / unboxing jest wysoce zoptymalizowany, a nawet zintensyfikowany i można go dobrze wprowadzić.

Oto kod:

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

A oto wyniki 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

W wersji ncopies występuje spora rozbieżność, ale ogólnie wydaje się, że jest ona 20 razy szybsza niż wersja z zakresu. (Chociaż byłbym skłonny uwierzyć, że zrobiłem coś złego).

Jestem zaskoczony, jak dobrze nCopiesdziała ta technika. Wewnętrznie nie robi to zbyt wiele, ponieważ strumień zwirtualizowanej listy jest po prostu implementowany za pomocą IntStream.range! Spodziewałem się, że konieczne będzie stworzenie wyspecjalizowanego rozdzielacza, aby to działało szybko, ale już wydaje się, że jest całkiem niezły.

Stuart Marks
źródło
6
Mniej doświadczeni programiści mogą być zdezorientowani lub wpaść w kłopoty, gdy dowiedzą się, że w nCopiesrzeczywistości nic nie kopiuje , a wszystkie „kopie” wskazują na ten jeden obiekt. Zawsze jest bezpiecznie, jeśli ten obiekt jest niezmienny , na przykład w tym przykładzie prymityw w pudełku. Nawiązujesz do tego w swoim stwierdzeniu „raz w pudełku”, ale miło byłoby wyraźnie wskazać tutaj zastrzeżenia, ponieważ takie zachowanie nie jest specyficzne dla auto-boksu.
William Price
1
Oznacza LongStream.rangeto, że jest znacznie wolniejszy niż IntStream.range? Więc dobrze, że pomysł nie oferowania IntStream(ale używania LongStreamdla wszystkich typów liczb całkowitych) został odrzucony. Zauważ, że w przypadku użycia sekwencyjnego nie ma żadnego powodu, aby używać strumienia: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));robi to samo, Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));ale może być nawet bardziej wydajneCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger
1
@Holger, te testy zostały przeprowadzone na czystym profilu, więc nie mają związku z rzeczywistością. Prawdopodobnie LongStream.rangedziała gorzej, ponieważ ma dwie mapy z LongFunctionwnętrzem, podczas gdy ncopiesma trzy mapy z IntFunction, ToLongFunctiona LongFunctionwięc wszystkie lambdy są monomorficzne. Przeprowadzenie tego testu na profilu typu wstępnie zanieczyszczonego (który jest bliższy rzeczywistej sytuacji) pokazuje, że ncopiesjest on 1,5 raza wolniejszy.
Tagir Valeev,
1
Premature Optimization FTW
Rafael Bugajewski
1
Ze względu na kompletność dobrze byłoby zobaczyć wzorzec porównujący obie te techniki ze zwykłą starą forpętlą. Chociaż twoje rozwiązanie jest szybsze niż Streamkod, przypuszczam, że forpętla pokonałaby którekolwiek z nich ze znacznym marginesem.
typeracer
35

Dla kompletności, a także dlatego, że nie mogłem się powstrzymać :)

Generowanie ograniczonej sekwencji stałych jest dość zbliżone do tego, co można zobaczyć w Haskell, tylko w przypadku gadatliwości na poziomie Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);
clstrfsck
źródło
() -> 1wygeneruje tylko jedynki, czy to jest zamierzone? Więc wynik byłby 1 1 1 1 1 1 1 1.
Christian Ullenboom,
4
Tak, na pierwszy przykład Haskella OP take 8 (repeat 1). assylias prawie obejmowały wszystkie pozostałe przypadki.
clstrfsck
3
Stream<T>ma również ogólną generatemetodę uzyskiwania nieskończonego strumienia innego typu, który można ograniczyć w ten sam sposób.
zstewart
11

Gdy funkcja powtarzania jest gdzieś zdefiniowana jako

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

Możesz go używać od czasu do czasu w ten sposób, np .:

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

Aby uzyskać i odpowiednik Haskella

take 8 (repeat 1)

Możesz pisać

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));
Hartmut P.
źródło
2
Ten jest niesamowity. Jednak zmodyfikowałem go, aby podać liczbę powtórzeń wstecz, zmieniając Runnableto, Function<Integer, ?>a następnie używając f.apply(i).
Fons
0

To jest moje rozwiązanie do implementacji funkcji czasów. Jestem junior, więc przyznaję, że mogłoby to nie być idealne. Z przyjemnością usłyszę, czy to nie jest dobry pomysł z jakiegokolwiek powodu.

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

Oto kilka przykładów użycia:

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

times(3, greet, "Hello World!");
JH
źródło