Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 IntStream ja DoubleStream

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:

42671481718508tietovirtaStream

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:

426714814818508falsepois17tietovirtaStreamfiltertruei%2==0eteenpäin

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.

4267148int14818508tietovirtaStreamfiltermapToInti%2==0Integer::intValue

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:

4267148tietovirta5081842StreamfiltermapToIntsum420i%2==0Integer::intValue-148

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:

main.java
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:

main.java
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:

main.java
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:

main.java
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 €:

main.java
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 yhteen
  • min()/max() - etsii pienimmän/suurimman luvun
  • average() - laskee lukujen keskiarvon
  • summaryStatistics() - laskee kerrallaan summan, suurimman, pienimmän luvut ja keskiarvon
void main() {
IntStream lukuja = new Random().ints(20, 0, 100);
IO.println(lukuja.summaryStatistics());
}
Tehtävä 6.3: Musiikkilista 1 p.

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 suodatus
  • sorted(): alkioiden järjestäminen
  • limit(): alkioiden lukumäärän rajaaminen
  • toList(): alkioiden kerääminen listaksi
Tee tehtävä TIMissa
Tehtävä 6.4: Keskiarvo raja-arvoilla 1 p.

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.

Tee tehtävä TIMissa