연관된 다수의 데이터를 관리하기 위한 수단으로 배열이 존재했습니다.
그런데 이 배열에는 몇가지 불편한 점이 있는데,
그 중 하나는 한 번 정해진 배열의 크기를 변경할 수 없다는 점입니다.
코드로 설명해보겠습니다.
String[] arrayObj = new String[2];
arrayObj[0] = "one";
arrayObj[1] = "two";
// arrayObj[2] = "three"; // 오류 발생 : ArrayIndexOutOfBoundsException
for(int i=0; i<arrayObj.length; i++){
System.out.println("arrayObj = " + arrayObj[i]);
}
배열의 크기를 지정하는 것은 정해둔 크기 이상의 값을 저장할 수 없다는 한계가 있습니다.
실행해보아도 오류가 발생합니다.
이에 반해 Collections Framework의 ArrayList를 사용하게 되면, 크기를 미리 지정하지 않고도 원하는 만큼 데이터를 저장하고, 수정할 수 있습니다.
다만, 사용법에 있어 차이점이 조금 있습니다.
ArrayList<String> arrayList = new ArrayList<String>(); // collections 하위 클래스, 배열 크기 지정 필요 x
arrayList.add("one"); // add() 메서드는 데이터 타입이 Object 여야 함 -> 모든 데이터 타입 허용하기 위해
arrayList.add("two");
arrayList.add("three");
for(int i=0; i<arrayList.size(); i++){ // length() == size()
String value = arrayList.get(i); // 데이터 타입 지정의 제약 사항 -> 컬렉션 프레임워크에서 지네릭 채택한 이유 (데이터 타입 지정의 중복은 번거롭기 때문)
System.out.println("value = " + value);
}
ArrayList는 컬렉션즈 프레임워크에서 자주 사용되는 여러 요소 중 하나입니다.
ArrayList 같은 장점이 있는 친구들이 아직 더 있다니 !!
부푼 기대를 안고 기쁜 마음으로 만나러 가보겠습니다.
Collections Framework에는 Collection, Map 이라는 최상위 카테고리가 있고, 그 아래에는 성격과 기능에 따라서 인터페이스와 다양한 자식클래스들로 구성되어있습니다.
이전에 살펴봤던 ArrayList는 List라는 인터페이스의 하위 클래스로 분류되고 있으며, 동시에 비슷한 성격을 가진 다른 구성요소로는 Vector, LinkedList가 있다는 것을 살펴볼 수 있습니다. 다른 클래스를 통해 접근해도 마찬가지로 상관관계를 살펴볼 수 있습니다.
주어진 상황에 따라 적절한 기능을 사용할 수 있어야 하며, 그럴 수 있도록 다양하면서도 응집도를 갖춘 모습으로 설계되었습니다.
예를 들어
적절히 선택할 수 있어야 합니다.
컬렉션즈 프레임워크는 유용하지만, 각각의 카테고리에 따라서 데이터를 저장하고 관리하는 방식이 다릅니다. 그렇기에 각각의 특성을 자세히 살펴보는 시간을 가질 필요가 있습니다.
코드를 통해서 Set과 List에 대해 이해하는 시간을 가져보겠습니다.
HashSet<Integer> A = new HashSet<Integer>(); // util패키지 통해 HashSet 객체 생성, 지네릭으로 Integer 타입 지정
A.add(1);
A.add(2);
A.add(3);
Iterator a = (Iterator) A.iterator();
while (a.hasNext()){
System.out.println("a.next() = " + a.next());
}
System.out.println("=================");
HashSet<Integer> B = new HashSet<Integer>();
B.add(1);
B.add(2);
B.add(2);
B.add(2);
B.add(2); // HashSet은 중복을 허용하지 않음
B.add(3);
Iterator b = (Iterator) B.iterator();
while (b.hasNext()){
System.out.println("b.next() = " + b.next());
}
System.out.println("=================");
ArrayList<Integer> C = new ArrayList<Integer>();
C.add(1);
C.add(2);
C.add(2);
C.add(2);
C.add(2); // ArrayList는 중복을 허용
C.add(3);
Iterator c = (Iterator) C.iterator();
while (c.hasNext()){
System.out.println("c.next() = " + c.next());
}
결과
a.next() = 1
a.next() = 2
a.next() = 3
=================
b.next() = 1
b.next() = 2
b.next() = 3
=================
c.next() = 1
c.next() = 2
c.next() = 2
c.next() = 2
c.next() = 2
c.next() = 3
HashSet은 중복된 데이터를 허용하지 않고 고유한 값만을 저장하고 있는 반면,
ArrayList는 중복 여부에 상관 없이 모든 데이터를 저장합니다.
이것은 List와 Set의 가장 중요한 차이점이기도 하며 (HashSet, LinkedHashSet, TreeSet), (ArrayList, Vector, LinkedList)와 같은 하위 클래스들은 모두 같은 영향을 받으면서 미세한 차이가 있습니다.
Set에 대해 조금 더 알아보면 집합이라는 의미를 알아볼 수 있습니다. 수학에서 집합은 교집합(intersect), 차집합(difference), 합집합(union)과 같은 연산을 할 수 있는데, Set도 마찬가지입니다.
코드로 살펴보겠습니다.
HashSet<Integer> A = new HashSet<Integer>();
A.add(1);
A.add(2);
A.add(3);
HashSet<Integer> B = new HashSet<Integer>();
B.add(3);
B.add(4);
B.add(5);
HashSet<Integer> C = new HashSet<Integer>();
C.add(1);
C.add(2);
System.out.println("A.containsAll(B) = " + A.containsAll(B)); // false
System.out.println("A.containsAll(C) = " + A.containsAll(C)); // true
결과
A.containsAll(B) = false
A.containsAll(C) = true
다른 집합들도 알아보기 위해 B 집합의 데이터를 A로 넘겨보겠습니다.
A.addAll(B);
Iterator i = A.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
결과
1
2
3
4
5
- addAll(): 지정한 컬렉션의 데이터를 모두 추가합니다.
- A와 B의 집합이 합집합이 되었습니다.
A.retainAll(B);
결과
3
A.removeAll(B);
결과
1
2
순서대로 합집합, 교집합, 차집합에 대해 알아보았습니다.
이전에 Set과 List의 차이는 중복 허용의 여부와 순서 보장이었는데,
Set이 중복을 허용하지 않는 이유는 집합을 활용하기 위해선 고유한 데이터가 필요했기 때문이라고 생각하게 되었습니다.
또한 Set은 집합의 특성상 데이터의 순서가 보장되지 않지만,
List는 입력과 반환 모두 순서가 보장된다는 것을 알 수 있습니다.
HashSet<Integer> A = new HashSet<Integer>();
A.add(1);
A.add(2);
A.add(3);
HashSet<Integer> B = new HashSet<Integer>();
B.add(3);
B.add(4);
B.add(5);
HashSet<Integer> C = new HashSet<Integer>();
C.add(1);
C.add(2);
System.out.println("A.containsAll(B) = " + A.containsAll(B)); // false
System.out.println("A.containsAll(C) = " + A.containsAll(C)); // true
//A.addAll(B); 합집합
//A.retainAll(B); 교집합
A.removeAll(B); // 차집합
Iterator i = A.iterator();
while (i.hasNext()){
System.out.println(i.next());
}
List 인터페이스를 살펴보면 대부분 index, 즉 순서와 연관되어 있는 인자를 전달받는 것을 알 수 있습니다. 그렇기 때문에 순서가 보장되지 않는 Set 인터페이스와는 보유한 메서드가 다른 것을 알 수 있습니다.
이것은 단순히 알고 넘어가는 것이 아니라,
데이터 타입을 지정할 때 바람직한 방향을 추구해야 함을 의미합니다.
Collection 인터페이스의 메서드만을 사용한다면 Collection으로 데이터 타입을 지정하는 것이 나중에 필요에 따라 변경할 수 있기 때문에 바람직합니다.
만약 특정 인터페이스에만 존재하는 메서드를 사용한다면, 예를 들어 데이터 타입을 List로 지정해주어야 하위 클래스를 통해 재사용성을 최대화 하거나 코드의 변경을 최소화 할 수 있습니다.
ArrayList<String, Integer> object = new ArrayList<String, Integer>();
보다는
List<String, Integer> object = new ArrayList<String, Integer>();
// ArrayList -> LinkedList로만 변경하면 됨
이런 코드가 변경에 더 용이합니다.
이번엔 HashMap을 통해서 Map에 대해 알아보겠습니다.
List는 순서가 보장되고, 중복이 허용되는 컨테이너였습니다. get()을 통해 원하는 데이터를 조회할 수 있었습니다.
Set은 순서가 보장되지 않고, 데이터가 중복되지 않는 집합 형태의 컨테이너 였습니다.
이번에 살펴볼 Map은 하나의 값만 저장했었던 것과 달리, "key"와 "value"라는 두개의 저장공간을 가집니다.
서로 다른 데이터타입을 지정할 수 있으며,
key를 조회하면 value에 저장된 값을 얻을 수 있습니다.
key는 중복을 허용하지 않는 고유값이며, value는 중복이 가능합니다. 만약 key 데이터를 중복 저장하려고 했다면,
value의 값은 마지막에 저장된 순서대로 값을 호출합니다.
코드를 통해 살펴보겠습니다.
HashMap<String, Integer> a = new HashMap<String, Integer>();
a.put("one", 1);
a.put("two", 2);
a.put("three", 3);
a.put("four", 4);
System.out.println(a.get("one"));
System.out.println(a.get("two"));
System.out.println(a.get("three"));
결과
1
2
3
HashMap 컨테이너의 객체를 통해 key와 value를 조회하고 호출할 수 있습니다.
HashMap을 활용할 수 있는 메서드를 만들어 보겠습니다.
static void iteratorUsingForEach(HashMap map){
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
static void iteratorUsingIterator(HashMap map){
Set<Map.Entry<String, Integer>> entries = map.entrySet();
Iterator<Map.Entry<String, Integer>> i = entries.iterator();
while(i.hasNext()){
Map.Entry<String, Integer> entry = i.next();
System.out.println(entry.getKey()+" : "+entry.getValue());
}
}
Map에는 iterator() 메서드가 없기 때문에
(the iterator's own {@code remove} operation), the results of)
Map의 데이터로 반복문을 사용하고 싶을 땐 entrySet을 통해 구현할 수 있습니다.
iteratorUsingForEach(a);
iteratorUsingIterator(a);
결과
one : 1
two : 2
three : 3
four : 4
one : 1
two : 2
three : 3
컬렉션을 사용하는 이유 중의 하나는 정렬과 같은 데이터와 관련된 작업을 하기 위해서이기도 합니다. 이번엔 패키지 java.util 내에 있는 Collections라는 클래스를 사용하는 방법을 알아보겠습니다.
class Computer {
int serial;
String owner;
public Computer(int serial, String owner) {
this.serial = serial;
this.owner = owner;
}
public String toString(){
return serial + " " + owner;
}
}
public class CollectionDemo{
public static void main(String[] args) {
List<Computer> computers = new ArrayList<Computer>();
computers.add(new Computer(8082, "h2-console"));
computers.add(new Computer(404, "NotFound"));
computers.add(new Computer(8080, "localhost"));
Iterator i = computers.iterator();
while(i.hasNext()){
System.out.println(i.next());
}
}
}
결과
8082 h2-console
404 NotFound
8080 localhost
ArrayList를 통해 순서대로 데이터를 추가해서 조회해봤습니다.
만약 출력된 결과를 숫자의 크기 순서로 정렬하고 싶다면 어떻게 해야 할까요?
Comparable 인터페이스를 활용해보겠습니다.
class Computer implements Comparable{
int serial;
String owner;
public Computer(int serial, String owner) {
this.serial = serial;
this.owner = owner;
}
public int compareTo(Object o){
return this.serial - ((Computer)o).serial;
}
public String toString(){
return serial + " " + owner;
}
}
public class CollectionDemo{
public static void main(String[] args) {
List<Computer> computers = new ArrayList<Computer>();
computers.add(new Computer(8082, "h2-console"));
computers.add(new Computer(404, "NotFound"));
computers.add(new Computer(8080, "localhost"));
Iterator i = computers.iterator();
System.out.println("before");
while(i.hasNext()){
System.out.println(i.next());
}
Collections.sort(computers);
System.out.println("\nafter");
i = computers.iterator();
while(i.hasNext()){
System.out.println(i.next());
}
}
}
sort()의 내부 구조를 살펴 보면 다음과 같습니다.
@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
sort()는 List 형식의 데이터만을 전달받습니다.
동시에 지정한 데이터타입을 살펴보면 Comparable을 상속받는다고 게시되어 있습니다.
즉, 방금 조회한 객체들은 모두 Comparable 인터페이스를 상속 받습니다.
Comparable 인터페이스의 내부구조는 다음과 같습니다.
public int compareTo(T o);
Computer class 에서 compareTo() 메서드를 강제하게 된 까닭은 Comparable 인터페이스의 메서드를 상속받았기 때문입니다.
compareTo()는 리턴값은 int, 인자는 Object 타입으로 지정된 걸 살펴볼 수 있는데 뺄셈을 통해 비교하여 순서를 정합니다.
정리하면, sort()를 실행하면 내부적으로 compareTo()를 실행하고
그 결과에 따라 객체의 선후 관계를 컴파일러가 판별하게 되어 정렬이 가능하게 됩니다.
결과
before
8082 h2-console
404 NotFound
8080 localhost
after
404 NotFound
8080 localhost
8082 h2-console
컬렉션은 데이터를 효과적이고 빠르게 활용할 수 있도록 선배 개발자들이 자료구조와 알고리즘을 통해 이룩한 성취입니다. 충분한 이해가 필요한 영역이지만, 언어 차원에서 이룩한 성취들을 잘 추상화 시켜놓은 것이 컬렉션즈 프레임워크 입니다.
많은 시간과 노력, 자본, 연구 등의 총집합이라고 할 수 있으며 아주 성능이 좋습니다.
이러한 프레임워크를 필요에 따라 효과적으로 사용하기 위해서는 알고리즘과 자료구조를 공부하여판별하는 능력을 훈련할 필요가 있습니다.
컬렉션은 데이터를 효과적이고 빠르게 활용할 수 있도록 선배 개발자들이 자료구조와 알고리즘을 통해 이룩한 성취입니다. 충분한 이해가 필요한 영역이지만, 언어 차원에서 이룩한 성취들을 잘 추상화 시켜놓은 것이 컬렉션즈 프레임워크 입니다.
많은 시간과 노력, 자본, 연구 등의 총집합이라고 할 수 있으며 아주 성능이 좋습니다.
이러한 프레임워크를 필요에 따라 효과적으로 사용하기 위해서는 알고리즘과 자료구조를 공부하여판별하는 능력을 훈련할 필요가 있습니다.
이 내용을 보니
young man, you don't need understand. you just get to used to them
이 말이 떠오르네
그 뜻인가