일반적인 생성자 ArrayList
는 다음과 같습니다.
ArrayList<?> list = new ArrayList<>();
그러나 초기 용량에 대한 매개 변수가있는 오버로드 된 생성자가 있습니다.
ArrayList<?> list = new ArrayList<>(20);
원하는 ArrayList
대로 추가 할 수있을 때 초기 용량 으로 생성하는 것이 유용한 이유는 무엇 입니까?
일반적인 생성자 ArrayList
는 다음과 같습니다.
ArrayList<?> list = new ArrayList<>();
그러나 초기 용량에 대한 매개 변수가있는 오버로드 된 생성자가 있습니다.
ArrayList<?> list = new ArrayList<>(20);
원하는 ArrayList
대로 추가 할 수있을 때 초기 용량 으로 생성하는 것이 유용한 이유는 무엇 입니까?
답변:
크기를 미리 알고 있다면 ArrayList
초기 용량을 지정하는 것이 더 효율적입니다. 이 작업을 수행하지 않으면 목록이 커짐에 따라 내부 배열을 반복적으로 재 할당해야합니다.
최종 목록이 클수록 재 할당을 피함으로써 더 많은 시간을 절약 할 수 있습니다.
즉, 사전 할당이 없어도 n
뒷면에 요소를 삽입 ArrayList
하는 데 총 O(n)
시간이 걸립니다. 다시 말해, 요소를 추가하는 것은 상각 된 상수 시간 연산입니다. 이것은 각각의 재 할당이 어레이의 크기를 지수 적으로, 전형적으로 씩 증가시킴으로써 달성된다 1.5
. 이 방법을 사용하면 총 작업 수를로 표시 할 수 있습니다O(n)
.
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)입니다.
왜냐하면 ArrayList
A는 동적 리사이징 어레이 는 초기 (기본) 고정 크기 어레이로서 구현되는 수단, 데이터 구조. 이것이 채워지면 배열은 두 배 크기로 확장됩니다. 이 작업은 비용이 많이 들기 때문에 최대한 적게 원합니다.
따라서 상한이 20 개의 항목 인 경우 초기 길이가 20 인 배열을 만드는 것이 기본값 인 15를 사용하는 것보다 낫습니다. 그런 다음 15*2 = 30
확장주기를 낭비하면서 크기를 조정하고 20 만 사용하십시오.
PS-AmitG가 말했듯이 확장 요소는 구현에 따라 다릅니다 (이 경우 (oldCapacity * 3)/2 + 1
)
int newCapacity = (oldCapacity * 3)/2 + 1;
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를 만드는 것이 좋습니다.
private static final int DEFAULT_CAPACITY = 10
나는 실제로 2 개월 전에 주제에 대한 블로그 게시물 을 썼습니다 . 이 기사는 C #에 대한 List<T>
것이지만 Java의 ArrayList
구현은 매우 유사합니다. ArrayList
동적 배열을 사용하여 구현 되므로 필요에 따라 크기가 커집니다. 용량 생성자의 이유는 최적화를위한 것입니다.
이러한 크기 조정 작업 중 하나가 발생하면 ArrayList는 배열의 내용을 기존 배열의 용량의 두 배인 새 배열로 복사합니다. 이 작업은 O (n) 시간에 실행됩니다 .
다음은 ArrayList
크기가 어떻게 증가 하는지에 대한 예입니다 .
10
16
25
38
58
... 17 resizes ...
198578
297868
446803
670205
1005308
따라서 목록의 용량은 10
11 번째 항목이 추가 될 때로 증가 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 연산
Java는 위와 같으며에서 시작하여 10
각 크기를 조정 50% + 1
합니다. C #은 시작될 4
때마다 훨씬 더 적극적으로 증가하여 크기를 조정할 때마다 두 배가됩니다. 1000000
C # 사용 3097084
작업에 대한 위 의 추가 예제 .
예를 들어 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에 추가되면 용량이 자동으로 증가합니다. 성장 정책에 대한 세부 사항은 요소를 추가하는 것이 상각 된 상각 시간 비용이라는 사실을 넘어서 명시되어 있지 않다. "
분명히, 어떤 종류의 범위를 보유할지 모를 경우 크기를 설정하는 것은 좋은 생각이 아닙니다. 그러나 특정 범위를 염두에두고 초기 용량을 설정하면 메모리 효율성이 향상됩니다. .
이것은 모든 단일 객체에 대한 재 할당 노력을 피하기위한 것입니다.
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;
add
이미 내부적으로 일부 성장 공식을 사용합니다. 따라서 질문에 대답하지 않습니다.
int newCapacity = (oldCapacity * 3)/2 + 1;
ArrayList 클래스에 존재하는. 여전히 답이 없다고 생각하십니까?
ArrayList
. 상각 된 재 할당에서는 어떤 경우 에도 초기 용량에 대한 가치가 있습니다. 그리고 질문은 다음과 같습니다. 왜 초기 용량에 비표준 값을 사용합니까? 이 외에도 "줄 사이에서 읽는 것"은 기술적 인 답변에서 원하는 것이 아닙니다. ;-)
최적화라고 말하고 싶습니다. 초기 용량이없는 ArrayList는 ~ 10 개의 빈 행을 가지며 추가를 수행 할 때 확장됩니다.
trimToSize () 를 호출해야하는 항목 수와 정확히 일치하는 목록을 만들려면
에 대한 내 경험에 ArrayList
따르면 초기 용량을 제공하는 것은 재 할당 비용을 피하는 좋은 방법입니다. 그러나주의해야 할 점이 있습니다. 위에서 언급 한 모든 제안에 따르면 요소 수의 대략적인 추정이 알려진 경우에만 초기 용량을 제공해야합니다. 그러나 우리가 아무 생각없이 초기 용량을 제공하려고 할 때, 예약되고 사용되지 않은 메모리의 양은 목록이 필요한 수의 요소로 채워지면 결코 필요하지 않을 수 있으므로 낭비가 될 것입니다. 내가 말하고있는 것은, 우리는 처음에 실용적이며 용량을 할당 할 수 있으며 런타임에 필요한 최소 용량을 알 수있는 현명한 방법을 찾습니다. ArrayList는라는 메소드를 제공합니다 ensureCapacity(int minCapacity)
. 그러나 현명한 길을 찾았습니다 ...
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에서 테스트했습니다.