Advanced Programming Topics Course Material
You might wonder why strings have been made stateless. Clearly creating new strings consumes way more memory! A stateful implementation would be much more memory efficient… right?
Consider the following code:
// Java
class Person
{
private String name;
public Person(String name)
{
setName(name);
}
public String getName()
{
return name;
}
public void setName(String name)
{
if ( name == null || this.name.length == 0 )
{
throw new IllegalArgumentException();
}
this.name = name;
}
}
A Person
has a name
which must not be empty, as enforced by setName
.
It is therefore Person
’s responsibility to “protect” name
to ensure it stays valid.
The code above is only correct if strings are immutable.
Imagine that String
had a clear()
method that would
set the String
object to ""
, we could then write
Person person = new Person("Sophie");
person.getName().clear();
Due to the fact that getName()
gives the caller direct access to the Person
’s name
,
he would be able to change it, Person
being none the wiser. The only
safeguard against this would be to copy the string. Let’s pretend String
offers
a copy()
method, so that we can correct our code:
// Java
class Person
{
private String name;
public Person(String name)
{
setName(name);
}
public String getName()
{
- return name;
+ return name.copy();
}
public void setName(String name)
{
if ( name == null || this.name.length == 0 )
{
throw new IllegalArgumentException();
}
this.name = name;
}
}
This, however, is not enough to keep the Person
’s name
safe:
String name = "Kevin";
Person person = new Person(name);
name.clear();
We need to perform some more copies:
// Java
class Person
{
private String name;
public Person(String name)
{
setName(name);
}
public String getName()
{
return name.copy();
}
public void setName(String name)
{
if ( name == null || this.name.length == 0 )
{
throw new IllegalArgumentException();
}
- this.name = name;
+ this.name = name.copy();
}
}
It might seem that as long as you don’t act as if you want to break things on purpose, everything will be fine. However, this is a naive mindset: we can assure you it’s all too easy to accidentally make a mistake, especially if you come back to your code after a couple of weeks or months. Before you know it, two unrelated parts of your codebase share the same object. As soon as one part modifies this object, it would make the other part misbehave. This kind of bug is infuriatingly hard to find. (For this reason, debuggers often allow you to tag objects with an “identity”, so that you can see if the same object appears at multiple locations.)
Now that we’ve rewritten Person
so as to make copies of name
everywhere,
surely there is no way to surreptitiously change the Person
’s name to
an invalid value? Sorry to disappoint you…
String name = "Martin";
new Thread(() -> { name.clear() }).start();
Person person = new Person(name);
If the timing is exactly right, it is possible that name
is cleared
between the moment it is checked and the moment it is copied.
Run the code in samples/person-race-condition
to see it in action.
We can fix this as follows:
// Java
class Person
{
private String name;
public Person(String name)
{
setName(name);
}
public String getName()
{
return name.copy();
}
public void setName(String name)
{
+ name = name.copy();
if ( name == null || this.name.length == 0 )
{
throw new IllegalArgumentException();
}
- this.name = name.copy();
+ this.name = name;
}
}
You might think this is a bit far fetched and that the user is clearly asking for trouble,
but keep in mind that in some situations, Person
could be a security sensitive class
and that the user could be maliciously attempting to subvert the system’s integrity.
The above examples should convince you (at least a little bit) that immutable strings can simplify your life: