Competentie: Ik kan mijn eigen datatypen maken en gebruiken (gebaseerd op primitieve typen)
Specifieke leerdoelen:
De opdrachten kunnen in de submodulen gevonden worden.
Een eigen datatype kan je maken om meerdere stukjes informatie (in de vorm van (primitieve) datatypen zoals int, double, boolean, char of een String / ArrayList, etc.) bij elkaar op te slaan. Denk bijvoorbeeld maar eens hoe je een “student” zou kunnen representeren in je code. Je hebt meestal dan een naam, een studentnummer, een adres, etc. en die data behoort allemaal tot één student. Als je vervolgens meerdere studenten wil gebruiken in je programma kom je al snel tegen het probleem aan dat het aantal variabelen dat je moet gebruiken enorm hard toeneemt. Er zijn wel “trucs” te bedenken om het leven hier makkelijker te maken, maar in deze competentie heb je geleerd dat de beste oplossing is om een eigen datatype te maken (bijv. “Student”). Door de computer te leren wat een “Student” is, kan deze je helpen met het bundelen van deze informatie, wat voor jou als programmeur het leven een stuk beter maakt.
Het aanmaken van een eigen datatype betekent letterlijk dat je een los bestand (een Java class) in je project moet opnemen met de naam van jouw datatype. Dit datatype kan je dan vervolgens attributen (of velden) geven voor de data die jij nodig hebt. Elk attribuut wordt vervolgens op een eigen regel gezet en eigenlijk als een variabele uitgeschreven. Van elk attribuut moet je dus, net als bij een variabele, een naam en een datatype opgeven.
Laten we als voorbeeld maar eens het datatype voor een Student maken waarin we een voornaam, achternaam, studentnummer en woonplaats willen opnemen.
Je begint hiermee door een bestand aan te maken genaamd “Student.java” (IntelliJ kan je hiermee helpen) en de volgende inhoud te geven.
public class Student {
String firstName = "No";
String lastName = "Name";
int studentNumber = -1;
String city = "Nowhere";
// Rest is omitted for demo..
}
We noemen dit bestand en stuk code ook wel een definitie van een eigen “klasse”. Jouw programma bevat dus nu de “klasse” “Student” (klasse is de vertaling van het woordje “class” wat je in de code kan lezen). De klasse “Student” kan je het beste voorstellen als een doos waarin je andere variabelen kan stoppen. Elke “student” die je gaat maken op basis van deze klasse zal namelijk automatisch een variabele krijgen voor “firstName”, “lastName”, etc. (alle attributen!). Om eventuele verdere problemen te voorkomen raden wij jullie aan om attributen in deze klassen altijd een eerste waarde te geven. Je snapt natuurlijk dat de student niet echt “No Name” heet, maar als we nu ergens “No Name” in ons programma zien weten we dat mogelijk een fout hebben gemaakt.
Het gebruik van dit datatype in jouw eigen code ziet er als volgt uit:
// Somewhere in your run method
Student newStudent = new Student();
newStudent.firstName = "Tristan";
newStudent.lastName = "Pothoven";
newStudent.studentNumber = 1234567;
newStudent.city = "Enschede";
SaxionApp.printLine(newStudent.firstName); // This will print "Tristan"
Let met name op de eerste regel. Het = new Student()
(oftewel de initialisatie van de variabele) is verplicht. Je geeft hier Java namelijk mee de opdracht om een stukje geheugen te reserveren voor de data die bij een Student hoort. Doe je dit niet en wordt er dus geen gereserveerd dan kan je de interne attributen (firstName, lastName), etc. niet gebruiken (want er is geen ruimte gereserveerd om de inhoud op te slaan).
Vervolgens mag je elk attribuut een waarde geven. Deze waarde overschrijft uiteraard de eerder gezette “standaard” waarden van een attribuut. Mocht je nu een attribuut vergeten te overschrijven, dan zal de standaardwaarde (zoals in de klasse genoemd) blijven staan.
Regel 1 zorgt er ook voor dat er een instantie van de klasse Student wordt aangemaakt. De term instantie komt ook weer uit het Engels (instance) en kan je als volgt onthouden: De klasse Student beschrijft welke informatie (attributen) je van een Student gaat onthouden, een instantie van de klasse Student is een ingevuld exemplaar (dus bijv. “newStudent” is nu ingevuld met de gegevens van Tristan Pothoven, studentnummer 1234567 woonachtig in Enschede.) Je hebt altijd maar 1 klasse (blauwdruk, beschrijving) van iets in je applicatie terwijl je wel tientallen of zelfs honderden instanties van de klasse kan hebben.
De kracht van het gebruik je eigen datatypen zal je vooral gaan zien als je meerdere instanties gaat gebruiken van dezelfde klasse. Stel je bijvoorbeeld eens voor dat je alle studenten van Saxion in een csv bestand hebt staan dat je wil gaan inlezen en het csv bestand ziet er als volgt uit:
studnr,firstName,lastName,city
1234567,Tristan,Pothoven,Enschede
1234568,Craig,Bradley,Deventer
1234579,Gerralt,Gottemaker,Deventer
.. etc.
dan kan eenvoudig een lijst met “Student” instanties maken door het volgende te doen:
// Somewhere in your run method.
CsvReader reader = new CsvReader("students.csv");
reader.skipRow(); // the headers
reader.setSeparator(','); // set the correct csv separator
ArrayList<Student> students = new ArrayList<>(); // We'll store all Student instances in this list.
while(reader.loadRow()) {
Student currentStudent = new Student(); // We need to initialize the variabele to make room for the contents
currentStudent.studentNumber = reader.getInt(0);
currentStudent.firstName = reader.getString(1);
currentStudent.lastName = reader.getString(2);
currentStudent.city = reader.getString(3);
students.add(currentStudent);
}
Na het uitvoeren van deze code heb je in je programma nu een ArrayList van “student instanties” die je kan gebruiken. Het helpt als je je deze lijst daadwerkelijk voor je ziet als een groep met mensen. In je code kan je hier nu van alles mee doen, bijv. laten uitprinten wie er in Enschede woont:
for(Student someStudent : students) { // Enhanced for-loop to iterate over the list of Student instances
if(someStudent.city.equals("Enschede")) { // Retrieve the city attribute (type String) from the student instance and check whether it matches Enschede
SaxionApp.printLine(someStudent.firstName + " lives in " + someStudent.city);
}
}
Tot nu toe heb je data in je applicatie eigenlijk altijd opgeslagen door middel van de primitieve datatypen (int, double, boolean, char) en met behulp van de String klasse. Het probleem wat je mogelijk wel kan inzien is dat je op een gegeven moment data wil gaan combineren: hoe koppel je bijvoorbeeld een studentnummer aan een naam van een student? Hier zijn wel trucs voor te bedenken met bijv. het gebruik van meerdere lijsten, maar eigenlijk is het veel handiger om de computer te leren wat nu een “Student” datatype is. En dat is precies wat je in deze competentie leert.
Voordat we beginnen is het echter goed om een paar termen helder te krijgen. Om te beginnen gaan we in deze competentie een eigen (referentie)datatype maken die gebaseerd is op de primitieve datatypen. Met primitieve datatypen bedoelen we alle datatypen binnen Java die je schrijft met kleine letters, bijv. int, double, boolean, char, etc. De reden waarom deze primitief worden genoemd heeft er vooral mee te maken dat je van dit soort datatypen precies weet hoe groot te zijn en hoeveel geheugen je hiervoor moet reserveren in je computer (er zijn meer redenen, maar die laten we nu even achterwege). Alle datatypen die met een hoofdletter beginnen (bijv. String, SaxionApp, Color, etc.) zijn complexere stukken code die allemaal gebouwd zijn met behulp van de primtieve datatypen. Het vervelende in het gebruik van deze datatypen is dat je vaak niet weet hoeveel geheugen er gebruikt moet worden om een variabele van dat type op te slaan. Neem bijv. maar eens een String in gedachte. In een variabele van het type String kan je 1 letter opslaan, 1 woord, 1 zin of zelfs 1 volledig boek. Je snapt dat dit qua geheugengebruik wel iets uitmaakt. In een volgende module leer je precies wat er onderwater gebeurt met dit soort type variabelen.
We gaan dus ons eigen complexe datatype maken in de vorm van een “klasse”. Een “klasse” is niets anders dan een beschrijving hoe het datatype er uit ziet (een blauwdruk). Zo’n klasse (of blauwdruk) representeert een “object” of een ding wat je je beter voor kan stellen. Denk bij object bijvoorbeeld maar aan een “Huis”, “Student”, “Persoon”, “Fabriek”, etc. We hebben het echter tot nu toe altijd over de ideeen achter het object.. dus we maken het nog niet concreet. Als je in je code daadwerkelijk een variabele gaat aanmaken van (bijv.) het type Student en daarin je eigen data gaat opslaan, dan noemen we dit ook wel dat je een instantie van dit type aan het aanmaken bent.
De termen “klasse”, “object” en “instantie” zijn in het begin lastig om uit elkaar te houden, maar vormen gezamenlijk wel een van de basisprincipes van het “Objectgeorienteerd programmeren”. Probeer dus altijd goed te snappen wat het onderscheid nu precies is.
Voor nu echter is het prima om een onderscheid te maken tussen “klasse” en “instantie”. Een klasse moet je daarbij zien als een omschrijving van het datatype en een instantie is het daadwerkelijke gebruik van het datatype in de vorm van het aanmaken van een variabele van dat type. De meeste programma’s die jij gaat schrijven in de komende tijd zullen waarschijnlijk een beperkt aantal klassen bevatten, maar gaan waarschijnlijk wel vele (tientallen / honderden) instanties van deze klassen bevatten.
Een eigen datatype maken in Java is erg eenvoudig. Het bestaat namelijk uit het aanmaken van een nieuw bestand en die de naam van je eigen datatype te geven. Vervolgens ga je in dit bestand je klasse definieren, inclusief alle attributen die bij deze klasse behoren.
Laten we echter beginnen bij het begin en een leeg bestand aanmaken. Die doe je door op de “src” map te klikken met de rechtermuisknop en vervolgens “New –> Class” te selecteren.
Je krijgt nu een popup te zien waarin je de naam van een klasse aangeeft. In dit geval kiezen wij voor “Student” en je zal zien dat er een (vrijwel) leeg bestand wordt opgeleverd met de juiste naam.
De enige inhoud die het bestand bevat is het “skelet” van een klasse. Dit skelet zal je in elk Java bestand terugvinden. (Kijk maar eens naar de Application klasse, daar zie je eenzelfde opzet, maar met veel meer inhoud.)
Het laatste wat je nu moet doen is attributen specificeren. Handig is om je hierbij voor te stellen dat je nu eigenlijk bezig bent met “geneste” variabelen. Wij zijn een klasse Student aan het maken dat eigenlijk niets anders is dan een labeltje voor een verzameling interne variabelen. Dus stel dat je het volgende zou opschrijven:
public class Student {
String firstName = "No";
String lastName = "Name";
int studentNumber = -1;
String city = "Nowhere";
}
dan moet je dat lezen alsof je een “Student” datatype hebt gedefinieerd, waarvan elke student een firstName (String), lastName (String), studentNumber (int), city (String) heeft.
Aangezien je eigenlijk niets anders doet dan variabelen aanmaken in de klasse Student, is het adviseerbaar om deze gelijk een initiele (liefst: ongeldige) waarde te geven.
En dat is alles! Je hebt nu een “Student” klasse gemaakt die gebruikt kan worden in jouw programma om alle data van een Student te bevatten.
Nu we de blauwdruk hebben vastgelegd van de klasse, dus we weten dat elke student een voornaam, achternaam, studentnummer en woonplaats heeft, kunnen we dit gaan gebruiken. Hiervoor moeten we een “instantie” van de klasse maken. Een instantie is niets anders dan een enkele invulling van de klasse, dus bijv. een “student” in een groep van meerdere studenten. (Neem bijv. je eigen klas, waarschijnlijk zit je in een groep met 16-32 studenten. Voor elk van die studenten geldt dat ze dezelfde blauwdruk hebben, maar je hebt wel 16-32 verschillende instanties van deze klasse.)
Als je een variabele met je eigen datatype wil maken (lees: een instantie van maken), doe je dit als volgt:
Student newStudent = new Student();
Merk op dat dit sterk lijkt op het gebruik van een normale variabele, behalve dan dat het rechterdeel = new Student()
. Zoals eerder genoemd heeft dit te maken met het feit dat Java opdracht moet geven om geheugenruimte te reserveren voor deze instantie. Dit is verplicht en zal leiden tot fouten als je het vergeet.
Een paar meer voorbeelden zouden kunnen zijn (de klassen zijn even weggelaten):
Person somePerson = new Person();
Dog myDog = new Dog();
House scaryHouse = new House();
Sport myFavoriteSport = new Sport();
Merk op dat de naam die de variabele krijgt (net zoals met normale variabelen) niets uitmaakt voor de werking van je programma, maar wel voor de leesbaarheid.
Als we terug naar het student voorbeeld, dan kunnen we de instantie (die nu nog leeg is!) invullen door de waarden van de variabelen in te vullen (overschrijven). Dit doe je in code op dezelfde manier als dat je elke andere variabele zou aanpassen, echter zit er nu een “niveau” extra in.
Student newStudent = new Student();
newStudent.firstName = "Tristan";
newStudent.lastName = "Pothoven";
newStudent.studentNumber = 1234567;
newStudent.city = "Enschede";
SaxionApp.printLine(newStudent.firstName); // This will print "Tristan"
Merk op dat, om bijv. de voornaam van deze student aan te passen, je eerst de variabelenaam van de variabele van het type Student moet aanroepen. Lees dus bij deze zin newStudent.firstName = "Tristan";
als: “Vervang van de student waarnaar ik verwijs met newStudent de waarde van firstName door..”. Je kan dus niet snel alle namen van alle studenten gelijk zetten (want dit is ook meestal niet wat je wil).
En dat is alles! Je hebt nu gezien hoe je een eigen datatype kan schrijven en gebruiken.. Maar waar we nog weinig aandacht aan hebben besteed is waarom we dit eigenlijk willen. We sluiten deze competentie daarom af met een uitgebreider voorbeeld.
Stel dat we de volgende datasets hebben:
students.csv
studnr,firstName,lastName,city
1234567,Tristan,Pothoven,Enschede
1234568,Craig,Bradley,Deventer
1234579,Gerralt,Gottemaker,Deventer
.. etc.
grades.csv (let vooral niet op de data zelf :-))
studnr,courseName,date,grade
1234567,Introduction to Programming,15102012,10
1234567,Curiosity,31122020,4
1234568,Introduction to Programming, 26092018,8
1234568,Curiosity, 31082019,8
1234579,Introduction to Programming, 31112017,9
1234579,Curiosity, 28092020,10
.. etc.
(Let op: De link tussen de datasets is het studentnummer..)
en we een programma willen schrijven dat een student in staat stelt zijn cijferlijst in te zien, dan kunnen we de Student klasse als volgt implementeren (om te beginnen).
public class Student {
String firstName = "";
String lastName = "";
int studentNumber = -1;
}
Deze klasse echter houdt nog geen rekening met de resultaten die voor toetsen zijn gehaald. We kunnen nu prima de eerste csv inlezen en hier een lijst van Studenten van maken, maar we willen nog iets meer met deze code. Als we naar de resultaten kijken zien we dat hier ook relatief eenvoudig een klasse voor te verzinnen is, bijv. genaamd CourseResult
public class CourseResult {
String courseName;
int grade;
// Date omitted for now..
}
Deze klasse stelt ons in staat om van verschillende modulen de resultaten op te slaan.. dus als we nu een lijst van CourseResult
’s aan een student kunnen koppelen, dan hebben we in feite een soort van cijferlijst gemaakt.
We maken dus een kleine aanpassing op de klasse Student:
public class Student {
String firstName = "";
String lastName = "";
int studentNumber = -1;
ArrayList<CourseResult> results = new ArrayList<>();
}
De klasse Student heeft nu een attribuut results
waarin we eventuele toetsresultaten (in de vorm van CourseResult
) instanties kunnen opslaan. Op deze manier koppelen we een bepaalde student dus aan zijn eigen resultaten.
Nu we de datatypen hebben gedefinieerd, kunnen we beginnen met het inlezen van de data uit het csv bestand. Elke regel zal worden vertaald naar een eigen Student instantie en toegevoegd worden aan de lijst.
// Somewhere in your run method.
CsvReader reader = new CsvReader("students.csv");
reader.skipRow(); // the headers
reader.setSeparator(','); // set the correct csv separator
ArrayList<Student> students = new ArrayList<>(); // We'll store all Student instance in this list.
while(reader.loadRow()) {
Student currentStudent = new Student(); // We need to initialize the variabele to make room for the contents
currentStudent.studentNumber = reader.getInt(0);
currentStudent.firstName = reader.getString(1);
currentStudent.lastName = reader.getString(2);
currentStudent.city = reader.getString(3);
students.add(currentStudent);
}
Merk op dat we nog niets met de results ArrayList gedaan hebben. Dit komt omdat we deze data namelijk uit een ander bestand moeten halen. Nadat de lijst met studenten is uitgelezen (en dus de ArrayList students is gevuld), kunnen we het volgende doen om, per resultaat, de data te verwerken:
// Continuation of previous example!
reader = new CsvReader("results.csv"); // We can reuse the variable "reader" because we don't need to read any more students from that file
reader.skipRow(); // the headers
reader.setSeparator(','); // set the correct csv separator
while(reader.loadRow()) {
int studentNumber = reader.getInt(0); // Store student number for easy access..
// Find the right student - Helper method that loops through students list and matches on student number.
Student student = findStudentByNumber(studentNumber);
// Create instance for course result
CourseResult result = new CourseResult();
result.courseName = reader.getString(1);
// We're skipping the date..
result.grade = reader.getInt(3);
// Add course result to the found student.
student.results.add(result);
}
In dit stuk code gebeurt samengevat het volgende: We lezen een resultaat in (per regel) en maken hier een instantie van CourseResult voor. Deze instantie echter zal in de juiste ArrayList moeten worden gestopt (elke student heeft namelijk zijn eigen lijst met resultaten). Aangezien we echter alleen maar een studentnummer hebben van deze student en niet “de gehele instantie” moeten we met behulp van een hulpmethode dit nummer omzetten naar de juiste instantie. (De implementatie van deze hulpmethode laten we even achterwegen.) Tenslotte wordt het resultaat (gebaseerd op de huidige) aan de lijst met resultaten van een specifieke student toegevoegd.
Let er op dat we in deze code ervan uitgaan dat alles goed gaat: niet-bestaande studenten (bijv. studentnummers die niet in students.csv voorkomen) zullen het systeem echt doen crashen. (Het afhandelen van dit probleem laten we even buiten deze samenvatting.)
Na het uitvoeren van deze code hebben we dus een “datamodel” wat bestaat uit een lijst van Student instanties, waarbij elke instantie een eigen lijst heeft van resultaten (van het type CourseResult). We kunnen vervolgens allerlei bewerkingen hier op doen, zoals bijv het tellen van het aantal voldoendes wat een student gehaald heeft:
// Per student..
for(Student currentStudent: students) {
int nrOfPassingGrades = 0;
// Check all grades for this student
for(CourseResult courseResult : currentStudent.results) {
if(courseResult.grade >= 6) { // If this grade is above 6
nrOfPassingGrades++; // Count it!
}
}
SaxionApp.printLine(currentStudent.firstName + " passed " + nrOfPassingGrades + " courses!");
}