Collection, Generics, Stream, 람다
본격적으로 새 내용에 들어가기 전에 OOP의 은닉화(싱글톤 패턴), 상속, 다형성(변수타입/매개변수/메서드), 추상화(추상클래스/인터페이스)까지 흐름을 한 번 정리하고 시작했다.
배열 대신 실무에서 많이 쓰는 게 Collection API라고 한다.
배열을 확장시켜놓은 자료구조 느낌이라고 설명하셨다.
{key : value} 형태List<String> list = new ArrayList<String>();
list.add("String");
Set<String> set = new HashSet<>();
set.add("hong");
set.add("inspire");
set.add("lgcns");
set.add("hong"); // 중복이라 무시됨
DB로 치면 table 하나가 행과 열로 이루어진 객체인 것처럼, List에 DTO를 담으면 그게 테이블의 여러 행(row)을 담는 것과 비슷한 느낌이다.
List에는 상속 관계의 DTO들도 그대로 담긴다.
List<PersonDTO> personList = new ArrayList<PersonDTO>();
personList.add(student);
personList.add(teacher);
personList.add(manager);
StudentDTO/TeacherDTO/ManagerDTO가 다 PersonDTO를 상속받고 있어서 가능한 건데, 저번에 배운 매개변수의 다형성(업캐스팅)이 List에도 똑같이 적용되는 것이다.
Collection은 객체만 담을 수 있어서, 기본 타입을 넣으려면 Wrapper Class로 자동 변환된다.
int → Integer)char → Character도 같은 방식<> 안에 타입을 미리 정해두는 문법이다. 이걸 안 쓰면 꺼낼 때마다 캐스팅이 필요한데, 타입을 지정하면 캐스팅이 사라진다.
컬렉션 관련해서 관례적으로 쓰는 문자들:
T - typeE - element (컬렉션의 요소, 컴파일 시점 타입 안정성 + 불필요한 캐스팅 지양이 목적)K - keyV - valueN - number제네릭 클래스도 만들어봤다.
public class ResponseTemplate<T> {
private int code;
private String message;
private T data;
...
}
// 입력성공 : 201, CREATED, xxxDTO
// 목록조회 : 200, OK, list
ResponseTemplate<List<PersonDTO>> response =
new ResponseTemplate<List<PersonDTO>>(200, "OK", personList);
응답 코드(201/200)랑 메시지(CREATED/OK)를 상황별로 다르게 담아서 돌려주는 껍데기를 하나 만든 셈이다. T 자리에 뭐가 들어가든 같은 구조로 응답을 감쌀 수 있다.
메서드 매개변수 타입 선언이나 리턴 타입 지정할 때 자주 쓰는 문법이라고 배웠다.
? extends T — 읽기 전용 (T의 하위 타입)? super T — 쓰기 전용 (T의 상위 타입)Map<String, List<? extends PersonDTO>> map = new HashMap<>();
map.put("student", studentList);
map.put("teacher", teacherList);
map.put("manager", managerList);
StudentDTO 리스트, TeacherDTO 리스트, ManagerDTO 리스트를 하나의 Map에 다 담으려니 그냥 List<PersonDTO>로는 타입이 안 맞아서 와일드카드가 필요했다. ? extends PersonDTO라고 하면 "PersonDTO거나 그 자식 타입"이라는 뜻이라 셋 다 넣을 수 있었다.
지금까지 짜던 for문 방식은 명령형 처리 방식이다.
"어떻게" 할지를 하나하나 다 적는 방식이다.
// 명령형 처리
List<String> filteringList = new ArrayList<String>();
for (int idx = 0; idx < personList.size(); idx++) {
PersonDTO person = personList.get(idx);
if (person.getName().startsWith("j")) {
filteringList.add(person.getName().toUpperCase());
}
}
Stream API는 선언적 처리 방식이다. "무엇을" 원하는지만 적으면 된다.
// Stream API (선언적 처리)
// 컬렉션을 Stream 객체로 바인딩하고 - 중간연산(0~N) - 연산결과를 최종연산(1)
List<String> filteringList = personList.stream()
.filter(s -> s.getName().startsWith("j"))
.map(s -> s.getName().toUpperCase())
.collect(Collectors.toList());
두 코드 결과는 같지만, 아래쪽은 인덱스도 없고 for문도 없다.
가독성, 병렬처리, 유지보수 측면에서 Stream이 낫다.
인터페이스가 가질 수 있는 메서드가 딱 하나인 것을 함수형 인터페이스라고 한다. 람다식을 쓰려면 이 형태여야 한다.
@FunctionalInterface
public interface InspireFunction {
public int max(int x, int y);
}
InspireFunction func01 = (x, y) -> x > y ? x : y;
System.out.println(func01.max(100, 200)); // 200
InspireFunction func02 = (x, y) -> x + y;
System.out.println(func02.max(100, 200)); // 300
같은 인터페이스인데 대입하는 람다에 따라 동작이 완전히 달라진다는 게 신기했다. 메서드 이름은 max인데 func02는 실제로는 덧셈을 하고 있다.
자바 표준 함수형 인터페이스 4가지:
| 인터페이스 | 매개변수 | 반환타입 | 메서드 |
|---|---|---|---|
| Supplier | X | O | get() |
| Consumer | O | X | accept() |
| Function | O | O | apply() |
| Predicate | O | Boolean | test() |
Supplier<String> supplier = () -> "inspire";
System.out.println(supplier.get());
Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(System.out::println).accept("lgcns inspire");
andThen은 Consumer 두 개를 이어붙여서 같은 입력값을 순서대로 각각 처리하게 만드는 역할을 한다. 위 코드는 consumer가 먼저 첫 단어만 출력하고, 이어붙인 System.out::println이 원본 문자열 전체를 한 번 더 출력한다.
System.out::println처럼 타입::메서드명 형태로 쓰는 걸 메서드 참조라고 하는데, (x) -> System.out.println(x)를 줄여쓴 것과 같다.
List<String> brands = Arrays.asList("samsung", "lg", "lgcns", "inspire", "camp", "6th");
Stream<String> stream = brands.stream();
stream.forEach((brand) -> System.out.println(brand));
// 메서드 참조방식
stream = brands.stream();
stream.forEach(System.out::println);
#LGCNS #LGCNS6기 #개발자 #LGCNSINSPIRECAMP