I seem to stumble across something interesting in ArrayList
implementation that I can't wrap my head around. Here is some code that shows what I mean:
public class Sandbox {
private static final VarHandle VAR_HANDLE_ARRAY_LIST;
static {
try {
Lookup lookupArrayList = MethodHandles.privateLookupIn(ArrayList.class, MethodHandles.lookup());
VAR_HANDLE_ARRAY_LIST = lookupArrayList.findVarHandle(ArrayList.class, "elementData", Object[].class);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
public static void main(String[] args) {
List<String> defaultConstructorList = new ArrayList<>();
defaultConstructorList.add("one");
Object[] elementData = (Object[]) VAR_HANDLE_ARRAY_LIST.get(defaultConstructorList);
System.out.println(elementData.length);
List<String> zeroConstructorList = new ArrayList<>(0);
zeroConstructorList.add("one");
elementData = (Object[]) VAR_HANDLE_ARRAY_LIST.get(zeroConstructorList);
System.out.println(elementData.length);
}
}
The idea is if you create an ArrayList
like this:
List<String> defaultConstructorList = new ArrayList<>();
defaultConstructorList.add("one");
And look inside what the elementData
(Object[]
where all elements are kept) the it will report 10
. Thus you add one element - you get 9 additional slots that are un-used.
If, on the other hand, you do:
List<String> zeroConstructorList = new ArrayList<>(0);
zeroConstructorList.add("one");
you add one element, space reserved is just for that element, nothing more.
Internally this is achieved via two fields:
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
When you create an ArrayList
via new ArrayList(0)
- EMPTY_ELEMENTDATA
will be used.
When you create an ArrayList
via new Arraylist()
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA
is used.
The intuitive part from inside me - simply screams "remove DEFAULTCAPACITY_EMPTY_ELEMENTDATA
" and let all the cases be handled with EMPTY_ELEMENTDATA
; of course the code comment:
We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first element is added
does make sense, but why would one inflate to 10
(a lot more than I asked for) and the other one to 1
(exactly as much as I requested).
Even if you use List<String> zeroConstructorList = new ArrayList<>(0)
, and keep adding elements, eventually you will get to a point where elementData
is bigger than the one requested:
List<String> zeroConstructorList = new ArrayList<>(0);
zeroConstructorList.add("one");
zeroConstructorList.add("two");
zeroConstructorList.add("three");
zeroConstructorList.add("four");
zeroConstructorList.add("five"); // elementData will report 6, though there are 5 elements only
But the rate at which it grows is smaller than the case of default constructor.
This reminds me about HashMap
implementation, where the number of buckets is almost always more than you asked for; but there that is done because of the need for "power of two" buckets needed, not the case here though.
So the question is - can someone explain this difference to me?