-
TIL #19 - 제네릭, Collection, ArrayList, Set, Map프로그래밍/TIL(국비과정) 2020. 4. 21. 20:56
# 제네릭 (Generic)
제네릭은 동적으로 타입을 결정하지 않고, 컴파일 시 타입이 결정되는 것이다.
class GenericTest<T>{ private T a; //a가 어떤 타입이 될지 모르기 때문에 제네릭 <T>로 public void setA(T a){ this.a = a; } public T getA(){ return a; } }
위와 같이 클래스 생성시 타입을 명시해준다.
총 4개의 제네릭 타입이 있다.
-<T> : Type 데이터 형
-<E> : Element
-<K> : Key
-<V> : Value
class GenericMain{ public static void main(String[] args) { GenericTest<String> aa = new GenericTest<String>(); // 실행 때 타입 선택 (String 타입) aa.setA("양아무개"); //aa.setA(25); System.out.println("이름 : " + aa.getA()); // 타입은 객체형만 묻는다(int 안됨) GenericTest<Integer> bb = new GenericTest<Integer>(); bb.setA(30); System.out.println("나이 : " + bb.getA()); } }
위와 같이 <T>로 설정해 놓으면
실행 때 타입을 선택할 수 있다.
타입은 객체형만 묻기 때문에 int와 같은 기본형은 사용할 수 없다.
# Collection
Collection은 객체를 담아주는 저장 창고와 같다.
앞서 배운 배열도 역시 그렇다.
하지만 배열의 경우엔 고정적이기 때문에 객체를 얼마나 넣어야할지 모르는 상황에서는 좋은 방법이 아니다.
이런 배열의 단점을 보완하준 것이 Collection 이다.
Collection은 객체 타입과 상관없이 저장이 가능하고 크기 조절도 가능하다.
인터페이스분류 구현클래스 Collection List ArrayList, Vector, LinkedList Set HashSet, TreeSet Map HashMap, Hashtable, TreeMap, Properties import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; class CollectionTest{ public static void main(String[] args) { //Collection coll = new Collection(){
Collection은 인터페이스라 생성이 안되므로 익명을 붙여서 메소드를 호출해와야한다.
그런데 지금은 Collections 중 ArrayList 만 불러오도록 한다.
ArrayList는 일반 클래스 이므로 익명을 붙여서 오버라이드를 하거나, implements를 해서 오버라이드를 해 줄 필요가 없다.
Collection coll = new ArrayList(); coll.add("호랑이"); coll.add("사자"); coll.add("호랑이"); coll.add("기린"); coll.add("코끼리");
ArrayList는 중복을 허용하고 순서를 지킨다.
그런데 여기까지 하면 ArrayList 안에 어떤 데이터가 들었는지 보이지 않는다는 메세지가 뜬다.
그렇기 때문에 제네릭을 사용해 String 타입인 것을 명시해 준다.
Collection<String> coll = new ArrayList<String>();
ArrayList의 크기를 확인하기 위해서는 size()를 사용한다.
System.out.println("크기 = " + coll.size());
그런다음 이터레이터를 선언한다.
Iterator it = coll.iterator();
Collection 안에 Iterator 지정자를 생성해준 것이다.
Iterator는 Vector, ArrayList, LinkedList 가 상속받는 인터페이스로 컬렉션으로부터 정보를 얻어내는 인터페이스이다.
Iterator의 method는 아래의 3개가 있다.
hasNext() : 현재 항목에 값이 있는지 확인. 있으면 true/ 없으면 false를 반환
next() : 항목에 값을 꺼내고 다음으로 이동
remove() : next()로 읽어온 요소를 삭제
그럼 Iterator로 ArrayList에 넣은 값들을 꺼내본다.
while문을 사용해 리스트 내에 값이 없을 때까지 꺼내본다.
while(it.hasNext()){ System.out.println(it.next()); }
우리는 위에서 호랑이라는 값을 2개 넣었다.
그럼에도 2개 출력된다.
또한 넣은 순서대로 출력이 되는 것을 확인할 수 있다.
먼저 들어간 것이 먼저 나오는 FIFO(First in First out) 인 것이다.
다음은 <String> 과 같은 제네릭이 없는 버전의 ArrayList에 값을 넣고 출력해본다.
아래의 ArrayList는 제네릭 타입이 명시되어 있지 않기 때문에 문자열과 숫자가 모두 잘 출력이 되는 것을 확인할 수 있다.
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; class CollectionTest2{ public static void main(String[] args) { ArrayList list = new ArrayList(); list.add("호랑이"); list.add("사자"); list.add("호랑이"); list.add(25); list.add(43.8); list.add("기린"); list.add("코끼리"); for(int i = 0; i < list.size(); i++){ System.out.println(list.get(i)); } } }
그런데 제네릭이 없으니 컴파일 과정에서 아래와 같은 메세지가 뜬다. 아래의 메세지는 오류가 아니고 타입이 없어 문제가 생길 지도 모른다는 걱정어린 메시지(?) 이다.
이런 메세지가 코드를 작동시키는데에는 큰 지장은 없지만, 거슬릴 수 있다.
그런 경우엔 main 메서드 위에 어노테이션을 준다.
@SuppressWarnings("unchecked") public static void main(String[] args) {
위의 어노테이션은 억제의 의미로 메세지가 나오지 않게 막는다.
실질적인 해결은 아니다. 실질적인 해결은 다시 제네릭 타입을 붙여주는 것이다.
하지만 위의 어노테이션을 붙이면 걱정어린 메시지는 나오지 않는다.
자바가 제공하는 어노테이션은 아래의 세개가 있다.
1. @Override
2. @Deprecated
3. @SuppressWarnings(옵션)
SuppressWarnings의 경우에는 많은 옵션들이 있으니까 필요에 따라 찾아 쓰면 될 것같다.
#Set
set은 list와 다르게 순서를 지키지 않고 저장하고 중복을 허용하지 않는다.
set으로 값을 저장해 보자.
import java.util.HashSet; import java.util.Iterator; import java.util.Set; class SetTest { public static void main(String[] args) { Set<String> set = new HashSet<String>(); set.add("호랑이"); set.add("사자"); set.add("호랑이"); set.add("기린"); set.add("코끼리");
Iterator를 통해 set을 불러온다.
Iterator it = set.iterator(); while(it.hasNext()){ System.out.println(it.next()); } } }
출력된 결과를 보면 입력된 값들의 순서와 전혀 다르고 중복값이 던 호랑이 하나가 없어진 것을 확인 할 수 있다.
#Map
Map은 키와 값이 함께 저장되는 형태이다.
Map<Key, Value>
Map은 value의 중복 허용은 하지만 Key에 대해선 중복을 허용하지 않는다.
아래의 코드를 보면 알 수 있다.
import java.util.Map; import java.util.HashMap; import java.util.Scanner; class MapTest { public static void main(String[] args) { // Map<Key,Value> Map<String,String> map = new HashMap<String,String>(); map.put("book101", "백설공주"); map.put("book201", "인어공주"); map.put("book102", "백설공주"); map.put("book301", "피오나"); map.put("book101", "엄지공주");
Map 인터페이스에 각각 String key와 value를 받는다.
그런데 book101과 백설공주 가 각각 중복이다.
이렇게 되면 결과가 어떻게 나올까.
먼저 book101을 출력해본다.
엄지공주가 나온다.
book101은 원래 백설공주였는데 마지막에 나온 엄지공주가 그 value 값을 덮어버렸다.
Map은 key에 대한 중복을 허용하지 않기 때문에 마지막에 나온 값이 먼저 나온 값을 덮어서 출력된다.
+ Map은 add가 아닌 put 이다.
만약 원하는 키 값의 데이터를 가지고 오고 싶은 경우엔 아래와 같다.
Scanner scan = new Scanner(System.in); System.out.print("코드 입력 : "); String key = scan.next(); if(!map.containsKey(key)){ System.out.println("없는 key값입니다"); }else { System.out.println(map.get(key)); }
key값을 입력받고
만약 map에 해당되는 key값이 없으면 없다는 메시지를 띄우고
있다면 key에 해당하는 값을 꺼내온다 (get())
#Vector
벡터는 기본용량 10개를 가지고 있고, 10개씩 증가한다.
만약 항목이 단 1개라도 넘게 되면 용량이 10 증가한다.
import java.util.Vector; class VectorTest { public static void main(String[] args){ Vector<String> v = new Vector<String>(); // 기본 용량 10개 , 10개씩 증가 System.out.println("벡터 크기 = " + v.size()); // 0 System.out.println("벡터 용량 = " + v.capacity()); // 기본 용량 10개 System.out.println(); System.out.println("항목 추가"); for(int i = 1; i <= 10; i++){ v.add(i +""); } System.out.println("벡터 크기 = " + v.size()); // 10 System.out.println("벡터 용량 = " + v.capacity()); // 10 : 현재값을 초과하지 않기 때문에 10개 그대로 System.out.println(); // 크기가 용량을 초과했을 때 // 용량은 10이 늘어나 총 20개가 된다. System.out.println("항목 초과"); v.addElement(5+""); // 중복허용, 순서 System.out.println("벡터 크기 = " + v.size()); // 11 System.out.println("벡터 용량 = " + v.capacity()); // 20 System.out.println(); // 벡터의 주소값은? System.out.println(v); // 들어간 순서대로 나오고 중복을 허용하는 것을 확인 System.out.println("항목 제거"); v.remove(10); // 인덱스 10번째 값 제거 System.out.println(v); } }
벡터는 중복을 허용한다.
벡터의 주소값을 출력하면 입력된 값들을 보여준다.
# Stack & Queue
Stack은 First in Last out으로 가장 먼저 들어간 것이 가장 나중에 나오는 구조이다.
import java.util.Stack; // First in last out class StackTest { public static void main(String[] args) { String[] groupA = {"A", "B", "C", "D"}; Stack<String> stack = new Stack<String>(); for(int i = 0; i < groupA.length; i++){ stack.push(groupA[i]); // 하나씩 스택에 넣는다 } while(!stack.isEmpty()){ // 스택이 비지 않을 동안 System.out.println(stack.pop()); // 스택을 꺼낸다. } } }
A, B, C, D의 순서대로 들어가서 나올 때는 D, C, B, A의 순서대로 나오게 된다.
Queue는 First in First out으로 가장 먼저 들어간 것이 가장 먼저 나오는 구조이다.
import java.util.LinkedList; // First in First out class QueueTest { public static void main(String[] args) { String[] item = {"A", "B", "C"}; LinkedList<String> q = new LinkedList<String>(); for(String n : item){ // item을 n에 넣고 q.offer(n); // 요소 추가 // 그것을 q에다 넣는다. } System.out.println("q의 크기 : " + q.size() + "\n"); String data = ""; while((data = q.poll()) != null){ // data를 꺼내온다. null 이 아니면 (데이터가 있으면) out.println(data + "삭제!"); // 삭제한다 out.println("q의 크기 : " + q.size() + "\n"); // 총 큐의 크기를 출력 }// data가 빌 때 까지 while 문 계속 } }
Queue의 경우 먼저 들어간 것이 먼저 나온다.
A, B, C, 의 순서대로 들어가서 A, B, C의 순서대로 나온다.
#Comparable/ Comparator 인터페이스
toString()은 Obejct의 메소드로 객체의 주소값을 보여준다.
만약 toString()을 했을 때 나는 주소값이 아닌 내가 입력한 이름과 나이 값이 출력되길 원하면 아래와 같이 오버라이드 해주면 된다.
PersonDTO.java
class PersonDTO{ private String name; private int age; // 생성자로 데이터 받기 public PersonDTO(String name, int age){ this.name = name; this.age = age; } public String getName(){ return name; } public int getAge(){ return age; } @Override public String toString(){ return "이름 = " + name + "\t 나이 ="+ age; } }
DTO 셋팅이 끝나면 Main 클래스로가 값을 입력하고 DTO를 불러 값을 출력해본다.
import java.util.ArrayList; class PersonMain { public ArrayList<PersonDTO> init(){ PersonDTO aa = new PersonDTO("홍길동", 25); PersonDTO bb = new PersonDTO("또치", 40); PersonDTO cc = new PersonDTO("도우너", 30); ArrayList<PersonDTO> list = new ArrayList<PersonDTO>(); list.add(aa); list.add(bb); list.add(cc); return list; } public static void main(String[] args) { PersonMain main = new PersonMain();// 호출, return 값 가지고 옴 ArrayList<PersonDTO> list = main.init(); // 호출 for(int i = 0; i < list.size(); i++){ System.out.println(list.get(i)); } // 확장형 for문 for(PersonDTO dto : list){ // 리스트가 왼쪽으로 어떤 타입의 데이터를 주는지 생각한다. System.out.println(dto); } } }
여기서 잠깐 ArrayList에 대해 보자.
PersonDTO를 불러와 각각 세개의 객체를 만들었다.
각 값에는 이름과 나이가 각자 다른 주소값으로 담겨있다.
이 이후에 할테지만 우리는 이 세 객체를 이름 순, 나이 순으로 정렬(sort)을 할 것이다.
그런데, 이름 순으로 정렬을 할 때 40살의 또치가 3번째로 위치할 때 또치의 나이 40은 또치를 따라가지 않는다.
각자 다른 필드의 값이기 때문이다. 그렇기 때문에 객체로 둔 지금의 상태에서 정렬을 한다면 이름과 나이가 각자 분리되어 정렬이 될 수 있다.
따라서 ArrayList를 만들어 list 안에 각각의 객체를 넣어주었다.
list.add(aa); list.add(bb); list.add(cc);
여기서 주의할 점은 list에는 객체의 값이 담긴 것이 아니라 주소값이 담긴 것이다.
또한 list는 담긴 객체의 주소값과는 별개로 자신만의 주소도 또 가지고 있다.
그 주소값을 return 해주는 것이다.
자바에서는 return aa, bb, cc; 가 불가능하기 때문에 이렇게 list를 만들어 각각의 주소값을 만들어준 후
그 주소값이 담긴 list를 main 메서드에서 다시 한 번 더 또 다른 list로 하여금 불러주어야한다.
PersonMain main = new PersonMain(); ArrayList<PersonDTO> list = main.init();
한 번에 세개를 돌려줄 수 없기 때문에 세개를 ArrayList 하나로 묶어서 이리저리 주고받는 것이다.
앞서 말했듯이 main 메서드의 list는 main 메서드 밖의 aa, bb, cc 객체의 주소값을 담은 list와는 다른 list이다.
main.init()의 주소를 받아 보관해주는 또 다른 리스트이다.
이제 앞에서 스포일러 했듯이, 나이를 정렬을 해주자.
PersonSort.java
그 전에 잠깐,
우리가 배열을 정렬할 떄는 어떻게 했을까?
import java.util.Arrays; class PersonSort{ public static void main(String[] args){ String[] ar = {"orange", "apple", "banana", "pear", "peach", "applemango"}; System.out.print("정렬 전 = "); for(String data : ar){ System.out.print(data + " "); } System.out.println(); Arrays.sort(ar); // sort 시키기 System.out.print("정렬 후 = "); for(String data : ar){ System.out.print(data + " "); } } }
배열을 정렬할 때는 Arrays.sort() 메소드를 통해 정렬시킬 수 있다.
지금은 Collections 중 하나인 ArrayList이기 때문에 Array가 아닌
Collections.sort() 로 정렬해준다.
일반적으로 int, char, double 같은 타입의 배열이라면 위의 Collections.sort() 로 정렬을 해준다.
import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; class PersonSort{ public static void main(String[] args){ System.out.println("PersonDTO를 나이 순서로 정렬"); PersonDTO aa = new PersonDTO("홍길동", 25); PersonDTO bb = new PersonDTO("또치", 40); PersonDTO cc = new PersonDTO("도우너", 30); ArrayList<PersonDTO> list = new ArrayList<PersonDTO>(); list.add(aa); list.add(bb); list.add(cc); // 리스트 안에 담아준다. System.out.println("정렬 전 : " + list); // 클래스명 @ 16진수 주소값 // list는 주소값이 안나온다. 항목을 보여준다. Collections.sort(list); System.out.println("정렬 후 : " + list); } }
그런데 지금은 나이 기준이지만, 나이 기준이 아닌 이름 기준으로 정렬 기준점을 바꾸려고 할 땐 어떻게 해야할까.
java.util에 Comparator 라는 인터페이스가 있다.
Comparator는 기본 정렬 기준 외에 다른 기준으로 정렬하고자할 때 사용한다.
위의 클래스에서 이름 기준으로 기준을 바꿔 보자.
System.out.println("PersonDTO를 이름 순서로 정렬"); Comparator<PersonDTO> com = new Comparator<PersonDTO>(){ @Override public int compare(PersonDTO o1, PersonDTO o2){ return o1.getName().compareTo(o2.getName()); // -1 0 1 오름차순 } }; // 새로운 기준을 세울 때 Comparator Collections.sort(list, com); // list 만 언급시 나이로 정렬이 된다. // com도 함께 넣어 com의 기준으로 정렬이 되도록 한다. System.out.println("정렬 후 = " + list); } }
Comparator을 선언하고 Comparator 안에 넣어줄 데이터 타입을 제너릭으로 선언해준다.
그런 후 compare 메소드를 Override 한다.
compare 메소드는 비교기준자와 비교대상자를 매개변수로 들어 있는데
비교대상자가 더 크면 -1, 둘이 같다면 0, 비교대상자가 더 작다면 1을 반환시켜준다.
위의 return 경우는 오름차순의 경우이고 만약 내림차순을 하고 싶다면 전체에 -1을 곱해준다.
return o1.getName().compareTo(o2.getName())*-1;
PersonDTO.java에도 Comparable을 적용시켜준다.
Comparable을 implements 해주고 제네릭으로 누구랑 비교해줄 것인지 적는다.
class PersonDTO implements Comparable<PersonDTO>{ // 1인분의 데이터를 가지고 있는 DTO // 가장 작은 단위의 클래스 private String name; private int age; // 생성자로 데이터 받기 public PersonDTO(String name, int age){ this.name = name; this.age = age; } public String getName(){ return name; } public int getAge(){ return age; } @Override public String toString(){ return "이름 = " + name + "\t 나이 ="+ age; } // 나이 정렬 @Override public int compareTo(PersonDTO o){ return this.age < o.age ? 1 : -1; // 내림차순 } // 위의 기준이 맘에 안들어 다른 기준으로 바꾸고 싶을 때 // Comparator }
또 위의 PersonSort.java 에서 사용했던 compareTo 메소드도 나이기준으로 결과가 나오도록 오버라이드 해준다.
'프로그래밍 > TIL(국비과정)' 카테고리의 다른 글
TIL #21 - SingleTon, Synchronized (0) 2020.04.24 TIL #20 - Comparable, Exception, 스레드 (0) 2020.04.22 TIL #18 - 계산기 프로그램 (0) 2020.04.20 TIL #18 - 중첩클래스, inner class , 익명클래스 (0) 2020.04.20 TIL #17 - 추상, enum, interface (0) 2020.04.18