Programmeren in Java/Enums
Enums, of ook wel enumeraties of opsommingen genoemd, zijn een speciaal soort datatype in Java. Ze worden gebruikt om een groep van constanten samen te houden die logischerwijs bij elkaar horen. Neem nu de maanden van het jaar. Je weet dat januari de eerste maand is van het jaar en februari de twee enzoverder... Je wilt ergens in je code constanten bijhouden die dit bijhoudt om bijvoorbeeld met datums te kunnen werken. Dus je zou in je code het volgende kunnen doen:
static final int JANUARY = 1;
static final int FEBRUARY = 2;
static final int MARCH = 3;
static final int APRIL = 4;
// enzoverder...
Dit zal goed werken, maar het kan beter. Wat als we dit willen gebruiken op andere plekken in onze code, dan zouden we wellicht deze code moeten kopiëren en zodra je code moet beginnen kopiëren om het te kunnen hergebruiken is het beter om een alternatief te vinden. Daarvoor worden dus enums gebruikt.
Basis
bewerkenZoals eerder werd vermeld is een enum een datatype dat een groep van constanten samenhoudt. Je kunt het zien als een soort klasse, maar een klasse waarvan je zelf geen instanties kunt aanmaken met een constructor en het sleutelwoord new, maar enkel pregedefiniëerde instanties kunt gebruiken. Een simpele enum aanmaken doe je als volgt.
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
}
Het is gelijkaardig met het maken van een gewone klasse maar in de plaats van het sleutelwoord class moet je enum gebruiken. Binnen de enum kun je dan de mogelijk instanties van de enum oplijsten, gescheiden door een komma en afgesloten met een puntkomma (;). De Java conventie zegt dat de namen van de verschillende enum instanties in hoofdletters moeten zijn(net zoals constanten) maar het zal geen fouten geven als je dit toch niet zou doen. Je kunt de enum in een eigen .java-bestand bewaren of ergens tussen voegen bij een andere klasse zoals bijvoorbeeld op de volgende manier.
public class Main {
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
}
public static void main (String[] args) {
// overige code
}
}
Een instantie van een enum oproepen doe je door de naam van de enum te gebruiken, gevolgd door een punt, met daarna de naam van de instantie die je nodig hebt, bijvoorbeeld Month.APRIL. Je kunt het dan als volgt gebruiken:
public class Main {
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
}
public static void main(String[] args) {
System.out.println(Month.APRIL); // Geeft als output "APRIL"
}
}
Nu hebben we nog niet voor iedere maand de "maand van het jaar"-eigenschap.
Enums in variabelen, constanten en functies
bewerkenEnums kun je ook makkelijk in een variabelen of constanten stoppen. Je doet dit simpelweg door een variabele of constante te declareren en dan vervolgens te initialiseren met een instantie van een enum. Bijvoorbeeld als volgt.
Month someMonth = Month.JANUARY;
final Month MY_BIRTH_MONTH = Month.APRIL;
System.out.println(someMonth); // Geeft JANUARY als output.
System.out.println(MY_BIRTH_MONTH); // Geeft APRIL als output.
Je vermeld dus als datatype de naam van de enum. En net als met alle andere datatypes kun je het gebruiken als datatype voor een parameter in een functie en als datatype voor een returnwaarde.
public Month getFollowingMonth(Month month) {
// hier je code...
}
Eigenschappen en de constructor
bewerkenEen eigenschap toevoegen is even simpel als een eigenschap toevoegen bij gewone klassen.
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
int monthOfYear;
}
We kunnen dus net zoals bij een klasse een eigenschap toevoegen door er één te declareren al dan niet met een access modifier. Als je geen access modifier gebruikt of public, kun je de eigenschap oproepen als volgt Month.APRIL.monthOfYear. Je zult echter wel meteen merken dat we de de variabele niet geïnitialiseerd hebben. Dus als we de enum gebruiken als volgt krijgen we niet het gewenste resultaat.
public class Main {
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
int monthOfYear;
}
public static void main(String[] args) {
System.out.println(Month.APRIL.monthOfYear); // Geeft als output 0
}
}
We krijgen "0" als output, omdat de standaardwaarde van een niet-geïnitialiseerde int nul is en dat is niet wat we willen hebben. We kunnen het ook niet rechtstreeks initaliseren in de enum zelf door bijvoorbeeld als volgt te doen:
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
int monthOfYear = 0;
}
Dit zal ervoor zorgen dat alle instanties van de enum nul hebben als waarde hebben voor deze eigenschap. De oplossing is simpel, net zoals bij gewone klassen kunnen we een constructor gebruiken om waardes te geven aan een instantie van een enum. Er werd eerder in dit hoofdstuk gezegd dat we geen instanties konden maken met een constructor. Dit is niet helemaal correct, het is namelijk wel mogelijk om dit te doen, maar enkel binnen de enum zelf. Een constructor toevoegen, doe je net zoals bij een gewone klasse. Je maakt een nieuwe methode aan met de naam van de enum.
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
int monthOfYear;
Month(){
}
}
Je moet geen access modifier vermelden bij de constructor van een enum, dit is omdat de enige die je mag gebruiken private is (private omdat je de constructor enkel binnen de enum mag gebruiken) en als je het zou vermelden het daardoor eigenlijk overbodig is. Nu hebben we echter nog steeds niet de eigenschap monthOfYear geïnitialiseerd, dit kun je net als bij andere constructors als volgt doen:
Month(int monthOfYear){
this.monthOfYear = monthOfYear;
}
De constructor heeft als parameter monthOfYear die we via het sleutelwoord this kunnen kopiëren naar de eigenschap monthOfYear van de enum. Maar we zijn er nog steeds niet, we hebben nog steeds nergens een waarde gegeven om te zeggen welke maand van het jaar een bepaalde maand is. Als je een IDE gebruikt zul je nu een error krijgen bij de opsomming van de verschillende maanden of krijg je een error bij het compileren van de java code. Wellicht krijg je dan iets in de aard van:
Error:(4, 5) constructor Month in enum Main.Month cannot be applied to given types; required: int found: no arguments reason: actual and formal argument lists differ in length
We moeten dus bij iedere maand een argument meegeven waar we zeggen welke maand van het jaar iedere maand is.
public enum Month {
JANUARY(1), FEBRUARY(2), MARCH(3), APRIL(4), MAY(5), JUNE(6), JULY(7), AUGUST(8), SEPTEMBER(9), OCTOBER(10), NOVEMBER(11), DECEMBER(12);
int monthOfYear;
Month(int monthOfYear) {
this.monthOfYear = monthOfYear;
}
}
Telkens wanneer de enum wordt aangemaakt, wordt de constructor opgeroepen om een instantie te maken van elke maand met de juiste waarde voor de eigenschap monthOfYear. Je kunt net zoals bij gewone klassen ook meerdere constructors definiëren. We kunnen dan ook nu de eigenschap correct gebruiken.
public class Main {
public enum Month {
JANUARY(1), FEBRUARY(2), MARCH(3), APRIL(4), MAY(5), JUNE(6), JULY(7), AUGUST(8), SEPTEMBER(9), OCTOBER(10), NOVEMBER(11), DECEMBER(12);
int monthOfYear;
Month(int monthOfYear) {
this.monthOfYear = monthOfYear;
}
}
public static void main(String[] args) {
System.out.println(Month.APRIL.monthOfYear); // Geeft als output 4
}
}
Ingebouwde methodes
bewerkenNet zoals gewone klassen hebben enums ingebouwde methodes. In dit stuk gaan we er een aantal overlopen. Je moet wel een onderscheid maken tussen de methodes van de enum zelf en één van de instanties van de enum, die zijn namelijk verschillend.
Een instantie van de enum heeft onder andere:
- name(): Geeft de naam van de instantie, bijvoorbeeld voor Month.MAY.name() wordt dat "MAY".
- equals(): Hiermee kun je twee instanties van een enum met elkaar vergelijken, net zoals gewone objecten.
- ordinal(): Kun je vergelijken met de index van array. Het is af te raden om deze methode te gebruiken.
- toString(): Geeft net zoals name() de naam van de instantie.
Het is niet mogelijk om deze methodes te overschrijven zoals bij klassen, behalve toString() maar dat wordt ook sterk afgeraden.
De enumklasse zelf heeft onder andere:
- values(): Deze geeft een array met al de mogelijke instanties van de enum in kwestie, handig voor te itereren met een lus.
- valueOf(): Geeft een instantie terug van een enum op basis van een meegeleverde String. De String wordt dan vergeleken met naam van de mogelijke instanties van de enum en de passende instantie wordt terug gegeven. Bijvoorbeeld: Month.valueOf("MAY") geeft de instantie MAY terug van de enum MONTH.
Eigen methodes
bewerkenJe kunt ook eigen methodes schrijven voor een enum. Hier moet een onderscheid gemaakt worden tussen methodes die gelinkt zijn aan de enumklasse zelf zoals Month.values() of die gelinkt zijn aan een instantie van een enum zoals Month.APRIL.name(). De onderscheid tussen de twee in de code wordt gemaakt door het sleutelwoord static.
public static void methodA() {} // Kan opgeroepen worden door Enum.MethodA()
public void methodB() {} // Kan opgeroepen worden door Enum.Instantie.MethodB()
Statische methodes
bewerkenAls voorbeeld van een statische methode gaan we een methode aanmaken aanmaken die de maanden van het jaar terug geeft die 31 dagen lang zijn.
public enum Month {
JANUARY(1), FEBRUARY(2), MARCH(3), APRIL(4), MAY(5), JUNE(6), JULY(7), AUGUST(8), SEPTEMBER(9), OCTOBER(10), NOVEMBER(11), DECEMBER(12);
int monthOfYear;
Month(int monthOfYear) {
this.monthOfYear = monthOfYear;
}
public static Month[] getMonthsWith31Days(){
return new Month[] {JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER};
}
}
Je kunt nu deze methode gebruiken als volgt Month.getMonthsWith31Days() en krijg je dus een array terug met de maanden van het jaar die 31 dagen lang zijn.
Instantie methodes
bewerkenJe kunt dus ook methodes hebben voor de instanties van de enum, dit doe je door het sleutelwoord weg te laten. In onderstaand voorbeeld, hebben we een extra eigenschap toegevoegd aan de enum voor het aantal dagen in de (juliaanse) maand en getters geplaatst om de waarde van de eigenschappen monthOfYear en daysInMonth op te roepen.
public class Main {
public enum Month {
JANUARY(1, 30), FEBRUARY(2,28), MARCH(3,31), APRIL(4, 30), MAY(5, 31), JUNE(6, 30), JULY(7, 31), AUGUST(8, 31), SEPTEMBER(9, 30), OCTOBER(10, 31), NOVEMBER(11, 30), DECEMBER(12, 31);
private int monthOfYear;
private int daysInMonth;
Month(int monthOfYear, int daysInMonth) {
this.monthOfYear = monthOfYear;
this.daysInMonth = daysInMonth;
}
public int getMonthOfYear() {
return monthOfYear;
}
public int getDaysInMonth() {
return daysInMonth;
}
}
public static void main(String[] args) {
System.out.println(Month.APRIL.getDaysInMonth()); // Geeft als output 30
}
}
Gebruiken met de switch
bewerkenJe kunt enums ook gebruiken met een switch, laten we daarvoor een nieuwe enum maken met als naam Compass die de vier windrichtingen bevat.
public class Main {
public enum Compass {
NORTH, EAST, SOUTH, WEST;
}
public static void main(String[] args) {
// Hier zetten we zo dadelijk een switch.
}
}
We willen de omgekeerde windrichting krijgen als uitvoer, bijvoorbeeld NORTH moet als output geven SOUTH, EAST geeft als output WEST etc. We kunnen daarvoor de switch gebruiken zoals hieronder.
public class Main {
public enum Compass {
NORTH, EAST, SOUTH, WEST;
}
public static void main(String[] args) {
//Een switch om de tegenovergestelde windrichting te krijgen.
switch (Compass.NORTH) { // North kun je hier veranderen in een andere enum-instantie.
case NORTH:
System.out.println(Compass.SOUTH);
break;
case EAST:
System.out.println(Compass.WEST);
break;
case SOUTH:
System.out.println(Compass.NORTH);
break;
case WEST:
System.out.println(Compass.EAST);
break;
}
// Geeft als output "SOUTH".
}
}
In de switch vermeld je dan bijvoorbeeld een variable die een de instantie van een enum bevat of kun je zoals hier de enum vermelden met een instantie (Compass.NORTH). Omdat je een enum gebruikt moet je bij case enkel de instantienamen van de enum vermelden. Echter zou het misschien beter te zijn om de switch te gebruiken in een methode van de enum aangezien dit logischerwijs bij elkaar hoort. Zoals volgt:
public class Main {
public enum Compass {
NORTH, EAST, SOUTH, WEST;
public Compass getOppositeDirection() {
Compass output = null;
switch (this) {
case NORTH:
output = SOUTH;
break;
case EAST:
output = WEST;
break;
case SOUTH:
output = NORTH;
break;
case WEST:
output = EAST;
}
return output;
}
}
public static void main(String[] args) {
System.out.println(Compass.NORTH.getOppositeDirection()); // Geeft als output "SOUTH".
}
}
We kunnen in de switch gebruik maken van het sleutelwoord this om dan met de switch de tegenovergestelde windrichting te krijgen. Maar hiermee wordt ook misschien duidelijk waarom het over het algemeen af te raden is om de switch te gebruiken, het is moeilijk uitbreidbaar. Stel dat we de enum aanpassen om nog meer windrichtingen te bevatten, bijvoorbeeld NORTHEAST voor het noordoosten. Wat gaat de switch dan doen?
public class Main {
public enum Compass {
NORTH, EAST, SOUTH, WEST, NORTHEAST, SOUTHEAST, SOUTHWEST, NORTHWEST;
public Compass getOppositeDirection() {
Compass output = null;
switch (this) {
case NORTH:
output = SOUTH;
break;
case EAST:
output = WEST;
break;
case SOUTH:
output = NORTH;
break;
case WEST:
output = EAST;
}
return output;
}
}
public static void main(String[] args) {
System.out.println(Compass.NORTHEAST.getOppositeDirection()); // Geeft null terug.
}
}
Als we blindelings windrichtingen toevoegen aan de enum, dan kan dit rampzalige gevolgen hebben als we de switch niet aanpassen. De switch zal hier een null geven voor alle windrichtingen die het niet verwerkt, wat voor fouten kan zorgen in de rest van je programma code die geen nulls verwachten. Dan zijn er ook nog de mogelijk andere switchen die de enum gebruiken.
Overerving
bewerkenEen enum kan niet overerven van een klasse of een andere enum, dit komt omdat een enum al overerft van Enum<E>. Enum<E> is dan ook de klasse waarvan een enum zijn ingebouwde functies heeft gekregen. Maar een enum kan wel binnen zichzelf overerven en daarmee ook overriden. Neem de code van het vorige stuk waarbij we tegenovergestelde windrichting wouden weten.
public enum Compass {
NORTH, EAST, SOUTH, WEST;
public abstract Compass getOppositeDirection();
}
We maakten van de methode getOppositeDirection() een abstracte methode. Dit geeft natuurlijk wel meteen het probleem dat een abstracte methode ergens geïmplementeerd moet worden. Dit kun je doen ter hoogte van iedere instantie van de enum zoals volgt.
public enum Compass {
NORTH {
@Override
public Compass getOppositeDirection() {
return SOUTH;
}
},
EAST {
@Override
public Compass getOppositeDirection() {
return WEST;
}
},
SOUTH {
@Override
public Compass getOppositeDirection() {
return NORTH;
}
},
WEST {
@Override
public Compass getOppositeDirection() {
return EAST;
}
};
public abstract Compass getOppositeDirection();
}
Als je dan nu Compass.NORTH.getOppositeDirection() gebruikt zul je SOUTH terug krijgen. Je bent niet verplicht om de methode abstract te maken om ze daarna te kunnen overriden. Je kunt ieder methode die niet final is overriden. Maar met abstract verplicht je wel een ontwikkelaar om een implementatie te doen van die methode telkens er instantie bij de enum wordt toegevoegd.
Het is ook mogelijk om interfaces te laten implementeren.
public interface Degrees {
public int getDegrees();
}
public enum Compass implements Degrees {
NORTH {
@Override
public int getDegrees() {
return 0;
}
},
EAST {
@Override
public int getDegrees() {
return 90;
}
},
SOUTH {
@Override
public int getDegrees() {
return 180;
}
},
WEST {
@Override
public int getDegrees() {
return 270;
}
};
}