Welcome to a world with Java 8!
It only took me all night with no sleep to learn what was needed to write this one freaking line of code. I'm sure it is already out there somewhere but I couldn't find it. So I'm sharing my hours and hours of research, enjoy. Woot!
Assuming:
ArrayList<ArrayList<String>> mainList = new ArrayList<ArrayList<String>>();
// populate this list here
(Or, rather, in Java 8:
ArrayList<ArrayList<String>> mainList = new ArrayList();
//Populate
)
Then all you need is:
String[][] stringArray = mainList.stream().map(u -> u.toArray(new String[0])).toArray(String[][]::new);
Bam! One line.
I'm not sure how fast it is compared to the other options. But this is how it works:
- Take a stream of the - mainList2D ArrayList. This stream is a bit like a Vector hooked up with a LinkedList and they had a kid. And that kid, later in life, dosed up on some NZT-48. I digress;- mainList.stream()is returning a stream of- ArrayList<String>elements. Or in even geekier speak:- mainList.stream()returns a- Stream<ArrayList<String>>, sorta.
 
- Call the - .mapfunction on that stream which will return a new stream with contents that match a new type specified by the parameters passed into- map. This- mapfunction will covert each element in our stream for us. It has a built in- foreachstatement. In order to accomplish this; the- mapfunction takes a lambda expression as its parameter. A Lambda expression is like a simple inline one-line function. Which has two data types gettin' Jiggy wit it. First is the type of data in the stream upon which it was called (- mainList.stream()).  The next type is the type of data it will map it out to, which is in the right half of the lambda expression:- u -> u.toArray(new String[0]). Here- uis an identifier you choose just like when using a- foreachstatement. The first half declares this like so:- u ->. And like a- foreach, the variable- uwill now be each element in the stream as it iterates through the stream.  Thus,- uis of the data type that the elements of the original stream are because it is them. The right half of the Lambda expression shows what to do with each element:- u.toArray(new String[0]). With the results being stored in their rightful place in a new stream. In this case we convert it to a- String[].. because after all, this is a 2D array of- String.. or rather from this point in the code, a 1D array of- String[](string arrays). Keep in mind that- uis ultimately an- ArrayList. Note, calling- toArrayfrom an- ArrayListobject will create a new array of the type passed into it. Here we pass in- new String[0]. Therefore it creates a new array of type- String[]and with length equal to the length of the ArrayList- u. It then fills this new array of strings with the contents of the- ArrayListand returns it. Which leaves the Lambda expression and back into- map. Then,- mapcollects these string arrays and creates a new stream with them, it has the associated type- String[]and then returns it. Therefore,- mapreturns a- Stream<String[]>, in this case. (Well, actually it returns a- Stream<Object[]>, which is confusing and needs conversion, see below)
 
- Therefore we just need to call - toArrayon that new stream of arrays of strings. But calling- toArrayon a- Stream<Object[]>is a bit different than calling it on an- ArrayList<String>, as we did before. Here, we have to use a function reference confusing thing. It grabs the type from this:- String[][]::new.  That- newfunction has type- String[][]. Basically, since the function is called toArray it will always be an- []of some sort. In our case since the data inside was yet another array we just add on another- []. I'm not sure why the NZT-48 wasn't working on this one. I would have expected a default call to- toArray()would be enough, seeing that its a stream and all. A stream thats specifically- Stream<String[]>. Anyone know why- mapactually returns a- Stream<Object[]>and not a stream of the type returned by the Lambda expression inside?
 
- Now that we got the - toArrayfrom our- mainListstream acting properly. We can just dump it into a local variable easy enough:- String[][] stringArray = mainList.stream...
 
Convert 2D ArrayList of Integers to 2D array of primitive ints
Now, I know some of you are out there going. "This doesn't work for ints!" As was my case. It does however work for "Ents", see above.  But, if you want a 2D primitive int array from a 2D ArrayList of Integer (ie. ArrayList<ArrayList<Integer>>). You gotta change around that middle[earth] mapping. Keep in mind that ArrayLists can't have primitive types. Therefore you can't call toArray on the ArrayList<Integer> and expect to get a int[]. You will need to map it out... again.
int[][] intArray = mainList.stream().map(  u  ->  u.stream().mapToInt(i->i).toArray()  ).toArray(int[][]::new);
I tried to space it out for readability. But you can see here that we have to go through the same whole mapping process again. This time we can't just simply call toArray on the ArrayList u; as with the above example. Here we are calling toArray on a Stream not an ArrayList. So for some reason we don't have to pass it a "type", I think its taking brain steroids. Therefore, we can take the default option; where it takes a hit of that NZT-48 and figures out the obvious for us this [run]time. I'm not sure why it couldn't just do that on the above example. Oh, thats right.... ArrayLists don't take NZT-48 like Streams do. Wait... what am I even talking about here?
Annnyhoow, because streams are sooo smart. Like Sheldon, we need a whole new protocol to deal with them. Apparently advanced intelligence doesn't always mean easy to deal with. Therefore, this new mapToInt is needed to make a new Stream which we can use its smarter toArray. And the i->i Lambda expression in mapToInt is a simple unboxing of the Integer to int, using the implicit auto-unboxing that allows int = Integer. Which, now, seems like a dumb trivial thing to do, as if intelligence has its limits. During my adventure learning all this: I actually tried to use mapToInt(null) because I expected a default behavior. Not an argument!! (cough Sheldon cough)  Then I say in my best Husker valley girl accent, "Afterall, it is called mapToInt, I would guess that, like, 84% of the time (42x2) it will be, like, passed i->i by, like, everyone, so like, omgawd!"  Needless to say, I feel a bit... like.. this guy. I don't know, why it doesn't work that way.
Well, I'm red-eye and half delirious and half asleep. I probably made some mistakes; please troll them out so I can fix them and let me know if there is an even better way!
PT