초기 용량으로 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.