Slimmer programmeren met een slimmere compiler

Nieuwe features in Java

Onze digitale samenleving is in een nieuwe stroomversnelling terecht gekomen. We zijn in staat geworden om machines te laten leren. Zelfrijdende auto’s zijn geen science fiction meer, robots kunnen door een grillig landschap rennen, computers kunnen telefonisch een afspraak voor je maken. Al deze mogelijkheden vragen ook om nieuwe manieren van programmeren. In Java is dat onder meer terug te zien in een relatief nieuwe focus op “functioneel programmeren”. Een slimmere compiler, en een grotere mate van abstractie bij het programmeren helpen daarbij. In deze blog belicht onze trainer bij Vijfhart enkele recente ontwikkelingen in Java, die slimmer programmeren mogelijk maken.

Object georiënteerd programmeren

Java is bekend geworden als object-georiënteerde taal. Daarin wordt een “artikel” of “bestelling” beschouwd als een object, met data (zoals naam of prijs) en gedrag (zoals het berekenen van de prijs met btw). Begrippen als overerving, encapsulation en polymorfisme komen uit de object-georiënteerde hoek. Java programmeurs zijn gewend om zulke objecten te zien als belangrijkste bouwstenen van applicaties. En de meeste van die bouwstenen zullen ze zelf moeten maken. In zekere zin geeft object oriëntatie dus niet alleen een model van de werkelijkheid in code, het stuurt ook de manier waarop programmeurs denken over problemen.

Imperatief programmeren

Ook processen, waarbij informatie moet worden verwerkt, worden traditioneel bekeken met een object-georiënteerde bril, namelijk: welke data heb ik (eventueel tijdelijk) nodig en in welke stappen (gedrag) kom ik tot mijn gewenste eindresultaat. En ook hierbij levert de programmeur de bouwstenen zelf aan. Er worden variabelen uit de kast gehaald om informatie tijdelijk te bewaren, loop-constructies om delen van het proces te herhalen, if-statements om condities te testen, etc. Wie dergelijke programmacode bekijkt merkt dat hierbij veel code wordt besteed aan “hoe krijg ik het uiteindelijk voor elkaar?“, en relatief weinig aan “wat moet er eigenlijk gebeuren?“. Deze manier van modelleren van processen wordt wel imperatief programmeren genoemd.

Onderstaand voorbeeld toont hoe op de imperatieve manier de totale prijs van alle artikelen onder de 10 euro bij elkaar worden opgeteld:

static double berekenTotaal(List artikelen){
  double totaal = 0;                          // variabele totaal
  for(int i=0; i<artikelen.size(); i++){      // variabele i, loop
    if(artikelen.get(i).getPrijs()<10){       // if statement
      totaal += artikelen.get(i).getPrijs();  // totaal aanpassen
    }
  }
  return totaal;                              // resultaat geven
}

Of in pseudo-code:

  • maak een variabele aan voor het totaal en zet deze op 0
  • begin een herhaling, met een variabele i die begint op 0
  • zolang i kleiner is dan het aantal elementen in de lijst artikelen:
    • als de prijs van het artikel kleiner is dan 10
      • haal uit de lijst op positie i de prijs op van het betreffende artikel
      • hoog het totaal op met die prijs
    • hoog i op met 1
    • herhaal bovenstaande stappen als i kleiner is dan het aantal elementen
  • geef de waarde van het totaal

Merk op dat het grootste deel van bovenstaande stappen te maken heeft met de technische randvoorwaarden om tot de oplossing van het probleem te komen.

Vanaf Java 8: functioneel programmeren met lambda expressies

Waar object-oriëntatie een relatief statisch model is van de werkelijkheid, legt functioneel programmeren meer nadruk op het proces. En anders dan de imperatieve manier van werken komt hierbij de focus te liggen op “wat moet er gebeuren”. Hierin zien we geen hulpvariabelen, loops, if statements, maar alleen functies die moeten worden uitgevoerd. Dezelfde berekening van de totale prijs ziet er dan bijvoorbeeld als volgt uit:

static double berekenTotaal(List artikelen){
  return artikelen.stream()
      .mapToDouble(artikel -> artikel.getPrijs()) // haal prijs op
      .filter(prijs -> prijs < 10) // alleen prijzen kleiner dan 10
      .sum();                      // totaal
}

Of in pseudo-code:

  • voor alle artikelen:
    • haal uit elk artikel de prijs
    • houd alleen prijzen over kleiner dan 10
    • tel deze bij elkaar op en geef het resultaat

Merk op dat in deze code geen variabelen zijn gemaakt en dat er ook geen for loop of if statement in voorkomt. Wel zien we twee keer staan: “doe iets met de informatie die langs komt”, namelijk: haal de prijs uit een artikel en kies alleen prijzen kleiner dan 10. Deze statements met pijltjes worden lambda expressies genoemd, waarmee sinds Java 8 kan worden gewerkt. Het programma wordt nu teruggebracht tot de essentie.

De informatie die op deze manier verwerkt wordt, wordt de Stream API genoemd. Een API is een Application Programming Interface. Zo’n API neemt veel van de complexiteit van standaardtaken – in dit geval m.b.t. het werken met een informatiestroom – uit handen. Deze standaardtaken krijgen een naam zoals map voor een bewerking, of filter voor het beperken van de stroom elementen. De logica die bepaalt hoe bewerkt of gefilterd moet worden geeft de programmeur zelf aan in de vorm van een lambda expressie.

De verdeling tussen standaard Java functionaliteit in de API en de taak van de programmeur kan als volgt worden verduidelijkt. We herhalen bovenstaande stappen, maar dan uitgesplitst naar API versus de programmeur:

De middelste kolom betreft algemene, veelvoorkomende taken. Onder de motorkap zitten daarin de variabelen, herhalingen en condities verstopt die we vroeger zelf moesten maken. Wat er voor de programmeur overblijft is het specificeren van welke functionaliteit bij elke stap gewenst is.

Meer abstractie met een slimmere compiler

Om het werken met lambda expressies mogelijk te maken is de compiler slimmer gemaakt. Vroeger kon de compiler niet veel meer dan broncode controleren en deze omzetten naar byte-code. Tegenwoordig kan de compiler ook code toevoegen, zodat we dat zelf niet hoeven te doen. Een deel van het “hoe precies” kan worden overgelaten aan de compiler, zodat de programmeur zich kan concentreren op de gewenste functionaliteit. Zo zijn bovenstaande lambda expressies in feite nog steeds objecten. De compiler maakt er zelf zoiets van:

static double berekenTotaal(List artikelen){
  return artikelen.stream()
         .mapToDouble(new ToDoubleFunction(){             
            public double applyAsDouble(Artikel artikel){ 
              return artikel.getPrijs();
            }
          }) 
         .filter(new DoublePredicate(){
            public boolean test(double prijs){
              return prijs < 10;
            }
         })
         .sum();
}

Het schuingedrukte deel heeft de compiler zelf toegevoegd. Dat wordt wel “boilerplate code” genoemd, noodzakelijke extra code om de gewenste functionaliteit werkend te krijgen. Het vetgedrukte deel bevat de gewenste functionaliteit die de programmeur specificeert met een lambda expressie.

Abstractie versus bruikbaarheid

De introductie van lambda expressies en streams in Java 8 was een grote stap in de richting van functioneel programmeren. De introductie van dergelijke nieuwe manieren van werken is altijd spannend. Het blijft in het begin namelijk de vraag of die nieuwe features wel gebruikt gaan worden. Oracle stelt in feite een ruil voor: Als jij bereidt bent om te leren werken met lambda expressies en streams, dan zal ik een groot deel van de complexiteit van het coderen voor je overnemen.

Inmiddels bestaan lambda’s en streams al een tijdje en we kunnen stellen dat deze ruil met succes is gemaakt. Een deel van dat succes is te verklaren uit de goede balans tussen abstractie en bruikbaarheid van de API. Het gebruik van streams en lambda expressies zorgt voor een benadering van programmeerproblemen op hoger, abstracter niveau, maar het blijft concreet genoeg om goed mee te kunnen werken.

Dat dat niet vanzelfsprekend is bewijst een package als java.nio.channels, bedoeld als abstractie-laag om entiteiten die I/O kunnen uitvoeren. Sinds de introductie daarvan in Java is er maar weinig gebruik van gemaakt: het werd door velen te ingewikkeld gevonden om goed mee te kunnen werken.

Vergelijkbare ontwikkelingen

In nieuwe versies van Java blijven ze stappen zetten in het overnemen van de complexiteit van boilerplate code. Een korte greep hieruit:

  • Java 5: enhanced for loop, neemt complexiteit weg van het doorlopen van alle elementen uit een array of collectie;
  • Java 7: Automatic Resource Management zorgt dat bestanden en andere af te sluiten bronnen automatisch worden afgesloten;
  • Java 9: JShell geeft een omgeving om snel stukjes code te kunnen testen, zonder daar classes voor te hoeven maken en te compileren;
  • Java 10: Local variable type inference, maakt het mogelijk om het datatype van variabelen af te leiden uit de waarde die eraan wordt toegekend.

Conclusie

Java is steeds beter in staat om de repeterende complexiteit van coderen uit handen te nemen van de programmeur. Dat is niet alleen prettig, maar ook noodzakelijk geworden om als programmeertaal bruikbaar te blijven. In de toekomst blijven uitdagingen op programmeergebied groeien. Denk bijvoorbeeld aan gedistribueerde systemen, die een continue stroom aan gegevens parallel moeten verwerken. Dergelijke problemen vereisen dat de programmeur zich kan concentreren op de business logica, en dat Java zelf slim genoeg is om boilerplate code zelf in te vullen.

Onderwerpen
Actieve filters: Wis alle filters
Pageloader
PRIVACY VOORWAARDEN

Jouw persoonsgegevens worden opgenomen in onze beschermde database en worden niet aan derden verstrekt. Je stemt hiermee in dat wij jou van onze aanbiedingen op de hoogte houden. In al onze correspondentie zit een afmeldmogelijkheid