1. 학습 주제
자바에서 자주 사용하는 기본 API와 제네릭의 핵심 개념을 정리했다.
특히 Object, String, StringBuilder, Wrapper, java.time의 역할을 확인했고, Object 기반 코드의 한계를 제네릭이 어떻게 해결하는지도 함께 이해할 수 있었다.
- Object의 toString(), equals(), hashCode() 이해하기
- String의 주요 메서드와 불변성 확인하기
- StringBuilder가 필요한 이유 익히기
- Wrapper 클래스와 파싱, 문자열 변환 정리하기
- 오토박싱과 오토언박싱 자세히 이해하기
- 제네릭의 필요성과 타입 안정성 이해하기
- 제한된 제네릭과 와일드카드 문법 정리하기
2. Object 클래스
모든 클래스는 Object를 상속받기 때문에, toString(), equals(), hashCode()는 매우 중요하다.
2.1 toString()
System.out.println("book1 =" + book1);
- 기본 toString()은 클래스명@해시코드 형태를 출력한다.
- 사람이 보기 좋은 객체 정보 출력을 위해 오버라이딩해서 사용한다.
@Override
public String toString() {
return "Book{" +
"number=" + number +
", title='" + title + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
2.2 equals()
System.out.println(book2 == book3);
System.out.println(book2.equals(book3));
- ==는 주소값 비교, 즉 동일성 비교이다.
- equals()는 내용 비교, 즉 동등성 비교를 위해 오버라이딩한다.
2.3 hashCode()
Set<Book> books = new HashSet<>();
books.add(book2);
System.out.println(books.contains(book3));
- HashSet, HashMap 같은 해시 기반 컬렉션은 hashCode()와 equals()를 함께 사용한다.
- equals()가 true라면 hashCode()도 같아야 한다.
- 그래서 equals()를 오버라이딩했다면 보통 hashCode()도 같이 오버라이딩해야 한다.
3. String
String은 자바에서 가장 많이 쓰는 클래스 중 하나이며, 중요한 특징은 불변성이다.
String rawData = " [PROD-001] Apple iphone 17 Pro | a17 bionic | 2500000 ";
예제에서 사용한 주요 메서드:
- trim() : 앞뒤 공백 제거
- toUpperCase() : 대문자 변환
- indexOf() : 특정 문자 위치 찾기
- substring() : 문자열 일부 자르기
- replace() : 특정 문자열 치환
- split() : 구분자를 기준으로 나누기
3.1 String 비교
String str1 = "java";
String str2 = "java";
String str3 = new String("java");
String str4 = new String("java");
- 리터럴은 문자열 상수 풀을 재사용할 수 있다.
- new String()은 새 객체를 만든다.
- 문자열 값 비교는 항상 .equals()를 사용하는 것이 안전하다.
3.2 String의 불변성
str2 += " mysql";
- 기존 문자열이 수정되는 것이 아니다.
- 새로운 String 객체가 만들어진다.
- 그래서 반복문 안에서 문자열을 계속 더하면 비효율적일 수 있다.
4. StringBuilder
문자열을 반복적으로 수정하거나 이어붙일 때는 StringBuilder가 더 효율적이다.
StringBuilder sb = new StringBuilder();
for(int i=0; i<50000; i++) {
sb.append(i);
}
- String은 불변 객체라 수정할 때마다 새 객체가 생긴다.
- StringBuilder는 같은 객체 내부를 바꾸면서 작업한다.
- 반복문 안에서 문자열 누적이 많으면 StringBuilder가 훨씬 유리하다.
주요 메서드:
- append() : 뒤에 붙이기
- delete() : 일부 삭제
- insert() : 특정 위치에 삽입
- reverse() : 순서 뒤집기
- toString() : 최종적으로 String으로 변환
5. Wrapper 클래스
기본 자료형은 객체가 아니기 때문에, 객체처럼 다뤄야 할 때 Wrapper 클래스를 사용한다.
예:
- int -> Integer
- double -> Double
- char -> Character
- boolean -> Boolean
예제에서는 문자열 숫자를 실제 숫자로 바꾸는 파싱을 사용했다.
double height = Double.parseDouble(heightStr);
double weight = Double.parseDouble(weightStr);
- 문자열 "170.5"는 그대로는 계산할 수 없다.
- parseDouble() 같은 메서드로 기본형 숫자로 바꿔야 한다.
반대로 숫자를 문자열로 바꾸는 것도 가능하다.
String bmiStr1 = String.valueOf(bmi);
String bmiStr2 = bmi + "";
6. 오토박싱과 오토언박싱
이 부분은 정말 중요하다.
6.1 박싱이란?
기본 자료형 값을 Wrapper 객체로 감싸는 것이다.
Integer age = Integer.valueOf(20);
원래는 이렇게 써야 하는데, 자바가 자동으로 바꿔준다.
Integer age = 20;
이것이 오토박싱이다.
- int 20을 Integer 객체로 자동 변환
- 개발자가 valueOf()를 직접 쓰지 않아도 됨
6.2 언박싱이란?
Wrapper 객체 안의 값을 다시 기본 자료형으로 꺼내는 것이다.
int unboxedAge = age.intValue();
이것도 자바가 자동으로 처리해준다.
int unboxedAge = age;
이것이 오토언박싱이다.
- Integer 객체 안의 값을 꺼내서 int로 바꿈
- 개발자가 intValue()를 직접 쓰지 않아도 됨
6.3 왜 필요한가?
제네릭, 컬렉션, API 대부분은 객체 타입을 다룬다.
그런데 int, double 같은 기본형은 객체가 아니다.
예를 들어:
- List<int> 는 안 됨
- List<Integer> 는 가능
즉 객체가 필요한 상황에서 기본형을 편하게 쓸 수 있도록 박싱/언박싱이 지원된다.
6.4 주의할 점
Integer num3 = 128;
Integer num4 = 128;
System.out.println(num3 == num4);
System.out.println(num3.equals(num4));
- Wrapper 클래스는 객체이므로 ==는 주소 비교가 될 수 있다.
- 값 비교는 .equals()를 쓰는 것이 안전하다.
한 줄 정리:
- 오토박싱: 기본형 -> Wrapper 객체
- 오토언박싱: Wrapper 객체 -> 기본형
7. java.time API
기존 Date보다 더 명확하고 안전한 날짜/시간 API이다.
예제에서 사용한 클래스:
- LocalDate : 날짜만
- LocalTime : 시간만
- LocalDateTime : 날짜와 시간 모두
LocalDate departureDate = LocalDate.of(2026, 12, 25);
LocalTime departureTime = LocalTime.of(13, 8);
LocalDateTime trainDeparture = LocalDateTime.of(departureDate, departureTime);
할 수 있는 작업:
- 연, 월, 일, 요일 꺼내기
- minusMinutes() 같은 날짜/시간 계산
- isBefore()로 비교
- DateTimeFormatter로 출력 형식 지정
- 문자열을 날짜 객체로 파싱
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String ticket = trainDeparture.format(formatter);
8. 제네릭이 왜 필요한가
제네릭을 배우기 전에 먼저 Object 기반 코드의 문제를 보는 게 중요하다.
8.1 일반 박스의 문제점
public class NormalBox {
private Object content;
}
Object는 모든 객체를 담을 수 있다.
그래서 얼핏 보면 유연해 보인다.
normalBox.setContent("안녕하세요");
normalBox.setContent(123);
하지만 문제가 생긴다.
String content = (String) normalBox.getContent();
- 꺼낼 때마다 강제 형변환이 필요하다
- 실수로 다른 타입을 넣어도 컴파일 에러가 안 난다
- 잘못 꺼내면 런타임 에러가 발생한다
즉 Object 기반 방식은 유연하지만 안전하지 않다.
9. 제네릭의 핵심 개념
제네릭은 “타입을 변수처럼 다루는 문법”이다.
public class GenericBox<T> {
private T content;
}
여기서 T는 타입 파라미터이다.
- T는 실제 타입이 아니라 타입 자리 표시자
- 사용할 때 진짜 타입을 넣는다
예:
GenericBox<String> stringBox = new GenericBox<>();
GenericBox<Integer> integerBox = new GenericBox<>();
이렇게 하면:
- stringBox는 문자열만 담는 박스
- integerBox는 정수 객체만 담는 박스
9.1 장점
- 강제 형변환이 줄어든다
- 타입 실수를 컴파일 단계에서 잡을 수 있다
- 코드 재사용성이 높아진다
String strContent = stringBox.getContent();
- 꺼낼 때 형변환이 필요 없다
- String이 보장되기 때문이다
9.2 왜 더 안전한가
NormalBox는 Object라서 아무거나 들어간다.
GenericBox<String>은 아예 문자열만 받게 제한된다.
즉 제네릭은:
- “아무거나 넣는 유연함” 대신
- “정해진 타입만 넣는 안전함”을 준다
10. 제네릭과 기본형
제네릭에는 기본형을 직접 넣을 수 없다.
// GenericBox<int> // 불가능
GenericBox<Integer> integerBox = new GenericBox<>();
왜냐하면 제네릭은 객체 타입을 기준으로 동작하기 때문이다.
그래서 기본형 대신 Wrapper 클래스를 사용한다.
- int 대신 Integer
- double 대신 Double
그리고 값을 꺼낼 때는 오토언박싱 덕분에 자연스럽게 기본형처럼 쓸 수 있다.
int intContent = integerBox.getContent();
겉보기에는 Integer를 int에 넣는 것 같지만, 내부적으로 오토언박싱이 일어난다.
11. 제한된 제네릭
제네릭 타입에 아무 타입이나 넣지 못하게 제한할 수도 있다.
public class RabbitFarm<T extends Rabbit> {
private T animal;
}
이 뜻은:
- T는 Rabbit 또는 Rabbit의 자식만 가능
- Snake 같은 전혀 다른 타입은 안 됨
가능한 예:
RabbitFarm<Rabbit> farm1 = new RabbitFarm<>(new Rabbit());
RabbitFarm<Bunny> farm2 = new RabbitFarm<>(new Bunny());
RabbitFarm<DrunkenBunny> farm3 = new RabbitFarm<>(new DrunkenBunny());
불가능한 예:
// RabbitFarm<Snake> rabbitFarm = new RabbitFarm<>();
이런 제한을 두는 이유는 “토끼 농장”에는 토끼 계열만 들어오게 보장하기 위해서다.
12. 와일드카드
와일드카드는 제네릭 타입을 받을 때 범위를 유연하게 표현하는 문법이다.
12.1 <?>
public void manageAnyFarm(RabbitFarm<?> farm)
- 어떤 타입이든 가능
- 타입을 정확히 모르지만 받기는 하겠다는 의미
12.2 <? extends Bunny>
public void manageBunnyFarm(RabbitFarm<? extends Bunny> farm)
- Bunny 또는 Bunny의 자식만 가능
- Bunny, DrunkenBunny 가능
- Rabbit은 불가능
의미:
- “Bunny 계열까지만 받겠다”
12.3 <? super Bunny>
public void manageRabbitOrBunnyFarm(RabbitFarm<? super Bunny> farm)
- Bunny 또는 Bunny의 부모 타입 가능
- Bunny, Rabbit 가능
의미:
- “Bunny를 담을 수 있는 더 넓은 범위까지 받겠다”
13. 제네릭을 이해하는 핵심 감각
제네릭은 문법보다 목적을 먼저 이해하면 편하다.
13.1 제네릭이 없으면
- Object로 다 담는다
- 꺼낼 때 형변환한다
- 실수해도 컴파일러가 잘 못 잡는다
13.2 제네릭이 있으면
- 애초에 넣을 수 있는 타입을 정한다
- 꺼낼 때 형변환이 줄어든다
- 잘못된 타입 사용을 컴파일 단계에서 막는다
즉 제네릭의 본질은:
- 코드 재사용
- 타입 안정성
- 형변환 감소
14. 흐름 정리
14.1 API
- Object의 toString(), equals(), hashCode() 정리
- String의 주요 메서드와 불변성 이해
- 반복 문자열 처리에는 StringBuilder 사용
- Wrapper 클래스로 파싱, 문자열 변환, 기본형-객체 변환 처리
- java.time으로 날짜/시간 안전하게 다루기
14.2 제네릭
- Object 기반 클래스의 한계 확인
- GenericBox<T>로 타입 안전한 구조 만들기
- Wrapper 타입과 오토언박싱 연결 이해
- extends, ?, ? extends, ? super로 범위 제한하기
15. 핵심 정리
- Object의 equals()와 hashCode()는 함께 고려해야 한다.
- String은 불변 객체라 반복 수정에 비효율적일 수 있다.
- 문자열을 많이 붙일 때는 StringBuilder가 유리하다.
- Wrapper 클래스는 기본형을 객체처럼 다룰 수 있게 해준다.
- 오토박싱은 기본형을 Wrapper 객체로 자동 변환하는 것이다.
- 오토언박싱은 Wrapper 객체를 기본형으로 자동 변환하는 것이다.
- Wrapper 객체 비교는 ==보다 .equals()가 안전하다.
- 제네릭은 타입을 일반화하면서도 타입 안전성을 지켜주는 문법이다.
- 제네릭이 없으면 Object와 형변환에 의존하게 되어 런타임 오류 위험이 커진다.
- T extends Rabbit은 타입 파라미터를 특정 상속 범위로 제한하는 문법이다.
- ? extends는 상한 제한, ? super는 하한 제한이라고 이해하면 정리하기 쉽다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]Exception, Checked/Unchecked, 예외 계층 구조, 다형성, IO 스트림 (0) | 2026.05.12 |
|---|---|
| [TIL]Java Collection Framework, List / Set / Map 핵심 정리 (0) | 2026.05.11 |
| [TIL]객체 배열 활용, 상속, 오버라이딩, 다형성, 동적 바인딩, 추상클래스와 인터페이스 (0) | 2026.05.07 |
| [TIL]static, final, singleton, 변수의 종류, 초기화 블록, 객체 배열 (0) | 2026.05.06 |
| [TIL]추상화, DTO, 생성자, 메서드 매개변수와 오버로딩 (0) | 2026.05.04 |