package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class B_getNthElementTest {
B_getNthElement solution;
@BeforeEach
void setUp() {
solution = new B_getNthElement();
}
@Test
@DisplayName("빈 List가 입력되었을때, null을 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>();
assertThat(solution.getNthElement(input, 0)).isEqualTo(null);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 3을 입력받았을 경우 3를 리턴해야 합니다.")
public void testFoo2() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 3)).isEqualTo(3);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 2을 입력받았을 경우 2를 리턴해야 합니다.")
public void testFoo3() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 2)).isEqualTo(2);
}
@Test
@DisplayName("[9, 5, 3, 1, 2], 3을 입력받았을 경우 1를 리턴해야 합니다.")
public void testFoo4() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(9, 5, 3, 1, 2));
assertThat(solution.getNthElement(input, 3)).isEqualTo(1);
}
}
[컬렉션] 연습문제
01_makeArrayList
문제
ArrayList를 선언하고 1부터 5까지 담은 뒤 리턴해야 합니다.
출력
- Integer 타입의 ArrayList를 반해야 합니다.
주의 사항
- ArrayList를 선언한 후 값을 담아 리턴해야 합니다.
입출력 예시
ArrayList<Integer> output = makeArrayList();
System.out.println(output); // --> [1, 2, 3, 4, 5]
힌트
- 메서드의 제목(makeArrayList)을 활용해 검색해 봅니다(java how make arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class A_makeArrayList {
public ArrayList<Integer> makeArrayList() {
// TODO:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class A_makeArrayList {
public ArrayList<Integer> makeArrayList() {
// TODO:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class A_makeArrayListTest {
A_makeArrayList solution;
@BeforeEach
void setUp() {
solution = new A_makeArrayList();
}
@Test
@DisplayName("정확한 List를 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
assertThat(solution.makeArrayList()).isEqualTo(input);
}
}
02_computeAverageOfNumbers
문제
Integer 타입의 ArrayList와 수를 입력받아 수가 인덱스로 가리키는 ArrayList의 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입의 arrayList
인자 2 : index
- int 타입의 인덱스 (0 이상의 정수)
출력
- Integer 타입을 리턴해야 합니다.
주의 사항
- 빈 ArrayList를 입력받은 경우, null을 리턴해야 합니다.
- ArrayList의 크기를 벗어나는 인덱스를 입력받은 경우, null을 리턴해야 합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 2, 4, 6, 8, 10]
Integer output = getNthElement(arryaList, 2);
System.out.println(output); // --> 4
힌트
- 생성된 ArrayList에 get() 메서드를 통해 특정 요소를 조회할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
public class B_getNthElement {
public Integer getNthElement(ArrayList<Integer> arrayList, int index) {
// TODO:
Integer output = null;
if (arrayList.isEmpty()) {
return null;
} else if(arrayList.size() < index){
return null;
}
output = arrayList.get(index);
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class B_getNthElement {
public Integer getNthElement(ArrayList<Integer> arrayList, int index) {
// TODO:
if (arrayList.size() == 0) {
return null;
} else if (index >= arrayList.size()) {
return null;
}
return arrayList.get(index);
}
}
Test 코드
package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class B_getNthElementTest {
B_getNthElement solution;
@BeforeEach
void setUp() {
solution = new B_getNthElement();
}
@Test
@DisplayName("빈 List가 입력되었을때, null을 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>();
assertThat(solution.getNthElement(input, 0)).isEqualTo(null);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 3을 입력받았을 경우 3를 리턴해야 합니다.")
public void testFoo2() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 3)).isEqualTo(3);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 2을 입력받았을 경우 2를 리턴해야 합니다.")
public void testFoo3() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 2)).isEqualTo(2);
}
@Test
@DisplayName("[9, 5, 3, 1, 2], 3을 입력받았을 경우 1를 리턴해야 합니다.")
public void testFoo4() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(9, 5, 3, 1, 2));
assertThat(solution.getNthElement(input, 3)).isEqualTo(1);
}
}
03_getLastElement
문제
문자열타입의 ArrayList를 입력받아 마지막 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입의 ArrayList
출력
- String 타입을 리턴해야 합니다.
주의 사항
- 빈 ArrayList의 경우 null을 리턴해야 합니다.
입출력 예시
<String> arrayList; // ["코", "드", "자", "바", "스", "프", "링"]
String output = getLastElement(arrayList);
System.out.println(output); // --> "링"
힌트
- size() 메서드를 통해 총 요소가 몇 개인지 확인할 수 있습니다.
- ArrayList의 인덱스는 0부터 시작합니다.
- 생성된 ArrayList에 get() 메서드를 통해 특정 요소를 조회할 수 있습니다.
내 코드
package com.choongang;
import java.util.ArrayList;
public class C_getLastElement {
public String getLastElement(ArrayList<String> arrayList) {
// TODO:
String output = null;
if (arrayList.isEmpty()) {
return null;
}
output = arrayList.get(arrayList.size() - 1);
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class C_getLastElement {
public String getLastElement(ArrayList<String> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return null;
} else {
return arrayList.get(arrayList.size() - 1);
}
}
}
Test 코드
package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class C_getLastElementTest {
C_getLastElement solution;
@BeforeEach
void setUp() {
solution = new C_getLastElement();
}
@Test
@DisplayName("빈 ArrayList의 경우 null을 리턴해야 합니다.")
public void testFoo() {
ArrayList<String> list = new ArrayList<>(Arrays.asList());
assertThat(solution.getLastElement(list)).isEqualTo(null);
}
@Test
@DisplayName("[\"사과\", \"바나나\", \"포도\", \"오렌지\"]를 입력받은 경우 \"오렌지\"를 리턴해야 합니다.")
public void testFoo2() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("사과", "바나나", "포도","오렌지"));
assertThat(solution.getLastElement(list)).isEqualTo("오렌지");
}
@Test
@DisplayName("[\"코드\", \"스프링\", \"코딩\"]를 입력받은 경우, \"코딩\"을 리턴해야 합니다.")
public void testFoo3() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("코드", "스프링", "코딩"));
assertThat(solution.getLastElement(list)).isEqualTo("코딩");
}
}
04_addLast
문제
String 타입을 요소로 가지는 ArrayList와 문자열 요소를 입력받아, 주어진 요소를 ArrayList의 맨 뒤에 추가하고 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입을 요소로 가지는 ArrayList
인자 2 : str
- String 타입의 임의의 문자열
출력
- 기존 ArrayList에 주어진 요소가 추가된 형태의 ArrayList를 리턴해야 합니다.
- [arrarList[0], arrarList[1], ..., arrayList[n-1], str]
- arrayList.size()는 n
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 새로운 ArrayList를 선언 / 할당해서 리턴하지 않습니다.
- 기존 ArrayList에 주어진 요소가 추가된 상태(주소값 동일)로 리턴해야 합니다.
입출력 예시
ArrayList<String> arrayList; // ["a", "b"]
ArrayList<String> output = addLast(arrayList, "c");
System.out.println(output); // --> ["a", "b", "c"]
힌트
- 메서드의 제목(addLast)을 활용해 검색해 봅니다(java how to add last in arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class D_addLast {
public ArrayList<String> addLast(ArrayList<String> arrayList, String str) {
// TODO:
arrayList.add(str);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class D_addLast {
public ArrayList<String> addLast(ArrayList<String> arrayList, String str) {
// TODO:
arrayList.add(str);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class D_addLastTest {
D_addLast solution = spy(D_addLast.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/D_addLast.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("새롭게 추가한 String타입의 값이 포함된 ArrayList를 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "c"));
assertThat(solution.addLast(input, "c")).isEqualTo(output);
}
@Test
@DisplayName("새롭게 추가한 String타입의 값이 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b" , "c", "d"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "c", "d", "apple"));
assertThat(solution.addLast(input, "apple")).isEqualTo(output);
}
@Test
@DisplayName("새롭게 추가한 String타입의 값이 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("code", "spring"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("code", "spring", "coding"));
assertThat(solution.addLast(input, "coding")).isEqualTo(output);
}
}
05_addNth
문제
Integer 타입을 요소로 가지는 ArrayList와 추가할 위치의 인덱스, 정수를 입력받아 주어진 요소를 ArrayList의 인덱스에 추가하고 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
인자 2 : index
- 요소를 추가할 위치의 인덱스 (0 이상의 정수)
인자 3 : element
- 임의의 정수
출력
- 기존 ArrayList에 주어진 요소가 추가된 형태의 ArrayList를 리턴해야 합니다.
- [arrarList[0], arrarList[1], ..., arrayList[index]..., arrayList[n]]
- arrayList[index]의 값은 element
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 새로운 ArrayList를 선언 / 할당해서 리턴하지 않습니다.
- 기존 ArrayList에 주어진 요소가 추가된 상태(주소값 동일)로 리턴해야 합니다.
- 입력받은 인덱스가 ArrayList의 크기를 벗어난 경우엔 null을 리턴합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 1, 2, 3]
ArrayList<Integer> output = addNth(arrayList, 1, 7);
System.out.println(output); // --> [0, 7, 1, 2, 3]
내 코드
package com.choongang;
import java.util.ArrayList;
public class E_addNth {
public ArrayList<Integer> addNth(ArrayList<Integer> arrayList, int index, int element) {
// TODO:
Integer a = element;
if (index > arrayList.size() - 1) {
return null;
}
arrayList.add(index, a);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class E_addNth {
public ArrayList<Integer> addNth(ArrayList<Integer> arrayList, int index, int element) {
// TODO:
if (index < 0 || index > -arrayList.size()) {
return null;
}
arrayList.add(index, element);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class E_addNthTest {
E_addNth solution = spy(E_addNth.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/E_addNth.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
assertThat(solution.addNth(input, 9, 0)).isEqualTo(null);
}
@Test
@DisplayName("추가한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
ArrayList<Integer> output = new ArrayList<Integer>(Arrays.asList(1,7,2,3,4,5));
assertThat(solution.addNth(input, 1, 7)).isEqualTo(output);
}
@Test
@DisplayName("추가한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
ArrayList<Integer> output = new ArrayList<Integer>(Arrays.asList(0,1,2,3,4,5));
assertThat(solution.addNth(input, 0, 0)).isEqualTo(output);
}
@Test
@DisplayName("추가한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo4() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
ArrayList<Integer> output = new ArrayList<Integer>(Arrays.asList(1,2,3,0,4,5));
assertThat(solution.addNth(input, 3, 0)).isEqualTo(output);
}
}
06_ modifyNthElement
문제
ArrayList와 요소, 수정할 위치의 인덱스를 입력받아 주어진 요소를 ArrayList의 인덱스의 값을 수정하고 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입의 요소를 갖는 ArrayList
인자 2 : index
- 요소를 추가할 위치의 인덱스 (0 이상의 정수)
인자 3 : str
- 임의의 문자열
출력
- 기존 ArrayList에 주어진 요소가 변경된 형태의 ArrayList를 리턴해야 합니다.
- [arrarList[0], arrarList[1], ..., arrayList[index]..., arrayList[n]]
- arrayList[index]의 값은 str
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 새로운 ArrayList를 선언 / 할당해서 리턴하지 않습니다.
- 기존 ArrayList에 주어진 요소가 수정된 상태(주소값 동일)로 리턴해야 합니다.
- 입력받은 인덱스가 ArrayList의 크기를 벗어난 경우엔 null을 리턴합니다.
- ArrayList의 remove와 add 메서드를 사용할 수 없습니다.
입출력 예시
ArrayList<String> arrayList; // ["여러분", "화이팅", "입니다"]
ArrayList<String> output = modifyNthElement(arrayList, 0, "여러분들");
System.out.println(output); // --> ["여러분들", "화이팅", "입니다"]
힌트
- 메서드의 제목(modifyNthElement)을 활용해 검색해 봅니다(java how modify element of arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class F_modifyNthElement {
public ArrayList<String> modifyNthElement(ArrayList<String> arrayList, int index, String str) {
// TODO:
String a = str;
if (index > arrayList.size() - 1) {
return null;
}
arrayList.set(index, a);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class F_modifyNthElement {
public ArrayList<String> modifyNthElement(ArrayList<String> arrayList, int index, String str) {
// TODO:
if (index < 0 || index >= arrayList.size()) {
return null;
}
arrayList.set(index, str);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class F_modifyNthElementTest {
F_modifyNthElement solution = spy(F_modifyNthElement.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/F_modifyNthElement.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("ArrayList의 add()와 remove()를 사용할 수 없습니다.")
public void testSet() {
String path = "src/main/com/codestates/coplit/Solution.java";
String text = readLineByLineJava8(path);
assertThat(StringUtils.countMatches(text, "add")).isZero();
assertThat(StringUtils.countMatches(text, "remove")).isZero();
}
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
assertThat(solution.modifyNthElement(input, 9, "d")).isEqualTo(null);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "d"));
assertThat(solution.modifyNthElement(input, 2, "d")).isEqualTo(output);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "음료수"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "콜라"));
assertThat(solution.modifyNthElement(input, 3, "콜라")).isEqualTo(output);
}
}
07_removeFromFront
문제
Integer 타입을 요소로 가지는 ArrayList를 입력받아 ArrayList의 첫번째 요소를 삭제하고, 삭제된 해당 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
출력
- ArrayList의 첫 번째 요소를 삭제하고 삭제한 요소를 리턴해야 합니다.
주의 사항
- 입력받은 ArrayList의 첫번째 요소는 삭제되어야 합니다.
- 비어있는 ArrayList를 입력받은 경우엔 null 을 리턴합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 1, 2, 3, 4, 5]
Integer output = removeFromFront(arrayList);
System.out.println(output); // --> 0
System.out.println(arrayList); // --> [1, 2, 3, 4, 5]
힌트
- 메서드의 제목을 활용해 검색해 봅니다(java how remove element of arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class G_removeFromFront {
public Integer removeFromFront(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.isEmpty()) {
return null;
}
Integer remove = arrayList.remove(0);
return remove;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class G_removeFromFront {
public Integer removeFromFront(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return null;
}
Integer result = arrayList.remove(0);
return result;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class F_modifyNthElementTest {
F_modifyNthElement solution = spy(F_modifyNthElement.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/F_modifyNthElement.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("ArrayList의 add()와 remove()를 사용할 수 없습니다.")
public void testSet() {
String path = "src/main/com/codestates/coplit/Solution.java";
String text = readLineByLineJava8(path);
assertThat(StringUtils.countMatches(text, "add")).isZero();
assertThat(StringUtils.countMatches(text, "remove")).isZero();
}
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
assertThat(solution.modifyNthElement(input, 9, "d")).isEqualTo(null);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "d"));
assertThat(solution.modifyNthElement(input, 2, "d")).isEqualTo(output);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "음료수"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "콜라"));
assertThat(solution.modifyNthElement(input, 3, "콜라")).isEqualTo(output);
}
}
08_removeFromNth
문제
String 타입을 요소로 가지는 ArrayList와 인덱스를 입력받아, ArrayList에 인덱스의 요소를 삭제한 후 해당 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입을 요소로 가지는 ArrayList
인자 2 : index
- 요소를 삭제할 위치의 index (0 이상의 정수)
출력
- ArrayList의 입력받은 인덱스의 요소를 삭제하고, 삭제된 해당 요소를 리턴해야 합니다.
주의 사항
- 기존 ArrayList에 주어진 인덱스의 요소가 삭제된 상태여야합니다.
- 입력받은 인덱스가 ArrayList의 크기를 벗어난 경우엔 null을 리턴합니다.
입출력 예시
ArrayList<String> arrayList; // [0, 1, 2, 3, 4, 5]
String output = removeFromNth(arrayList, 3);
System.out.println(output); // --> "3"
내 코드
package com.choongang;
import java.util.ArrayList;
public class H_removeFromNth {
public String removeFromNth(ArrayList<String> arrayList, int index) {
// TODO:
if (arrayList.isEmpty()) {
return null;
}
if (index > arrayList.size() - 1) {
return null;
}
String remove = arrayList.remove(index);
return remove;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class H_removeFromNth {
public String removeFromNth(ArrayList<String> arrayList, int index) {
// TODO:
if (index < 0 || index >= arrayList.size()) {
return null;
}
return arrayList.remove(index);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class H_removeFromNthTest {
H_removeFromNth solution = spy(H_removeFromNth.class);
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("1","2","3","4","5"));
assertThat(solution.removeFromNth(input, 9)).isEqualTo(null);
}
@Test
@DisplayName("해당 index의 값이 삭제된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a","b","c","d","e"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a","c","d","e"));
assertThat(solution.removeFromNth(input, 1)).isEqualTo("b");
}
@Test
@DisplayName("해당 index의 값이 삭제된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("김코딩", "자바", "스프링", "톰캣"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("김코딩", "자바", "스프링"));
assertThat(solution.removeFromNth(input, 3)).isEqualTo("톰캣");
}
@Test
@DisplayName("해당 index의 값이 삭제된 ArrayList를 리턴해야 합니다")
public void testFoo4() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("code", "states", "spring"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("code", "states"));
assertThat(solution.removeFromNth(input, 2)).isEqualTo("spring");
}
}
09_removeFromBackOfNew
문제
ArrayList와 요소를 입력받아, 마지막 요소가 제거된 새로운 ArrayList을 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
출력
- Integer 타입을 요소로 가지는 새로운 ArrayList(주소값 다름)를 리턴해야 합니다.
주의 사항
- 입력받은 ArrayList를 수정하지 않아야 합니다(immutability).
- 빈 ArrayList를 입력받은 경우 null을 리턴해야 합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 1, 2, 3, 4, 5]
ArrayList<Integer> output = removeFromBackOfNew(arrayList);
System.out.println(arrayList); // --> [0, 1, 2, 3, 4, 5]
System.out.println(output); // --> [0, 1, 2, 3, 4]
내 코드
package com.choongang;
import java.util.ArrayList;
public class I_removeFromBackOfNew {
public ArrayList<Integer> removeFromBackOfNew(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.isEmpty()) {
return null;
}
ArrayList<Integer> a = new ArrayList<>();
for (int i = 1; i < arrayList.size(); i++) {
a.add(i);
}
return a;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class I_removeFromBackOfNew {
public ArrayList<Integer> removeFromBackOfNew(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return null;
}
// 1. 반복문을 arrayList.size() - 1 번 돌면서 (반복문)
// 새로운 List에 값을 저장하고 반환
ArrayList<Integer> newArrayList = new ArrayList<>();
for (int i = 0; i < arrayList.size() - 1; i++) {
Integer currentEl = arrayList.get(i);
newArrayList.add(currentEl);
}
// 2. arrayList를 복사한 후, 해당 새로운 List의 마지막 요소를 제거하고 반환
return newArrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class I_removeFromBackOfNewTest {
I_removeFromBackOfNew solution;
@BeforeEach
void setUp() {
solution = new I_removeFromBackOfNew();
}
@Test
@DisplayName("새로운 ArrayList를 만들어서 리턴해야 합니다.")
public void testFoo() {
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
ArrayList<Integer> list_2 = solution.removeFromBackOfNew(list);
assertThat(list == list_2).isFalse();
}
@Test
@DisplayName("빈 ArrayList를 입력받은 경우 null을 리턴해야 합니다.")
public void test() {
ArrayList<Integer> list = new ArrayList<Integer>();
assertThat(solution.removeFromBackOfNew(list)).isNull();
}
@Test
@DisplayName("마지막 요소를 제거한 ArrayList를 리턴해야 합니다.")
public void test_1() {
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
ArrayList<Integer> list_2 = new ArrayList<Integer>(Arrays.asList(1, 2));
assertThat(solution.removeFromBackOfNew(list)).isEqualTo(list_2);
}
}
10_arrayToArrayList
문제
String 타입을 요소로 가지는 배열을 입력받아, String 타입을 요소로 가지는 ArrayList로 변환하여 리턴해야 합니다.
입력
인자 1 : arr
- String 타입을 요소로 가지는 배열
출력
- String 타입을 요소로 가지는 ArrayList를 리턴해야 합니다.
입출력 예시
String[] arr = {"백엔드", "개발자", "김코딩"};
List<String> output = arrayToArrayList(arr);
System.out.println(output); // --> ["백엔드", "개발자", "김코딩"]
주의사항
- 빈 배열의 경우 null을 리턴해야 합니다.
힌트
- 메서드의 제목(arrayToList)을 활용해 검색해 봅니다(java array to List)
내 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
public class J_arrayToArrayList {
public ArrayList<String> arrayToArrayList(String[] arr) {
// TODO:
if (arr.length == 0) {
return null;
}
ArrayList<String> output = new ArrayList<>();
for (String s : arr) {
output.add(s);
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
public class J_arrayToArrayList {
public ArrayList<String> arrayToArrayList(String[] arr) {
// TODO:
if (arr.length == 0) {
return null;
}
ArrayList<String> newArrayList = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
newArrayList.add(arr[i]);
}
// 다른 방법 1, 2, 3
// for (String str : arr) {
// newArrayList.add(str);
// }
// Collection.addAll(newArrList, arr);
// return new ArrayList<>(Arrays.asList(arr));
return newArrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class J_arrayToArrayListTest {
J_arrayToArrayList solution;
@BeforeEach
void setUp() {
solution = new J_arrayToArrayList();
}
@Test
@DisplayName("빈 배열을 입력받은 경우 null을 리턴해야 합니다.")
public void testFoo() {
assertThat(solution.arrayToArrayList(new String[]{})).isNull();
}
@Test
@DisplayName("입력받은 배열과 동일한 요소를 가진 List를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("코드", "스프링", "부트캠프"));
assertThat(solution.arrayToArrayList(new String[]{"코드", "스프링", "부트캠프"})).isEqualTo(list);
}
@Test
@DisplayName("입력받은 배열과 동일한 요소를 가진 List를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("java", "spring", "tomcat"));
assertThat(solution.arrayToArrayList(new String[]{"java", "spring", "tomcat"})).isEqualTo(list);
}
}
11_clearArrayList
문제
입력받은 ArrayList의 모든 요소를 삭제한 뒤 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
출력
- 입력받은 ArrayList의 요소를 모두 삭제한 뒤, 동일한 주소값을 갖는 ArrayList를 리턴해야 합니다.
주의 사항
- 기존 ArrayList에 모든 요소를 삭제한 후 리턴해야 합니다.
- 입력받은 ArrayList와 리턴하는 ArrayList는 같은 주소값을 가져야 합니다.
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
ArrayList<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
ArrayList<Integer> output = clearArrayList(arrayList);
System.out.println(output); // --> []
힌트
- 메서드의 제목(clearArrayList)을 활용해 검색해 봅니다(java how to clear arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class K_clearArrayList {
public ArrayList<Integer> clearArrayList(ArrayList<Integer> arrayList) {
// TODO:
arrayList.removeAll(arrayList);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class K_clearArrayList {
public ArrayList<Integer> clearArrayList(ArrayList<Integer> arrayList) {
// TODO:
arrayList.clear();
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class K_clearArrayListTest {
K_clearArrayList test = spy(K_clearArrayList.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/K_clearArrayList.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("비어있는 ArrayList를 리턴해야 합니다.")
public void testFoo() {
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4));
ArrayList<Integer> output = test.clearArrayList(list);
assertThat(output.size()).isEqualTo(0);
}
}
12_sumAllElements
문제
Integer 타입의 ArrayList를 입력받아 모든 요소를 더한 값을 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입의 ArrayList
출력
- int 타입을 리턴해야 합니다.
주의 사항
- 비어있는 ArrayList를 입력받은 경우 0을 리턴해야 합니다.
- Iterator를 이용해 요소를 순회해야 합니다.
입출력 예시
ArrayList<Integer> arrayList; // [1, 2, 3, 4, 5]
int output = sumAllElements(arrayList);
System.out.println(output); // --> 15
힌트
- Iterator를 이용해 요소를 순회할 수 있습니다.
내 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
public class L_sumAllElements {
public int sumAllElements(ArrayList<Integer> arrayList) {
// TODO:
int output = 0;
Iterator<Integer> iter = arrayList.iterator();
while (iter.hasNext()) {
output += iter.next();
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
public class L_sumAllElements {
public int sumAllElements(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return 0;
}
int sumNum = 0;
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer nextNumber = iterator.next();
sumNum += nextNumber;
iterator.remove();
}
return sumNum;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class L_sumAllElementsTest {
L_sumAllElements test = spy(L_sumAllElements.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("Iterator를 사용해야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/L_sumAllElements.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "Iterator")).isNotZero();
}
@Test
@DisplayName("1, 2, 3, 4, 5의 요소를 가진 ArrayList를 입력받을 경우 15를 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
assertThat(test.sumAllElements(list)).isEqualTo(15);
}
@Test
@DisplayName("12, 23, 45의 요소를 가진 ArrayList를 입력받을 경우 80를 리턴해야 합니다")
public void testFoo2() {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(12, 23, 45));
assertThat(test.sumAllElements(list)).isEqualTo(80);
}
@Test
@DisplayName("비어있는 ArrayList를 입력받을 경우 0을 리턴해야 합니다")
public void testFoo3() {
ArrayList<Integer> list = new ArrayList<>();
assertThat(test.sumAllElements(list)).isEqualTo(0);
}
}
13_getValue
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 키를 입력받아, 키에 해당하는 값을 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
인자 2 : key
- String 타입의 키
출력
- Integer 타입의 값을 리턴해야 합니다.
입출력 예시
HashMap<String, Integer> hashMap; // {a=1, b=2, c=3}
Integer output = getValue(hashMap, "b");
System.out.println(output); // --> 2
내 코드
package com.choongang;
import java.util.HashMap;
import java.util.Map;
public class M_getValue {
public Integer getValue(HashMap<String, Integer> hashMap, String key) {
// TODO:
Integer output = 0;
for (String s : hashMap.keySet()) {
output = hashMap.get(key);
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
import java.util.Map;
public class M_getValue {
public Integer getValue(HashMap<String, Integer> hashMap, String key) {
// TODO:
Integer reslut = hashMap.get(key);
return reslut;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class M_getValueTest {
M_getValue solution;
HashMap<String, Integer> map = new HashMap<String, Integer>(){{
put("A", 1);
put("B", 2);
put("C", 3);
put("head", 10);
put("tail", 19);
put("number", 99);
}};
@BeforeEach
void setUp() {
solution = new M_getValue();
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo() {
assertThat(solution.getValue(map, "A")).isEqualTo(1);
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo2() {
assertThat(solution.getValue(map, "B")).isEqualTo(2);
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo3() {
assertThat(solution.getValue(map, "tail")).isEqualTo(19);
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo4() {
assertThat(solution.getValue(map, "number")).isEqualTo(99);
}
}
14_addKeyAndValue
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 Key, Value를 입력받아 HashMap에 Key-Value 쌍을 추가한 후 새롭게 추가된 요소를 포함한 HashMap을 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
인자 2 : key
- String 타입의 키
인자 3 : value
- int 타입의 값
출력
- <String, Integer> 타입을 요소로 가지는 HashMap
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- HashMap에 key - Value 쌍을 저장해야합니다.
입출력 예시
HashMap<String, Integer> hashMap = new HashMap<>(){{
put("a", 1);
put("b", 2);
put("c", 3);
}};
HashMap<String, Integer> output = addKeyAndValue(hashMap, "d", 4);
System.out.println(output); // {a=1, b=2, c=3, d=4}
내 코드
package com.choongang;
import java.util.HashMap;
public class N_addKeyAndValue {
public HashMap<String, Integer> addKeyAndValue(HashMap<String, Integer> hashMap, String key, int value) {
// TODO:
HashMap<String, Integer> output = new HashMap<>();
output.put(key, value);
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class N_addKeyAndValue {
public HashMap<String, Integer> addKeyAndValue(HashMap<String, Integer> hashMap, String key, int value) {
// TODO:
hashMap.put(key, value);
return hashMap;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class N_addKeyAndValueTest {
N_addKeyAndValue test = spy(N_addKeyAndValue.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/N_addKeyAndValue.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("추가한 키와 값을 요소로 가지는 HashMap을 리턴해야 합니다")
public void testFoo() {
HashMap<String, Integer> map = new HashMap<String, Integer>();
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("A", 1);
}};
assertThat(test.addKeyAndValue(map, "A", 1)).isEqualTo(output);
}
@Test
@DisplayName("추가한 키와 값을 요소로 가지는 HashMap을 리턴해야 합니다")
public void testFoo2() {
HashMap<String, Integer> map = new HashMap<String, Integer>();
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("code", 99);
}};
assertThat(test.addKeyAndValue(map, "code", 99)).isEqualTo(output);
}
@Test
@DisplayName("추가한 키와 값을 요소로 가지는 HashMap을 리턴해야 합니다")
public void testFoo3() {
HashMap<String, Integer> map = new HashMap<String, Integer>();
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("spring", 100);
}};
assertThat(test.addKeyAndValue(map, "spring", 100)).isEqualTo(output);
}
}
15_removeEntry
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 문자열을 입력받아, String 타입의 변수 key를 키(key)로 가지고있는 Entry를 제거합니다.
입력
인자 1 : hashMap
- <String, String> 타입을 요소로 가지는 HashMap
인자 2 : key
- 임의의 문자열
출력
- 별도의 리턴문(return statement)을 작성하지 않습니다.
주의 사항
- 인자로 전달받은 문자열은 key로서 항상 존재합니다.
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<String, Integer> hashMap; // {a=1, b=2, c=3}
removeEntry(hashMap, "b");
System.out.println(hashMap); // --> {a=1, c=3}
내 코드
package com.choongang;
import java.util.HashMap;
public class O_removeEntry {
public void removeEntry(HashMap<String, Integer> hashMap, String key) {
// TODO:
hashMap.remove(key);
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class O_removeEntry {
public void removeEntry(HashMap<String, Integer> hashMap, String key) {
// TODO:
hashMap.remove(key);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class O_removeEntryTest {
O_removeEntry solution;
HashMap<String, Integer> map = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
}};
@BeforeEach
void setUp() {
solution = new O_removeEntry();
}
@Test
@DisplayName("입력받은 key와 연결되어 있는 value를 삭제해야 합니다.")
public void testFoo() {
solution.removeEntry(map, "a");
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("b", 2);
put("c", 3);
}};
assertThat(map).isEqualTo(output);
}
@Test
@DisplayName("입력받은 key와 연결되어 있는 value를 삭제해야 합니다.")
public void testFoo2() {
solution.removeEntry(map, "b");
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("c", 3);
}};
assertThat(map).isEqualTo(output);
}
@Test
@DisplayName("입력받은 key와 연결되어 있는 value를 삭제해야 합니다.")
public void testFoo3() {
solution.removeEntry(map, "c");
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
}};
assertThat(map).isEqualTo(output);
}
}
16_clearHashMap
문제
<Integer, Boolean> 타입을 요소로 가지는 HashMap을 입력받아 모든 Entry를 제거합니다.
입력
인자 1 : hashMap
- <Integer, Boolean> 타입을 요소로 가지는 HashMap
출력
- 별도의 리턴문(return statement)을 작성하지 않습니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<Integer, Boolean> hashMap = new HashMap<Integer, Boolean>(){{
put(1, true);
put(3, false);
put(5, true);
}};
clearHashMap(hashMap);
System.out.println(hashMap); // --> {}
힌트
- 메서드의 제목(clearHashMap)을 활용해 검색해 봅니다(java how to clear HashMap)
내 코드
package com.choongang;
import java.util.HashMap;
public class P_clearHashMap {
public void clearHashMap(HashMap<Integer, Boolean> hashMap) {
// TODO:
hashMap.clear();
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class P_clearHashMap {
public void clearHashMap(HashMap<Integer, Boolean> hashMap) {
// TODO:
hashMap.clear();
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class P_clearHashMapTest {
P_clearHashMap test = spy(P_clearHashMap.class);
HashMap<Integer, Boolean> hashMap = new HashMap<Integer, Boolean>(){{
put(1, true);
put(3, false);
put(5, true);
}};
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/P_clearHashMap.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("모든 요소가 삭제되어야 합니다.")
public void testFoo() {
HashMap<Integer, Boolean> output = new HashMap<>();
test.clearHashMap(hashMap);
assertThat(hashMap.size()).isEqualTo(0);
assertThat(hashMap).isEqualTo(output);
}
}
17_getSize
문제
<Integer, Integer> 타입을 요소로 가지는 HashMap을 입력받아 Entry의 개수를 출력합니다.
입력
인자 1 : hashMap
- <Integer, Integer> 타입을 요소로 가지는 HashMap
출력
- int 타입을 리턴해야 합니다.
- 입력받은 HashMap의 크기를 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<Integer, Integer> hashMap = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
put(3, 30);
}};
int output = getSize(hashMap);
System.out.println(output); // --> 3
내 코드
package com.choongang;
import java.util.HashMap;
public class Q_getSize {
public int getSize(HashMap<Integer, Integer> hashMap) {
// TODO:
return hashMap.size();
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class Q_getSize {
public int getSize(HashMap<Integer, Integer> hashMap) {
// TODO:
return hashMap.size();
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class Q_getSizeTest {
Q_getSize test = spy(Q_getSize.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/Q_getSize.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("입력된 HashMap의 정확한 크기를 리턴해야 합니다")
public void testFoo() {
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
put(3, 30);
}};
assertThat(test.getSize(map)).isEqualTo(3);
}
@Test
@DisplayName("입력된 HashMap의 정확한 크기를 리턴해야 합니다")
public void testFoo2() {
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
}};
assertThat(test.getSize(map)).isEqualTo(2);
}
@Test
@DisplayName("입력된 HashMap의 정확한 크기를 리턴해야 합니다")
public void testFoo3() {
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
put(3, 30);
put(4, 330);
put(5, 430);
}};
assertThat(test.getSize(map)).isEqualTo(5);
}
}
18_addEvenValues
문제
<Character, Integer> 타입을 요소로 가지는 HashMap을 입력받아 짝수 값(Value) 끼리 모두 더한 값을 리턴해야 합니다.
입력
인자 1 : hashMap
- <Character, Integer> 타입을 요소로 가지는 HashMap
출력
- int 타입의 짝수 Value들의 총합을 리턴해야 합니다.
입출력 예시
HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
put('a', 1);
put('b', 4);
put('c', 6);
put('d', 9);
}};
int output = addEvenValues(hashMap);
System.out.println(output); // --> 10
힌트
- HashMap이 제공해주는 메서드를 통해 저장되어있는 Key 모음(keySet)에 바로 접근할 수 있습니다.
내 코드
package com.choongang;
import java.util.HashMap;
public class R_addEvenValues {
public int addEvenValues(HashMap<Character, Integer> hashMap) {
// TODO:
int output = 0;
for (Character c : hashMap.keySet()) {
Integer num = hashMap.get(c);
if (num % 2 == 0) {
output += num;
}
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class R_addEvenValues {
public int addEvenValues(HashMap<Character, Integer> hashMap) {
// TODO:
int result = 0;
for (Character key : hashMap.keySet()) {
Integer value = hashMap.get(key);
if (value % 2 == 0) {
result += value;
}
}
return result;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class R_addEvenValuesTest {
R_addEvenValues solution;
@BeforeEach
void setUp() {
solution = new R_addEvenValues();
}
@Test
@DisplayName("{'a'=1, 'b'=4, 'c'=6, 'd'=9}를 요소로 가지는 HashMap을 입력받은 경우 10을 리턴합니다")
public void testFoo() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('a', 1);
put('b', 4);
put('c', 6);
put('d', 9);
}};
assertThat(solution.addEvenValues(map)).isEqualTo(10);
}
@Test
@DisplayName("{'a'=2, 'b'=4, 'c'=6, 'd'=8}를 요소로 가지는 HashMap을 입력받은 경우 20을 리턴합니다")
public void testFoo2() {
HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
put('a', 2);
put('b', 4);
put('c', 6);
put('d', 8);
}};
assertThat(solution.addEvenValues(hashMap)).isEqualTo(20);
}
@Test
@DisplayName("{'a'=0, 'b'=0, 'c'=0, 'd'=0}를 요소로 가지는 HashMap을 입력받은 경우 0을 리턴합니다")
public void testFoo3() {
HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
put('a', 0);
put('b', 0);
put('c', 0);
put('d', 0);
}};
assertThat(solution.addEvenValues(hashMap)).isEqualTo(0);
}
}
19_addFullNameEntry
문제
한 사람의 firstName, lastName Entry가 저장되어있는 HashMap을 입력 받아, fullName 이라는 새 Entry를 HashMap에 저장하고 해당 HashMap을 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, String> 타입을 요소로 가지는 HashMap
출력
- HashMap에 firstName, lastName가 이미 저장되어 있음을 이용해 fullName이라는 key와 알맞은 문자열을 저장한 후 해당 HashMap을 리턴해야 합니다.
주의 사항
- 입력받은 HashMap과 리턴하는 HashMap는 같은 주소값을 가져야 합니다.
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<String, String> hashMap = new HashMap<String, String>(){{
put("firstName", "김");
put("lastName", "코딩");
}};
HashMap<String, String> output = addFullNameEntry(hashMap);
System.out.println(output); // --> {firstName=김, fullName=김코딩, lastName=코딩}
내 코드
package com.choongang;
import java.util.Collection;
import java.util.HashMap;
public class S_addFullNameEntry {
public HashMap<String, String> addFullNameEntry(HashMap<String, String> hashMap) {
// TODO:
hashMap.put("fullName", hashMap.get("firstName") + hashMap.get("lastName"));
return hashMap;
}
}
레퍼런스 코드
package com.choongang;
import java.util.Collection;
import java.util.HashMap;
public class S_addFullNameEntry {
public HashMap<String, String> addFullNameEntry(HashMap<String, String> hashMap) {
// TODO:
hashMap.put("fullName", hashMap.get("firstName") + hashMap.get("lastName"));
return hashMap;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class S_addFullNameEntryTest {
S_addFullNameEntry test = spy(S_addFullNameEntry.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/S_addFullNameEntry.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("성과 이름이 합쳐진 값이 추가된 HashMap을 리턴해야 합니다")
public void testFoo() {
HashMap<String, String> map = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "coding");
}};
HashMap<String, String> output = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "coding");
put("fullName", "kimcoding");
}};
assertThat(test.addFullNameEntry(map)).isEqualTo(output);
}
@Test
@DisplayName("성과 이름이 합쳐진 값이 추가된 HashMap을 리턴해야 합니다")
public void testFoo2() {
HashMap<String, String> map = new HashMap<String, String>(){{
put("firstName", "code");
put("lastName", "states");
}};
HashMap<String, String> output = new HashMap<String, String>(){{
put("firstName", "code");
put("lastName", "states");
put("fullName", "codestates");
}};
assertThat(test.addFullNameEntry(map)).isEqualTo(output);
}
@Test
@DisplayName("성과 이름이 합쳐진 값이 추가된 HashMap을 리턴해야 합니다")
public void testFoo3() {
HashMap<String, String> map = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "latte");
}};
HashMap<String, String> output = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "latte");
put("fullName", "kimlatte");
}};
assertThat(test.addFullNameEntry(map)).isEqualTo(output);
}
}
20_isContain
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 문자열을 입력받아, HashMap에 문자열을 key로 한 Entry가 있는지의 여부를 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
인자 2 : key
- 임의의 문자열
출력
- boolean 타입을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("김코딩", 20);
put("박해커", 25);
put("최자바", 30);
}};
boolean output = isContain(hashMap, "김코딩");
System.out.println(output); // --> true
힌트
- HashMap에 문자열을 key로 포함(contain)하고 있는지 확인할 수 있습니다.
내 코드
package com.choongang;
import java.util.HashMap;
public class T_isContain {
public boolean isContain(HashMap<String, Integer> hashMap, String key) {
// TODO:
boolean b = hashMap.containsKey(key);
return b;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class T_isContain {
public boolean isContain(HashMap<String, Integer> hashMap, String key) {
// TODO:
return hashMap.containsKey(key);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class T_isContainTest {
T_isContain test = spy(T_isContain.class);
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("김코딩", 20);
put("박해커", 25);
put("최자바", 30);
put("김러키", 3);
put("바닐라", 2);
put("김라떼", 3);
}};
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/T_isContain.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo() {
assertThat(test.isContain(hashMap, "code")).isFalse();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo2() {
assertThat(test.isContain(hashMap, "states")).isFalse();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo3() {
assertThat(test.isContain(hashMap, "김러키")).isTrue();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo4() {
assertThat(test.isContain(hashMap, "바닐라")).isTrue();
}
}
21_getElementOfListEntry
문제
List를 value로 가지는 HashMap, key, 인덱스를 입력받아, key에 해당하는 키(key)가 존재하는 경우, index가 가리키는 List의 요소를 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, List<String>> 타입을 요소로 갖는 HashMap
인자 2 : key
- 임의의 문자열
인자 3 : index
- List의 인덱스 (0 이상의 정수)
출력
- String 타입의 List의 요소를 리턴해야 합니다.
주의 사항
- 입력 인자 hashMap의 경우 key는 String 타입, value는 String 타입을 요소로 갖는 List 입니다.
- 주어진 수가 List의 범위를 벗어나지 않는 경우에만 List의 요소를 리턴해야 합니다.
- 그 외의 경우, null를 리턴해야 합니다.
입출력 예시
HashMap<String, List<String>> hashMap = new HashMap<String, List<String>>(){{
put("apple", Arrays.asList("apple", "red"));
put("banana", Arrays.asList("delicious"));
}};
String output = getElementOfListEntry(hashMap, "apple", 1);
System.out.println(output); // --> "red"
내 코드
package com.choongang;
import java.util.*;
public class U_getElementOfListEntry {
public String getElementOfListEntry(HashMap<String, List<String>> hashMap, String key, int index) {
// TODO:
if (index > hashMap.size() - 1) {
return null;
}
if (!hashMap.containsKey(key)) {
return null;
}
List<String> list = hashMap.get(key);
return list.get(index);
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class U_getElementOfListEntry {
public String getElementOfListEntry(HashMap<String, List<String>> hashMap, String key, int index) {
// TODO:
if (hashMap.containsKey(key)) {
List<String> list = hashMap.get(key);
if(index >= 0 && index < list.size()) {
return list.get(index);
}
}
return null;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class U_getElementOfListEntryTest {
U_getElementOfListEntry solution;
HashMap<String, List<String>> hashMap = new HashMap<String, List<String>>(){{
put("apple", Arrays.asList("apple", "red"));
put("banana", Arrays.asList("delicious"));
put("people", Arrays.asList("kim", "Lee", "cho", "va"));
put("cat", Arrays.asList());
}};
@BeforeEach
void setUp() {
solution = new U_getElementOfListEntry();
}
@Test
@DisplayName("HashMap의 요소인 List에서 정확한 값을 리턴해야 합니다")
public void testFoo() {
assertThat(solution.getElementOfListEntry(hashMap, "apple", 0)).isEqualTo("apple");
}
@Test
@DisplayName("HashMap의 요소인 List에서 정확한 값을 리턴해야 합니다")
public void testFoo2() {
assertThat(solution.getElementOfListEntry(hashMap, "banana", 0)).isEqualTo("delicious");
}
@Test
@DisplayName("HashMap의 요소인 List에서 정확한 값을 리턴해야 합니다")
public void testFoo3() {
assertThat(solution.getElementOfListEntry(hashMap, "people", 0)).isEqualTo("kim");
}
@Test
@DisplayName("존재하지 않는 key를 입력한 경우 null을 리턴해야 합니다")
public void testFoo4() {
assertThat(solution.getElementOfListEntry(hashMap, "dog", 0)).isNull();
}
@Test
@DisplayName("크기를 벗어난 index를 입력한 경우 null을 리턴해야 합니다")
public void testFoo5() {
assertThat(solution.getElementOfListEntry(hashMap, "people", 9)).isNull();
}
}
22_isMember
문제
회원 정보(username, password)가 저장되어있는 HashMap이 있습니다. username과 password를 입력받아 HashMap에 저장된 회원정보와 일치한지 확인하려 합니다. 입력받은 username과 password를 이용해 회원이 맞는지 여부를 리턴해야 합니다.
입력
인자 1 : member
- <String, String> 타입을 요소로 갖는 HashMap
출력
- 전달받은 HashMap에 일치하는 username-password 쌍이 있는지 확인하고, 있으면 true를 없으면 false를 리턴해야 합니다.
주의 사항
- containsValue()의 사용은 금지됩니다.
입출력 예시
HashMap<String, String> member = new HashMap<String, String>(){{
put("kimcoding", "1234");
put("parkhacker", "qwer");
}};
boolean output = isMember(member, "parkhacker", "qwer");
System.out.println(output); // --> true
내 코드
package com.choongang;
import java.util.HashMap;
public class V_isMember {
public boolean isMember(HashMap<String, String> member, String username, String password) {
// TODO:
if (!member.containsKey(username)) {
return false;
}
return member.get(username).equals(password);
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class V_isMember {
public boolean isMember(HashMap<String, String> member, String username, String password) {
// TODO:
if (member.containsKey(username)) {
String value = member.get(username);
if (value.equals(password)) {
return true;
}
}
return false;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class V_isMemberTest {
V_isMember solution;
HashMap<String, String> member = new HashMap<String, String>(){{
put("kimcoding", "1234");
put("parkhacker", "qwer");
put("code", "states");
put("lucky", "q1w2e3");
}};
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@BeforeEach
void setUp() {
solution = new V_isMember();
}
@Test
@DisplayName("존재하지 않는 username을 입력한 경우 false를 리턴합니다")
public void testFoo() {
assertThat(solution.isMember(member, "latte", "cat")).isFalse();
}
@Test
@DisplayName("username과 password가 일치하지 않는 경우 false를 리턴합니다")
public void testFoo2() {
assertThat(solution.isMember(member, "lucky", "q1w2")).isFalse();
}
@Test
@DisplayName("username과 password가 일치하는 경우 true를 리턴합니다")
public void testFoo3() {
assertThat(solution.isMember(member, "code", "states")).isTrue();
}
@Test
@DisplayName("username과 password가 일치하는 경우 true를 리턴합니다")
public void testFoo4() {
assertThat(solution.isMember(member, "parkhacker", "qwer")).isTrue();
}
@Test
@DisplayName("containsValue()를 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/V_isMember.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "containsValue(")).isZero();
}
}
23_select
문제
String 타입을 요소로 가지는 배열과 <String, Integer> 타입을 요소로 가지는 HashMap을 입력받아, 배열의 각 요소들을 HashMap의 키로 했을 때 그 값을 추출하여 만든 새로운 HashMap을 리턴해야 합니다.
입력
인자 1 : arr
- String 타입을 요소로 가지는 배열
인자 2 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
출력
- 새로운 HashMap을 리턴해야 합니다.
주의 사항
- 입력받은 HashMap에 존재하지 않는 키는 무시합니다.
- 입력받은 HashMap을 수정하지 않아야 합니다.
입출력 예시
String[] arr = {"a", "c", "e"};
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
}};
HashMap<String, Integer> output = select(arr, hashMap);
System.out.println(output); // --> { "a"=1, "c"=3 }
내 코드
package com.choongang;
import java.util.HashMap;
public class W_select {
public HashMap<String, Integer> select(String[] arr, HashMap<String, Integer> hashMap) {
// TODO:
HashMap<String, Integer> output = new HashMap<>();
for (String s : arr) {
if (hashMap.containsKey(s)) {
output.put(s, hashMap.get(s));
}
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class W_select {
public HashMap<String, Integer> select(String[] arr, HashMap<String, Integer> hashMap) {
// TODO:
HashMap<String, Integer> result = new HashMap<>();
for (String str : arr) {
if (hashMap.containsKey(str)) {
Integer value = hashMap.get(str);
result.put(str, value);
}
}
return result;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class W_selectTest {
W_select solution;
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
put("d", 4);
put("e", 5);
}};
@BeforeEach
void setUp() {
solution = new W_select();
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo() {
String[] arr = new String[]{"a", "b", "c", "d", "e"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
put("d", 4);
put("e", 5);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo2() {
String[] arr = new String[]{"a", "b", "c", "d", "f", "z"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
put("d", 4);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo3() {
String[] arr = new String[]{"b"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("b", 2);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo4() {
String[] arr = new String[]{"a", "e", "code", "states"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("e", 5);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
}
24_countAllCharacter
문제
문자열을 입력받아 문자열을 구성하는 각 문자(letter)를 키로 갖는 HashMap을 리턴해야 합니다. 각 키의 값은 해당 문자가 문자열에서 등장하는 횟수를 의미하는 int 타입의 값이어야 합니다.
입력
인자 1 : str
- String 타입의 공백이 없는 문자열
출력
- <Character, Integer> 타입을 요소로 갖는 HashMap을 리턴해야 합니다.
주의 사항
- 빈 문자열을 입력받은 경우, null을 리턴해야 합니다.
입출력 예시
HashMap<Character, Integer> output = countAllCharacter("banana");
System.out.println(output); // --> {b=1, a=3, n=2}
내 코드
package com.choongang;
import java.util.HashMap;
public class X_countAllCharacter {
public HashMap<Character, Integer> countAllCharacter(String str) {
// TODO:
if (str.isEmpty()) {
return null;
}
HashMap<Character, Integer> output = new HashMap<>();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (output.containsKey(c)) {
int value = output.get(c);
output.put(c, value + 1);
} else {
output.put(c, 1);
}
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class X_countAllCharacter {
public HashMap<Character, Integer> countAllCharacter(String str) {
// TODO:
if (str.isEmpty()) {
return null;
}
// 1. 주어진 문자열 str을 캐릭터 배열로 변경
char[] arr = str.toCharArray();
// 2. 결과를 담을 map 선언.
HashMap<Character, Integer> reslut = new HashMap<>();
// 3. 캐릭터 배열을 순회
for (char c : arr) {
// 2-1. 각 배열의 요소인 char가 키로 이미 존재하는지 확인
if (reslut.containsKey(c)) {
// 2-1-2. 존재한다면 헤딩 키의; 기존의 값 +1인 값으로 덮어씌우기(put)
// 기존의 키 가져오기
Integer currentValue = reslut.get(c);
reslut.put(c, currentValue + 1);
} else {
// 2-1-1. 존재하지 않는다면, 해당 키에 값을 1로 넣어서 map에 추가(put)
reslut.put(c, 1);
}
}
// 4. 순회가 끝나면 해당 map 반환
return reslut;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class X_countAllCharacterTest {
X_countAllCharacter solution;
@BeforeEach
void setUp() {
solution = new X_countAllCharacter();
}
@Test
@DisplayName("빈 문자열을 입력받은 경우, null을 리턴해야 합니다.")
public void testFoo() {
assertThat(solution.countAllCharacter("")).isNull();
}
@Test
@DisplayName("\"banana\"를 입력받은 경우, {b=1, a=3, n=2}와 같은 HashMap을 리턴해야 합니다")
public void testFoo2() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('b', 1);
put('a', 3);
put('n', 2);
}};
assertThat(solution.countAllCharacter("banana")).isEqualTo(map);
}
@Test
@DisplayName("\"codestates\"를 입력받은 경우, {'a'=1, 'c'=1, 'd'=1, 'e'=2, 'o'=1, 's'=2, 't'=2}와 같은 HashMap을 리턴해야 합니다")
public void testFoo3() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('a', 1);
put('c', 1);
put('d', 1);
put('e', 2);
put('o', 1);
put('s', 2);
put('t', 2);
}};
assertThat(solution.countAllCharacter("codestates")).isEqualTo(map);
}
@Test
@DisplayName("\"kimcoding\"를 입력받은 경우, {'c'=1, 'd'=1, 'g'=1, 'i'=2, 'k'=1, 'm'=1, 'n'=1, 'o'=1}와 같은 HashMap을 리턴해야 합니다")
public void testFoo4() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('c', 1);
put('d', 1);
put('g', 1);
put('i', 2);
put('k', 1);
put('m', 1);
put('n', 1);
put('o', 1);
}};
assertThat(solution.countAllCharacter("kimcoding")).isEqualTo(map);
}
}
Java Effective
애너테이션(Annotation)
이번 챕터에서 학습할 애너테이션(annotation)은 여러분들이 지금까지 많이 사용해 온 주석과 기능적으로 비슷합니다. 즉, 주석을 통해 코드에 대한 정보를 제공할 수 있듯, 애너테이션도 정보 전달을 위한 목적으로 만들어진 문법 요소입니다.
주석과 애너테이션은 정보 전달이라는 유사한 기능을 수행하지만, 정보를 전달하는 대상에서 차이점을 가집니다. 주석은 개발자, 즉 사람에게 정보를 전달하는 기능을 담당하는 반면, 애너테이션은 다른 프로그램에게 정보를 전달합니다.
아래 학습 목표를 숙지한 한 후, 다음 콘텐츠에서부터 애너테이션에 대해 학습을 시작해 봅시다.
학습 목표
- 애너테이션의 개념을 설명할 수 있다.
- 표준 애너테이션을 이해하고 사용할 수 있다.
- 메타 애너테이션이 무엇이고, 왜 필요한지 이해한다.
- 사용자 정의 애너테이션을 정의하는 기초적인 문법을 이해한다.
애너테이션이란?
애너테이션은 소스 코드가 컴파일되거나 실행될 때 컴파일러 및 다른 프로그램에게 필요한 정보를 전달해 주는 문법 요소입니다. 애너테이션이 어떻게 생겼는지 직접 코드에서 확인해 봅시다.
먼저, 아래와 같이 인텔리제이에 인터페이스를 정의해 봅시다.
public interface ExampleInterface {
void example();
}
그다음, 아래와 같이 클래스를 정의하고, ExampleInterface를 구현하도록 해주세요.
public class ExampleClass implements ExampleInterface {
}
아래의 단축키를 사용하여 ExampleInterface의 추상 메서드를 구현하도록 합니다.
- Mac OS : cmd + n 입력 후, Implement Methods 클릭
- Windows : alt + insert 입력 후, Implement Methods 클릭

엔터를 누르거나, OK를 눌러주세요.

입력이 완료되면 구현해야 하는 메서드가 자동으로 정의됩니다.
public class ExampleClass implements ExampleInterface {
@Override
public void example() {
}
}
여기에서 보이는 @Override가 바로 애너테이션입니다. 보이는 것처럼 애너테이션은 @로 시작하며, 클래스, 인터페이스, 필드, 메서드 등에 붙여서 사용할 수 있습니다.
참고로, 위 예시에서의 @Override는 example()이 추상 메서드를 구현하거나, 상위 클래스의 메서드를 오버라이딩한 메서드라는 것을 컴파일러에게 알려주는 역할을 합니다. (@Override의 자세한 기능에 대해서는 이후에 자세히 학습합니다. )
이처럼 애너테이션은 컴파일러 또는 다른 프로그램에 필요한 정보를 제공해 주는 역할을 합니다.
애너테이션의 종류
애너테이션에는 JDK가 기본적으로 제공하는 애너테이션도 있지만, 다른 프로그램에서 제공하는 애너테이션도 있습니다. 다른 프로그램이 제공하는 애너테이션은 해당 프로그램의 사용 방법을 학습할 때 따로 학습해야 합니다. 여기에서는 JDK에 내장된 애너테이션 중에서 중요한 몇 가지만 추려보겠습니다.
JDK에서 기본적으로 제공하는 애너테이션은 아래의 두 가지로 구분됩니다.
- 표준 애너테이션 : JDK에 내장된 일반적인 애너테이션입니다.
- 메타 애너테이션 : 다른 애너테이션을 정의할 때 사용하는 애너테이션입니다.
표준 애너테이션은 앞서 살펴본 @Override와 같이 다른 문법 요소에 붙여서 사용하는 일반적인 애너테이션을 의미하며, 메타 애너테이션은 애너테이션을 직접 정의해서 사용할 때 사용하는 애너테이션입니다.
즉, 애너테이션도 사용자가 직접 정의해서 사용할 수 있으며, 이러한 애너테이션을 사용자 정의 애너테이션이라고 합니다.
다음 콘텐츠에서부터 표준 애너테이션과 메타 애너테이션에 대해 조금 더 자세히 학습해 보도록 하겠습니다.
표준 애너테이션
먼저 자바가 기본적으로 제공하는 표준 애너테이션에 대해 살펴보겠습니다.
아래 소개된 것 외에 몇 가지 표준 애너테이션들이 더 있지만, 우리는 그중에서 가장 빈번하게 사용되는 네 가지를 중심으로 간략히 살펴보도록 하겠습니다.
@Override
@Override는 메서드 앞에만 붙일 수 있는 애너테이션으로, 선언한 메서드가 상위 클래스의 메서드를 오버라이딩하거나 추상 메서드를 구현하는 메서드라는 것을 컴파일러에게 알려주는 역할을 수행합니다.
예를 들어, 아래와 같이 SuperClass의 example()를 SubClass에서 오버라이딩할 때에, @Override를 붙여주면, 컴파일러는 SubClass의 example()이 상위 클래스의 메서드를 오버라이딩한 것으로 간주합니다.
class SuperClass {
public void example() {
System.out.println("example() of SuperClass");
}
}
class SubClass extends SuperClass {
@Override
public void example() {
System.out.println("example() of SubClass");
}
}
컴파일 과정에서 컴파일러가 @Override를 발견하면, @Override가 붙은 메서드와 같은 이름을 가진 메서드가 상위 클래스(또는 인터페이스)에 존재하는지 검사합니다. 즉, SuperClass에 example()이 존재하는지 검사합니다.
만약, 상위 클래스(또는 인터페이스)에서 @Override가 붙어있는 메서드명과 같은 이름의 메서드를 찾을 수 없다면 컴파일러가 컴파일 에러를 발생시킵니다.
그렇다면, 상위 클래스에 오버라이딩 같은 같은 이름의 메서드가 존재하는지 왜 확인해야 할까요?
종종 코드를 작성하다 보면 어떤 메서드를 오버라이딩하거나 구현할 때, 개발자의 실수로 메서드의 이름이 잘못 작성되는 경우가 발생합니다.
class SuperClass {
public void example() {
System.out.println("example() of SuperClass");
}
}
class SubClass extends SuperClass {
public void exapmle() { // 메서드 이름에 오타가 있습니다.
System.out.println("example() of SubClass");
}
}
이러면, 위 예시처럼 @Override를 붙이지 않으면 컴파일러는 exapmle()이라는 새로운 메서드를 정의하는 것으로 간주하고, 에러를 발생시키지 않습니다.
즉, 컴파일 에러 없이 코드가 그대로 실행될 수 있어 실행 시에 런타임 에러가 발생할 것이며, 런타임 에러 발생 시에, 어디에서 에러가 발생했는지 에러의 원인을 찾아내기 어려워집니다.
그러나, @Override를 사용하면 example()이 오버라이딩 메서드라는 것을 컴파일러가 인지하고, 상위 클래스에 example()이 존재하는지 확인하기 때문에, 이러한 상황을 방지할 수 있습니다.
즉, @Override는 컴파일러에게 “컴파일러야, 이 메서드는 상위 클래스의 메서드를 오버라이딩하거나 추상 메서드를 구현하는 메서드인데, 만약에 내가 실수해서 오버라이딩 및 구현이 잘 안 되면 에러를 발생시켜서 나에게 알려줄래?”라고 부탁하는 것과 같습니다.
@Deprecated
@Deprecated는 기존에 사용하던 기술이 다른 기술로 대체되어 기존 기술을 적용한 코드를 더 이상 사용하지 않도록 유도하는 경우에 사용합니다.
아래 예시를 보면, OldClass의 oldField와 getOldField()에 @Deprecated가 붙어 있습니다.
class OldClass {
@Deprecated
private int oldField;
@Deprecated
int getOldField() { return oldField; }
}
이때, 다른 클래스에서 OldClass를 인스턴스화하여 getOldField()를 호출하면 아래와 같이 취소선이 뜨면서, 인텔리제이가 경고 메시지를 출력해 줍니다.


또한, 해당 코드를 직접 컴파일해 보면 아래와 같이 경고 메시지가 출력됩니다.

이처럼 @Deprecated는 애너테이션이 붙은 대상이 새로운 것으로 대체되었으니 기존의 것을 사용하지 않도록 유도하는 기능을 합니다.
즉, 기존의 코드를 다른 코드와의 호환성 문제로 삭제하기 곤란해 남겨두어야만 하지만 더 이상 사용하는 것을 권장하지 않을 때 @Deprecated를 사용합니다.
@SuppressWarnings
@SuppressWarnings 애너테이션은 컴파일 경고 메시지가 나타나지 않도록 합니다. 때에 따라서 경고가 발생할 것이 충분히 예상됨에도 묵인해야 할 때 주로 사용합니다.
아래와 같이 @SuppressWarnings 뒤에 괄호를 붙이고 그 안에 억제하고자 하는 경고메시지를 지정해 줄 수 있습니다. 아래의 내용을 모두 외울 필요는 없습니다. 여기에서는 특정 내용과 관련된 경고 메시지를 선택적으로 억제할 수 있다는 사실만 이해하고 넘어가도 충분합니다.

더 나아가 아래와 같이 중괄호에 여러 개의 경고 유형을 나열함으로써 여러 개의 경고를 한 번에 묵인하게 할 수 있습니다.
@SuppressWarnings({"deprecation", "unused", "null"})
@FunctionalInterface
@FunctionalInterface 애너테이션은 함수형 인터페이스를 선언할 때, 컴파일러가 함수형 인터페이스의 선언이 바르게 선언되었는지 확인하도록 합니다. 만약 바르게 선언되지 않은 경우, 에러를 발생시킵니다.
참고로, 함수형 인터페이스는 단 하나의 추상 메서드만을 가져야 하는 제약이 있습니다. 함수형 인터페이스에 대해서는 람다 챕터에서 자세히 학습하니, 여기에서는 “함수형 인터페이스가 제대로 작성되었는지 컴파일러에게 검사할 것을 요구하는 애너테이션이구나” 정도로만 이해해 주세요.
@FunctionalInterface
public interface ExampleInterface {
public abstract void example(); // 단 하나의 추상 메서드
}
표준 애너테이션 종류
자바의 내장 표준 에너테이션
- @Override
- @Deprecated
- @SuppressWarnings
- @SafeVarargs
- @FunctionalInterface
- @Retention
- @Documented
- @Target
- @Inherited
- @Repeatable
- @Native
스프링 프레임워크의 주요 표준 에너테이션
- @Required
- @Autowired
- @Qualifier
- @Primary
- @Value
- @Component
- @Service
- @Repository
- @Controller
- @RestController
- @RequestMapping
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping
- @ResponseStatus
- @RequestBody
- @ResponseBody
- @ModelAttribute
- @PathVariable
- @RequestParam
- @SessionAttributes
- @CookieValue
- @ExceptionHandler
- @Transactional
- @EnableTransactionManagement
- @EnableAspectJAutoProxy
- @Aspect
- @Before
- @After
- @AfterReturning
- @AfterThrowing
- @Around
- @Configuration
- @Bean
- @Profile
- @Scope
- @Import
- @Lazy
메타 애너테이션
- 메타 애너테이션(meta-annotation)은 애너테이션을 정의하는 데에 사용되는 애너테이션으로, 애너테이션의 적용 대상 및 유지 기간을 지정하는 데에 사용됩니다.
먼저, 앞서 살펴보았던 @Override가 어떻게 정의되어 있는지 확인해 보겠습니다. @Override의 소스 코드는 아래와 같습니다.

여기에서 알 수 있는 것처럼, 애너테이션을 정의할 때는 @interface 키워드를 사용하여 정의합니다.
또한, 애너테이션 정의부 상단에 @Target, @Retention 애너테이션이 붙어 있는 것을 확인할 수 있습니다. 이들은 @Override의 적용 대상과 유지 기간을 지정하는 역할을 합니다.
이처럼 메타 애너테이션은 애너테이션을 정의하는 데에 사용되며, 메타 애너테이션을 사용하여 애너테이션의 다양한 특성을 지정할 수 있습니다.
아래에서부터는 대표적인 메타 애너테이션 몇 가지를 살펴보도록 하겠습니다.
@Target
@Target 애너테이션은 이름 그대로 애너테이션을 적용할 “대상"을 지정하는 데 사용됩니다.
다음의 표에 나와 있는 내용이 @Target 애너테이션을 사용하여 지정할 수 있는 대상의 타입이며, 모두 java.lang.annotation.ElementType이라는 열거형에 정의되어 있습니다.

다음의 예시를 통해 좀 더 알아보도록 합시다.
import static java.lang.annotation.ElementType.*;
//import문을 이용하여 ElementType.TYPE 대신 TYPE과 같이 간단히 작성할 수 있습니다.
@Target({FIELD, TYPE, TYPE_USE}) // 적용대상이 FIELD, TYPE
public @interface CustomAnnotation { } // CustomAnnotation을 정의
@CustomAnnotation // 적용대상이 TYPE인 경우
class Main {
@CustomAnnotation // 적용대상이 FIELD인 경우
int i;
}
위의 코드 예제는 @Target 애너테이션의 용법을 잘 보여주고 있습니다.
바로 이어지는 챕터에서 가볍게 살펴보겠지만, 애너테이션을 사용자가 직접 정의할 때(@interface 사용), @Target 애너테이션을 통해 사용자가 정의한 애너테이션이 어디에 적용될 수 있는지를 설정할 수 있습니다.
위의 예제에서는 @Target({FIELD, TYPE, TYPE_USE}) 을 사용하여, 각각 필드, 타입, 그리고 타입이 사용되는 모든 대상(변수)에 애너테이션이 적용되도록 한 것을 확인할 수 있습니다.
@Documented
다음으로 @Documented 애너테이션은 애너테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 하는 애너테이션 설정입니다.
자바에서 제공하는 표준 애너테이션과 메타 애너테이션 중 @Override와 @SuppressWarnings를 제외하고는 모두 @Documented가 적용되어 있습니다.
@Documented
@Target(ElementType.Type)
public @interface CustomAnnotation { }
@Inherited
@Inherited 애너테이션은 이름에서도 알 수 있듯이 하위 클래스가 애너테이션을 상속받도록 합니다. @Inherited 애너테이션을 상위 클래스에 붙이면, 하위 클래스도 상위 클래스에 붙은 애너테이션들이 동일하게 적용됩니다.
@Inherited // @SuperAnnotation이 하위 클래스까지 적용
@interface SuperAnnotation{ }
@SuperAnnotation
class Super { }
class Sub extends Super{ } // Sub에 애너테이션이 붙은 것으로 인식
위의 코드 예제에서, Super 상위 클래스로부터 확장된 Sub 하위 클래스는 상위 클래스와 동일하게 @SuperAnnotation에 정의된 내용들을 적용받게 됩니다.
@Retention
@Retention 애너테이션도 이름 그대로 특정 애너테이션의 지속 시간을 결정하는 데 사용합니다. 애너테이션과 관련한 유지 정책(retention policy)의 종류에는 다음의 세 가지가 있습니다.
유지정책이란 애너테이션이 유지되는 기간을 지정하는 속성입니다.

각각의 유지 정책은 언제까지 애너테이션이 유지될지를 결정합니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
//오버라이딩이 제대로 되었는지 컴파일러가 확인하는 용도
//클래스 파일에 남길 필요 없이 컴파일 시에만 확인하고 사라짐
public @interface Override(){ }
예를 들면, 위의 예제에서 Override 애너테이션은 컴파일러가 사용하면 끝나기 때문에, 실행 시에는 더 이상 사용되지 않음을 의미합니다.
@Repeatable
마지막으로, @Repeatable 애너테이션은 애너테이션을 여러 번 붙일 수 있도록 허용한다는 의미가 있습니다.
아래 예제에서, 사용자 타입의 애너테이션 Work를 정의하고, @Repeatable 애너테이션을 사용하여 이것을 여러 번 사용할 수 있도록 하였습니다.
@Repeatable(Works.class) // Work 애너테이션을 여러 번 반복해서 쓸 수 있게 한다.
@interface Work{
String value();
}
이제 아래와 같이 일반적인 애너테이션과 다르게 Work 애너테이션을 하나의 대상에 여러 번 적용하는 것이 가능해졌습니다.
@Work("코드 업데이트")
@Work("메서드 오버라이딩")
class Main{
... 생략 ...
}
참고로, @Repeatable 애너테이션은 일반적인 애너테이션과 달리 같은 이름의 애너테이션이 여러 번 적용될 수 있기 때문에, 이 애너테이션들을 하나로 묶어주는 애너테이션도 별도로 작성해야 합니다.
@interface Works { // 여러 개의 Work애너테이션을 담을 컨테이너 애너테이션 Works
Work[] value();
}
@Repeatable(Works.class) // 컨테이너 애너테이션 지정
@interface Work {
String value();
}
사용자 정의 애너테이션
마지막으로, 앞서 언급했듯이 사용자 정의 애너테이션이 있지만, 현재 단계에서 크게 중요하지 않기 때문에 가볍게 참고만 하고 지나가도록 합니다.
사용자 정의 애너테이션은 이름 그대로 사용자가 직접 애너테이션을 정의해서 사용하는 것을 의미합니다.
애너테이션을 정의하는 방법은 인터페이스를 정의하는 것과 비슷합니다.
@interface 애너테이션명 { // 인터페이스 앞에 @기호만 붙이면 애너테이션을 정의할 수 있습니다.
타입 요소명(); // 애너테이션 요소를 선언
}
한 가지 유의할 점은, 애너테이션은 java.lang.annotation 인터페이스를 상속받기 때문에 다른 클래스나 인터페이스를 상속받을 수 없다는 사실입니다.
혹시 사용자 정의 애너테이션과 관련된 좀 더 자세한 내용이 궁금하다면, java custom annotation 등 키워드를 사용해서 찾아보는 것을 권장합니다.
람다(Lambda)
람다식(Lambda Expression)은 함수형 프로그래밍 기법을 지원하는 자바의 문법요소입니다.
람다식은 수학자 알론조 처치(Alonzo Church)가 발표한 람다 계산법에서 시작되었는데, 이를 그의 제자 존 맥카시(John McCarthy)가 프로그래밍 언어에 도입하면서 본격적으로 컴퓨터 프로그래밍 언어에서 사용되기 시작했습니다.
람다식은 간단히 말해서 메서드를 하나의 ‘식(expression)’으로 표현한 것으로, 코드를 매우 간결하면서 명확하게 표현할 수 있다는 큰 장점이 있습니다.
최근 함수형 프로그래밍이 다시금 주목을 받게 되면서 자바도 JDK 1.8 이후 람다식 등 함수형 프로그래밍 문법 요소를 도입하면서 기존의 객체지향 프로그래밍과 함수형 프로그래밍을 혼합하는 방식으로 더욱 효율적인 프로그래밍을 할 수 있게 되었습니다.
사실 람다식은 뒤에서 좀 더 자세히 보겠지만, 객체지향적 언어의 특성을 가진 자바의 특성에 따라 일반적인 함수가 아니라 익명의 객체이기 때문에 기존 자바의 문법 요소를 해치지 않으면서 함수형 프로그래밍 기법을 사용할 수 있는 장치가 필요했습니다. 이에 따라 함수형 인터페이스(functional interface가 만들어지게 되었습니다.
좀 더 자세한 내용은 하나씩 학습해 가도록 하겠습니다.
그럼, 본격적인 학습에 들어가기에 앞서 아래 학습 목표를 통해 먼저 배울 내용을 확인해 봅시다.
학습 목표
- 람다식이 무엇이고, 어떻게 사용할 수 있는지 이해할 수 있다.
- 함수형 인터페이스를 통해 람다를 다루는 방법을 이해하고 설명할 수 있다.
- 람다식을 메서드 참조 방식으로 변환할 수 있다.
- 스트림(Stream)은 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자입니다.
스트림을 사용하면 List, Set, Map, 배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있습니다.
스트림은 데이터 소스를 다루는 풍부한 메서드를 제공합니다.
이를 활용하면, 다량의 데이터에 복잡한 연산을 수행하면서도, 가독성과 재사용성이 높은 코드를 작성할 수 있습니다.
람다식의 기본 문법
앞서 개요에서 언급했던 것처럼, 람다식은 함수(메서드)를 좀 더 간단하고 편리하게 표현하기 위해 고안된 문법 요소라 할 수 있습니다.
다음의 예를 한번 살펴볼까요?
//기존 메서드 표현 방식
void sayhello() {
System.out.println("HELLO!")
}
//위의 코드를 람다식으로 표현한 식
() -> System.out.println("HELLO!")
위의 메서드는 같은 메서드를 각각 기존의 방식과 람다식으로 표현한 것입니다.
어떤가요?
아직 구체적인 람다 문법을 배우지는 않았지만, 좀 더 간편해 보이지 않나요?
가장 먼저 두드러지는 차이는 람다식에서는 기본적으로 반환타입과 이름을 생략할 수 있다는 점입니다. 따라서 람다함수를 종종 이름이 없는 함수, 즉 익명 함수(anonymous function)라 부르기도 합니다.
그럼, 이제 아래 예시를 통해 메서드를 람다식으로 만드는 방법에 대해 살펴보도록 하겠습니다.
int sum(int num1, int num2) {
return num1 + num2;
}
여기 우리에게 익숙한 더하기 기능을 수행하는 sum 메서드가 있습니다. 이 메서드를 어떻게 람다식으로 바꿀 수 있을까요?
(int num1, int num2) -> { // 반환타입과 메서드명 제거 + 화살표 추가
return num1 + num2;
}
앞서 봤던 것처럼, 반환타입과 메서드명을 제거하고 코드 블록 사이에 화살표를 추가해 주면 됩니다.
다음의 예시들도 함께 살펴봅시다.
// 기존 방식
void example1() {
System.out.println(5);
}
// 람다식
() -> {System.out.println(5);}
// 기존 방식
int example2() {
return 10;
}
// 람다식
() -> {return 10;}
// 기존 방식
void example3(String str) {
System.out.println(str);
}
// 람다식
(String str) -> { System.out.println(str);}
이렇게 람다식을 사용하면 기존 방식을 좀 더 간편하면서도 명확하게 표현할 수 있습니다.
그런데 놀라운 것은 이 외에도 특정 조건이 충족되면 람다식을 더욱 축약하여 표현할 수 있습니다.
다시 이전의 sum 메서드로 돌아가 보겠습니다.
// 기존 방식
int sum(int num1, int num2) {
return num1 + num2;
}
// 람다식
(int num1, int num2) -> {
num1 + num2
}
이 sum 메서드는 내용을 조금 더 축약할 수 있습니다.
먼저, 메서드 바디에 문장이 실행문이 하나만 존재할 때 우리는 중괄호와 return 문을 생략할 수 있습니다. 이 경우, 세미콜론까지 생략해야 합니다.
(int num1, int num2) -> num1 + num2
그럼 반대로, 실행문이 두 개 이상이면 중괄호를 생략할 수 없겠죠?
두 번째로, 매개변수 타입을 함수형 인터페이스를 통해 유추할 수 있는 경우에는 매개변수의 타입을 생략할 수 있습니다. 함수형 인터페이스에 대해서는 다음 콘텐츠에서 학습합니다.
(num1, num2) -> num1 + num2
어떤가요?
믿기 어렵지만, 위의 람다식은 변신 전의 sum 메서드와 정확히 동일한 메서드입니다. 신기하지 않나요?
이 외에도 매개변수가 하나인 경우 소괄호 생략할 수 있는 등의 조건들이 있는데, 추가적인 축약 표현은 검색을 통해 공부해 보시기 바랍니다.
또한, 여러분은 여기에서 이러한 축약 표현을 모두 외우지 않으셔도 됩니다. 외운다고 해도 바로 사용할 수 있는 것이 아니기 때문입니다. 어떤 경우에 축약이 가능한지, 어떤 경우에 불가능한지 여러 번 코드를 입력해 보시면서 자연스럽게 익숙해지시기를 바랍니다.
함수형 인터페이스
자바에서 함수는 반드시 클래스 안에서 정의되어야 하므로 메서드가 독립적으로 있을 수 없고 반드시 클래스 객체를 먼저 생성한 후 생성한 객체로 메서드를 호출해야 합니다.
이런 맥락에서 지금까지 우리가 메서드와 동일시 여겼던 람다식 또한 사실은 객체입니다. 더 정확히는 이름이 없기 때문에 익명 객체라 할 수 있습니다.
앞서 우리가 봤었던 sum 메서드를 한번 다시 살펴보겠습니다.
// sum 메서드 람다식
(num1, num2) -> num1 + num2
// 람다식을 객체로 표현
new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
}
위의 람다식으로 표현한 sum 메서드는 사실 아래와 같은 이름이 없는 익명 객체입니다.
익명 객체는 익명 클래스를 통해 만들 수 있는데, 익명 클래스란 객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한 번만 사용되는 일회용 클래스입니다.
그리고 위에서 봤던 것처럼 아래와 같이 생성과 선언을 한 번에 할 수 있습니다.
new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
}
다시 돌아가서, 만약에 람다식이 객체라 한다면 앞서 우리가 배웠던 것처럼 이 객체에 접근하고 사용하기 위한 참조변수가 필요합니다.
그런데 기존에 객체를 생성할 때 만들었던 Object 클래스에는 sum이라는 메서드가 없으므로, Object 타입의 참조변수에 담는다고 하더라도 sum 메서드를 사용할 수 없습니다.
public class LamdaExample1 {
public static void main(String[] args) {
// 람다식 Object obj = (num1, num2) -> num1 + num2; 로 대체 가능
Object obj = new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
};
obj.sum(1, 2);
}
}
출력 결과
java: cannot find symbol
symbol: method sum(int,int)
location: variable obj of type java.lang.Object
다시 이야기하면, 위의 코드 예제에서 익명 객체를 생성하여 참조변수 obj에 담아준다고 하더라도 sum 메서드를 사용할 수 있는 방법이 없습니다.
이 같은 문제를 해결하기 위해 사용하는 자바의 문법 요소가 바로 자바의 함수형 인터페이스(Functional Interface)라 할 수 있습니다.
즉, 자바에서 함수형 프로그래밍을 하기 위한 새로운 문법 요소를 도입하는 대신, 기존의 인터페이스 문법을 활용하여 람다식을 다루는 것이라 할 수 있습니다.
함수형 인터페이스에는 단 하나의 추상 메서드만 선언될 수 있는데, 이는 람다식과 인터페이스의 메서드가 1:1로 매칭되어야 하기 때문입니다.
그럼, 이제 위의 예제에서 직면했던 문제를 함수형 인터페이스를 적용하여 풀어보도록 하겠습니다.
public class LamdaExample1 {
public static void main(String[] args) {
/* Object obj = new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
};
*/
ExampleFunction exampleFunction = (num1, num2) -> num1 + num2;
System.out.println(exampleFunction.sum(10,15));
}
@FunctionalInterface // 컴파일러가 인터페이스가 바르게 정의되었는지 확인하도록 합니다.
interface ExampleFunction {
int sum(int num1, int num2);
}
// 출력값
25
위의 예제에서 함수형 인터페이스인 ExampleFunction에 추상메서드 sum()이 정의되어 있습니다. 이 함수형 인터페이스는 람다식을 참조할 참조변수를 선언할 때, 타입으로 사용하기 위해 필요합니다.
함수형 인터페이스 타입으로 선언된 참조변수 exampleFunction에 람다식이 할당되었으며, 이제 exampleFunction을 통해 sum() 메서드를 호출할 수 있습니다.
이처럼, 함수형 인터페이스를 사용하면 참조변수의 타입으로 함수형 인터페이스를 사용하여 우리가 원하는 메서드에 접근할 수 있습니다.
매개변수와 리턴값이 없는 람다식
다음과 같이 매개변수와 리턴값이 없는 추상 메서드를 가진 함수형 인터페이스가 있다고 가정해 보겠습니다.
@FunctionalInterface
public interface MyFunctionalInterface {
void accept();
}
이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 합니다. 람다식에서 매개변수가 없는 이유는 accept()가 매개변수를 가지지 않기 때문입니다.
MyFunctionalInterface example = () -> { ... };
// example.accept();
람다식이 대입된 인터페이스의 참조 변수는 위의 주석과 같이 accept()를 호출할 수 있습니다. accept()의 호출은 람다식의 중괄호 {}를 실행시킵니다.
아래의 예시를 보면서 매개변수와 리턴값이 없는 경우의 람다식을 좀 더 익혀봅시다.
@FunctionalInterface
interface MyFunctionalInterface {
void accept();
}
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example = () -> System.out.println("accept() 호출");
example.accept();
}
}
// 출력값
accept() 호출
매개변수가 있는 람다식
다음과 같이 매개 변수가 있고 리턴값이 없는 추상 메서드를 가진 함수형 인터페이스가 있다고 가정해 보겠습니다.
@FunctionalInterface
public interface MyFunctionalInterface {
void accept(int x);
}
이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 합니다. 람다식에서 매개변수가 한 개인 이유는 추상메서드 accept()가 매개변수를 하나만 가지기 때문입니다.
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example;
example = (x) -> {
int result = x * 5;
System.out.println(result);
};
example.accept(2);
example = (x) -> System.out.println(x * 5);
example.accept(2);
}
}
// 출력값
10
10
람다식이 대입된 인터페이스 참조 변수는 다음과 같이 accept()를 호출할 수 있습니다. 위의 예시와 같이 매개값으로 2를 주면 람다식의 x 변수에 2가 대입되고, x는 중괄호 { }에서 사용됩니다.
리턴값이 있는 람다식
다음과 같이 매개 변수와 리턴값을 가지는 추상 메서드를 포함하는 함수형 인터페이스가 있습니다.
@FunctionalInterface
public interface MyFunctionalInterface {
int accept(int x, int y);
}
이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 합니다. 람다식에서 매개 변수가 두 개인 이유는 accept()가 매개변수를 두 개 가지기 때문입니다.
또한, accept()가 리턴 타입이 있기 때문에 중괄호 { }에는 return 문이 있어야 합니다.
아래의 예시를 보겠습니다.
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example;
example = (x, y) -> {
int result = x + y;
return result;
};
int result1 = example.accept(2, 5);
System.out.println(result1);
example = (x, y) -> { return x + y; };
int result2 = example.accept(2, 5);
System.out.println(result2);
example = (x, y) -> x + y;
//return문만 있으면, 중괄호 {}와 return문 생략 가능
int result3 = example.accept(2, 5);
System.out.println(result3);
example = (x, y) -> sum(x, y);
//return문만 있으면, 중괄호 {}와 return문 생략 가능
int result4 = example.accept(2, 5);
System.out.println(result4);
}
public static int sum(int x, int y){
return x + y;
}
}
//출력값
7
7
7
7
람다식이 대입된 인터페이스 참조변수는 다음과 같이 accept()를 호출할 수 있습니다. 매개값으로 2와 5를 주면 람다식의 x 변수에 2, y 변수에 5가 대입되고 x와 y는 중괄호에서 사용됩니다.
자바에서 기본적으로 제공하는 함수형 인터페이스
마지막으로, 자바에서는 빈번하게 사용되는 함수형 인터페이스를 기본적으로 제공하고 있습니다. 즉, 기본적으로 내장된 함수형 인터페이스를 사용하여 매번 같은 기능을 수행하는 함수형 인터페이스를 직접 만드는 번거로움을 줄여주는 것입니다.
메서드 레퍼런스
메서드 참조는 람다식에서 불필요한 매개변수를 제거할 때 주로 사용합니다.
즉, 람다식으로 더욱 간단해진 익명 객체를 더욱더 간단하게 사용하고 싶은 개발자의 요구가 반영된 산물이라 할 수 있습니다.
람다식은 종종 기존 메서드를 단순히 호출만 하는 경우가 많습니다.
다음 예시코드는 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 정적 메서드를 호출하는 람다식입니다.
(left, right) -> Math.max(left, right)
람다식은 단순히 두 개의 값을 Math.max() 메서드의 매개값으로 전달하는 역할만 하므로 다소 불편해 보입니다. 또한, 이 경우 입력값과 출력값의 반환타입을 쉽게 유추할 수 있으므로 입력값과 출력값을 일일이 적어주는 게 크게 중요하지 않습니다.
이럴 때는 다음과 같이 메서드 참조를 이용하면 매우 깔끔하게 처리할 수 있습니다.
// 클래스이름::메서드이름
Math :: max // 메서드 참조
메서드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 인터페이스의 추상 메서드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라집니다.
IntBinaryOperator 인터페이스는 두 개의 int 매개값을 받아 int 값을 리턴하므로, Math::max 메서드 참조를 대입할 수 있습니다.
IntBinaryOperator operato = Math :: max; //메서드 참조
메서드 참조는 정적 혹은 인스턴스 메서드를 참조할 수 있고 생성자 참조도 가능합니다.
지금부터 하나씩 살펴보겠습니다.
정적 메서드와 인스턴스 메서드 참조
정적 메서드를 참조할 때는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메서드 이름을 기술하면 됩니다.
클래스 :: 메서드
인스턴스 메서드의 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 ::기호를 붙이고 인스턴스 메서드 이름을 기술하면 됩니다.
참조 변수 :: 메서드
다음 예제는 Calculator의 정적 및 인스턴스 메서드를 참조합니다.
//Calculator.java
public class Calculator {
public static int staticMethod(int x, int y) {
return x + y;
}
public int instanceMethod(int x, int y) {
return x * y;
}
}
import java.util.function.IntBinaryOperator;
public class MethodReferences {
public static void main(String[] args) throws Exception {
IntBinaryOperator operator;
/*정적 메서드
클래스이름::메서드이름
*/
operator = Calculator::staticMethod;
System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));
/*인스턴스 메서드
인스턴스명::메서드명
*/
Calculator calculator = new Calculator();
operator = calculator::instanceMethod;
System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
}
}
/*
정적메서드 결과 : 8
인스턴스 메서드 결과 : 15
*/
꼭 직접 코드를 입력해 보면서 결과를 확인해 주세요.
생성자 참조
메서드 참조는 생성자 참조도 포함합니다.
생성자를 참조한다는 것은 객체 생성을 의미합니다. 단순히 메서드 호출로 구성된 람다식을 메서드 참조로 대치할 수 있듯이, 단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치할 수 있습니다.
(a,b) -> new 클래스(a,b)
이 경우 생성자 참조로 표현하면 다음과 같습니다. 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 됩니다.
//생성자 참조 문법
클래스 :: new
생성자가 오버로딩되어 여러 개가 있으면 컴파일러는 함수형 인터페이스의 추상 메서드와 동일한 매개 변수 타입과 개수가 있는 생성자를 찾아 실행합니다.
만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생합니다.
다음의 예제를 한번 확인해 보도록 합시다.
//Member.java
public class Member {
private String name;
private String id;
public Member() {
System.out.println("Member() 실행");
}
public Member(String id) {
System.out.println("Member(String id) 실행");
this.id = id;
}
public Member(String name, String id) {
System.out.println("Member(String name, String id) 실행");
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
public String getId() {
return id;
}
}
import java.util.function.BiFunction;
import java.util.function.Function;
public class ConstructorRef {
public static void main(String[] args) throws Exception {
Function<String, Member> function1 = Member::new;
Member member1 = function1.apply("kimcoding");
BiFunction<String, String, Member> function2 = Member::new;
Member member2 = function2.apply("kimcoding", "김코딩");
}
}
/*
Member(String id) 실행
Member(String name, String id) 실행
*/
위의 코드 예제는 생성자 참조를 이용해서 두 가지 방법으로 Member 객체를 생성하고 있습니다.
하나는 Function<String, Member> 함수형 인터페이스의 Member apply(String) 메서드를 이용해서 Member 객체를 생성하고, 다른 하나는 BiFunction<String, String, Member> 함수형 인터페이스의 Member 객체를 생성합니다.
이때 생성자 참조는 두 가지 방법 모두 같지만, 실행되는 Member 생성자가 다른 것을 볼 수 있습니다.
스트림(Stream)
스트림(Stream)은 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자입니다.
스트림을 사용하면 List, Set, Map, 배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있습니다.
스트림은 데이터 소스를 다루는 풍부한 메서드를 제공합니다.
이를 활용하면, 다량의 데이터에 복잡한 연산을 수행하면서도, 가독성과 재사용성이 높은 코드를 작성할 수 있습니다.
학습 목표
- 스트림의 핵심적인 개념과 특징을 이해할 수 있다.
- 스트림의 생성, 중간 연산, 최종 연산의 세 단계로 구성된 스트림 파이프라인을 이해하고 활용할 수 있다.
- 스트림의 주요 메서드를 활용해 원하는 데이터 처리를 할 수 있다.
스트림의 핵심 개념과 특징
앞에서 간략하게 언급했듯이, 스트림(Stream)이란 자바8부터 도입된 문법으로 배열 및 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 하는 반복자입니다.

영어에서 스트림(stream)의 사전적 의미는 위의 사진과 같은 “개울”, “냇가” 또는 어떤 “흐름”을 의미하는데, 이와 유사하게 자바에서의 스트림은 “데이터의 흐름”을 의미합니다. 좀 더 구체적으로, 각 데이터를 흐름에 따라 우리가 원하는 결과로 가공하고 처리하는 일련의 과정과 관련이 있습니다.
아마 아직은 그 의미가 잘 와닿지 않으시겠다고 생각합니다.
그럼, 이제부터 스트림이 어떤 맥락에서 도입되었고, 어떤 핵심적인 특징을 가지고 있으며, 어떻게 활용될 수 있는지 살펴보면서 순차적으로 스트림에 대한 좀 더 자세한 내용을 알아보도록 하겠습니다.
스트림(Stream)의 도입 배경
역사적으로, 많은 경우 어떤 기술적 발전은 기존에 있었던 어떤 문제나 한계를 극복하기 위한 시도들에서 이뤄져 왔습니다. 지금부터 우리가 학습하게 될 스트림 또한 마찬가지입니다.
지금까지 우리는 자바 배열과 컬렉션에 관한 내용을 학습하면서, 이러한 자료구조를 통해 많은 수의 데이터를 좀 더 효과적으로 다룰 수 있다는 사실을 이해할 수 있었습니다. 더 나아가, 이렇게 저장된 데이터들에 반복적으로 접근하여 우리가 원하는 모양대로 데이터로 가공하기 위해 for문과 Iterator를 활용해 왔습니다.
하지만, 이렇게 기존 방식대로 데이터를 처리하는 데에는 크게 두 가지 한계가 있습니다.
먼저는 for 문 이나 Iterator를 사용하는 경우, 많은 경우 코드가 길고 복잡해질 수 있습니다.
한 가지 간단한 예를 들어보겠습니다.
- Iterator를 사용한 반복 처리
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class PrintNumberOperator {
public static void main(String[] args) {
// 각 숫자를 배열화
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// Iterator 생성
Iterator<Integer> it = list.iterator();
// 리스트를 순회하며 값 출력
while (it.hasNext()) {
int num = it.next();
System.out.print(num);
}
}
}
//출력값
12345
위의 코드 예제는 List 컬렉션에서 Iterator를 사용하여 값을 순서대로 출력하는 프로그램의 코드입니다.
그럼, 이제 같은 결과를 출력하는 코드를 스트림을 사용하여 작성해 보겠습니다.
- 스트림을 사용한 반복 처리
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class PrintNumberOperatorByStream {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::print);
}
}
//출력값
12345
어떤가요?
사실 위의 짧은 코드만으로는 그 차이가 크게 다가오지 않을 수 있습니다. 하지만, 작성해야 하는 로직이 더 길고 복잡해진다면, 스트림을 사용한 코드 작성의 효과는 분명해질 것입니다. 이 부분은 계속 이어지는 내용을 통해 스트림에 대해 알아가면서 차차 확인해 보시길 바랍니다.
추가적으로, 스트림을 사용하면 선언형 프로그래밍(Declarative Programming) 방식으로 데이터를 처리할 수 있어 더욱 인간 친화적이고 직관적인 코드 작성이 가능합니다.
지금까지 우리가 알고리즘 문제를 풀고, 실습 과제를 하면서 코드를 사용했던 방식은 어떤 주어진 과업을 달성하기 위해 코드 한 줄 한 줄의 동작 원리를 이해하고 순차적이고 세세하게 이를 규정하는 방식이었습니다. 이를 명령형 프로그래밍(Imperative Programming)이라고 하는데, 이러한 방식은 “어떻게” 코드를 작성할지에 대한 내용에 초점을 두고 있다고 할 수 있습니다.
반면, 명령형 프로그래밍과 다르게 선언형 프로그래밍은 “어떻게”가 아닌 “무엇”에 집중하여 코드를 작성하는 코드 작성 방법론을 의미합니다. 즉, 내부의 동작 원리를 모르더라도 어떤 코드가 어떤 역할을 하는지 직관적으로 이해할 수 있습니다. 여기서 “어떻게”에 대한 부분은 추상화되어 있습니다.
마찬가지로 코드를 통해 좀 더 이해해 보겠습니다.
- 명령형 프로그래밍 방식
import java.util.List;
public class ImperativeProgramming {
public static void main(String[] args){
// List에 있는 숫자 중에서 4보다 큰 짝수의 합계 구하기
List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
int sum = 0;
for(int number : numbers){
if(number > 4 && (number % 2 == 0)){
sum += number;
}
}
System.out.println("명령형 프로그래밍을 사용한 합계 : " + sum);
}
}
//출력값
명령형 프로그래밍을 사용한 합계 : 14
위의 코드 예제는 “어떻게”에 초점을 둔 명령형 프로그래밍으로 조건에 부합하는 리스트의 합계를 구하는 코드입니다. 이제 같은 코드를 스트림을 사용하여 선언형 프로그래밍 방식으로 바꿔서 표현해 보겠습니다.
- 선언형 프로그래밍 방식
import java.util.List;
public class DeclarativePrograming {
public static void main(String[] args){
// List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
int sum =
numbers.stream()
.filter(number -> number > 4 && (number % 2 == 0))
.mapToInt(number -> number)
.sum();
System.out.println("선언형 프로그래밍을 사용한 합계 : " + sum);
}
}
//출력값
선언형 프로그래밍을 사용한 합계 : 14
어떤가요?
아직 스트림에 대한 구체적인 내용을 배우지는 않았고, “어떻게”에 해당하는 각 함수의 내부 동작을 전혀 알 수 없지만 한 눈에도 어떤 흐름으로 어떤 일들이 일어나고 있는지 파악하는 일이 어렵지 않습니다. 이처럼 스트림을 사용하면 보다 직관적이고 간결한 코드 작성이 용이합니다.
또 한 가지 여기서 눈 여겨봐야 할 부분은 각 메서드에서 람다식을 사용하여 데이터를 처리하고 있다는 사실입니다.
곧 좀 더 자세히 살펴보겠지만, filter 메서드에서는 람다식을 사용하여 주어진 데이터 소스에서 4보다 크면서 짝수인 수를 걸러낼 수 있도록 조건을 주었고, mapToInt 메서드 또한 람다식을 사용하여 이렇게 걸러진 값들을 int 타입의 정수로 바꿔주고 있습니다. 이처럼 스트림에서 람다식을 사용하여 데이터를 어떻게 처리할지 규정할 수 있습니다.
두 번째로, 기존 방식으로 데이터를 처리하는 경우에는 데이터 소스를 각기 다른 방식으로 다뤄야 한다는 불편함이 있습니다. 다른 말로 표현하면, 표준화된 하나의 방식이 아닌 데이터 소스에 따라 그에 부합하는 방식의 메서드를 각각 다르게 적용해서 데이터 처리를 해야 한다는 의미입니다.
예를 들면, 어떤 데이터 집합을 순차적으로 정렬해야 한다고 했을 때, 같은 정렬 기능을 수행하는 데 있어서 배열의 경우에는 Arrays.sort()를, List의 경우에는 Collections.sort()를 각각 다르게 사용해야 합니다. 이렇듯 각각의 필요한 기능들을 사용할 때마다 데이터 소스에 맞는 방식을 찾아야 하는 일은 개발자의 입장에서 매우 번거롭고 불편한 일이 아닐 수 없습니다.
이러한 문제를 해결하기 위해 도입된 자바의 문법요소가 바로 스트림입니다.
이제 스트림을 사용하면, 데이터 소스가 무엇이냐에 관계없이 같은 방식으로 데이터를 가공/처리할 수 있습니다. 다른 말로, 배열이냐 컬렉션이냐에 관계없이 하나의 통합된 방식으로 데이터를 다룰 수 있게 되었다는 뜻입니다.
아래 예시를 통해 좀 더 알아보겠습니다.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamOperator {
public static void main(String[] args) {
// ArrayList
List<String> fruitList = new ArrayList<>();
fruitList.add("바나나 ");
fruitList.add("사과 ");
fruitList.add("오렌지 ");
// 배열
String[] fruitArray = {"바나나 ", "사과 ", "오렌지 "};
// 각각 스트림 생성
Stream<String> ListStream = fruitList.stream();
Stream<String> ArrayStream = Arrays.stream(fruitArray);
// 출력
ListStream.forEach(System.out::print);
ArrayStream.forEach(System.out::print);
}
}
//출력값
바나나 사과 오렌지 바나나 사과 오렌지
위의 예제는 스트림을 사용하여 각기 다른 데이터 소스를 동일한 방식으로 처리하는 모습을 잘 보여주고 있습니다. 문자열을 요소로 가지는 List와 문자열 배열에 각각 스트림을 생성하고 forEach() 메서드를 사용해서 각 요소를 순회하며 출력해주고 있습니다.
참고로 forEach() 메서드는 데이터 소스의 각 요소를 순회하면서 람다식 안에 정의된 어떤 명령을 실행하는 데 사용하는 최종연산자입니다. 스트림의 기본적인 구조와 그 안에서 빈번하게 활용되는 메서드들에 대해서는 뒤에서 좀 더 자세하게 알아보도록 하겠습니다.
결론적으로, 스트림은 앞서 언급한 기존 방식의 두 가지 한계를 효과적으로 보완하면서, 좀 더 간결하고 직관적인 코드 작성을 가능하게 합니다. 다음으로 이제 스트림의 핵심적인 특징들에 대해 살펴보도록 하겠습니다.
스트림의 특징
자바의 다른 문법 요소들과 마찬가지로, 스트림 또한 여러 복잡하고 깊은 내용들을 가지고 있지만 이 단계에서 우리는 먼저 딱 4 가지의 핵심적인 특징들만 기억하는 것으로 충분합니다. 핵심적으로 기억해야 하는 스트림의 4 가지의 특징들은 다음과 같습니다.
- 스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계의 파이프라인으로 구성될 수 있다.
- 스트림은 원본 데이터 소스를 변경하지 않는다(read-only).
- 스트림은 일회용이다(onetime-only).
- 스트림은 내부 반복자이다.
이제 각각의 특징들을 간략하게 살펴보도록 하겠습니다.
1. 스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계의 파이프라인으로 구성될 수 있다.
스트림을 바르게 이해하기 위해서는 아래 그림으로 요약될 수 있는 스트림 파이프 라인(stream pipeline)에 대한 이해가 필수적입니다. 사실 아래 도식을 이해하면 스트림의 핵심을 모두 이해했다고 봐도 큰 과장이 아닙니다.

위에서 언급한 것처럼, 스트림 파이프라인은 1) 스트림의 생성, 2) 중간 연산, 3) 최종 연산이라는 총 세 가지 단계로 구성되어 있습니다. 사실 중간 연산을 생략하고 곧바로 최종연산으로 넘어가는 두 단계 구성도 가능하지만, 지금은 가장 빈번하게 사용되는 완전체 파이프라인을 가정해 보겠습니다.
간략하게 흐름을 설명하면, 먼저 배열, 컬렉션, 임의의 수 등 다양한 데이터 소스를 일원화하여 스트림으로 작업하기 위해서는 스트림을 생성해야 합니다. 스트림이 생성되고 나면, 최종 처리를 위한 중간 연산을 수행할 수 있습니다.
여기에는 필터링, 매핑, 정렬 등의 작업이 포함되며, 중간 연산의 결과는 또 다른 스트림이기 때문에 계속 연결해서 연산을 수행할 수 있습니다. 이렇게 연결된 모양새가 마치 파이프라인과 같다고 해서 이러한 구조를 스트림 파이프라인이라고 합니다.
마지막으로, 이렇게 중간 연산이 완료된 스트림을 최종적으로 처리하는 최종 연산(총합, 평균, 카운팅 등)을 끝으로 스트림은 닫히고 모든 데이터 처리가 완료됩니다. 최종 연산의 경우는 스트림의 요소를 소모하면서 연산을 수행하기 때문에 최종적으로 단 한 번의 연산만 가능합니다. 따라서 최종 연산 후에 다시 데이터를 처리하고 싶다면, 다시 스트림을 생성해주어야 합니다.
아래 예시를 잠시 살펴보겠습니다.

위의 도식은 남성과 여성으로 구성된 어떤 회원 컬렉션을 스트림을 사용하여 의도한 데이터로 가공하는 과정을 보여주는 스트림 파이프라인입니다. 가장 먼저 스트림을 생성하고, 생성한 스트림에서 중간 연산 단계로 성별이 남자인 회원만 필터링한 후에, 그중에서 나이 요소만을 매핑한 후, 최종 연산을 통해 남자 회원들의 나이 평균을 구했습니다.
아직 잘 이해가 안 되거나 어렵게 느껴지더라도 괜찮습니다. 스트림 파이프라인을 어떻게 활용할 수 있는지에 대한 좀 더 구체적인 내용은 이어지는 챕터에서 자세히 학습할 예정이니, 지금은 전체적인 구조와 흐름을 이해하는 것에 집중해 주세요.
2. 스트림은 원본 데이터 소스를 변경하지 않는다(read-only).
스트림은 그 원본이 되는 데이터 소스의 데이터들을 변경하지 않습니다. 오직 데이터를 읽어올 수 있고, 데이터에 대한 변경과 처리는 생성된 스트림 안에서만 수행됩니다. 이는 원본 데이터가 스트림에 의해 임의로 변경되거나 데이터가 손상되는 일을 방지하기 위함입니다.
3. 스트림은 일회용이다(onetime-only).
위에서 스트림 파이프라인에 관해 설명할 때 잠시 언급했듯이, 스트림은 일회용입니다. 다르게 표현하면, 스트림이 생성되고 여러 중간 연산을 거쳐 마지막 연산이 수행되고 난 후에는 스트림은 닫히고 다시 사용할 수 없습니다. 만약 추가적인 작업이 필요하다면, 다시 스트림을 생성해야 합니다. 마치 컬렉션에서 배웠던 Iterator와 비슷하다고 할 수 있습니다.
4. 스트림은 내부 반복자이다.
내부 반복자(Internal Iterator)를 이해하기 위해서 먼저 이에 반대되는 개념인 외부 반복자(External Iterator)를 알면 도움이 됩니다. 외부 반복자란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 의미합니다. 인덱스를 사용하는 for문, Iterator를 사용하는 while문 이 대표적입니다.
반면 스트림은 반대로 컬렉션 내부에 데이터 요소 처리 방법(람다식)을 주입해서 요소를 반복처리 하는 방식입니다. 아래 그림을 통해 좀 더 살펴보겠습니다.

위에서 확인할 수 있는 것처럼, 외부 반복자의 경우 요소가 필요할 때마다 순차적으로 컬렉션에서 필요한 요소들을 불러오지만, 내부반복자는 데이터 처리 코드만 컬렉션 내부로 주입해 줘서 그 안에서 모든 데이터 처리가 이뤄지도록 합니다. 병렬 작업, 멀티 코어 최적화 등 어려운 컴퓨터공학 용어를 사용하지 않더라도 한 눈에도 더욱 효율적인 데이터 처리가 가능하다는 사실을 어렵지 않게 이해할 수 있습니다.
지금까지 스트림이 어떤 필요에 따라 자바8 이후에 도입되었으며, 어떤 구조와 특징을 가지고 있는지 살펴보았습니다. 이어지는 챕터에서는 이제 이러한 스트림 파이프라인의 구조 속에서 스트림의 데이터 처리 작업이 어떻게 구성되고 사용될 수 있는지 좀 더 구체적인 내용을 살펴보도록 하겠습니다.
스트림의 생성
이제 본격적으로 스트림 파이프라인의 세 단계를 차례대로 하나씩 살펴보도록 하겠습니다.
앞서 설명한 것처럼, 스트림으로 데이터를 처리하기 위해서는 가장 먼저 스트림을 생성해야 합니다. 스트림을 생성할 수 있는 데이터 소스는 배열, 컬렉션, 임의의 수, 특정 범위의 정수 등 다양한데, 이에 따라 스트림의 생성 방법에 조금씩 차이가 있습니다.
여기서는 그중에서 가장 많이 쓰이는 배열, 컬렉션, 그리고 임의의 수로 스트림을 생성하는 방법을 학습해 보도록 하겠습니다. 대부분은 이 세 가지를 사용하여 스트림을 생성하기 때문에, 아래 소개되는 기본적인 스트림 생성법을 먼저 기억해 두시고, 이후에 필요하다면 좀 더 다양한 스트림 생성 방법을 찾아서 활용하시길 바랍니다.
마지막으로 아래에서 소개되는 코드 예제들은 반드시 눈으로만 확인하지 마시고, 직접 본인의 인텔리제이에서 입력해 보시면서 몸소 내용을 체화하시기를 당부드립니다.
이럼 이제 스트림의 생성에 대해 좀 더 자세한 내용을 알아보겠습니다.
배열 스트림 생성
먼저 배열을 데이터 소스로 하는 스트림 생성은 Arrays 클래스의 stream() 메서드 또는 Stream 클래스의 of() 메서드를 사용할 수 있습니다.
- Arrays.stream()
- public class StreamCreator { public static void main(String[] args) { // 문자열 배열 선언 및 할당 String[] arr = new String[]{"김코딩", "이자바", "박해커"}; // 문자열 스트림 생성 Stream<String> stream = Arrays.stream(arr); // 출력 stream.forEach(System.out::println); } } // 출력값 김코딩 이자바 박해커
- Stream.of()
- import java.util.stream.Stream; public class StreamCreator { public static void main(String[] args) { // 문자열 배열 선언 및 할당 String[] arr = new String[]{"김코딩", "이자바", "박해커"}; // 문자열 스트림 생성 Stream<String> stream = Stream.of(arr); // 출력 stream.forEach(System.out::println); } } // 출력값 김코딩 이자바 박해커
위의 예제의 출력값을 확인해 보면 Arrays.stream()와 Stream.of() 메서드 모두 같은 값을 출력하고 있다는 사실을 알 수 있습니다. 따라서, 배열로 스트림을 생성할 시에는 둘 중에 더 편한 메서드를 임의로 선택하여 사용할 수 있습니다.
추가로, Arrays 클래스에는 int, long , double과 같은 기본형 배열을 데이터 소스로 스트림을 생성하는 메서드도 있습니다.
Arrays 클래스

한 가지만 더 언급하면, IntStream의 경우는 일반적인 Stream 클래스에는 없는 숫자와 관련한 여러 유용한 메서드들이 정의되어 있으므로 기억해 두면 요긴하게 사용할 수 있습니다.
IntStream의 유용한 기능들
import java.util.Arrays;
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
// int형 배열로 스트림 생성
int[] intArr = {1,2,3,4,5,6,7};
IntStream intStream = Arrays.stream(intArr);
// 숫자와 관련된 경우 intStream을 사용하는 것을 권장
System.out.println("sum=" + intStream.sum());
// System.out.println("average=" + intStream.average());
}
}
//출력값
sum=28
참고로 이어지는 내용을 통해 자세히 살펴보겠지만, 숫자 연산과 관련된 대부분의 메서드(합계, 카운팅, 평균, 최대값 등)는 최종 연산자이기 때문에 최초 사용 시 스트림이 닫히게 됩니다. 예를 들면, 위의 코드 예제에서 sum() 메서드를 호출한 이후에 다시 average() 메서드를 호출하면 에러가 발생합니다. 이 부분도 직접 코드로 확인해 보시길 바랍니다.
컬렉션 스트림 생성
컬렉션 타입(List, Set 등)의 경우, 컬렉션의 최상위 클래스인 Collection에 정의된 stream() 메서드를 사용하여 스트림을 생성할 수 있습니다. 따라서 Collection으로부터 확장된 하위클래스 List와 Set을 구현한 컬렉션 클래스들은 모두 stream() 메서드를 사용하여 스트림을 생성할 수 있습니다.
아래 예제를 통해 확인해 보겠습니다.
- 컬렉션 스트림 생성
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreator {
public static void main(String[] args) {
// 요소들을 리스트
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::print);
}
}
//출력값
1234567
위의 예제는 List 타입의 스트림을 생성하는 과정을 보여주고 있습니다. Arrays 클래스에 static 하게 선언된 asList() 메서드를 사용하여 요소들을 리스트 타입의 참조변수에 할당한 뒤에 stream() 메서드를 사용하여 스트림을 생성하였습니다.
임의의 수 스트림 생성
난수를 생성하는 자바의 기본 내장 클래스 Random 클래스 안에는 해당 타입의 난수들을 반환하는 스트림을 생성하는 메서드들이 정의되어 있습니다. 예를 들면, int() 메서드의 경우 int 형의 범위 안에 있는 난수들을 무한대로 생성하여 IntStream 타입의 스트림으로 반환합니다.
여러분들의 인텔리제이를 열어서 아래 코드를 입력해 보세요.
import java.util.Random;
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
// 난수 생성
IntStream ints = new Random().ints();
ints.forEach(System.out::println);
}
}
int 형의 범위에서 출력값이 무한대로 생성되는 걸 확인하셨나요?
이렇게 스트림의 크기가 정해지지 않은 것을 **무한 스트림(infinite stream)**이라 부르는데, 무한 스트림은 주로 뒤에서 배우게 될 limit() 메서드와 함께 사용하거나 매개변수로 스트림의 사이즈를 전달해서 그 범위를 제한할 수 있습니다.
import java.util.Random;
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
// 스트림 생성의 범위를 5개로 제한
IntStream ints = new Random().ints(5);
IntStream ints = new Random().ints().limit(5);
ints.forEach(System.out::println);
}
}
추가로, IntStream과 LongStream에 정의된 range() 나 rangeClosed() 메서드를 사용하면 다음과 같이 특정 범위의 정수값을 스트림으로 생성해서 반환하는 것도 가능합니다.
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
//특정 범위의 정수
IntStream intStream = IntStream.rangeClosed(1, 10);
intStream.forEach(System.out::println);
}
}
//출력값
12345678910
rangeClosed()와 range()의 차이는 두 번째로 전달되는 매개 변수가 범위 안에 포함되는지 여부에 따라 구분될 수 있습니다. rangeClosed()는 끝 번호가 포함되어 위의 코드 예제처럼 1~10까지의 숫자가 출력되는 반면, range()의 경우에는 끝번호가 포함되지 않아 1~9까지의 숫자가 출력됩니다.
이렇게 스트림의 생성에 대한 핵심적인 내용들을 살펴봤습니다. 이어지는 챕터에서는 스트림의 생성 이후에 사용할 수 있는 중간 연산자에 대해 학습해 보도록 하겠습니다.
스트림의 중간 연산
이제 스트림의 생성 이후에 수행할 수 있는 중간 연산자(Intermediate Operation)에 대해 알아보도록 하겠습니다.
앞서 스트림의 특징에서 설명한 것처럼, 스트림의 중간 연산자의 결과는 스트림을 반환하기 때문에 여러 개의 연산자를 연결하여 우리가 원하는 데이터 처리를 수행할 수 있다고 했습니다.

여기 Oracle에서 제공하는 공식 스펙 문서를 확인해 보면 다양한 중간 연산자들을 확인해 볼 수 있는데, 여기서는 그중에서 가장 빈번하게 사용되는 필터링(filtering), 매핑(maping), 정렬(sorting) 등 몇 가지 중간 연산자를 중심으로 설명하도록 하겠습니다.
중간 연산자에 관한 내용은 처음부터 모든 내용을 완벽하게 숙지하려 하기보다는, 주로 많이 사용되는 중간 연산자들의 기본적인 개념과 사용법을 먼저 잘 이해한 후에, 다음에 필요하다면 검색을 통해 추가적인 내용들을 찾아 활용하시는 방향을 권장해 드립니다.
먼저 구체적인 내용을 확인하기 전에, 전체적인 코드 구조를 파악해 보도록 하겠습니다.

위의 코드는 앞에서 우리가 확인했던 도식을 코드화하여 보여주고 있습니다.
우리가 스트림의 핵심 개념과 특징에서 살펴본 것처럼, 최초에 데이터 소스를 가지고 스트림을 생성한 후에 중간 연산자로 데이터를 가공하고, 최종 연산자를 통해서 스트림 작업을 종료합니다. 위의 코드 구조를 먼저 머릿속에 그려두고 이어지는 아래의 내용들을 하나씩 학습해 가시길 바랍니다.
그럼, 이제부터 본격적으로 중간 연산자들을 하나씩 살펴보겠습니다.
필터링(filter() , distinct() )
필터링은 이름 그대로 우리의 필요에 따라 조건에 맞는 데이터들만을 정제하는 역할을 하는 중간 연산자를 가리킵니다.
- distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용합니다.
- filter(): Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어냅니다. filter() 메서드에는 매개값으로 조건(Predicate)을 주고, 조건이 참이 되는 요소만 필터링합니다. 여기서 조건은 람다식을 사용하여 정의할 수 있습니다.
import java.util.Arrays;
import java.util.List;
public class FilteringExample {
public static void main(String[] args) throws Exception {
List<String> names = Arrays.asList("김코딩", "이자바", "박해커", "김코딩", "박해커");
names.stream()
.distinct() //중복 제거
.forEach(element -> System.out.println(element));
System.out.println();
names.stream()
.filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링
.forEach(element -> System.out.println(element));
System.out.println();
names.stream()
.distinct() //중복제거
.filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링
.forEach(element -> System.out.println(element));
}
}
// 출력값
김코딩
이자바
박해커
김코딩
김코딩
김코딩
위의 코드 예제는 필터링의 과정을 순차적으로 잘 보여주고 있습니다. 가장 첫 번째 스트림에서는 중복 제거 중간 연산만을 수행하여 리스트 타입의 참조변수 names 안에 중복되고 있는 “김코딩” 요소를 제거했습니다. 두 번째 스트림에서는 “김”씨 성을 가진 요소들만 필터링하여 “김코딩”을 두 번 출력해주고 있습니다.
지금쯤이면 이제 잘 이해하고 계시겠지만, 첫 번째와 두 번째 스트림은 각각 독립적인 스트림입니다. 대표적인 최종 연산자인 forEach()로 첫 번째 스트림이 닫히고 난 후에 두 번째 스트림이 생성되어 새로운 연산을 수행하고 있습니다.
마지막 세 번째 스트림은 중복제거와 필터링을 모두 수행하였고, 그 결과로 “김코딩”이라는 문자열을 단 한 번 출력해주고 있습니다.
매핑(map())
매핑은 스트림 내 요소들에서 원하는 필드만 추출하거나 특정 형태로 변환할 때 사용하는 중간 연산자입니다. 위의 filter() 메서드와 마찬가지로 값을 변환하기 위한 조건을 람다식으로 정의합니다.
아래 예제를 통해 확인해 보겠습니다.
import java.util.Arrays;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("kimcoding", "javalee", "hackerna", "luckyguy");
names.stream()
.map(element -> element.toUpperCase()) // 요소들을 하나씩 대문자로 변환
.forEach(element->System.out.println(element));
}
}
// 출력값
KIMCODING
JAVALEE
HACKERNA
LUCKYGUY
위의 예제는 리스트 타입의 참조변수 names 안에 정의된 각 요소를 순회하면서 소문자 이름을 대문자로 변환한 값들이 담긴 스트림으로 반환하는 연산 과정을 보여주고 있습니다.
또한, 다음과 같이 각 요소에 어떤 연산을 실행하고 난 후의 값을 반환받을 수 있습니다. 아래의 코드 예제는 각 요소에 값에 3을 곱한 값을 스트림으로 반환하여 출력하는 과정을 보여주고 있습니다.
import java.util.Arrays;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args)
{
List<Integer> list = Arrays.asList(1, 3, 6, 9);
// 각 요소에 3을 곱한 값을 반환
list.stream().map(number -> number * 3).forEach(System.out::println);
}
}
// 출력값
3
9
18
27
map()과 함께 많이 사용되는 flatMap() 중간 연산자도 있습니다.
예제를 통해 그 차이를 이해해 보겠습니다.
만약에 아래와 같은 이중 배열이 있고, 그 안의 배열을 위에서 학습한 map() 메서드를 사용하여 하나씩 출력해 주는 프로그램을 만들어야 한다고 가정해 보겠습니다.
// 주어진 이중 배열
String[][] namesArray = new String[][]{{"박해커", "이자바"}, {"김코딩", "나박사"}};
// 기대하는 출력값
박해커
이자바
김코딩
나박사
이제 앞에서 배웠던 방식으로 map()을 사용하여 기대하는 값을 출력해 보겠습니다.
// map() 사용
Arrays.stream(namesArray)
.map(inner -> Arrays.stream(inner))
.forEach(System.out::println);
// 출력값
java.util.stream.ReferencePipeline$Head@3cb5cdba
java.util.stream.ReferencePipeline$Head@56cbfb61
출력값은 우리의 기대와는 다르게 위와 같은 스트림 객체의 값을 반환할 것입니다.
왜냐하면 위의 연산에서 map() 메서드는 Stream<Stream<String>> 즉 중첩 스트림을 반환하고 있기 때문입니다. 여기서 우리가 원하는 결과값을 출력하기 위해서는 반환 타입이 Stream<Stream<String>> 이 아닌 Stream<String> 이 되어야 합니다.
그렇다면 우리가 원하는 결과값을 얻기 위해서 어떻게 해야 할까요?
첫 번째 방법은 아래와 같이 위의 코드를 조금 수정하는 것입니다.
- 기존 방식의 수정
// map 사용
Arrays.stream(namesArray)
.map(inner -> Arrays.stream(inner))
.forEach(names -> names.forEach(System.out::println));
// 출력값
박해커
이자바
김코딩
나박사
위의 코드에서 확인할 수 있는 것처럼, forEach() 메서드 안의 람다식의 정의에서, 각 요소에 대하여 다시 forEach() 메서드를 출력함으로써 뎁스가 있는 요소들에 접근하여 이를 출력할 수 있습니다.
하지만 지금처럼 이중구조가 아닌 뎁스가 3중, 4중, 5중으로 깊어지는 경우는 어떨까요?
아마 코드를 작성하는 개발자 입장에서도 매우 번거롭고 작성된 코드 또한 가독성이 떨어질 것입니다.
이런 경우, 우리는 flatMap()을 활용할 수 있습니다.
- flatMap()
// flatMap()
Arrays.stream(namesArray).flatMap(Arrays::stream).forEach(System.out::println);
// 출력값
박해커
이자바
김코딩
나박사
위의 코드에서 확인할 수 있는 것처럼, flatMap() 은 중첩 구조를 제거하고 단일 컬렉션(Stream<String>)으로 만들어주는 역할을 합니다. 이를 요소들을 “평평하게”한다는 의미에서 플래트닝(flattening)이라고 합니다.
이처럼, 위와 같이 배열 요소들의 뎁스가 있는 작업들을 수행할 때 flatMap() 메서드를 활용하면 훨씬 간편하고 효과적으로 같은 작업을 수행할 수 있습니다.
정렬(sorted())
sorted() 메서드는 이름처럼 정렬할 때 사용하는 중간 연산자입니다.
sorted() 메서드를 사용하여 정렬할 때는 괄호(()) 안에 Comparator라는 인터페이스에 정의된 static 메서드와 디폴트 메서드를 사용하여 간편하게 정렬 작업을 수행할 수 있습니다. 괄호 안에 아무 값도 넣지 않은 상태로 호출하면 기본 정렬(오름차순)로 정렬됩니다.
마찬가지로, 예시를 통해 좀 더 알아보겠습니다.
- 기본 정렬
import java.util.Arrays;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 동물들의 이름을 모아둔 리스트
List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");
// 인자값 없는 sort() 호출
animals.stream().sorted().forEach(System.out::println);
}
}
// 출력값
Cow
Duck
Horse
Lion
Monkey
Tiger
- 역순으로 정렬
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args) {
List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");
// 인자값에 Comparator 인터페이스에 규정된 메서드 사용
animals.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
}
}
// 출력값
Tiger
Monkey
Lion
Horse
Duck
Cow
위의 기본 정렬은 생략하고, 아래 요소들의 값을 역순으로 정렬하고 출력하고 있는 아래의 코드만 잠시 살펴보겠습니다. 앞서 언급했던 것처럼, Comparator 인터페이스 안에 정의된 reverseOrder()라는 이름의 메서드를 인자로 넘겨 호출하는 것만으로 손쉽게 역순으로 정렬하는 기능을 실행할 수 있습니다.
Comparator 인터페이스와 그 안에 정의된 더 다양한 정렬 기능들이 궁금하시다면, 여기 공식 스펙 문서를 통해 한번 천천히 확인해 보시기를 바랍니다.
기타
앞에서 언급한 중간 연산자 외에 다음과 같은 중간 연산자들이 있습니다. 아래 중간 연산자들은 비교적 쉽고 이해하기 어렵지 않기 때문에 코드 예제에 대한 추가적인 설명은 생략하도록 하겠습니다.
- skip() - 스트림의 일부 요소들을 건너뜁니다.
import java.util.stream.IntStream;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 1~10 범위의 정수로 구성된 스트림 생성
IntStream intStream = IntStream.rangeClosed(1, 10);
// 앞의 5개의 숫자를 건너뛰고 숫자 6부터 출력
intStream.skip(5).forEach(System.out::println);
}
}
// 출력값
6
7
8
9
10
- limit() - 스트림의 일부를 자릅니다.
import java.util.stream.IntStream;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 1~10 범위의 정수로 구성된 스트림 생성
IntStream intStream = IntStream.rangeClosed(1, 10);
// 앞에서부터 5개의 숫자만 출력
intStream.limit(5).forEach(System.out::println);
}
}
// 출력값
1
2
3
4
5
- peek() - forEach()와 마찬가지로, 요소들을 순회하며 특정 작업을 수행합니다. forEach()와의 핵심적인 차이는 중간 연산자인지의 여부입니다. peek()는 중간 연산자이기 때문에 여러 번 연결하여 사용할 수 있지만, forEach()는 최종 연산자이기 때문에 마지막에 단 한 번만 사용할 수 있습니다. 이러한 peek()의 특성 때문에 주로 코드의 에러를 찾기 위한 디버깅(debugging) 용도로 종종 활용됩니다.
import java.util.stream.IntStream;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 요소들을 사용하여 IntStream 생성
IntStream intStream3 = IntStream.of(1, 2, 2, 3, 3, 4, 5, 5, 7, 7, 7, 8);
// 짝수만 필터링하여 합계 구하기
int sum = intStream3.filter(element -> element % 2 == 0)
.peek(System.out::println)
.sum();
System.out.println("합계 = " + sum);
}
}
// 출력값
2
2
4
8
합계 = 16
지금까지 스트림의 중간 연산자에 대해서 알아봤습니다. 이어지는 챕터에서는 중간 연산자 이후에 스트림 파이프라인의 최종 단계를 담당하는 최종 연산자에 대해서 알아보도록 하겠습니다.
스트림의 최종 연산
이제 스트림 파이프라인의 세 단계 중 마지막에 해당하는 최종 연산(Terminal Operation)에 대해서 알아보도록 하겠습니다.
사실 그중에 하나는 이미 우리가 익숙하게 잘 알고 있습니다. 뭘까요?
맞습니다. 지금까지 우리는 중간 연산자에 대해 학습하면서 그 연산의 결과를 확인하기 위한 수단으로 forEach() 메서드를 사용해 왔습니다. 그리고 forEach() 메서드는 대표적인 최종 연산자입니다.
다르게 표현해 보면, forEach() 메서드가 스트림 파이프라인에서 최종적으로 사용되고 나면, 해당 스트림은 닫히고 모든 연산이 종료된다는 의미입니다.

또한, 앞에서 자세히 다루지 않았지만 중간 연산은 최종 연산자가 수행될 때야 비로소 스트림의 요소들이 중간 연산을 거쳐 가공된 후에 최종 연산에서 소모되는데 이를 좀 어려운 말로 “지연된 연산(lazy evaluation)”이라고 부릅니다.
다시 돌아가서, 스트림에는 forEach() 메서드 외에도 다양한 최종 연산자들이 존재합니다. 앞서 중간 연산자 챕터에서 강조했던 것처럼, 지금 단계에서 모든 최종 연산자를 완벽하게 알고 적절하게 사용하기는 매우 어려운 일입니다.
따라서 아래 내용을 통해 기본적인 개념과 사용법을 먼저 이해한 후에 그때그때 필요에 따라 추가적인 검색을 통해 비어있는 부분들을 채워가시는 공부 방향을 추천드립니다. 참고로 forEach()의 경우, 우리가 앞의 예제들을 통해 계속 사용해 왔기 때문에 여기서 따로 다루지는 않겠습니다.
다시 강조하자면, 반드시 눈으로만 확인하지 마시고 본인의 인텔리제이에서 직접 입력해 보면서 내용을 체화하시길 바랍니다.
그럼, 이제 최종 연산자에 대해서 하나씩 살펴보도록 하겠습니다.
기본 집계(sum() , count() , average(), max() , min())
가장 간단한 부분부터 먼저 살펴볼까요?
앞에서 잠깐 언급했던 것처럼, 숫자와 관련된 기본적인 집계의 경우에는 대부분 최종 연산자라고 생각해도 크게 틀리지 않습니다.
아래 예제 코드와 같이 최종연산자를 사용하여 숫자와 관련된 데이터 처리를 간편하게 수행할 수 있습니다.
- 기본 집계
import java.util.Arrays;
public class TerminalOperationExample {
public static void main(String[] args) {
// int형 배열 생성
int[] intArray = {1,2,3,4,5};
// 카운팅
long count = Arrays.stream(intArray).count();
System.out.println("intArr의 전체 요소 개수 " + count);
// 합계
long sum = Arrays.stream(intArray).sum();
System.out.println("intArr의 전체 요소 합 " + sum);
// 평균
double average = Arrays.stream(intArray).average().getAsDouble();
System.out.println("전체 요소의 평균값 " + average);
// 최대값
int max = Arrays.stream(intArray).max().getAsInt();
System.out.println("최대값 " + max);
// 최소값
int min = Arrays.stream(intArray).min().getAsInt();
System.out.println("최소값 " + min);
// 배열의 첫 번째 요소
int first = Arrays.stream(intArray).findFirst().getAsInt();
System.out.println("배열의 첫 번째 요소 " + first);
}
}
// 출력값
intArr의 전체 요소 개수 5
intArr의 전체 요소 합 15
전체 요소의 평균값 3.0
최대값 5
최소값 1
배열의 첫 번째 요소 1
위의 코드 예제의 경우, 대부분 코드를 확인하면 직관적으로 이해할 수 있기 때문에 따로 설명은 생략하고, 딱 한 가지만 언급하고 지나가도록 하겠습니다.
혹시 평균, 최대값, 최소값, 배열의 첫 번째 요소를 구하는 예제 코드에서 의문이 생기지 않으셨나요?
분명 스트림의 최종 연산자로 스트림이 닫힌다고 배웠었는데, 뒤에 getAsInt() 또는 getAsDouble() 메서드가 다시 붙고 있습니다. 이 부분을 어떻게 이해할 수 있을까요?
결론적으로 말하면, 최종 연산자로 스트림이 닫힌다는 사실에는 변함이 없습니다.
이해를 돕기 위해, 평균을 구하는 스트림 작업에 해당하는 예제 코드를 잠시 가져와 쪼개보겠습니다.
- 기존 코드
double average = Arrays.stream(intArray).average().getAsDouble();
- 재작성한 코드
import java.util.Arrays;
import java.util.OptionalDouble;
public class TerminalOperationExample {
public static void main(String[] args) {
// int형 배열 생성
int[] intArr = {1,2,3,4,5};
// 평균값을 구해 Optional 객체로 반환
OptionalDouble average = Arrays.stream(intArr).average();
System.out.println(average);
// 기본형으로 변환
double result = average.getAsDouble();
System.out.println("전체 요소의 평균값 " + result);
}
}
//출력값
OptionalDouble[3.0]
전체 요소의 평균값 3.0
아래 재작성한 코드는 기존의 코드와 완전히 동일합니다. 구분하니까 이제 어떤 일이 벌어졌는지 좀 더 명확하게 보입니다. average() 연산자가 반환하는 값의 반환 타입을 확인해 보니 OptionalDouble이라고 되어있습니다.
참고로 지금 자세한 내용까지 알 필요는 없지만, OptionalDouble 클래스는 일종의 래퍼 클래스로, null 값으로 인해서 NullPointerException (NPE) 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하기 위한 목적으로 도입된 것입니다. 즉, 연산 결과를 Optional 객체 안에 담아서 반환하면, 따로 if문을 사용한 조건문으로 반환된 결과가 null인지 여부를 체크하지 않아도 에러가 발생하지 않도록 코드를 작성할 수 있습니다.
다시 돌아가서, 결론적으로 average() 최종연산자가 반환하는 값의 타입이 OptionalDouble , 즉 래퍼 클래스 객체로 되어있기 때문에 우리가 원하는 기본형으로 변환하는 과정이 한 번 더 필요하다는 사실이 중요합니다. 이름에서 알 수 있듯이, getAsDouble()과 getAsInt()는 객체로 반환되는 값을 다시 기본형으로 변환하기 위해 사용되는 메서드로 스트림 파이프라인과는 관계가 없다는 사실을 기억하도록 합시다.
혹시 Optional 객체와 관련된 추가적인 내용이 궁금하시다면, 여기 공식 스펙 문서를 통해 좀 더 살펴보는 것도 좋은 공부가 될 것입니다.
매칭(allMatch(), anyMatch(), noneMatch() )
다음으로 매칭과 관련한 최종연산자를 살펴보겠습니다.
match() 메서드를 사용하면 조건식 람다 Predicate를 매개변수로 넘겨 스트림의 각 데이터 요소가 특정한 조건을 충족하는지 않는지 검사하여, 그 결과를 boolean 값으로 반환합니다.
- *match() 메서드*는 크게 다음의 3가지 종류가 있습니다.
- allMatch() - 모든 요소가 조건을 만족하는지 여부를 판단합니다.
- noneMatch() - 모든 요소가 조건을 만족하지 않는지 여부를 판단합니다.
- anyMatch() - 하나라도 조건을 만족하는 요소가 있는지 여부를 판단합니다.
- 매칭
import java.util.Arrays;
public class TerminalOperationExample {
public static void main(String[] args) throws Exception {
// int형 배열 생성
int[] intArray = {2,4,6};
// allMatch()
boolean result = Arrays.stream(intArray).allMatch(element-> element % 2 == 0);
System.out.println("요소 모두 2의 배수인가요? " + result);
// anyMatch()
result = Arrays.stream(intArray).anyMatch(element-> element % 3 == 0);
System.out.println("요소 중 하나라도 3의 배수가 있나요? " + result);
// noneMatch()
result = Arrays.stream(intArray).noneMatch(element -> element % 3 == 0);
System.out.println("요소 중 3의 배수가 하나도 없나요? " + result);
}
}
// 출력값
요소 모두 2의 배수인가요? true
요소 중 하나라도 3의 배수가 있나요? true
요소 중 3의 배수가 하나도 없나요? false
이 부분은 크게 어렵지 않기 때문에 따로 추가적인 설명을 덧붙이지 않겠습니다. 본인의 인텔리제이에서 직접 입력해 보고 충분히 연습해 보세요.
요소 소모(reduce())
이름에서 유추할 수 있듯이, reduce() 최종 연산자는 스트림의 요소를 줄여나가면서 연산을 수행하고 최종적인 결과를 반환합니다.
사실 스트림의 최종 연산은 모두 요소를 소모하여 연산을 수행하지만, reduce() 메서드의 경우에는 먼저 첫 번째와 두 번째 요소를 가지고 연산을 수행하고, 그 결과와 다음 세 번째 요소를 가지고 또다시 연산을 수행하는 식으로 연산이 끝날 때까지 반복합니다.
그렇기 때문에 reduce() 메서드의 매개변수 타입은 앞서 람다와 관련 함수형 인터페이스에서 배웠던 BinaryOperator<T> 로 정의되어 있습니다.
Optional<T> reduce(BinaryOperator<T> accumulator)
참고로, reduce() 메서드는 최대 3개까지 매개변수를 받을 수 있는데, 지금은 2개를 받는 경우까지만 알고 넘어가도 충분합니다.
T reduce(T identity, BinaryOperator<T> accumulator)
위에서 첫 번째 매개변수 identity는 특정 연산을 시작할 때 설정되는 초기값을 의미합니다. 두 번째 accumulator는 각 요소를 연산하여 나온 누적된 결과값을 생성하는 데 사용하는 조건식입니다.
아래 예제를 통해서 좀 더 알아보겠습니다.
- reduce()
import java.util.Arrays;
public class TerminalOperationExample {
public static void main(String[] args) throws Exception {
int[] intArray = {1,2,3,4,5};
// sum()
long sum = Arrays.stream(intArray).sum();
System.out.println("intArray 전체 요소 합: " + sum);
// 초기값이 없는 reduce()
int sum1 = Arrays.stream(intArray)
.map(element -> element * 2)
.reduce((a , b) -> a + b)
.getAsInt();
System.out.println("초기값이 없는 reduce(): " + sum1);
// 초기값이 있는 reduce()
int sum2= Arrays.stream(intArray)
.map(element -> element * 2)
.reduce(5, (a ,b) -> a + b);
System.out.println("초기값이 있는 reduce(): " + sum2);
}
}
// 출력값
intArray 전체 요소 합: 15
초기값이 없는 reduce(): 30
초기값이 있는 reduce(): 35
첫 번째 예제는 우리가 익히 알고 있는 sum() 메서드를 활용하여 숫자 요소들의 총합을 도출하는 스트림 작업입니다. 1+2+3+4+5의 결과로 숫자 15 가 나왔습니다.
두 번째와 세 번째는 모두 reduce() 메서드를 사용하고 있지만, 세 번째의 경우에는 초기값으로 5 가 설정되어 있기 때문에 최종적인 연산의 결과가 두 번째 예제보다 5 가 더 많은 20 이 출력되었습니다.
위 배열의 값을 사용하여 조금 더 구체적인 흐름을 살펴보면 다음과 같습니다.
- accumulator: (a, b) -> a + b (a: 누적된 값, b: 새롭게 더해질 값)
- 최초 연산 시 1+2 → a: 3, b: 3
- 3+3 → a: 6, b: 4
- 6+4 → a: 10, b: 5
- 10+5 → 최종 결과:15
참고로, 앞서 배웠던 count()와 sum()과 같은 집계 메서드 또한 내부적으로 모두 reduce()를 사용하여 연산을 수행합니다.
요소 수집(collect())
스트림은 중간 연산을 통한 요소들의 데이터 가공 후 요소들을 수집하는 최종 처리 메서드인 collect()를 지원합니다. 좀 더 구체적으로, 스트림의 요소들을 List, Set, Map 등 다른 타입의 결과로 수집하고 싶은 경우에 collect() 메서드를 유용하게 사용할 수 있습니다.
collect() 메서드는 Collector 인터페이스 타입의 인자를 받아서 처리할 수 있는데, 직접 구현하거나 미리 제공된 것들을 사용할 수 있습니다. 참고로, 빈번하게 사용되는 기능들은 Collectors 클래스에서 제공하고 있습니다.

위의 그림은 Collectors 클래스에 정의된 기능 중에 스트림의 요소들을 List, Set, Map 등 다른 타입의 결과로 수집할 수 있는 정적 메서드들을 보여주고 있습니다.
사실 collect() 메서드는 단순히 요소를 수집하는 기능 이외에도 요소 그룹핑 및 분할 등 다른 기능들을 제공합니다. 여기서는 따로 세부적인 내용들을 다루지는 않겠습니다.
그럼, 이제 간단한 예제를 살펴보겠습니다.
- collect()
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class TerminalOperationExample {
public static void main(String[] args) {
// Student 객체로 구성된 배열 리스트 생성
List<Student> totalList = Arrays.asList(
new Student("김코딩", 100, Student.Gender.Male),
new Student("박해커", 80, Student.Gender.Male),
new Student("이자바", 90, Student.Gender.Female),
new Student("나미녀", 60, Student.Gender.Female)
);
// 스트림 연산 결과를 Map으로 반환
Map<String, Integer> maleMap = totalList.stream()
.filter(s -> s.getGender() == Student.Gender.Male)
.collect(Collectors.toMap(
student -> student.getName(), // Key
student -> student.getScore() // Value
));
// 출력
System.out.println(maleMap);
}
}
class Student {
public enum Gender {Male, Female};
private String name;
private int score;
private Gender gender;
public Student(String name, int score, Gender gender) {
this.name = name;
this.score = score;
this.gender = gender;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
public Gender getGender() {
return gender;
}
}
// 출력값
{김코딩=100, 박해커=80}
이제 위의 예제 코드를 순차적으로 한번 살펴보겠습니다. 먼저 4개의 Student 타입의 객체로 이뤄진 리스트 배열을 만들었습니다. 보시는 것처럼 “김코딩”과 “김인기”라는 이름의 남성 2명과 “이자바”와 “나미녀”라는 이름의 여성 2명이 있습니다.
이렇게 리스트 배열로 이뤄진 데이터 요소들에서 남학생들의 이름과 점수만을 추출하여 이름과 점수를 각각 키(key)와 값(value)으로 하는 Map 타입의 결과를 수집하고자 할 때 collect() 메서드가 위의 코드처럼 유용하게 사용될 수 있습니다.
흐름을 간략하게 설명해 보면, 먼저 리스트 배열에 스트링을 생성하고, 중간 연산자 filter() 메서드를 통해 성별이 남자인 학생들만 필터링한 후, 마지막으로 최종 연산자 collect()에 Collectors 클래스 안에 정의된 정적 메서드(toMap())를 사용하면 아래 출력값과 같이 우리가 의도했던 대로 Map 타입의 결과물을 받아볼 수 있습니다.
기타
앞에서 언급한 최종 연산자들 외에도 findAny() , findFirst() , toArray() 등의 최종 연산자가 있지만, 따로 여기서 다루지는 않겠습니다. 이제 스트림과 관련해서 필요한 모든 핵심적인 개념과 지식은 모두 전달드렸습니다. 직접 활용을 충분히 해보시고, 추가적인 이해가 필요한 부분들은 스스로 검색을 통해 찾아가면서 이해를 넓혀가시기를 권장해 드립니다.
Optional Class
Optional<T>은 NullPointerException(NPE), 즉 null 값으로 인해 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입되었습니다.
연산 결과를 Optional에 담아서 반환하면, 따로 조건문을 작성해주지 않아도 NPE가 발생하지 않도록 코드를 작성할 수 있습니다.
Optional<T>
Optional 클래스는 모든 타입의 객체를 담을 수 있는 래퍼(Wrapper) 클래스입니다.
public final class Optional<T> {
private final T value; // T타입의 참조변수
}
Optional 객체를 생성하려면 of() 또는 ofNullable()을 사용합니다. 참조변수의 값이 null일 가능성이 있다면, ofNullable()을 사용합니다.
Optional<String> opt1 = Optional.ofNullable(null);
Optional<String> opt2 = Optional.ofNullable("123");
System.out.println(opt1.isPresent()); //Optional 객체의 값이 null인지 아닌지를 리턴합니다.
System.out.println(opt2.isPresent());
Optional<T> 타입의 참조변수를 기본값으로 초기화하려면 empty() 메서드를 사용합니다.
Optional<String> opt3 = Optional.<String>empty();
Optional 객체에 객체에 저장된 값을 가져오려면 get()을 사용합니다.
참조변수의 값이 null일 가능성이 있다면 orElse()메서드를 사용해 디폴트 값을 지정할 수 있습니다.
Optional<String> optString = Optional.of("java");
System.out.println(optString);
System.out.println(optString.get());
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("kimcoding");
System.out.println(name);
Optional 객체는 스트림과 유사하게 여러 메서드를 연결해서 작성할 수 있습니다(이를 메서드 체이닝이라고 합니다).
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
List<String> languages = Arrays.asList(
"Ruby", "Python", "Java", "Go", "Kotlin");
Optional<List<String>> listOptional = Optional.of(languages);
int size = listOptional
.map(List::size)
.orElse(0);
System.out.println(size);
}
}
Optional 객체에서 제공하는 전체 메서드는 공식문서를 참고하세요.
심화 학습 (Optional)
위 내용을 모두 실습하고 시간이 남는다면, 다음 예제를 다양하게 변형해보며 Optional 객체 활용법을 탐구해보세요.
//CarClub.java
package com.jungmin.optional;
public class CarClub {
public static void main(String[] args) {
MemberService memberService = new MemberService();
String insuranceName =
memberService
.getMember("jungmin")
.flatMap(Member::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getCompanyName)
.orElse("Not result");
}
}
//MemberService.java
package com.jungmin.optional;
import java.util.Optional;
public class MemberService {
public Optional<Member> getMember(String id) {
Optional<Insurance> insurance = Optional.of(new Insurance("Samsung direct"));
Optional<Car> car = Optional.of(new Car("XC-60", 60_000_000, insurance));
return Optional.of(new Member("Jungmin", "jungmin0102", car));
}
}
//Member.java
package com.jungmin.optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Optional;
@Getter
@AllArgsConstructor
public class Member {
String name;
String id;
Optional<Car> car;
}
//Insurance.java
package com.jungmin.optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class Insurance {
String companyName;
}
package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class B_getNthElementTest {
B_getNthElement solution;
@BeforeEach
void setUp() {
solution = new B_getNthElement();
}
@Test
@DisplayName("빈 List가 입력되었을때, null을 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>();
assertThat(solution.getNthElement(input, 0)).isEqualTo(null);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 3을 입력받았을 경우 3를 리턴해야 합니다.")
public void testFoo2() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 3)).isEqualTo(3);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 2을 입력받았을 경우 2를 리턴해야 합니다.")
public void testFoo3() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 2)).isEqualTo(2);
}
@Test
@DisplayName("[9, 5, 3, 1, 2], 3을 입력받았을 경우 1를 리턴해야 합니다.")
public void testFoo4() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(9, 5, 3, 1, 2));
assertThat(solution.getNthElement(input, 3)).isEqualTo(1);
}
}
[컬렉션] 연습문제
01_makeArrayList
문제
ArrayList를 선언하고 1부터 5까지 담은 뒤 리턴해야 합니다.
출력
- Integer 타입의 ArrayList를 반해야 합니다.
주의 사항
- ArrayList를 선언한 후 값을 담아 리턴해야 합니다.
입출력 예시
ArrayList<Integer> output = makeArrayList();
System.out.println(output); // --> [1, 2, 3, 4, 5]
힌트
- 메서드의 제목(makeArrayList)을 활용해 검색해 봅니다(java how make arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class A_makeArrayList {
public ArrayList<Integer> makeArrayList() {
// TODO:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class A_makeArrayList {
public ArrayList<Integer> makeArrayList() {
// TODO:
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
arrayList.add(4);
arrayList.add(5);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class A_makeArrayListTest {
A_makeArrayList solution;
@BeforeEach
void setUp() {
solution = new A_makeArrayList();
}
@Test
@DisplayName("정확한 List를 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
assertThat(solution.makeArrayList()).isEqualTo(input);
}
}
02_computeAverageOfNumbers
문제
Integer 타입의 ArrayList와 수를 입력받아 수가 인덱스로 가리키는 ArrayList의 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입의 arrayList
인자 2 : index
- int 타입의 인덱스 (0 이상의 정수)
출력
- Integer 타입을 리턴해야 합니다.
주의 사항
- 빈 ArrayList를 입력받은 경우, null을 리턴해야 합니다.
- ArrayList의 크기를 벗어나는 인덱스를 입력받은 경우, null을 리턴해야 합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 2, 4, 6, 8, 10]
Integer output = getNthElement(arryaList, 2);
System.out.println(output); // --> 4
힌트
- 생성된 ArrayList에 get() 메서드를 통해 특정 요소를 조회할 수 있습니다.
내 코드
package com.choongang;
import java.util.*;
public class B_getNthElement {
public Integer getNthElement(ArrayList<Integer> arrayList, int index) {
// TODO:
Integer output = null;
if (arrayList.isEmpty()) {
return null;
} else if(arrayList.size() < index){
return null;
}
output = arrayList.get(index);
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class B_getNthElement {
public Integer getNthElement(ArrayList<Integer> arrayList, int index) {
// TODO:
if (arrayList.size() == 0) {
return null;
} else if (index >= arrayList.size()) {
return null;
}
return arrayList.get(index);
}
}
Test 코드
package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class B_getNthElementTest {
B_getNthElement solution;
@BeforeEach
void setUp() {
solution = new B_getNthElement();
}
@Test
@DisplayName("빈 List가 입력되었을때, null을 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>();
assertThat(solution.getNthElement(input, 0)).isEqualTo(null);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 3을 입력받았을 경우 3를 리턴해야 합니다.")
public void testFoo2() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 3)).isEqualTo(3);
}
@Test
@DisplayName("[0, 1, 2, 3, 4, 5, 6, 7], 2을 입력받았을 경우 2를 리턴해야 합니다.")
public void testFoo3() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7));
assertThat(solution.getNthElement(input, 2)).isEqualTo(2);
}
@Test
@DisplayName("[9, 5, 3, 1, 2], 3을 입력받았을 경우 1를 리턴해야 합니다.")
public void testFoo4() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(9, 5, 3, 1, 2));
assertThat(solution.getNthElement(input, 3)).isEqualTo(1);
}
}
03_getLastElement
문제
문자열타입의 ArrayList를 입력받아 마지막 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입의 ArrayList
출력
- String 타입을 리턴해야 합니다.
주의 사항
- 빈 ArrayList의 경우 null을 리턴해야 합니다.
입출력 예시
<String> arrayList; // ["코", "드", "자", "바", "스", "프", "링"]
String output = getLastElement(arrayList);
System.out.println(output); // --> "링"
힌트
- size() 메서드를 통해 총 요소가 몇 개인지 확인할 수 있습니다.
- ArrayList의 인덱스는 0부터 시작합니다.
- 생성된 ArrayList에 get() 메서드를 통해 특정 요소를 조회할 수 있습니다.
내 코드
package com.choongang;
import java.util.ArrayList;
public class C_getLastElement {
public String getLastElement(ArrayList<String> arrayList) {
// TODO:
String output = null;
if (arrayList.isEmpty()) {
return null;
}
output = arrayList.get(arrayList.size() - 1);
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class C_getLastElement {
public String getLastElement(ArrayList<String> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return null;
} else {
return arrayList.get(arrayList.size() - 1);
}
}
}
Test 코드
package com.choongang;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
class C_getLastElementTest {
C_getLastElement solution;
@BeforeEach
void setUp() {
solution = new C_getLastElement();
}
@Test
@DisplayName("빈 ArrayList의 경우 null을 리턴해야 합니다.")
public void testFoo() {
ArrayList<String> list = new ArrayList<>(Arrays.asList());
assertThat(solution.getLastElement(list)).isEqualTo(null);
}
@Test
@DisplayName("[\"사과\", \"바나나\", \"포도\", \"오렌지\"]를 입력받은 경우 \"오렌지\"를 리턴해야 합니다.")
public void testFoo2() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("사과", "바나나", "포도","오렌지"));
assertThat(solution.getLastElement(list)).isEqualTo("오렌지");
}
@Test
@DisplayName("[\"코드\", \"스프링\", \"코딩\"]를 입력받은 경우, \"코딩\"을 리턴해야 합니다.")
public void testFoo3() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("코드", "스프링", "코딩"));
assertThat(solution.getLastElement(list)).isEqualTo("코딩");
}
}
04_addLast
문제
String 타입을 요소로 가지는 ArrayList와 문자열 요소를 입력받아, 주어진 요소를 ArrayList의 맨 뒤에 추가하고 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입을 요소로 가지는 ArrayList
인자 2 : str
- String 타입의 임의의 문자열
출력
- 기존 ArrayList에 주어진 요소가 추가된 형태의 ArrayList를 리턴해야 합니다.
- [arrarList[0], arrarList[1], ..., arrayList[n-1], str]
- arrayList.size()는 n
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 새로운 ArrayList를 선언 / 할당해서 리턴하지 않습니다.
- 기존 ArrayList에 주어진 요소가 추가된 상태(주소값 동일)로 리턴해야 합니다.
입출력 예시
ArrayList<String> arrayList; // ["a", "b"]
ArrayList<String> output = addLast(arrayList, "c");
System.out.println(output); // --> ["a", "b", "c"]
힌트
- 메서드의 제목(addLast)을 활용해 검색해 봅니다(java how to add last in arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class D_addLast {
public ArrayList<String> addLast(ArrayList<String> arrayList, String str) {
// TODO:
arrayList.add(str);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class D_addLast {
public ArrayList<String> addLast(ArrayList<String> arrayList, String str) {
// TODO:
arrayList.add(str);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class D_addLastTest {
D_addLast solution = spy(D_addLast.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/D_addLast.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("새롭게 추가한 String타입의 값이 포함된 ArrayList를 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "c"));
assertThat(solution.addLast(input, "c")).isEqualTo(output);
}
@Test
@DisplayName("새롭게 추가한 String타입의 값이 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b" , "c", "d"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "c", "d", "apple"));
assertThat(solution.addLast(input, "apple")).isEqualTo(output);
}
@Test
@DisplayName("새롭게 추가한 String타입의 값이 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("code", "spring"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("code", "spring", "coding"));
assertThat(solution.addLast(input, "coding")).isEqualTo(output);
}
}
05_addNth
문제
Integer 타입을 요소로 가지는 ArrayList와 추가할 위치의 인덱스, 정수를 입력받아 주어진 요소를 ArrayList의 인덱스에 추가하고 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
인자 2 : index
- 요소를 추가할 위치의 인덱스 (0 이상의 정수)
인자 3 : element
- 임의의 정수
출력
- 기존 ArrayList에 주어진 요소가 추가된 형태의 ArrayList를 리턴해야 합니다.
- [arrarList[0], arrarList[1], ..., arrayList[index]..., arrayList[n]]
- arrayList[index]의 값은 element
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 새로운 ArrayList를 선언 / 할당해서 리턴하지 않습니다.
- 기존 ArrayList에 주어진 요소가 추가된 상태(주소값 동일)로 리턴해야 합니다.
- 입력받은 인덱스가 ArrayList의 크기를 벗어난 경우엔 null을 리턴합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 1, 2, 3]
ArrayList<Integer> output = addNth(arrayList, 1, 7);
System.out.println(output); // --> [0, 7, 1, 2, 3]
내 코드
package com.choongang;
import java.util.ArrayList;
public class E_addNth {
public ArrayList<Integer> addNth(ArrayList<Integer> arrayList, int index, int element) {
// TODO:
Integer a = element;
if (index > arrayList.size() - 1) {
return null;
}
arrayList.add(index, a);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class E_addNth {
public ArrayList<Integer> addNth(ArrayList<Integer> arrayList, int index, int element) {
// TODO:
if (index < 0 || index > -arrayList.size()) {
return null;
}
arrayList.add(index, element);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class E_addNthTest {
E_addNth solution = spy(E_addNth.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/E_addNth.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
assertThat(solution.addNth(input, 9, 0)).isEqualTo(null);
}
@Test
@DisplayName("추가한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
ArrayList<Integer> output = new ArrayList<Integer>(Arrays.asList(1,7,2,3,4,5));
assertThat(solution.addNth(input, 1, 7)).isEqualTo(output);
}
@Test
@DisplayName("추가한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
ArrayList<Integer> output = new ArrayList<Integer>(Arrays.asList(0,1,2,3,4,5));
assertThat(solution.addNth(input, 0, 0)).isEqualTo(output);
}
@Test
@DisplayName("추가한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo4() {
ArrayList<Integer> input = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
ArrayList<Integer> output = new ArrayList<Integer>(Arrays.asList(1,2,3,0,4,5));
assertThat(solution.addNth(input, 3, 0)).isEqualTo(output);
}
}
06_ modifyNthElement
문제
ArrayList와 요소, 수정할 위치의 인덱스를 입력받아 주어진 요소를 ArrayList의 인덱스의 값을 수정하고 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입의 요소를 갖는 ArrayList
인자 2 : index
- 요소를 추가할 위치의 인덱스 (0 이상의 정수)
인자 3 : str
- 임의의 문자열
출력
- 기존 ArrayList에 주어진 요소가 변경된 형태의 ArrayList를 리턴해야 합니다.
- [arrarList[0], arrarList[1], ..., arrayList[index]..., arrayList[n]]
- arrayList[index]의 값은 str
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- 새로운 ArrayList를 선언 / 할당해서 리턴하지 않습니다.
- 기존 ArrayList에 주어진 요소가 수정된 상태(주소값 동일)로 리턴해야 합니다.
- 입력받은 인덱스가 ArrayList의 크기를 벗어난 경우엔 null을 리턴합니다.
- ArrayList의 remove와 add 메서드를 사용할 수 없습니다.
입출력 예시
ArrayList<String> arrayList; // ["여러분", "화이팅", "입니다"]
ArrayList<String> output = modifyNthElement(arrayList, 0, "여러분들");
System.out.println(output); // --> ["여러분들", "화이팅", "입니다"]
힌트
- 메서드의 제목(modifyNthElement)을 활용해 검색해 봅니다(java how modify element of arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class F_modifyNthElement {
public ArrayList<String> modifyNthElement(ArrayList<String> arrayList, int index, String str) {
// TODO:
String a = str;
if (index > arrayList.size() - 1) {
return null;
}
arrayList.set(index, a);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class F_modifyNthElement {
public ArrayList<String> modifyNthElement(ArrayList<String> arrayList, int index, String str) {
// TODO:
if (index < 0 || index >= arrayList.size()) {
return null;
}
arrayList.set(index, str);
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class F_modifyNthElementTest {
F_modifyNthElement solution = spy(F_modifyNthElement.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/F_modifyNthElement.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("ArrayList의 add()와 remove()를 사용할 수 없습니다.")
public void testSet() {
String path = "src/main/com/codestates/coplit/Solution.java";
String text = readLineByLineJava8(path);
assertThat(StringUtils.countMatches(text, "add")).isZero();
assertThat(StringUtils.countMatches(text, "remove")).isZero();
}
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
assertThat(solution.modifyNthElement(input, 9, "d")).isEqualTo(null);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "d"));
assertThat(solution.modifyNthElement(input, 2, "d")).isEqualTo(output);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "음료수"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "콜라"));
assertThat(solution.modifyNthElement(input, 3, "콜라")).isEqualTo(output);
}
}
07_removeFromFront
문제
Integer 타입을 요소로 가지는 ArrayList를 입력받아 ArrayList의 첫번째 요소를 삭제하고, 삭제된 해당 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
출력
- ArrayList의 첫 번째 요소를 삭제하고 삭제한 요소를 리턴해야 합니다.
주의 사항
- 입력받은 ArrayList의 첫번째 요소는 삭제되어야 합니다.
- 비어있는 ArrayList를 입력받은 경우엔 null 을 리턴합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 1, 2, 3, 4, 5]
Integer output = removeFromFront(arrayList);
System.out.println(output); // --> 0
System.out.println(arrayList); // --> [1, 2, 3, 4, 5]
힌트
- 메서드의 제목을 활용해 검색해 봅니다(java how remove element of arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class G_removeFromFront {
public Integer removeFromFront(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.isEmpty()) {
return null;
}
Integer remove = arrayList.remove(0);
return remove;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class G_removeFromFront {
public Integer removeFromFront(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return null;
}
Integer result = arrayList.remove(0);
return result;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class F_modifyNthElementTest {
F_modifyNthElement solution = spy(F_modifyNthElement.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while) 사용은 금지됩니다")
public void 반복문_for_while_사용은_금지됩니다() {
String path = "src/main/java/com/choongang/F_modifyNthElement.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("ArrayList의 add()와 remove()를 사용할 수 없습니다.")
public void testSet() {
String path = "src/main/com/codestates/coplit/Solution.java";
String text = readLineByLineJava8(path);
assertThat(StringUtils.countMatches(text, "add")).isZero();
assertThat(StringUtils.countMatches(text, "remove")).isZero();
}
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
assertThat(solution.modifyNthElement(input, 9, "d")).isEqualTo(null);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a", "b", "c"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a", "b", "d"));
assertThat(solution.modifyNthElement(input, 2, "d")).isEqualTo(output);
}
@Test
@DisplayName("수정한 값이 정확한 위치에 포함된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "음료수"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("고기", "햄버거", "순대", "콜라"));
assertThat(solution.modifyNthElement(input, 3, "콜라")).isEqualTo(output);
}
}
08_removeFromNth
문제
String 타입을 요소로 가지는 ArrayList와 인덱스를 입력받아, ArrayList에 인덱스의 요소를 삭제한 후 해당 요소를 리턴해야 합니다.
입력
인자 1 : arrayList
- String 타입을 요소로 가지는 ArrayList
인자 2 : index
- 요소를 삭제할 위치의 index (0 이상의 정수)
출력
- ArrayList의 입력받은 인덱스의 요소를 삭제하고, 삭제된 해당 요소를 리턴해야 합니다.
주의 사항
- 기존 ArrayList에 주어진 인덱스의 요소가 삭제된 상태여야합니다.
- 입력받은 인덱스가 ArrayList의 크기를 벗어난 경우엔 null을 리턴합니다.
입출력 예시
ArrayList<String> arrayList; // [0, 1, 2, 3, 4, 5]
String output = removeFromNth(arrayList, 3);
System.out.println(output); // --> "3"
내 코드
package com.choongang;
import java.util.ArrayList;
public class H_removeFromNth {
public String removeFromNth(ArrayList<String> arrayList, int index) {
// TODO:
if (arrayList.isEmpty()) {
return null;
}
if (index > arrayList.size() - 1) {
return null;
}
String remove = arrayList.remove(index);
return remove;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class H_removeFromNth {
public String removeFromNth(ArrayList<String> arrayList, int index) {
// TODO:
if (index < 0 || index >= arrayList.size()) {
return null;
}
return arrayList.remove(index);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class H_removeFromNthTest {
H_removeFromNth solution = spy(H_removeFromNth.class);
@Test
@DisplayName("범위를 벗어난 index를 인자로 받을경우 null을 리턴해야 합니다")
public void testFoo() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("1","2","3","4","5"));
assertThat(solution.removeFromNth(input, 9)).isEqualTo(null);
}
@Test
@DisplayName("해당 index의 값이 삭제된 ArrayList를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("a","b","c","d","e"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("a","c","d","e"));
assertThat(solution.removeFromNth(input, 1)).isEqualTo("b");
}
@Test
@DisplayName("해당 index의 값이 삭제된 ArrayList를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("김코딩", "자바", "스프링", "톰캣"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("김코딩", "자바", "스프링"));
assertThat(solution.removeFromNth(input, 3)).isEqualTo("톰캣");
}
@Test
@DisplayName("해당 index의 값이 삭제된 ArrayList를 리턴해야 합니다")
public void testFoo4() {
ArrayList<String> input = new ArrayList<String>(Arrays.asList("code", "states", "spring"));
ArrayList<String> output = new ArrayList<String>(Arrays.asList("code", "states"));
assertThat(solution.removeFromNth(input, 2)).isEqualTo("spring");
}
}
09_removeFromBackOfNew
문제
ArrayList와 요소를 입력받아, 마지막 요소가 제거된 새로운 ArrayList을 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
출력
- Integer 타입을 요소로 가지는 새로운 ArrayList(주소값 다름)를 리턴해야 합니다.
주의 사항
- 입력받은 ArrayList를 수정하지 않아야 합니다(immutability).
- 빈 ArrayList를 입력받은 경우 null을 리턴해야 합니다.
입출력 예시
ArrayList<Integer> arrayList; // [0, 1, 2, 3, 4, 5]
ArrayList<Integer> output = removeFromBackOfNew(arrayList);
System.out.println(arrayList); // --> [0, 1, 2, 3, 4, 5]
System.out.println(output); // --> [0, 1, 2, 3, 4]
내 코드
package com.choongang;
import java.util.ArrayList;
public class I_removeFromBackOfNew {
public ArrayList<Integer> removeFromBackOfNew(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.isEmpty()) {
return null;
}
ArrayList<Integer> a = new ArrayList<>();
for (int i = 1; i < arrayList.size(); i++) {
a.add(i);
}
return a;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class I_removeFromBackOfNew {
public ArrayList<Integer> removeFromBackOfNew(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return null;
}
// 1. 반복문을 arrayList.size() - 1 번 돌면서 (반복문)
// 새로운 List에 값을 저장하고 반환
ArrayList<Integer> newArrayList = new ArrayList<>();
for (int i = 0; i < arrayList.size() - 1; i++) {
Integer currentEl = arrayList.get(i);
newArrayList.add(currentEl);
}
// 2. arrayList를 복사한 후, 해당 새로운 List의 마지막 요소를 제거하고 반환
return newArrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class I_removeFromBackOfNewTest {
I_removeFromBackOfNew solution;
@BeforeEach
void setUp() {
solution = new I_removeFromBackOfNew();
}
@Test
@DisplayName("새로운 ArrayList를 만들어서 리턴해야 합니다.")
public void testFoo() {
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
ArrayList<Integer> list_2 = solution.removeFromBackOfNew(list);
assertThat(list == list_2).isFalse();
}
@Test
@DisplayName("빈 ArrayList를 입력받은 경우 null을 리턴해야 합니다.")
public void test() {
ArrayList<Integer> list = new ArrayList<Integer>();
assertThat(solution.removeFromBackOfNew(list)).isNull();
}
@Test
@DisplayName("마지막 요소를 제거한 ArrayList를 리턴해야 합니다.")
public void test_1() {
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
ArrayList<Integer> list_2 = new ArrayList<Integer>(Arrays.asList(1, 2));
assertThat(solution.removeFromBackOfNew(list)).isEqualTo(list_2);
}
}
10_arrayToArrayList
문제
String 타입을 요소로 가지는 배열을 입력받아, String 타입을 요소로 가지는 ArrayList로 변환하여 리턴해야 합니다.
입력
인자 1 : arr
- String 타입을 요소로 가지는 배열
출력
- String 타입을 요소로 가지는 ArrayList를 리턴해야 합니다.
입출력 예시
String[] arr = {"백엔드", "개발자", "김코딩"};
List<String> output = arrayToArrayList(arr);
System.out.println(output); // --> ["백엔드", "개발자", "김코딩"]
주의사항
- 빈 배열의 경우 null을 리턴해야 합니다.
힌트
- 메서드의 제목(arrayToList)을 활용해 검색해 봅니다(java array to List)
내 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
public class J_arrayToArrayList {
public ArrayList<String> arrayToArrayList(String[] arr) {
// TODO:
if (arr.length == 0) {
return null;
}
ArrayList<String> output = new ArrayList<>();
for (String s : arr) {
output.add(s);
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
public class J_arrayToArrayList {
public ArrayList<String> arrayToArrayList(String[] arr) {
// TODO:
if (arr.length == 0) {
return null;
}
ArrayList<String> newArrayList = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
newArrayList.add(arr[i]);
}
// 다른 방법 1, 2, 3
// for (String str : arr) {
// newArrayList.add(str);
// }
// Collection.addAll(newArrList, arr);
// return new ArrayList<>(Arrays.asList(arr));
return newArrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class J_arrayToArrayListTest {
J_arrayToArrayList solution;
@BeforeEach
void setUp() {
solution = new J_arrayToArrayList();
}
@Test
@DisplayName("빈 배열을 입력받은 경우 null을 리턴해야 합니다.")
public void testFoo() {
assertThat(solution.arrayToArrayList(new String[]{})).isNull();
}
@Test
@DisplayName("입력받은 배열과 동일한 요소를 가진 List를 리턴해야 합니다")
public void testFoo2() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("코드", "스프링", "부트캠프"));
assertThat(solution.arrayToArrayList(new String[]{"코드", "스프링", "부트캠프"})).isEqualTo(list);
}
@Test
@DisplayName("입력받은 배열과 동일한 요소를 가진 List를 리턴해야 합니다")
public void testFoo3() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("java", "spring", "tomcat"));
assertThat(solution.arrayToArrayList(new String[]{"java", "spring", "tomcat"})).isEqualTo(list);
}
}
11_clearArrayList
문제
입력받은 ArrayList의 모든 요소를 삭제한 뒤 해당 ArrayList를 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입을 요소로 가지는 ArrayList
출력
- 입력받은 ArrayList의 요소를 모두 삭제한 뒤, 동일한 주소값을 갖는 ArrayList를 리턴해야 합니다.
주의 사항
- 기존 ArrayList에 모든 요소를 삭제한 후 리턴해야 합니다.
- 입력받은 ArrayList와 리턴하는 ArrayList는 같은 주소값을 가져야 합니다.
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
ArrayList<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
ArrayList<Integer> output = clearArrayList(arrayList);
System.out.println(output); // --> []
힌트
- 메서드의 제목(clearArrayList)을 활용해 검색해 봅니다(java how to clear arrayList)
내 코드
package com.choongang;
import java.util.ArrayList;
public class K_clearArrayList {
public ArrayList<Integer> clearArrayList(ArrayList<Integer> arrayList) {
// TODO:
arrayList.removeAll(arrayList);
return arrayList;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
public class K_clearArrayList {
public ArrayList<Integer> clearArrayList(ArrayList<Integer> arrayList) {
// TODO:
arrayList.clear();
return arrayList;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class K_clearArrayListTest {
K_clearArrayList test = spy(K_clearArrayList.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/K_clearArrayList.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("비어있는 ArrayList를 리턴해야 합니다.")
public void testFoo() {
ArrayList<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4));
ArrayList<Integer> output = test.clearArrayList(list);
assertThat(output.size()).isEqualTo(0);
}
}
12_sumAllElements
문제
Integer 타입의 ArrayList를 입력받아 모든 요소를 더한 값을 리턴해야 합니다.
입력
인자 1 : arrayList
- Integer 타입의 ArrayList
출력
- int 타입을 리턴해야 합니다.
주의 사항
- 비어있는 ArrayList를 입력받은 경우 0을 리턴해야 합니다.
- Iterator를 이용해 요소를 순회해야 합니다.
입출력 예시
ArrayList<Integer> arrayList; // [1, 2, 3, 4, 5]
int output = sumAllElements(arrayList);
System.out.println(output); // --> 15
힌트
- Iterator를 이용해 요소를 순회할 수 있습니다.
내 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
public class L_sumAllElements {
public int sumAllElements(ArrayList<Integer> arrayList) {
// TODO:
int output = 0;
Iterator<Integer> iter = arrayList.iterator();
while (iter.hasNext()) {
output += iter.next();
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
public class L_sumAllElements {
public int sumAllElements(ArrayList<Integer> arrayList) {
// TODO:
if (arrayList.size() == 0) {
return 0;
}
int sumNum = 0;
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer nextNumber = iterator.next();
sumNum += nextNumber;
iterator.remove();
}
return sumNum;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class L_sumAllElementsTest {
L_sumAllElements test = spy(L_sumAllElements.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("Iterator를 사용해야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/L_sumAllElements.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "Iterator")).isNotZero();
}
@Test
@DisplayName("1, 2, 3, 4, 5의 요소를 가진 ArrayList를 입력받을 경우 15를 리턴해야 합니다")
public void testFoo() {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
assertThat(test.sumAllElements(list)).isEqualTo(15);
}
@Test
@DisplayName("12, 23, 45의 요소를 가진 ArrayList를 입력받을 경우 80를 리턴해야 합니다")
public void testFoo2() {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(12, 23, 45));
assertThat(test.sumAllElements(list)).isEqualTo(80);
}
@Test
@DisplayName("비어있는 ArrayList를 입력받을 경우 0을 리턴해야 합니다")
public void testFoo3() {
ArrayList<Integer> list = new ArrayList<>();
assertThat(test.sumAllElements(list)).isEqualTo(0);
}
}
13_getValue
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 키를 입력받아, 키에 해당하는 값을 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
인자 2 : key
- String 타입의 키
출력
- Integer 타입의 값을 리턴해야 합니다.
입출력 예시
HashMap<String, Integer> hashMap; // {a=1, b=2, c=3}
Integer output = getValue(hashMap, "b");
System.out.println(output); // --> 2
내 코드
package com.choongang;
import java.util.HashMap;
import java.util.Map;
public class M_getValue {
public Integer getValue(HashMap<String, Integer> hashMap, String key) {
// TODO:
Integer output = 0;
for (String s : hashMap.keySet()) {
output = hashMap.get(key);
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
import java.util.Map;
public class M_getValue {
public Integer getValue(HashMap<String, Integer> hashMap, String key) {
// TODO:
Integer reslut = hashMap.get(key);
return reslut;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class M_getValueTest {
M_getValue solution;
HashMap<String, Integer> map = new HashMap<String, Integer>(){{
put("A", 1);
put("B", 2);
put("C", 3);
put("head", 10);
put("tail", 19);
put("number", 99);
}};
@BeforeEach
void setUp() {
solution = new M_getValue();
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo() {
assertThat(solution.getValue(map, "A")).isEqualTo(1);
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo2() {
assertThat(solution.getValue(map, "B")).isEqualTo(2);
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo3() {
assertThat(solution.getValue(map, "tail")).isEqualTo(19);
}
@Test
@DisplayName("입력받은 key에 해당하는 값을 리턴해야 합니다")
public void testFoo4() {
assertThat(solution.getValue(map, "number")).isEqualTo(99);
}
}
14_addKeyAndValue
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 Key, Value를 입력받아 HashMap에 Key-Value 쌍을 추가한 후 새롭게 추가된 요소를 포함한 HashMap을 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
인자 2 : key
- String 타입의 키
인자 3 : value
- int 타입의 값
출력
- <String, Integer> 타입을 요소로 가지는 HashMap
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
- HashMap에 key - Value 쌍을 저장해야합니다.
입출력 예시
HashMap<String, Integer> hashMap = new HashMap<>(){{
put("a", 1);
put("b", 2);
put("c", 3);
}};
HashMap<String, Integer> output = addKeyAndValue(hashMap, "d", 4);
System.out.println(output); // {a=1, b=2, c=3, d=4}
내 코드
package com.choongang;
import java.util.HashMap;
public class N_addKeyAndValue {
public HashMap<String, Integer> addKeyAndValue(HashMap<String, Integer> hashMap, String key, int value) {
// TODO:
HashMap<String, Integer> output = new HashMap<>();
output.put(key, value);
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class N_addKeyAndValue {
public HashMap<String, Integer> addKeyAndValue(HashMap<String, Integer> hashMap, String key, int value) {
// TODO:
hashMap.put(key, value);
return hashMap;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class N_addKeyAndValueTest {
N_addKeyAndValue test = spy(N_addKeyAndValue.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/N_addKeyAndValue.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("추가한 키와 값을 요소로 가지는 HashMap을 리턴해야 합니다")
public void testFoo() {
HashMap<String, Integer> map = new HashMap<String, Integer>();
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("A", 1);
}};
assertThat(test.addKeyAndValue(map, "A", 1)).isEqualTo(output);
}
@Test
@DisplayName("추가한 키와 값을 요소로 가지는 HashMap을 리턴해야 합니다")
public void testFoo2() {
HashMap<String, Integer> map = new HashMap<String, Integer>();
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("code", 99);
}};
assertThat(test.addKeyAndValue(map, "code", 99)).isEqualTo(output);
}
@Test
@DisplayName("추가한 키와 값을 요소로 가지는 HashMap을 리턴해야 합니다")
public void testFoo3() {
HashMap<String, Integer> map = new HashMap<String, Integer>();
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("spring", 100);
}};
assertThat(test.addKeyAndValue(map, "spring", 100)).isEqualTo(output);
}
}
15_removeEntry
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 문자열을 입력받아, String 타입의 변수 key를 키(key)로 가지고있는 Entry를 제거합니다.
입력
인자 1 : hashMap
- <String, String> 타입을 요소로 가지는 HashMap
인자 2 : key
- 임의의 문자열
출력
- 별도의 리턴문(return statement)을 작성하지 않습니다.
주의 사항
- 인자로 전달받은 문자열은 key로서 항상 존재합니다.
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<String, Integer> hashMap; // {a=1, b=2, c=3}
removeEntry(hashMap, "b");
System.out.println(hashMap); // --> {a=1, c=3}
내 코드
package com.choongang;
import java.util.HashMap;
public class O_removeEntry {
public void removeEntry(HashMap<String, Integer> hashMap, String key) {
// TODO:
hashMap.remove(key);
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class O_removeEntry {
public void removeEntry(HashMap<String, Integer> hashMap, String key) {
// TODO:
hashMap.remove(key);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class O_removeEntryTest {
O_removeEntry solution;
HashMap<String, Integer> map = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
}};
@BeforeEach
void setUp() {
solution = new O_removeEntry();
}
@Test
@DisplayName("입력받은 key와 연결되어 있는 value를 삭제해야 합니다.")
public void testFoo() {
solution.removeEntry(map, "a");
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("b", 2);
put("c", 3);
}};
assertThat(map).isEqualTo(output);
}
@Test
@DisplayName("입력받은 key와 연결되어 있는 value를 삭제해야 합니다.")
public void testFoo2() {
solution.removeEntry(map, "b");
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("c", 3);
}};
assertThat(map).isEqualTo(output);
}
@Test
@DisplayName("입력받은 key와 연결되어 있는 value를 삭제해야 합니다.")
public void testFoo3() {
solution.removeEntry(map, "c");
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
}};
assertThat(map).isEqualTo(output);
}
}
16_clearHashMap
문제
<Integer, Boolean> 타입을 요소로 가지는 HashMap을 입력받아 모든 Entry를 제거합니다.
입력
인자 1 : hashMap
- <Integer, Boolean> 타입을 요소로 가지는 HashMap
출력
- 별도의 리턴문(return statement)을 작성하지 않습니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<Integer, Boolean> hashMap = new HashMap<Integer, Boolean>(){{
put(1, true);
put(3, false);
put(5, true);
}};
clearHashMap(hashMap);
System.out.println(hashMap); // --> {}
힌트
- 메서드의 제목(clearHashMap)을 활용해 검색해 봅니다(java how to clear HashMap)
내 코드
package com.choongang;
import java.util.HashMap;
public class P_clearHashMap {
public void clearHashMap(HashMap<Integer, Boolean> hashMap) {
// TODO:
hashMap.clear();
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class P_clearHashMap {
public void clearHashMap(HashMap<Integer, Boolean> hashMap) {
// TODO:
hashMap.clear();
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class P_clearHashMapTest {
P_clearHashMap test = spy(P_clearHashMap.class);
HashMap<Integer, Boolean> hashMap = new HashMap<Integer, Boolean>(){{
put(1, true);
put(3, false);
put(5, true);
}};
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/P_clearHashMap.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("모든 요소가 삭제되어야 합니다.")
public void testFoo() {
HashMap<Integer, Boolean> output = new HashMap<>();
test.clearHashMap(hashMap);
assertThat(hashMap.size()).isEqualTo(0);
assertThat(hashMap).isEqualTo(output);
}
}
17_getSize
문제
<Integer, Integer> 타입을 요소로 가지는 HashMap을 입력받아 Entry의 개수를 출력합니다.
입력
인자 1 : hashMap
- <Integer, Integer> 타입을 요소로 가지는 HashMap
출력
- int 타입을 리턴해야 합니다.
- 입력받은 HashMap의 크기를 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<Integer, Integer> hashMap = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
put(3, 30);
}};
int output = getSize(hashMap);
System.out.println(output); // --> 3
내 코드
package com.choongang;
import java.util.HashMap;
public class Q_getSize {
public int getSize(HashMap<Integer, Integer> hashMap) {
// TODO:
return hashMap.size();
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class Q_getSize {
public int getSize(HashMap<Integer, Integer> hashMap) {
// TODO:
return hashMap.size();
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class Q_getSizeTest {
Q_getSize test = spy(Q_getSize.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/Q_getSize.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("입력된 HashMap의 정확한 크기를 리턴해야 합니다")
public void testFoo() {
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
put(3, 30);
}};
assertThat(test.getSize(map)).isEqualTo(3);
}
@Test
@DisplayName("입력된 HashMap의 정확한 크기를 리턴해야 합니다")
public void testFoo2() {
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
}};
assertThat(test.getSize(map)).isEqualTo(2);
}
@Test
@DisplayName("입력된 HashMap의 정확한 크기를 리턴해야 합니다")
public void testFoo3() {
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(){{
put(1, 10);
put(2, 20);
put(3, 30);
put(4, 330);
put(5, 430);
}};
assertThat(test.getSize(map)).isEqualTo(5);
}
}
18_addEvenValues
문제
<Character, Integer> 타입을 요소로 가지는 HashMap을 입력받아 짝수 값(Value) 끼리 모두 더한 값을 리턴해야 합니다.
입력
인자 1 : hashMap
- <Character, Integer> 타입을 요소로 가지는 HashMap
출력
- int 타입의 짝수 Value들의 총합을 리턴해야 합니다.
입출력 예시
HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
put('a', 1);
put('b', 4);
put('c', 6);
put('d', 9);
}};
int output = addEvenValues(hashMap);
System.out.println(output); // --> 10
힌트
- HashMap이 제공해주는 메서드를 통해 저장되어있는 Key 모음(keySet)에 바로 접근할 수 있습니다.
내 코드
package com.choongang;
import java.util.HashMap;
public class R_addEvenValues {
public int addEvenValues(HashMap<Character, Integer> hashMap) {
// TODO:
int output = 0;
for (Character c : hashMap.keySet()) {
Integer num = hashMap.get(c);
if (num % 2 == 0) {
output += num;
}
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class R_addEvenValues {
public int addEvenValues(HashMap<Character, Integer> hashMap) {
// TODO:
int result = 0;
for (Character key : hashMap.keySet()) {
Integer value = hashMap.get(key);
if (value % 2 == 0) {
result += value;
}
}
return result;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class R_addEvenValuesTest {
R_addEvenValues solution;
@BeforeEach
void setUp() {
solution = new R_addEvenValues();
}
@Test
@DisplayName("{'a'=1, 'b'=4, 'c'=6, 'd'=9}를 요소로 가지는 HashMap을 입력받은 경우 10을 리턴합니다")
public void testFoo() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('a', 1);
put('b', 4);
put('c', 6);
put('d', 9);
}};
assertThat(solution.addEvenValues(map)).isEqualTo(10);
}
@Test
@DisplayName("{'a'=2, 'b'=4, 'c'=6, 'd'=8}를 요소로 가지는 HashMap을 입력받은 경우 20을 리턴합니다")
public void testFoo2() {
HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
put('a', 2);
put('b', 4);
put('c', 6);
put('d', 8);
}};
assertThat(solution.addEvenValues(hashMap)).isEqualTo(20);
}
@Test
@DisplayName("{'a'=0, 'b'=0, 'c'=0, 'd'=0}를 요소로 가지는 HashMap을 입력받은 경우 0을 리턴합니다")
public void testFoo3() {
HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>(){{
put('a', 0);
put('b', 0);
put('c', 0);
put('d', 0);
}};
assertThat(solution.addEvenValues(hashMap)).isEqualTo(0);
}
}
19_addFullNameEntry
문제
한 사람의 firstName, lastName Entry가 저장되어있는 HashMap을 입력 받아, fullName 이라는 새 Entry를 HashMap에 저장하고 해당 HashMap을 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, String> 타입을 요소로 가지는 HashMap
출력
- HashMap에 firstName, lastName가 이미 저장되어 있음을 이용해 fullName이라는 key와 알맞은 문자열을 저장한 후 해당 HashMap을 리턴해야 합니다.
주의 사항
- 입력받은 HashMap과 리턴하는 HashMap는 같은 주소값을 가져야 합니다.
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<String, String> hashMap = new HashMap<String, String>(){{
put("firstName", "김");
put("lastName", "코딩");
}};
HashMap<String, String> output = addFullNameEntry(hashMap);
System.out.println(output); // --> {firstName=김, fullName=김코딩, lastName=코딩}
내 코드
package com.choongang;
import java.util.Collection;
import java.util.HashMap;
public class S_addFullNameEntry {
public HashMap<String, String> addFullNameEntry(HashMap<String, String> hashMap) {
// TODO:
hashMap.put("fullName", hashMap.get("firstName") + hashMap.get("lastName"));
return hashMap;
}
}
레퍼런스 코드
package com.choongang;
import java.util.Collection;
import java.util.HashMap;
public class S_addFullNameEntry {
public HashMap<String, String> addFullNameEntry(HashMap<String, String> hashMap) {
// TODO:
hashMap.put("fullName", hashMap.get("firstName") + hashMap.get("lastName"));
return hashMap;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class S_addFullNameEntryTest {
S_addFullNameEntry test = spy(S_addFullNameEntry.class);
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/S_addFullNameEntry.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("성과 이름이 합쳐진 값이 추가된 HashMap을 리턴해야 합니다")
public void testFoo() {
HashMap<String, String> map = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "coding");
}};
HashMap<String, String> output = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "coding");
put("fullName", "kimcoding");
}};
assertThat(test.addFullNameEntry(map)).isEqualTo(output);
}
@Test
@DisplayName("성과 이름이 합쳐진 값이 추가된 HashMap을 리턴해야 합니다")
public void testFoo2() {
HashMap<String, String> map = new HashMap<String, String>(){{
put("firstName", "code");
put("lastName", "states");
}};
HashMap<String, String> output = new HashMap<String, String>(){{
put("firstName", "code");
put("lastName", "states");
put("fullName", "codestates");
}};
assertThat(test.addFullNameEntry(map)).isEqualTo(output);
}
@Test
@DisplayName("성과 이름이 합쳐진 값이 추가된 HashMap을 리턴해야 합니다")
public void testFoo3() {
HashMap<String, String> map = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "latte");
}};
HashMap<String, String> output = new HashMap<String, String>(){{
put("firstName", "kim");
put("lastName", "latte");
put("fullName", "kimlatte");
}};
assertThat(test.addFullNameEntry(map)).isEqualTo(output);
}
}
20_isContain
문제
<String, Integer> 타입을 요소로 가지는 HashMap과 문자열을 입력받아, HashMap에 문자열을 key로 한 Entry가 있는지의 여부를 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
인자 2 : key
- 임의의 문자열
출력
- boolean 타입을 리턴해야 합니다.
주의 사항
- 반복문(for, while) 사용은 금지됩니다.
입출력 예시
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("김코딩", 20);
put("박해커", 25);
put("최자바", 30);
}};
boolean output = isContain(hashMap, "김코딩");
System.out.println(output); // --> true
힌트
- HashMap에 문자열을 key로 포함(contain)하고 있는지 확인할 수 있습니다.
내 코드
package com.choongang;
import java.util.HashMap;
public class T_isContain {
public boolean isContain(HashMap<String, Integer> hashMap, String key) {
// TODO:
boolean b = hashMap.containsKey(key);
return b;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class T_isContain {
public boolean isContain(HashMap<String, Integer> hashMap, String key) {
// TODO:
return hashMap.containsKey(key);
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class T_isContainTest {
T_isContain test = spy(T_isContain.class);
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("김코딩", 20);
put("박해커", 25);
put("최자바", 30);
put("김러키", 3);
put("바닐라", 2);
put("김라떼", 3);
}};
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@Test
@DisplayName("반복문(for, while)을 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/T_isContain.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "for")).isZero();
assertThat(StringUtils.countMatches(text, "while")).isZero();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo() {
assertThat(test.isContain(hashMap, "code")).isFalse();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo2() {
assertThat(test.isContain(hashMap, "states")).isFalse();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo3() {
assertThat(test.isContain(hashMap, "김러키")).isTrue();
}
@Test
@DisplayName("정확한 boolean값을 리턴해야 합니다")
public void testFoo4() {
assertThat(test.isContain(hashMap, "바닐라")).isTrue();
}
}
21_getElementOfListEntry
문제
List를 value로 가지는 HashMap, key, 인덱스를 입력받아, key에 해당하는 키(key)가 존재하는 경우, index가 가리키는 List의 요소를 리턴해야 합니다.
입력
인자 1 : hashMap
- <String, List<String>> 타입을 요소로 갖는 HashMap
인자 2 : key
- 임의의 문자열
인자 3 : index
- List의 인덱스 (0 이상의 정수)
출력
- String 타입의 List의 요소를 리턴해야 합니다.
주의 사항
- 입력 인자 hashMap의 경우 key는 String 타입, value는 String 타입을 요소로 갖는 List 입니다.
- 주어진 수가 List의 범위를 벗어나지 않는 경우에만 List의 요소를 리턴해야 합니다.
- 그 외의 경우, null를 리턴해야 합니다.
입출력 예시
HashMap<String, List<String>> hashMap = new HashMap<String, List<String>>(){{
put("apple", Arrays.asList("apple", "red"));
put("banana", Arrays.asList("delicious"));
}};
String output = getElementOfListEntry(hashMap, "apple", 1);
System.out.println(output); // --> "red"
내 코드
package com.choongang;
import java.util.*;
public class U_getElementOfListEntry {
public String getElementOfListEntry(HashMap<String, List<String>> hashMap, String key, int index) {
// TODO:
if (index > hashMap.size() - 1) {
return null;
}
if (!hashMap.containsKey(key)) {
return null;
}
List<String> list = hashMap.get(key);
return list.get(index);
}
}
레퍼런스 코드
package com.choongang;
import java.util.*;
public class U_getElementOfListEntry {
public String getElementOfListEntry(HashMap<String, List<String>> hashMap, String key, int index) {
// TODO:
if (hashMap.containsKey(key)) {
List<String> list = hashMap.get(key);
if(index >= 0 && index < list.size()) {
return list.get(index);
}
}
return null;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class U_getElementOfListEntryTest {
U_getElementOfListEntry solution;
HashMap<String, List<String>> hashMap = new HashMap<String, List<String>>(){{
put("apple", Arrays.asList("apple", "red"));
put("banana", Arrays.asList("delicious"));
put("people", Arrays.asList("kim", "Lee", "cho", "va"));
put("cat", Arrays.asList());
}};
@BeforeEach
void setUp() {
solution = new U_getElementOfListEntry();
}
@Test
@DisplayName("HashMap의 요소인 List에서 정확한 값을 리턴해야 합니다")
public void testFoo() {
assertThat(solution.getElementOfListEntry(hashMap, "apple", 0)).isEqualTo("apple");
}
@Test
@DisplayName("HashMap의 요소인 List에서 정확한 값을 리턴해야 합니다")
public void testFoo2() {
assertThat(solution.getElementOfListEntry(hashMap, "banana", 0)).isEqualTo("delicious");
}
@Test
@DisplayName("HashMap의 요소인 List에서 정확한 값을 리턴해야 합니다")
public void testFoo3() {
assertThat(solution.getElementOfListEntry(hashMap, "people", 0)).isEqualTo("kim");
}
@Test
@DisplayName("존재하지 않는 key를 입력한 경우 null을 리턴해야 합니다")
public void testFoo4() {
assertThat(solution.getElementOfListEntry(hashMap, "dog", 0)).isNull();
}
@Test
@DisplayName("크기를 벗어난 index를 입력한 경우 null을 리턴해야 합니다")
public void testFoo5() {
assertThat(solution.getElementOfListEntry(hashMap, "people", 9)).isNull();
}
}
22_isMember
문제
회원 정보(username, password)가 저장되어있는 HashMap이 있습니다. username과 password를 입력받아 HashMap에 저장된 회원정보와 일치한지 확인하려 합니다. 입력받은 username과 password를 이용해 회원이 맞는지 여부를 리턴해야 합니다.
입력
인자 1 : member
- <String, String> 타입을 요소로 갖는 HashMap
출력
- 전달받은 HashMap에 일치하는 username-password 쌍이 있는지 확인하고, 있으면 true를 없으면 false를 리턴해야 합니다.
주의 사항
- containsValue()의 사용은 금지됩니다.
입출력 예시
HashMap<String, String> member = new HashMap<String, String>(){{
put("kimcoding", "1234");
put("parkhacker", "qwer");
}};
boolean output = isMember(member, "parkhacker", "qwer");
System.out.println(output); // --> true
내 코드
package com.choongang;
import java.util.HashMap;
public class V_isMember {
public boolean isMember(HashMap<String, String> member, String username, String password) {
// TODO:
if (!member.containsKey(username)) {
return false;
}
return member.get(username).equals(password);
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class V_isMember {
public boolean isMember(HashMap<String, String> member, String username, String password) {
// TODO:
if (member.containsKey(username)) {
String value = member.get(username);
if (value.equals(password)) {
return true;
}
}
return false;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class V_isMemberTest {
V_isMember solution;
HashMap<String, String> member = new HashMap<String, String>(){{
put("kimcoding", "1234");
put("parkhacker", "qwer");
put("code", "states");
put("lucky", "q1w2e3");
}};
private static String readLineByLineJava8(String filePath) { // .java code to String
File file = new File(filePath);
String absolutePath = file.getAbsolutePath(); //절대 경로 찾기
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines(Paths.get(absolutePath), StandardCharsets.UTF_8)) {
stream.forEach(s -> contentBuilder.append(s).append("\n"));
} catch (IOException e) {
e.printStackTrace();
}
return contentBuilder.toString();
}
@BeforeEach
void setUp() {
solution = new V_isMember();
}
@Test
@DisplayName("존재하지 않는 username을 입력한 경우 false를 리턴합니다")
public void testFoo() {
assertThat(solution.isMember(member, "latte", "cat")).isFalse();
}
@Test
@DisplayName("username과 password가 일치하지 않는 경우 false를 리턴합니다")
public void testFoo2() {
assertThat(solution.isMember(member, "lucky", "q1w2")).isFalse();
}
@Test
@DisplayName("username과 password가 일치하는 경우 true를 리턴합니다")
public void testFoo3() {
assertThat(solution.isMember(member, "code", "states")).isTrue();
}
@Test
@DisplayName("username과 password가 일치하는 경우 true를 리턴합니다")
public void testFoo4() {
assertThat(solution.isMember(member, "parkhacker", "qwer")).isTrue();
}
@Test
@DisplayName("containsValue()를 사용하지 말아야 합니다")
public void 반복문_사용_체크() {
String path = "src/main/java/com/choongang/V_isMember.java"; // 파일 위치
String text = readLineByLineJava8(path); //코드를 모두 java 파일로 변환
assertThat(StringUtils.countMatches(text, "containsValue(")).isZero();
}
}
23_select
문제
String 타입을 요소로 가지는 배열과 <String, Integer> 타입을 요소로 가지는 HashMap을 입력받아, 배열의 각 요소들을 HashMap의 키로 했을 때 그 값을 추출하여 만든 새로운 HashMap을 리턴해야 합니다.
입력
인자 1 : arr
- String 타입을 요소로 가지는 배열
인자 2 : hashMap
- <String, Integer> 타입을 요소로 가지는 HashMap
출력
- 새로운 HashMap을 리턴해야 합니다.
주의 사항
- 입력받은 HashMap에 존재하지 않는 키는 무시합니다.
- 입력받은 HashMap을 수정하지 않아야 합니다.
입출력 예시
String[] arr = {"a", "c", "e"};
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
}};
HashMap<String, Integer> output = select(arr, hashMap);
System.out.println(output); // --> { "a"=1, "c"=3 }
내 코드
package com.choongang;
import java.util.HashMap;
public class W_select {
public HashMap<String, Integer> select(String[] arr, HashMap<String, Integer> hashMap) {
// TODO:
HashMap<String, Integer> output = new HashMap<>();
for (String s : arr) {
if (hashMap.containsKey(s)) {
output.put(s, hashMap.get(s));
}
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class W_select {
public HashMap<String, Integer> select(String[] arr, HashMap<String, Integer> hashMap) {
// TODO:
HashMap<String, Integer> result = new HashMap<>();
for (String str : arr) {
if (hashMap.containsKey(str)) {
Integer value = hashMap.get(str);
result.put(str, value);
}
}
return result;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class W_selectTest {
W_select solution;
HashMap<String, Integer> hashMap = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
put("d", 4);
put("e", 5);
}};
@BeforeEach
void setUp() {
solution = new W_select();
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo() {
String[] arr = new String[]{"a", "b", "c", "d", "e"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
put("d", 4);
put("e", 5);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo2() {
String[] arr = new String[]{"a", "b", "c", "d", "f", "z"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("b", 2);
put("c", 3);
put("d", 4);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo3() {
String[] arr = new String[]{"b"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("b", 2);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
@Test
@DisplayName("배열의 요소와 일치하는 요소만을 포함하는 HashMap을 리턴해야 합니다")
public void testFoo4() {
String[] arr = new String[]{"a", "e", "code", "states"};
HashMap<String, Integer> output = new HashMap<String, Integer>(){{
put("a", 1);
put("e", 5);
}};
assertThat(solution.select(arr, hashMap)).isEqualTo(output);
}
}
24_countAllCharacter
문제
문자열을 입력받아 문자열을 구성하는 각 문자(letter)를 키로 갖는 HashMap을 리턴해야 합니다. 각 키의 값은 해당 문자가 문자열에서 등장하는 횟수를 의미하는 int 타입의 값이어야 합니다.
입력
인자 1 : str
- String 타입의 공백이 없는 문자열
출력
- <Character, Integer> 타입을 요소로 갖는 HashMap을 리턴해야 합니다.
주의 사항
- 빈 문자열을 입력받은 경우, null을 리턴해야 합니다.
입출력 예시
HashMap<Character, Integer> output = countAllCharacter("banana");
System.out.println(output); // --> {b=1, a=3, n=2}
내 코드
package com.choongang;
import java.util.HashMap;
public class X_countAllCharacter {
public HashMap<Character, Integer> countAllCharacter(String str) {
// TODO:
if (str.isEmpty()) {
return null;
}
HashMap<Character, Integer> output = new HashMap<>();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (output.containsKey(c)) {
int value = output.get(c);
output.put(c, value + 1);
} else {
output.put(c, 1);
}
}
return output;
}
}
레퍼런스 코드
package com.choongang;
import java.util.HashMap;
public class X_countAllCharacter {
public HashMap<Character, Integer> countAllCharacter(String str) {
// TODO:
if (str.isEmpty()) {
return null;
}
// 1. 주어진 문자열 str을 캐릭터 배열로 변경
char[] arr = str.toCharArray();
// 2. 결과를 담을 map 선언.
HashMap<Character, Integer> reslut = new HashMap<>();
// 3. 캐릭터 배열을 순회
for (char c : arr) {
// 2-1. 각 배열의 요소인 char가 키로 이미 존재하는지 확인
if (reslut.containsKey(c)) {
// 2-1-2. 존재한다면 헤딩 키의; 기존의 값 +1인 값으로 덮어씌우기(put)
// 기존의 키 가져오기
Integer currentValue = reslut.get(c);
reslut.put(c, currentValue + 1);
} else {
// 2-1-1. 존재하지 않는다면, 해당 키에 값을 1로 넣어서 map에 추가(put)
reslut.put(c, 1);
}
}
// 4. 순회가 끝나면 해당 map 반환
return reslut;
}
}
Test 코드
package com.choongang;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
class X_countAllCharacterTest {
X_countAllCharacter solution;
@BeforeEach
void setUp() {
solution = new X_countAllCharacter();
}
@Test
@DisplayName("빈 문자열을 입력받은 경우, null을 리턴해야 합니다.")
public void testFoo() {
assertThat(solution.countAllCharacter("")).isNull();
}
@Test
@DisplayName("\"banana\"를 입력받은 경우, {b=1, a=3, n=2}와 같은 HashMap을 리턴해야 합니다")
public void testFoo2() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('b', 1);
put('a', 3);
put('n', 2);
}};
assertThat(solution.countAllCharacter("banana")).isEqualTo(map);
}
@Test
@DisplayName("\"codestates\"를 입력받은 경우, {'a'=1, 'c'=1, 'd'=1, 'e'=2, 'o'=1, 's'=2, 't'=2}와 같은 HashMap을 리턴해야 합니다")
public void testFoo3() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('a', 1);
put('c', 1);
put('d', 1);
put('e', 2);
put('o', 1);
put('s', 2);
put('t', 2);
}};
assertThat(solution.countAllCharacter("codestates")).isEqualTo(map);
}
@Test
@DisplayName("\"kimcoding\"를 입력받은 경우, {'c'=1, 'd'=1, 'g'=1, 'i'=2, 'k'=1, 'm'=1, 'n'=1, 'o'=1}와 같은 HashMap을 리턴해야 합니다")
public void testFoo4() {
HashMap<Character, Integer> map = new HashMap<Character, Integer>(){{
put('c', 1);
put('d', 1);
put('g', 1);
put('i', 2);
put('k', 1);
put('m', 1);
put('n', 1);
put('o', 1);
}};
assertThat(solution.countAllCharacter("kimcoding")).isEqualTo(map);
}
}
Java Effective
애너테이션(Annotation)
이번 챕터에서 학습할 애너테이션(annotation)은 여러분들이 지금까지 많이 사용해 온 주석과 기능적으로 비슷합니다. 즉, 주석을 통해 코드에 대한 정보를 제공할 수 있듯, 애너테이션도 정보 전달을 위한 목적으로 만들어진 문법 요소입니다.
주석과 애너테이션은 정보 전달이라는 유사한 기능을 수행하지만, 정보를 전달하는 대상에서 차이점을 가집니다. 주석은 개발자, 즉 사람에게 정보를 전달하는 기능을 담당하는 반면, 애너테이션은 다른 프로그램에게 정보를 전달합니다.
아래 학습 목표를 숙지한 한 후, 다음 콘텐츠에서부터 애너테이션에 대해 학습을 시작해 봅시다.
학습 목표
- 애너테이션의 개념을 설명할 수 있다.
- 표준 애너테이션을 이해하고 사용할 수 있다.
- 메타 애너테이션이 무엇이고, 왜 필요한지 이해한다.
- 사용자 정의 애너테이션을 정의하는 기초적인 문법을 이해한다.
애너테이션이란?
애너테이션은 소스 코드가 컴파일되거나 실행될 때 컴파일러 및 다른 프로그램에게 필요한 정보를 전달해 주는 문법 요소입니다. 애너테이션이 어떻게 생겼는지 직접 코드에서 확인해 봅시다.
먼저, 아래와 같이 인텔리제이에 인터페이스를 정의해 봅시다.
public interface ExampleInterface {
void example();
}
그다음, 아래와 같이 클래스를 정의하고, ExampleInterface를 구현하도록 해주세요.
public class ExampleClass implements ExampleInterface {
}
아래의 단축키를 사용하여 ExampleInterface의 추상 메서드를 구현하도록 합니다.
- Mac OS : cmd + n 입력 후, Implement Methods 클릭
- Windows : alt + insert 입력 후, Implement Methods 클릭

엔터를 누르거나, OK를 눌러주세요.

입력이 완료되면 구현해야 하는 메서드가 자동으로 정의됩니다.
public class ExampleClass implements ExampleInterface {
@Override
public void example() {
}
}
여기에서 보이는 @Override가 바로 애너테이션입니다. 보이는 것처럼 애너테이션은 @로 시작하며, 클래스, 인터페이스, 필드, 메서드 등에 붙여서 사용할 수 있습니다.
참고로, 위 예시에서의 @Override는 example()이 추상 메서드를 구현하거나, 상위 클래스의 메서드를 오버라이딩한 메서드라는 것을 컴파일러에게 알려주는 역할을 합니다. (@Override의 자세한 기능에 대해서는 이후에 자세히 학습합니다. )
이처럼 애너테이션은 컴파일러 또는 다른 프로그램에 필요한 정보를 제공해 주는 역할을 합니다.
애너테이션의 종류
애너테이션에는 JDK가 기본적으로 제공하는 애너테이션도 있지만, 다른 프로그램에서 제공하는 애너테이션도 있습니다. 다른 프로그램이 제공하는 애너테이션은 해당 프로그램의 사용 방법을 학습할 때 따로 학습해야 합니다. 여기에서는 JDK에 내장된 애너테이션 중에서 중요한 몇 가지만 추려보겠습니다.
JDK에서 기본적으로 제공하는 애너테이션은 아래의 두 가지로 구분됩니다.
- 표준 애너테이션 : JDK에 내장된 일반적인 애너테이션입니다.
- 메타 애너테이션 : 다른 애너테이션을 정의할 때 사용하는 애너테이션입니다.
표준 애너테이션은 앞서 살펴본 @Override와 같이 다른 문법 요소에 붙여서 사용하는 일반적인 애너테이션을 의미하며, 메타 애너테이션은 애너테이션을 직접 정의해서 사용할 때 사용하는 애너테이션입니다.
즉, 애너테이션도 사용자가 직접 정의해서 사용할 수 있으며, 이러한 애너테이션을 사용자 정의 애너테이션이라고 합니다.
다음 콘텐츠에서부터 표준 애너테이션과 메타 애너테이션에 대해 조금 더 자세히 학습해 보도록 하겠습니다.
표준 애너테이션
먼저 자바가 기본적으로 제공하는 표준 애너테이션에 대해 살펴보겠습니다.
아래 소개된 것 외에 몇 가지 표준 애너테이션들이 더 있지만, 우리는 그중에서 가장 빈번하게 사용되는 네 가지를 중심으로 간략히 살펴보도록 하겠습니다.
@Override
@Override는 메서드 앞에만 붙일 수 있는 애너테이션으로, 선언한 메서드가 상위 클래스의 메서드를 오버라이딩하거나 추상 메서드를 구현하는 메서드라는 것을 컴파일러에게 알려주는 역할을 수행합니다.
예를 들어, 아래와 같이 SuperClass의 example()를 SubClass에서 오버라이딩할 때에, @Override를 붙여주면, 컴파일러는 SubClass의 example()이 상위 클래스의 메서드를 오버라이딩한 것으로 간주합니다.
class SuperClass {
public void example() {
System.out.println("example() of SuperClass");
}
}
class SubClass extends SuperClass {
@Override
public void example() {
System.out.println("example() of SubClass");
}
}
컴파일 과정에서 컴파일러가 @Override를 발견하면, @Override가 붙은 메서드와 같은 이름을 가진 메서드가 상위 클래스(또는 인터페이스)에 존재하는지 검사합니다. 즉, SuperClass에 example()이 존재하는지 검사합니다.
만약, 상위 클래스(또는 인터페이스)에서 @Override가 붙어있는 메서드명과 같은 이름의 메서드를 찾을 수 없다면 컴파일러가 컴파일 에러를 발생시킵니다.
그렇다면, 상위 클래스에 오버라이딩 같은 같은 이름의 메서드가 존재하는지 왜 확인해야 할까요?
종종 코드를 작성하다 보면 어떤 메서드를 오버라이딩하거나 구현할 때, 개발자의 실수로 메서드의 이름이 잘못 작성되는 경우가 발생합니다.
class SuperClass {
public void example() {
System.out.println("example() of SuperClass");
}
}
class SubClass extends SuperClass {
public void exapmle() { // 메서드 이름에 오타가 있습니다.
System.out.println("example() of SubClass");
}
}
이러면, 위 예시처럼 @Override를 붙이지 않으면 컴파일러는 exapmle()이라는 새로운 메서드를 정의하는 것으로 간주하고, 에러를 발생시키지 않습니다.
즉, 컴파일 에러 없이 코드가 그대로 실행될 수 있어 실행 시에 런타임 에러가 발생할 것이며, 런타임 에러 발생 시에, 어디에서 에러가 발생했는지 에러의 원인을 찾아내기 어려워집니다.
그러나, @Override를 사용하면 example()이 오버라이딩 메서드라는 것을 컴파일러가 인지하고, 상위 클래스에 example()이 존재하는지 확인하기 때문에, 이러한 상황을 방지할 수 있습니다.
즉, @Override는 컴파일러에게 “컴파일러야, 이 메서드는 상위 클래스의 메서드를 오버라이딩하거나 추상 메서드를 구현하는 메서드인데, 만약에 내가 실수해서 오버라이딩 및 구현이 잘 안 되면 에러를 발생시켜서 나에게 알려줄래?”라고 부탁하는 것과 같습니다.
@Deprecated
@Deprecated는 기존에 사용하던 기술이 다른 기술로 대체되어 기존 기술을 적용한 코드를 더 이상 사용하지 않도록 유도하는 경우에 사용합니다.
아래 예시를 보면, OldClass의 oldField와 getOldField()에 @Deprecated가 붙어 있습니다.
class OldClass {
@Deprecated
private int oldField;
@Deprecated
int getOldField() { return oldField; }
}
이때, 다른 클래스에서 OldClass를 인스턴스화하여 getOldField()를 호출하면 아래와 같이 취소선이 뜨면서, 인텔리제이가 경고 메시지를 출력해 줍니다.


또한, 해당 코드를 직접 컴파일해 보면 아래와 같이 경고 메시지가 출력됩니다.

이처럼 @Deprecated는 애너테이션이 붙은 대상이 새로운 것으로 대체되었으니 기존의 것을 사용하지 않도록 유도하는 기능을 합니다.
즉, 기존의 코드를 다른 코드와의 호환성 문제로 삭제하기 곤란해 남겨두어야만 하지만 더 이상 사용하는 것을 권장하지 않을 때 @Deprecated를 사용합니다.
@SuppressWarnings
@SuppressWarnings 애너테이션은 컴파일 경고 메시지가 나타나지 않도록 합니다. 때에 따라서 경고가 발생할 것이 충분히 예상됨에도 묵인해야 할 때 주로 사용합니다.
아래와 같이 @SuppressWarnings 뒤에 괄호를 붙이고 그 안에 억제하고자 하는 경고메시지를 지정해 줄 수 있습니다. 아래의 내용을 모두 외울 필요는 없습니다. 여기에서는 특정 내용과 관련된 경고 메시지를 선택적으로 억제할 수 있다는 사실만 이해하고 넘어가도 충분합니다.

더 나아가 아래와 같이 중괄호에 여러 개의 경고 유형을 나열함으로써 여러 개의 경고를 한 번에 묵인하게 할 수 있습니다.
@SuppressWarnings({"deprecation", "unused", "null"})
@FunctionalInterface
@FunctionalInterface 애너테이션은 함수형 인터페이스를 선언할 때, 컴파일러가 함수형 인터페이스의 선언이 바르게 선언되었는지 확인하도록 합니다. 만약 바르게 선언되지 않은 경우, 에러를 발생시킵니다.
참고로, 함수형 인터페이스는 단 하나의 추상 메서드만을 가져야 하는 제약이 있습니다. 함수형 인터페이스에 대해서는 람다 챕터에서 자세히 학습하니, 여기에서는 “함수형 인터페이스가 제대로 작성되었는지 컴파일러에게 검사할 것을 요구하는 애너테이션이구나” 정도로만 이해해 주세요.
@FunctionalInterface
public interface ExampleInterface {
public abstract void example(); // 단 하나의 추상 메서드
}
표준 애너테이션 종류
자바의 내장 표준 에너테이션
- @Override
- @Deprecated
- @SuppressWarnings
- @SafeVarargs
- @FunctionalInterface
- @Retention
- @Documented
- @Target
- @Inherited
- @Repeatable
- @Native
스프링 프레임워크의 주요 표준 에너테이션
- @Required
- @Autowired
- @Qualifier
- @Primary
- @Value
- @Component
- @Service
- @Repository
- @Controller
- @RestController
- @RequestMapping
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping
- @ResponseStatus
- @RequestBody
- @ResponseBody
- @ModelAttribute
- @PathVariable
- @RequestParam
- @SessionAttributes
- @CookieValue
- @ExceptionHandler
- @Transactional
- @EnableTransactionManagement
- @EnableAspectJAutoProxy
- @Aspect
- @Before
- @After
- @AfterReturning
- @AfterThrowing
- @Around
- @Configuration
- @Bean
- @Profile
- @Scope
- @Import
- @Lazy
메타 애너테이션
- 메타 애너테이션(meta-annotation)은 애너테이션을 정의하는 데에 사용되는 애너테이션으로, 애너테이션의 적용 대상 및 유지 기간을 지정하는 데에 사용됩니다.
먼저, 앞서 살펴보았던 @Override가 어떻게 정의되어 있는지 확인해 보겠습니다. @Override의 소스 코드는 아래와 같습니다.

여기에서 알 수 있는 것처럼, 애너테이션을 정의할 때는 @interface 키워드를 사용하여 정의합니다.
또한, 애너테이션 정의부 상단에 @Target, @Retention 애너테이션이 붙어 있는 것을 확인할 수 있습니다. 이들은 @Override의 적용 대상과 유지 기간을 지정하는 역할을 합니다.
이처럼 메타 애너테이션은 애너테이션을 정의하는 데에 사용되며, 메타 애너테이션을 사용하여 애너테이션의 다양한 특성을 지정할 수 있습니다.
아래에서부터는 대표적인 메타 애너테이션 몇 가지를 살펴보도록 하겠습니다.
@Target
@Target 애너테이션은 이름 그대로 애너테이션을 적용할 “대상"을 지정하는 데 사용됩니다.
다음의 표에 나와 있는 내용이 @Target 애너테이션을 사용하여 지정할 수 있는 대상의 타입이며, 모두 java.lang.annotation.ElementType이라는 열거형에 정의되어 있습니다.

다음의 예시를 통해 좀 더 알아보도록 합시다.
import static java.lang.annotation.ElementType.*;
//import문을 이용하여 ElementType.TYPE 대신 TYPE과 같이 간단히 작성할 수 있습니다.
@Target({FIELD, TYPE, TYPE_USE}) // 적용대상이 FIELD, TYPE
public @interface CustomAnnotation { } // CustomAnnotation을 정의
@CustomAnnotation // 적용대상이 TYPE인 경우
class Main {
@CustomAnnotation // 적용대상이 FIELD인 경우
int i;
}
위의 코드 예제는 @Target 애너테이션의 용법을 잘 보여주고 있습니다.
바로 이어지는 챕터에서 가볍게 살펴보겠지만, 애너테이션을 사용자가 직접 정의할 때(@interface 사용), @Target 애너테이션을 통해 사용자가 정의한 애너테이션이 어디에 적용될 수 있는지를 설정할 수 있습니다.
위의 예제에서는 @Target({FIELD, TYPE, TYPE_USE}) 을 사용하여, 각각 필드, 타입, 그리고 타입이 사용되는 모든 대상(변수)에 애너테이션이 적용되도록 한 것을 확인할 수 있습니다.
@Documented
다음으로 @Documented 애너테이션은 애너테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 하는 애너테이션 설정입니다.
자바에서 제공하는 표준 애너테이션과 메타 애너테이션 중 @Override와 @SuppressWarnings를 제외하고는 모두 @Documented가 적용되어 있습니다.
@Documented
@Target(ElementType.Type)
public @interface CustomAnnotation { }
@Inherited
@Inherited 애너테이션은 이름에서도 알 수 있듯이 하위 클래스가 애너테이션을 상속받도록 합니다. @Inherited 애너테이션을 상위 클래스에 붙이면, 하위 클래스도 상위 클래스에 붙은 애너테이션들이 동일하게 적용됩니다.
@Inherited // @SuperAnnotation이 하위 클래스까지 적용
@interface SuperAnnotation{ }
@SuperAnnotation
class Super { }
class Sub extends Super{ } // Sub에 애너테이션이 붙은 것으로 인식
위의 코드 예제에서, Super 상위 클래스로부터 확장된 Sub 하위 클래스는 상위 클래스와 동일하게 @SuperAnnotation에 정의된 내용들을 적용받게 됩니다.
@Retention
@Retention 애너테이션도 이름 그대로 특정 애너테이션의 지속 시간을 결정하는 데 사용합니다. 애너테이션과 관련한 유지 정책(retention policy)의 종류에는 다음의 세 가지가 있습니다.
유지정책이란 애너테이션이 유지되는 기간을 지정하는 속성입니다.

각각의 유지 정책은 언제까지 애너테이션이 유지될지를 결정합니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
//오버라이딩이 제대로 되었는지 컴파일러가 확인하는 용도
//클래스 파일에 남길 필요 없이 컴파일 시에만 확인하고 사라짐
public @interface Override(){ }
예를 들면, 위의 예제에서 Override 애너테이션은 컴파일러가 사용하면 끝나기 때문에, 실행 시에는 더 이상 사용되지 않음을 의미합니다.
@Repeatable
마지막으로, @Repeatable 애너테이션은 애너테이션을 여러 번 붙일 수 있도록 허용한다는 의미가 있습니다.
아래 예제에서, 사용자 타입의 애너테이션 Work를 정의하고, @Repeatable 애너테이션을 사용하여 이것을 여러 번 사용할 수 있도록 하였습니다.
@Repeatable(Works.class) // Work 애너테이션을 여러 번 반복해서 쓸 수 있게 한다.
@interface Work{
String value();
}
이제 아래와 같이 일반적인 애너테이션과 다르게 Work 애너테이션을 하나의 대상에 여러 번 적용하는 것이 가능해졌습니다.
@Work("코드 업데이트")
@Work("메서드 오버라이딩")
class Main{
... 생략 ...
}
참고로, @Repeatable 애너테이션은 일반적인 애너테이션과 달리 같은 이름의 애너테이션이 여러 번 적용될 수 있기 때문에, 이 애너테이션들을 하나로 묶어주는 애너테이션도 별도로 작성해야 합니다.
@interface Works { // 여러 개의 Work애너테이션을 담을 컨테이너 애너테이션 Works
Work[] value();
}
@Repeatable(Works.class) // 컨테이너 애너테이션 지정
@interface Work {
String value();
}
사용자 정의 애너테이션
마지막으로, 앞서 언급했듯이 사용자 정의 애너테이션이 있지만, 현재 단계에서 크게 중요하지 않기 때문에 가볍게 참고만 하고 지나가도록 합니다.
사용자 정의 애너테이션은 이름 그대로 사용자가 직접 애너테이션을 정의해서 사용하는 것을 의미합니다.
애너테이션을 정의하는 방법은 인터페이스를 정의하는 것과 비슷합니다.
@interface 애너테이션명 { // 인터페이스 앞에 @기호만 붙이면 애너테이션을 정의할 수 있습니다.
타입 요소명(); // 애너테이션 요소를 선언
}
한 가지 유의할 점은, 애너테이션은 java.lang.annotation 인터페이스를 상속받기 때문에 다른 클래스나 인터페이스를 상속받을 수 없다는 사실입니다.
혹시 사용자 정의 애너테이션과 관련된 좀 더 자세한 내용이 궁금하다면, java custom annotation 등 키워드를 사용해서 찾아보는 것을 권장합니다.
람다(Lambda)
람다식(Lambda Expression)은 함수형 프로그래밍 기법을 지원하는 자바의 문법요소입니다.
람다식은 수학자 알론조 처치(Alonzo Church)가 발표한 람다 계산법에서 시작되었는데, 이를 그의 제자 존 맥카시(John McCarthy)가 프로그래밍 언어에 도입하면서 본격적으로 컴퓨터 프로그래밍 언어에서 사용되기 시작했습니다.
람다식은 간단히 말해서 메서드를 하나의 ‘식(expression)’으로 표현한 것으로, 코드를 매우 간결하면서 명확하게 표현할 수 있다는 큰 장점이 있습니다.
최근 함수형 프로그래밍이 다시금 주목을 받게 되면서 자바도 JDK 1.8 이후 람다식 등 함수형 프로그래밍 문법 요소를 도입하면서 기존의 객체지향 프로그래밍과 함수형 프로그래밍을 혼합하는 방식으로 더욱 효율적인 프로그래밍을 할 수 있게 되었습니다.
사실 람다식은 뒤에서 좀 더 자세히 보겠지만, 객체지향적 언어의 특성을 가진 자바의 특성에 따라 일반적인 함수가 아니라 익명의 객체이기 때문에 기존 자바의 문법 요소를 해치지 않으면서 함수형 프로그래밍 기법을 사용할 수 있는 장치가 필요했습니다. 이에 따라 함수형 인터페이스(functional interface가 만들어지게 되었습니다.
좀 더 자세한 내용은 하나씩 학습해 가도록 하겠습니다.
그럼, 본격적인 학습에 들어가기에 앞서 아래 학습 목표를 통해 먼저 배울 내용을 확인해 봅시다.
학습 목표
- 람다식이 무엇이고, 어떻게 사용할 수 있는지 이해할 수 있다.
- 함수형 인터페이스를 통해 람다를 다루는 방법을 이해하고 설명할 수 있다.
- 람다식을 메서드 참조 방식으로 변환할 수 있다.
- 스트림(Stream)은 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자입니다.
스트림을 사용하면 List, Set, Map, 배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있습니다.
스트림은 데이터 소스를 다루는 풍부한 메서드를 제공합니다.
이를 활용하면, 다량의 데이터에 복잡한 연산을 수행하면서도, 가독성과 재사용성이 높은 코드를 작성할 수 있습니다.
람다식의 기본 문법
앞서 개요에서 언급했던 것처럼, 람다식은 함수(메서드)를 좀 더 간단하고 편리하게 표현하기 위해 고안된 문법 요소라 할 수 있습니다.
다음의 예를 한번 살펴볼까요?
//기존 메서드 표현 방식
void sayhello() {
System.out.println("HELLO!")
}
//위의 코드를 람다식으로 표현한 식
() -> System.out.println("HELLO!")
위의 메서드는 같은 메서드를 각각 기존의 방식과 람다식으로 표현한 것입니다.
어떤가요?
아직 구체적인 람다 문법을 배우지는 않았지만, 좀 더 간편해 보이지 않나요?
가장 먼저 두드러지는 차이는 람다식에서는 기본적으로 반환타입과 이름을 생략할 수 있다는 점입니다. 따라서 람다함수를 종종 이름이 없는 함수, 즉 익명 함수(anonymous function)라 부르기도 합니다.
그럼, 이제 아래 예시를 통해 메서드를 람다식으로 만드는 방법에 대해 살펴보도록 하겠습니다.
int sum(int num1, int num2) {
return num1 + num2;
}
여기 우리에게 익숙한 더하기 기능을 수행하는 sum 메서드가 있습니다. 이 메서드를 어떻게 람다식으로 바꿀 수 있을까요?
(int num1, int num2) -> { // 반환타입과 메서드명 제거 + 화살표 추가
return num1 + num2;
}
앞서 봤던 것처럼, 반환타입과 메서드명을 제거하고 코드 블록 사이에 화살표를 추가해 주면 됩니다.
다음의 예시들도 함께 살펴봅시다.
// 기존 방식
void example1() {
System.out.println(5);
}
// 람다식
() -> {System.out.println(5);}
// 기존 방식
int example2() {
return 10;
}
// 람다식
() -> {return 10;}
// 기존 방식
void example3(String str) {
System.out.println(str);
}
// 람다식
(String str) -> { System.out.println(str);}
이렇게 람다식을 사용하면 기존 방식을 좀 더 간편하면서도 명확하게 표현할 수 있습니다.
그런데 놀라운 것은 이 외에도 특정 조건이 충족되면 람다식을 더욱 축약하여 표현할 수 있습니다.
다시 이전의 sum 메서드로 돌아가 보겠습니다.
// 기존 방식
int sum(int num1, int num2) {
return num1 + num2;
}
// 람다식
(int num1, int num2) -> {
num1 + num2
}
이 sum 메서드는 내용을 조금 더 축약할 수 있습니다.
먼저, 메서드 바디에 문장이 실행문이 하나만 존재할 때 우리는 중괄호와 return 문을 생략할 수 있습니다. 이 경우, 세미콜론까지 생략해야 합니다.
(int num1, int num2) -> num1 + num2
그럼 반대로, 실행문이 두 개 이상이면 중괄호를 생략할 수 없겠죠?
두 번째로, 매개변수 타입을 함수형 인터페이스를 통해 유추할 수 있는 경우에는 매개변수의 타입을 생략할 수 있습니다. 함수형 인터페이스에 대해서는 다음 콘텐츠에서 학습합니다.
(num1, num2) -> num1 + num2
어떤가요?
믿기 어렵지만, 위의 람다식은 변신 전의 sum 메서드와 정확히 동일한 메서드입니다. 신기하지 않나요?
이 외에도 매개변수가 하나인 경우 소괄호 생략할 수 있는 등의 조건들이 있는데, 추가적인 축약 표현은 검색을 통해 공부해 보시기 바랍니다.
또한, 여러분은 여기에서 이러한 축약 표현을 모두 외우지 않으셔도 됩니다. 외운다고 해도 바로 사용할 수 있는 것이 아니기 때문입니다. 어떤 경우에 축약이 가능한지, 어떤 경우에 불가능한지 여러 번 코드를 입력해 보시면서 자연스럽게 익숙해지시기를 바랍니다.
함수형 인터페이스
자바에서 함수는 반드시 클래스 안에서 정의되어야 하므로 메서드가 독립적으로 있을 수 없고 반드시 클래스 객체를 먼저 생성한 후 생성한 객체로 메서드를 호출해야 합니다.
이런 맥락에서 지금까지 우리가 메서드와 동일시 여겼던 람다식 또한 사실은 객체입니다. 더 정확히는 이름이 없기 때문에 익명 객체라 할 수 있습니다.
앞서 우리가 봤었던 sum 메서드를 한번 다시 살펴보겠습니다.
// sum 메서드 람다식
(num1, num2) -> num1 + num2
// 람다식을 객체로 표현
new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
}
위의 람다식으로 표현한 sum 메서드는 사실 아래와 같은 이름이 없는 익명 객체입니다.
익명 객체는 익명 클래스를 통해 만들 수 있는데, 익명 클래스란 객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한 번만 사용되는 일회용 클래스입니다.
그리고 위에서 봤던 것처럼 아래와 같이 생성과 선언을 한 번에 할 수 있습니다.
new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
}
다시 돌아가서, 만약에 람다식이 객체라 한다면 앞서 우리가 배웠던 것처럼 이 객체에 접근하고 사용하기 위한 참조변수가 필요합니다.
그런데 기존에 객체를 생성할 때 만들었던 Object 클래스에는 sum이라는 메서드가 없으므로, Object 타입의 참조변수에 담는다고 하더라도 sum 메서드를 사용할 수 없습니다.
public class LamdaExample1 {
public static void main(String[] args) {
// 람다식 Object obj = (num1, num2) -> num1 + num2; 로 대체 가능
Object obj = new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
};
obj.sum(1, 2);
}
}
출력 결과
java: cannot find symbol
symbol: method sum(int,int)
location: variable obj of type java.lang.Object
다시 이야기하면, 위의 코드 예제에서 익명 객체를 생성하여 참조변수 obj에 담아준다고 하더라도 sum 메서드를 사용할 수 있는 방법이 없습니다.
이 같은 문제를 해결하기 위해 사용하는 자바의 문법 요소가 바로 자바의 함수형 인터페이스(Functional Interface)라 할 수 있습니다.
즉, 자바에서 함수형 프로그래밍을 하기 위한 새로운 문법 요소를 도입하는 대신, 기존의 인터페이스 문법을 활용하여 람다식을 다루는 것이라 할 수 있습니다.
함수형 인터페이스에는 단 하나의 추상 메서드만 선언될 수 있는데, 이는 람다식과 인터페이스의 메서드가 1:1로 매칭되어야 하기 때문입니다.
그럼, 이제 위의 예제에서 직면했던 문제를 함수형 인터페이스를 적용하여 풀어보도록 하겠습니다.
public class LamdaExample1 {
public static void main(String[] args) {
/* Object obj = new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
};
*/
ExampleFunction exampleFunction = (num1, num2) -> num1 + num2;
System.out.println(exampleFunction.sum(10,15));
}
@FunctionalInterface // 컴파일러가 인터페이스가 바르게 정의되었는지 확인하도록 합니다.
interface ExampleFunction {
int sum(int num1, int num2);
}
// 출력값
25
위의 예제에서 함수형 인터페이스인 ExampleFunction에 추상메서드 sum()이 정의되어 있습니다. 이 함수형 인터페이스는 람다식을 참조할 참조변수를 선언할 때, 타입으로 사용하기 위해 필요합니다.
함수형 인터페이스 타입으로 선언된 참조변수 exampleFunction에 람다식이 할당되었으며, 이제 exampleFunction을 통해 sum() 메서드를 호출할 수 있습니다.
이처럼, 함수형 인터페이스를 사용하면 참조변수의 타입으로 함수형 인터페이스를 사용하여 우리가 원하는 메서드에 접근할 수 있습니다.
매개변수와 리턴값이 없는 람다식
다음과 같이 매개변수와 리턴값이 없는 추상 메서드를 가진 함수형 인터페이스가 있다고 가정해 보겠습니다.
@FunctionalInterface
public interface MyFunctionalInterface {
void accept();
}
이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 합니다. 람다식에서 매개변수가 없는 이유는 accept()가 매개변수를 가지지 않기 때문입니다.
MyFunctionalInterface example = () -> { ... };
// example.accept();
람다식이 대입된 인터페이스의 참조 변수는 위의 주석과 같이 accept()를 호출할 수 있습니다. accept()의 호출은 람다식의 중괄호 {}를 실행시킵니다.
아래의 예시를 보면서 매개변수와 리턴값이 없는 경우의 람다식을 좀 더 익혀봅시다.
@FunctionalInterface
interface MyFunctionalInterface {
void accept();
}
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example = () -> System.out.println("accept() 호출");
example.accept();
}
}
// 출력값
accept() 호출
매개변수가 있는 람다식
다음과 같이 매개 변수가 있고 리턴값이 없는 추상 메서드를 가진 함수형 인터페이스가 있다고 가정해 보겠습니다.
@FunctionalInterface
public interface MyFunctionalInterface {
void accept(int x);
}
이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 합니다. 람다식에서 매개변수가 한 개인 이유는 추상메서드 accept()가 매개변수를 하나만 가지기 때문입니다.
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example;
example = (x) -> {
int result = x * 5;
System.out.println(result);
};
example.accept(2);
example = (x) -> System.out.println(x * 5);
example.accept(2);
}
}
// 출력값
10
10
람다식이 대입된 인터페이스 참조 변수는 다음과 같이 accept()를 호출할 수 있습니다. 위의 예시와 같이 매개값으로 2를 주면 람다식의 x 변수에 2가 대입되고, x는 중괄호 { }에서 사용됩니다.
리턴값이 있는 람다식
다음과 같이 매개 변수와 리턴값을 가지는 추상 메서드를 포함하는 함수형 인터페이스가 있습니다.
@FunctionalInterface
public interface MyFunctionalInterface {
int accept(int x, int y);
}
이 인터페이스를 타깃 타입으로 갖는 람다식은 다음과 같은 형태로 작성해야 합니다. 람다식에서 매개 변수가 두 개인 이유는 accept()가 매개변수를 두 개 가지기 때문입니다.
또한, accept()가 리턴 타입이 있기 때문에 중괄호 { }에는 return 문이 있어야 합니다.
아래의 예시를 보겠습니다.
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example;
example = (x, y) -> {
int result = x + y;
return result;
};
int result1 = example.accept(2, 5);
System.out.println(result1);
example = (x, y) -> { return x + y; };
int result2 = example.accept(2, 5);
System.out.println(result2);
example = (x, y) -> x + y;
//return문만 있으면, 중괄호 {}와 return문 생략 가능
int result3 = example.accept(2, 5);
System.out.println(result3);
example = (x, y) -> sum(x, y);
//return문만 있으면, 중괄호 {}와 return문 생략 가능
int result4 = example.accept(2, 5);
System.out.println(result4);
}
public static int sum(int x, int y){
return x + y;
}
}
//출력값
7
7
7
7
람다식이 대입된 인터페이스 참조변수는 다음과 같이 accept()를 호출할 수 있습니다. 매개값으로 2와 5를 주면 람다식의 x 변수에 2, y 변수에 5가 대입되고 x와 y는 중괄호에서 사용됩니다.
자바에서 기본적으로 제공하는 함수형 인터페이스
마지막으로, 자바에서는 빈번하게 사용되는 함수형 인터페이스를 기본적으로 제공하고 있습니다. 즉, 기본적으로 내장된 함수형 인터페이스를 사용하여 매번 같은 기능을 수행하는 함수형 인터페이스를 직접 만드는 번거로움을 줄여주는 것입니다.
메서드 레퍼런스
메서드 참조는 람다식에서 불필요한 매개변수를 제거할 때 주로 사용합니다.
즉, 람다식으로 더욱 간단해진 익명 객체를 더욱더 간단하게 사용하고 싶은 개발자의 요구가 반영된 산물이라 할 수 있습니다.
람다식은 종종 기존 메서드를 단순히 호출만 하는 경우가 많습니다.
다음 예시코드는 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 정적 메서드를 호출하는 람다식입니다.
(left, right) -> Math.max(left, right)
람다식은 단순히 두 개의 값을 Math.max() 메서드의 매개값으로 전달하는 역할만 하므로 다소 불편해 보입니다. 또한, 이 경우 입력값과 출력값의 반환타입을 쉽게 유추할 수 있으므로 입력값과 출력값을 일일이 적어주는 게 크게 중요하지 않습니다.
이럴 때는 다음과 같이 메서드 참조를 이용하면 매우 깔끔하게 처리할 수 있습니다.
// 클래스이름::메서드이름
Math :: max // 메서드 참조
메서드 참조도 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 인터페이스의 추상 메서드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라집니다.
IntBinaryOperator 인터페이스는 두 개의 int 매개값을 받아 int 값을 리턴하므로, Math::max 메서드 참조를 대입할 수 있습니다.
IntBinaryOperator operato = Math :: max; //메서드 참조
메서드 참조는 정적 혹은 인스턴스 메서드를 참조할 수 있고 생성자 참조도 가능합니다.
지금부터 하나씩 살펴보겠습니다.
정적 메서드와 인스턴스 메서드 참조
정적 메서드를 참조할 때는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메서드 이름을 기술하면 됩니다.
클래스 :: 메서드
인스턴스 메서드의 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 ::기호를 붙이고 인스턴스 메서드 이름을 기술하면 됩니다.
참조 변수 :: 메서드
다음 예제는 Calculator의 정적 및 인스턴스 메서드를 참조합니다.
//Calculator.java
public class Calculator {
public static int staticMethod(int x, int y) {
return x + y;
}
public int instanceMethod(int x, int y) {
return x * y;
}
}
import java.util.function.IntBinaryOperator;
public class MethodReferences {
public static void main(String[] args) throws Exception {
IntBinaryOperator operator;
/*정적 메서드
클래스이름::메서드이름
*/
operator = Calculator::staticMethod;
System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));
/*인스턴스 메서드
인스턴스명::메서드명
*/
Calculator calculator = new Calculator();
operator = calculator::instanceMethod;
System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
}
}
/*
정적메서드 결과 : 8
인스턴스 메서드 결과 : 15
*/
꼭 직접 코드를 입력해 보면서 결과를 확인해 주세요.
생성자 참조
메서드 참조는 생성자 참조도 포함합니다.
생성자를 참조한다는 것은 객체 생성을 의미합니다. 단순히 메서드 호출로 구성된 람다식을 메서드 참조로 대치할 수 있듯이, 단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치할 수 있습니다.
(a,b) -> new 클래스(a,b)
이 경우 생성자 참조로 표현하면 다음과 같습니다. 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 됩니다.
//생성자 참조 문법
클래스 :: new
생성자가 오버로딩되어 여러 개가 있으면 컴파일러는 함수형 인터페이스의 추상 메서드와 동일한 매개 변수 타입과 개수가 있는 생성자를 찾아 실행합니다.
만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생합니다.
다음의 예제를 한번 확인해 보도록 합시다.
//Member.java
public class Member {
private String name;
private String id;
public Member() {
System.out.println("Member() 실행");
}
public Member(String id) {
System.out.println("Member(String id) 실행");
this.id = id;
}
public Member(String name, String id) {
System.out.println("Member(String name, String id) 실행");
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
public String getId() {
return id;
}
}
import java.util.function.BiFunction;
import java.util.function.Function;
public class ConstructorRef {
public static void main(String[] args) throws Exception {
Function<String, Member> function1 = Member::new;
Member member1 = function1.apply("kimcoding");
BiFunction<String, String, Member> function2 = Member::new;
Member member2 = function2.apply("kimcoding", "김코딩");
}
}
/*
Member(String id) 실행
Member(String name, String id) 실행
*/
위의 코드 예제는 생성자 참조를 이용해서 두 가지 방법으로 Member 객체를 생성하고 있습니다.
하나는 Function<String, Member> 함수형 인터페이스의 Member apply(String) 메서드를 이용해서 Member 객체를 생성하고, 다른 하나는 BiFunction<String, String, Member> 함수형 인터페이스의 Member 객체를 생성합니다.
이때 생성자 참조는 두 가지 방법 모두 같지만, 실행되는 Member 생성자가 다른 것을 볼 수 있습니다.
스트림(Stream)
스트림(Stream)은 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자입니다.
스트림을 사용하면 List, Set, Map, 배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있습니다.
스트림은 데이터 소스를 다루는 풍부한 메서드를 제공합니다.
이를 활용하면, 다량의 데이터에 복잡한 연산을 수행하면서도, 가독성과 재사용성이 높은 코드를 작성할 수 있습니다.
학습 목표
- 스트림의 핵심적인 개념과 특징을 이해할 수 있다.
- 스트림의 생성, 중간 연산, 최종 연산의 세 단계로 구성된 스트림 파이프라인을 이해하고 활용할 수 있다.
- 스트림의 주요 메서드를 활용해 원하는 데이터 처리를 할 수 있다.
스트림의 핵심 개념과 특징
앞에서 간략하게 언급했듯이, 스트림(Stream)이란 자바8부터 도입된 문법으로 배열 및 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 하는 반복자입니다.

영어에서 스트림(stream)의 사전적 의미는 위의 사진과 같은 “개울”, “냇가” 또는 어떤 “흐름”을 의미하는데, 이와 유사하게 자바에서의 스트림은 “데이터의 흐름”을 의미합니다. 좀 더 구체적으로, 각 데이터를 흐름에 따라 우리가 원하는 결과로 가공하고 처리하는 일련의 과정과 관련이 있습니다.
아마 아직은 그 의미가 잘 와닿지 않으시겠다고 생각합니다.
그럼, 이제부터 스트림이 어떤 맥락에서 도입되었고, 어떤 핵심적인 특징을 가지고 있으며, 어떻게 활용될 수 있는지 살펴보면서 순차적으로 스트림에 대한 좀 더 자세한 내용을 알아보도록 하겠습니다.
스트림(Stream)의 도입 배경
역사적으로, 많은 경우 어떤 기술적 발전은 기존에 있었던 어떤 문제나 한계를 극복하기 위한 시도들에서 이뤄져 왔습니다. 지금부터 우리가 학습하게 될 스트림 또한 마찬가지입니다.
지금까지 우리는 자바 배열과 컬렉션에 관한 내용을 학습하면서, 이러한 자료구조를 통해 많은 수의 데이터를 좀 더 효과적으로 다룰 수 있다는 사실을 이해할 수 있었습니다. 더 나아가, 이렇게 저장된 데이터들에 반복적으로 접근하여 우리가 원하는 모양대로 데이터로 가공하기 위해 for문과 Iterator를 활용해 왔습니다.
하지만, 이렇게 기존 방식대로 데이터를 처리하는 데에는 크게 두 가지 한계가 있습니다.
먼저는 for 문 이나 Iterator를 사용하는 경우, 많은 경우 코드가 길고 복잡해질 수 있습니다.
한 가지 간단한 예를 들어보겠습니다.
- Iterator를 사용한 반복 처리
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class PrintNumberOperator {
public static void main(String[] args) {
// 각 숫자를 배열화
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// Iterator 생성
Iterator<Integer> it = list.iterator();
// 리스트를 순회하며 값 출력
while (it.hasNext()) {
int num = it.next();
System.out.print(num);
}
}
}
//출력값
12345
위의 코드 예제는 List 컬렉션에서 Iterator를 사용하여 값을 순서대로 출력하는 프로그램의 코드입니다.
그럼, 이제 같은 결과를 출력하는 코드를 스트림을 사용하여 작성해 보겠습니다.
- 스트림을 사용한 반복 처리
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class PrintNumberOperatorByStream {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::print);
}
}
//출력값
12345
어떤가요?
사실 위의 짧은 코드만으로는 그 차이가 크게 다가오지 않을 수 있습니다. 하지만, 작성해야 하는 로직이 더 길고 복잡해진다면, 스트림을 사용한 코드 작성의 효과는 분명해질 것입니다. 이 부분은 계속 이어지는 내용을 통해 스트림에 대해 알아가면서 차차 확인해 보시길 바랍니다.
추가적으로, 스트림을 사용하면 선언형 프로그래밍(Declarative Programming) 방식으로 데이터를 처리할 수 있어 더욱 인간 친화적이고 직관적인 코드 작성이 가능합니다.
지금까지 우리가 알고리즘 문제를 풀고, 실습 과제를 하면서 코드를 사용했던 방식은 어떤 주어진 과업을 달성하기 위해 코드 한 줄 한 줄의 동작 원리를 이해하고 순차적이고 세세하게 이를 규정하는 방식이었습니다. 이를 명령형 프로그래밍(Imperative Programming)이라고 하는데, 이러한 방식은 “어떻게” 코드를 작성할지에 대한 내용에 초점을 두고 있다고 할 수 있습니다.
반면, 명령형 프로그래밍과 다르게 선언형 프로그래밍은 “어떻게”가 아닌 “무엇”에 집중하여 코드를 작성하는 코드 작성 방법론을 의미합니다. 즉, 내부의 동작 원리를 모르더라도 어떤 코드가 어떤 역할을 하는지 직관적으로 이해할 수 있습니다. 여기서 “어떻게”에 대한 부분은 추상화되어 있습니다.
마찬가지로 코드를 통해 좀 더 이해해 보겠습니다.
- 명령형 프로그래밍 방식
import java.util.List;
public class ImperativeProgramming {
public static void main(String[] args){
// List에 있는 숫자 중에서 4보다 큰 짝수의 합계 구하기
List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
int sum = 0;
for(int number : numbers){
if(number > 4 && (number % 2 == 0)){
sum += number;
}
}
System.out.println("명령형 프로그래밍을 사용한 합계 : " + sum);
}
}
//출력값
명령형 프로그래밍을 사용한 합계 : 14
위의 코드 예제는 “어떻게”에 초점을 둔 명령형 프로그래밍으로 조건에 부합하는 리스트의 합계를 구하는 코드입니다. 이제 같은 코드를 스트림을 사용하여 선언형 프로그래밍 방식으로 바꿔서 표현해 보겠습니다.
- 선언형 프로그래밍 방식
import java.util.List;
public class DeclarativePrograming {
public static void main(String[] args){
// List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
int sum =
numbers.stream()
.filter(number -> number > 4 && (number % 2 == 0))
.mapToInt(number -> number)
.sum();
System.out.println("선언형 프로그래밍을 사용한 합계 : " + sum);
}
}
//출력값
선언형 프로그래밍을 사용한 합계 : 14
어떤가요?
아직 스트림에 대한 구체적인 내용을 배우지는 않았고, “어떻게”에 해당하는 각 함수의 내부 동작을 전혀 알 수 없지만 한 눈에도 어떤 흐름으로 어떤 일들이 일어나고 있는지 파악하는 일이 어렵지 않습니다. 이처럼 스트림을 사용하면 보다 직관적이고 간결한 코드 작성이 용이합니다.
또 한 가지 여기서 눈 여겨봐야 할 부분은 각 메서드에서 람다식을 사용하여 데이터를 처리하고 있다는 사실입니다.
곧 좀 더 자세히 살펴보겠지만, filter 메서드에서는 람다식을 사용하여 주어진 데이터 소스에서 4보다 크면서 짝수인 수를 걸러낼 수 있도록 조건을 주었고, mapToInt 메서드 또한 람다식을 사용하여 이렇게 걸러진 값들을 int 타입의 정수로 바꿔주고 있습니다. 이처럼 스트림에서 람다식을 사용하여 데이터를 어떻게 처리할지 규정할 수 있습니다.
두 번째로, 기존 방식으로 데이터를 처리하는 경우에는 데이터 소스를 각기 다른 방식으로 다뤄야 한다는 불편함이 있습니다. 다른 말로 표현하면, 표준화된 하나의 방식이 아닌 데이터 소스에 따라 그에 부합하는 방식의 메서드를 각각 다르게 적용해서 데이터 처리를 해야 한다는 의미입니다.
예를 들면, 어떤 데이터 집합을 순차적으로 정렬해야 한다고 했을 때, 같은 정렬 기능을 수행하는 데 있어서 배열의 경우에는 Arrays.sort()를, List의 경우에는 Collections.sort()를 각각 다르게 사용해야 합니다. 이렇듯 각각의 필요한 기능들을 사용할 때마다 데이터 소스에 맞는 방식을 찾아야 하는 일은 개발자의 입장에서 매우 번거롭고 불편한 일이 아닐 수 없습니다.
이러한 문제를 해결하기 위해 도입된 자바의 문법요소가 바로 스트림입니다.
이제 스트림을 사용하면, 데이터 소스가 무엇이냐에 관계없이 같은 방식으로 데이터를 가공/처리할 수 있습니다. 다른 말로, 배열이냐 컬렉션이냐에 관계없이 하나의 통합된 방식으로 데이터를 다룰 수 있게 되었다는 뜻입니다.
아래 예시를 통해 좀 더 알아보겠습니다.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamOperator {
public static void main(String[] args) {
// ArrayList
List<String> fruitList = new ArrayList<>();
fruitList.add("바나나 ");
fruitList.add("사과 ");
fruitList.add("오렌지 ");
// 배열
String[] fruitArray = {"바나나 ", "사과 ", "오렌지 "};
// 각각 스트림 생성
Stream<String> ListStream = fruitList.stream();
Stream<String> ArrayStream = Arrays.stream(fruitArray);
// 출력
ListStream.forEach(System.out::print);
ArrayStream.forEach(System.out::print);
}
}
//출력값
바나나 사과 오렌지 바나나 사과 오렌지
위의 예제는 스트림을 사용하여 각기 다른 데이터 소스를 동일한 방식으로 처리하는 모습을 잘 보여주고 있습니다. 문자열을 요소로 가지는 List와 문자열 배열에 각각 스트림을 생성하고 forEach() 메서드를 사용해서 각 요소를 순회하며 출력해주고 있습니다.
참고로 forEach() 메서드는 데이터 소스의 각 요소를 순회하면서 람다식 안에 정의된 어떤 명령을 실행하는 데 사용하는 최종연산자입니다. 스트림의 기본적인 구조와 그 안에서 빈번하게 활용되는 메서드들에 대해서는 뒤에서 좀 더 자세하게 알아보도록 하겠습니다.
결론적으로, 스트림은 앞서 언급한 기존 방식의 두 가지 한계를 효과적으로 보완하면서, 좀 더 간결하고 직관적인 코드 작성을 가능하게 합니다. 다음으로 이제 스트림의 핵심적인 특징들에 대해 살펴보도록 하겠습니다.
스트림의 특징
자바의 다른 문법 요소들과 마찬가지로, 스트림 또한 여러 복잡하고 깊은 내용들을 가지고 있지만 이 단계에서 우리는 먼저 딱 4 가지의 핵심적인 특징들만 기억하는 것으로 충분합니다. 핵심적으로 기억해야 하는 스트림의 4 가지의 특징들은 다음과 같습니다.
- 스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계의 파이프라인으로 구성될 수 있다.
- 스트림은 원본 데이터 소스를 변경하지 않는다(read-only).
- 스트림은 일회용이다(onetime-only).
- 스트림은 내부 반복자이다.
이제 각각의 특징들을 간략하게 살펴보도록 하겠습니다.
1. 스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계의 파이프라인으로 구성될 수 있다.
스트림을 바르게 이해하기 위해서는 아래 그림으로 요약될 수 있는 스트림 파이프 라인(stream pipeline)에 대한 이해가 필수적입니다. 사실 아래 도식을 이해하면 스트림의 핵심을 모두 이해했다고 봐도 큰 과장이 아닙니다.

위에서 언급한 것처럼, 스트림 파이프라인은 1) 스트림의 생성, 2) 중간 연산, 3) 최종 연산이라는 총 세 가지 단계로 구성되어 있습니다. 사실 중간 연산을 생략하고 곧바로 최종연산으로 넘어가는 두 단계 구성도 가능하지만, 지금은 가장 빈번하게 사용되는 완전체 파이프라인을 가정해 보겠습니다.
간략하게 흐름을 설명하면, 먼저 배열, 컬렉션, 임의의 수 등 다양한 데이터 소스를 일원화하여 스트림으로 작업하기 위해서는 스트림을 생성해야 합니다. 스트림이 생성되고 나면, 최종 처리를 위한 중간 연산을 수행할 수 있습니다.
여기에는 필터링, 매핑, 정렬 등의 작업이 포함되며, 중간 연산의 결과는 또 다른 스트림이기 때문에 계속 연결해서 연산을 수행할 수 있습니다. 이렇게 연결된 모양새가 마치 파이프라인과 같다고 해서 이러한 구조를 스트림 파이프라인이라고 합니다.
마지막으로, 이렇게 중간 연산이 완료된 스트림을 최종적으로 처리하는 최종 연산(총합, 평균, 카운팅 등)을 끝으로 스트림은 닫히고 모든 데이터 처리가 완료됩니다. 최종 연산의 경우는 스트림의 요소를 소모하면서 연산을 수행하기 때문에 최종적으로 단 한 번의 연산만 가능합니다. 따라서 최종 연산 후에 다시 데이터를 처리하고 싶다면, 다시 스트림을 생성해주어야 합니다.
아래 예시를 잠시 살펴보겠습니다.

위의 도식은 남성과 여성으로 구성된 어떤 회원 컬렉션을 스트림을 사용하여 의도한 데이터로 가공하는 과정을 보여주는 스트림 파이프라인입니다. 가장 먼저 스트림을 생성하고, 생성한 스트림에서 중간 연산 단계로 성별이 남자인 회원만 필터링한 후에, 그중에서 나이 요소만을 매핑한 후, 최종 연산을 통해 남자 회원들의 나이 평균을 구했습니다.
아직 잘 이해가 안 되거나 어렵게 느껴지더라도 괜찮습니다. 스트림 파이프라인을 어떻게 활용할 수 있는지에 대한 좀 더 구체적인 내용은 이어지는 챕터에서 자세히 학습할 예정이니, 지금은 전체적인 구조와 흐름을 이해하는 것에 집중해 주세요.
2. 스트림은 원본 데이터 소스를 변경하지 않는다(read-only).
스트림은 그 원본이 되는 데이터 소스의 데이터들을 변경하지 않습니다. 오직 데이터를 읽어올 수 있고, 데이터에 대한 변경과 처리는 생성된 스트림 안에서만 수행됩니다. 이는 원본 데이터가 스트림에 의해 임의로 변경되거나 데이터가 손상되는 일을 방지하기 위함입니다.
3. 스트림은 일회용이다(onetime-only).
위에서 스트림 파이프라인에 관해 설명할 때 잠시 언급했듯이, 스트림은 일회용입니다. 다르게 표현하면, 스트림이 생성되고 여러 중간 연산을 거쳐 마지막 연산이 수행되고 난 후에는 스트림은 닫히고 다시 사용할 수 없습니다. 만약 추가적인 작업이 필요하다면, 다시 스트림을 생성해야 합니다. 마치 컬렉션에서 배웠던 Iterator와 비슷하다고 할 수 있습니다.
4. 스트림은 내부 반복자이다.
내부 반복자(Internal Iterator)를 이해하기 위해서 먼저 이에 반대되는 개념인 외부 반복자(External Iterator)를 알면 도움이 됩니다. 외부 반복자란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 의미합니다. 인덱스를 사용하는 for문, Iterator를 사용하는 while문 이 대표적입니다.
반면 스트림은 반대로 컬렉션 내부에 데이터 요소 처리 방법(람다식)을 주입해서 요소를 반복처리 하는 방식입니다. 아래 그림을 통해 좀 더 살펴보겠습니다.

위에서 확인할 수 있는 것처럼, 외부 반복자의 경우 요소가 필요할 때마다 순차적으로 컬렉션에서 필요한 요소들을 불러오지만, 내부반복자는 데이터 처리 코드만 컬렉션 내부로 주입해 줘서 그 안에서 모든 데이터 처리가 이뤄지도록 합니다. 병렬 작업, 멀티 코어 최적화 등 어려운 컴퓨터공학 용어를 사용하지 않더라도 한 눈에도 더욱 효율적인 데이터 처리가 가능하다는 사실을 어렵지 않게 이해할 수 있습니다.
지금까지 스트림이 어떤 필요에 따라 자바8 이후에 도입되었으며, 어떤 구조와 특징을 가지고 있는지 살펴보았습니다. 이어지는 챕터에서는 이제 이러한 스트림 파이프라인의 구조 속에서 스트림의 데이터 처리 작업이 어떻게 구성되고 사용될 수 있는지 좀 더 구체적인 내용을 살펴보도록 하겠습니다.
스트림의 생성
이제 본격적으로 스트림 파이프라인의 세 단계를 차례대로 하나씩 살펴보도록 하겠습니다.
앞서 설명한 것처럼, 스트림으로 데이터를 처리하기 위해서는 가장 먼저 스트림을 생성해야 합니다. 스트림을 생성할 수 있는 데이터 소스는 배열, 컬렉션, 임의의 수, 특정 범위의 정수 등 다양한데, 이에 따라 스트림의 생성 방법에 조금씩 차이가 있습니다.
여기서는 그중에서 가장 많이 쓰이는 배열, 컬렉션, 그리고 임의의 수로 스트림을 생성하는 방법을 학습해 보도록 하겠습니다. 대부분은 이 세 가지를 사용하여 스트림을 생성하기 때문에, 아래 소개되는 기본적인 스트림 생성법을 먼저 기억해 두시고, 이후에 필요하다면 좀 더 다양한 스트림 생성 방법을 찾아서 활용하시길 바랍니다.
마지막으로 아래에서 소개되는 코드 예제들은 반드시 눈으로만 확인하지 마시고, 직접 본인의 인텔리제이에서 입력해 보시면서 몸소 내용을 체화하시기를 당부드립니다.
이럼 이제 스트림의 생성에 대해 좀 더 자세한 내용을 알아보겠습니다.
배열 스트림 생성
먼저 배열을 데이터 소스로 하는 스트림 생성은 Arrays 클래스의 stream() 메서드 또는 Stream 클래스의 of() 메서드를 사용할 수 있습니다.
- Arrays.stream()
- public class StreamCreator { public static void main(String[] args) { // 문자열 배열 선언 및 할당 String[] arr = new String[]{"김코딩", "이자바", "박해커"}; // 문자열 스트림 생성 Stream<String> stream = Arrays.stream(arr); // 출력 stream.forEach(System.out::println); } } // 출력값 김코딩 이자바 박해커
- Stream.of()
- import java.util.stream.Stream; public class StreamCreator { public static void main(String[] args) { // 문자열 배열 선언 및 할당 String[] arr = new String[]{"김코딩", "이자바", "박해커"}; // 문자열 스트림 생성 Stream<String> stream = Stream.of(arr); // 출력 stream.forEach(System.out::println); } } // 출력값 김코딩 이자바 박해커
위의 예제의 출력값을 확인해 보면 Arrays.stream()와 Stream.of() 메서드 모두 같은 값을 출력하고 있다는 사실을 알 수 있습니다. 따라서, 배열로 스트림을 생성할 시에는 둘 중에 더 편한 메서드를 임의로 선택하여 사용할 수 있습니다.
추가로, Arrays 클래스에는 int, long , double과 같은 기본형 배열을 데이터 소스로 스트림을 생성하는 메서드도 있습니다.
Arrays 클래스

한 가지만 더 언급하면, IntStream의 경우는 일반적인 Stream 클래스에는 없는 숫자와 관련한 여러 유용한 메서드들이 정의되어 있으므로 기억해 두면 요긴하게 사용할 수 있습니다.
IntStream의 유용한 기능들
import java.util.Arrays;
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
// int형 배열로 스트림 생성
int[] intArr = {1,2,3,4,5,6,7};
IntStream intStream = Arrays.stream(intArr);
// 숫자와 관련된 경우 intStream을 사용하는 것을 권장
System.out.println("sum=" + intStream.sum());
// System.out.println("average=" + intStream.average());
}
}
//출력값
sum=28
참고로 이어지는 내용을 통해 자세히 살펴보겠지만, 숫자 연산과 관련된 대부분의 메서드(합계, 카운팅, 평균, 최대값 등)는 최종 연산자이기 때문에 최초 사용 시 스트림이 닫히게 됩니다. 예를 들면, 위의 코드 예제에서 sum() 메서드를 호출한 이후에 다시 average() 메서드를 호출하면 에러가 발생합니다. 이 부분도 직접 코드로 확인해 보시길 바랍니다.
컬렉션 스트림 생성
컬렉션 타입(List, Set 등)의 경우, 컬렉션의 최상위 클래스인 Collection에 정의된 stream() 메서드를 사용하여 스트림을 생성할 수 있습니다. 따라서 Collection으로부터 확장된 하위클래스 List와 Set을 구현한 컬렉션 클래스들은 모두 stream() 메서드를 사용하여 스트림을 생성할 수 있습니다.
아래 예제를 통해 확인해 보겠습니다.
- 컬렉션 스트림 생성
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreator {
public static void main(String[] args) {
// 요소들을 리스트
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::print);
}
}
//출력값
1234567
위의 예제는 List 타입의 스트림을 생성하는 과정을 보여주고 있습니다. Arrays 클래스에 static 하게 선언된 asList() 메서드를 사용하여 요소들을 리스트 타입의 참조변수에 할당한 뒤에 stream() 메서드를 사용하여 스트림을 생성하였습니다.
임의의 수 스트림 생성
난수를 생성하는 자바의 기본 내장 클래스 Random 클래스 안에는 해당 타입의 난수들을 반환하는 스트림을 생성하는 메서드들이 정의되어 있습니다. 예를 들면, int() 메서드의 경우 int 형의 범위 안에 있는 난수들을 무한대로 생성하여 IntStream 타입의 스트림으로 반환합니다.
여러분들의 인텔리제이를 열어서 아래 코드를 입력해 보세요.
import java.util.Random;
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
// 난수 생성
IntStream ints = new Random().ints();
ints.forEach(System.out::println);
}
}
int 형의 범위에서 출력값이 무한대로 생성되는 걸 확인하셨나요?
이렇게 스트림의 크기가 정해지지 않은 것을 **무한 스트림(infinite stream)**이라 부르는데, 무한 스트림은 주로 뒤에서 배우게 될 limit() 메서드와 함께 사용하거나 매개변수로 스트림의 사이즈를 전달해서 그 범위를 제한할 수 있습니다.
import java.util.Random;
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
// 스트림 생성의 범위를 5개로 제한
IntStream ints = new Random().ints(5);
IntStream ints = new Random().ints().limit(5);
ints.forEach(System.out::println);
}
}
추가로, IntStream과 LongStream에 정의된 range() 나 rangeClosed() 메서드를 사용하면 다음과 같이 특정 범위의 정수값을 스트림으로 생성해서 반환하는 것도 가능합니다.
import java.util.stream.IntStream;
public class StreamCreator {
public static void main(String[] args) {
//특정 범위의 정수
IntStream intStream = IntStream.rangeClosed(1, 10);
intStream.forEach(System.out::println);
}
}
//출력값
12345678910
rangeClosed()와 range()의 차이는 두 번째로 전달되는 매개 변수가 범위 안에 포함되는지 여부에 따라 구분될 수 있습니다. rangeClosed()는 끝 번호가 포함되어 위의 코드 예제처럼 1~10까지의 숫자가 출력되는 반면, range()의 경우에는 끝번호가 포함되지 않아 1~9까지의 숫자가 출력됩니다.
이렇게 스트림의 생성에 대한 핵심적인 내용들을 살펴봤습니다. 이어지는 챕터에서는 스트림의 생성 이후에 사용할 수 있는 중간 연산자에 대해 학습해 보도록 하겠습니다.
스트림의 중간 연산
이제 스트림의 생성 이후에 수행할 수 있는 중간 연산자(Intermediate Operation)에 대해 알아보도록 하겠습니다.
앞서 스트림의 특징에서 설명한 것처럼, 스트림의 중간 연산자의 결과는 스트림을 반환하기 때문에 여러 개의 연산자를 연결하여 우리가 원하는 데이터 처리를 수행할 수 있다고 했습니다.

여기 Oracle에서 제공하는 공식 스펙 문서를 확인해 보면 다양한 중간 연산자들을 확인해 볼 수 있는데, 여기서는 그중에서 가장 빈번하게 사용되는 필터링(filtering), 매핑(maping), 정렬(sorting) 등 몇 가지 중간 연산자를 중심으로 설명하도록 하겠습니다.
중간 연산자에 관한 내용은 처음부터 모든 내용을 완벽하게 숙지하려 하기보다는, 주로 많이 사용되는 중간 연산자들의 기본적인 개념과 사용법을 먼저 잘 이해한 후에, 다음에 필요하다면 검색을 통해 추가적인 내용들을 찾아 활용하시는 방향을 권장해 드립니다.
먼저 구체적인 내용을 확인하기 전에, 전체적인 코드 구조를 파악해 보도록 하겠습니다.

위의 코드는 앞에서 우리가 확인했던 도식을 코드화하여 보여주고 있습니다.
우리가 스트림의 핵심 개념과 특징에서 살펴본 것처럼, 최초에 데이터 소스를 가지고 스트림을 생성한 후에 중간 연산자로 데이터를 가공하고, 최종 연산자를 통해서 스트림 작업을 종료합니다. 위의 코드 구조를 먼저 머릿속에 그려두고 이어지는 아래의 내용들을 하나씩 학습해 가시길 바랍니다.
그럼, 이제부터 본격적으로 중간 연산자들을 하나씩 살펴보겠습니다.
필터링(filter() , distinct() )
필터링은 이름 그대로 우리의 필요에 따라 조건에 맞는 데이터들만을 정제하는 역할을 하는 중간 연산자를 가리킵니다.
- distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용합니다.
- filter(): Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어냅니다. filter() 메서드에는 매개값으로 조건(Predicate)을 주고, 조건이 참이 되는 요소만 필터링합니다. 여기서 조건은 람다식을 사용하여 정의할 수 있습니다.
import java.util.Arrays;
import java.util.List;
public class FilteringExample {
public static void main(String[] args) throws Exception {
List<String> names = Arrays.asList("김코딩", "이자바", "박해커", "김코딩", "박해커");
names.stream()
.distinct() //중복 제거
.forEach(element -> System.out.println(element));
System.out.println();
names.stream()
.filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링
.forEach(element -> System.out.println(element));
System.out.println();
names.stream()
.distinct() //중복제거
.filter(element -> element.startsWith("김")) // 김씨 성을 가진 요소만 필터링
.forEach(element -> System.out.println(element));
}
}
// 출력값
김코딩
이자바
박해커
김코딩
김코딩
김코딩
위의 코드 예제는 필터링의 과정을 순차적으로 잘 보여주고 있습니다. 가장 첫 번째 스트림에서는 중복 제거 중간 연산만을 수행하여 리스트 타입의 참조변수 names 안에 중복되고 있는 “김코딩” 요소를 제거했습니다. 두 번째 스트림에서는 “김”씨 성을 가진 요소들만 필터링하여 “김코딩”을 두 번 출력해주고 있습니다.
지금쯤이면 이제 잘 이해하고 계시겠지만, 첫 번째와 두 번째 스트림은 각각 독립적인 스트림입니다. 대표적인 최종 연산자인 forEach()로 첫 번째 스트림이 닫히고 난 후에 두 번째 스트림이 생성되어 새로운 연산을 수행하고 있습니다.
마지막 세 번째 스트림은 중복제거와 필터링을 모두 수행하였고, 그 결과로 “김코딩”이라는 문자열을 단 한 번 출력해주고 있습니다.
매핑(map())
매핑은 스트림 내 요소들에서 원하는 필드만 추출하거나 특정 형태로 변환할 때 사용하는 중간 연산자입니다. 위의 filter() 메서드와 마찬가지로 값을 변환하기 위한 조건을 람다식으로 정의합니다.
아래 예제를 통해 확인해 보겠습니다.
import java.util.Arrays;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("kimcoding", "javalee", "hackerna", "luckyguy");
names.stream()
.map(element -> element.toUpperCase()) // 요소들을 하나씩 대문자로 변환
.forEach(element->System.out.println(element));
}
}
// 출력값
KIMCODING
JAVALEE
HACKERNA
LUCKYGUY
위의 예제는 리스트 타입의 참조변수 names 안에 정의된 각 요소를 순회하면서 소문자 이름을 대문자로 변환한 값들이 담긴 스트림으로 반환하는 연산 과정을 보여주고 있습니다.
또한, 다음과 같이 각 요소에 어떤 연산을 실행하고 난 후의 값을 반환받을 수 있습니다. 아래의 코드 예제는 각 요소에 값에 3을 곱한 값을 스트림으로 반환하여 출력하는 과정을 보여주고 있습니다.
import java.util.Arrays;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args)
{
List<Integer> list = Arrays.asList(1, 3, 6, 9);
// 각 요소에 3을 곱한 값을 반환
list.stream().map(number -> number * 3).forEach(System.out::println);
}
}
// 출력값
3
9
18
27
map()과 함께 많이 사용되는 flatMap() 중간 연산자도 있습니다.
예제를 통해 그 차이를 이해해 보겠습니다.
만약에 아래와 같은 이중 배열이 있고, 그 안의 배열을 위에서 학습한 map() 메서드를 사용하여 하나씩 출력해 주는 프로그램을 만들어야 한다고 가정해 보겠습니다.
// 주어진 이중 배열
String[][] namesArray = new String[][]{{"박해커", "이자바"}, {"김코딩", "나박사"}};
// 기대하는 출력값
박해커
이자바
김코딩
나박사
이제 앞에서 배웠던 방식으로 map()을 사용하여 기대하는 값을 출력해 보겠습니다.
// map() 사용
Arrays.stream(namesArray)
.map(inner -> Arrays.stream(inner))
.forEach(System.out::println);
// 출력값
java.util.stream.ReferencePipeline$Head@3cb5cdba
java.util.stream.ReferencePipeline$Head@56cbfb61
출력값은 우리의 기대와는 다르게 위와 같은 스트림 객체의 값을 반환할 것입니다.
왜냐하면 위의 연산에서 map() 메서드는 Stream<Stream<String>> 즉 중첩 스트림을 반환하고 있기 때문입니다. 여기서 우리가 원하는 결과값을 출력하기 위해서는 반환 타입이 Stream<Stream<String>> 이 아닌 Stream<String> 이 되어야 합니다.
그렇다면 우리가 원하는 결과값을 얻기 위해서 어떻게 해야 할까요?
첫 번째 방법은 아래와 같이 위의 코드를 조금 수정하는 것입니다.
- 기존 방식의 수정
// map 사용
Arrays.stream(namesArray)
.map(inner -> Arrays.stream(inner))
.forEach(names -> names.forEach(System.out::println));
// 출력값
박해커
이자바
김코딩
나박사
위의 코드에서 확인할 수 있는 것처럼, forEach() 메서드 안의 람다식의 정의에서, 각 요소에 대하여 다시 forEach() 메서드를 출력함으로써 뎁스가 있는 요소들에 접근하여 이를 출력할 수 있습니다.
하지만 지금처럼 이중구조가 아닌 뎁스가 3중, 4중, 5중으로 깊어지는 경우는 어떨까요?
아마 코드를 작성하는 개발자 입장에서도 매우 번거롭고 작성된 코드 또한 가독성이 떨어질 것입니다.
이런 경우, 우리는 flatMap()을 활용할 수 있습니다.
- flatMap()
// flatMap()
Arrays.stream(namesArray).flatMap(Arrays::stream).forEach(System.out::println);
// 출력값
박해커
이자바
김코딩
나박사
위의 코드에서 확인할 수 있는 것처럼, flatMap() 은 중첩 구조를 제거하고 단일 컬렉션(Stream<String>)으로 만들어주는 역할을 합니다. 이를 요소들을 “평평하게”한다는 의미에서 플래트닝(flattening)이라고 합니다.
이처럼, 위와 같이 배열 요소들의 뎁스가 있는 작업들을 수행할 때 flatMap() 메서드를 활용하면 훨씬 간편하고 효과적으로 같은 작업을 수행할 수 있습니다.
정렬(sorted())
sorted() 메서드는 이름처럼 정렬할 때 사용하는 중간 연산자입니다.
sorted() 메서드를 사용하여 정렬할 때는 괄호(()) 안에 Comparator라는 인터페이스에 정의된 static 메서드와 디폴트 메서드를 사용하여 간편하게 정렬 작업을 수행할 수 있습니다. 괄호 안에 아무 값도 넣지 않은 상태로 호출하면 기본 정렬(오름차순)로 정렬됩니다.
마찬가지로, 예시를 통해 좀 더 알아보겠습니다.
- 기본 정렬
import java.util.Arrays;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 동물들의 이름을 모아둔 리스트
List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");
// 인자값 없는 sort() 호출
animals.stream().sorted().forEach(System.out::println);
}
}
// 출력값
Cow
Duck
Horse
Lion
Monkey
Tiger
- 역순으로 정렬
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class IntermediateOperationExample {
public static void main(String[] args) {
List<String> animals = Arrays.asList("Tiger", "Lion", "Monkey", "Duck", "Horse", "Cow");
// 인자값에 Comparator 인터페이스에 규정된 메서드 사용
animals.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
}
}
// 출력값
Tiger
Monkey
Lion
Horse
Duck
Cow
위의 기본 정렬은 생략하고, 아래 요소들의 값을 역순으로 정렬하고 출력하고 있는 아래의 코드만 잠시 살펴보겠습니다. 앞서 언급했던 것처럼, Comparator 인터페이스 안에 정의된 reverseOrder()라는 이름의 메서드를 인자로 넘겨 호출하는 것만으로 손쉽게 역순으로 정렬하는 기능을 실행할 수 있습니다.
Comparator 인터페이스와 그 안에 정의된 더 다양한 정렬 기능들이 궁금하시다면, 여기 공식 스펙 문서를 통해 한번 천천히 확인해 보시기를 바랍니다.
기타
앞에서 언급한 중간 연산자 외에 다음과 같은 중간 연산자들이 있습니다. 아래 중간 연산자들은 비교적 쉽고 이해하기 어렵지 않기 때문에 코드 예제에 대한 추가적인 설명은 생략하도록 하겠습니다.
- skip() - 스트림의 일부 요소들을 건너뜁니다.
import java.util.stream.IntStream;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 1~10 범위의 정수로 구성된 스트림 생성
IntStream intStream = IntStream.rangeClosed(1, 10);
// 앞의 5개의 숫자를 건너뛰고 숫자 6부터 출력
intStream.skip(5).forEach(System.out::println);
}
}
// 출력값
6
7
8
9
10
- limit() - 스트림의 일부를 자릅니다.
import java.util.stream.IntStream;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 1~10 범위의 정수로 구성된 스트림 생성
IntStream intStream = IntStream.rangeClosed(1, 10);
// 앞에서부터 5개의 숫자만 출력
intStream.limit(5).forEach(System.out::println);
}
}
// 출력값
1
2
3
4
5
- peek() - forEach()와 마찬가지로, 요소들을 순회하며 특정 작업을 수행합니다. forEach()와의 핵심적인 차이는 중간 연산자인지의 여부입니다. peek()는 중간 연산자이기 때문에 여러 번 연결하여 사용할 수 있지만, forEach()는 최종 연산자이기 때문에 마지막에 단 한 번만 사용할 수 있습니다. 이러한 peek()의 특성 때문에 주로 코드의 에러를 찾기 위한 디버깅(debugging) 용도로 종종 활용됩니다.
import java.util.stream.IntStream;
public class IntermediateOperationExample {
public static void main(String[] args) {
// 요소들을 사용하여 IntStream 생성
IntStream intStream3 = IntStream.of(1, 2, 2, 3, 3, 4, 5, 5, 7, 7, 7, 8);
// 짝수만 필터링하여 합계 구하기
int sum = intStream3.filter(element -> element % 2 == 0)
.peek(System.out::println)
.sum();
System.out.println("합계 = " + sum);
}
}
// 출력값
2
2
4
8
합계 = 16
지금까지 스트림의 중간 연산자에 대해서 알아봤습니다. 이어지는 챕터에서는 중간 연산자 이후에 스트림 파이프라인의 최종 단계를 담당하는 최종 연산자에 대해서 알아보도록 하겠습니다.
스트림의 최종 연산
이제 스트림 파이프라인의 세 단계 중 마지막에 해당하는 최종 연산(Terminal Operation)에 대해서 알아보도록 하겠습니다.
사실 그중에 하나는 이미 우리가 익숙하게 잘 알고 있습니다. 뭘까요?
맞습니다. 지금까지 우리는 중간 연산자에 대해 학습하면서 그 연산의 결과를 확인하기 위한 수단으로 forEach() 메서드를 사용해 왔습니다. 그리고 forEach() 메서드는 대표적인 최종 연산자입니다.
다르게 표현해 보면, forEach() 메서드가 스트림 파이프라인에서 최종적으로 사용되고 나면, 해당 스트림은 닫히고 모든 연산이 종료된다는 의미입니다.

또한, 앞에서 자세히 다루지 않았지만 중간 연산은 최종 연산자가 수행될 때야 비로소 스트림의 요소들이 중간 연산을 거쳐 가공된 후에 최종 연산에서 소모되는데 이를 좀 어려운 말로 “지연된 연산(lazy evaluation)”이라고 부릅니다.
다시 돌아가서, 스트림에는 forEach() 메서드 외에도 다양한 최종 연산자들이 존재합니다. 앞서 중간 연산자 챕터에서 강조했던 것처럼, 지금 단계에서 모든 최종 연산자를 완벽하게 알고 적절하게 사용하기는 매우 어려운 일입니다.
따라서 아래 내용을 통해 기본적인 개념과 사용법을 먼저 이해한 후에 그때그때 필요에 따라 추가적인 검색을 통해 비어있는 부분들을 채워가시는 공부 방향을 추천드립니다. 참고로 forEach()의 경우, 우리가 앞의 예제들을 통해 계속 사용해 왔기 때문에 여기서 따로 다루지는 않겠습니다.
다시 강조하자면, 반드시 눈으로만 확인하지 마시고 본인의 인텔리제이에서 직접 입력해 보면서 내용을 체화하시길 바랍니다.
그럼, 이제 최종 연산자에 대해서 하나씩 살펴보도록 하겠습니다.
기본 집계(sum() , count() , average(), max() , min())
가장 간단한 부분부터 먼저 살펴볼까요?
앞에서 잠깐 언급했던 것처럼, 숫자와 관련된 기본적인 집계의 경우에는 대부분 최종 연산자라고 생각해도 크게 틀리지 않습니다.
아래 예제 코드와 같이 최종연산자를 사용하여 숫자와 관련된 데이터 처리를 간편하게 수행할 수 있습니다.
- 기본 집계
import java.util.Arrays;
public class TerminalOperationExample {
public static void main(String[] args) {
// int형 배열 생성
int[] intArray = {1,2,3,4,5};
// 카운팅
long count = Arrays.stream(intArray).count();
System.out.println("intArr의 전체 요소 개수 " + count);
// 합계
long sum = Arrays.stream(intArray).sum();
System.out.println("intArr의 전체 요소 합 " + sum);
// 평균
double average = Arrays.stream(intArray).average().getAsDouble();
System.out.println("전체 요소의 평균값 " + average);
// 최대값
int max = Arrays.stream(intArray).max().getAsInt();
System.out.println("최대값 " + max);
// 최소값
int min = Arrays.stream(intArray).min().getAsInt();
System.out.println("최소값 " + min);
// 배열의 첫 번째 요소
int first = Arrays.stream(intArray).findFirst().getAsInt();
System.out.println("배열의 첫 번째 요소 " + first);
}
}
// 출력값
intArr의 전체 요소 개수 5
intArr의 전체 요소 합 15
전체 요소의 평균값 3.0
최대값 5
최소값 1
배열의 첫 번째 요소 1
위의 코드 예제의 경우, 대부분 코드를 확인하면 직관적으로 이해할 수 있기 때문에 따로 설명은 생략하고, 딱 한 가지만 언급하고 지나가도록 하겠습니다.
혹시 평균, 최대값, 최소값, 배열의 첫 번째 요소를 구하는 예제 코드에서 의문이 생기지 않으셨나요?
분명 스트림의 최종 연산자로 스트림이 닫힌다고 배웠었는데, 뒤에 getAsInt() 또는 getAsDouble() 메서드가 다시 붙고 있습니다. 이 부분을 어떻게 이해할 수 있을까요?
결론적으로 말하면, 최종 연산자로 스트림이 닫힌다는 사실에는 변함이 없습니다.
이해를 돕기 위해, 평균을 구하는 스트림 작업에 해당하는 예제 코드를 잠시 가져와 쪼개보겠습니다.
- 기존 코드
double average = Arrays.stream(intArray).average().getAsDouble();
- 재작성한 코드
import java.util.Arrays;
import java.util.OptionalDouble;
public class TerminalOperationExample {
public static void main(String[] args) {
// int형 배열 생성
int[] intArr = {1,2,3,4,5};
// 평균값을 구해 Optional 객체로 반환
OptionalDouble average = Arrays.stream(intArr).average();
System.out.println(average);
// 기본형으로 변환
double result = average.getAsDouble();
System.out.println("전체 요소의 평균값 " + result);
}
}
//출력값
OptionalDouble[3.0]
전체 요소의 평균값 3.0
아래 재작성한 코드는 기존의 코드와 완전히 동일합니다. 구분하니까 이제 어떤 일이 벌어졌는지 좀 더 명확하게 보입니다. average() 연산자가 반환하는 값의 반환 타입을 확인해 보니 OptionalDouble이라고 되어있습니다.
참고로 지금 자세한 내용까지 알 필요는 없지만, OptionalDouble 클래스는 일종의 래퍼 클래스로, null 값으로 인해서 NullPointerException (NPE) 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하기 위한 목적으로 도입된 것입니다. 즉, 연산 결과를 Optional 객체 안에 담아서 반환하면, 따로 if문을 사용한 조건문으로 반환된 결과가 null인지 여부를 체크하지 않아도 에러가 발생하지 않도록 코드를 작성할 수 있습니다.
다시 돌아가서, 결론적으로 average() 최종연산자가 반환하는 값의 타입이 OptionalDouble , 즉 래퍼 클래스 객체로 되어있기 때문에 우리가 원하는 기본형으로 변환하는 과정이 한 번 더 필요하다는 사실이 중요합니다. 이름에서 알 수 있듯이, getAsDouble()과 getAsInt()는 객체로 반환되는 값을 다시 기본형으로 변환하기 위해 사용되는 메서드로 스트림 파이프라인과는 관계가 없다는 사실을 기억하도록 합시다.
혹시 Optional 객체와 관련된 추가적인 내용이 궁금하시다면, 여기 공식 스펙 문서를 통해 좀 더 살펴보는 것도 좋은 공부가 될 것입니다.
매칭(allMatch(), anyMatch(), noneMatch() )
다음으로 매칭과 관련한 최종연산자를 살펴보겠습니다.
match() 메서드를 사용하면 조건식 람다 Predicate를 매개변수로 넘겨 스트림의 각 데이터 요소가 특정한 조건을 충족하는지 않는지 검사하여, 그 결과를 boolean 값으로 반환합니다.
- *match() 메서드*는 크게 다음의 3가지 종류가 있습니다.
- allMatch() - 모든 요소가 조건을 만족하는지 여부를 판단합니다.
- noneMatch() - 모든 요소가 조건을 만족하지 않는지 여부를 판단합니다.
- anyMatch() - 하나라도 조건을 만족하는 요소가 있는지 여부를 판단합니다.
- 매칭
import java.util.Arrays;
public class TerminalOperationExample {
public static void main(String[] args) throws Exception {
// int형 배열 생성
int[] intArray = {2,4,6};
// allMatch()
boolean result = Arrays.stream(intArray).allMatch(element-> element % 2 == 0);
System.out.println("요소 모두 2의 배수인가요? " + result);
// anyMatch()
result = Arrays.stream(intArray).anyMatch(element-> element % 3 == 0);
System.out.println("요소 중 하나라도 3의 배수가 있나요? " + result);
// noneMatch()
result = Arrays.stream(intArray).noneMatch(element -> element % 3 == 0);
System.out.println("요소 중 3의 배수가 하나도 없나요? " + result);
}
}
// 출력값
요소 모두 2의 배수인가요? true
요소 중 하나라도 3의 배수가 있나요? true
요소 중 3의 배수가 하나도 없나요? false
이 부분은 크게 어렵지 않기 때문에 따로 추가적인 설명을 덧붙이지 않겠습니다. 본인의 인텔리제이에서 직접 입력해 보고 충분히 연습해 보세요.
요소 소모(reduce())
이름에서 유추할 수 있듯이, reduce() 최종 연산자는 스트림의 요소를 줄여나가면서 연산을 수행하고 최종적인 결과를 반환합니다.
사실 스트림의 최종 연산은 모두 요소를 소모하여 연산을 수행하지만, reduce() 메서드의 경우에는 먼저 첫 번째와 두 번째 요소를 가지고 연산을 수행하고, 그 결과와 다음 세 번째 요소를 가지고 또다시 연산을 수행하는 식으로 연산이 끝날 때까지 반복합니다.
그렇기 때문에 reduce() 메서드의 매개변수 타입은 앞서 람다와 관련 함수형 인터페이스에서 배웠던 BinaryOperator<T> 로 정의되어 있습니다.
Optional<T> reduce(BinaryOperator<T> accumulator)
참고로, reduce() 메서드는 최대 3개까지 매개변수를 받을 수 있는데, 지금은 2개를 받는 경우까지만 알고 넘어가도 충분합니다.
T reduce(T identity, BinaryOperator<T> accumulator)
위에서 첫 번째 매개변수 identity는 특정 연산을 시작할 때 설정되는 초기값을 의미합니다. 두 번째 accumulator는 각 요소를 연산하여 나온 누적된 결과값을 생성하는 데 사용하는 조건식입니다.
아래 예제를 통해서 좀 더 알아보겠습니다.
- reduce()
import java.util.Arrays;
public class TerminalOperationExample {
public static void main(String[] args) throws Exception {
int[] intArray = {1,2,3,4,5};
// sum()
long sum = Arrays.stream(intArray).sum();
System.out.println("intArray 전체 요소 합: " + sum);
// 초기값이 없는 reduce()
int sum1 = Arrays.stream(intArray)
.map(element -> element * 2)
.reduce((a , b) -> a + b)
.getAsInt();
System.out.println("초기값이 없는 reduce(): " + sum1);
// 초기값이 있는 reduce()
int sum2= Arrays.stream(intArray)
.map(element -> element * 2)
.reduce(5, (a ,b) -> a + b);
System.out.println("초기값이 있는 reduce(): " + sum2);
}
}
// 출력값
intArray 전체 요소 합: 15
초기값이 없는 reduce(): 30
초기값이 있는 reduce(): 35
첫 번째 예제는 우리가 익히 알고 있는 sum() 메서드를 활용하여 숫자 요소들의 총합을 도출하는 스트림 작업입니다. 1+2+3+4+5의 결과로 숫자 15 가 나왔습니다.
두 번째와 세 번째는 모두 reduce() 메서드를 사용하고 있지만, 세 번째의 경우에는 초기값으로 5 가 설정되어 있기 때문에 최종적인 연산의 결과가 두 번째 예제보다 5 가 더 많은 20 이 출력되었습니다.
위 배열의 값을 사용하여 조금 더 구체적인 흐름을 살펴보면 다음과 같습니다.
- accumulator: (a, b) -> a + b (a: 누적된 값, b: 새롭게 더해질 값)
- 최초 연산 시 1+2 → a: 3, b: 3
- 3+3 → a: 6, b: 4
- 6+4 → a: 10, b: 5
- 10+5 → 최종 결과:15
참고로, 앞서 배웠던 count()와 sum()과 같은 집계 메서드 또한 내부적으로 모두 reduce()를 사용하여 연산을 수행합니다.
요소 수집(collect())
스트림은 중간 연산을 통한 요소들의 데이터 가공 후 요소들을 수집하는 최종 처리 메서드인 collect()를 지원합니다. 좀 더 구체적으로, 스트림의 요소들을 List, Set, Map 등 다른 타입의 결과로 수집하고 싶은 경우에 collect() 메서드를 유용하게 사용할 수 있습니다.
collect() 메서드는 Collector 인터페이스 타입의 인자를 받아서 처리할 수 있는데, 직접 구현하거나 미리 제공된 것들을 사용할 수 있습니다. 참고로, 빈번하게 사용되는 기능들은 Collectors 클래스에서 제공하고 있습니다.

위의 그림은 Collectors 클래스에 정의된 기능 중에 스트림의 요소들을 List, Set, Map 등 다른 타입의 결과로 수집할 수 있는 정적 메서드들을 보여주고 있습니다.
사실 collect() 메서드는 단순히 요소를 수집하는 기능 이외에도 요소 그룹핑 및 분할 등 다른 기능들을 제공합니다. 여기서는 따로 세부적인 내용들을 다루지는 않겠습니다.
그럼, 이제 간단한 예제를 살펴보겠습니다.
- collect()
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class TerminalOperationExample {
public static void main(String[] args) {
// Student 객체로 구성된 배열 리스트 생성
List<Student> totalList = Arrays.asList(
new Student("김코딩", 100, Student.Gender.Male),
new Student("박해커", 80, Student.Gender.Male),
new Student("이자바", 90, Student.Gender.Female),
new Student("나미녀", 60, Student.Gender.Female)
);
// 스트림 연산 결과를 Map으로 반환
Map<String, Integer> maleMap = totalList.stream()
.filter(s -> s.getGender() == Student.Gender.Male)
.collect(Collectors.toMap(
student -> student.getName(), // Key
student -> student.getScore() // Value
));
// 출력
System.out.println(maleMap);
}
}
class Student {
public enum Gender {Male, Female};
private String name;
private int score;
private Gender gender;
public Student(String name, int score, Gender gender) {
this.name = name;
this.score = score;
this.gender = gender;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
public Gender getGender() {
return gender;
}
}
// 출력값
{김코딩=100, 박해커=80}
이제 위의 예제 코드를 순차적으로 한번 살펴보겠습니다. 먼저 4개의 Student 타입의 객체로 이뤄진 리스트 배열을 만들었습니다. 보시는 것처럼 “김코딩”과 “김인기”라는 이름의 남성 2명과 “이자바”와 “나미녀”라는 이름의 여성 2명이 있습니다.
이렇게 리스트 배열로 이뤄진 데이터 요소들에서 남학생들의 이름과 점수만을 추출하여 이름과 점수를 각각 키(key)와 값(value)으로 하는 Map 타입의 결과를 수집하고자 할 때 collect() 메서드가 위의 코드처럼 유용하게 사용될 수 있습니다.
흐름을 간략하게 설명해 보면, 먼저 리스트 배열에 스트링을 생성하고, 중간 연산자 filter() 메서드를 통해 성별이 남자인 학생들만 필터링한 후, 마지막으로 최종 연산자 collect()에 Collectors 클래스 안에 정의된 정적 메서드(toMap())를 사용하면 아래 출력값과 같이 우리가 의도했던 대로 Map 타입의 결과물을 받아볼 수 있습니다.
기타
앞에서 언급한 최종 연산자들 외에도 findAny() , findFirst() , toArray() 등의 최종 연산자가 있지만, 따로 여기서 다루지는 않겠습니다. 이제 스트림과 관련해서 필요한 모든 핵심적인 개념과 지식은 모두 전달드렸습니다. 직접 활용을 충분히 해보시고, 추가적인 이해가 필요한 부분들은 스스로 검색을 통해 찾아가면서 이해를 넓혀가시기를 권장해 드립니다.
Optional Class
Optional<T>은 NullPointerException(NPE), 즉 null 값으로 인해 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입되었습니다.
연산 결과를 Optional에 담아서 반환하면, 따로 조건문을 작성해주지 않아도 NPE가 발생하지 않도록 코드를 작성할 수 있습니다.
Optional<T>
Optional 클래스는 모든 타입의 객체를 담을 수 있는 래퍼(Wrapper) 클래스입니다.
public final class Optional<T> {
private final T value; // T타입의 참조변수
}
Optional 객체를 생성하려면 of() 또는 ofNullable()을 사용합니다. 참조변수의 값이 null일 가능성이 있다면, ofNullable()을 사용합니다.
Optional<String> opt1 = Optional.ofNullable(null);
Optional<String> opt2 = Optional.ofNullable("123");
System.out.println(opt1.isPresent()); //Optional 객체의 값이 null인지 아닌지를 리턴합니다.
System.out.println(opt2.isPresent());
Optional<T> 타입의 참조변수를 기본값으로 초기화하려면 empty() 메서드를 사용합니다.
Optional<String> opt3 = Optional.<String>empty();
Optional 객체에 객체에 저장된 값을 가져오려면 get()을 사용합니다.
참조변수의 값이 null일 가능성이 있다면 orElse()메서드를 사용해 디폴트 값을 지정할 수 있습니다.
Optional<String> optString = Optional.of("java");
System.out.println(optString);
System.out.println(optString.get());
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("kimcoding");
System.out.println(name);
Optional 객체는 스트림과 유사하게 여러 메서드를 연결해서 작성할 수 있습니다(이를 메서드 체이닝이라고 합니다).
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
List<String> languages = Arrays.asList(
"Ruby", "Python", "Java", "Go", "Kotlin");
Optional<List<String>> listOptional = Optional.of(languages);
int size = listOptional
.map(List::size)
.orElse(0);
System.out.println(size);
}
}
Optional 객체에서 제공하는 전체 메서드는 공식문서를 참고하세요.
심화 학습 (Optional)
위 내용을 모두 실습하고 시간이 남는다면, 다음 예제를 다양하게 변형해보며 Optional 객체 활용법을 탐구해보세요.
//CarClub.java
package com.jungmin.optional;
public class CarClub {
public static void main(String[] args) {
MemberService memberService = new MemberService();
String insuranceName =
memberService
.getMember("jungmin")
.flatMap(Member::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getCompanyName)
.orElse("Not result");
}
}
//MemberService.java
package com.jungmin.optional;
import java.util.Optional;
public class MemberService {
public Optional<Member> getMember(String id) {
Optional<Insurance> insurance = Optional.of(new Insurance("Samsung direct"));
Optional<Car> car = Optional.of(new Car("XC-60", 60_000_000, insurance));
return Optional.of(new Member("Jungmin", "jungmin0102", car));
}
}
//Member.java
package com.jungmin.optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Optional;
@Getter
@AllArgsConstructor
public class Member {
String name;
String id;
Optional<Car> car;
}
//Insurance.java
package com.jungmin.optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class Insurance {
String companyName;
}