melli-182
melli-182

Reputation: 1234

Difference between an empty ArrayList and an ArrayList with null elements?

I am coding some validators for a REST service which parses a JSON and I found out something that sounds weird for me (I am not a Java expert at all).

Consider having two ArrayLists:

ArrayList<Object> list1 = new ArrayList<Object>();
ArrayList<Object> list2 = new ArrayList<Object>();

Both lists have something in common, they are completely empty (or full of null elements). But if I do:

list1.add(null);

Although both remain completely empty, they have completely different behaviors. And to make some methods the results are very different:

System.out.println(list1.contains(null));  //prints true!
System.out.println(list2.contains(null));  //prints false

System.out.println(CollectionUtils.isNotEmpty(list1));  //prints true
System.out.println(CollectionUtils.isNotEmpty(list2));  //prints false

System.out.println(list1.size());  //prints 1
System.out.println(list2.size());  //prints 0

Doing some research, and looking at the implementation of each of these methods, you can determine the reason for these differences, but still do not understand why it would be valid or useful to differentiate between these lists.

I am mostly agree whit the answers, but I'm not yet convinced all. This is the implementation of the method remove:

/**
 * Removes the first occurrence of the specified element from this list,
 * if it is present.  If the list does not contain the element, it is
 * unchanged.  More formally, removes the element with the lowest index
 * <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
 * (if such an element exists).  Returns <tt>true</tt> if this list
 * contained the specified element (or equivalently, if this list
 * changed as a result of the call).
 *
 * @param o element to be removed from this list, if present
 * @return <tt>true</tt> if this list contained the specified element
 */
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}


/*
 * Private remove method that skips bounds checking and does not
 * return the value removed.
 */
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

So, now if i do:

ArrayList<Object> list = new ArrayList<Object>();
list.add(null);
System.out.println(list.contains(null)); //prints true!
list.remove(null);
System.out.println(list.contains(null));  //prints false!

What am I missing?

Upvotes: 7

Views: 38425

Answers (7)

Ahmad Ahmad
Ahmad Ahmad

Reputation: 79

ArrayList typically use an iterator that determines the actual number of elements (size). The rest of elements are set to null and aren't accessible (I guess for performance reasons).

ArrayList<Integer> nums = new ArrayList<>(20);
System.out.println(nums.size()); // 0
System.out.println(nums.contains(null)); // false.
nums.add(null);
System.out.println(nums.size()); // 1
System.out.println(nums.contains(null)); // true

as you can see the actual number of elements was 0 in the first code (even if the capacity is 20, method won't work based on it), so the number of accessible elements is 0, and of course null won't be found in this case.

in the second code you have 1 accessible value which is null and that's why you'll get true.

In essence, any value that isn't explicitly inserted will be treated like it doesn't exist.

Upvotes: 0

Atomiz
Atomiz

Reputation: 71

The difference is in the List size, not its capacity. When instantiating a new ArrayList<>(5), the initial capacity is set to 5, but the size remains 0, because we haven't added any elements. If we invoke add(null) only the elements size() will increase and all functions iterating through the array of elements will remain inbetween the size of the List, not the capacity.

This is so when we need to rapidly add new elements, the array doesn't get copied into another array with a size + 1, instead Java increases the capacity by half when the size is equal to it to improve the performance by reducing the functions performed.

Upvotes: -1

Matej
Matej

Reputation: 1757

The entire confusion stems from the incorrect initial assumption that the ArrayLists you created are full of nulls. They are not, they are completely empty - meaning they have zero elements. As such, you will get false if you call contains(null)on them, and true when you call isEmpty().

Once you added null to list1, it ceased to be empty, and indeed contains a null element - list2 is still empty and still doesn't contain null.

In your last code sample, the second call to Collections.isNotEmpty definitely does not return true. The list had one null element, you removed it, so it's empty again.

The main take-aways here are:

  • Calling either the empty constructor, or the constructor with initial capacity of an ArrayList creates an empty list. This list does not contain any elements, not even null.
  • null is a perfectly valid element of an ArrayList. The cases where this might be useful are not extremely common but they do exist.

Upvotes: 0

John Bollinger
John Bollinger

Reputation: 180103

A List is empty if it contains zero elements. This is both the natural meaning of the term and the test performed by the isEmpty() method.

null is a value that any ArrayList may contain as an element. Clearly, in that case the list has at least one element, and therefore is not empty. No matter what the initial state of a List, if an invocation of add(null) on that list completes normally then afterward the list is not empty. Similarly, each null element in a list contributes to its size.

Why add(item) doesnt validate if item!=null ?

Ask Joshua Bloch. He designed it. Most people take him to be a pretty smart guy, and consider the Collections API a highly successful effort.

Seriously, it is a design choice that Lists may contain null elements. (Implementations are permitted to reject nulls, but ArrayList, as a general-purpose implementation, accepts them).

Why contains(null) says false if the list is full of nulls?

It doesn't. Don't just take my word for it -- the data you present also contradict you.

Upvotes: 1

soulsabr
soulsabr

Reputation: 904

Fortunately you don't have to be a java expert or an expert at all.

The best way to think of this is like a parking lot. An array with null elements is like a parking lot with nothing parked in it. An empty array is like the plans for a parking lot.

Upvotes: 1

Wormbo
Wormbo

Reputation: 4992

An ArrayList explicitly is allowed and able to store null values, because they might be meaningful to your program. And empty list is empty (i.e. doesn't contain anything, not even null. After you successfully add(null) (it returns true to signal success), the list must obviously return true on contains(null) as well. In fact, you can even remove(null) from that list and it will be empty again.

Upvotes: 3

dsh
dsh

Reputation: 12213

A list containing null is NOT empty. It contains null. Lists are allowed to contain null, so you can put null in it if you want.

Upvotes: 13

Related Questions