Java Immutable Collections

Java Immutable Collections

Unmodifiable collections are usually read-only views (wrappers) of other collections. You can't add, remove or clear them, but the underlying collection can change.

Immutable collections can't be changed at all - they don't wrap another collection - they have their own elements.

Here's a quote from guava's ImmutableList

Unlike Collections.unmodifiableList(java.util.List<? extends T>), which is a view of a separate collection that can still change, an instance of ImmutableList contains its own private data and will never change.

So, basically, in order to get an immutable collection out of a mutable one, you have to copy its elements to the new collection, and disallow all operations.

Enforce immutable collections in a Java record?

You can do it already, the arguments of the constructor are mutable:

record SomeRecord(Set<Thing> set) {
public SomeRecord {
set = Set.copyOf(set);
}
}

A related discussion mentions the argument are not final in order to allow such defensive copying. It is still the responsibility of the developer to ensure the rule on equals() is held when doing such copying.

Immutable vs Unmodifiable collection

An unmodifiable collection is often a wrapper around a modifiable collection which other code may still have access to. So while you can't make any changes to it if you only have a reference to the unmodifiable collection, you can't rely on the contents not changing.

An immutable collection guarantees that nothing can change the collection any more. If it wraps a modifiable collection, it makes sure that no other code has access to that modifiable collection. Note that although no code can change which objects the collection contains references to, the objects themselves may still be mutable - creating an immutable collection of StringBuilder doesn't somehow "freeze" those objects.

Basically, the difference is about whether other code may be able to change the collection behind your back.

How do I use collections in immutable class safely in Java?

Why I cant see the change on previously copied version ?

Precisely because you copied it! It's a copy - a different ArrayList object from the original, that just happens to contain the same elements.

This is reference and I think it should be point the same memory area

That is only true in the case of:

public List<String> getCourseList() {
return courseList;
}

which is why you see the change on clist1. With clone(), you are creating a new object, and allocating new memory. Sure, you are still returning a reference to an object, but it's not the same reference that courseList stores. It's a reference to the copy.

must I use the deep copy in immutable classes always ?

No, as long as the elements in the collection are immutable. The whole point of making a copy is so that users can't do things like this:

List<String> list = student.getCourseList();
list.add("New Course");

If getCourseList didn't return a copy, the above code would change the student's course list! We certainly don't want that to happen in an immutable class, do we?

If the list elements are immutable as well, then users of your class won't be able to mutate them anyway, so you don't need to copy the list elements.


Of course, all of this copying can be avoided if you just use an immutable list:

private final List<String> courseList;
public ImmutableStudent(String _name, Long _id, String _uni, ArrayList<String> _courseList){
name = _name;
id = _id;
uni = _uni;
courseList = Collections.unmodifiableList(_courseList)
};

Java - Add one element to an immutable list

I would create a new ArrayList append the element and then return that as an unmodifiable list. Something like,

private static <T> List<T> appendOne(List<T> al, T t) {
List<T> bl = new ArrayList<>(al);
bl.add(t);
return Collections.unmodifiableList(bl);
}

And to test it

public static void main(String[] args) {
List<String> al = appendOne(new ArrayList<>(), "1");
List<String> bl = appendOne(al, "2");
System.out.println(bl);
}

I get (unsurprisingly):

[1, 2]

See this code run at IdeOne.com.

How to create Immutable List in java?

Once your beanList has been initialized, you can do

beanList = Collections.unmodifiableList(beanList);

to make it unmodifiable. (See Immutable vs Unmodifiable collection)

If you have both internal methods that should be able to modify the list, and public methods that should not allow modification, I'd suggest you do

// public facing method where clients should not be able to modify list    
public List<Bean> getImmutableList(int size) {
return Collections.unmodifiableList(getMutableList(size));
}

// private internal method (to be used from main in your case)
private List<Bean> getMutableList(int size) {
List<Bean> beanList = new ArrayList<Bean>();
int i = 0;

while(i < size) {
Bean bean = new Bean("name" + i, "address" + i, i + 18);
beanList.add(bean);
i++;
}
return beanList;
}

(Your Bean objects already seem immutable.)


As a side-note: If you happen to be using Java 8+, your getMutableList can be expressed as follows:

return IntStream.range(0,  size)
.mapToObj(i -> new Bean("name" + i, "address" + i, i + 18))
.collect(Collectors.toCollection(ArrayList::new));

Immutable collections in Java

If I understand you correctly, you want an immutable list that has convenient add/remove methods that return new list instances that reuse as much of the original list structure as possible. You could do something like this:

public abstract class ImmutableList<T> implements Iterable<T> {
/**
* Adds an element to the head of the list, returning the new list.
*
* @param o The element to be added to the list.
* @return The list consisting of the element <var>o</var> followed by
* this list.
*/
public final ImmutableList<T> add(final T o) {
return new Node<>(o, this);
}

/**
* Removes the element <var>o</var> resulting in a new list which
* is returned to the caller.
*
* @param o The object to be removed from the list.
* @return A list consisting of this list with object <var>o</var> removed.
*/
public abstract ImmutableList<T> remove(final T o);

public abstract boolean isEmpty();
public abstract int size();

public abstract boolean contains(final T o);

private ImmutableList() {}

/**
* Returns a "standard" enumeration over the elements of the list.
*/
public Iterator<T> iterator() {
return new NodeIterator<>(this);
}

/**
* The empty list. Variables of type ImmutableList should be
* initialised to this value to create new empty lists.
*/
private static final ImmutableList<?> EMPTY = new ImmutableList<Object>() {
@Override
public ImmutableList<Object> remove(final Object o) {
return this;
}

@Override
public boolean isEmpty() {
return true;
}

@Override
public int size() {
return 0;
}

@Override
public boolean contains(final Object o) {
return false;
}
};

@SuppressWarnings("unchecked")
public static <T> ImmutableList<T> empty() {
return (ImmutableList<T>)EMPTY;
}

public static <T> ImmutableList<T> create(final T head) {
return new Node<>(head, ImmutableList.<T>empty());
}

static class Node<T> extends ImmutableList<T> {
private final int _size;

private Node(final T element, final ImmutableList<T> next) {
_element = element;
_next = ArgumentHelper.verifyNotNull(next, "next");
_size = next.size() + 1;
}

public ImmutableList<T> remove(final T old) {
if (_element == old) {
return _next;
}
else {
final ImmutableList<T> n = _next.remove(old);
if (n == _next) {
return this;
}
else {
return new Node<>(_element, n);
}
}
}

@Override
public boolean isEmpty() {
return false;
}

@Override
public int size() {
return _size;
}

@Override
public boolean contains(final T o) {
return Objects.equals(_element, o) || _next.contains(o);
}

private final T _element;
private final ImmutableList<T> _next;
}

private class NodeIterator<T> implements Iterator<T> {
private ImmutableList<T> _current;

private NodeIterator(final ImmutableList<T> head) {
_current = ArgumentHelper.verifyNotNull(head, "head");
}

public boolean hasNext() {
return !_current.isEmpty();
}

public T next() {
final T result = ((Node<T>)_current)._element;
_current = ((Node<T>)_current)._next;
return result;
}

public void remove() {
throw new UnsupportedOperationException();
}
}
}

For this implementation, a new list is build by adding item(s) to ImmutableList.empty().

Note that this is not a particularly wonderful implementation; new elements are appended to the beginning of the list, as opposed to the end. But perhaps this will give you an idea of where to start.

What is the relationship between Collections and ImmutableCollections

Immutable Collection are same as Normal collection but cannot be modified after creation (i.e it is read only). you cannot add, delete or update items, if you try it throws unsupportetOperationException.

You cannot instantiate ImmutableList by new ImmutableList() because it is a abstract class. you can use ImmutableList.of() method to get immuatabel list.

If you want to know more about immutableList see Geeksforgeeks - ImmutableList in java

Why does ImmutableCollection.contains(null) fail?

why does in Java the call coll.contains(null) fail for ImmutableCollections?

Because the design team (the ones who have created guava) decided that, for their collections, null is unwanted, and therefore any interaction between their collections and a null check, even in this case, should just throw to highlight to the programmer, at the earliest possible opportunity, that there is a mismatch. Even where the established behaviour (as per the existing implementations in the core runtime itself, such as ArrayList and friends, as well as the javadoc), rather explicitly go the other way and say that a non-sequitur check (is this pear part of this list of apples?) strongly suggests that the right move is to just return false and not throw.

In other words, guava messed up. But now that they have done so, going back is potentially backwards compatibility breaking. It really isn't very - you are replacing an exception thrown with a false return value; presumably code could be out there that relies on the NPE (catching it and doing something different from what the code would do had contains(null) returned false instead of throwing) - but that's a rare case, and guava breaks backwards compatibility all the time.

And how can I properly check for nulls in a Collection in general?

By calling .contains(null), just as you are. The fact that guava doesn't do it right doesn't change the answer. You might as well ask 'how do I add elements to a list', and counter the answer of "well, you call list.add(item) to do that" with: Well, I have this implementation of the List interface that plays Rick Astley over the speaker instead of adding to the list, so, I reject your answer.

That's.. how java and interfaces work: You can have implementations of them, and the only guardianship that they do what the interface dictates they must, is that the author understands there is a contract that needs to be followed.

Now, normally a library so badly written they break contract for no good reason*, isn't popular. But guava IS popular. Very popular. That gets at a simple truth: No library is perfect. Guava's API design is generally quite good (in my opinion, vastly superior to e.g. Apache commons libraries), and the team actively spends a lot of time debating proper API design, in the sense that the code that one would write using guava is nice (as defined by: Easy to understand, has few surprises, easy to maintain, easy to test, and probably easy to mutate to deal with changing requirements - the only useful definition for nebulous terms like 'nice' or 'elegant' code - it's code that does those things, anything else is pointless aesthetic drivel). In other words, they are actively trying, and they usually get it right.

Just, not in this case. Work around it: return item != null && coll.contains(item); will get the job done.

There is one major argument in favour of guava's choice: They 'contract break' is an implicit break - one would expect that .contains(null) works, and always returns false, but it's not explicitly stated in the javadoc that one must do this. Contrast to e.g. IdentityHashMap, which uses identity equivalence (a==b) and not value equality (a.equals(b)) in its .containsKey etc implementations, which explicitly goes against the javadoc contract as stated in the j.u.Map interface. IHM has an excellent reason for it, and highlights the discrepancy, plus explains the reason, in the javadoc. Guava isn't nearly as clear about their bizarre null behaviour, but, here's a crucial thing about null in java:

Its meaning is nebulous. Sometimes it means 'empty', which is bad design: You should never write if (x == null || x.isEmpty()) - that implies some API is badly coded. If null is semantically equivalent to some value (such as "" or List.of()), then you should just return "" or List.of(), and not null. However, in such a design, list.contains(null) == false) would make sense.

But sometimes null means not found, irrelevant, not applicable, or unknown (for example, if map.get(k) returns null, that's what it means: Not found. Not 'I found an empty value for you'). This matches with what NULL means in e.g. SQL. In all those cases, .contains(null) should be returning neither true nor false. If I hand you a bag of marbles and ask you if there is a marble in there that is grue, and you have no idea what grue means, you shouldn't answer either yes or no to my query: Either answer is a meaningless guess. You should tell me that the question cannot be answered. Which is best represented in java by throwing, which is precisely what guava does. This also matches with what NULL does in SQL. In SQL, v IN (x) returns one of 3 values, not 2 values: It can resolve to true, false, or null. v IN (NULL) would resolve to NULL and not false. It is answering a question that can't be answered with the NULL value, which is to be read as: Don't know.

In other words, guava made a call on what null implies which evidently does not match with your definitions, as you expect .contains(null) to return false. I think your viewpoint is more idiomatic, but the point is, guava's viewpoint is different but also consistent, and the javadoc merely insinuates, but does not explicitly demand, that .contains(null) returns false.

That's not useful whatsoever in fixing your code, but hopefully it gives you a mental model, and answers your question of "why does it work like this?".



Related Topics



Leave a reply



Submit