상속
상속
- 기존 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소
- 두 클래스를 상위 클래스와 하위 클래스로 나누어 상위 클래스의 멤버를 하위 클래스와 공유하는 것을 의미
- 위 두 클래스를 서로 상속 관계에 있다고 하며, 하위 클래스는 상위 클래스가 가진 모든 멤버를 상속받게 됨
- 하위 클래스는 상위 클래스의 멤버를 상속받기 때문에 하위 클래스의 멤버 개수는 항상 상위 클래스의 멤버 개수보다 같거나 많음
- "~클래스로부터 확장되었다" -> 두 클래스 간 상속 관계를 설정할 때 사용하는 extends 키워드 자체가 확장한다는 의미를 갖고 있음
- 자바의 객체지향 프로그래밍에서는 단일 상속만을 허용함
- 다중 상속은 허용되지 않음
- 인터페이스를 통해 다중 상속과 비슷한 효과를 낼 수 있음
상속의 사용 이유
- 상속을 통해 클래스를 작성하면 코드를 재사용하여 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있어 코드의 중복을 제거할 수 있다!
- 다형적 표현이 가능하다!
- 다형성
- 하나의 객체가 여러 모양으로 표현될 수 있는 성질
포함 관계
- 포함(composite)
- 상속처럼 클래스를 재사용할 수 있는 방법
- 클래스의 멤버로 다른 클래스 타입의 참조 변수를 선언하는 것
public class Person {
String name;
int age;
Address address;
public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
public String toString() {
return String.format("이름 : %s\n나이 : %d\n주소 : %s", name, age, address.toString());
}
class Address {
String country, city, street;
public Address(String country, String city, String street) {
this.country = country;
this.city = city;
this.street = street;
}
public String toString() {
return country + " " + city + " " + street;
}
}
}
- Address 클래스로 변수들을 묶어준 후에 Person 클래스에 참조변수를 선언하여 코드의 중복을 없애고 포함관계로 재사용하고 있다!
포함 관계 vs 상속
- 상속
- 클래스 간의 관계가 '~은 ~이다(IS-A)' 관계
- Ex. 고양이는 동물이다
- 포함 관계
- 클래스 간의 관계가 '~은 ~을 갖고 있다(HAS-A)' 관계
- Ex. 사람은 주소를 갖고 있다
메서드 오버라이딩
메서드 오버라이딩(Overriding)
- 상위 클래스로부터 상속받은 메서드와 동일한 이름의 메서드를 재정의
- 상위 클래스에 정의된 메서드를 하위 클래스에서 자신의 동작에 맞게 변경하고자 할 때 사용
class Animal {
void howl() {
System.out.println("Howl...");
}
}
class Dog extends Animal {
void howl() {
System.out.println("멍멍");
}
}
class Wolf extends Animal {
void howl() {
System.out.println("아우우~");
}
}
- Animal 클래스의 howl 메서드를 Animal 클래스를 상속받은 Dog, Wolf 클래스 각각에서 오버라이딩 하는 예시
- 만약 Dog나 Wolf의 인스턴스를 통해 howl 메서드를 호출하면 Animal의 howl 메서드가 아닌 Dog나 Wolf의 howl 메서드가 호출된다
메서드 오버라이딩의 조건
- 메서드 선언부가 상위 클래스의 것과 같아야 한다
- 접근 제어자의 범위가 상위 클래스의 메서드보다 같거나 넓어야 한다
- 예외는 상위 클래스의 메서드보다 많이 선언할 수 없다
- 상위 클래스의 메서드에서 throws를 선언했더라도 하위 클래스는 throws를 처리하지 않을 수 있다
- 하위 클래스는 상위 클래스의 메서드에서 throws하는 예외와 같은 예외를 throws할 수 있다
- 1, 2번 규칙을 통해 하위 클래스에서는 상위 클래스보다 상위 예외를 throws 할 수 없음을 알 수 있다
- 하위 클래스는 상위 클래스의 메서드에서 throws하는 예외의 하위 예외만 throws할 수 있다
- 하위 클래스는 Runtime 예외를 상위 클래스의 메서드와 상관없이 throws할 수 있다
메서드 오버라이딩 장점
- 같은 기능을 구현한 하위 클래스에서 동일한 메서드를 사용함으로써 코드의 일관성을 제공할 수 있다
super 및 super()
- 공통적으로 모두 상위 클래스가 존재함을 가정하고 상속 관계를 전제로 한다
super
class Laptop {
String model = "노트북";
String brand = "브랜드";
}
class GalaxyBook extends Laptop {
String model = "갤럭시북";
boolean calledBixby;
GalaxyBook() {
super.model = model;
super.brand = "삼성";
}
}
class MacBook extends Laptop {
String model = "맥북";
boolean calledSiri;
MacBook() {
super.model = model;
super.brand = "애플";
}
}
- 상위 클래스의 변수를 참조해야하는 경우가 존재하는데 그 경우 super 키워드를 사용하여 부모의 객체의 멤버값을 참고할 수 있다
- 위 예시처럼 상위 클래스와 하위 클래스에 같은 이름의 변수가 존재할 때, super 키워드를 통해 구분할 수 있다
- 만약 super 키워드를 붙여주지 않는다면, 컴파일러는 자신이 속한 인스턴스 객체의 멤버를 먼저 참조한다
super()
class Laptop {
String model = "노트북";
String brand = "브랜드";
public Laptop() {
System.out.println("Laptop 생성자");
}
}
class GalaxyBook extends Laptop {
String model = "갤럭시북";
boolean calledBixby;
GalaxyBook() {
super();
System.out.println("GalaxyBook 생성자");
super.model = model;
super.brand = "삼성";
}
}
class MacBook extends Laptop {
String model = "맥북";
boolean calledSiri;
MacBook() {
super();
System.out.println("MacBook 생성자");
super.model = model;
super.brand = "애플";
}
}
- Laptop 클래스를 확장하여 GalaxyBook, MacBook 클래스를 생성하고 GalaxyBook, MacBook 생성자를 통해 상위 클래스의 생성자를 super()를 통해 호출
- super() 메서드는 생성자 안에서만 사용 가능하고 반드시 첫 줄에 위치해야 한다!
- 모든 생성자의 첫 줄에는 반드시 this() 또는 super()가 선언되어야 한다!
- 만약 super()가 없다면 컴파일러가 자동으로 생성자의 첫 줄에 super()를 삽입
- 상위 클래스에 기본 생성자가 없으면 에러가 발생한다
- 왜 생성자의 첫 줄에 존재해야 하는가?
- 하위 클래스의 멤버가 상위 클래스의 멤버를 사용할 수도 있는데, 이 때 조상의 멤버들이 먼저 초기화되어있지 않으면 안되기 때문!
- 상위 클래스 생성자의 호출은 클래스의 상속관계를 거슬러 올라가며 반복된다
- 모든 클래스의 최고 조상인 Object 클래스의 생성자인 Object()까지 가서야 끝난다
Object 클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다! 만약 개발자가 추가하지 않는다면 컴파일러가 자동으로 첫 줄에 super()를 추가한다!
Object 클래스
- 자바의 클래스 상속계층도에서 최상위에 위치한 최상위 클래스
- 자바의 모든 클래스는 Object 클래스로부터 확장된다
- 컴파일러는 컴파일 과정에서 다른 클래스로부터 상속 받지 않는 클래스에 자동으로 extends Object를 추가하여 Object 클래스를 상속받도록 한다
캡슐화
- 특정 객체 안에 관련된 속성 및 기능을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것
캡슐화를 하는 이유
- 데이터 보호의 목적
- 내부적으로만 사용되는 데이터에 대한 불필요한 외부 노출을 방지
캡슐화의 장점
- 정보 은닉(data hiding)
- 외부로부터 객체의 속성 및 기능이 함부로 변경되지 못하게 막는다
- 데이터가 변경되더라도 다른 객체에 영향을 주지 않기에 독립성을 확보한다
- 유지보수 및 코드 확장 시에 오류의 범위를 최소화할 수 있어 코드 유지보수에 용이하다
패키지(Package)
- 특정한 목적을 공유하는 클래스와 인터페이스의 묶음
패키지의 장점
- 클래스를 그룹 단위로 묶어 효과적으로 관리
- 패키지는 물리적인 하나의 디렉토리
- 디렉토리는 하나의 계층구조를 가지는데, 계층 구조 간 구분은 점(.)을 통해 표현
- 클래스의 충돌을 방지
- 같은 이름의 클래스를 가지더라도 다른 패키지에 존재한다면 이름명으로 인한 충돌을 방지할 수 있다
- 패키지가 있다면 소스 코드의 첫 번째 줄에 package 패키지명 으로 패키지를 표시
import문
- 다른 패키지 내의 클래스를 사용하기 위해 작성
import 패키지명.클래스명; 또는 import 패키지명.*;
- 같은 패키지에서 여러 클래스가 사용될 때는 import 패키지명.*로 작성하여 해당 패키지의 모든 클래스를 패키지명 없이 사용
- import문은 컴파일 시에 처리되므로 프로그램 성능에 영향을 주지 않는다
package package1;
public class Address {
public String country, city, street;
public void print() {
System.out.println(country + " " + city + " " + street);
}
}
package package2;
public class Person {
public static void main(String[] args) {
package1.Address address = new package1.Address();
}
}
- 만약 다른 패키지에 존재하는 클래스를 사용할 때, import문을 사용하지 않는다면 다른 패키지에 존재하는 클래스를 사용할 때 매번 패키지명을 붙여야만 한다
- import문을 통해 컴파일러에게 사전에 소스파일에 사용된 클래스에 대한 정보를 제공
접근 제어자
제어자(Modifier)
- 클래스, 필드, 메서드, 생성자 등에 부가적인 의미를 부여하는 키워드
- 접근 제어자와 기타 제어자로 구분할 수 있다
- 접근 제어자
- public, protected, default, private
- 기타 제어자
- static, abstract, final, native, transient, synchronized 등
- 하나의 대상에 여러 제어자를 사용할 수 있다
- 각 대상에 대해 접근 제어자는 한 번만 사용할 수 있다
접근 제어자(Access Modifier)
- 캡슐화를 구현하기 위한 핵심적인 방법
- 접근 제어자를 통해 클래스 외부로의 불필요한 데이터 노출을 방지하고 외부로부터 임의로 변경되지 않도록 막을 수 있다
접근 제어자 | 접근 제한 범위 |
---|
private | 동일 클래스에서만 접근 가능 |
default | 동일 패키지 내에서만 접근 가능 |
protected | 동일 패키지 및 다른 패키지의 하위 클래스에서 접근 가능 |
public | 접근 제한 없음 |
- 접근 제한 범위
- public > protected > default > private
- default
- 아무런 접근 제어자를 붙이지 않은 경우 기본적인 설정을 의미
getter와 setter
getter와 setter
- 캡슐화 목적 달성을 위해 private과 같은 접근 제어자를 이용하기도 하는데 이럴 경우 데이터 변경을 할 수 없는 경우가 생김
- 이러한 경우 getter와 setter 메서드를 사용
public class Person {
private String name;
private int age;
private String residentRegistrationNumber;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getResidentRegistrationNumber() {
return residentRegistrationNumber;
}
public void setResidentRegistrationNumber(String residentRegistrationNumber) {
this.residentRegistrationNumber = residentRegistrationNumber;
}
}
- setter 메서드
- 외부에서 메서드에 접근하여 조건에 맞을 경우 데이터 값을 변경 가능하게 해준다
- 일반적으로 메서드명 앞에 set-을 붙여 정의한다
- getter 메서드
- 설정한 변수 값을 읽어오는데 사용하는 메서드
- 객체 외부에서 필드값을 사용하기에 부적절한 경우가 있는데, 이런 경우 그 값을 가공해서 외부로 전달하는 역할을 하기도 한다
- 일반적으로 메서드명 앞에 get-을 붙여 정의한다
getter와 setter 메서드 사용 이유
- 데이터를 효과적으로 보호하면서 의도하는 값으로 값을 변경하여 캡슐화를 보다 효과적으로 달성할 수 있다