초기 용량으로 ArrayList를 시작하는 이유는 무엇입니까?


149

일반적인 생성자 ArrayList는 다음과 같습니다.

ArrayList<?> list = new ArrayList<>();

그러나 초기 용량에 대한 매개 변수가있는 오버로드 된 생성자가 있습니다.

ArrayList<?> list = new ArrayList<>(20);

원하는 ArrayList대로 추가 할 수있을 때 초기 용량 으로 생성하는 것이 유용한 이유는 무엇 입니까?


17
ArrayList 소스 코드를 보려고 했습니까?
AmitG

@Joachim Sauer : 언젠가 소스를주의 깊게 읽으면 인식이됩니다. 그가 소스를 읽었을 때 시도해 보았습니다. 나는 당신의 측면을 이해했습니다. 감사.
AmitG

ArrayList의 성능이 좋지 않은 기간, 왜 그런 구조를 사용하고 싶습니까?
PositiveGuy

답변:


196

크기를 미리 알고 있다면 ArrayList초기 용량을 지정하는 것이 더 효율적입니다. 이 작업을 수행하지 않으면 목록이 커짐에 따라 내부 배열을 반복적으로 재 할당해야합니다.

최종 목록이 클수록 재 할당을 피함으로써 더 많은 시간을 절약 할 수 있습니다.

즉, 사전 할당이 없어도 n뒷면에 요소를 삽입 ArrayList하는 데 총 O(n)시간이 걸립니다. 다시 말해, 요소를 추가하는 것은 상각 된 상수 시간 연산입니다. 이것은 각각의 재 할당이 어레이의 크기를 지수 적으로, 전형적으로 씩 증가시킴으로써 달성된다 1.5. 이 방법을 사용하면 총 작업 수를로 표시 할 수 있습니다O(n) .


5
알려진 크기를 미리 할당하는 것이 좋지만 일반적으로 그렇게하지 않는 것은 끔찍한 일이 아닙니다. 최종 크기가 n 인 목록의 경우 log (n) 재 할당이 필요합니다 .
Joachim Sauer

2
@PeterOlson O(n log n)log n작업 n시간을 할 것 입니다. 그것은 큰 과대 평가입니다 ( 상한이기 때문에 큰 O로 기술적으로 정확 하지만 ). s + s * 1.5 + s * 1.5 ^ 2 + ... + s * 1.5 ^ m (s * 1.5 ^ m <n <s * 1.5 ^ (m + 1)) 요소를 총계로 복사합니다. 나는 합계가 좋지 않아서 머리 꼭대기에서 정확한 수학을 줄 수는 없습니다 (크기 조정 요소 2의 경우 2n이므로 1.5n 줄이거 나 작은 상수를 취할 수 있음). t는이 합계가 최대 n보다 큰 상수 인자임을 알기 위해 너무 많은 흠집을 내야합니다. 따라서 O (k * n) 복사본이 필요합니다. 물론 O (n)입니다.

1
@delnan : 그와 논쟁 할 수 없습니다! ;) BTW, 나는 당신의 가늘게 뜨는 주장을 정말로 좋아했습니다; 트릭의 레퍼토리에 추가하겠습니다.
NPE

6
두 배로 논쟁하는 것이 더 쉽습니다. 하나의 요소로 시작하여 가득 찬 경우 두 배로 가정하십시오. 8 개의 요소를 삽입한다고 가정하십시오. 하나를 삽입하십시오 (비용 : 1). 2 개 삽입-2 개, 요소 1 개를 복사하고 2 개 삽입 (비용 : 2). 세 개 삽입-이중, 두 요소 복사, 세 개 삽입 (비용 : 3). 4를 삽입하십시오 (비용 : 1). 5 개 삽입-이중, 4 개 요소 복사, 5 개 삽입 (비용 : 5). 6, 7, 8을 삽입하십시오 (비용 : 3). 총 비용 : 1 + 2 + 3 + 1 + 5 + 3 = 16. 삽입 된 요소 수의 두 배 입니다. 이 스케치에서 평균 비용이 일반적으로 인서트 당 2 임을 증명할 수 있습니다 .
Eric Lippert

9
그것은 시간 비용 입니다 . 또한 시간이 지남에 따라 낭비되는 공간 의 양 이 0 %이고 시간의 일부가 100 %에 가깝다는 것을 알 수 있습니다. 요소를 2에서 1.5 또는 4 또는 100으로 변경하거나 낭비되는 공간의 평균 량과 복사에 소요되는 평균 시간이 변경되지만 시간 복잡도는 요인이 무엇이든 평균적으로 선형으로 유지됩니다.
Eric Lippert

41

왜냐하면 ArrayListA는 동적 리사이징 어레이 는 초기 (기본) 고정 크기 어레이로서 구현되는 수단, 데이터 구조. 이것이 채워지면 배열은 두 배 크기로 확장됩니다. 이 작업은 비용이 많이 들기 때문에 최대한 적게 원합니다.

따라서 상한이 20 개의 항목 인 경우 초기 길이가 20 인 배열을 만드는 것이 기본값 인 15를 사용하는 것보다 낫습니다. 그런 다음 15*2 = 30확장주기를 낭비하면서 크기를 조정하고 20 만 사용하십시오.

PS-AmitG가 말했듯이 확장 요소는 구현에 따라 다릅니다 (이 경우 (oldCapacity * 3)/2 + 1)


9
실제로int newCapacity = (oldCapacity * 3)/2 + 1;
AmitG

25

Arraylist의 기본 크기는 10 입니다.

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
    this(10);
    } 

따라서 100 개 이상의 레코드를 추가하려는 경우 메모리 재 할당 오버 헤드를 볼 수 있습니다.

ArrayList<?> list = new ArrayList<>();    
// same as  new ArrayList<>(10);      

따라서 Arraylist에 저장 될 요소 수에 대한 아이디어가 있다면 10으로 시작한 다음 증가시키는 대신 해당 크기의 Arraylist를 만드는 것이 좋습니다.


향후 JDK 버전의 기본 용량이 항상 10 일 것이라는 보장은 없습니다. –private static final int DEFAULT_CAPACITY = 10
vikingsteve

17

나는 실제로 2 개월 전에 주제에 대한 블로그 게시물 을 썼습니다 . 이 기사는 C #에 대한 List<T>것이지만 Java의 ArrayList구현은 매우 유사합니다. ArrayList동적 배열을 사용하여 구현 되므로 필요에 따라 크기가 커집니다. 용량 생성자의 이유는 최적화를위한 것입니다.

이러한 크기 조정 작업 중 하나가 발생하면 ArrayList는 배열의 내용을 기존 배열의 용량의 두 배인 새 배열로 복사합니다. 이 작업은 O (n) 시간에 실행됩니다 .

다음은 ArrayList크기가 어떻게 증가 하는지에 대한 예입니다 .

10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308

따라서 목록의 용량은 1011 번째 항목이 추가 될 때로 증가 50% + 1합니다 16. 17 번째 항목에서 ArrayList가 다시 증가합니다 25. 이제 원하는 용량이 이미 알려진 목록을 작성하는 예를 고려하십시오 1000000. ArrayList크기 생성자를 사용하지 않고 생성하면 크기 조정시 O (1) 또는 O (n)ArrayList.add 1000000 이 걸리는 시간 이 호출됩니다 .

1000000 + 16 + 25 + ... + 670205 + 1005308 = 4015851 연산

생성자를 사용하여 이것을 비교 한 다음 O (1)ArrayList.add 에서 실행되도록 보장하는 호출하십시오 .

1000000 + 1000000 = 2000000 연산

자바 대 C #

Java는 위와 같으며에서 시작하여 10각 크기를 조정 50% + 1합니다. C #은 시작될 4때마다 훨씬 더 적극적으로 증가하여 크기를 조정할 때마다 두 배가됩니다. 1000000C # 사용 3097084작업에 대한 위 의 추가 예제 .

참고 문헌


9

예를 들어 ArrayList의 초기 크기를 설정하면 ArrayList<>(100)내부 메모리의 재 할당 횟수가 줄어 듭니다.

예:

ArrayList example = new ArrayList<Integer>(3);
example.add(1); // size() == 1
example.add(2); // size() == 2, 
example.add(2); // size() == 3, example has been 'filled'
example.add(3); // size() == 4, example has been 'expanded' so that the fourth element can be added. 

위의 예에서 볼 수 있듯이 ArrayList필요한 경우 확장 할 수 있습니다. 이것이 표시하지 않는 것은 Arraylist의 크기가 일반적으로 두 배라는 것입니다 (새 크기는 구현에 따라 다릅니다). 다음은 Oracle 에서 인용 한 것입니다 .

"각 ArrayList 인스턴스에는 용량이 있습니다. 용량은 목록에 요소를 저장하는 데 사용되는 배열의 크기입니다. 항상 최소한 목록 크기보다 큽니다. 요소가 ArrayList에 추가되면 용량이 자동으로 증가합니다. 성장 정책에 대한 세부 사항은 요소를 추가하는 것이 상각 된 상각 시간 비용이라는 사실을 넘어서 명시되어 있지 않다. "

분명히, 어떤 종류의 범위를 보유할지 모를 경우 크기를 설정하는 것은 좋은 생각이 아닙니다. 그러나 특정 범위를 염두에두고 초기 용량을 설정하면 메모리 효율성이 향상됩니다. .


3

ArrayList는 많은 값을 포함 할 수 있으며 큰 초기 삽입을 수행 할 때 다음 항목에 더 많은 공간을 할당하려고 할 때 CPU주기를 낭비하지 않도록 시작하여 더 큰 스토리지를 할당하도록 ArrayList에 지시 할 수 있습니다. 따라서 처음에 약간의 공간을 할당하는 것이 더 효과적입니다.


3

이것은 모든 단일 객체에 대한 재 할당 노력을 피하기위한 것입니다.

int newCapacity = (oldCapacity * 3)/2 + 1;

내부적 new Object[]으로 생성됩니다. arraylist에 요소를 추가 할 때
JVM을 작성해야합니다 new Object[]. 당신이 호출 할 때 다음 재 할당을위한 코드 (당신이 너 한테 어떤 생각) 위의 모든 시간이 없다면 arraylist.add()다음 new Object[]무의미하다 만들 수있다 우리는 추가 할 각각의 모든 객체에 대해 1 씩 크기를 증가시키기위한 시간을 잃어버린 있습니다. 따라서 Object[]다음 수식으로 크기를 늘리는 것이 좋습니다 .
(JSL은 매번 1 씩 증가하는 대신 동적으로 배열 목록을 증가시키기 위해 아래에 주어진 캐스팅 공식을 사용했습니다. 성장하기 위해서는 JVM이 노력해야합니다)

int newCapacity = (oldCapacity * 3)/2 + 1;

ArrayList는 각 단일에 대해 재 할당을 수행 하지 않습니다.add 이미 내부적으로 일부 성장 공식을 사용합니다. 따라서 질문에 대답하지 않습니다.
AH

@AH 제 답변은 음성 테스트 입니다. 줄 사이를 잘 읽어보십시오. 나는 말했다 "당신이 (arraylist.add 호출 할 때 무의미 우리가 시간을 잃어버린되는 만들 수있다 당신이)를 재 할당을위한 코드 (당신이 너 한테 어떤 생각) 이상 다음 새 개체 [] 모든 시간이 없다면." 그리고 코드 입니다 int newCapacity = (oldCapacity * 3)/2 + 1;ArrayList 클래스에 존재하는. 여전히 답이 없다고 생각하십니까?
AmitG

1
나는 여전히 답이 없다고 생각합니다 ArrayList. 상각 된 재 할당에서는 어떤 경우 에도 초기 용량에 대한 가치가 있습니다. 그리고 질문은 다음과 같습니다. 왜 초기 용량에 비표준 값을 사용합니까? 이 외에도 "줄 사이에서 읽는 것"은 기술적 인 답변에서 원하는 것이 아닙니다. ;-)
AH

@AH ArrayList에 재 할당 프로세스가 없다면 어떻게 되었습니까? 대답도 마찬가지입니다. 답의 정신을 읽으십시오 :-). 내가 더 잘 알고 상각 재 할당이 초기 용량에 대한 값을 어떤 경우에 발생에서의 ArrayList를.
AmitG

2

각 ArrayList는 init capacity 값이 "10"으로 생성되었다고 생각합니다. 어쨌든 생성자 내에서 용량을 설정하지 않고 ArrayList를 만들면 기본값으로 생성됩니다.


2

최적화라고 말하고 싶습니다. 초기 용량이없는 ArrayList는 ~ 10 개의 빈 행을 가지며 추가를 수행 할 때 확장됩니다.

trimToSize () 를 호출해야하는 항목 수와 정확히 일치하는 목록을 만들려면


0

에 대한 내 경험에 ArrayList따르면 초기 용량을 제공하는 것은 재 할당 비용을 피하는 좋은 방법입니다. 그러나주의해야 할 점이 있습니다. 위에서 언급 한 모든 제안에 따르면 요소 수의 대략적인 추정이 알려진 경우에만 초기 용량을 제공해야합니다. 그러나 우리가 아무 생각없이 초기 용량을 제공하려고 할 때, 예약되고 사용되지 않은 메모리의 양은 목록이 필요한 수의 요소로 채워지면 결코 필요하지 않을 수 있으므로 낭비가 될 것입니다. 내가 말하고있는 것은, 우리는 처음에 실용적이며 용량을 할당 할 수 있으며 런타임에 필요한 최소 용량을 알 수있는 현명한 방법을 찾습니다. ArrayList는라는 메소드를 제공합니다 ensureCapacity(int minCapacity). 그러나 현명한 길을 찾았습니다 ...


0

initialCapacity의 유무에 관계없이 ArrayList를 테스트
했으며 놀라운 결과를 얻었습니다. LOOP_NUMBER를 100,000 이하로 설정하면 initialCapacity를 설정하는 것이 효율적입니다.

list1Sttop-list1Start = 14
list2Sttop-list2Start = 10


그러나 LOOP_NUMBER를 1,000,000으로 설정하면 결과가 다음과 같이 변경됩니다.

list1Stop-list1Start = 40
list2Stop-list2Start = 66


마지막으로 어떻게 작동하는지 알 수 없었습니다!
샘플 코드 :

 public static final int LOOP_NUMBER = 100000;

public static void main(String[] args) {

    long list1Start = System.currentTimeMillis();
    List<Integer> list1 = new ArrayList();
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list1.add(i);
    }
    long list1Stop = System.currentTimeMillis();
    System.out.println("list1Stop-list1Start = " + String.valueOf(list1Stop - list1Start));

    long list2Start = System.currentTimeMillis();
    List<Integer> list2 = new ArrayList(LOOP_NUMBER);
    for (int i = 0; i < LOOP_NUMBER; i++) {
        list2.add(i);
    }
    long list2Stop = System.currentTimeMillis();
    System.out.println("list2Stop-list2Start = " + String.valueOf(list2Stop - list2Start));
}

windows8.1 및 jdk1.7.0_80에서 테스트했습니다.


1
불행히도 currentTimeMillis 공차는 최대 100 밀리 초입니다 (결과에 따라 다름). 올바른 작업을 수행하기 위해 사용자 지정 라이브러리를 사용하는 것이 좋습니다.
Bogdan
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.