오브젝트 책을 스터디하는 과정에서 정리한 글입니다.
다형성의 기반을 제공
한다. * 피터 코드의 상속 규칙
"규칙에 만족하지 않는다면 상속을 이용하면 안된다"
1 "자식 클래스와 부모 클래스 사이는 역할 수행 관계가 아니어야 한다"
2 "한 클래스의 인스턴스는 다른 서브 클래스의 객체로 변환할 필요가 절대 없어야 한다"
3 "자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장 수행해야 한다"
4 "자식 클래스가 단지 일부 기능을 재사용할 목적으로 유틸리티 역할을 수행하는 클래스를 상속하지 않아야 한다"
5 "자식 클래스가 역할, 트랜잭션, 디바이스 등을 특수화 해야한다"
과대광고가 널리 퍼지면서, 맹신과 추종이 자라나게 됨
간단히 말해 다형성은 여러 타입을 대상으로 동작할 수 있는 코드를 작성할 수 있는 방법
public class Money {
public Money plus(Money amount) { ... }
public Money plus(BigDecimal amount) { ... }
public Money plus(long amount) { ... }
}
Subtype 다형성
이라고도 부름public static void execute(Calculator calculator) {
System.out.println("실행결과");
calculator.run();
}
public static void main(String[] args) {
Calculator c1 = new CalculatorDecoPlus(); // 다형성
c1.setOprands(10,20);
Calculator c2 = new CalculatorDecoMinus(); // 다형성
c2.setOprands(10,20);
execute(c1);
execute(c2);
}
데이터와 행동을
객체라고 불리는 하나의 실행 단위 안으로 통합하는 것. 객체지향 프로그램을 작성하기 위해서는 항상
"데이터" 와 "행동"이라는
두가지 관점을 함께 고려해야함
다형성
을 가능하게 하는 타입 계층을 구축하기 위한 것 Pass:3 Fail:2, A:1 B:1 C:1 D:0 F:2
public class Lecture {
private int pass; // 이수여부
private String title; // 과목명
private List<Integer> scores = new ArrayList<>(); // 성적을 보관할 List scores를 인스턴스 변수로 가짐
public Lecture(String title, int pass, List<Integer> scores) {
this.title = title;
this.pass = pass;
this.scores = scores;
}
public double average() {
return scores.stream().mapToInt(Integer::intValue).average().orElse(0);
}
public List<Integer> getScores() {
return Collections.unmodifiableList(scores);
}
public String evaluate() {
return String.format("Pass:%d Fail:%d", passCount(), failCount());
}
private long passCount() {
return scores.stream().filter(score -> score >= pass).count();
}
private long failCount() {
return scores.size() - passCount();
}
}
Lecture lecture = new Lecture("오브젝트 스터디", 70, Arrays.asList(81,95,75,50,45));
String evaluration = lecture.evaluate(); // 결과 : "Paas:3 Fail:2"
Lecture 의 출력결과에 등급별로 통계를 추가하는 기능인 클래스가 필요하다고 가정한다. GradeLecture
public class GradeLecture extends Lecture {
private List<Grade> grades;
public GradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
super(name, pass, scores);
this.grades = grades;
}
@Override
public String evaluate() {
return super.evaluate() + ", " + gradesStatistics();
}
private String gradesStatistics() {
return grades.stream().map(grade -> format(grade)).collect(joining(" "));
}
private String format(Grade grade) {
return String.format("%s:%d", grade.getName(), gradeCount(grade));
}
private long gradeCount(Grade grade) {
return getScores().stream().filter(grade::include).count();
}
public double average(String gradeName) {
return grades.stream()
.filter(each -> each.isName(gradeName))
.findFirst()
.map(this::gradeAverage)
.orElse(0d);
}
private double gradeAverage(Grade grade) {
return getScores().stream()
.filter(grade::include)
.mapToInt(Integer::intValue)
.average()
.orElse(0);
}
}
등급의 이름과 각 등급 범위를 정의하는 최소성적과 최대성적을 인스턴스 변수로 포함한다.
public class Grade {
private String name;
private int upper,lower;
private Grade(String name, int upper, int lower) {
this.name = name;
this.upper = upper;
this.lower = lower;
}
public String getName() {
return name;
}
public boolean isName(String name) {
return this.name.equals(name);
}
public boolean include(int score) {
return score >= lower && score <= upper;
}
}
public class GradeLecture extends Lecture {
private List<Grade> grades;
public GradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
super(name, pass, scores);
this.grades = grades;
}
@Override
public String evaluate() {
return super.evaluate() + ", " + gradesStatistics(); // Lecture 클래스의 evaluate 메소드를 실행
}
private String gradesStatistics() {
return grades.stream().map(grade -> format(grade)).collect(joining(" "));
}
private String format(Grade grade) {
return String.format("%s:%d", grade.getName(), gradeCount(grade));
}
private long gradeCount(Grade grade) {
return getScores().stream().filter(grade::include).count();
}
public double average(String gradeName) {
return grades.stream()
.filter(each -> each.isName(gradeName))
.findFirst()
.map(this::gradeAverage)
.orElse(0d);
}
private double gradeAverage(Grade grade) {
return getScores().stream()
.filter(grade::include)
.mapToInt(Integer::intValue)
.average()
.orElse(0);
}
}
* 여기서 주목할 부분은 GradeLecture 와 Lecture에 구현된 두 evaluate 메소드의 시그니처가 완전히 동일하다.
* 부모와 자식이 동일한 시그니처를 가진 메소드가 존재할 경우, 자식의 우선순위가 높다. (`뒤 후반부에 설명`)
* 동일한 시그니처의 경우 자식 메소드가 부모 클래스의 메소드를 가리게 된다.
* 부모 클래스의 구현을 새로운 구현으로 대체하는것을 `메소드 오버라이딩` 이라고 한다.
Lecture lecture = new GradeLecture("오브젝트 스터디", 70, Arrays.asList(new Grade("A", 100, 85),
new Grade("B", 84, 80)),
Arrays.asList(81,95,75,50,45));
lecture.evaluate(); // 결과 : "Paas:3 Fail:2, A:1 B:1..."
메소드 오버로딩
이라 부름여기까지 상속을 설명하는데 예제가 준비됨, 데이터와 행동이라는 두가지 관점에서 살펴보기
행동 관점에서 상속과 다형성의 기본 개념을 이해하려면, 상속 관계로 연결된 클래스 사이의 메소드 탐색과정을 이해하는 것이 중요함
포인터
를 갖게 하는게 경제적이 포인터를 이용하면, 클래스의 상속 계층을 따라 부모 클래스의 정의로 이동하는 것이 가능하다
각 교수별로 강의에 대한 성적 통계를 계산하는 기능을 추가해보기..
public class Professor {
private String name;
private Lecture lecture;
public Professor(String name, Lecture lecture) {
this.name = name;
this.lecture = lecture;
}
public String compileStatistics() {
return String.format("[%s] %s - Avg: %.1f", name,
lecture.evaluate(), lecture.average());
}
}
Professor professor = new Professor("이교수",
new Lecture("오브젝트",
70,
Arrays.asList(81,95,75,50,45));
professor.compileStatistics();
// 결과 : [이교수] Paas:3 Fail:2 - Avg:69.2
이처럼 코드 안에서 선언된 참조 타입과 무관하게, 실제로 메시지를 수신하는 객체의 타입에 따라 실행되는 메소드가 달라질 수 있음
- 이는, 업캐스팅과 동적바인딩이라는 메커니즘이 작용하기 때문
Lecture lecture = new GradeLecture (...) ; // 가능
public class Professor {
public Professor(String name, Lecture lecture) { ... }
}
Professor professor = new Professor("이교수", new GradeLecture(...)); // 가능
전통 언어에서 함수를 실행하는 방법은 함수를 호출하는것.
컴파일타임에 결정
된다. 호출될 코드가 결정된다는 것을 의미
정적바인딩, 초기바인딩, 컴파일타임바인딩
이라 부름 객체지향 언어에서는 메시지를 수신했을 때 실행될 메소드가 런타임에 결정
됨
동적바인딩, 지연바인딩
이라 부름 객체 지향 시스템은 다음 규칙에 따라 실행할 메소드를 선택함
메시지 탐색과 밀접한 변수는 self 참조
이다.
상속을 이용하면 메시지를 수신한 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다는 것이다.
이 동적인 문맥을 결정하는 것은 바로 메시지를 수신한 객체를 가리키는 self 참조
다.
동일한 코드라고 하더라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변한다.
public class Lecture {
public String stats() {
return String.format("Title ....., getEvaluationMethod());
}
public String getEvaluationMethod() {
return "Pass or Fail"
}
}
정확한 말은 아님
현재 클래스의 메소드를 호출하는 것이 아니라 , 현재 객체에게 메시지를 전송하라는 것.
개념 중요
- GradeLecture 에 stats을 전송하면 self 참조는 GradeLecture 의 인스턴스를 가리키고 있음. 그래서 메소드 탐색은 GradeLecture 클래스부터 시작하게 된다.
- GradeLecture 클래스는 stats 메시지를 처리할 적절한 메소드가 없으므로, 부모 클래스인 Lecture 에서 메소드 탐색을 계속하고 실행하게 된다.
- 여기서 self 참조가 가리키는건 GradeLecture. 하지만 stats 메소드를 실행하는 중에 getEvaluationMethod를 만나게 될땐, self참조가 가리키는 GradeLecture 에서부터 다시 시작한다
public class GradeLecture extends Lecture {
@Override
public String evaluate() {
return super.evaluate() + ", " + gradesStatistics();
}
}
public class FormattedGradeLecture extends GradeLecture {
public FormattedGradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
super(name, pass, grades, scores);
}
public String formatAverage() {
return String.format("Avg: %1.1f", super.average());
}
}
public class Grade Extends Evaluation {
public void push(String subject, int data) {
add(subject, data);
}
public void revise(String subject, int data) {
change(subject, data);
}
public String view(String contents) {
return contents;
}
}
위임
이라고 부른다. public class Grade {
private Evaluation evaluation = new Evaluation();
public void push(String subject, int data) {
evaluation.add(subject, data);
}
public void revise(String subject, int data) {
evaluation.change(subject, data);
}
public String view(String contents) {
return evaluation.result(contents);
}
public int size() { // 위임 메소드를 서브 클래스에 추가했다
return evaluation.size();
}
}