
오늘은 Java Collection API의 List, Set, Map과 Generics를 배우고 람다식과 함수형 인터페이스를 거쳐 Stream API까지 학습했다.
특히 Collection에 객체를 담는 방법과 Stream을 이용해 컬렉션의 데이터를 필터링하고 변환하는 흐름을 실습했다.

{key : value}, {key : value}, {key : value}, …⭐️ 이 셋은 인터페이스로 객체 생성이 불가능함
implement를 사용해야함
ArrayList, HashSet, HashMap 같은 구현 클래스를 생성해서 인터페이스 타입으로 받는 것배열과 차이점: 배열 = 고정 길이

<E> : generics
파라미터 타입
T - type
E - Element
K - key
V - value
N - number
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("String");
// list.add(10); --> err
// list.add(true); --> err
System.out.println("debug >>>> " + list);
}
출처: https://gangnam-americano.tistory.com/41
CollectionApp.javaimport java.util.ArrayList;
import java.util.List;
import java.util.Vector;
public class CollectionApp {
/*
실무에서는 배열 대신 Collection API (-java util)
- List
- Set
- Map
*/
public static void main(String[] args) {
List list = new ArrayList<>();
// List list = new Vector<>();
// vector = arraylist
list.add("String");
list.add(10);
// toString method가 오버라이딩
// -> 주소가 가진 주소값 출력
System.out.println("debug >>>> " + list);
}
}
list.add(10); // Boxing 적용됨
primitive type → reference type
Boxing
reference type → primitive type
UnBoxing
int → Integerchar → CharacterList list = new ArrayList<>(); // 가변길이 배열
출력:
debug >>>> [String, 10, true]
순서를 가진 상태로 출력된다.
System.out.println(
"debug >>>> Arraylist Element xxxxDTO"
);
List<PersonDTO> personList
= new ArrayList<PersonDTO>();
StudentDTO student
= StudentDTO.builder()
.name("또치")
.build();
TeacherDTO teacher
= TeacherDTO.builder()
.name("또치")
.build();
ManagerDTO manager
= ManagerDTO.builder()
.name("또치")
.build();
// Upcasting
personList.add(student);
personList.add(teacher);
personList.add(manager);
// Downcasting
personList.add(student);
personList.add(teacher);
personList.add(manager);
for(int idx = 0; idx < personList.size(); idx++){
PersonDTO person = personList.get(idx);
System.out.println(
person.personInfo()
);
}
배운 내용: generics를 클래스 선언 시 사용하는 방법
GenericsApp.javaimport java.util.ArrayList;
import java.util.List;
import features.oop.sub.ManagerDTO;
import features.oop.sub.StudentDTO;
import features.oop.sub.TeacherDTO;
import features.oop.sup.PersonDTO;
public class GenericsApp {
public static void main(String[] args) {
System.out.println();
System.out.println(
"debug >>> Generics App "
);
List<PersonDTO> personList
= new ArrayList<PersonDTO>();
// List<? extends PersonDTO> personList
// = new ArrayList<PersonDTO>();
// 읽기 전용이므로 add에 에러가 뜸
// List<? super PersonDTO> personList
// = new ArrayList<PersonDTO>();
// 쓰기 전용으로 add에 에러가 뜨지 않음
StudentDTO student
= StudentDTO.builder()
.name("또치")
.build();
TeacherDTO teacher
= TeacherDTO.builder()
.name("또치")
.build();
ManagerDTO manager
= ManagerDTO.builder()
.name("또치")
.build();
personList.add(student);
personList.add(teacher);
personList.add(manager);
}
}
extends vs superextendsadd를 뒤에 사용할 수 없음List<? extends PersonDTO> personList
= new ArrayList<PersonDTO>();
personList.add(student);
personList.add(teacher);
personList.add(manager);
// 에러
superList<? super PersonDTO> personList
= new ArrayList<PersonDTO>();
@ToString@ToString(callSuper = true)
callSuper가 필요한 이유참고자료: https://khj93.tistory.com/entry/JAVA-람다식Rambda란-무엇이고-사용법#google_vignette
람다 대수는 이름을 가질 필요가 없다
두 개 이상 입력이 있는 함수는 최종적으로 1개의 입력만 받는 람다 대수로 단순화 될 수 있다.
이름이 없는 함수
일급 객체(first class citizen)
(→) 함수 몸체로 이용하여 사용할 수 있음{} 생략 가능return문으로만 구성된 경우 {} 생략 불가// 정상적인 유형
() -> {}
() -> 1
() -> { return 1; }
(int x) -> x + 1
(x) -> x + 1
x -> x + 1
(int x) -> { return x + 1; }
x -> { return x + 1; }
(int x, int y) -> x + y
(x, y) -> x + y
(x, y) -> { return x + y; }
(String lam) -> lam.length()
lam -> lam.length()
(Thread lamT) -> {
lamT.start();
}
lamT -> {
lamT.start();
}
// 잘못된 유형
// 선언된 type과 선언되지 않은 type을
// 같이 사용 할 수 없다.
(x, int y) -> x + y
(x, final y) -> x + y
인터페이스가 가질 수 있는 메서드가 딱 하나인 것을 의미
종류
java.util.function 패키지에 포함Supplier
→ 매개변수 X
→ 반환 타입 O
Consumer
→ 매개변수 O
→ 반환 타입 X
Predicate
→ 매개변수 O
→ 반환 타입 boolean
Function
→ 매개변수 O
→ 반환 타입 O
@FunctionalInterfacepackage features.lambda;
@FunctionalInterface
public interface InspireFunction {
public int max(int x, int y);
}
실습 코드:
import features.lambda.InspireFunction;
public class StreamApp {
public static void main(String[] args) {
InspireFunction func01
= (x, y) -> x > y ? x : y;
System.out.println(
func01.max(100, 200)
);
System.out.println();
InspireFunction func02
= (x, y) -> x + y;
System.out.println(
func02.max(100, 200)
);
}
}
Supplier<Double> randomValue
= () -> Math.random();
Double value = randomValue.get();
하나의 입력을 받아 아무것도 반환하지 않는 연산
Supplier와 반대
데이터 처리, 이벤트 리스너, 스트림 연산 등에 사용
데이터 변형 없이 소비하려는 경우
Consumer<Integer> display
= x -> System.out.println(x);
display.accept(10);
argument를 받아 boolean 값을 반환하는 함수형 인터페이스
주로 특정 조건을 테스트하는 데 사용
Stream API와 자주 같이 사용
Predicate<Integer> greaterThanTen
= x -> x > 10;
boolean result
= greaterThanTen.test(15);
Function<Integer, String> intToString
= x -> Integer.toString(x);
String result
= intToString.apply(123);
forEachforEach문을 사용하는 게 좋음list.forEach()실습 내용에 해당하는 개념
return을 가지나 break 없이 무조건 끝까지 루프문을 돈다.
즉 리스트 사이즈 만큼 무조건 돌게 설계되어 있는 함수이다.
List<String> brands
= Arrays.asList(
"samsung",
"lg",
"lg cns",
"inspire"
);
brands.forEach(
(brand) -> {
System.out.println(brand);
}
);
System.out::println
데이터를 변경하지 않음
1회용
최종 연산 전까지 중간 연산을 수행하지 않음
작업을 내부적으로 반복 처리
forEach()병렬 실행 가능
| 종류 | 특징 |
|---|---|
Stream<T> | 범용 Stream |
IntStream | 값 타입이 Int인 Stream |
LongStream | 값 타입이 Long인 Stream |
DoubleStream | 값 타입이 Double인 Stream |
| 방법 | 의미 |
|---|---|
Arrays.stream() | 배열 Stream |
.stream() | 컬렉션 Stream |
Stream.builder() | Stream Builder |
컬렉션을 Stream 객체로 바인딩하고 중간 연산(0~N)의 연산 결과를 최종 연산(1)으로 처리한다.
일반적인 방식:
List<String> filteringList
= new ArrayList<String>();
for(int idx = 0; idx < list.size(); idx++){
PersonDTO person
= personList.get(idx);
if(person.getName().startsWith("j")){
filteringList.add(
person.getName().toUpperCase()
);
}
}
Stream API:
List<String> filteringList
= personList.stream()
.filter(null)
.map(null)
.collect(null);
실습:
List<String> filteringList
= personList.stream()
.filter(
s -> s.getName().startsWith("m")
)
.map(
s -> s.getName().toUpperCase()
)
.collect(
Collectors.toList()
);
System.out.println(filteringList);
또 다른 예:
personList.stream()
.filter(
person ->
person.getName().length() > 5
)
.forEach(
System.out::println
);
Collection
↓
stream()
↓
중간 연산
filter()
map()
distinct()
sorted()
↓
최종 연산
collect()
forEach()
count()
0~1000까지의 값 중 500 이상이며 짝수이면서 5의 배수인 수의 합
System.out.println(
IntStream.range(0, 1001)
.skip(500)
.filter(i -> i % 2 == 0)
.filter(i -> i % 5 == 0)
.sum()
);
실행 결과:
38250
List<Integer> evens
= nums.stream()
.filter(n -> n % 2 == 0)
.collect(
Collectors.toList()
);
List<String> lengths
= nums.stream()
.map(String::valueOf)
.map(String::length)
.collect(
Collectors.toList()
);
Set<String> unique
= words.stream()
.distinct()
.sorted()
.collect(
Collectors.toSet()
);
forEachList<String> brands
= Arrays.asList(
"samsung",
"lg",
"lg cns",
"inspire"
);
brands.forEach(
(brand) -> {
System.out.println(brand);
}
);
System.out.println();
System.out.println(
"debug >>>> collection -> Stream"
);
Stream<String> stream
= brands.stream();
stream.forEach(
(brand) -> {
System.out.println(brand);
}
);
forEach를 사용하기는 하지만 수행 속도가 다름순서 x
중복 x
수정 가능(mutable)
FastLookup이 필요할 때 주로 쓰임
import java.util.Set;
Set<String> set = new HashSet<>();
| 구현 클래스 | 특징 |
|---|---|
HashSet | 중복 X / 순서 보장 X |
TreeSet | 중복 X / 오름차순 데이터 정렬 |
LinkedHashSet | 중복 X / 입력 순서대로 데이터 저장 |
key-value 방식
List나 배열과 차이점
오늘 실습에서는 Collection과 Generics를 기존 OOP 코드와 연결했다.
List<PersonDTO> personList
= new ArrayList<PersonDTO>();
StudentDTO student
= StudentDTO.builder()
.name("또치")
.build();
TeacherDTO teacher
= TeacherDTO.builder()
.name("또치")
.build();
ManagerDTO manager
= ManagerDTO.builder()
.name("또치")
.build();
personList.add(student);
personList.add(teacher);
personList.add(manager);
for(int idx = 0; idx < personList.size(); idx++){
PersonDTO person
= personList.get(idx);
System.out.println(
person.personInfo()
);
}
업캐스팅에 의해 하나의 PersonDTO 타입 List에 서로 다른 자식 객체를 담았다.
람다식에서는 함수형 인터페이스를 직접 만들어 사용했다.
@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)
);
InspireFunction func02
= (x, y) -> x + y;
System.out.println(
func02.max(100, 200)
);
Stream에서는 기존 반복문으로 작성하던 필터링과 변환을 체이닝 형태로 작성했다.
List<String> filteringList
= personList.stream()
.filter(
s -> s.getName().startsWith("m")
)
.map(
s -> s.getName().toUpperCase()
)
.collect(
Collectors.toList()
);
personList.stream()
.filter(
person ->
person.getName().length() > 5
)
.forEach(
System.out::println
);
| 개념 | 핵심 내용 |
|---|---|
| List | 중복 허용, 순서 존재, 가변 길이 |
| Set | 중복 허용 X, 순서 존재 X, 가변 길이 |
| Map | Key-Value 형태로 저장 |
| Generics | 컴파일 시점에 타입을 지정해 타입 안정성을 높임 |
| 람다식 | (매개변수) -> 표현식/블록 형태 |
| 함수형 인터페이스 | 한 개의 추상 메서드를 갖는 인터페이스 |
| Supplier | 인자 X, 반환값 O |
| Consumer | 인자 O, 반환값 X |
| Predicate | 인자 O, boolean 반환 |
| Function | 인자 O, 반환값 O |
| Stream API | 컬렉션 데이터를 중간 연산과 최종 연산으로 처리 |
| 중간 연산 | filter, map, distinct, sorted |
| 최종 연산 | collect, forEach, count |
| 메서드 참조 | System.out::println과 같이 람다 대신 메서드를 직접 참조 |
핵심 흐름은 다음과 같다.
Collection
↓
Generics로 타입 지정
↓
Lambda
↓
Functional Interface
↓
Stream
↓
filter / map
↓
collect / forEach
핵심: 람다와 스트림을 익숙하게 사용하면 반복문과 컬렉션 조작을 선언적으로 바꾸어 코드가 훨씬 깔끔해진다.