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.
hashCode()
andisAbsolute()
are true methods on the object, and they don't mutate any state. Methods likelength()
,exists()
anddelete()
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.