Kokoelmien käsittely: Stream API
osaamistavoitteet
- Ymmärrät deklaratiivisen ja imperatiivisen ohjelmoinnin eron kokoelmien käsittelyssä
- Tunnet Stream API:n keskeiset käsitteet (väli- ja lopetusoperaatiot)
- Osaat käyttää striimejä kokoelmien suodattamiseen, muuntamiseen ja lajitteluun
- Osaat hyödyntää
Optional-tyyppiä mahdollisesti puuttuvien arvojen käsittelyssä - Tunnet perustietotyypeille tarkoitetut striimit, kuten
IntStreamjaDoubleStream
Olemme toistaiseksi käyttäneet silmukoita kokoelmien käsittelyyn. Jos haluamme esimerkiksi laskea listasta jokaisen parillisen alkion summan, kirjoitamme ratkaisun tavallisesti näin:
void main() {
List<Integer> numeroita = List.of(508, 18, 17, -148, 67, 42, -41);
int summa = 0;
for (int numero : numeroita) {
if (numero % 2 == 0) {
summa += numero;
}
}
IO.println("Summa: " + summa);
}
Tätä ohjelmointitapaa kutsutaan imperatiiviseksi. Siinä kirjoitamme vaihe vaiheelta, mitä tietokoneen pitää tehdä, jonka seurauksena pääsemme lopputulokseen, jonka tiedämme haluavamme.
Datan prosessoinnissa on kuitenkin usein selkeämpää kuvata, millaisen lopputuloksen haluamme, sen sijaan että kertoisimme tarkat suoritusvaiheet. Tätä kutsutaan deklaratiiviseksi ohjelmoinniksi. Javan Stream API tarjoaa tähän työkalut hyödyntämällä lambdalausekkeita. Sen avulla voimme korvata yllä olevan silmukan yhdellä rivillä:
void main() {
List<Integer> numeroita = List.of(508, 18, 17, -148, 67, 42, -41);
int summa = numeroita.stream().filter(i -> i % 2 == 0).mapToInt(Integer::intValue).sum();
IO.println("Summa: " + summa);
}
Striimien perustoiminta
Tarkastellaan yllä olevaa esimerkkiä tarkemmin. Huomaamme, että rivi koostuu neljästä eri osasta:
numeroita // Käsiteltävä kokoelma
.stream() // 1. Muunto striimiksi
.filter(i -> i % 2 == 0) // 2. Suodatus
.mapToInt(Integer::intValue) // 3. Muunnos perustietotyypiksi
.sum(); // 4. Arvon laskeminen
Käydään jokainen vaihe läpi.
1. Kokoelman muuntaminen striimiksi
Kaikilla Javan kokoelmilla on stream()-metodi, joka palauttaa
Stream<T>-tyyppisen olion eli striimin
(JavaDoc).
Striimiä voi ajatella liukuhihnana tai koneena, joka ottaa kokoelman ja tuottaa
siitä yhden alkion kerrallaan tietovirtana:
2. Suodatinfunktio filter
Striimin filter() on metodi, joka suorittaa parametrina annetun
lambdalausekkeen jokaiselle alkiolle. Jos lauseke palauttaa alkiolle true,
alkio jatkaa eteenpäin tietovirrassa. Jos taas lauseke palauttaa false, alkio
poistetaan tietovirrasta.
Toisin sanoen, filter() on eräänlainen suodatin, joka lambdalausekkeen
perusteella joko antaa alkion mennä läpi tai suodattaa sen pois:
3. Muunnosfunktio map
Striimien tärkeimpiä työkaluja ovat erilaiset muunnokset eli kuvaukset. Nämä
metodit alkavat yleensä sanalla map. Ne ottavat yhden alkion kerrallaan ja
muuttavat sen joksikin muuksi.
Esimerkiksi mapToInt() on muunnos, joka ottaa alkion ja muuttaa sen
int-tyyppiseksi luvuksi annetun funktion avulla. Tässä käytämme
Integer::intValue -funktioviitettä, joka muuttaa Integer-olion tavalliseksi
kokonaisluvuksi.
Tarvitsemme tämän vaiheen siksi, että yleinen Stream<T> on geneerinen, ja
Javassa geneeristen tyyppien sisällä ei voi olla perustietotyyppejä (kuten
int). Kutsumalla mapToInt() striimi muuttuu IntStream-tyyppiseksi
(JavaDoc).
IntStream on optimoitu kokonaislukujen käsittelyyn ja se tarjoaa valmiita
tilastollisia metodeja, kuten sum().
4. Arvon laskeminen
Striimin päätteeksi kutsumme aina jotain lopetusfunktiota. Se ottaa vastaan tietovirran lopussa olevat alkiot ja palauttaa ne ohjelmalle halutussa muodossa (esim. summana tai listana).
Tässä esimerkissä käytimme sum()-metodia, joka laskee luvut yhteen ja
palauttaa lopputuloksen yhtenä lukuna:
Striimien käyttäminen
Kaikki, mitä on mahdollista tehdä striimeillä, voitaisiin kirjoittaa myös tavallisina silmukoina. Kuitenkin yhdistämällä eri Stream API -funktioita saamme usein hyvin ytimekkäitä ratkaisuja ongelmiin, jotka muuten vaatisivat useita rivejä imperatiivista koodia.
Striimien luominen
Yleisin tapa on luoda striimi suoraan kokoelmasta. Kaikilla Javan
Collection-rajapinnan toteuttavilla luokilla on stream()-metodi:
void main() {
List<String> hedelmia = List.of("omena", "päärynä", "appelsiini");
Map<String, Integer> asukaslukuja = Map.of(
"Helsinki", 695526,
"Tampere", 263526,
"Jyväskylä", 149967
);
Set<String> automerkkeja = Set.of("BMW", "Audi", "Hyundai", "Volvo");
Stream<String> hedelmiaStream = hedelmia.stream();
Stream<Map.Entry<String, Integer>> asukaslukujaStream = asukaslukuja.entrySet().stream();
Stream<String> automerkkejaStream = automerkkeja.stream();
IO.println("Hedelmiä, jossa ei ole p-kirjainta: " + hedelmiaStream.filter(h -> !h.contains("p")).toList());
IO.println("Kaupunki, jossa on eniten asukkaita: " + asukaslukujaStream.max(Comparator.comparing(Map.Entry::getValue)).get().getKey());
IO.println("Automerkkien nimet yhdistettynä: " + automerkkejaStream.reduce("", (p, n) -> p + n));
}
Myös taulukoista voidaan luoda striimi Arrays.stream-metodilla:
void main() {
int[] arvosanoja = {5, 1, 2, 3, 4, 5, 2, 5, 5, 4};
String[] opettajat = {"Denis", "Antti-Jussi", "Sami", "Karri"};
IntStream arvosanojaStream = Arrays.stream(arvosanoja);
Stream<String> opettajatStream = Arrays.stream(opettajat);
IO.println("Arvosanojen keskiarvo: " + arvosanojaStream.average().getAsDouble());
IO.println("Opettaja, jolla on pisin etunimi: " + opettajatStream.max(Comparator.comparing(String::length)).get());
}
Mainittakoon tässä vaiheessa, että perustietotyypeille on olemassa omat
striimiluokat IntStream, DoubleStream, LongStream, jne. Nämä erikoisluokat
tarjoavat muun muassa erilaisia tilastofunktioita, kuten max, min, average
ja sum. Kokoelmien tapauksessa perustietotyypit kääritään kuitenkin aina
käärijäluokkaan, jolloin striimit ovat muotoa Stream<Integer>,
Stream<Double>, Stream<Long>. Stream-luokka tarjoaa aiemmin mainitut
mapToInt, mapToDouble ja vastaavia metodeja, jolla striimin voi muuttaa
perustietotyyppiversioon.
Voimme myös luoda striimejä, jotka tuottavat äärettömästi arvoja. Esimerkiksi
Stream.generate kutsuu annettua funktiota toistuvasti. Tällöin on käytettävä
alkioita rajoittavia metodeja, kuten limit, joka pysäyttää tietovirran halutun
määrän jälkeen:
void main() {
Stream<String> risuaitoja = Stream.generate(() -> "#");
List<String> kymmenenRisuaitaa = risuaitoja.limit(10).toList();
IO.println(kymmenenRisuaitaa);
}
Striimin välioperaatiot
Kaikki striimin metodit, jotka palauttavat uuden Stream-olion, ovat ns.
välioperaatioita (engl. intermediate operations). Niitä käytetään
tietovirrassa liikkuvien alkioiden muokkaamiseen ja suodattamiseen.
Kuvitellaan, että ylläpidämme kaupan ostostietoja. Haluamme selvittää syyskuun ostosten keskihinnan.
public class Ostotapahtuma {
private double hinta;
private LocalDate pvm;
}
Sen sijaan, että kirjoittaisimme silmukan ja if-ehtoja, rakennetaan haluttua
tulosta antavan striimin vaihe vaiheelta. Aloitetaan ensin ottamalla mukaan vain
syyskuun mukaan. Voimme käyttää filter()-metodia, joka suodattaa striimistä
alkioita annetun boolean-funktion perusteella:
void main() {
List<Ostotapahtuma> ostotapahtumat = List.of(
new Ostotapahtuma(100.0, LocalDate.of(2025, Month.JANUARY, 2)),
new Ostotapahtuma(21.5, LocalDate.of(2025, Month.JULY, 3)),
new Ostotapahtuma(12.0, LocalDate.of(2025, Month.SEPTEMBER, 1)),
new Ostotapahtuma(5.25, LocalDate.of(2025, Month.SEPTEMBER, 12)),
new Ostotapahtuma(245.0, LocalDate.of(2025, Month.SEPTEMBER, 21)),
new Ostotapahtuma(342.0, LocalDate.of(2025, Month.OCTOBER, 2))
);
Stream<Ostotapahtuma> vainSyyskuu =
ostotapahtumat.stream()
.filter(o -> o.getPvm().getMonth() == Month.SEPTEMBER);
vainSyyskuu.forEach(IO::println);
}
Nyt kun meillä on vain syyskuun ostokset suodatettu mukaan, haluamme laskea
niiden keskiarvohinnan. Keskiarvo voidaan laskea vain luvuista, kun taas
ostotapahtuma on Ostotapahtuma-tyyppinen. Voimme käyttää striimin
map-metodia, joka muuntaa jokaisen alkion arvon toiseksi annetun
muunnosfunktion perusteella. Meidän muunnosfunktiossa riittää hakea
Ostotapahtuma-olion hinta-attribuutti, jolloin näin saadaan striimin
luvuista:
void main() {
List<Ostotapahtuma> ostotapahtumat = List.of(
new Ostotapahtuma(100.0, LocalDate.of(2025, Month.JANUARY, 2)),
new Ostotapahtuma(21.5, LocalDate.of(2025, Month.JULY, 3)),
new Ostotapahtuma(12.0, LocalDate.of(2025, Month.SEPTEMBER, 1)),
new Ostotapahtuma(5.25, LocalDate.of(2025, Month.SEPTEMBER, 12)),
new Ostotapahtuma(245.0, LocalDate.of(2025, Month.SEPTEMBER, 21)),
new Ostotapahtuma(342.0, LocalDate.of(2025, Month.OCTOBER, 2))
);
Stream<Double> syyskuunHinnat =
ostotapahtumat.stream()
.filter(o -> o.getPvm().getMonth() == Month.SEPTEMBER)
// HIGHLIGHT_GREEN_BEGIN
.map(o -> o.getHinta());
// HIGHLIGHT_GREEN_END
syyskuunHinnat.forEach(IO::println);
}
Huomaa, että tuloksena on Stream<Double>, eli käärijäluokkaan tallennettu
liukuluku. Jotta keskiarvon laskenta olisi helpompaa, muunnetaan Double-alkiot
perustyyppiinsä mapToDouble()-metodilla:
void main() {
List<Ostotapahtuma> ostotapahtumat = List.of(
new Ostotapahtuma(100.0, LocalDate.of(2025, Month.JANUARY, 2)),
new Ostotapahtuma(21.5, LocalDate.of(2025, Month.JULY, 3)),
new Ostotapahtuma(12.0, LocalDate.of(2025, Month.SEPTEMBER, 1)),
new Ostotapahtuma(5.25, LocalDate.of(2025, Month.SEPTEMBER, 12)),
new Ostotapahtuma(245.0, LocalDate.of(2025, Month.SEPTEMBER, 21)),
new Ostotapahtuma(342.0, LocalDate.of(2025, Month.OCTOBER, 2))
);
// HIGHLIGHT_YELLOW_BEGIN
DoubleStream syyskuunHinnat =
// HIGHLIGHT_YELLOW_END
ostotapahtumat.stream()
.filter(o -> o.getPvm().getMonth() == Month.SEPTEMBER)
.map(o -> o.getHinta())
// HIGHLIGHT_GREEN_BEGIN
.mapToDouble(d -> d.doubleValue());
// HIGHLIGHT_GREEN_END
syyskuunHinnat.forEach(IO::println);
}
DoubleStream sisältää valmiiksi average()-metodin, joka kerää ja palauttaa
striimissä olevien alkioiden keskiarvon:
void main() {
List<Ostotapahtuma> ostotapahtumat = List.of(
new Ostotapahtuma(100.0, LocalDate.of(2025, Month.JANUARY, 2)),
new Ostotapahtuma(21.5, LocalDate.of(2025, Month.JULY, 3)),
new Ostotapahtuma(12.0, LocalDate.of(2025, Month.SEPTEMBER, 1)),
new Ostotapahtuma(5.25, LocalDate.of(2025, Month.SEPTEMBER, 12)),
new Ostotapahtuma(245.0, LocalDate.of(2025, Month.SEPTEMBER, 21)),
new Ostotapahtuma(342.0, LocalDate.of(2025, Month.OCTOBER, 2))
);
OptionalDouble syyskuunKeskiarvo =
ostotapahtumat.stream()
.filter(o -> o.getPvm().getMonth() == Month.SEPTEMBER)
.map(o -> o.getHinta())
.mapToDouble(d -> d.doubleValue())
// HIGHLIGHT_GREEN_BEGIN
.average();
// HIGHLIGHT_GREEN_END
IO.println(syyskuunKeskiarvo);
}
Huomaa, että average() ei palauta suoraan double-arvoa, vaan
OptionalDouble-olion. Tämä johtuu siitä, että jos striimi on tyhjä
(esimerkiksi yhtään syyskuun ostosta ei löytyisi), keskiarvoa ei voida laskea.
Palaamme tähän hieman alempana.
Striimien lopetusoperaatiot
Kaikki striimin metodit, jotka palauttavat jotain muuta kuin uuden striimin, ovat lopetusoperaatioita (engl. terminal operations). Lopetusoperaatiot yleensä käyvät läpi striimissä kaikki alkiot ja tuottavat arvon tai sivuvaikutuksen.
Eräs tavallinen lopetusoperaatio on striimin alkioiden kerääminen
kokoelmaksi. Esimerkiksi toList() kerää striimin alkiot listaksi ja
toArray() taulukoksi:
void main() {
List<Integer> arvosanoja = List.of(1, 4, 5, -1, 0, 15, 2, 4, 5);
List<Integer> oikeitaArvosanoja = arvosanoja.stream()
.filter(i -> 1 <= i && i <= 5)
.sorted()
.toList();
int[] oikeitaArvosanojaTaulu = arvosanoja.stream()
.filter(i -> 1 <= i && i <= 5)
.mapToInt(i -> i.intValue())
.sorted()
.toArray();
IO.println(oikeitaArvosanoja);
IO.println(Arrays.toString(oikeitaArvosanojaTaulu));
}
Huomaa, että lopetusoperaation jälkeen striimi yleensä lasketaan käytetyksi, eikä jo käytettyä striimiä voi enää yleensä käyttää sen jälkeen, vaan tarvitaan uusi striimi. Jo käytetyn striimin uudelleenkäyttäminen aiheuttaa yleensä virheen:
void main() {
List<Integer> arvosanoja = List.of(1, 4, 5, -1, 0, 15, 2, 4, 5);
Stream<Integer> arvosanojaStream = arvosanoja.stream()
.filter(i -> 1 <= i && i <= 5)
.sorted();
// toList() lopettaa striimin
List<Integer> arvosanojaLista = arvosanojaStream.toList();
// VIRHE: yritetään käyttää jo käytettyä striimiä
long arvosanatLkm = arvosanojaStream.count();
}
java.lang.IllegalStateException: stream has already been operated upon or closed
Kuten kokoelmissa, myös striimeissä on forEach()-metodi, jonka avulla voi
suorittaa mielivaltaista koodia jokaiselle alkiolle:
void main() {
IntStream.range(0, 10) // Striimi kokonaisluvuista 0-9
.filter(i -> i % 2 == 1) // Otetaan vain parittomat kokonaisluvut
.forEach(IO::println); // Suoritetaan IO.println jokaiselle luvulle
}
Striimit sisältävät myös muutaman apufunktion yleisempiin ongelmiin. min() ja
max() -metodit keräävät striimin alkiot ja palauttavat alkioista suurimman.
Kummatkin metodit ottavat parametrina Comparator-vertailijafunktion.
void main() {
List<String> opet = List.of("Denis", "Antti-Jussi", "Sami", "Karri");
Optional<String> pisinNimi = opet.stream().max(Comparator.comparing(String::length));
Optional<String> lyhinNimi = opet.stream().min(Comparator.comparing(String::length));
IO.println("Pisin: " + pisinNimi);
IO.println("Lyhin: " + lyhinNimi);
}
Huomaa, että max(), min() ja monet muut striimin lopetusfunktiot eivät
palauta arvoja suoraan, vaan Optional<T>-olion
(JavaDoc).
Nimensä mukaan tällainen olio kuvastaa arvon mahdollista puuttumista.
Esimerkiksi, jos striimissä ei ole yhtään alkiota tai jos lopetusfunktio ei voi
muuten laskea arvoa, se palauttaa Optional.empty-arvon kuvastamaan laskennan
epäonnistumista:
void main() {
List<String> opet = List.of("Denis", "Antti-Jussi", "Sami", "Karri");
Optional<String> pisinNimi = opet.stream()
.filter(s -> s.startsWith("V"))
.max(Comparator.comparing(String::length));
IO.println("Pisin: " + pisinNimi);
}
Ennen kuin palautettua arvoa voi käyttää, tulee ensin tarkistaa, sisältääkö
Optional<T>-olio tuloksen. Tämä onnistuu esimerkiksi isPresent()-metodilla.
Kun tiedetään, että arvo on olemassa, se voidaan hakea get()-metodilla:
void main() {
List<String> opet = List.of("Denis", "Antti-Jussi", "Sami", "Karri");
Optional<String> pisinNimi = opet.stream().max(Comparator.comparing(String::length));
if (pisinNimi.isPresent()) {
String nimi = pisinNimi.get();
IO.println("Pisin: " + nimi);
} else {
IO.println("Annetuilla ehdoilla ei löytynyt yhtään nimeä");
}
}
Mainittakoon, että Optional<T>-tyyppi sisältää joukon muita apufunktioita,
joilla voi välttyä ylimääräisiltä if-rakenteilta.
Palataan vielä hetkeksi striimeihin. Striimit soveltuvat kätevästi arvojen
etsimiseen kokoelmista; findFirst()-metodi palauttaa ensimmäisen alkion, joka
pääsee "tietovirran loppuun" asti. Esimerkiksi, jos haluaisimme löytää
varastosovelluksesta ostotapahtuman, joka oli tehty syyskuussa ja ylittänyt
hinnaltaan 100 €:
void main() {
List<Ostotapahtuma> ostotapahtumat = List.of(
new Ostotapahtuma(100.0, LocalDate.of(2025, Month.JANUARY, 2)),
new Ostotapahtuma(21.5, LocalDate.of(2025, Month.JULY, 3)),
new Ostotapahtuma(12.0, LocalDate.of(2025, Month.SEPTEMBER, 1)),
new Ostotapahtuma(5.25, LocalDate.of(2025, Month.SEPTEMBER, 12)),
new Ostotapahtuma(245.0, LocalDate.of(2025, Month.SEPTEMBER, 21)),
new Ostotapahtuma(342.0, LocalDate.of(2025, Month.OCTOBER, 2))
);
Optional<Ostotapahtuma> tapahtuma =
ostotapahtumat.stream()
.filter(o -> o.getPvm().getMonth() == Month.SEPTEMBER)
.filter(o -> o.getHinta() > 100.0)
.findFirst();
if (tapahtuma.isPresent()) {
IO.println(tapahtuma.get());
} else {
IO.println("Tapahtumaa ei löytynyt");
}
}
Lopuksi, striimeillä on myös joitain tilastoihin liittyviä operaatioita.
Esimerkiksi aiemmin koodissa mainittu count()-metodi palauttaa kokonaislukuna,
kuinka monta alkiota striimissä on. Lisäksi perustietotyypeille tarkoitetuissa
striimeissä IntStream, DoubleStream ja LongStream löytyy muun muassa
seuraavia tilastometodeja:
sum()- summaa luvut yhteenmin()/max()- etsii pienimmän/suurimman luvunaverage()- laskee lukujen keskiarvonsummaryStatistics()- laskee kerrallaan summan, suurimman, pienimmän luvut ja keskiarvon
void main() {
IntStream lukuja = new Random().ints(20, 0, 100);
IO.println(lukuja.summaryStatistics());
}
Olkoon käytössä luokka Kappale, joka edustaa yksittäistä musiikkikappaletta.
Kappaleella on nimi, genre ja kesto sekunteina:
class Kappale {
String nimi;
String genre;
int kestoSekunteina;
}
Lisää luokalle tarvittavat näkyvyysmääreet, muodostaja, tarpeelliset
saantimetodit (getterit) sekä sopiva toString()-metodin toteutus.
Tee funktio teeSoittolista(kappaleet, genre, kappaleita), joka palauttaa
korkeintaan kappaleita-muuttujan ilmoittaman määrän kappaleita, joiden genre
vastaa annettua genre-parametria. Kappaleiden tulee olla järjestettynä keston
mukaan lyhyemmästä pisimpään.
Voit käyttää seuraavaa mallilistaa koodisi testaamiseen:
Lista mallikappaleista
List<Kappale> biisilista = List.of(
new Kappale("Bohemian Rhapsody", "Rock", 354),
new Kappale("Levitating", "Pop", 203),
new Kappale("Sandstorm", "Electronic", 225),
new Kappale("Paranoid", "Metal", 168),
new Kappale("Toxic", "Pop", 198),
new Kappale("Master of Puppets", "Metal", 515),
new Kappale("Cha Cha Cha", "Pop", 175),
new Kappale("Hotel California", "Rock", 390),
new Kappale("Stay", "Pop", 141),
new Kappale("Enter Sandman", "Metal", 331),
new Kappale("Bad Romance", "Pop", 295),
new Kappale("Midnight City", "Electronic", 243),
new Kappale("Billie Jean", "Pop", 294),
new Kappale("Hard Rock Hallelujah", "Metal", 247),
new Kappale("Thriller", "Pop", 357),
new Kappale("As It Was", "Pop", 167),
new Kappale("Paint It, Black", "Rock", 202),
new Kappale("Hollywood Hills", "Rock", 210),
new Kappale("Get Lucky", "Electronic", 369),
new Kappale("Shake It Off", "Pop", 219),
new Kappale("Ace of Spades", "Metal", 169),
new Kappale("Rolling in the Deep", "Pop", 228),
new Kappale("Sweet Child O' Mine", "Rock", 356),
new Kappale("Borderline", "Pop", 210),
new Kappale("Back in Black", "Rock", 255),
new Kappale("Shape of You", "Pop", 233),
new Kappale("Fear of the Dark", "Metal", 438),
new Kappale("Blinding Lights", "Pop", 200),
new Kappale("Stairway to Heaven", "Rock", 482),
new Kappale("Uptown Funk", "Pop", 269),
new Kappale("Smells Like Teen Spirit", "Rock", 301),
new Kappale("Short Pop Song", "Pop", 120)
);
Älä käytä silmukoita, vaan toteuta teeSoittolista käyttäen striimejä.
Vinkki
Saatat tarvita ainakin seuraavia Stream-metodeja:
filter(): alkioiden suodatussorted(): alkioiden järjestäminenlimit(): alkioiden lukumäärän rajaaminentoList(): alkioiden kerääminen listaksi
Tee funktio double keskiarvo(int[] luvut, int minimi, int maksimi). Funktio
laskee taulukkona annettujen lukujen keskiarvon seuraavilla ehdoilla:
- Jos alkio on pienempi tai yhtä suuri kuin
minimi, alkio hylätään eikä sitä lasketa keskiarvoon mukaan. - Jos alkio on suurempi tai yhtä suuri kuin
maksimi, kyseinen alkio ja kaikki sen jälkeen tulevat alkiot hylätään.
Esimerkki:
IO.println(keskiarvo(new int[] { -5, 1, -4, 0, 98 }, -7, 99));
IO.println(keskiarvo(new int[] { 11, 4, 2, 6, 99, 12, 0, -3 }, 3, 99));
IO.println(keskiarvo(new int[] { 99, 1, 2, 3 }, 0, 99));
18.0
7.0
0.0
Ensimmäinen kutsu palauttaa 18.0, sillä aineisto on kokonaisuudessaan minimin
ja maksimin välissä. Toinen kutsu palauttaa taas 7.0, sillä vain luvut 11, 4
ja 6 otetaan keskiarvoon mukaan: luku 2 on pienempi kuin minimi ja kaikki
luvusta 99 alkaen hylätään.
Jos keskiarvoa ei voida laskea, funktio palauttaa minimi-parametrin arvon.
Älä käytä silmukoita, vaan toteuta funktio käyttäen striimejä.
Vinkki
Tutustu IntStream-tyyppiin
(JavaDoc)
ja sen metodeihin. Voit hyötyä ainakin seuraavista:
filter(): alkioiden suodattaminen pois striimistätakeWhile(): ottaa alkioita striimistä niin kauan kuin ehto on tosi; heti kun ehto on epätosi, striimin käsittely loppuu siihen (kuin "hana", joka suljetaan).average(): laskee keskiarvon
Huomaa, että average() palauttaa OptionalDouble-olion
(JavaDoc).
Olio sisältää orElse()-metodin, jonka avulla voit palauttaa joko lasketun
arvon tai vaihtoehtoisen oletusarvon.
Bonustieto
Samankaltainen tehtävä tehdään Ohjelmointi 1 -kurssilla käyttäen silmukoita (ks. Ohjelmointi 1: demo 6, tehtävä B1). Jos olet suorittanut kyseisen kurssin, voit verrata striimeillä tehtyä ratkaisuasi aiemmin tekemääsi silmukkaratkaisuun.