10

The most popular answer is - it is an object whose state does not change after creation.

What does it actually mean?

My understanding is that any method call on the object should give the same result. So, it makes me wonder could an immutable object refer to mutable data? For example, Is an immutable object referring to a random number generator still immutable?

I sometimes stumble on examples like java.io.File which says "Instances of the File class are immutable; that is, once created, the abstract pathname represented by a File object will never change". However, this object refers to a file system (via global variables internally) and there is for example length() method which returns the current size of a referred file, and obviously could change during the lifetime of a File object.

Is it just terminology wars or some inconsistency in the immutability notion? Is there any well-defined theory around this?

There are few Java examples of objects which I could not decide if they are really immutable or not.

Example 1

class E1 {
   private final char[] data;
   public E1(char[] data) { this.data = data; }
   public char[] getData() {return data;}
}
// The class is kind of immutable,
// as it always returns reference to the same array
// and it could not changed
char data[] = new char[] {'A'};
var e = new E1(data);
var result1 = e.getData();
data[0] = 'B';
var result2 = e.getData();
assert result1 == result2; // method result never changes.
// It's thread-safe, and seems ticks all the boxes needed for immutability

Is it immutable or not?

Example 2

class E2 {
   private final char[] data;
   public E2(char[] data) { this.data = data; }
   public char[] getData() {return data;}
   // we just added an extra method...
   public char getSome() {return data[0];}
}
// and now we could do the following
char data[] = new char[] {'A'};
var e = new E2(data);
var result1 = e.getSome();
data[0] = 'B';
var result2 = e.getSome();
assert result1 != result2; // oh, now it's changing
// and it's not even thread safe anymore

Is it immutable or not?

Example 3

static int globalVar = 0;
...
class E3 {
   private final int value;
   public E3(int value) { this.value = value; }
   public int getSome() { return value + globalVar++; }
}

// no state changing in the class at all, so nothing could be changed?
// but it is not even thread-safe!

Is it immutable or not?

Example 4

class E4 {
   private final AtomicInt counter = new AtomicInt();
   public int getSome() {return counter.incrementAndGet();}
}

// counter reference is not possible to change, and it is even thread-safe
// as it's usual true for immutable objects... but?..

Is it immutable or not?

21
  • If the array is modified inside the object, the pointer to the start of the array has not been changed. It follows the same basic philosophy as pointers to another object. Commented May 18, 2023 at 21:55
  • 10
    java.io.File is somewhat of a misnomer; it doesn't represent/encapsulate a file, but a path to a file. The docs describe it as "An abstract representation of file and directory pathnames". It's immutable cause you can't use any of the methods on it to change that path. Methods like hashCode() and isAbsolute() are true methods on the object, and they don't mutate any state. Methods like length(), exists() and delete() are more like convenience functions that could have been static/free functions, but happen to be placed on the class itself; these also don't change the abstract path. Commented May 19, 2023 at 0:30
  • 4
    Think of immutability more like: "pointless to make copies". If you had multiple copies they would still return identical data at all times, regardless whether the returned data changes. It is more a compiler/runtime thing than something the programmer needs to be concerned with. It is good to be aware of but its purpose is to make it easier on compilers and runtimes. Commented May 19, 2023 at 6:01
  • 1
    @kan E3 is immutable because there is nothing to be mutated. It is pointless to call it that though for the same reason. Is the ocean a carpenter? Is the moon deductable? Commented May 19, 2023 at 8:28
  • 3
    Note this old Q&A here: What are the consequences of immutable classes with references to mutable classes?. The top answer says E1-E4 are not "truly immutable", but the comments below show some dispute.
    – Doc Brown
    Commented May 19, 2023 at 10:09

9 Answers 9

17

I think the core issue here is that the term "immutability" is often not a uniquely and strictly defined term in the mathematical sense. Different people can have different kind of degrees of immutability in mind, depending on the context. As one example, even Wikipedia talks about "weak vs strong immutability". So when you talk about immutability, it is best you clarify the context in which you are working. Here are some questions which might be relevant:

  • Which programming language are you using? Different language / environments provide different guarantees for immutability at the compile time and/or run time level, and different languages / environments also provide different ways to implement or circumvent those guarantees.

  • What is your goal for a certain component when designing it in an immutable fashion? Are you trying to eliminate side-effects, maybe for achieving thread safety and/or deterministic behaviour?

  • Are you solving a practical problem, or do you want to reason about certain code for making a mathematical proof?

So for example, from a practical point of view, if your are after fully deterministic and thread-safe behaviour, all your examples E1 - E4 are not "sufficiently immutable" to guarantee this. But beware, this may also depend on the specific component and the abstraction it creates:

  • when a class like E1 (or E2) is intendended to be a container for some character data, there should be no easy way to manipulate its contents from the outside, otherwise it is probably but "sufficiently immutable" for this purpose.

  • but when a class like E1 or E2 is intendended to be some kind of proxy or adapter for a mutable object, then the guarantee not to change the internal reference to a different object might be "immutable enough" for a certain purpose. The File class you mentioned is such a real-world example - though the docs call it "immutable", certain methods like canWrite() don't guarantee a deterministic result.

In short, when you design a component, it is probably best to state the degree of immutability in the documentation and/or APIs contract of the component. That should help to avoid misunderstandings.

Some additional references:

4
  • 3
    So... that's kind of reduces term "immutability" to be very context specific and not easy to use. In other words, if I read a documentation of a particular component and it says "X is immutable" - it does not tell much, as it may mean different things and I should still investigate further to understand what is ok and what is not ok to do with the X. That's kind of disappointing, but I guess it's how our reality looks like.
    – kan
    Commented May 19, 2023 at 11:51
  • 7
    @kan: yes, that is my point. However, I guess in most sanely designed libraries or frameworks, when you encounter a new component which is described as being immutable, finding out what that means should not be too hard. The Java File class is a good example, it clearly states immutability for the file name string which it holds, nothing more, nothing less - quite easy to understand.
    – Doc Brown
    Commented May 19, 2023 at 13:10
  • Shallow vs deep immutabilty, maybe?
    – Pablo H
    Commented May 19, 2023 at 13:57
  • @PabloH: there is nothing wrong in introducing such terms when you define clearly what you mean by them. Just don't expect the rest of the world to understand what you mean when you forget to provide such a definition.
    – Doc Brown
    Commented May 22, 2023 at 11:35
8

I've always thought of immutability in terms of the assignment operator: =. If all instance variables are marked final (or readonly, or const — whichever word the language uses) then they can only be assigned a value once. You should only see one x = y expression executed during the lifetime of that object. Note that you can still see multiple assignments to an instance variable, but only when they occur in different constructors. So, at a mechanical level, you can only execute x = y once for each instance variable.

Things can get murky when objects refer to mutable data. The examples in your question illustrate this very well. Each example deserves a "mechanical" description of immutability and a "conceptual" description.

The File class

The concept of mutability does not prohibit change in the rest of the world. Every File object is immutable because the file path cannot be changed — the file path is the only piece of information the object holds in memory. The data in the file exists in storage, not RAM. The File object is an abstract representation of a file, not the file itself.

As a result, the file could change, because someone overwrites the file, or appends to it. Nothing about the File object changes. The path name in its instance variable has not changed. In fact, someone could delete the file, but still, the file path in the File object does not change.

At a mechanical level, the File object is immutable. When we extend the concept of a "file" to represent the data contained in that file, then a File object could be treated as a mutable object. In abstract terms, we treat a File object as if it were the actual file, and since another process or thread could change that file, the concept of a "file" is mutable.

So, a File object can be mutable if your perspective is the file in storage, or immutable if your perspective is the data representing the file path that the File object holds in an instance variable.

Examples 1 and 2: passing an array to an object

This was an interesting example. At first glance, it appears to defy immutability the moment an object is constructed:

char data[] = new char[] {'A'};
var e = new E1(data);

data[0] = 'B'; // <-- changes the first char for 'data' and E1

You must understand how Java implements arrays before you can understand how this example is immutable at a mechanical level. Arrays in Java are objects passed by reference, not by value. Your second example is immutable, because the instance variable in E1 is only assigned to once during the lifetime of an E1 object. The instance variable in E1 is not an array, it is a pointer to an array. The pointer never changes. The mental leap we need to make is the pointer cannot be changed to point to a different array of chars, and is therefore immutable, but the array itself is mutable.

Here, again, we encounter a difference in how the programming language enforces immutability, and how the human applies the concept of immutability to the object. E1 is immutable, but the array is not. There are code paths allowing you to bypass E1 to mutate the array.

At this point, I would argue that you've broken encapsulation and data hiding with examples 1 and 2 because code outside is modifying the array passed to E1. This is the real reason why immutability is hard to enforce in those examples. Example 2 is just a slight restatement of example 1, and the same rules apply.

Example 3: using global state

The value instance variable is only ever assigned to once. This class is immutable in the truest sense of the word. The getSome() method returns the sum of the instance variable and some global variable. The concepts of immutability and determinism are different, and example 3 illustrates this perfectly. You can have an immutable object with individual methods that are not thread-safe or deterministic. The fact that getSome() uses the instance variable and global variable is an implementation detail hidden from consumers of the E3 object.

Immutability does not guarantee thread-safety or determinism. Each is a separate concept that can apply to different parts of the same object.

Example 4: references to mutable objects

This example is really just a restatement of example 2, except now E4 holds a reference to a mutable object instead of an array. Remember that the counter instance variable is only assigned to once in the lifetime of the E4 object, which makes the E4 object immutable. The pointer to the AtomicInt object does not change, but the internal state of the AtomicInt object can change. While E4 is immutable, the getSome() method is not deterministic. Example 4 is another good illustration of the fact immutability and determinism are different concepts that can apply to different parts of the same object.

Is E4 immutable? Yes. Should a programmer consider the abstraction that E4 provides to be immutable? That is unknown. This all depends on your perspective. From the perspective of the E4 class, it is immutable. From the perspective of code using an E4 object, the E4 object is not deterministic, and it is not immediately apparent that E4 is immutable. Often times, immutability is an implementation detail that consumers of an abstraction are unaware of.

10
  • 1
    What exactly mean data owned by a particular object? Usually when we speak about really immutable objects, like String - it has all the data encapsulated, so nothing could be changed, like it makes a defensive copy of the char-array passed into constructor, so it could not be modified externally later - but technically String does not own the array, only reference to it. Where is borderline of ownership?
    – kan
    Commented May 18, 2023 at 21:16
  • I've added some weird examples into my question to consider about "data owned" concept.
    – kan
    Commented May 18, 2023 at 21:31
  • A File object is not an abstract representation of a file. If it was, it would be a mutable file. A File object is a path and the document calls this out. It's an immutable path. Commented May 19, 2023 at 13:11
  • @DocBrown, coffee consumed, and answer updated. Commented May 19, 2023 at 13:12
  • 1
    @kan: you have to be careful with how far you take immutability. It doesn't automatically apply to all objects in a graph simply because a final keyword exists. Remember references and pointers. Be careful not to generalize the Wikipedia statement without properly thinking through the code. Commented May 19, 2023 at 21:56
4

The most popular answer is that it is an object which state does not change after creation.

Yes, it means the objects state doesn't change after it is initialized. But that doesn't guarantee its behavior.

What does it actually mean? My understanding is that any method call on the object should give the same result.

It only means that if the object methods are referentially transparent. A fancy way of saying the state of the rest of the world doesn't change what they do. If a method is only dependent on its object's instance and the arguments passed to it then it's referentially transparent. Which means it will always do/return the same thing when called the same way.

When specifically applied to functions (rather than any old expression) this referentially transparent concept is called being deterministic.

However, an immutable (keeps the same state once created) object could have a method that checks if a file exists. That's not deterministic unless you consider the file system one of the method's arguments. Implied arguments like that can make methods hard to predict and reason about.

Could immutable object refer a mutable data? Like using random number generator inside?

Yes, the file example does exactly that. So does referring to a random number generator. In either case if you want deterministic behavior (which would make the method testable) you'll want to pass in these side effects as arguments. That would let a test control the perceived state of the file system or the random seed thus making the method's behavior predictable, that is deterministic. If the method does something interesting, and needs that interesting something tested, controlling these becomes critical. So a method that parametrizes them is far easier to test.

But just being immutable doesn't guarantee anything other than that the behavior isn't changing because someone messed with the object's state. Do that, especially asynchronously, and method behavior becomes really hard to predict.

Also, objects can hold immutable references to mutable things (like other objects). Even immutable strings can refer to things, like files, that change outside the scope of the object's immutability. So understand that immutable is a shallow concept. If you want to ensure methods have deterministic behavior you have to look deeper than object instance state.

Is there any well defined theory around this?

Yes, these concepts are all borrowed from functional programming. What we OOP nerds call state they call "a closures enclosing scope".


As for Example 1. Let me show you something:

char data[] = new char[] {'A'};
var e = new E1(data);
var result1 = e.getData();
assert result1[0] == 'A'; //Pass. As expected.
data[0] = 'B';
assert result1[0] == 'B'; //Pass! What is this magic?
assert result1[0] == 'A'; //Fail! Hmm, seems mutable.

Right, that's mutable. But what mutated? It wasn't E1. E1 doesn't hold the character array. It holds a reference to it. And that reference hasn't changed. The character array is located right where it was before. Which is why both data and result1 can refer to it.

If my job is to preserve an immutable ledger of addresses to houses against changes and you put your house address in my ledger then understand, it's not my fault when you leave the oven on and burn the house down. No, my job is to preserve knowledge of where to send the fire department.

So what you've done with Example 2 hasn't changed how mutable the character array is. It's changed whether any of the class's methods depend on the mutable contents of the array. The class's state is still immutable. But now you have method that is no longer deterministic because it depends on more than that immutable state.

If you really want to protect the contents of that array from change (thread safety and such) consider making what's called a defensive copy for E1. Both when you learn about this array and when you tell others about it. That way you can have a private copy of it all to yourself. But you shouldn't trust that a defensive copy is happening just because someone tells you that E1 is immutable. That's not what that means.

To make this blisteringly clear let's consider a stateless object. Lets call it E5. It holds nothing in instance state. Pass it nothing when you construct it. That must be immutable, right? Well you can get the same non-deterministic behavior you saw in E2. Changing data[0] will change the behavior of e5.getSome(data). This is non-deterministic because the getSome signature only shows it depends on e5 and the reference data. But because of what getSome does it also depends on what data refers to.

class E5 {public char getSome(char[] data){return data[0];}}

public static void driveE5(){
    char data[] = new char[] {'A'};
    E5 e5 = new E5(); // stateless
    var result1 = e5.getSome(data);
    data[0] = 'B';
    var result2 = e5.getSome(data);
    assert result1 != result2; // oh, now it's changing
}

Touching something immutable doesn't make you immutable.

What makes a method deterministic is what it depends on. Instance state is just one of many things a method can depend on. The difficulty in figuring out what a method depends on is why I like short methods.

What I'm trying to drive home is you can do everything possible to make an object's instance state immutable and still have non-deterministic methods. You can do everything javaranch recommends to make your class strongly immutable:

  • Make all fields private.
  • Don't provide mutators.
  • Ensure that methods can't be overridden by either making the class final (Strong Immutability) or making your methods final (Weak Immutability).
  • If a field isn't primitive or immutable, make a deep clone on the way in and the way out.

And still end up with a method doing weird non-deterministic crap because it's looking at the pretty colors in your lava lamp.

As a counter example, consider an object that lets you set its state to whatever whenever, but its methods are fully deterministic because they all ignore everything that isn't their own arguments. Instance state means nothing to them. A bit useless and hard to read but perfectly possible to code.

Immutability is about what you can and can't do to an object. But that only limits what can affect its behavior. Immutability doesn't, on its own, guarantee the behavior of the object. Other things than state must be considered and excluded if you want guarantees on what this thing will do.

To round up your examples:

Example 1 Example 2 Example 3 Example 4
Immutable yes yes yes yes
Deterministic yes no no no

Why? Because every example protects instance state once created but every example except 1 has at least 1 method whose behavior depends on more than just its instance and arguments.

9
  • 1
    Still a bit confused. So, File is immutable but non-deterministic, while String is immutable and deterministic, right? Could you please explain about my four examples if each of them is immutable or not and why?
    – kan
    Commented May 18, 2023 at 22:19
  • Haskell is a different beast. It relies a lot on purity, and there is no internal state, no mutability. It's revolving around IO/State monads, and I could not see where immutability concept even could be applied.
    – kan
    Commented May 18, 2023 at 22:35
  • 1
    I think this answer does not clearly clarify the terminolgy problem of the OP. Would you call E1 (or E2 - which is essentially the same) immutable or not?
    – Doc Brown
    Commented May 19, 2023 at 9:56
  • @kan, Java's File objects are mere pointers, virtual references to the underlying file system. Bear in mind where Java is running (JVM). JVM states are decoupled from the state of the underlying system (O.S). That's why you can create instances pointing to drivers , folders or files that don't exist. It's also why you can point to existing drivers, folder and files, delete/unmount any of them and the JVM remain unaware and in a "right state".
    – Laiv
    Commented May 19, 2023 at 11:36
  • @Laiv: I think there was no need to delete your answer - just an introducing statement along the lines of "in the context of defensive programming, my preferred idea of immutability is the following ... " would have been fine.
    – Doc Brown
    Commented May 19, 2023 at 11:44
2

It depends what is the object and what isn't the object.

A Java File object is only a path, not a file, and I believe the documentation calls this out. The path cannot be changed, so it is considered immutable.

If a File object were to be considered an actual file, it would be mutable, since the file can change. You'd also run into other problems that caused by pretending that a file path is a file - like what happens when the file is renamed or doesn't exist.

I would say that in your examples 1, 2 and 4 all the data is most likely part of the object, and therefore the objects are mutable. Example 3 is contrived and could probably benefit from a redesign. But consider:

// Reference to an array stored elsewhere
public class <T> ArrayRef<T> {
    private T[] array;
    public ArrayRef(T[] array) {this.array = array;}
    public T[] getArray() {return this.array;}
}

Exactly the same code, but now it's clear the purpose of an ArrayRef is to point to some other array which we do not think is part of the ArrayRef. Even though the code is exactly the same we can call it an immutable array reference because we are only talking about the reference and not the array.

And you can also consider:

class Node {
    final Graph g;
    final int index;
    // constructor omitted for brevity
    int getWeight() {return g.nodeWeights[index];}
    void setWeight(int w) {g.nodeWeights[index] = w;}
}

Now we have a mutable object with only immutable fields!

3
  • E3 is kind of close enough to java.io.FIle, but instead of global filesystem state it has a state in a global var. Look into the implementation and you'll find private static final FileSystem FS. Not a big difference from what I could imagine.
    – kan
    Commented May 19, 2023 at 21:32
  • @kan FileSystem isn't the actual filesystem, either - just a way to access one. I couldn't see the purpose of E3. Commented May 20, 2023 at 18:09
  • FileSystem is a thin wrapper over syscalls to the file system, which is a global state. Hence both File and E3 have an immutable internal state, while accessing and manipulating some global state.
    – kan
    Commented May 21, 2023 at 13:37
2

There is no strictly defined meaning of “immutable”. Roughly it means that an object will not change after some point after its creation.

Just a few possible meanings: C++ “const” objects cannot be modified after the constructor has finished running. The exception is “mutable” fields which can always be modified. You should have the semantics that such a modification doesn’t change the meaning of an object; it might be used for caching for example. On the other hand a const object might have a pointer to a non-const std::string; modifying this is allowed for const objects but reasonably should count as modifying.

C++ objects referred to by const pointer or const reference are modifiable except not through that pointer - always a nice surprise. And once the destructor is running the object is modifiable again.

Objective-C has non-modifiable classes. There is nothing in the language enforcing this, there are just no methods to modify the object. Usually they come in pairs with a modifiable class that supports all the methods of a non-modifiable one. There are operations like reference counting that modify an object at a lower method, but they don’t count for calling an object in modifiable. “Modifiable” is a property of the class, not an instance.

In Swift, modifiable objects must be modified at least once before the first use, unmodifiable objects exactly once, and in a way that can be checked and verified by the compiler. And methods can be declared as “mutating” meaning they can’t be called for unmodifiable objects.

1

I began on a longer answer, but this is the digested version.

In object-oriented file system libraries, a File object does not represent a file object, but a file handle, or file address (i.e. a path) by which a file handle can be got.

This is because the file is owned and managed by the file system itself, and the master copy typically exists on disk and outside the realm of main memory where objects exist.

The reason the master copy never leaves disk, is because typically the contents of the file are important and mustn't be removed from the realm of durable storage. Instead, you can read copies, and (if changing the file) you must periodically submit your changes to the file system which manages the master copy.

But the programmer never gets possession of the file itself which is always safely tucked away.

From this point of view, the object File is misnamed. It ought to be named FileHandle.

It is certainly possible for the FileHandle to be an immutable object in the strict sense, but as you've fathomed, it really means nothing because the File and its contents are not immutable, and the FileHandle in the possession of your program is merely a level of indirection to the file which always resides completely outside the context of the program.

What the programmer regards as real, the file, has no direct representation in terms of the types of objects available to him.

2
  • 1
    A java.io.File is a string path. Your notion of a FileHandle would correspond to java.io.FileDescriptor, which is a specific instance of an open file, with a read/write position and other mutable attributes. You should also look at how the Windows API assigns semantics to handles.
    – Nayuki
    Commented May 20, 2023 at 4:16
  • @Nayuki, I'd be no more satisfied with Java's approach. A pure string path could more easily be named "FilePath", and when I looked up FileDescriptor, it was immediately described as a "handle", so could be more appropriately named "FileHandle". Either way, neither of the things are actually named in accordance with what many programmers naturally think of as the "file object".
    – Steve
    Commented May 20, 2023 at 6:55
0

None of these examples truly are.

  • E1 does not prevent client code from modifying the content of its internals. It only prevents shallow mutability, but that is a very weak form of immutability. The class doesn't give any meaningful protection, anyone can still do anything they like to the array. This example shows immutability is a transitive property for composition: if an object is immutable, it can only contain references to immutable data.
  • E2 has the same issue.
  • E3 tries to hide the issue with a global, but this is actually an even more obvious case of a non-immutable class: the class's behavior references global data. This is essentially the same as putting the global data inside the object and making it public. This example shows that immutability is also transitive for simply referencing outside data.
  • E4 is again referencing a non-immutable type, AtomicInt. Interestingly, atomicity only makes sense on underlying mutable types.

The reason why this is so categorical is that if you define immutability less strongly, you get an essentially useless property. Immutability is amazing because it removes the majority of the mental load associated with programming. Immutability prevents code from misbehaving with your object, meaning that you can truly trust the object.

You probably heard people talking about object invariants. That functions which modify the state of an object should check that all the invariants are respected when the function begins and finishes. With immutability, you don't even need that concept: all you need is to guarantee that the method used to create your object will always create a well-formed object.

However, all of the problems of mutability surface again if you allow even a small part of the object to change in a way that can be perceived at the boundaries of the object. And the boundaries of the object are every piece of data that can be reached from the results of its methods.

3
  • +1 I would love to see an explanation for the downvote.
    – JimmyJames
    Commented May 19, 2023 at 21:33
  • 1
    E3 is kind of close enough to java.io.File, but instead of global filesystem state it has a state in a global var. Look into the implementation and you'll find private static final FileSystem FS. Not a big difference from what I could imagine. And File is called immutable in JavaDoc...
    – kan
    Commented May 19, 2023 at 21:39
  • @kan That's because from Java's internal perspective, File really is immutable. But due to doing I/O, the return values of its methods can vary independently of its own state. In a way it is indeed similar to E3: the files on the system (or the internet, whatever) are global state. Languages that are more concerned with immutability will draw a much stricter line. Haskell for instance has IO monads which work very distinctly from the rest of the language.
    – Kafein
    Commented Jun 16, 2023 at 9:16
-1

I think it's most useful to think of a reference-type variable as being a slate (or, for younger folks, a dry-erase board) which can either be blank, or hold a notation meaning e.g. "Object #573" [the 573rd object created since the program began].

If there have been 572 objects created since a program began, and a program executes myThing = new int[100];, that will cause the system to create a 573rd object, of type int[] and length 100, and write "Object #573" into myThing. This will immutably establish, always and forevermore, that on that particular execution of the program, type of the 573rd object created is int[], and its length is 100. The contents of the 100 int slots within the array might change, and it may be possible to overwrite the contents of myThing with a reference to an array of some other size, or (depending upon the type of myThing, a reference to something else entirely), but the 573rd object created on that execution will always and forevermore be an int[] of size 100, and any object reference that exists within that program execution holds "Object #573", it will identify that array.

If MyThing is a private field of some other Object #47, and the code for that object's type sets MyThing once and has no means to set it ever again, then field MyThing of Object #47 will always hold "Object #573", identifying the aforementioned array. The value of the integers held within the array may change, but field MyThing won't.

Oftentimes, programs use references as a proxy for the contents of the objects identified thereby. If MyThing is being used to hold 100 numbers, and those numbers might change, then MyThing should be viewed as mutable. If, however, MyThing is being used to identify e.g. a place into which numbers should be stored for the benefit of some other object, or a place into which some other object will store numbers in future, then MyThing should be viewed as immutable, since it will always and forevermore identify Object #573.

As a minor note, Java references don't actually store references as object numbers, but if a program has created 574 or more objects, exactly one object will have been created between the 572nd object and the 574th object. If one had a means of knowing which object that was, kept a reference, and knew that it was the 573rd object, then given any other reference to the 573rd object, one could determine that it was in fact a reference to the 573rd object.

-1

All of your examples are mutable

I'll go through each and explain why. In general, most of these answers have been too focused on language specifics and not on the language-agnostic purpose and function of the code. Immutability is not really about whether it's possible to change some state -- only whether it's acceptable. It's similar to public/private visibility: it's often possible for an external object to directly access or modify a private field, but it requires a workaround and it's generally not acceptable. Like all programming, it's about communicating to the programmer what's going on.

Immutability is about reusing and sharing objects safely

The main reason we care about immutability is because we want to share objects with different parts of our code, and use them more than once, and still be able to predict their behavior. If I receive an object as a method parameter, I want to know that that object will behave the same regardless of when I make use of it, how many times, who else has access to it, etc.

It's about predictable and reliable behavior. If you share a mutable object with an unpredictable consumer, your object's behavior is now also unpredictable. If you share an immutable object with an unpredictable consumer, it doesn't matter. Your object is still going to behave how you expect.

In your examples E1 and E2, you are exposing a mutable data structure. You've made getData() public, meaning you've made an affirmative decision that other objects are meant to access this data directly. You chose not to copy the array first, nor to declare the return type as something immutable -- both of these changes would make E1 and E2 immutable (if you do the same to the input array, too). Nowhere have you communicated to the consumer of these classes that they must ensure not to modify the returned array ("immutability by fiat", a last resort). I don't care that they pass the test result1 == result2; there are plenty of other tests for immutability that they would fail. If two different consumers both have access to the same instance of an E1, can each change how the object behaves for the other? Yes! So those classes are mutable.

Internal State has nothing to do with it

The only thing that matters is whether the behavior is predictable or not. Your class E3 is mutable because each individual caller of getSome() changes the behavior for every other caller. That's the thing we really care about: if we share this object, does it behave predictably? No, it doesn't.

E4 is indeed thread-safe; plenty of mutable data structures are thread-safe. That doesn't make them immutable. To predict the next number it gives me, I need to know how many times anyone has called it. Its behavior is changing all the time. That doesn't mean it's bad! That would be a great class for claiming slots in a large array shared by multiple threads, for instance. Mutable doesn't mean bad.

As an example of an immutable object whose internal state changes, consider a lazy or cached object. The first time it's accessed, it does some computation or makes some request to get the value, stores it, and returns that stored value from then on. However, its public behavior is the same regardless of whether it's been cached or not: ask it for the value, and you'll get it -- the same one every time. It is immutable.

Determinism is overrated

The only thing that matters is whether the behavior is predictable or not. A nondeterministic object can be considered immutable if non-determinism is the expected behavior. For example, I would argue that a true random number generator is immutable, because its behavior never changes. Every time you ask it for a number, it gives you a random number from the same distribution. It doesn't matter how many times you've asked it, nor how many other objects are also asking it for numbers -- always the same behavior. That's immutability. Of course, if you could change the parameters of the distribution after it's created, it's no longer immutable.

Are pseudo-RNGs immutable? If you expect them to deterministically produce the same sequence from the same seed every time, then they are mutable: they are not safe to share. Another object could ask it for a number and throw off your sequence. But if they don't support seeds and nobody ever expects them to be deterministic -- if they are "pretending" to be true RNGs, then I argue they become immutable. Their behavior is defined by the unpredictability of their outputs, and nobody else can change that behavior out from under you.

7
  • 1
    E3 is mutable because each individual caller of getSome() changes the behavior for every other caller. - the same is true for e.g. java.io.File.delete() and few other methods, but it was declared as immutable anyway.
    – kan
    Commented May 20, 2023 at 9:52
  • 1
    The E1 could be easily shared, no problems, the data it returns is not its state. And in my example data is a simple array, but could be any other mutable object which has own means for thread-safety/etc. Could an immutable object share a mutable data - seems there is no consensus... Personally I'm inclined to a "weak immutability" notion.
    – kan
    Commented May 20, 2023 at 10:04
  • No, an immutable object cannot share mutable data. It's hard to imagine anyone arguing that java.io.File could possibly be immutable if it has methods like .delete(). The argument "but it's just a reference to a file" could be extended to every reference to anything, and then literally nothing is mutable. Conceptually, you are working with a file, which is mutable. Commented May 21, 2023 at 23:44
  • That's the thing. There is no "official" immutable meaning. Someone could call this kind as weak immutability - object's internal state is immutable, but external state is not the object's.
    – kan
    Commented May 23, 2023 at 9:12
  • @kan "external state is not the object's" -- you're "thinking in Java" and worrying about the specifics of how it's implemented inside the object. The best software engineering principles have nothing to do with objects or lines of code or language specifics; they're logical. Forget the lines of code: what does it do; what is it for? Can its functionality change while you're holding it? Then it's mutable! Commented May 24, 2023 at 17:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.