Java 객체지향 심화 - 상속, 다형성, 인터페이스, 제너릭
📚 오늘 배운 내용 요약
| 주제 | 핵심 내용 |
|---|
| 상속(Inheritance) | 기존 클래스의 기능을 물려받아 확장 |
| 오버라이딩(Override) | 부모 메서드를 재정의 |
| 동적 바인딩 | 실제 객체 기준으로 메서드 호출 |
| 업캐스팅 | 자식 객체를 부모 타입으로 참조 |
| 다운캐스팅 | 부모 타입을 자식 타입으로 변환 |
| 추상 클래스 | 공통 기능 정의 및 구현 강제 |
| 인터페이스 | 다중 구현을 위한 계약 |
| Wrapper Class | 기본형을 객체 형태로 사용 |
| Generic | 타입을 일반화하여 재사용성 향상 |
1) 상속과 오버라이딩
| 학습 내용 | 설명 |
|---|
| extends | 부모 클래스를 상속 |
| super() | 부모 생성자 호출 |
| 오버라이딩 | 부모 메서드를 재정의 |
| toString() | 객체 출력 형식 변경 |
| 동적 바인딩 | 실제 객체 기준 메서드 실행 |
| 정적 바인딩 | 변수 타입 기준 필드 접근 |
public class Example01 {
public static void main(String[] args) {
Developer dev = new Developer("Tom", "Java");
BackendDeveloper backend =
new BackendDeveloper("Jane", "Spring");
dev.work();
backend.work();
}
}
class BackendDeveloper extends Developer {
String version = "2.0";
BackendDeveloper(String name, String skill) {
super(name, skill);
}
@Override
void work() {
System.out.println(name + "이 백엔드 개발을 수행합니다.");
}
}
class Developer {
String version = "1.0";
String name;
String skill;
Developer(String name, String skill) {
this.name = name;
this.skill = skill;
}
void work() {
System.out.println(name + "이 개발을 수행합니다.");
}
}
- BackendDeveloper가 Developer 상속
- 부모 생성자는 super()로 호출
- work() 메서드를 재정의하여 기능 확장
- 같은 메서드 호출이지만 실제 객체 타입에 따라 다르게 실행
| 느낀 점 | 내용 |
|---|
| 상속은 확장 | 기존 기능을 재사용하면서 기능 추가 가능 |
| 오버라이딩 중요 | 다형성의 핵심 |
| 동적 바인딩 이해 | 변수 타입보다 실제 객체 타입이 중요 |
2) 접근 범위와 상속
| 학습 내용 | 설명 |
|---|
| protected | 상속 관계에서 접근 가능 |
| private | 해당 클래스 내부만 접근 가능 |
| super | 부모 메서드 호출 |
| 오버라이딩 제한 | 접근 범위를 더 좁게 변경 불가 |
class Engineer {
protected String secret = "SECRET";
void work() {
System.out.println("작업 수행");
}
}
class SoftwareEngineer extends Engineer {
@Override
public void work() {
super.work();
System.out.println("추가 작업 수행");
}
}
- protected 필드는 자식 클래스 접근 가능
- super를 통해 부모 기능 재사용
- 오버라이딩 시 접근 범위를 줄일 수 없음
| 느낀 점 | 내용 |
|---|
| protected 활용 중요 | 상속 구조에서 자주 사용 |
| 부모 기능 재사용 가능 | super 활용 |
| 접근제어 설계 필요 | 무조건 public 사용 지양 |
3) 업캐스팅과 다운캐스팅
| 학습 내용 | 설명 |
|---|
| 업캐스팅 | 자식 객체 → 부모 타입 |
| 다운캐스팅 | 부모 타입 → 자식 타입 |
| instanceof | 타입 확인 |
| 패턴 매칭 | 다운캐스팅 간소화 |
Animal animal = new Dog();
animal.eat();
if(animal instanceof Dog dog){
dog.eat();
}
- 업캐스팅은 자동 수행
- 다운캐스팅은 명시적으로 수행
- instanceof로 안전성 확보
| 느낀 점 | 내용 |
|---|
| 업캐스팅 활용도 높음 | 다형성 구현 가능 |
| 다운캐스팅 위험성 존재 | 타입 확인 필수 |
| instanceof 중요 | 런타임 오류 예방 |
4) 추상 클래스
| 학습 내용 | 설명 |
|---|
| abstract class | 객체 생성 불가 |
| 추상 메서드 | 구현 강제 |
| 공통 기능 정의 | 코드 중복 감소 |
abstract class Fruit {
abstract int getPrice();
void info() {
System.out.println("과일 정보");
}
}
class Apple extends Fruit {
@Override
int getPrice() {
return 1000;
}
}
- Fruit는 직접 생성 불가
- 하위 클래스가 반드시 구현
- 공통 기능은 부모에서 제공
| 느낀 점 | 내용 |
|---|
| 설계 강제 가능 | 구현 규칙 제공 |
| 코드 중복 감소 | 공통 기능 관리 |
| 다형성과 궁합 좋음 | 부모 타입 활용 가능 |
5) 인터페이스
| 학습 내용 | 설명 |
|---|
| interface | 구현 계약 |
| implements | 인터페이스 구현 |
| 다중 구현 | 여러 인터페이스 구현 가능 |
| 다형성 | 인터페이스 타입 활용 |
interface Playable {
void play();
}
interface Streamable {
void stream();
}
class SmartTV implements Playable, Streamable {
@Override
public void play() {
System.out.println("재생");
}
@Override
public void stream() {
System.out.println("스트리밍");
}
}
- 인터페이스는 기능 명세 제공
- 클래스는 여러 인터페이스 구현 가능
- 다중 상속 문제 해결
| 느낀 점 | 내용 |
|---|
| 느슨한 결합 가능 | 유지보수 유리 |
| 다형성 극대화 | 인터페이스 기반 설계 |
| 실무 활용도 매우 높음 | Spring에서도 자주 사용 |
6) Wrapper Class
| 학습 내용 | 설명 |
|---|
| Wrapper | 기본형을 객체로 변환 |
| Boxing | 기본형 → 객체 |
| Unboxing | 객체 → 기본형 |
| Auto Boxing | 자동 변환 |
List<Integer> scoreList =
new ArrayList<>();
Integer value = 100;
int number = value;
- Generic에는 기본형 사용 불가
- Wrapper 클래스 사용 필요
- 자동 Boxing/Unboxing 지원
| 느낀 점 | 내용 |
|---|
| Generic 사용 시 필수 | Integer 자주 사용 |
| 내부 동작 이해 필요 | 성능 영향 가능 |
| 기본형과 객체형 구분 중요 | 메모리 사용 차이 존재 |
7) Generic
| 학습 내용 | 설명 |
|---|
| Generic | 타입을 일반화 |
| 타입 안정성 | 컴파일 시 오류 확인 |
| 재사용성 | 다양한 타입 지원 |
| Generic Method | 메서드 단위 타입 선언 |
class Storage<T> {
private T data;
public void setData(T data) {
this.data = data;
}
public T getData() {
return data;
}
public <K> K echo(K value) {
return value;
}
}
- T는 클래스 전체에서 사용
- 자료형을 외부에서 결정
- String, Integer 등 다양한 타입 활용 가능
| 느낀 점 | 내용 |
|---|
| 코드 재사용성 증가 | 타입별 클래스 생성 불필요 |
| 컴파일 단계 검증 가능 | 안정성 향상 |
| 컬렉션 프레임워크 핵심 | List, Map 모두 Generic 사용 |
🚨 직면한 문제 : Generic 이해 부족
문제 상황
수업 중 다음 코드가 이해되지 않았다.
Box<String> box1 = new Box<>();
Box<Integer> box2 = new Box<>();
특히 T가 무엇인지, 왜 String이나 Integer를 넣을 수 있는지 혼란스러웠다.
해결 과정
Generic이 없는 경우
class StringBox {
private String value;
}
class IntegerBox {
private Integer value;
}
타입마다 클래스를 새로 만들어야 한다.
Generic 사용
class Box<T> {
private T value;
}
사용 시
Box<String> textBox = new Box<>();
Box<Integer> numberBox = new Box<>();
컴파일러가 자동으로 타입을 결정한다.
최종적으로 이해한 내용
| 구분 | 의미 |
|---|
| T | Type |
| E | Element |
| K | Key |
| V | Value |
| ? | Wildcard |
Generic의 장점
| 장점 | 설명 |
|---|
| 타입 안정성 | 컴파일 시 오류 확인 |
| 캐스팅 감소 | 형변환 불필요 |
| 재사용성 | 여러 타입 지원 |
| 가독성 향상 | 의도 명확 |
오늘 배운 점
Generic은 "여러 타입을 받는다"가 아니라
"클래스나 메서드를 정의할 때 타입을 나중에 결정하도록 미루는 기술"
이라는 것을 이해하게 되었다.
특히 List, List, Map<String,Integer> 같은 컬렉션들이 모두 Generic을 기반으로 동작한다는 점을 알게 되었고, 앞으로 Spring과 Java 컬렉션을 사용할 때 Generic 문법을 훨씬 자연스럽게 읽을 수 있을 것 같다.