상속·생성자·오버라이딩
extends 관계·상속 메모리·오버라이딩·접근 제어·super와 생성자 순서를 게시글 계층으로 한 번에 연결합니다.
상속은 여러 클래스에 공통인 상태와 행동을 부모 클래스에 두고 자식이 재사용·확장하는 문법입니다.
단순 코드 복사 제거보다 “자식은 부모의 한 종류인가”라는 타입 관계가 먼저입니다.
게시판에서 텍스트·코드·영상 형식을 모두 게시글로 다룰 때 상속 후보가 생깁니다.
class Post {
void publish() {
System.out.println("게시글 공개");
}
}
class TextPost extends Post {
}
TextPost textPost = new TextPost();
textPost.publish();TextPost extends Post는 텍스트 게시글이 게시글의 한 종류임을 나타냅니다.
자식에 publish를 다시 쓰지 않아도 부모의 메서드를 사용할 수 있습니다.
상태를 생성자로 받아야 할 때는 부모 초기화가 먼저 필요하므로, 이제 실패 예를 통해 super(...)의 위치를 확인합니다.
부모 생성자 호출
public final class MissingParentConstructor {
public static void main(String[] args) {
CodePost post = new CodePost();
System.out.println(post);
}
private static class Post {
Post(String title) {
System.out.println(title);
}
}
private static final class CodePost extends Post {
}
}error: constructor Post in class Post cannot be applied to given typesCodePost에 생성자를 쓰지 않으면 컴파일러가 기본 생성자와 첫 문장 super()를 가정합니다.
부모 Post에는 매개변수 없는 생성자가 없으므로 호출할 수 없습니다.
자식 생성자에서 부모가 요구하는 값을 명시해야 합니다.
CodePost(String title) {
super(title);
}상속 인스턴스에는 부모 부분과 자식 부분이 함께 만들어지므로 두 생성자 모두 실행돼야 합니다.
부모 상태를 먼저 초기화한 뒤 자식 상태를 초기화합니다.
extends와 기능 확장
public final class PostInheritance {
public static void main(String[] args) {
TextPost textPost = new TextPost("가입 인사", 40, 120);
CodePost codePost = new CodePost("로그인 구현", 50, "feature/login");
textPost.publish();
textPost.previewBody();
codePost.publish();
codePost.commit();
}
private static class Post {
private final String title;
private final int viewCount;
Post(String title, int viewCount) {
if (title.isBlank() || viewCount <= 0) throw new IllegalArgumentException();
this.title = title;
this.viewCount = viewCount;
}
void publish() {
System.out.println("publish=" + title + ":views=" + viewCount);
}
protected String title() {
return title;
}
}
private static final class TextPost extends Post {
private final int bodyLength;
TextPost(String title, int viewCount, int bodyLength) {
super(title, viewCount);
this.bodyLength = bodyLength;
}
void previewBody() {
System.out.println("preview=" + title() + ":chars=" + bodyLength);
}
}
private static final class CodePost extends Post {
private final String branch;
CodePost(String title, int viewCount, String branch) {
super(title, viewCount);
this.branch = branch;
}
void commit() {
System.out.println("commit=" + title() + ":" + branch);
}
}
}publish=가입 인사:views=40
preview=가입 인사:chars=120
publish=로그인 구현:views=50
commit=로그인 구현:feature/login두 자식은 title·viewCount와 publish를 물려받고 자기 필드와 행동을 추가합니다.
자바 클래스는 extends 뒤에 부모 하나만 선택할 수 있습니다.
클래스 다중 상속을 허용하면 두 부모가 같은 메서드를 제공할 때 어느 구현을 사용할지 모호한 다이아몬드 문제가 생길 수 있습니다.
여러 역할 조합은 뒤에서 인터페이스 다중 구현으로 다룹니다.
private 필드는 자식도 직접 접근할 수 없습니다.
부모가 보호해야 할 상태는 private으로 두고 title() 같은 protected 또는 public 행동으로 필요한 협력을 제공합니다.
protected를 모든 자식 편의용 공개 필드처럼 쓰면 부모 불변식을 우회하므로 메서드 경계를 우선합니다.
상속 객체의 구조
new CodePost(...)를 실행하면 하나의 객체 참조를 얻지만 그 인스턴스 안에는 Post에서 정의한 상태와 CodePost에서 정의한 상태가 모두 있습니다.
메서드를 찾을 때 현재 타입에서 시작해 부모 방향으로 올라갑니다.
codePost.commit()은 CodePost에서 찾습니다.codePost.publish()는 CodePost에 없으므로 Post에서 찾습니다.- 부모에서 자식 방향으로는 임의로 내려가 메서드를 찾지 않습니다.
객체가 두 개 만들어진다는 뜻은 아닙니다.
하나의 상속 인스턴스 안에 클래스 계층에 대응하는 부분이 함께 초기화된다고 이해합니다.
메서드 코드는 각 객체에 복사되지 않고 클래스 정보로 관리됩니다.
메서드 오버라이딩
public final class OverridePost {
public static void main(String[] args) {
TextPost textPost = new TextPost("가입 인사", 40, 120);
CodePost codePost = new CodePost("로그인 구현", 50, "feature/login");
textPost.publish();
codePost.publish();
}
private static class Post {
protected final String title;
protected final int viewCount;
Post(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
void publish() {
System.out.println("post=" + title + ":views=" + viewCount);
}
}
private static final class TextPost extends Post {
private final int bodyLength;
TextPost(String title, int viewCount, int bodyLength) {
super(title, viewCount);
this.bodyLength = bodyLength;
}
@Override
void publish() {
System.out.println("text=" + title + ":chars=" + bodyLength);
}
}
private static final class CodePost extends Post {
private final String branch;
CodePost(String title, int viewCount, String branch) {
super(title, viewCount);
this.branch = branch;
}
@Override
void publish() {
super.publish();
System.out.println("code=branch/" + branch);
}
}
}text=가입 인사:chars=120
post=로그인 구현:views=50
code=branch/feature/login메서드 이름, 매개변수 목록, 반환 호환성이 부모 메서드와 맞으면 오버라이딩합니다.
@Override는 필수 문법은 아니지만 서명이 어긋나 새 메서드가 되는 실수를 컴파일러가 잡게 하므로 붙입니다.
자식 오버라이딩 메서드 안에서 super.publish()로 부모 구현을 명시적으로 호출할 수 있습니다.
필드는 오버라이딩되지 않습니다.
부모와 자식에 같은 이름 필드를 만들면 두 저장 위치가 생겨 참조 타입에 따른 숨김 문제가 생깁니다.
같은 개념 상태는 부모에 하나만 두는 편이 낫습니다.
super 참조
this는 현재 객체의 현재 클래스 관점, super는 같은 객체의 부모 클래스 관점에서 멤버를 찾습니다.
@Override
void publish() {
super.publish();
System.out.println("child extension");
}super가 별도의 부모 객체 참조를 반환하는 것은 아닙니다.
오버라이딩된 자식 메서드 대신 부모 구현을 선택하는 특별한 키워드입니다.
생성자에서 super(...)는 부모 생성자를 호출하며 첫 문장이어야 합니다.
this(...)로 같은 클래스 생성자에 위임했다면 그 위임 경로의 최종 생성자가 부모를 호출합니다.
생성 실행 순서는 최상위 부모에서 가장 구체적인 자식 방향입니다.
자식 생성자에 먼저 진입하더라도 첫 문장인 super 호출을 따라 부모 초기화가 끝난 후 자식 본문이 실행됩니다.
부모 생성자에서 오버라이딩 가능한 메서드를 호출하면 아직 자식 필드가 초기화되기 전에 자식 구현이 실행될 수 있으므로 피합니다.
상속 선택 기준
CodePost is a Post는 자연스럽지만 Board is a Scanner는 자연스럽지 않습니다.
단지 Scanner 메서드를 재사용하려고 상속하면 타입 의미가 왜곡됩니다.
“가지고 사용한다” 관계는 구성으로 표현합니다.
final class BoardCli {
private final Scanner scanner;
}상속은 부모 계약을 자식이 지킬 수 있어야 합니다.
부모를 기대하는 위치에 자식을 넣어도 핵심 의미가 깨지지 않아야 다형성으로 확장할 수 있습니다.
공통 코드가 조금 있다는 이유만으로 계층을 만들면 부모 변경이 모든 자식에 퍼지고 잘못된 결합이 생깁니다.
게시글 계층의 부모 배열
public final class InheritedPostArray {
public static void main(String[] args) {
Post[] posts = {
new TextPost("가입 인사", 40),
new CodePost("로그인 구현", 50)
};
int total = 0;
for (Post post : posts) {
post.publish();
total += post.viewCount();
}
System.out.println("total=" + total);
}
private static class Post {
private final String title;
private final int viewCount;
Post(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
int viewCount() {
return viewCount;
}
protected String title() {
return title;
}
void publish() {
System.out.println("post=" + title);
}
}
private static final class TextPost extends Post {
TextPost(String title, int viewCount) {
super(title, viewCount);
}
@Override
void publish() {
System.out.println("text=" + title());
}
}
private static final class CodePost extends Post {
CodePost(String title, int viewCount) {
super(title, viewCount);
}
@Override
void publish() {
System.out.println("code=" + title());
}
}
}text=가입 인사
code=로그인 구현
total=90부모 타입 배열에 서로 다른 자식 참조를 넣고 공통 메서드를 호출했습니다.
왜 자식 구현이 선택되는지는 다음 문서부터 다형적 참조와 동적 디스패치로 자세히 다룹니다.
연습 문제
부모 Post에 title과 publish(), 자식 TextPost에 bodyLength, ImagePost에 imageCount를 두세요.
각 자식이 publish를 오버라이딩하고 생성자에서 super로 title을 전달합니다.
해설 보기
public final class PostInheritanceExercise {
public static void main(String[] args) {
Post[] posts = {
new TextPost("가입 인사", 300),
new ImagePost("행사 사진", 4)
};
for (Post post : posts) {
post.publish();
}
}
private static class Post {
protected final String title;
Post(String title) {
this.title = title;
}
void publish() {
System.out.println("publish=" + title);
}
}
private static final class TextPost extends Post {
private final int bodyLength;
TextPost(String title, int bodyLength) {
super(title);
this.bodyLength = bodyLength;
}
@Override
void publish() {
System.out.println("text=" + title + ":chars=" + bodyLength);
}
}
private static final class ImagePost extends Post {
private final int imageCount;
ImagePost(String title, int imageCount) {
super(title);
this.imageCount = imageCount;
}
@Override
void publish() {
System.out.println("image=" + title + ":count=" + imageCount);
}
}
}text=가입 인사:chars=300
image=행사 사진:count=4공통 title과 타입 계약은 부모에, 게시글 형식별 상태와 표현은 자식에 있습니다.
자식 생성자는 부모 초기화를 먼저 위임합니다.