final 변수와 상수
final 키워드는 이름 그대로 끝이라는 뜻.
변수에 final 키워드가 붙으면 더는 값을 변경할 수 없음
final은 class, method를 포함, 여러 곳에 붙을 수 있다.
final - 지역 변수
package final1;
public class FinalLocalMain {
public static void main(String[] args) {
//final 지역 변수
final int data1;
data1 = 10; //최초 한 번만 할당 가능
//data1 = 20; // 컴파일 오류
//final 지역 변수 2
final int data2 = 10;
//data2 = 20; // 컴파일 오류
method(10);
}
static void method(final int parameter) {
// parameter = 20; // 컴파일 오류
}
}
- final을 지역 변수에 설정할 경우 최초 한 번만 할당할 수 있음, 이후에 변수의 값을 변경하려면 컴파일 오류 발생
- final을 지역 변수 선언 시 바로 초기화한 경우 이미 값이 할당되었기 때문에 값을 할당할 수 없음
- 매개변수에 final이 붙으면 메서드 내부에서 매개변수의 값을 변경할 수 없음(메서드 호출 시점에 사용된 값이 끝까지 사용됨)
final - 필드(멤버 변수)
package final1;
public class ConstructInit {
final int value;
public ConstructInit(int value) {
this.value = value;
}
}
- 필드에 사용할 경우 해당 필드는 생성자를 통해서 한 번만 초기화 될 수 있다.
package final1;
public class FieldInit {
static final int CONST_VALUE = 10;
final int value = 10;
}
- final 필드를 필드에서 초기화하면 이미 값이 설정되었기 때문에 생성자를 통해서도 초기화할 수 없음.
- static 변수에도 final 을 선언할 수 있음
package final1;
public class FinalFieldMain {
public static void main(String[] args) {
//final 필드 - 생성자 초기화
System.out.println("생성자 초기화");
ConstructInit constructInit1 = new ConstructInit(10);
ConstructInit constructInit2 = new ConstructInit(20);
System.out.println(constructInit1.value);
System.out.println(constructInit2.value);
//final 필드 - 필드 초기화
System.out.println("필드 초기화");
FieldInit fieldInit1 = new FieldInit();
FieldInit fieldInit2 = new FieldInit();
FieldInit fieldInit3 = new FieldInit();
System.out.println(fieldInit1.value);
System.out.println(fieldInit2.value);
System.out.println(fieldInit3.value);
//상수
System.out.println("상수");
System.out.println(FieldInit.CONST_VALUE);
}
}
ConstructInit과 같이 생성자를 사용해서 final 필드를 초기화 하는 경우, 각 인스턴스마다 final 필드에 다른 값을 할당 할 수 있음(이후 값 변경 불가)
- FieldInit과 같이 final 필드를 필드에서 초기화하는 경우, 모든 인스턴스가 같은 값을 가진다.
- 생성자 초기화와 다르게 필드 초기화는 필드의 코드에 final 필드 값이 미리 정해져 있다.
- 모든 인스턴스가 같은 값을 사용하기에 결과적으로 메모리를 낭비하게 됨(이럴 때 사용하면 좋은 것이 static 영역)
static final
- FieldInit.MY_VALUE는 static 영역에 존재, final 키워드를 사용해서 초기화 값이 변하지 않음
- static 영역은 단 하나만 존재하는 영역, MY_VALUE 변수는 JVM 상에서 하나만 존재하므로 중복과 메모리 비효율 문제를 모두 해결할 수 있다.
상수(constant)
상수는 변하지 않고, 항상 일정한 값을 갖는 수(자바에서는 보통 단 하나만 존재하는 변하지 않는 고정도니 값을 상수라 함).
상수는 static final 키워드를 사용
자바 상수 특징
- static final 키워드를 사용
- 대문자를 사용하고 구분은 _(언더스코어)로 한다(관례)
- 일반적인 변수와 상수를 구분하기 위해 사용
- 필드를 직접 접근해서 사용
- 상수는 기능이 아니라 고정된 값 자체를 사용하는 것이 목적
- 상수는 값을 변경할 수 없다. 따라서 필드에 직접 접근해도 데이터가 변하는 문제가 발생하지 않음
package final1;
public class Constant {
// 수학 상수
public static final double PI = 3.14;
// 시간 상수
public static final int HOURS_IN_DAY = 24;
public static final int MINUTES_IN_HOUR = 60;
public static final int SECONDS_IN_MINUTE = 60;
// 애플리케이션 설정 상수
public static final int MAX_USERS = 1000;
}
- 애플리케이션 안에는 다양한 상수가 존재할 수 있다. 수학, 시간 등등 실생활에서 사용하는 상수부터, 애플리케이션의 다양한 설정을 위한 상수들도 있다.
- 보통 이런 상수들은 애플리케이션 전반에서 사용되기 때문에 public을 자주 사용, 특정 위치에서만 사용될 경우 다른 접근자를 사용
- 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있다.
- 상수는 런타임에 변경할 수 없다. 상수를 변경하려면 프로그램을 종료하고, 코드를 변경한 다음에 프로그램을 다시 실행해야 한다.
추가로 상수는 중앙에서 값을 하나로 관리할 수 있다는 장점도 있음.
ConstantMain1 - 상수 없음
package final1;
public class ConstantMain1 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수: " + 1000);
int currentUserCount = 999;
process(currentUserCount++);
process(currentUserCount++);
process(currentUserCount++);
}
private static void process(int currentUserCount) {
if (currentUserCount > 1000) {
System.out.println("대기자로 등록합니다.");
} else {
System.out.println("게임에 참여합니다.");
}
}
}
코드의 문제점
- 프로그램 최대 참여자 수를 2000명으로 변경해야 하면 2곳의 변경 포인트가 발생(애플리케이션 100곳에서 해당 숫자를 사용했다면 100곳 모두 변경해야 함)
- 매직 넘버 문제가 발생, 숫자 1000이라는 것이 무슨 뜻인지, 값만 보고 이해하기 어려움
ConstantMain2 - 상수 사용
package final1;
public class ConstantMain2 {
public static void main(String[] args) {
System.out.println("프로그램 최대 참여자 수: " + Constant.MAX_USERS);
int currentUserCount = 999;
process(currentUserCount++);
process(currentUserCount++);
process(currentUserCount++);
}
private static void process(int currentUserCount) {
if (currentUserCount > Constant.MAX_USERS) {
System.out.println("대기자로 등록합니다.");
} else {
System.out.println("게임에 참여합니다.");
}
}
}
- Constant.MAX_USERS 상수를 사용, 프로그램 최대 참여자 수 변경 시 Constant.MAX_USERS의 상수 값만 변경하면 된다.
- 매직 넘버 문제를 해결(사람이 인지할 수 있게 MAX_USERS라는 변수명으로 이해 가능).
final 변수와 참조
final은 변수의 값을 변경하지 못하게 막음
- 변수는 크게 기본형과 참조형 변수가 있음(기본형은 값을 보관, 참조형은 객체의 참조값을 보관)
- final을 기본형 변수에 사용 시 값을 변경할 수 없음
- final을 참조형 변수에 사용 시 참조값을 변경할 수 없음
package final1;
public class Data {
public int value;
}
package final1;
public class FinalRefMain {
public static void main(String[] args) {
final Data data = new Data();
// data = new Data();
// 참조 대상의 값은 변경 가능
data.value = 10;
System.out.println(data.value);
data.value = 20;
System.out.println(data.value);
}
}
- 참조형 변수 data에 final이 붙었다, 변수 선언 시점에 참조값을 할당했으므로 참조값을 변경할 수 없음.
- 참조 대상의 객체 값은 변경할 수 있다.
- 참조형 변수 data에 final이 붙음, 이 경우 참조형 변수에 들어있는 참조값을 다른 값으로 변경하지 못함(다른 객체를 참조할 수 없다). 참조형 변수에 들어있는 참조값만 변경하지 못하는 것
- Data.value는 final이 아님, 따라서 값을 변경할 수 있다.
참조형 변수에 final이 붙으면 참조 대상 자체를 다른 대상으로 변경하지 못하는 것이지, 참조하는 대상의 값은 변경할 수 있다.
정리
final은 매우 유용한 제약, 특정 변수의 값을 할당한 이후에 변경하지 않아야 한다면 final을 사용.
package final1.ex;
public class Member {
private final String id; // final 키워드 사용
private String name;
public Member(String id, String name) {
this.id = id;
this.name = name;
}
public void changeData(String name) {
//this.id = id; // 컴파일 오류
this.name = name;
}
public void print() {
System.out.println("id:" + id + ", name:" + name);
}
}
package final1.ex;
public class MemberMain {
public static void main(String[] args) {
Member member = new Member("myId", "kim");
member.print();
member.changeData("seo");
member.print();
}
}
상속
예제 코드
package extends1.ex1;
public class ElectricCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
package extends1.ex1;
public class GasCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.ex1;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
}
}
전기차(ElectricCar)와 가솔린차(GasCar)를 만듬, 전기차는 이동(move( )), 충전(charge( )) 기능이 있고, 가솔린차는 이동(move( )), 주유(fillUp( )) 기능이 있음
전기차와 가솔린차는 자동차(Car)의 구체적인 개념, 자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념, 둘의 공통 기능인 이동(move( ))을 사용하는 데는 상속 관계를 사용하는 것이 효과적임.
상속 관계
상속은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해준다. 기존 클래스의 속성과 기능을 그대로 물려받는 것.
상속을 사용하려면 extends 키워드를 사용하면 됨(extends 대상은 하나만 선택할 수 있다).
용어 정리
- 부모 클래스 (슈퍼 클래스) : 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
- 자식 클래스 (서브 클래스) : 부모 클래스로부터 필드와 메서드를 상속받는 클래스
상속 관계를 사용하는 코드
package extends1.ex2;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
}
package extends1.ex2;
public class ElectricCar extends Car {
public void charge() {
System.out.println("충전합니다.");
}
}
package extends1.ex2;
public class GasCar extends Car{
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.ex2;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
}
}
전기차와 가솔린차가 Car를 상속 받은 덕분에 electricCar.move(), gasCar.move()를 사용할 수 있다.
자식 클래스가 부모 클래스의 기능을 물려 받아서 사용할 수 있지만, 부모 클래스는 자식 클래스에 접근할 수 없다(자식 클래스는 extends Parent를 통해서 부모를 알고 있기 때문에 접근할 수 있음).
단일 상속
자바는 다중 상속을 지원하지 않음, extend 대상은 하나만 선택할 수 있음, 부모 하나만 선택할 수 있다(부모가 또 다른 부모를 하나 가지는 것은 괜찮음).
다중 상속 그림
다중 상속을 사용하게 될 경우 AirplaneCar 입장에서 move( )를 호출할 때 어떤 부모의 move( )를 사용해야 할지 애매한 문제가 발생(다이아몬드 문제). 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해질 수 있음.
상속과 메모리 구조
상속 관계를 객체로 생성할 때 메모리 구조
ElectricCar electricCar = new ElectricCar();
new ElectricCar( )를 호출하면 ElectricCar 뿐 아니라 상속 관계에 있는 Car까지 함께 포함해서 인스턴스를 생성, 참조값은 x001로 하나지만 실제로 그 안에서는 Car, ElectricCar라는 두 가지 클래스 정보가 공존하는 것.
상속이라고 해서 단순하게 부모의 필드와 메서드만 물려 받는 것이 아님, 상속 관계를 사용하면 부모 클래스도 함께 포함해서 생성됨(외부에서 볼 때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분 됨).
electricCar.charge( ) 호출
electricCar.charge( )를 호출하면 참조값을 확인해서 x001.charge( )를 호출함. 따라서 x001 을 찾아서 charge( )를 호출하면 되는 것이다. 그런데 상속 관계의 경우에는 내부에 부모와 자식이 모두 존재함.
이때 부모인 Car 를 통해 charge( )를 찾을지 아니면 ElectricCar를 통해 charge( )를 찾을지 선택해야 함( 호출하는 변수의 타입(클래스)을 기준으로 선택) . electricCar변수의 타입이 ElectricCar 이므로 인스턴스 내부에 같은 타입인 ElectricCar를 통해서 charge( ) 를 호출
electricCar.move( ) 호출
electricCar.move( ) 호출 시 먼저 x001 참조로 이동, 내부에 Car, ElectricCar 두 타입이 존재, 호출하는 변수의 타입과 같은 것을 선택(ElectricCar).
상속 관계에서는 자식 타입에 해당 기능이 없으면 부모 타입으로 올라가서 찾는다(ElectricCar의 부모인 Car로 올라가 move( )를 찾아서 호출).
만약 부모에서도 해당 기능을 찾지 못하면 더 상위 부모에서 필요한 기능을 찾아봄(부모에서 부모로 계속 올라가면서 필드나 메서드를 찾는 것, 계속 찾아도 없으면 컴파일 오류 발생).
지금까지 설명한 상속과 메모리 구조는 반드시 이해해야 한다.
- 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
- 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 하는데 이때 호출자의 타입을 통해 대상 타입을 찾는다.
- 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.
상속과 기능 추가
상속관계에 다음 기능을 추가
- 모든 차량에 문열기(openDoor( )) 기능을 추가
- 새로운 수소차(HydrogenCar)를 추가
- 수소차는 fillHydrogen( ) 기능을 통해 수소를 충전할 수 있음
package extends1.ex3;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
// 추가
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
상위 부모인 Car에 openDoor( ) 기능 추가 시 Car의 자식들은 해당 기능을 모두 물려 받는다.
package extends1.ex3;
public class HydrogenCar extends Car {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
수소차를 추가, Car를 상속받아 move( ), openDoor( )와 같은 기능을 바로 사용 가능.
package extends1.ex3;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
electricCar.openDoor();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
gasCar.openDoor();
HydrogenCar hydrogenCar = new HydrogenCar();
hydrogenCar.move();
hydrogenCar.fillHydrogen();
hydrogenCar.openDoor();
}
}
기능 추가와 클래스 확장
상속 관계 덕에 중복은 줄어들고, 새로운 수소차를 편리하게 확장(extend)한 것을 알 수 있음.
상속과 메서드 오버라이딩
부모 타입의 기능을 자식에서 다르게 재정의 하고 싶은 경우가 발생할 수 있음.
부모에게서 상속 받은 기능을 자식이 재정의 하는 것을 메서드 오버라이딩(Overriding)이라 함.
package extends1.overriding;
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
부모의 move( ) 기능을 그래도 사용하지 않고 메서드 이름은 같은 새로운 기능을 사용하기 위해 ElectricCar의 move( ) 메서드를 새로 만듦.
부모의 기능을 자식이 새로 재정의하는 것을 메서드 오버라이딩이라 함, 이제 ElectricCar의 move( )를 호출하면 Car의 move( )가 아니라 ElectricCar의 move( )가 호출 됨.
@Override
@이 붙은 부분을 애노테이션이라 함(애노테이션은 주석과 비슷한데, 프로그램이 읽을 수 있는 특별한 주석이라 생각하면 됨).
- 이 애노테이션은 상위 클래스의 메서드를 오버라이드하는 것임을 나타냄
- 이름 그대로 오버라이딩한 메서드 위에 이 애노테이션을 붙여야 함
- 컴파일러는 이 애노테이션을 보고 메서드가 정확히 오버라이드 되었는지 확인함(오버라이딩 조건을 만족시키지 않으면 컴파일 에러를 발생시킴).
- 이 경우 부모에 move( ) 메서드가 없다면 컴파일 오류가 발생
- 이 기능은 필수가 아니지만 코드의 명확성을 위해 붙여주는 것이 좋음
오버라이딩과 클래스
Car의 move( ) 메서드를 ElectricCar에서 오버라이딩.
오버라이딩과 메모리 구조
- electricCar.move( )를 호출
- 호출한 electricCar의 타입은 ElectricCar이다, 따라서 인스턴스 내부의 ElectricCar 타입에서 시작
- ElectricCar 타입에 move( ) 메서드가 있음(해당 메서드를 실행). 실행할 메서드를 찾았으므로 부모 타입을 찾지 않는다.
오버로딩(Overloading)과 오버라이딩(Overriding)
- 메서드 오버로딩: 메서드 이름이 같고 파라미터가 다른 메서드를 여러 개 정의하는 것(번역하면 과적, 과하게 물건을 담았다는 뜻)
- 메서드 오버라이딩: 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정을 의미(상속 관계에서 사용), 부모의 기능을 자식이 다시 정의하는 것(단순 해석하면 무언가를 넘어서 타는 것, 자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버리는 것). 번역하면 무언가를 다시 정의한다고 해서 재정의라 함.
메서드 오버라이딩 조건
메서드 오버라이딩은 까다로운 조건을 가지고 있음(지금은 부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해)
메서드 오버라이딩 조건
- 메서드 이름: 메서드 이름이 같아야 한다.
- 메서드 파라미터(매개변수): 파라미터 타입, 순서, 개수가 같아야 한다.
- 반환 타입: 반환 타입이 같아야 한다. 단, 반환 타입이 하위 클래스 타입일 수 있다.
- 접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안된다.
- 상위 클래스의 메서드가 protected로 선언되어 있으면 하위 클래스에서 public 또는 protected로 오버라이드 할 수 있지만, private 또는 default로 오버라이드 할 수 없음
- 예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없다. 하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다.
- static, final, private: 키워드가 붙은 메서드는 오버라이딩 될 수 없다.
- static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미 없다(클래스 이름을 통해 필요한 곳에 직접 접근하면 된다).
- final 메서드는 재정의를 금지한다.
- private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않음(오버라이딩 불가).
- 생성자 오버라이딩: 생성자는 오버라이딩 할 수 없다.
상속과 접근 제어
- + : public
- # : protected
- ~ : default
- - : private
접근 제어자의 종류
- private : 모든 외부 호출을 막는다
- default(pakage-private) : 같은 패키지 안에서 호출은 허용한다
- protected : 같은 패키지 안에서 호출은 허용한다, 패키지가 달라도 상속 관계의 호출은 허용한다
- public : 모든 외부 호출을 허용한다.
private → default → protected → public
부모 메서드
package extends1.access.parent;
public class Parent {
public int publicValue;
protected int protectedValue;
int defaultValue;
private int privateValue;
public void publicMethod() {
System.out.println("Parent.publicMethod");
}
protected void protectedMethod() {
System.out.println("Parent.protectedMethod");
}
void defaultMethod() {
System.out.println("Parent.defaultMethod");
}
private void privateMethod() {
System.out.println("Parent.privateMethod");
}
public void printParent() {
System.out.println("==Parent 메서드 안==");
System.out.println("publicValue = " + publicValue);
System.out.println("protectedValue = " + protectedValue);
System.out.println("defaultValue = " + defaultValue);
System.out.println("privateValue = " + privateValue);
// 부모 메서드 안에서 모두 접근 가능
defaultMethod();
privateMethod();
}
}
자식 메서드
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
//defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
//privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
//defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
//privateMethod(); // 접근 불가, 컴파일 오류
printParent();
}
}
실행 코드
package extends1.access;
import extends1.access.child.Child;
public class ExtendsAccessMain {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
코드 실행 시 Child.call( ) → Parent.printParent( ) 순서로 호출
Child는 부모의 public, protected 필드나 메서드만 접근할 수 있다. Parent.printParent( )의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 접근 가능.
접근 제어와 메모리 구조
본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 줌(객체 내부에서는 자식과 부모가 구분되어 있기 때문). 자식 타입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같다.
super - 부모 참조
부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없다. 이때 super 키워드를 사용하면 부모를 참조할 수 있다.
부모 클래스
package extends1.super1;
public class Parent {
public String value = "parent";
public void hello() {
System.out.println("Parent.hello");
}
}
자식 클래스
package extends1.super1;
public class Child extends Parent {
public String value = "child";
@Override
public void hello() {
System.out.println("Child.hello");
}
public void call() {
System.out.println("this value = " + this.value); // this. 생략 가능
System.out.println("this value = " + super.value);
this.hello(); // this. 생략 가능
super.hello();
}
}
call( ) 메서드
- this 는 자기 자신의 참조를 뜻함, this는 생략할 수 있음
- super는 부모 클래스에 대한 참조를 뜻함
- 필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있음
package extends1.super1;
public class Super1Main {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
super 메모리 그림
super - 생성자
상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어짐, Child를 만들면 Parent까지 함께 만들어지는 것(각각의 생성자도 모두 호출되어야 함).
상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다(규칙).
상속 관계에서 부모의 생성자를 호출할 때는 super(...)를 사용
클래스 A
package extends1.super2;
public class ClassA {
public ClassA() {
System.out.println("Class A 생성자");
}
}
- ClassA는 최상위 부모 클래스
클래스 B
package extends1.super2;
public class ClassB extends ClassA {
public ClassB(int a) {
super(); // 기본 생성자 생략 가능
System.out.println("ClassB 생성자 a=" + a);
}
public ClassB(int a, int b) {
super(); // 기본 생성자 생략 가능
System.out.println("ClassB 생성자 a=" + a + ", b=" + b);
}
}
- ClassB는 ClassA를 상속 받음, 상속을 받으면 생성자의 첫줄에 super(...)를 사용해서 부모 클래스의 생성자를 호출해야 함
- 예외로 생성자 첫줄에 this(...)를 사용할 수는 있음, super(...)는 자식의 생성자 안에서 언젠가는 반드시 호출해야 함
- 부모 클래스의 생성자가 기본 생성자(파라미터가 없는 생성자)인 경우에는 super(...)를 생략 가능
- 상속 관계에서 첫줄에 super(...)를 생략하면 자바는 부모의 기본 생성자를 호출하는 super()를 자동으로 만들어 줌
- 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능을 제공
클래스 C
package extends1.super2;
public class ClassC extends ClassB {
public ClassC() {
super(10, 20); // 기본 생성자가 없으면 직접 호출
System.out.println("ClassC 생성자");
}
}
- ClassC는 ClassB를 상속 받음, ClassB에는 두 생성자가 있음
- ClassB(int a)
- ClassB(int a, int b)
- 생성자는 하나만 호출할 수 있다, 두 생성자 중에 하나를 선택하면 된다.
- super(10, 20)를 통해 부모 클래스의 ClassB(int a, int b) 생성자를 선택
- 참고로 ClassC의 부모인 ClassB에는 기본 생성자가 없다, 따라서 부모의 기본 생성자를 호출하는 super( )를 사용하거나 생략할 수 없음
package extends1.super2;
public class Super2Main {
public static void main(String[] args) {
ClassC classC = new ClassC();
}
}
실행하면 ClassA → ClassB → ClassC 순서로 실행 됨. 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행되어 하나씩 아래로 내려오는 것(초기화는 최상위 부모부터 이루어짐, 자식 생성자의 첫줄에서 부모의 생성자를 호출해야 하기 때문).
1~3까지의 과정
new ClassC( )를 통해 classC 인스턴스를 생성.
이때 ClassC( )의 생성자가 먼저 호출되는 것이 맞지만, ClassC( )의 생성자는 가장 먼저 super(...)를 통해 ClassB(...)의 생성자를 호출, ClassB( )의 생성자도 부모인 ClassA( )의 생성자를 가장 먼저 호출함.
4~6까지의 과정
- ClassA( )의 생성자는 최상위 부모, 생성자 코드를 실행하면서 print문의 내용을 출력, 생성자 호출이 끝나면 ClassA( )를 호출한 ClassB(...) 생성자로 제어권이 돌아감
- ClassB(...) 생성자가 코드를 실행하면서 print문의 내용을 출력, 생성자 호출이 끝나면 ClassB(...)를 호출한 ClassC( )의 생성자로 제어권이 돌아감
- ClassC( )가 마지막으로 생성자 코드를 실행하면서 print문의 내용을 출력함.
정리
- 상속 관계의 호출은 결과적으로 부모에서 자식 순서로 실행(부모의 데이터를 먼저 초기화 그 다음에 자식의 데이터를 초기화)
- 상속 관계에서 자식 클래스의 생성자 첫줄에 반드시 super(...)를 호출해야 한다. 단 기본 생성자(super( ))인 경우 생략 가능
this(...)와 함께 사용
코드 첫줄에 this(...)를 사용하더라도 반드시 한 번은 super(...)를 호출해야 한다.
package extends1.super2;
public class ClassB extends ClassA {
public ClassB(int a) {
this(a, 0); //기본 생성자 생략 가능
System.out.println("ClassB 생성자 a=" + a);
}
public ClassB(int a, int b) {
super(); //기본 생성자 생략 가능
System.out.println("ClassB 생성자 a=" + a + " b=" + b);
}
}
package extends1.super2;
public class Super2Main {
public static void main(String[] args) {
//ClassC classC = new ClassC();
ClassB classB = new ClassB(100);
}
}
문제 풀이
1. 상속 관계 상품
쇼핑몰의 판매 상품을 만들기
- Book, Album, Movie 3가지 상품을 클래스로 만들어야 함
- 코드 중복이 없게 상속 관계를 사용, 부모 클래스는 Item이라는 이름을 사용
- 공통 속성 : name, price
- Book : 저자(author), isbn(isbn)
- Album : 아티스트(artist)
- Movie : 감독(director), 배우(actor)
다음 코드를 참고해서 Item, Book, Album, Movie 클래스 제작
package extends1.ex;
public class ShopMain {
public static void main(String[] args) {
Book book = new Book("JAVA", 10000, "han", "12345");
Album album = new Album("앨범1", 15000, "seo");
Movie movie = new Movie("영화1", 18000, "감독1", "배우1");
book.print();
album.print();
movie.pritn();
int sum = book.getPrice() + album.getPrice() + movie.getPrice();
System.out.println("상품 가격의 합: " + sum);
}
}
실행 결과
이름:JAVA, 가격:10000
- 저자:han, isbn:12345
이름:앨범1, 가격:15000
- 아티스트:seo
이름:영화1, 가격:18000
- 감독:감독1, 배우:배우1
상품 가격의 합: 43000
Item 클래스
package extends1.ex;
public class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public void print() {
System.out.println("이름:" + name + ", 가격:" + price);
}
public int getPrice() {
return price;
}
}
Book 클래스
package extends1.ex;
public class Book extends Item{
private String author;
private String isbn;
public Book(String name, int price, String author, String isbn) {
super(name, price);
this.author = author;
this.isbn = isbn;
}
@Override
public void print() {
super.print();
System.out.println("- 저자:" + author + ", isbn:" + isbn);
}
}
Album 클래스
package extends1.ex;
public class Album extends Item{
private String artist;
public Album(String name, int price, String artist) {
super(name, price);
this.artist = artist;
}
@Override
public void print() {
super.print();
System.out.println("- 아티스트:" + artist);
}
}
movie 클래스
package extends1.ex;
public class Movie extends Item{
private String director;
private String actor;
public Movie(String name, int price, String director, String actor) {
super(name, price);
this.director = director;
this.actor = actor;
}
@Override
public void print() {
super.print();
System.out.println("- 감독:" + director + ", 배우:" + actor);
}
}
- 실행 결과를 맞추기 위해 @Override 실시
- 이름과 가격을 먼저 출력하기 위해 super.print( )로 Item에 있는 print( )가 먼저 나오게 한다.
정리
클래스와 메서드에 사용되는 final
클래스에 final
- 상속 끝!
- final로 선언된 클래스는 확장될 수 없다. 다른 클래스가 final로 선언된 클래스를 상속받을 수 없다.
- 예: public final class MyFinalClass {...}
메서드에 final
- 오버라이딩 끝!
- final로 선언된 메서드는 오버라이드 될 수 없다. 상속받은 서브 클래스에서 이 메서드를 변경할 수 없다.
- 예: public final void myFinalMethod() {...}
'항해 99 > Java' 카테고리의 다른 글
연산자, 조건문, 반복문, 배열 (1) | 2024.02.06 |
---|---|
Java 기초 개념 (0) | 2024.02.05 |
Java 기초 11 - 자바 메모리 구조와 Static (1) | 2024.01.29 |
Java 기초 10 - 패키지, 접근 제어자 (2) | 2024.01.29 |
Java 기초 9 - 생성자 (1) | 2024.01.25 |