[제너릭(Generic)]
자료구조를 이용할 때 주로 쓰이며, 타입을 잘 모르겠을때 사용한다. <>안에 쓰인 이름을 가지는 타입을 정의하며, 그 타입을 정의하는 사람은 이게 int인지, String인지 모른다. 사실상 우리가 직접 제너릭을 정의 할 일은 많이 없다. 하지만, 몇몇 API는 제너릭 타입으로 정의 되어 있어서, 사실 제너릭 정의보단 제너릭 선언, 사용이 더 중요하다. 런타임시에는 존재하지 않고, 컴파일 레벨에서 확정됨.
[p1]
package p1;
public class App {
public static void main(String[] args) {
// 제너릭
// 결론: 사실상 자료구조를 이용할때만 사용한다.
AAA<String> v1 = new AAA<String>();
// 제너릭으로 타입 T로 정의된 클래스 AAA를 String로 생성 후 v1에 주소값 대입.
// 이때 인스턴스는 참조타입으로만 생성 가능. wrapper 클래스의 필요가 이것.
AAA<Integer> v2 = new AAA<>();
// 그래서 int 대신 Integer를 씀. 근데, 뒤쪽 <>는 비워서 쓰는게 관례.
v2.a = 20;
// 이러면 박싱이 발생해서 연산 수행은 비추천. 아니면 언박싱 해서 쓰던가...
BBB<Integer, String> v3 = new BBB<>();
BBB<System, Math> v4 = new BBB<>();
// 참조타입이면 다 되니까 이렇게도 됨.
// 물론 이 둘은 인스턴스 생성이 막혀있기 때문에, 아무런 의미도 없다.
}
}
class AAA<T> {
// 타입을 정의하는것. 정의한 사람은 어떤건지는 모름.
// 명명법: 보통 대문자로 이뤄지먀, 보통 T(Type)를 많이 쓰지만, 그외에도 더 있긴 하다.
T a; // 이렇게 타입으로 쓸 수 있다.
String b;
int c; // 물론 T가 아닌 타입도 쓸 수 있다.
}
class BBB<X, Y> { // 물론 타입 여러개를 정의 할 수도 있다.
Y a;
X b;
X c;
}
[자료구조]
자료구조는 자료를 담는 구조이며, 여러가지가 포함 되어 있으며, 배열과 클래스 또한 자료 구조에 포함 될 수 있다. 기존의 문법만 가지고는 자료를 적절하게 다루는데 부족함이 있기 때문에, 알고리즘을 고안했다. 그걸 API로 구현하며, 그런 자료구조 API를 컬렉션 프레임워크(Collection Framework)라고 부른다.
여러 자료구조 알고리즘은 모두 장점과 단점(보통은 성능관련)을 다 가지고 있으며, 장단점 구분이 중요하다. 잘 모르더라도, 다 정리가 되어 있어서 찾아보면 된다. 앱 개발시 최소한으로 알아야 하는것으로는 ArryList, HashMap 등이 있다.「자료구조에는 List계열, Set계열, Map계열이 있으며, 각각 여러가지 알고리즘을 포함한다. 보통 List와 Map이 제일 중요하며, 몰라서는 안된다.」
List는 입력 순서가 있는것을 다룰 때 주로 사용하고 배열과 관련된 것이 많으며, 배열의 단점을 보완하기 위한 자료구조 알고리즘이라고 볼 수 있다.
배열의 장단점: 값의 접근 속도가 매우 빠르다는것이 매우 큰 장점이며, 단점으로는 선언 시 배열의 크기를 지정해주어야 하기 때문에 값을 계속 저장하다 보면 배열의 범위를 벗어나게 되는 단점이 존재. 그리고, 배열은 값을 삽입·삭제 할때 값을 복사해서 삽입·삭제 하기 때문에 속도가 느려지는 문제가 생긴다. LinkedList와 정반대라고 볼 수 있다.
값의 접근을 위해서는 get메서드를, 값의 삽입을 위해서는 add메서드를, 값의 삭제를 위해서는 remove 메서드를 사용한다.
- LinkedList: Node를 기반으로 작동하며, 삽입·삭제를 용이하게 하기 위해서 사용한다. 동작 방식으로는 값을 복사로 다루는게 아니라 배열에 값을 넣을때 Node를 사용한다.
Node는 하나의 저장공간을 말하며, 저장공간의 최소단위를 지칭하는것이다. Node에는 값이 저장된 주소값을 저장할 value, 다음 Node의 주소값을 가지게 될 next로 구성되며, LinkedList는 이러한 Node들을 계속해서 연결(Link)하는 방식으로 작동한다. 그렇기 때문에 값에 접근하기 위해서 배열을 하나씩 따라가서 값에 접근하기 때문에 get을 하는 속도가 느리다는 치명적인 단점이 있다. 그래서 보통 get을 할때 앞에서 뒤로 접근 하는것만이 아니라, 2중 LinkedList를 사용해서 앞뒤로 값에 접근한다. 그렇기때문에, 첫번째 값과 마지막 값을 다룰때, 혹은 그와 가까울수록 큰 장점을 보이지만, 중간에 있는 값에 접근할수록 단점이 부각된다.- ArrayList: 배열을 기반으로 작동하는 것이며, 배열의 장단점을 그대로 가져간다. StudentManagerV1에서 배열을 다룰때 사용했던 방법(중간에 값이 늘어날때마다 계속해서 배열을 2배 늘리는 방법)과 비슷하다. LinkedList와 달리, 배열을 기반으로 작동하기 때문에 첫번째, 혹은 그와 가까운 값을 다룰때 매우 불리하지만, LinkedList보다는 get의 속도가 빠르다.
[p2] - LinkedList API 직접 구현해보기.
package p2;
public class App {
public static void main(String[] args) {
// String[] nameList = new String[10];
LinkedList list = new LinkedList();
list.add(new StudentDto("민수"));
list.add(new StudentDto("준수"));
list.add(new StudentDto("희수"));
list.add(new StudentDto("철수"));
list.add(new StudentDto("영수"));
// 3번째로 들어간 값을 출력
StudentDto s1 = list.get(3);
System.out.println(s1.name);
// 2번째 값 지우고 2번째 값 출력하는거
list.remove(2);
StudentDto s2 = list.get(2);
System.out.println(s2.name);
// 2번에 새로운 값 삽입하는거.
list.add(new StudentDto("영희"), 2);
StudentDto s3 = list.get(2);
System.out.println(s3.name);
}
}
class StudentDto {
String name;
// API 구동 원리의 이해에 목적이 있기 때문에 정의는 다 하지 않음.
public StudentDto(String name) {
// 마찬가지로 테스트 용이라 public. setter, getter 안만들어서.
this.name = name;
}
}
////////// 아래 링크드 리스트 코드 //////////
class Node {
// class Node<T> 로 하면 제네릭으로 바꿀 수 있다. 이렇게 하면 Node를 사용하는것은 전부
// Node의 뒤에 <T>를 붙이거나 자료형을 T로 바꿔야 함. 노란색 밑줄 찾으면 됨.
StudentDto value;
Node next;
}
// API
class LinkedList {
private Node first = null;
public void add(StudentDto value) { // 앞의 노드를 뒤의 노드와 연결 시키는 것.
Node newnode = new Node(); // 배열 대신에 사용할 것.
newnode.value = value;
if(first == null) {
first = newnode;
return;
}
// 꼬리 찾기 (마지막은 항상 null이니까, 그 null인 부분 찾기)
Node tempNode = first;
while(true) {
if(tempNode.next == null) {
break;
}
tempNode = tempNode.next;
}
// 꼬리에 노드 연결
tempNode.next = newnode;
}
// 삭제
public void remove(int index) {
Node temp = first;
for(int i = 0; i < index; i++) {
temp = temp.next;
}
temp.next = temp.next.next;
}
// 삽입 - 내 생각엔 add 대신에 insert가 더 적절할 듯?
public void add(StudentDto value, int index) {
Node newNode = new Node();
newNode.value = value;
Node temp = first;
for(int i = 0; i < index; i++) {
temp = temp.next;
}
newNode.next = temp.next;
temp.next = newNode;
}
public StudentDto get(int index) {
Node temp = first;
for(int i = 0; i < index; i++) {
temp = temp.next;
}
return temp.value;
}
}
// 완벽한 코드 아님. 예외처리 해야 하고, first도 좀 더 다뤄야 함.
[p3] - LinkedList API 사용해보기
package p3;
import java.util.LinkedList;
public class App {
public static void main(String[] args) {
LinkedList<StudentDto> list = new LinkedList<>();
list.add(new StudentDto());
list.add(new StudentDto());
list.add(new StudentDto());
list.add(new StudentDto());
list.add(new StudentDto());
/* List<StudentDto> list = new ArrayList<>(); // ArrayList일땐 이렇게 씀.
list.add(new StudentDto());
list.add(new StudentDto());
list.add(new StudentDto());
list.add(new StudentDto());
list.add(new StudentDto()); */
list.remove(0);
// ArrayList일때는 0번을 지우는 일은 없어야 함. 배열 기반이기 때문에.
// 하지만, LinkedList일때는 아주 좋은 코드.
StudentDto s1 = list.get(2);
// 사실 get 쓰면 안됨. get을 쓸 일이 많으면 LinkedList 쓰면 안됨.
// ArrayList라면 좋은 코드다.
System.out.println(s1);
}
}
class StudentDto {
// ..
}
이 두개의 List를 기반으로 Stack, Queue, Deque를 구현한다. 즉, Stack, Queue는 LinkedList, ArrayList의 사용방법이라고 할 수 있다.
이 3개의 개념은, 중간의 값을 삽입·삭제할 수 없으며, 모두 다 첫번째 값과 마지막 값만을 다룬다. 중간의 값을 다루려고 하는 순간부터 그거는 Stack, Queue, Deque가 아닌, 그냥 List가 된다.
- Stack: 추상적인 자료구조이고, 벡터(Vector)을 상속받는다. 후입선출(LIFO) 방식이고, 후입선출은 아래에서 위로 쌓고 위에있는것만 가져올 수 있는 방식이다. 그래서 보통 그림으로 그릴때는 세로방향의 비커같은 박스로 표현하며, 순서가 있기 때문에 List 계열에 속한다. 주요 API로는 값을 넣는 push, 값을 뽑는 pop이 있는데, 여기서 pop은 remove(값을 뽑은 후 삭제)를 상속받고 있다. Stack은 순차적으로 삽입하고, 삭제할때는 앞의 값이 아닌 뒤의 값만 삭제하기 때문에 뒤에 있는 값을 다루기 쉽고, 접근속도가 빠른 ArrayList로 구현한다. 하지만, Stack은 추상적인 자료구조이기 때문에 구현이 안되는데 구현을 해버렸기 때문에 Java에서는 사용을 권장하지 않는다. - 대신, 후술 할 ArrayDeque를 사용한다
- Queue: Interface를 상속받으며, 선입선출(FIFO) 방식이다. 보통 그림으로 그리면 가로방향의 끝이 뚫려있는 파이프 같이 그린다. 얘도 순서가 있어서 List 계열에 속한다. 보통 주문 시스템 같은곳에 쓰이며, 주요 API로는 값을 넣는 offer, 값을 뽑아는 poll이 있다. 가장 먼저 들어온 값이 가장 먼저 나가는 구조라서 앞의 값을 주로 다루기 때문에, ArrayList가 아닌, LinkedList로 구현을 해야 한다. ArrayList로 하면 큰일 남.
- Deque: 어디서든 넣을 수 있고, 어디서든 값을 가져올 수 있다. 그림으로 그리면, Queue와 비슷하지만 Queue는 값을 앞에서만 넣고, 뺄때도 앞의 값만을 가져올 수 있는데, Deque는 값을 앞뒤에서 넣고 뺄 수 있다. Deque의 주요 API로는 값을 앞에서 넣는 addFirst, 뒤에서 넣는 addLast. 값을 앞에서 빼고 삭제하는 pollFirst, 뒤에서 빼고 지우는 pollLast, 값을 앞에서 빼고 삭제하지 않는 peakFirst, 뒤에서 빼고 삭제하지 않는 peskLast 가 있다. Deque를 구현할 때는 일반론적으로는 LinkedList를 사용한다고 할 수 있지만, 사실은 특이한 알고리즘이 적용된 배열을 사용한다. 하지만 ArrayList로는 사용이 불가하며, ArrayList로 만약 구현을 한다고 하면 후술할 ArrayDeque를 사용해야 한다.
- ArrayDeque: 원형으로 이어져있는 Deque. 이것도 일반론적으로는, LinkedList를 쓰지만, 요즘에는 특수한 Array로 구현한다. 사실 Java에서는 Stack, Queue, Deque 대신에 ArrayDeque를 사용하는데, ArrayDeque는 Stack, Queue, Deque의 구현체라고 할 수 있다. 즉, ArrayDeque를 통해서 Stack, Queue, Deque을 사용하는것으로 볼 수 있다.
[p4] - Stack, Queue, Deque, ArrayDeque 사용.
package p4;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class App {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
// 사실 사용이 권장되진 않음. abstract인데, 구현해버려서.
stack.push("안녕하세요");
stack.push("반가워요");
stack.push("잘부탁해요");
String value = stack.pop(); // pop는 remove를 상속받음
Queue<String> queue = new LinkedList<>();
queue.offer("주문1"); // add도 있긴 한데, 쓰지 않는게 좋음
queue.offer("주문2");
queue.offer("주문3");
String order = queue.poll();
Deque<String> deque = new LinkedList<>();
deque.addFirst("");
deque.addFirst("");
deque.addLast("");
deque.addFirst("");
deque.addLast("");
deque.pollLast();
deque.pollFirst();
deque.pollLast();
deque.pollLast();
deque.pollFirst();
ArrayDeque<String> stack1 = new ArrayDeque<>();
stack1.push("order");
stack.pop();
ArrayDeque<String> queue1 = new ArrayDeque<>();
queue1.offer("주문1");
String order1 = queue1.poll();
System.out.println(value + order + order1);
}
}
[p5] - LinkedList, ArrayList 반복, Iterator 사용.
package p5;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class App {
public static void main(String[] args) {
// 일반적 웹 프로그램에서는 ArrayList가 답이다.
//왜냐면, 데이터를 저장하는건 java에서 하는게 아니라, DB가 하는 일이기 때문에.
// Queue나 그런걸 쓸거면 그냥 DB를 쓰는게 맞다고 할 수 있다.
List<String> list = new ArrayList<>();
// List에는 없는 추가 기능이 ArrayList에는 있을 수 있기 때문에,
// 그런 추가 기능을 쓴다면 ArrayList<String> list = new ArrayList<>();
// 일반적으로는 그냥 List로 쓰는게 맞다. 그런 경우는 거의 없음.
// LinkedList, ArrayList의 주요 API들
list.add("값1");
list.add("값2");
list.add("값3");
// 값 추가. 둘다(ArrayList, LinkedList를 말함) 문제 없음.
list.add(0, "값1");
// 값 추가. ArrayList에는 권장되지 않음. 성능이 저하됨. 특히 앞쪽의 값일수록.
list.remove(3);
// 값 삭제. ArrayList에는 권장되지 않음. 성능이 저하됨. 특히 앞쪽의 값일수록.
list.get(0);
// 값 접근. LinkedList에는 권장되지 않음. 쓰면 쓸 수록 성능이 저하됨.
for(int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 이 코드는 접근속도가 빠른 ArrayList를 반복시키는거라 실질적으로 문제가 없지만,
// list를 LinkedList로 생성하면 문제가 생김.
// LinkedList는 get이 여러번 있으면 반복이 엄청나게 돌기 때문에.
// 하지만, 반복을 돌릴 수 있어야 하기 때문에. 반복을 시켜주는 API가 따로 있다.
for(String value : list) {
// foreach는 오른쪽에는 항상 iterator를 상속받은 클래스만 들어갈 수 있음.
// List는 iterator를 상속 받고있다.
System.out.println(value);
// 컬렉션 프레임워크를 쓸 때문 무조건 foreach문을 쓰는게 좋다.
}
// Iterator<String> iterator = list.iterator();
// iterator()는 오버라이딩이 되어 있어서, LinkedList일 경우,
// ArrayList일 경우에 맞는 iterator를 반환해서, 그에 맞는 반복 방식이 동작함.
// while (iterator.hasNext()) {
// String value = iterator.next();
// System.out.println(value);
// }
}
}

- 지금까지 했던것들을 반복하고, 복기가 되어있어야 한다. 반복 시에는, 모든 패키지명, 클래스명, 메서드명, 변수명 등을 최대한 바꿔서 해보아라.
- 복기가 된다면(100%는 아니더라도) 거의 비슷한 용도의 프로젝트를 만들어라. ex) 도서관리 프로그램 등..