상속
Inheritance: 부모가 자식에게 물려주는 행위
상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 개발 시간을 단축시킨다.
예를 들어 다음 예시처럼 자식 클래스(B)에서 처음부터 필드와 메소드 4개를 작성하는 것보다 field1과 method1을 부모 클래스(A)에서 상속받고 field2와 method2만 추가 작성하는 것이 효율적이다
public class A {
int field;
void method1(){...}
}
public class B extends A{
String field2;
void method2(){...}
}
상속의 또 다른 이점은 클래스의 수정을 최소화할 수 있다는 것이다.
부모 클래스를 수정하면 모든 자식 클래스에 수정 효과를 가져온다.
B, C가 A를 상속할 경우 A의 필드와 메소드를 수정하면 B, C를 수정하지 않아도 수정된 A의 필드와 메소드를 이용할 수 있다.
현실에서 상속은 부모가 자식을 선택해서 물려주지만, 프로그램에서는 자식이 부모를 선택한다.
자식 클래스를 선언할 때 어떤 부모로부터 상속받을 것인지를 결정하고, 부모 클래스를 다음과 같이 extends 뒤에 기술한다.
public class 자식클래스 extends 부모클래스 {
}
다른 언어와는 달리 자바는 다중 상속을 허용하지 않는다.
즉, 여러 개의 부모 클래스를 상속할 수 없다.
따라서 extends 뒤에는 단 하나의 부모 클래스만이 와야 한다.
Phone클래스를 상속해서 SmartPhone 클래스를 작성하는 예제
package CH07;
public class Phone {
//필드 선언
public String model;
public String color;
//메소드 선언
public void bell(){
System.out.println("벨이 울립니다.");
}
public void sendVoice(String message){
System.out.println("자기: " + message);
}
public void receiveVoice(String message){
System.out.println("상대방: " + message);
}
public void hangUp(){
System.out.println("전화를 끊습니다.");
}
}
-----------------------------------------------------------------
package CH07;
public class SmartPhone extends Phone{
//필드 선언
public boolean wifi;
//생성자 선언
public SmartPhone(String model, String color){
this.model = model;
this.color = color;
}
//메소드 선언
public void setWifi(boolean wifi){
this.wifi = wifi;
System.out.println("와이파이 상태를 변경했습니다.");
}
public void internet(){
System.out.println("인터넷에 연결합니다.");
}
}
--------------------------------------------------------------------
package CH07;
public class SmartPhoneExample {
public static void main(String[] args) {
//SmartPhone 객체 생성
SmartPhone myPhone = new SmartPhone("갤럭시", "은색");
//Phone으로부터 상속받은 필드 읽기
System.out.println("모델: " + myPhone.model);
System.out.println("색상: " + myPhone.color);
//SmartPhone의 필드 읽기
System.out.println("와이파이 상태: " + myPhone.wifi);
//Phone으로부터 상속받은 메소드 호출
myPhone.bell();
myPhone.sendVoice("여보세요");
myPhone.receiveVoice("안녕하세요! 저는 홍길동인데요");
myPhone.sendVoice("아~ 네, 반갑습니다.");
myPhone.hangUp();
//SmartPhone의 메소드 호출
myPhone.setWifi(true);
myPhone.internet();
}
}
실행 결과
모델: 갤럭시
색상: 은색
와이파이 상태: false
벨이 울립니다.
자기: 여보세요
상대방: 안녕하세요! 저는 홍길동인데요
자기: 아~ 네, 반갑습니다.
전화를 끊습니다.
와이파이 상태를 변경했습니다.
인터넷에 연결합니다.
자바에서 자식 객체를 생성하면 부모 객체가 먼저 생성된 다음에 자식 객체가 생성된다.
다음 코드는 SmartPhone 객체만 생성되는 것처럼 보이지만, 사실은 부모인 Phone 객체가 먼저 생성되고 그 다음에 자식인 SmartPhone 객체가 생성된 것이다.
자식클래스 변수 = new 자식클래스();

부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super()에 의해 호출된다.
//자식 생성자 선언
public 자식클래스(...){
super();
...
}
super()는 컴파일 과정에서 자동 추가되는데, 이것은 부모의 기본 생성자를 호출한다.
만약 부모 클래스에 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생한다.
부모클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 개발자는 다음과 같이 super(매개값,...)코드를 직접 넣어야 한다.
이 코드는 매개값의 타입과 개수가 일치하는 부모 생성자를 호출한다.
//자식 생성자 선언
publc 자식클래스(...){
super(매개값,...);
...
}
부모 클래스가 기본 생성자를 가지고 있는 경우
package CH07;
public class Phone {
//필드 선언
public String model;
public String color;
//기본 생성자 선언
public Phone(){
System.out.println("Phone() 생성자 실행");
}
}
------------------------------------------------------------------
package CH07;
public class SmartPhone extends Phone{
//자식 생성자 선언
public SmartPhone(String model, String color){
super(); //생략 가능(컴파일 시 자동 추가됨)
this.model = model;
this.color = color;
System.out.println("SmartPhone(String model, String color) 생성자 실행");
}
}
------------------------------------------------------------------
package CH07;
public class SmartPhoneExample {
public static void main(String[] args) {
//SmartPhone 객체 생성
SmartPhone myPhone = new SmartPhone("갤럭시", "은색");
//Phone으로부터 상속받은 필드 읽기
System.out.println("모델: " + myPhone.model);
System.out.println("색상: " + myPhone.color);
}
}
실행 결과
Phone() 생성자 실행
SmartPhone(String model, String color) 생성자 실행
모델: 갤럭시
색상: 은색
부모 클래스가 매개변수를 갖는 생성자가 있는 경우
package CH07;
public class Phone {
//필드 선언
public String model;
public String color;
//매개변수를 갖는 생성자 선언
public Phone(String model, String color){
this.model = model;
this.color = color;
System.out.println("Phone(String model, String color) 생성자 실행");
}
}
------------------------------------------------------------------
package CH07;
public class SmartPhone extends Phone{
//자식 생성자 선언
public SmartPhone(String model, String color){
super(model, color); //반드시 작성해야함
System.out.println("SmartPhone(String model, String color) 생성자 실행");
}
}
------------------------------------------------------------------
package CH07;
public class SmartPhoneExample {
public static void main(String[] args) {
//SmartPhone 객체 생성
SmartPhone myPhone = new SmartPhone("갤럭시", "은색");
//Phone으로부터 상속받은 필드 읽기
System.out.println("모델: " + myPhone.model);
System.out.println("색상: " + myPhone.color);
}
}
실행 결과
Phone(String model, String color) 생성자 실행
SmartPhone(String model, String color) 생성자 실행
모델: 갤럭시
색상: 은색
부모 클래스의 모든 메소드가 자식 클래스엑 맞게 설계되어 있다면 가장 이상적인 상속이지만,
어떤 메소드는 자식 클래스가 사용하기에 적합하지 않을 수 있다.
이러한 메소드는 자식 클래스에서 재정의해서 사용해야 한다.
이것을 메소드 오버라이딩Overriding이라고 한다.
메소드가 오버라이딩되었다면, 해당 부모 메소드는 숨겨지고, 자식 메소드가 우선적으로 사용된다.
메소드 오버라이딩을 할 때는 다음과 같은 규칙에 주의해서 작성해야 한다.
오버라이딩 예제
package CH07;
public class Calculator {
//메소드 선언
public double areaCircle(double r){
System.out.println("Calculator 객체의 areaCircle() 실행");
return 3.141592 * r * r;
}
}
----------------------------------------------------------------------------
package CH07;
public class Computer extends Calculator{
//메소드 오버라이딩
@Override // 컴파일 시 정확히 오버라이딩이 되었는지 체크해 줌(생략 가능)
public double areaCircle(double r){
System.out.println("Computer 객체의 areaCircle() 실행");
return Math.PI * r * r;
}
}
----------------------------------------------------------------------------
package CH07;
public class ComputerExample {
public static void main(String[] args) {
int r = 10;
Calculator calculator = new Calculator();
System.out.println("원 면적: " + calculator.areaCircle(r));
System.out.println();
Computer computer = new Computer();
System.out.println("원 면적: " + computer.areaCircle(r));
}
}
실행 결과
Calculator 객체의 areaCircle() 실행
원 면적: 314.1592
Computer 객체의 areaCircle() 실행
원 면적: 314.1592653589793
자바는 @Override를 붙이면 컴파일 단계에서 정확히 오버라이딩이 되었는지 체크하고, 문제가 있다면 컴파일 에러를 출력한다.
메소드를 재정의하면, 부모 메소드는 숨겨지고 자식 메소드만 사용되기 때문에
부모 메소드의 일부만 변경된다 하더라도 중복된 내용을 자식 메소드도 가지고 있어야 한다.
ex) 부모 메소드가 100줄 코드를 가지고 있을 경우, 자식 메소드에서 1줄만 추가하고 싶어도 100줄의 코드를 자식 메소드에서 다시 작성해야 한다.
이 문제는 자식 메소드와 부모 메소드의 공종 작업 처리 기법을 이요하면 매우 쉽게 해결된다.
자식 메소드 내에서 부모 메소드를 호출하는 것인데, 다음과 같이 super 키워드와 도트(.) 연산자를 사용하면 숨겨진 부모 메소드를 호출할 수 있다.
class Parent {
public void method(){
//작업 처리1
}
}
class Child extends Parent{
@Override
void method(){
super.method();
//작업 처리2
}
}
super.method() 의 위치는 작업 처리2 전후 어디든 올 수 있다.
우선 처리가 되어야 할 내용을 먼저 작성하면 된다.
이 방법은 부모 메소드를 재사용함으로써 자식 메소드의 중복 작업 내용을 없애는 효과를 가져온다.
다음 예제는 일반 비행 모드일 때는 Airplane의 fly()를 사용하고,
초음속 비행 모드일 때는 오버라이딩된 SupersonicAirplane의 fly()를 사용한다.
package CH07;
public class Airplane {
//메소드 선언
public void land(){
System.out.println("착륙합니다.");
}
public void fly(){
System.out.println("일반 비행합니다.");
}
public void takeOff(){
System.out.println("이륙합니다.");
}
}
-------------------------------------------------------------------------------
package CH07;
public class SuperSonicAirplane extends Airplane{
//상수 선언
public static final int NORMAL = 1;
public static final int SUPERSONIC = 2;
//상태 필드 선언
public int flyMode = NORMAL;
//메소드 재정의
@Override
public void fly(){
if(flyMode == SUPERSONIC){
System.out.println("초음속 비행합니다.");
}else{
//Airplane 객체의 fly() 메소드 호출
super.fly();
}
}
}
-------------------------------------------------------------------------------
package CH07;
public class SupersonicAirplaneExample {
public static void main(String[] args) {
SuperSonicAirplane sa = new SuperSonicAirplane();
sa.takeOff();
sa.fly();
sa.flyMode = SuperSonicAirplane.SUPERSONIC;
sa.fly();
sa.flyMode = SuperSonicAirplane.NORMAL;
sa.fly();;
sa.land();
}
}
실행 결과
이륙합니다.
일반 비행합니다.
초음속 비행합니다.
일반 비행합니다.
착륙합니다.
우리가 6장 11절에서 살펴보았듯이, 필드 선언 시에 final을 붙이면 초기값 설정 후 값을 변경할 수 없다. 그렇다면 클래스와 메소드에 final을 붙이면 어떤 효과가 일어날까?
final 클래스와 final 메소드는 상속과 관련이 있다.