본문 바로가기

Java

Java Collection, List, ArrayList

Collection 등장 이유

  • 배열은 선언과 동시에 배열의 크기가 초기화 되고 이후에 변경이 불가능하다.
  • 배열은 같은 기본형 데이터 타입의 자료형만 저장할 수 있다.
  • 배열에서 많이 사용하는 것(길이, 탐색 등)을 매번 구현해야 한다.
  • 배열을 사용하며 많이 사용하는 자료구조(스택, 큐 등)을 매번 구현해야 한다.

List

배열 대신 List를 사용하는 이유

  배열 List
크기 고정으로 크기 할당 크기를 고정할 필요 없이 자동으로 늘어남
삽입/삭제 속도 느림, 많은 작업이 필요할 수 있음 빠름, 인덱스 값으로 삽입, 삭제 가능
메서드 자주 사용하는 것도 구현해야 함 자주 사용하는 것은 메소드로 사용 가능(길이, 탐색, 필터링 등)
저장 타입 기본형 데이터 타입의 자료형만 저장 가능 객체(Object) 저장 가능
제네릭 타입 지원X 지원O, 컴파일 시 타입 안정성 보장
-> 코드의 신뢰도, 유지보수성 향상

길이를 구할 때를 생각해본다면

배열은 길이를 모를 때, 길이를 구하기 위해서는 배열 전체를 돌면서 하나하나 구해야 하므로 O(n)의 시간복잡도를 가진다.

 

List의 구현체 중 많이 쓰이는 구현체인 ArrayList의 길이를 구하기 위해서는 아래처럼 한다.

List<Integer> list = new ArrayList<>();
int size = list.size(); // ArrayList의 길이를 구함

 그리고 ArrayList size()의 코드를 살펴보면

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

	private int size;
    
	public int size() {
		return size;
	}

}

ArrayList 내부에 멤버 변수로 size를 가지고 있고,

길이를 구할 때 호출하는 size() 메소드는 이 멤버 변수를 바로 반환한다.

이처럼 ArrayList는 길이에 대한 정보를 멤버 변수로 가지고 있기 때문에 O(1)의 시간복잡도를 가진다.

 

따라서 길이를 구할 때 배열은 O(n), ArrayList는 O(1)의 시간복잡도를 가진다.

배열이나 List를 사용할 때 길이는 정말 자주 쓰이기 때문에 이런 곳에서 빠르게 처리하면 효율적인 코드를 작성할 수 있을 것이다.List의 구현체 중 많이 쓰이는 구현체인 ArrayList의 경우

또한 이미 구현되어 있는 메소드를 사용하는 게 직접 하나하나 구현하는 것보다 시간이 적게 들므로 생산성이 높아진다.

그렇기 때문에 배열보다 List를 많이 사용하는 것이다.

 

ArrayList

List<Object> list = new ArrayList<>();

보통 많이 쓰이는 형태는 이렇다.

왜 이렇게 업캐스팅하여 사용할까?

1. 구현은 ArrayList로 하면서 List에 있는 메소드도 사용이 가능하다.

2. List를 사용하고 싶은데, List는 인터페이스이므로 구현체인 ArrayList로 초기화한다.

직접 구현체를 만들어 사용하는 방법도 있는데, 단점은 아래처럼 메소드를 다 직접 구현해주어야 한다는 것이다.

List<String> a = new List<String>() {
    @Override
    public int size() { return 0; }

    @Override
    public boolean isEmpty() { return false; }
    
    ...
}

2개로 보이지만 생략된 것을 포함하면 수십개다.

3. Java의 다형성을 이용해 의존도를 낮추고 코드의 재사용성을 높인다.

List의 구현체는 ArrayList 말고도 LinkedList, Stack 등이 있다.

기획 변경 등의 이유로 처음에는 ArrayList로 구현했던 것을 Stack으로 변경해야 할 때를 가정해보자.

 

ArrayList<Object> list1 = new ArrayList<>(); // 1
List<Object> list2 = new ArrayList<>(); // 2

1의 경우와 2의 경우 중 Stack으로 수정하기 쉬운 것은 2의 경우이다.

 

ArrayList<Object> list1 = new ArrayList<>();

public ArrayList<Object2> doSometing(ArrayList<Object1> list1) {
	ArrayList<Object2> list2 = ...
    ...
    return list2;
}

위 코드는 (정말 대충 예시로 작성해봄...) 1의 경우처럼 선언 및 초기화를 했을 때의 예시 코드이다.

ArrayList에서 Stack으로 변경하기 위해서는

ArrayList로 선언했기 때문에 list1이 넘겨지거나 하는 메소드의 파라미터 타입 등 모든 부분을 ArrayList에서 Stack으로 수정해주어야 한다.

당장 이 짧은 코드만 해도 다섯 부분을 수정해주어야 한다.

 

List<Object> list2 = new ArrayList<>();

public List<Object2> doSometing(List<Object1> list1) {
	List<Object2> list2 = ...
    ...
    return list2;
}

위 코드는 같은 것을 2의 경우처럼 선언 및 초기화를 했을 때의 예시 코드이다.

이 경우 ArrayList에서 Stack으로 변경하기 위해서는 다른 부분을 수정할 필요 없이 초기화하는 부분만 new Stack<>();으로 해주면 된다.

 

이처럼 Java의 다형성을 이용해 의존도를 낮추고 코드의 재사용성을 높임으로써 생산성을 높일 수 있다.

 

 

참고

https://docs.oracle.com/javase/8/docs/api/java/util/package-summary.html

https://velog.io/@devharrypmw/Java-List-Set-Map%EC%9D%98-%ED%8A%B9%EC%A7%95%EA%B3%BC-%EC%B0%A8%EC%9D%B4%EC%A0%90

https://devhooney.tistory.com/187

https://bibi6666667.tistory.com/236

https://choichumji.tistory.com/165