1.3-Object-Georienteerd-Programmeren

De SaxionApp als bibliotheek gebruiken

Competentie: Ik kan een nieuw Java-project aanmaken met IntelliJ en (indien nodig) een library opnemen.

Competentie: Ik begrijp hoe Java I/O werkt met betrekking tot de console en hoe ik gegevens uit csv-bestanden kan lezen met behulp van de Scanner klasse.

Competentie: Ik begrijp de concepten van static en non-static variabelen en methoden, en kan deze principes toepassen op mijn eigen code.

Introductie

Sinds je (bij ons) bent begonnen met programmeren, heb je SaxionApp statements gebruikt om de meest voorkomende input / output operaties (I/O) af te handelen, zoals het afdrukken van teksten, het tekenen van vormen en het ophalen van input van een gebruiker. Maar, zoals je nu waarschijnlijk wel door hebt, is de SaxionApp functionaliteit niet opgenomen in een standaard Java installatie. Het is namelijk een zogeheten (software) bibliotheek (of library) die wij hebben gemaakt zodat jullie iets eenvoudiger kunnen leren programmeren. Je kunt bibliotheken het beste vergelijken met een verzameling voorgedefinieerde klassen (en methoden) waar bepaalde functionaliteit al in zit verwerkt. Het gebruik van deze libraries is een zeer gangbare werkwijze binnen software ontwikkeling. Er zijn maar weinig projecten waar geen bibliotheken gebruikt worden!

Het is echter tijd om de trainingswieltjes weg te halen en om naar Java zonder de SaxionApp te kijken. En daarbij de SaxionApp te behandelen als wat het is: een bibliotheek. (En bibliotheken moet je alleen opnemen als het nodig is.) Dus van nu af aan gebruiken we de SaxionApp alleen waarvoor hij het meest geschikt is: het maken van grafische gebruikersinterfaces. Alle oefeningen die dit niet nodig hebben, hoeven deze bibliotheek dus niet meer te hebben. Deze week bespreken we hoe dit allemaal moet.

Naast het oefenen met het maken van projecten en het toevoegen van bibliotheken, bespreken we deze week ook een nieuw OO-concept: static. Static variabelen en methoden onderscheiden zich van normale variabelen en methoden doordat ze iets zeggen over een hele klasse in plaats van over een instantie van die klasse. Vandaag leren we je precies wat dit betekent en waarvoor je static kunt gebruiken.

Een nieuw project maken

Vanaf deze week verstrekken we geen template projecten meer voor de oefeningen: je moet je eigen op Java gebaseerde (IntelliJ) projecten maken voor elke afzonderlijke oefening. De tutorial over het maken van een basis Java-project laten we hier achterwege, want er zijn genoeg tutorials online te vinden. Zorg ervoor dat je je aan de volgende regels houdt (die waarschijnlijk je standaardinstellingen zijn):

Voor het gemak hebben we ook een (Engelstalige) omschrijving gemaakt. Deze kan je hier vinden.

Denk ook goed na over waar je je projecten wilt opslaan. Laat geen projecten rondslingeren in je “Downloads” map bijvoorbeeld. Het is vrij gemakkelijk om een complete puinhoop van je werk te maken… Probeer dit te voorkomen!

In de ChillyGame die je vorige week misschien gestart bent staat ook beschreven hoe je een project maakt, inclusief hoe je de SaxionApp toevoegt als bibliotheek. Dus als je nog steeds niet zeker weet wat je moet doen, kijk dan gerust (nog een keer) naar deze handleiding.

Hoe je bepaalde dingen doet zonder de SaxionApp

In dit hoofdstuk bespreken we hoe je bepaalde dingen kunt doen die je de afgelopen weken hebt gebruikt en waarschijnlijk intussen als gemakkelijk beschouwt. Denk hierbij bijvoorbeeld aan iets printen naar het scherm (dat was SaxionApp.printLine(..)), een integer lezen van een gebruiker (SaxionApp.readInt()), etc.

Echter, al deze functies zijn niet standaard beschikbaar in Java (net zoals de rest van de SaxionApp) en ze zijn oorspronkelijk door ons gebouwd om ervoor te zorgen dat je je geen zorgen hoefde te maken over dingen waarvan we nog niet wilden dat je je er zorgen over zou maken. Neem bijvoorbeeld de readInt() methode die je gebruikte om een integer van de gebruiker te krijgen. Waarschijnlijk is het je wel eens overkomen dat je een woord (String), letter (char) of misschien zelfs een kommagetal (double) hebt ingevoerd in plaats van een integer. De SaxionApp gaf op dat moment een foutmelding en liet je daarna een andere waarde invoeren.

Dit is geen standaard Java gedrag: als je een woord (String) intypt, terwijl je de computer opdraagt een geheel getal in te lezen, kan de computer niet anders dan concluderen dat er iets niet klopt en zal daarom een exception gooien. (Herinner je je vorige week nog?)

Hetzelfde geldt voor het printen van waarden: we hebben de SaxionApp geschreven zodat je je geen zorgen hoeft te maken over het gebruik van kleuren of op verschillende posities in het scherm tekst te kunnen zetten. Dit is ook geen standaard gedrag. Om kleuren te gebruiken heb je nu eenmaal iets van een kleurenscherm nodig (wat niet standaard is) en om de positie te veranderen moet het systeem coördinaten begrijpen. Omdat de standaard Java uitvoer niets weet van kleuren (of coördinaten) moesten we dit implementeren. En zo hebben we nog wel meer dingen afgeschermd!

Omdat er nogal wat tijd in het maken van de SaxionApp is gaan zitten is het niet iets waarvan we je kunnen vragen het eenvoudig te herbouwen (in een weekje). We zullen daarom nog steeds de SaxionApp bibliotheek gebruiken wanneer we die nodig hebben. Sommige dingen kunnen we echter prima doen zonder de SaxionApp en daar gaan we mee oefenen in de komende weken. Dit zijn:

We gaan er van uit dat bij alle opdrachten waar getekend moet worden, je vanaf nu de SaxionApp zelf zal toevoegen.

Alle andere oefeningen maak je vanaf nu met de console output.

De console a.k.a. prompt a.k.a. terminal a.k.a. shell…

Standaard gebruikt Java de console om met de gebruiker te communiceren. Deze console heeft veel verschillende namen, zoals command line prompt (Windows), terminal (Mac/Linux) of magisch zwart schermpje, enz. Waarschijnlijk heb je het op dit moment in je carrière al eens gezien.

Command prompt

Het idee van de console is dat je deze kan gebruiken om instructies aan je besturingssysteem te geven. Maar je kan deze ook gebruiken om output van een programma te printen of input te vragen voor jouw applicatie (de overige functies negeren we binnen dit vak).

Gelukkig heeft IntelliJ zelf een eigen console, wat het werken met de console erg gemakkelijk maakt. De afgelopen weken heb je deze console mogelijk al gezien (vooral wanneer je programma crashte), maar waarschijnlijk slechts zelden gebruikt voor interactie met je programma. De console in IntelliJ bevindt zich (standaard) onderaan je scherm en wordt telkens weergegeven wanneer je een Java-programma start.

IntelliJ-console

Het gebruik van de console verschilt op een aantal manieren met het gebruik van de SaxionApp. Waar je met de SaxionApp slechts naar één “output” (het scherm) kon schrijven met SaxionApp.printLine(..), heb je nu ineens de mogelijkheid om dit op meerdere manieren te doen.

Naar de console schrijven: System.out en System.err

Er zijn twee kanalen om berichten naar de console te schrijven: System.out en System.err.

Het gebruik van beide kanalen lijkt erg op dat wat je gewend bent bij de SaxionApp. De voornaamste methoden die je moet kennen zijn:

Merk op dat het verschil tussen de aanroepen zit in de toevoeging van “ln” in de eerste aanroep. Het verschil in gedrag hiervan is precies hetzelfde als het verschil tussen SaxionApp.printLine(..) en SaxionApp.print(..). In de eerste versie wordt een nieuwe regel toegevoegd aan het einde van de tekst (ook wel een newline genoemd) en in de tweede versie wordt deze newline niet toegevoegd.

Intermezzo: Nieuwe regels afdrukken

Er is ook een speciaal karakter om een newline weer te geven, zodat je meerdere regels kunt invoeren in een enkele printopdracht. Hier zijn, afhankelijk van besturingssysteem verschillende karakters voor bedacht. Unix(-gebaseerde) systemen gebruiken vrijwel altijd het karakter \n (let op, wordt gezien als 1 karakter), Windows(-gebaseerde) systemen gebruiken \r\n (twee karakters).

Merk op dat de backslash in een char of een String gebruikt wordt als een zogenaamd escape character. Na de backslash volgt iets met een speciale betekenis, waarbij de “n” staat voor een nieuwe regel (en de “r” voor het feit dat de cursor moet worden teruggezet). Zie ook Java documentatie: Character en het volgende voorbeeld dat alleen werkt onder Unix:

Newline example

Uitvoer:

Hi!
This is a multiline
example where you
can see what a newline
is!

Door het verschil tussen \n en \r\n kan je mogelijk denken dat je nu code gaat schrijven die of voor Unix of voor Windows werkt en technisch gezien klopt dit gevoel, maar gelukkig heeft Java hier een eenvoudige oplossing voor in de vorm van de methode: System.lineSeperator(). Deze methode levert, afhankelijk van je OS, de juiste karakters op om een zin te breken. Probeer dus te wennen aan het feit dat je niet zelf deze tekens gaat invoegen, maar deze methode gebruikt waar nodig.

De meerwaarde van dit wordt het beste zichtbaar in combinatie met het gebruik van de toString() methode. Neem het volgende voorbeeld:

public class StudentGroup {

    private String groupName; // e.g. "EHI1V.Sa" or "DHI1V.So"
    private ArrayList<Student> listOfStudents;

    // Omitted the rest, but you may assume that we load in a list of students with a proper toString implementation.


    @Override
    public String toString() {
        String lineSep = System.lineSeperator();
        String result = "Student group: " + groupName + lineSep;
        
        for(Student s : listOfStudents) {
            result += s + lineSep; // Note that we now call the "toString" method from Student. We added a line seperator to the line to put each student on a new line.
        }
        
        return result;
    }
}

Opmerking: officieel is het beter om een StringBuilder te gebruiken bij het maken van dit soort toStrings (waarbij een String wordt aangevuld door een loop), maar dat negeren we nu bewust even! Als je hier meer over wilt weten, zie Java Tutorials: The Stringbuilder Class.

Voor het printen naar de console gebruiken we het public attribuut out van de klasse Systeem. Dit verwijst altijd naar het normale uitvoerkanaal dat aan je systeem is gekoppeld, bijvoorbeeld de console.

Echter, naast System.out, heb je nog een andere kanaal om eventueel naar af te printen: System.err. Je gebruikt dezelfde methoden om naar System.err te schrijven als naar System.out, namelijk println(..) en print(..). Het err attribuut verwijst echter naar de plaats waar je fouten wilt zien, dus zie het als een speciaal kanaal voor fouten. Standaard is dit kanaal ook te zien in je console, maar IntelliJ geeft tekst die je naar System.err stuurt in rood weer! Zo kun je gemakkelijk onderscheid maken tussen normale uitvoer en fouten.

De reden dat we op dit onderscheid wijzen, is dat we je willen trainen in het maken van dit onderscheid: gewone uitvoer (zoals afdrukken naar de gebruiker, overzichten, etc.) wordt afgedrukt met System.out, alle fouten (bijv. uitzonderingen) moeten naar System.err worden gestuurd.

Naast System.out en System.err heeft je systeem nog een derde interessant kanaal waar we gebruik van gaan maken: System.in. Het zal je misschien niet verbazen, maar System.in is geassocieerd met je standaard invoerapparaat, wat in bijna alle gevallen je toetsenbord is.

Intermezzo: String.format(…), String.formatted(…) en System.out.printf(..)

Nu we meer data in de console gaan laten printen, is het een goed idee om gelijk maar eens naar te kijken naar de manieren hoe je deze tekst een beetje fatsoenlijk kan opmaken. En alhoewel opmaak in deze context wat ruim gebruikt is (we gaan echt geen woorden onderstreept of dikgedrukt maken op de console), zijn er wel een aantal handige dingen waar we jullie bekend mee willen maken.

Denk bijvoorbeeld maar eens aan een kassabonnetje van een winkel. Het is je vast al eens opgevallen dat de getallen daar (over het algemeen) netjes uitgelijnd staan. Dit komt omdat er waarschijnlijk wat witruimte is gelaten achter het product wat op het lijstje staat waarbij ook rekening is gehouden met de lengte van de naam van het product. “Kaas” heeft beduidend meer witruimte nodig dan “wattenstaafjes”, etc. En dan hebben we het nog niet eens gehad over dat sommige namen worden afgekort omdat ze sowieso al te lang zijn.

De meeste programmeertalen bieden om deze problemen eenvoudig op te lossen de mogelijkheid om String waarden in een zeker format te gieten, waarbij je een format vooral moet zien als “de rest van de zin waar een waarde in moet passen”.

Neem bijvoorbeeld het volgende voorbeeld.

System.out.printf("%s is currently %d years old", "Tristan", 39)

De bovenstaande String "%s is currently %d years old" is het format waarin de waarden “Tristan” en 39 op een bepaalde plek moeten worden gezet. In dit geval staat %s voor een String waarde en %d voor een getal (digit). De %-waarden geven dus eigenlijk plekken aan, die ingevuld worden met de waarden van de overige argumenten. Deze argumenten mogen natuurlijk ook uit variabelen komen (of methoden).

Bovenstaande printf statement heeft als bijwerking dat het resultaat geprint wordt. Je kan echter ook een String variabele opbouwen door het gebruik van de format(..) en formatted(..) methode. Onderstaande voorbeelden hebben een gelijk effect:

String result = "%s has %d dogs with an average age of %f".formatted("Tristan", nrOfDogs, getAvgDogAge());

// This line is identical to the line above, BUT uses a static method. We'll discuss static later in this document.
String result2 = String.format("%s has %d dogs with an average age of %f", "Tristan", nrOfDogs, getAvgDogAge());

Het fijne van deze manier van werken is dat je geen String concatenatie meer hoeft te doen met + symbolen en dat je een nieuwe regel op elk moment kan toevoegen door %n op te nemen in je format string.

Meer voorbeelden kan je hier vinden. De officiele documentatie kan je hier vinden.

De Scanner gebruiken om gegevens van je toetsenbord in te lezen

Het grote probleem van de System.in klasse is dat je niet getypeerd data kan uitlezen. Het is alleen mogelijk om de letterlijke bytes in te lezen die worden gegenereerd wanneer je een letter op je toetsenbord typt. Er moet dus wat werk verzet worden om die toetsaanslagen om te zetten in waarden met een bepaald type (zoals int, String, enz.).

Gelukkig kent Java de klasse Scanner (klik op de link om naar de officiële pagina te gaan!), die veel van deze functies voor ons kan uitvoeren tijdens het lezen van gegevens. Voor deze module willen we het echter eenvoudig houden en gebruiken we slechts 1 methode om data in te lezen: nextLine().

Om bijvoorbeeld een zin uit te lezen van de gebruiker kun je de volgende code gebruiken:

Scanner consoleScanner = new Scanner(System.in); // Link the "Scanner" to System.in, it will now listen to your keybord.

String input = consoleScanner.nextLine(); // Read a value from the console.

Als je vervolgens van deze input een ander datatype wilt maken, zal je deze moeten parsen. Parsen is een proces waarmee de input (in dit geval tekst) geanalyseerd wordt en wordt omgezet naar een zeker ander formaat. De meeste data typen die je eerder hebt gebruikt, hebben standaard parsers in Java. De belangrijkste methoden waarmee je data kan parsen (en die je moet onthouden) zijn:

Als je dus een getal wil inlezen via een instantie van de Scanner klasse, dan ziet dit er als volgt uit:

Scanner consoleScanner = new Scanner(System.in);

System.out.print("Please enter a numerical value: ");
String inputString = consoleScanner.nextLine();
int intValue = Integer.parseInt(inputString);

Afhankelijk van het soort parse methode dat je aanroept kan het zijn dat de methode wel een NumberFormatException gooit als je iets probeert te parsen dat niet naar een bepaald type om te zetten is. Hier loop je in de opdrachten vanzelf tegen aan en is eenvoudig op te lossen met een try { ... } catch (...) blok zoals je vorige week geleerd hebt.

Merk op dat door het uitvoeren van de nextLine() methode je console zal gaan wachten op invoer (zoals je misschien gewend bent). Op je console is dit echter niet altijd even goed zichtbaar.. dus als je naar een “leeg scherm” aan het kijken bent, vraag je dan af of er geen input gevraagd wordt.

Over nextInt(), nextDouble(), etc.

Als je de documentatie van de Scanner klasse goed doorleest, zie je dat deze ook een aantal hulpmethoden aanbiedt zoals nextInt(), nextDouble() en nextBoolean(). De reden waarom wij er voor kiezen deze methoden niet te gebruiken is omdat deze methoden soms wat bijwerkingen hebben die niet altijd even goed te begrijpen zijn. Om de uitleg daarom zo simpel mogelijk te houden (en niet allemaal uitzonderingen te hoeven bespreken) raden we je aan om bovenstaande aanpak van eerst data in lezen als String en daarna te parsen te gebruiken.

Ondanks de kleine leercurve is de Scanner klasse erg handig in gebruik, omdat het op dezelfde manier zoals gegevens van het toetsenbord ingelezen worden, ook gegevens uit andere bronnen (zoals bestanden, websites, etc.) kan inlezen. En dus kunnen we de CsvReader uit de SaxionApp ermee vervangen.

De CsvReader vervangen

Om de CsvReader van de SaxionApp te vervangen moeten we eerst begrijpen hoe het eigenlijk werkt. Ten eerste opent de reader een bestand en leest dit bestand regel voor regel in met behulp van de loadRow() methode. Na het lezen van een regel wordt deze automatisch verwerkt en gesplitst in een array van (String) elementen. De elementen zijn dan individueel toegankelijk met de getInt(...) of getString(...) methoden, waarbij de CsvReader automatisch type conversies (bijv. van String naar int) doet waar dit nodig is.

Deze week houden we het iets eenvoudiger, maar gebruiken we wel dezelfde strategie.

Beschouw het volgende stuk CSV data (in een bestand genaamd csv-data.csv):

firstName;lastName;studentNr
Tristan;Pothoven;001234
Evert;Duipmans;004234
Craig;Bradley;006212
Ruud;Greven;016324
//..etc.

Als we een bestand regel voor regel willen inlezen met behulp van de Scanner klasse, kunnen we dit als volgt doen:

Scanner fileReader = new Scanner(new File("csv-data.csv"));

// We'll skip the first line (header), so we don't store the output from nextLine!
fileReader.nextLine(); 

while (fileReader.hasNext()){ // You can ask the Scanner if there is a next line! Check out the documentation!
    String lineInFile = fileReader.nextLine(); // Read the line into memory..
        
    System.out.println(lineInFile); // Just print it for now..
}

fileReader.close(); // Close the file after use.

Merk op dat we nu new File("csv-data.csv") gebruiken als constructorargument voor het creëren van een Scanner instantie, in plaats van System.in. Daarmee binden we de Scanner instantie aan een bestand op je systeem (bijv. csv-data.csv). Hier zitten natuurlijk wel een paar voorwaarden aan, zo moet dit bestand bestaan (en toegankelijk zijn) voor ons programma. Zo niet, dan wordt een FileNotFoundException gegooid waarmee Java zegt dat het bestand niet gevonden kan worden op de opgegeven locatie (of niet toegankelijk is). Ook hier kan je weer foutafhandeling toepassen, maar dit bespreken we later in de module.

Merk op dat de variabele lineInFile nu een volledige regel (in String vorm) bevat uit het bestand: Tristan;Pothoven;001234. Deze regel moeten we zelf nog wel verwerken. Het plan is om eerst deze gehele String te splitsen en dan de afzonderlijke delen te gebruiken als attributen in onze objecten. De String klasse (klik op de link) biedt gelukkig enige functionaliteit om dit te doen door middel van de split(String regex) methode, waarbij je “regex” als scheidingsteken mag beschouwen. Uit deze methode komt een String[] (String array) met alle individuele waarden die we nu kunnen gebruiken:

// Assume the file reading code is here as well..

String[] lineParts = lineInFile.split(";"); // Split the entire line based on the ; character.
String firstName = lineParts[0]; // "Tristan"
String lastName = lineParts[1]; // "Pothoven"
int studentNumber = Integer.parseInt(lineParts[2]); // 001234, Remember the parsers from earlier?

Omdat de split methode een String[] teruggeeft, zullen we ook hier dus bepaalde waarden moeten parsen. Dit werkt op precies dezelfde manier als het uitlezen van data van het toetsenbord.

Laten we het voorbeeld afsluiten met een compleet voorbeeld:

 public ArrayList<Student> readStudents(String file) {
    ArrayList<Student> result = new ArrayList<>;
    
    try {
        Scanner scanner = new Scanner(new File(file));
    
        // Skip header row
        scanner.nextLine();
    
        while (scanner.hasNext()) {
            String lineFromScanner = scanner.nextLine(); // Read the entire line
            String[] lineParts = lineFromScanner.split(","); // Break it into parts
    
            String firstName = lineParts[0];
            String lastName = lineParts[1];
            int studentNumber = Integer.parseInt(lineParts[2]); // Parse the String into an int.
            
            Student s = new Student(firstName, lastName, studentNumber); // Let's assume this exists!
            result.add(s);
        }
        
        scanner.close();
    } catch (FileNotFoundException ex) { // Note that this exception MUST be caught. We'll discuss later why.
        System.err.println("Cannot find CSV file: " + ex);
    }
    
    return result;
}

Misschien is het je opgevallen in bovenstaande voorbeeld dat we bij bestanden ervoor kiezen om de verbinding met het bestand te sluiten zodra we klaar zijn met de close() methode, terwijl we dit bij System.in niet doen. In principe willen we jullie leren om, zodra je een verbinding met iets legt, je de verbinding sluit als je er klaar mee bent. Echter, de reden waarom we dit niet doen voor System.in is dat je deze verbinding niet opnieuw kunt openen nadat je hem gesloten hebt (totdat je de applicatie opnieuw opstart). Het is daarom, voor onze opdrachten beter om System.in gewoon te vergeten om af te sluiten, maar bij alle andere verbindingen kan je dit beter gewoon wel doen!

Merk ook op dat de FileNotFoundException gevangen moet worden met een try-catch blok. Dit komt omdat deze exception een zogenaamde “checked exception” is, maar daar gaan we later in deze module dieper op in. Voorlopig mag je dit voorbeeld gewoon gebruiken in je eigen opdrachten.

En dat is het! Gefeliciteerd, je hebt in je eigen (basis) CsvReader gemaakt.

Je programma starten met de main(...) methode.

Het laatste probleem dat we moeten oplossen bij het maken van een nieuw project is begrijpen waar de eerste regel code van je programma moet staan. (Dus wat is de eerste regel code die de computer moet uitvoeren?)

Elk programma dat je schrijft heeft een startpunt nodig. En in Java is dat startpunt een methode die main heet. Je hebt deze methode al gezien in al je oefeningen tot nu toe:

public static void main(String[] args) {
    SaxionApp.start(new Application()); // This might look a different if we included a screen size.
}

Deze methode staat ergens in elk Java project (ook al maak je hem zelf niet). Het is daarbij verplicht dat deze methode er altijd hetzelfde uitziet: Java is hier heel strikt in. Waar methoden normaal gesproken volledig naar eigen smaak te maken zijn (je kunt de naam, de argumenten en het terugkeertype kiezen), is de main vooraf vastgelegd.

Dus laten we de main methode eens ontleden:

public static void main(String[] args) { .. }

De main methode heeft geen return type (dus void), maar krijgt wel een argument in de vorm van een String[]. In deze array slaat Java alle commandline-argumenten op die werden gegeven toen het programma werd gestart. Aangezien we in deze module geen commandline-argumenten zullen gebruiken, gaan we deze niet in veel detail bespreken, maar het is goed om te weten dat je van buitenaf waarden kan meegeven. Op deze manier kan je bijvoorbeeld bestandsnamen meegeven waar data uit gehaald moet worden (of waar data moet worden opgeslagen).

We geven een voorbeeld:

public class Arguments {

    public static void main(String[] args) { 
        for(String argument : args) { // Print everything in the "args" array.
            System.out.println(argument);
        }
        
        // More code could go here..
    }
    
}

Dit programma kan je als volgt gebruiken:

Command line arguments

Vanaf nu mag je je code direct in de main methode van je programma’s schrijven.

Waar we het nog niet over gehad hebben is dat de main methode ook het keyword static bevat. Dit betekent dat de main methode altijd op een klasse moet worden aangeroepen en niet op een individueel object. Wat dit precies betekent zullen we nu bespreken.

Static versus non-static

In deze module heb je veel methoden geschreven die direct gerelateerd zijn aan de waarden van attributen van instanties. Neem bijvoorbeeld de methode getName() van een Person klasse: je verwacht dat deze methode de naam teruggeeft van de persoon die door die instantie wordt beschreven. We noemen dit feit ook wel dat de methode context afhankelijk is (meestal gebruik je ook this). De waarden van de attributen in het object bepalen de waarde die wordt teruggegeven door de methode-aanroep.

public class Person {
    private String name; // This attribute will have a value specific to this instance!

    public Person(String name) {
        this.name = name;
    }
    
    public String getName() { // This method is context-specific!
        return name;
    }
}

Je kan je ook vast voorstellen dat er methoden zijn die niet afhankelijk zijn van de context (of de inhoud van een object). Deze methoden worden ook wel contextloze of static methoden genoemd (let op het woord static in de methodedefinitie). De meeste methoden die je schreef in de cursus Introductie Programmeren waren in feite contextloos (en zouden static kunnen worden gemaakt, maar dit hebben we niet gedaan om het eenvoudig te houden). Static methoden vertonen altijd hetzelfde gedrag als je ze met hetzelfde argument aanroept, ongeacht de toestand van een gegeven object.

Neem bijvoorbeeld de volgende methode:

public static int sum(int[] input) {
  int result = 0;
  
  for(int value : input) {
    result += value;
  }
  
  return result;
}

De methode sum(int[] input), zoals hierboven getoond, is alleen afhankelijk van de variabele input. Geen enkele andere waarde is nodig om deze methode zijn werk te laten doen.

Vanaf nu willen we dat je alle contextloze methoden static maakt, wat betekent dat je bevestigt dat de methode geen context nodig heeft.

Naast de sum methode, kun je je waarschijnlijk nog wel een paar andere nuttige methoden verzinnen die handig zijn als je werkt met integer arrays, zoals bijvoorbeeld: calcAverage (om een gemiddelde te berekenen), findMin (om de kleinste waarde te vinden), findMax, enz. Mocht je nu deze functies vaker willen gebruiken (door een applicatie heen), kan je er voor kiezen om deze methoden op te nemen in een eigen klasse, bijvoorbeeld Utils (afkorting van utilities).

public class IntArrayUtils {
    public static int sum(int[] input) { ... }
    
    public static double calcAverage(int[] input) { ... }
    
    public static int findMin(int[] input) { .. } 
    
    public static int findMax(int[] input) { .. } 
}

De vraag die je je nu bij deze klasse moet stellen is: Waarom zou je deze klasse willen instantiëren? Wat is de meerwaarde van het hebben van een object van het type IntArrayUtils? De klasse bevat geen interne attributen en in feite is de klasse gewoon een bestand om wat handige methoden in te stoppen.

Je kan de bovenstaande klasse als volgt gebruiken:

int[] numbers = { 5, 2, 7, 7 }; // Suppose these numbers are from some caluation.

int sum = IntArrayUtils.sum(numbers); // Just call the method directly on the class instead of an object!

In het vorige voorbeeld noemen we ook het feit dat de methoden nu gekoppeld zijn aan de klasse en niet zozeer aan eventuele instanties van de klasse. Ze zijn contextloos. Static methoden (en variabelen) zijn gekoppeld aan klassen, niet-static methoden (en variabelen) aan objecten (of instanties).

Neem nu de eerder genoemde klasse Person en stel dat je je programma wilt voorzien van functionaliteit waarmee je gegevens uit een CSV-bestand kunt lezen, bijvoorbeeld in de vorm van de methode readPersonsFromFile(String fileName).

Deze methode heeft te maken met deze Person klasse. Je wilt namelijk de tekst (uit het CSV-bestand) converteren naar verschillende Person instanties. Daarom is het voor de leesbaarheid van je programma handig om deze methode op te nemen in de Person klasse. Alleen, de methode readPersonsFromFile(String) is niet gebonden aan een specifieke instantie van Person. Met andere woorden, de methode hoort bij de klasse (en niet bij het object) en dus moet je deze methode static maken.

Dit ziet er als volgt uit:

public class Person {
    private String name; // This attribute will have a value specific to this instance!

    public Person(String name) {
        this.name = name;
    }
    
    public String getName() { // This method is context-specific!
        return name;
    }
    
    public static ArrayList<Person> readPersonsFromFile(String fileName) {
        // See earlier examples on how to use the Scanner to read files!
    }
}

Merk op dat de methode nog steeds is opgenomen in de eigenlijke klasse Person (tussen de accolades): de methode is dus eigenlijk een onderdeel van deze klasse, maar functioneert op een ander niveau.

Deze methode is nu te gebruiken als:

public class Application {

    public static void main(String[] args) {
        ArrayList<Person> listOfPersons = Person.readPersonsFromFile("persons.csv");
        // Do stuff..
        
    }
}

Een ‘static’ methode wordt aangeroepen via de klasse Person en niet via een individuele instantie. Het grote voordeel van deze aanpak is dat je geen instantie nodig hebt en dat je nu ook gewoon de functionaliteit die met de klasse Person te maken heeft op de juiste plaats kunt opslaan. Dit betekent dat het vinden van deze specifieke methode later veel gemakkelijker is!

Je kan ook statische methodes aanroepen via een instantie van het object, maar dit wordt sterk afgeraden. Als je dit in IntelliJ doet, krijg je de volgende waarschuwing:

Static method via instance

Het sleutelwoord static kan ook gebruikt worden voor attributen van een klasse. Een static attribuut kan gezien worden als een variabele die gedeeld wordt door alle instanties. Dit is bijvoorbeeld nuttig wanneer je klasseconstanten wilt maken of andere waarden wilt declareren die een speciale betekenis kunnen hebben binnen een klasse.

Laten we een voorbeeld nemen waarbij we het hebben over de wettelijke leeftijd van een persoon (dus op welke leeftijd deze persoon als volwassen wordt beschouwd).

Je kunt zo’n variabele declareren als:

public class Person {
    public static final int LEGAL_AGE = 18;
    
    // Omitted other code..
}

(Het woord final betekent dat een variabele zich gedraagt als een constante, dus nadat deze variabele is aangemaakt, mag je de waarde ervan niet meer veranderen. Hier komen we later in de module nog op terug.)

In je code ziet het gebruik van LEGAL_AGE er als volgt uit:

int readAgeFromUser = ..; // Determine age via interface or Scanner

if(readAgeFromUser < Person.LEGAL_AGE) {
    // We know now this person is not yet of legal age!    
}

Uiteraard kun je de variabele ook direct vervangen door de waarde 18, maar als de wet ooit verandert, zul je deze waarde moeten vervangen op alle plaatsen waar je het gebruikt. Een (static) variabele als constante gebruiken betekent dat je maar één verandering hoeft door te voeren, wat veel gemakkelijker is! (En het tweede voordeel is dat het je code ook veel leesbaarder maakt!)

In Java ben je trouwens al veel meer static attributen tegengekomen. Bijvoorbeeld in de (utility!) klasse Math. De waarde van pi is in deze klasse gedefinieerd en je kunt deze op alle plaatsen in je code gebruiken door Math.PI te gebruiken. Het attribuut PI is gedeclareerd als public static final variabele in de klasse Math.

Math pi

Videos

Static vs Non-static