Zoeken…


Opmerkingen

Onveranderlijke objecten hebben een vaste status (geen setters), dus alle status moet bekend zijn tijdens het maken van objecten.

Hoewel technisch niet vereist, is het best om alle velden final . Dit zal de onveranderlijke klasse thread-safe maken (zie Java Concurrency in Practice, 3.4.1).

De voorbeelden tonen verschillende patronen die hierbij kunnen helpen.

Een onveranderlijke versie van een type maken met defensief kopiëren.

Sommige basistypen en klassen in Java zijn fundamenteel veranderlijk. Alle arraytypen kunnen bijvoorbeeld worden gewijzigd, evenals klassen zoals java.util.Data . Dit kan lastig zijn in situaties waarin een onveranderlijk type verplicht is.

Een manier om hiermee om te gaan, is een onveranderlijke wrapper voor het mutable type te maken. Hier is een eenvoudige wrapper voor een reeks gehele getallen

public class ImmutableIntArray {
    private final int[] array;
    
    public ImmutableIntArray(int[] array) {
        this.array = array.clone();
    }

    public int[] getValue() {
        return this.clone();
    }
}

Deze klasse werkt met defensief kopiëren om de veranderlijke status (de int[] ) te isoleren van elke code die deze muteert:

  • De constructor gebruikt clone() om een afzonderlijke kopie van de parameterarray te maken. Als de aanroeper van de constructor vervolgens de parameterarray zou wijzigen, heeft dit geen invloed op de status van de ImmutableIntArray .

  • De methode getValue() gebruikt ook clone() om de array te maken die wordt geretourneerd. Als de beller de resultaatmatrix zou wijzigen, heeft dit geen invloed op de status van de ImmutableIntArray .

We kunnen ook methoden toevoegen aan ImmutableIntArray om alleen- ImmutableIntArray op de ingepakte array uit te voeren; haal bijvoorbeeld de lengte op, haal de waarde op bij een bepaalde index, enzovoort.

Merk op dat een onveranderlijk wikkeltype dat op deze manier is geïmplementeerd, niet compatibel is met het oorspronkelijke type. Je kunt de eerste niet eenvoudigweg vervangen door de laatste.

Het recept voor een onveranderlijke klasse

Een onveranderlijk object is een object waarvan de status niet kan worden gewijzigd. Een onveranderlijke klasse is een klasse waarvan de instanties onveranderlijk zijn door ontwerp en implementatie. De Java-klasse die meestal wordt gepresenteerd als een voorbeeld van onveranderlijkheid is java.lang.String .

Het volgende is een stereotiep voorbeeld:

public final class Person {
    private final String name;
    private final String ssn;     // (SSN == social security number)

    public Person(String name, String ssn) {
        this.name = name;
        this.ssn = ssn;
    }

    public String getName() {
        return name;
    }
   
    public String getSSN() {
        return ssn;
    }
}

Een variatie hierop is om de constructor als private te verklaren en in plaats daarvan een public static fabrieksmethode te bieden.


Het standaardrecept voor een onveranderlijke klasse is als volgt:

  • Alle eigenschappen moeten worden ingesteld in de constructor (s) of fabrieksmethode (n).
  • Er zouden geen setters moeten zijn.
  • Als het noodzakelijk is om setters op te nemen om redenen van interface-compatibiliteit, moeten ze niets doen of een uitzondering maken.
  • Alle eigenschappen moeten als private en final worden verklaard.
  • Voor alle eigenschappen die verwijzen naar muteerbare typen:
    • de eigenschap moet worden geïnitialiseerd met een diepe kopie van de waarde die wordt doorgegeven via de constructor, en
    • de getter van de eigenschap moet een diepe kopie van de eigenschapswaarde retourneren.
  • De klasse moet als final worden verklaard om te voorkomen dat iemand een veranderlijke subklasse van een onveranderlijke klasse maakt.

Een paar andere dingen om op te merken:

  • De onveranderlijkheid verhindert niet dat een object nul kan worden gesteld; bijv. null kan worden toegewezen aan een String variabele.
  • Als onveranderlijke klasseneigenschappen als final worden verklaard, zijn instanties inherent thread-safe. Dit maakt onveranderlijke klassen een goede bouwsteen voor het implementeren van multi-threaded applicaties.

Typische ontwerpfouten die voorkomen dat een klasse onveranderlijk is

Enkele setters gebruiken, zonder alle benodigde eigenschappen in de constructor (s) in te stellen

public final class Person { // example of a bad immutability
    private final String name;
    private final String surname;
    public Person(String name) {
        this.name = name;
      }
    public String getName() { return name;}
    public String getSurname() { return surname;}
    public void setSurname(String surname) { this.surname = surname); }
}

Het is gemakkelijk om aan te tonen dat de Person klasse niet onveranderlijk is:

Person person = new Person("Joe");
person.setSurname("Average"); // NOT OK, change surname field after creation

Om het probleem te verhelpen, verwijdert u eenvoudig setSurname() en setSurname() u de constructor als volgt:

public Person(String name, String surname) {
    this.name = name;
    this.surname = surname;
  }

Instantievariabelen niet markeren als privé en definitief

Bekijk de volgende les:

public final class Person {
    public String name;
    public Person(String name) {
        this.name = name;
     }
    public String getName() {
        return name;
    }
    
}

Het volgende fragment laat zien dat de bovenstaande klasse niet onveranderlijk is:

Person person = new Person("Average Joe");
person.name = "Magic Mike"; // not OK, new name for person after creation

Om dit te verhelpen, markeert u eenvoudig naameigenschap als private en final .


Het blootstellen van een veranderlijk object van de klasse in een getter

Bekijk de volgende les:

import java.util.List;
import java.util.ArrayList;
public final class Names {
    private final List<String> names;
    public Names(List<String> names) {
        this.names = new ArrayList<String>(names);
    }
    public List<String> getNames() {
        return names;
    }
    public int size() {
        return names.size();
    }
}

Names klasse Names lijkt op het eerste gezicht onveranderlijk, maar dat is niet zoals de volgende code laat zien:

List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
Names names = new Names(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList = names.getNames();
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"

Dit gebeurde omdat een wijziging in de referentielijst die wordt geretourneerd door getNames() de feitelijke lijst met Names kan wijzigen.

Om dit op te lossen, vermijdt u eenvoudigweg verwijzingen naar de veranderlijke objecten van de verwijzende klasse, hetzij door defensieve kopieën te maken, als volgt:

public List<String> getNames() {
   return new ArrayList<String>(this.names); // copies elements
}

of door getters zo te ontwerpen dat alleen andere onveranderlijke objecten en primitieven worden geretourneerd, als volgt:

public String getName(int index) {
    return names.get(index);
}
public int size() {
    return names.size();
}

Injecterende constructor met object (en) die kunnen worden gewijzigd buiten de onveranderlijke klasse

Dit is een variatie op de vorige fout. Bekijk de volgende les:

import java.util.List;
public final class NewNames {
    private final List<String> names;
    public Names(List<String> names) {
        this.names = names;
    }
    public String getName(int index) {
        return names.get(index);
    }
    public int size() {
        return names.size();
    }
}

Zoals de Names klasse eerder, lijkt ook de NewNames klasse op het eerste gezicht onveranderlijk, maar dat is het niet, in feite bewijst het volgende fragment het tegendeel:

List<String> namesList = new ArrayList<String>();
namesList.add("Average Joe");
NewNames names = new NewNames(namesList);
System.out.println(names.size()); // 1, only containing "Average Joe"
namesList.add("Magic Mike");
System.out.println(names.size()); // 2, NOT OK, now names also contains "Magic Mike"

Om dit op te lossen, zoals in de vorige fout, maakt u eenvoudig defensieve kopieën van het object zonder het direct toe te wijzen aan de onveranderlijke klasse, dwz de constructor kan als volgt worden gewijzigd:

    public Names(List<String> names) {
        this.names = new ArrayList<String>(names);
    }

Laat de methoden van de klasse overschrijven

Bekijk de volgende les:

public class Person {
    private final String name;
    public Person(String name) {
        this.name = name;
      }
    public String getName() { return name;}
}

Person class lijkt op het eerste gezicht onveranderlijk, maar stel dat een nieuwe subklasse van Person is gedefinieerd:

public class MutablePerson extends Person {
    private String newName;
    public MutablePerson(String name) {
        super(name);            
    }
    @Override
    public String getName() {
        return newName;
    }
    public void setName(String name) {
        newName = name;
    }
}

nu kan Person (on) mutabiliteit worden misbruikt door polymorfisme met behulp van de nieuwe subklasse:

Person person = new MutablePerson("Average Joe");
System.out.println(person.getName()); prints Average Joe
person.setName("Magic Mike"); // NOT OK, person has now a new name!
System.out.println(person.getName()); // prints Magic Mike    

Om dit te verhelpen, ofwel markeren de klas als final , zodat het niet kan worden verlengd of verklaren al zijn aannemer (s) als private .



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow