일단 씻고 나가자
Integer.toString() / String.valueOf() , Stream map() / mapToObj() 본문
Integer.toString() / String.valueOf() , Stream map() / mapToObj()
일단 씻고 나가자 2025. 3. 25. 16:11사건 발생
코딩 테스트를 풀다가 int 배열을 String 배열로 바꿀 때 에러가 났다.

IDE로 코드를 옮겨보니 mapToObj 에서 에러가 나더라.
보니까 아예 boxed() 이후에는 mapToObj가 아니라 map이 들어가야 했다.
그래서 문득 mapToObj, map 두 메서드가 있는 걸 생각해냈고, 다음과 같이 고쳐봤다.

도무지 이해가 가지 않는 에러. 그래 나랑 한번 해보자 하고 이렇게도 고쳐봤다.

컴파일 에러는 무조건 나의 무지 탓이니라.
찾아 보니 Integer::toString / String::valueOf 및 map / mapToObj 의 차이를
알지도 못하고 사용하고 있었다.
+ 이거 이해하는 데 개 열받았다.
Integer.toString() / String.valueOf()
둘 다 정수형을 문자형으로 파싱할 때 사용된다.
(valueOf 는 정수 말고도 다른 데이터 형도 가능)
먼저 Integer.toString() 이다.

내용은 확인할 필요 없고,
메서드 시그니쳐를 확인해보면 int 를 매개 변수로 받고 있다.
다음은 String.valueOf() 이다.

내부적으로 Integer의 toString()을 활용하고 있다.
따라서 두 메서드는 다른 것이 아닌, 같은 성능과 결과를 내는 것이라고 보면 된다.
어 그러면 valueOf 를 사용하면 toString 을 또 호출하니까 메모리나 성능에 손해가 있나?
JVM의 JIT(Just In Time) 컴파일러가 '인라이닝'으로 최적화 시점에서 이를 방지해준다.
인라이닝이란 메서드 호출을 삭제하고 메서드 내용을 바로 삽입하는 과정이다.
![]() | ![]() |
예를 들어 이렇다.
보통 함수를 호출하면 JVM은 스택 프레임을 생성하고, 함수 종료 시 리턴하는데,
함수를 코드에 직접 박아 최적화하는 기법이다.
모든 함수가 인라이닝 되진 않으며,
다음과 같은 조건 하의 메서드가 주로 인라이닝 된다.
|
Stream map() / mapToObj()
둘 다 특정 형태의 변수를 다른 형태의 변수로 바꿔주나,
하나하나씩 차근차근 살펴볼 필요가 있다.
먼저 직접 두 메서드를 비교하기 전에,
Stream이 제공하는 자료구조 형태를 살펴보아야 한다.
int(Integer)를 기준으로, Stream은 IntStream / Stream<Integer> 두 가지 형태를 제공하는데,
Stream<?> 형태는 어떤 객체가 오더라도 stream의 자료구조 안에 넣는 형태이고,
IntStream은 premitive 타입을 자료구조로 다루는 형태이다. (DoubleStream 등이 따로 있음)
왜 이렇게 나눠놨냐? 당연히 성능 때문이다.
int, long, double 등 premitive 타입은 객체 연산을 위해 wrapper를 제공하지만,
boxing 과정에서 불필요한 자원이 들게 된다.
또한 premitive 타입이 보장되기 때문에
sum() / average() / max() / min() 등의 숫자 연산 메서드를 제공해주고,
객체 변환을 위해 boxed() / mapToObj() 메서드도 제공해준다.
map()
map() 부터 살펴보자.
map() 은 Stream<객체> , PremitiveStream 둘 모두에서 사용할 수 있는 메서드이다.
먼저 Stream은
Arrays.stream(어떤 배열) 같은 형태로 특정 객체 컬렉션을 stream으로 변환할 때,
해당 배열의 타입에 따라 알아서 반환 값을 정해준다.
Stream<객체> | PremitiveStream |
![]() | ![]() |
왼쪽 그림과 같이 어떤 객체를 stream으로 반환하면 Stream<객체> 형태로 반환하고,
오른쪽 그림과 같이 premitive type을 stream으로 반환하면 PremitiveStream 형태로 반환한다.
* 이때 IntStream의 형태를 .boxed() 로 boxing 하게 되면
PremitiveStream -> Stream<Integer> 으로 형태가 바뀌게 되는데,
이때 {1, 2, 3} 의 기본형 배열이 {Integer(1), Integer(2), Integer(3)} 처럼 객체 형태로 개별 원소가 바뀌게 된다.
자, 그러니까 Stream 이 두 가지 형태를 가졌다는 건 알았고,
이젠 두 형태의 map() 이 다른 반환값을 가진다는 걸 알아야 한다.
Stream<객체> | PremitiveStream |
![]() | ![]() |
이와 같이 어떤 Stream의 형태였냐에 따라 같은 map() 메서드라도 다른 반환값을 가지는 것을 볼 수 있으며,
따라서 PremitiveStream 에서 map(다른 객체로 변환) 의 메서드는 사용할 수 없고
오로지 IntStream 만을 반환하는 로직만 사용할 수 있다는 것을 알 수 있다.

우리의 목표는 int(Integer) 를 String 으로 바꾸는 것.
앞의 과정을 통해 IntStream.map() 은 못 쓴다는 걸 알았고,
그럼 .boxed() 를 통해 IntStream 을 Stream<Integer> 으로 바꾸어 사용하면 어떨까?
Stream<객체> 형태로 바뀌기 때문에 이 방법은 논리적으로 오류가 없다!!!!!!!

하지만 오류는 있다.
껄껄 쉽게 될 줄 알았니?
근데 사람 미치게 만드는 게 또 이거는 된다.


Integer::toString 만 안 되는 이유는 뭘까??
그것은 Integer 는 내부적으로 2 개의 toString() 을 가지고 있기 때문이다.

Stream 에서의 람다 추론은 기본적으로 다음 두 가지를 할 수 있다.
- 매개 변수가 0개 혹은 1개일 때 알아서 어떻게 활용할지 판단한다.
- static 메서드를 실행할 것인지, 인스턴스 메서드를 실행할 것인지 판단한다.
그러니까 클래스::메서드 형태에서, 메서드() / 메서드(stream 원소) 두 가지를 알아서 판단해준다는 뜻이다.
당연히 뭔 소린지 와닿지 않는다. 예를 들어보자.
![]() | ![]() |
![]() |
메서드가 인스턴스/static 이냐, 매개변수가 있냐/없냐 가 달라졌음에도
같은 코드로 Stream.map() 메서드 호출은 정상 실행된다.
그러니까 Stream 이 [클래스::메서드] 형태로 해당 클래스의 메서드를 실행하려고 할 때,
- 번째 case) Stream 이 순회하는 개별 any 객체가 forString() 을 실행하도록 해야겠다.
- 번째 case) Stream 이 순회하는 개별 any 객체를 Any.forString() static 메서드의 매개변수에 넣어야겠다.
이 두 가지 경우를 알아서 추론하여 실행해준다는 것이다.
다시 Integer::toString 으로 돌아가면 어떨까?
IntStream 의 경우, 순회하는 원소들도 int(Integer), toString(int i) 의 매개변수도 int 이다.
그럼 컴파일러는 혼란에 빠진다.
- 번째 case) Stream 이 순회하는 개별 Integer 의 객체.toString() 을 실행 해야하나?
- 번째 case) Stream 이 순회하는 개별 int 를 Integer.toString(int i) 메서드의 매개변수에 넣어야하나?
따라서 IntStream.boxed().map(Integer::toString) 은 컴파일 에러를 낼 수밖에 없고,
이는 boxed() 를 통해 개별 int 가 객체로 진화했기 때문에 벌어진 일이다.
mapToObj()
mapToObj() 는 어떨까?
mapToObj() 는 심플하게, PremitiveStream 에서만 사용 가능하고, Stream<객체> 에 사용할 수 없다.
왜인지 추론하자면 Premitive 타입 자료형을 객체로 만들어주기 위해 map 대신 다른 메서드를 구현한 것 같고,
따라서 mapToObj 는 객체로 매핑한다는 메서드 명처럼 이미 Object 인 Stream 에는 사용하지 못하는 것이다.
Premitive 타입에서만 사용 가능하니까 IntStream 이지 Stream<Integer> 가 아니고,
즉, Stream 내부에선 Integer 가 아닌 int 원소들이 돌아다닐 것이므로,
얘네는 객체가 아니니까 위에서의 map() 의 경우처럼
멤버 메서드인지 static 메서드인지 헷갈릴 필요 없이 무조건 static toString(int i) 가 선택될 것이다.
따라서 mapToObj() 의 매개 변수로, String::valueOf 는 물론, Integer::toString 또한 사용할 수 있다 ^-^
+ 추가로, boxed() 역시 PrimitiveStream 에서만 사용 가능하다.
mapToObj() 의 의미와 비슷하게, primitive 를 wrapper 로 바꿔주는 역할을 위해 존재하는 메서드 같다.
- IntStream.boxed() -> int 를 Integer 으로
- IntStream.mapToObj() -> int 를 다른 객체로
이렇기 때문에 boxed() 를 한 순간 자연스럽게 int 가 아닌 일반 객체와 같은 취급을 받게 되고,
따라서 boxed() 으로 객체 변환을 했는데 굳이 mapToObj() 로 객체 변환을 할 필요가 없어 사용할 수 없다.
반대도 마찬가지, mapToObj() 로 이미 객체인 원소를 boxed() 로 객체로 바꿔줄 이유가 없기 때문에,
boxed().mapToObj() / mapToObj().boxed() 둘 다 불가능한 것이다.
정리
아리송한 여러분을 위해,
그리고 금붕어 머리통의 미래의 나를 위해 총정리 가겠다.
Stream 의 자료구조는 두 가지
- Stream<?>
- PremitiveStream
Stream 에서 람다 추론은 두 가지
- 매개 변수가 없는, 개별 원소의 인스턴스 메서드 실행
- 매개 변수가 하나, 개별 원소를 static 메서드의 매개변수에 넣어 실행
각 구조마다 쓸 수 있는 메서드와 의미는 다음과 같음
\ | map() | mapToObj() | boxed() |
같은 형태의 Stream 반환 | Object 형태의 Stream 반환 | Wrapper 형태의 Stream 반환 | |
PremitiveStream | PremitiveStream -> PremitiveStream | PremitiveStream -> Stream<Object> | PremitiveStream -> Stream<Wrapper> |
Stream<Object> | Stream<Object> -> Stream<Object> | X | X |
- Integer::toString 은 Stream<Wrapper> 에서 Integer -> String 변환 시에만 toString() 때문에 문제가 생김
마치며
멍청해서 하루종일 검색해야 이해할 수 있었다.
(애꿎은 GPT 에게 느낌표를 남발하며 공격해서 미안하다..)
Stream 과 람다의 개념도 부족했는데, toString 의 에러 때문에 더 화가 나버린 것.
근데 덕분에 항상 공부해야지.. 공부해야지.. 했던 Stream 과 람다의 추론 방식에 대해 알게 된 것 같아
아주 아주 기분이 좋다.
앞으로 관련 개념을 봐도 속으로 우쭐댈 수 있을 것.
나를 죽이지 못하는 고통은 나를 강하게 만들 것이다.. (육군임)
'Language > Java' 카테고리의 다른 글
업 캐스팅 / 다운 캐스팅 (UpCasting / DownCasting) (0) | 2024.07.30 |
---|