‘Mijn object is immutable, want alles is final en ik heb geen Setters!’

In deze blog ga ik in op de bovenstaande stelling. Is het namelijk wel zo dat een object immutable is wanneer alles final is en er geen Setters zijn gedefinieerd? Het klinkt in ieder geval erg logisch. Tot je er mee gaat werken en je situaties tegen komt waarbij dat absoluut niet zo is! Erg vervelend wanneer je verwacht dat je object immutable is en deze dat niet blijkt te zijn…

Ik zal in deze blog ingaan op de volgende vragen:

  1. Wat is een immutable object?
  2. Waarom klopt de stelling niet?
  3. Hoe maak je een object dan immutable?

Wat is een immutable object?

Een immutable object is een object waarvan, zodra deze is aangemaakt, de state niet meer gewijzigd kan worden. Effectief gezien betekend dit dat zodra je een immutable object hebt aangemaakt het niet meer mogelijk is om er iets aan te wijzigen.

Neem het volgende voorbeeld:

public final class Hond
{
    private final String naam;
    private final int leeftijd;
    
    public Hond(String naam, int leeftijd)
    {
        this.naam = naam;
        this.leeftijd = leeftijd;
    }
    
    public String getNaam()
    {
        return naam;
    }
    
    public int getLeeftijd()
    {
        return leeftijd;
    }
}

Dit is een voorbeeld van een immutable object. Zodra er een instantie van Hond gemaakt wordt, is het niet meer mogelijk om er iets aan te wijzigen. Er zijn geen Setters en alle velden (en in dit geval ook de class) zijn final.

Een van de voordelen van een immutable object is dat een instantie veilig doorgegeven kan worden zonder dat je bang hoeft te zijn dat iemand er iets aan wijzigt. Effectief gezien is een immutable object dus ook thread-safe.

Klik hier en hier voor meer informatie over een immutable object.

Waarom klopt de stelling niet?

Wanneer je alleen kijkt naar het bovenstaande voorbeeld dan zou je kunnen denken dat de stelling best wel eens zou kunnen kloppen. Het object is immutable, omdat alles gedefinieerd is als final en omdat er geen Setters aanwezig zijn. Het is dus niet mogelijk om er iets aan te wijzigen. In dit voorbeeld klopt deze stelling inderdaad. Maar wat nu als we er iets aan gaan wijzigen:

import java.util.Date;

public final class Hond
{
    private final String naam;
    private final Date geboortedatum;
    
    public Hond(String naam, Date geboortedatum)
    {
        this.naam = naam;
        this.geboortedatum = geboortedatum;
    }
    
    public String getNaam()
    {
        return naam;
    }
    
    public Date getGeboortedatum() 
    {
        return geboortedatum;
    }
}

We hebben het object Hond voorzien van een geboortedatum i.p.v. een leeftijd. Ook deze is final en er is geen Setter gedefinieerd. Is Hond nu nog immutable? Neem de volgende code:

public static void main(String[] args)
{
    Hond hond = new Hond("Laika", new Date());
    System.out.println(hond.getGeboortedatum());
    Date date = hond.getGeboortedatum();
    date.setTime(100l);
    System.out.println(hond.getGeboortedatum());
}

De eerste System.out.println print netjes de geboortedatum uit, maar wat print de 2e uit? Komt de code wel bij de 2e of wordt er een error opgegooid bij het commando date.setTime(100l);? Laten we dit eens gaan analyseren.

Wat geef je in Java nu precies aan met final? Dat het object zelf niet gewijzigd mag worden of alleen de referentie naar dat object?

Bij Java (en ook bij andere programmeertalen) is het zo dat je alleen de referentie naar het object final maakt. Dat wil dus zeggen dat zodra je via een Getter het object ophaalt je de referentie naar hetzelfde object terug krijgt. Het maakt vanaf dat moment niet meer uit of je de referentie final gemaakt hebt of niet, want je kunt gewoon de methode setTime() van Date aanroepen en de tijd wordt gewijzigd. En omdat je alleen de referentie naar het object hebt, wordt dit ook gewijzigd in je instantie van Hond. De 2e System.out.println print dus de gewijzigde geboortedatum uit. Al met al betekend dit dus dat Hond nu niet meer immutable is!

Hoe maak je een object dan immutable?

Om Hond nu goed immutable te maken, moeten we eerst bepalen wat we terug willen geven. Willen we het volledige Date object terug geven of alleen maar de datum in de vorm van een String?

De laatste optie is veruit de makkelijkste, maar geeft de minste vrijheid voor de gebruiker van Hond. Wanneer deze bijvoorbeeld alleen maar het geboortejaar wil gebruiken dan zal hij of zij middels een substring of iets dergelijks het geboortejaar eruit moeten halen. Aan de andere kant maak je als ontwikkelaar wel meer duidelijk over het gebruik van je immutabel object.

Wanneer je het volledige Date object terug wilt geven, dan zul je , aangezien Java de referentie terug geeft, ervoor moeten zorgen dat hij in plaats daarvan een kopie van het object terug geeft. Dit kan bijvoorbeeld op de volgende manier:

public Date getGeboortedatum() 
{
    return new Date(geboortedatum.getTime());
}

Nu maken we eerst een nieuw Date object aan die dezelfde tijd krijgt. Vervolgens geven we deze terug (dit wordt ook wel defensive copy genoemd). De gebruiker heeft nu een ander Date object terug gekregen en kan deze wel wijzigen, maar dat zal geen effect hebben binnen Hond. Hond is nu weer wel immutable.

Conclusie

De stelling dat een object immutable is wanneer alle velden final zijn en geen Setters bevatten klopt dus niet altijd. Je zult er dus rekening mee moeten houden dat objecten waarnaar je refereert, niet per definitie immutable zijn.

Het is mogelijk om dit af te vangen middels defensive copy of door alleen dat terug te geven wat jij terug wil geven (bijvoorbeeld een zinnige(!) String representatie).