Look into generics as type clarification process, you can assign typed value to a variable of raw type AND vice versa. In core generics are a shortcut for the programmers to avoid making type casting too much, which also helps to catch some logical errors at compile time.
At the very basics ArrayList will always implicitly have items of type Object.
So
test[i] = new ArrayList<String>(); because test[i] has type of ArrayList.
The bit
test[3] = new ArrayList<String>();
test[2] = new HashSet<String>();
did not work - as was expected, because HashSet simply is not a subclass of ArrayList. Generics has nothing to do here. Strip away the generics and you'll see the obvious reason.
However,
test[2] = new ArrayList<String>();
test[3] = new ArrayList<HashSet>();
will work nicely, because both items are ArrayLists.
Hope this made sense...