ArrayList 의 toArray 사용시 배열 초기화 길이를 0 으로 해야하는 이유

의문

자바 코드를 작성하다보면 종종 ArrayList 를 toArray 메서드를 이용해 배열로 바꿔야 할때가 있다.

List<String> temp = new ArrayList<>();
temp.add("test");
String[] strings = temp.toArray(new String[temp.size()]);

하지만 위의 코드는 Intelij 가 warning 을 만드는 코드이다.

empty array 로 변경하라고 추천해준다.

실제로 Replace argument with empty array 를 클릭해보면 empty array 초기화로 코드를 바꿔준다.

변경 후 코드

왜 길이를 미리 정한 배열보다 길이를 0으로 초기화한 배열을 사용해야하는걸까?

 

검색결과

성능 차이

두가지에 대한 성능테스트는 stackoverflow 글에서 찾을 수 있었다. 해당 글을 보면 아주 재미있게도 길이를 미리 정해준것보다 0으로 초기화한것이 성능상 아주 약간이지만 더 좋았다. 밑의 답변들을 더 읽어보면 OpenJDK6 버전 이전에는 런타임에서 배열을 새로 생성하기 위해 리플렉션을 이용해서 만들었는데 그 당시에는 길이를 미리 정해주는것이 더 나았다고 한다. 하지만 OpenJDK6 버전 이후에는 그 기능이 강화되어 길이를 미리 정해둔것과 0으로 초기화한것이 차이가 없어졌고 오히려 가끔은 0으로 초기화한것이 더 나을때도 있다고 한다.

동시성 이슈

해당 stackoverflow 의 다른 답변을 보면 길이를 정해서 넘길경우 동시성 이슈가 생길 수 있다고 한다.

passing pre-sized array is dangerous for a concurrent or synchronized collection as a data race is possible between the size and toArray call which may result in extra nulls at the end of the array, if the collection was concurrently shrunk during the operation.

 

ArrayList 의 toArray() 의 구현체를 한번 살펴보자

해당 메서드는 넘겨받은 a 에 대해 length 를 접근해 List 스스로가 가지고 있는 size 멤버필드를 이용한 조건문들이 존재한다. 만약 해당 메서드에 배열의 길이를 미리 초기화 한 뒤 넘겨서 코드를 실행했는데 도중에 다른 스레드에서 해당 List 의 size 를 줄인다면 원래라면 타지 않았어야할 마지막 조건문이 true 가 되고 그 결과 extra nulls 가 추가될 가능성이 있다. 그렇기에 동시성 이슈가 생길 가능성이 있다는 뜻이다.

 

결론

배열의 길이를 미리 정하고 넘기는것과 길이를 0으로 초기화 하고 넘기는것은 결국 똑같은 일을 한다. 차이라고 하면 메서드 안에서 넘겨받은 배열의 길이가 List 의 size 보다 작다면 새로운 배열을 런타임에 만들어내고, 그 안의 값들을 채워 넣는다. OpenJDK 6 이후부터는 성능상의 차이도 거의 없다고 봐도 무방하며, 가장 크게 와닿는 부분은 동시성문제 였다. 그렇기 때문에 toArray 메서드를 사용할때에는 배열의 사이즈를 0으로 초기화 한 뒤 넘겨주도록 하자.