Java 8 : Se ei oo sun isoisän Java enää…

Jep, olen benchmarkannut aiemmin muita ohjelmointikieliä kuten Scala ja Groovy sillä miten paljon koodia vaatii tehdä rss-feedin lukija, joka esim. poimii sieltä title ja link tageja. Vanhalla Javalla koodaten – juu tuo vie hetkenkin verran. Ja sisältänee while tai for-silmukkaa ja imperatiivista koodia. Ei siinä mitään, koodaushan on kivaa.

Tässä java 8 versio RSS lukijasta joka lukee ja tulostaa Tieturin blogin otsikoita ja linkkejä – käytössä Streamit ja Lambdat:

new BufferedReader(
  new URLReader(new URL("http://feeds.feedburner.com/tieturi")))
    .lines()
    .filter(a -> a.contains("<title>") || a.contains("<link>"))
    .forEach(a -> System.out.println(a));

Jep, one-lineri. Valmistaudu näkemään tällaista koodia enemmänkin.. 😉

 

Tykkäätkö erityisesti hitaasta, monimutkaisesta ja vaikeasta? Älä missään nimessä siirry Java 8:aan!

Jep, provosoivaan otsikkoon vähän sisältöä. Olen jo vuoden päivät opiskellut Java 8 version tulevia muutoksia, kyseessä on tosiaan iso pläjäys joka koodaajaa hyödyntävää tavaraa. Ne summautuvat yhteen tehokkuudeksi – monissa paikoissa voi nyt tehdä koodaustyöt siten että koodirivejä syntyy vähemmän – koodi on luettavampaa ja näin ollen kalliita huolimattomuusvirheitä on vähemmän – ja kaiken lisäksi uudella tapaa tehty koodi voi skaalautua moniydinprosessoireissa ihan uudella tapaa. Turha odotella paria vuotta kun kaikesta tästä voi nauttia NYT. Koodaajille tätä ei ole tarpeen paljoa perustella, mutta koko projektin kannalta näkyy jo selvää rahaa kun siirtyy käyttämään fiksumpia uusin piirtein varustettuja alustoja. Jos pyörität vielä Java versioita 5 tai 6, on peiliin katsomisen aika. 😉

Java 1.8 julkaistaan näillä näkymin 18.3. lopullisena versiona. Siitä on jo toimivaa Release Candidate versiota tarjolla. Jokainen kehitysympäristö tukee jo enemmän tai vähemmän sen käyttöä. Ja mikä tärkeintä, Tieturilla on tarjolla heti koulutusta jossa saat kickstartin uusiin piirteisiin ohjatusti ja vinkkejä parhaista käytännöistä ja myös sudenkuopista joita voi tulla vastaan.

Java 8 uudet piirteet (Huhti ja toukokuussa toteutus):
http://www.tieturi.fi/kurssit/kurssi.html?course=85000345

Tärkeimmät tulossa olevat uutuudet ovat tietysti Lambdat, joka toimii kevyenä porttihuumeena, ja sitten Streamit, joissa piilee se kova kama. Sen ohella vietellään kovasti kohden funktionaalista ohjelmointia joka on testattavampaa ja skaalautuvampaa. Vanha kunnon isoisän Javahan alkaa tässä näyttämään ihan Scalalta! Kun soppaan lisätään vielä uusi päivämääräkirjasto, joka on Jodatimen mukaan tehty ja ensimmäistä kertaa alkaa olla kohtuullisen mehukas – on aika painavat syyt päivittää itsensä ja ympäristönsä ASAP.

Tein vähän mallikoodia omaan käyttöön testatakseni piirteitä. Lähtötilanteena oli käydä läpi vähän tietokannastamme löytyvää kurssitarjontaa, ja tehdä siihen erilaisia hakuja. Otin tahallisesti laajan otoksen kaikesta mitä löytyi, joten datan laadussa on hyvin tyypillisiä heikkouksia, mm. puuttuvia kenttiä, ja tein siihen liittyvää perinteistä hyvin rumaa mutta myös hyvin tyypillistä imperatiivista koodia. Kuvaavaa on että koodin monimutkaisuuden vuoksi ehdin tehdä parikymmentä kertaa jonkin virheen joka testatessa kävi ilmi. Huolimattomuusvirheet voivat isommassa projektissa tulla äkkiä hyvin kalliiksi etenkin jos testaus ei ole automatisoitua, kattavaa, säännöllistä  ja nopeaa. Lähdin alunperin testailemaan suorituskykyä, mutta matkalla löytyikin muuta. Katsotaanpa mitä tapahtui.

Lähtötilanne: Kannasta on haettu tauludataa, joka on talletettu JSON olioihin. Pääohjelma lataa ne levyltä, ja prosessoi ne läpi, tulostaen lopuksi käsitellyt tiedot. Pääohjelma on tämän näköinen:

public static void main(String[] args)
    throws IOException, JAXBException {
  List<Course> courses = loadCourses("test.json");
  long begin = System.nanoTime();
  List<Course> validPublicTrainings = filterAndFixCourses(courses);
  long end = System.nanoTime();

  // Print values out to control how it works 
  for (Course course : validPublicTrainings) { 
    System.out.println(course);
  }

  System.out.println("Number of filtered courses: " 
    + validPublicTrainings.size()); 
  System.out.println("Time spent: " + (end - begin) / 1000000.0 
    + "ms"); 
}

Melko yksinkertaista tavaraa, siis. Kaikki mehukas tapahtuu filterAndFixCourses() funktiossa, sen tehtävä on suodattaa kurssit jotka ovat julkisia, valideja, ja pistää ne päivämäärän mukaan järjestykseen. Ongelmia tuottaa, että osassa dataa arvoja puuttuu ja ne ovat null-arvoj (Kaiken tämän voisi tehdä kannassa mutta tämä ei ole kantakoodauksen artikkeli 😉

Tältä näyttää perinteiseen imperatiiviseen tyyliin kirjoitettu varsin ruma ja optimoimaton (mutta myös varsin tyypillinen) esimerkki:

public static List<Course> filterAndFixCourses(
    List<Course> courseList) {
  // Get all trainings that are public and valid
  List<Course> validPublicTrainings = new ArrayList<>();
  for (Course course : courseList) {
    if (course.getType() != null && course.getType() == 3 
        && course.getStatus() != null && course.getStatus() == 3) {
      if (course.getBeginDate() != null 
          && course.getEndDate() != null) {
        // Fix dates by adding +10 hours, 36000ms
        course.setBeginDate(new Date(
           course.getBeginDate().getTime() + 36000));
        course.setEndDate(new Date(
           course.getEndDate().getTime() + 36000));
      }
     validPublicTrainings.add(course);
    }
  }

  // Sort by begindate and enddate
  // Descending order for dates, meaning that newest dates go first
  // Null values go to bottom of list
  Comparator<Course> c = new Comparator<Course>() {
    @Override
      public int compare(Course o1, Course o2) {
        if (o1.getBeginDate() == null 
            && o2.getBeginDate() == null) {
          return 0;
        } else if (o1.getBeginDate() == null) {
          return 1;
        } else if (o2.getBeginDate() == null) {
          return -1;
        } else if (o1.getEndDate() == null 
            && o2.getEndDate() == null) {
          return 0;
        } else if (o1.getEndDate() == null) {
          return 1;
    } else if (o2.getEndDate() == null) {
      return -1;
    } else if (o1.getBeginDate().before(o2.getBeginDate())) {
      return 1;
    } else if (o1.getBeginDate().after(o2.getBeginDate())) {
      return -1;
    } else if (o1.getEndDate().before(o2.getEndDate())) {
      return 1;
    } else if (o1.getEndDate().after(o2.getEndDate())) {
      return -1;
    } else {
      return 0;
    }
   }
  };
  Collections.sort(validPublicTrainings, c);
  return validPublicTrainings;
}

Rumaa? Jep, elämä on. Tuota voisi stilisoida moninkin tavoin mutta totuus on, että tuollaista koodia löytyy tuotantojärjestelmistä ympäri maailman. Kuten Venkat Subramaniam sanoi luennoissaan, kun tuota kirjoittelee päivän, tuntee olonsa likaiseksi ja kaipaa suihkua.

Miltäpä tämä sitten näyttäisi Java 8 avulla? Kokeillaanpa uudestaan. Ei mikromanageroida ulkopuolelta mitä pitäisi tehdä, vaan käytetään Lambdoja ja Streameja, ja annetaan algoritmit suoraan Streamille, näin:

public static List<Course> filterAndFixCourses(
    List<Course> courseList) {
  return courseList.stream()
    .filter(c -> c.getType() != null)
    .filter(c -> c.getType() == 3)
    .filter(c -> c.getStatus() != null)
    .filter(c -> c.getStatus() == 3)
    .sorted((o1, o2) -> {
       if (o1.getBeginDate() == null 
            && o2.getBeginDate() == null) {
         return 0;
       } else if (o1.getBeginDate() == null) {
         return 1;
       } else if (o2.getBeginDate() == null) {
         return -1;
       } else if (o1.getEndDate() == null 
           && o2.getEndDate() == null) {
         return 0;
       } else if (o1.getEndDate() == null) {
         return 1;
       } else if (o2.getEndDate() == null) {
         return -1;
       } else if (o1.getBeginDate().before(o2.getBeginDate())) {
         return 1;
       } else if (o1.getBeginDate().after(o2.getBeginDate())) {
         return -1;
       } else if (o1.getEndDate().before(o2.getEndDate())) {
         return 1;
       } else if (o1.getEndDate().after(o2.getEndDate())) {
         return -1;
       } else {
         return 0;
       }
     })
     .map((c) -> fixDate(c))
     .collect(Collectors.toList());
}

Parempi? Mmmhmm… Tavallaan, imperatiivisen tyylin mikromanagerointi on poistunut, ja voisi sanoa että koodi on alkuun jopa ymmärrettävämpää. Mutta monimutkaiset asiat ovat edelleen monimutkaisia ja sotkuisia, vaikeasti ymmärrettäviä. Mutta meillä on käsissämme funktionaalinen ohjelmointityyli. Mitäpä jos.. käyttäisimme funktioita hoitamaan likaiset työt? Sama uusiksi funktioreferensseillä Java 8 tapaan:

public static List<Course> filterAndFixCourses(
    List<Course> courseList) {
  return courseList.stream()
    .filter(App::typeThreeIsValid)
    .filter(App::statusThreeIsValid)
    .sorted(App::sortByDate)
    .map(App::fixDate)
    .collect(Collectors.toList());
}

Aaaaaivan! Koodirivien määrä rupsahti 53:sta kuuteen – tai jos tarkkoja ollaan niin yhteen, tuohan on kaikki samaa return lauseketta eli one-lineri. Tietysti osa koodia siirtyi vain funktioihin joka aina kannattaisi tehdä. Kuten yllä näkyy, hyvä idea nimetä funktiot dokumentatiiviseen tapaan, vähän kuin hyvissä yksikkötesteissä.

Mutta luettavuuden voisi väittää parantuneen, kun ylläolevaa koodia lukee, sitä voi jopa ymmärtää. Kenties jopa kuukauden päästä palatessaan asiaan.

Mitäpä tapahtui suorituskyvylle? Kellotin vähän millisekunteja. Imperatiivinen koodiesimerkki rusikoi 100 000 tietueen massaa n. 27ms verran. Funktionaalinen Java 8 versio käytti samaan aikaa n. 54ms. Hetki, miten tässä näin kävi? Eikö tämän pitänyt olla nopeampaa? No ei. Skaalautuvampaa, kyllä. Nopeushävikkiä voi syntyä esim. immutable-olioiden kopioinnista, ja toisaalta tässä ovat operaatiot niin nopeita että ylimääräisten askelien overhead jää suuremmaksi kuin mikään tehosäästö.

Mutta, tämänhän piti skaalautua mukavasti moniydinprosessoreille. Tässä koneessani on niitä 8, ajetaanpa testi uusiksi tällä koodilla:

// Best version of Java 8 features with parallel execution
 public static List<Course> filterAndFixCourses(
    List<Course> courseList) {
    return courseList.parallelStream()
       .filter(App::typeThreeIsValid)
       .filter(App::statusThreeIsValid)
       .sorted(App::sortByDate)
       .map(App::fixDate)
       .collect(Collectors.toList());
}

Kuten huomaat, ainoa mitä piti muuttaa oli korvata stream() parallelStream() kutsulla. Nyt taustalla käytetään Fork&Joinia ja hajoitetaan ja hallitaan operaatioita, ajaen ne rinnakkain. Mitenkäs suuri nopeushyöty saadaan tästä?

Turpiin tuli. Nyt aikaa meni 90ms, eli lähes neljä kertaa hitaampaa. Mikä meni vikaan?

Kuten aiemminkin, overhead iski. Thread poolin pystytys ja töiden skedulointi vie suoritusaikaa. Tässä esimerkissä laskennallinen tehtävä on edelleen triviaalin nopea ja siksi sen tekeminen rinnakkain ei hyödytä tarpeeksi. Eli jos nopeutta hakee, tämä kannattaisi edelleen tehdä mutkattomammin imperatiivisesti. Ytimien lisääminenkään tuskin auttaisi, koska overhead.

Mutta.. entäpä jos työ olisi raskaampaa.. Nykyään dataa haetaan usein verkon yli, tai tietokannasta, useissa erissä. Edellämainitun koodin ongelmana alkoi olemaan muistin riittäminen jos tietueita olisi vaikka miljoona. Looginen ratkaisu olisi hakea sitä sopivammissa viipaleissa kannasta tai verkon yli.

Entä jos siinä menisi esim. 10ms per kierros aikaa? Simuloidaan koodiin pientä verkkoviivettä, näin:

try {
 Thread.sleep(10);
 } catch (InterruptedException ex) { }

Joka kierroksella menee hetki aikaa, joko laskenta on raskasta tai haetaan jotain verkon yli tai kannasta. 18793 tehtävää (viive on vain suodatetuilla alkioilla), 10ms viivettä per tehtävä. Nyt tilanne muuttuu, imperatiiviselta koodilta kestää: 194 045ms tehdä työ. Ei kovin suuri yllätys, 100k kertaa 10 on miljoona. Entä miten suoriutuu parallel Stream Java 8 versio? Suoritusaika on 25272ms, eli imperatiivisen koodin aika jaettuna ytimien määrällä plus overhead. Kahdeksan ytimen kone ajaa operaatiot lähes 8x nopeammin, ja skaalautuu jos ytimiä onkin 16, 32, jne..

Yhteenveto: Java 8 uudet piirteet ovat uutta arsenaalia ohjelmoijan työkalupakissa. Jo selkeyden ja ongelmanratkaisun välineinä niitä ei ole varaa olla käyttämättä. Ne eivät automaattisesti ole ylivoimaisia suoritusnopeudessa – mutta tehtävissä joissa rinnakkaisuudesta voi olla hyötyä, ne tuovat ylivertaista ja ohjelmoijalle helppoa skaalautuvuutta. Ja vielä on yksi syy lisää opiskella tämä NYT: Ellet opiskele, pian voi pamahtaa silmille Java-koodia jota et enää pysty ymmärtämään. Muistatko miten kävi Java 5 kohdalla? 😉

Pari tärppiä vielä loppuun. Streamien debuggaus voi olla hankalaa – ja tuki vielä huteraa kehitysvälineissä. Apuun tulee peek()-funktio, jolla voit kurkata prosessoinnin sisään. Esim. näin:

return courseList.parallelStream()
  .filter(App::typeThreeIsValid)
  .filter(App::statusThreeIsValid)
  .sorted(App::sortByDate)
  .peek(System.out::println)
  .map(App::fixDate)
  .collect(Collectors.toList());

Ei siis hullumpaa uudistusta. Matkaa on vielä Scalan hienouksiin mutta nyt meillä on jotain käyttökelpoista joka taas inspiroi koodaamista vuosiksi eteenpäin.

JFokus 2014 loppuraportti

Osallistuin tänä vuonna JFokukseen sekä kuuntelijana että myös näyttelyn puolella yläkerrassa. Meillä oli täkynä LeapMotion laite ja se herättikin huomiota. Olen jo aiemmin bloggaillut sen koodailusta, joka on erityisen hauskaa Java 8 Lambdoilla ja JavaFX:llä.

With Great Power Comes Great Responsibility

Seminaarissa näytti toistuvan jo viime vuoden San Fransciscon JavaOne seminaarin teemat – osittain jopa samat luennot, slidet ja vitsit. Kuumia pääteemoja näyttää tälläkin vuodella olevan:

  • Tuleva Java 8 ja sen kaikki muutokset,mutta erityisesti Lambdat
  • Internet of Things muodossa: Halpoja laitteita jotka mittaavat/ohjaavat, helposti koodattavissa
  • Reaktiiviset käyttöliittymät ja JavaScriptin uusi tuleminen

Itse innostuin myös uudella tavalla Stream API:sta. Kuten Venkat Subramaniam sanoi jo JavaOne seminaarissa – Lambdat ovat porttihuume, ja Streamit ovat se kova huume johon ne johtavat, ja joista on vaikeampaa päästä eroon. Streameissä manifestoituu kaikki se uusi hyvä (ja huono) mitä Java versio 8 tuo mukanaan, ja ne tuovat valtavasti voimaa koodaajan arkeen.Ja niitä voi alkaa käyttämään heti. Tätä heijastaen Tieturin Java 8 kurssilla Streamien osuutta tullaan syventämään.

Venkat puhuu Lambdojen ja Streamien voimasta

Java 8 julkaisu on 18. Marraskuuta – oletko valmis? Release Candidate 1 on saatavilla heti, ja paljon tietoa. Uusien piirteiden myötä tulee uskomattomasti voimaa – tehokkuuta ja koodauksen selkeyttä – mutta myös vastuuta ja sudenkuoppia. Paras alkaa treenaamaan, montaa vuotta näitä ei voi väistellä enää. Itse ajan RC1 versiota Java 8:sta Raspissani ja muutama muukin päivittäisessä käytössä oleva työkalu on suoraan JDK 8:lla rakennettu.

Java 1.8.0 RC1 in Raspberry Pi

Mielenkiintoista on myös,että koska tämä ei ole Oraclen seminaari, myös Google näkyi täällä, ja osallistuin mielenkiintoiseen esitykseen tänä vuonna julkaistavasta Google Glass-älylaseista. Pär Sikö piti hyvän esityksen niiden ohjelmoinnista. Ne ovat näemmä käytössä jo mm. palomiehellä ja parillakin lääkärillä työssä, ja koodausmahdollisuudet sen kun vain lisääntyvät.Uuden GTK rajapinnan myötä monetkin Android 4:lle kehitetyt sovellukset ovat siirrettävissä uuteen käyttöön – kunhan vain käyttöliittymän rajoitukset huomioidaan.

Google Glass - What's Best In Life?

Tuli itsekin kokeiltua niitä – lasit reagoivat kiitettävän nopeasti komentoihin ja näyttö on kirkas,selkeä ja hieno. Käyttöliittymä saa tottumattoman takeltelemaan, etenkin kun jotain menee pieleen. Mielenkiintoista miten olemassaolevat Android kehitysvälineet – ja myös osaaminen – sopivat suoraan tälle.

JFokus

Ruotsin JFokus on skandinaavisen softakehittäjän kannalta mielenkiintoinen ja helposti saatavilla oleva seminaari jota voin lämpimästi suositella.

JFokus 2014

Matka alkoi taas kohden Tukholmaa – määränpäänä JFokus 2014 – Ruotsin suurin kehittäjäkonferenssi. Sisältö näyttäisi suuntautuvan samantapaiseksi kuin JavaOne viime vuonna – luvassa siis Lambdoja ja Esineiden Internettiä IoT – kaikenlaista kiinnostavaa.

Minä tulen tänä vuonna päivystämään Tieturin/Informatorin standilla – hihasta voi tulla nykäisemään tai osallistumaan LeapMotion laitteen arvontaan. Toivon mukaan ehdin pari esitystäkin kuuntelemaan.

http://www.jfokus.se/jfokus/