기존의 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것을 의미하며 상속은 캡슐화, 추상화와 더불어 객체 지향 프로그래밍을 구성하는 중요한 특징 중 하나이다. extends
예약어를 통해 상속을 받게 되면 부모 클래스에서 정의한 모든 필드와 메소드를 물려받아 자식 클래스에서 사용이 가능하다.
상속을 하더라도 예외가 존재하는데 부모 클래스의 private 접근 제한을 갖는 필드 및 메소드는 자식이 물려받을 수 없다.
부모와 자식 클래스가 서로 다른 패키지에 있다면, 부모의 default 접근 제한을 갖는 필드 및 메소드도 자식이 물려받을 수 없다 default 접근 제한은 ‘같은 패키지에 있는 클래스’만 접근이 가능하게 제한하기 때문이다. 이외의 경우는 모두 상속의 대상이 된다.
상속은 단일 상속만 가능하다, 즉 extend 부모 클래스1, 부모 클래스2 이런 식으로 다중 상속이 불가능하다. 부모 클래스가 하나일 필요는 없다 부모 클래스의 부모 클래스가 있다면 자식 클래스는 모든 부모 클래스의 필드와 메소드를 전부 상속 받는다. 만약 부모 클래스가 없을 경우 모든 클래스는 상속계층도 최상위 클래스인 Object 클래스를 컴파일러에 의해 자동으로 상속받는다, 부모 클래스가 있을 경우에는 부모 클래스가 Object 클래스를 상속 받기 때문에 자식 클래스에서도 Object 클래스를 상속 받는다.
자바에서는 자식 객체를 생성하면, 부모 객체를 먼저 생성한 후, 자식 객체가 그 다음에 생성 한다. 아래의 코드 main클래스에서 부모 객체를 생성하지 않았지만 내부적으로는 Parent 객체를 생성하고 Child 객체를 생성한다. 객체는 생성자를 호출해야만 생성되는데,부모 객체를 생성할 때 부모 생성자를 어디서 호출할까? 부모 클래스(Parent)는 명시적 생성자 선언이 없고, 자식 클래스(Child)는 명시적 생성자 선언이 존재한다, 그러면 부모 클래스의 기본 생성자 선언은 자식클래스의 생성자 첫줄에 super(); 라고 자동으로 생성되어 호출한다. 또한 부모 클래스 에서 명시적 생성자가 있을 경우 super()를 통하여 호출하지 않으면 컴파일 에러가 발생하기에 자식 생성자 첫줄에 super() 를 써서 호출 해주어야 한다.
super : 부모 클래스 객체를 지칭, 부모 클래스의 멤버나 메소드에 접근시 사용
super() : 부모 클래스의 생성자를 호출
부모 클래스에서 정의한 메소드를 자식 클래스에서 재정의 하는 것을 오버라이딩
이라 하며 몇가지 조건이 존재한다.
public class Parent{
String name;
int age;
public void Print(){
System.out.println("이름과 나이 : "+name+" "+age);
}
public class Child extends Parent{
Child (String name, int age){
this.name = name;
this.age = age;
}
public static void main (String[] args){
Child child = new Child("홍길동", 20);
Child.Print();
}
연관된 목적을 가지는 변수와 함수를 하나의 클래스로 묶어 외부에서 쉽게 접근하지 못하도록 은닉하는 것을 뜻하며, 중요한 데이터를 쉽게 바꾸지 못하도록 정보 은닉을 위해 사용한다.
캡슐화를 통해 외부에서 내부의 정보에 접근하거나 변경할 수 없게 막고 객체가 제공하는 필드와 메소드를 통해서만 접근이 가능하다. 접근을 제한함으로써 유지보수나 확장 시 오류의 범위를 최소화할 수 있고 객체 내의 정보 손상과 오용을 방지하고 데이터가 변경되어도 다른 객체에 영향을 주지 않아 독립성이 좋다. 캡슐화는 접근제어자를 통해 이루어진다.
접근 제어자
public : 접근 제한 없음
protected: 동일한 패키지 내에서 또는 다른 패키지의 자식 클래스에서만 접근이 가능
default : 접근 제한자를 명시하지 않으면 default 값이 되며, 동일한 패키지 내에서만 접근 가능
private: 자기 자신의 클래스 내에서만 접근 가능
아래 코드를 보면 필드의 변수에 private 접근제어자를 선언하여 member 클래스 내에서가 아닌외부에서는 직접적인 접근이 불가능하며 getter와 setter를 통해서만 접근이 가능하다
public class member {
private String id;
private String pw;
private int age;
//getter
public String getId() {
return id;
}
public String getPw() {
return pw;
}
public int getAge() {
return age;
}
//setter
public void setId(String id) {
this.id = id;
}
public void setPw(String pw) {
this.pw = pw;
}
public void setAge(int age) {
this.age = age;
}
}
캐스팅(casting)이란 타입을 변환하는 것을 말하며 형변환이라고도 한다. 상속 관계에 있는 부모 자식 클래스 간에는 서로 간의 형변환이 가능하다.
업캐스팅 : 자식 클래스 -> 부모 클래스 (형변환 연산자 생략 가능)
다운캐스팅 : 부모 클래스 -> 자식 클래스 (형변환 연산자 생략 불가)
다형성의 이점
여러 객체를 하나의 타입으로 관리가 가능하기 때문에 코드 관리가 편리해 유지보수가 용이함
객체를 재사용하기 쉬워지기 때문에 개발자의 코드 재사용성이 높아짐
클래스간 의존성이 줄어들며 확장성이 높고 결합도가 낮아져 안전성이 높아짐
다형성 필수 조건
상속 관계
다형성을 활용하기 위해서는 필수로 부모-자식 간 클래스 상속이 이루어져야 한다.
오버라이딩 필수 (자식 클래스에서 메소드 재정의)
다형성이 보장되기 위해서는 하위 클래스 메소드가 반드시 재정의되어 있어야 한다.
업캐스팅 (자식 클래스의 객체가 부모 클래스 타입으로 형변환 되는 것)
부모 타입으로 자식클래스를 업캐스팅하여 객체를 생성해야 한다.
public class Person{
public void Print(){
System.out.println("hi~");
}
}
public class Singer extends Parent{
@Override
public void Print(){
System.out.println("La La~");
}
}
public class Dancer extends Parent{
@Override
public void Print(){
System.out.println("둠칫 둠칫~");
}
}
public class Main{
public static void main (String[] args){
Person p1 = new Singer(); //자식 클래스 업캐스팅
p1.print(); // Singer 클래스에서 오버라이딩한 La La~ 출력
p1 = new Dancer();
p1.print(); // Dancer 클래스에서 오버라이딩한 둠칫 둠칫~ 출력
}
}
추상클래스
하나 이상의 추상 메소드를 포함하는 클래스를 가리켜 추상 클래스라고 한다. 반드시 사용되어야 하는 메소드를 추상 클래스에 추상 메소드로 선언해 놓으면 이 클래스를 상속받는 모든 클래스에서는 해당 추상 메소드를 반드시 오버라이딩 해야하는 강제성을 부여한다.
추상 클래스는 인스턴스를 생성할 수 없고 반드시 상속받아 구현한다. 추상 메소드는 선언부만 작성하고 구현부는 구현하는 클래스에서 작성하도록 비워둔다. 생성자, 필드, 일반 메소드도 작성이 가능하다.
abstract class Animal { abstract void cry(); }
class Cat extends Animal { void cry() { System.out.println("냐옹냐옹~"); } }
class Dog extends Animal { void cry() { System.out.println("멍멍!"); } }
public class Main {
public static void main(String[] args) {
// Animal a = new Animal(); // 추상 클래스는 인스턴스를 생성할 수 없음.
Cat c = new Cat();
Dog d = new Dog();
c.cry();
d.cry();
}
}
인터페이스
극단적으로 동일한 목적 하에 동일한 기능을 수행하게끔 강제하는 것이 바로 인터페이스의 역할이자 개념이다. 다형성을 극대화하여 개발코드 수정을 줄이고, 구현 클래스보다 인터페이스에 의존하게 하여 클래스간의 결합도를 낮추어 프로그램 유지보수성을 높이기 위해 사용한다.
모든 필드가 public static final로 정의되고, 모든 메서드가 public abstract 로 정의된다.(static과 default 메서드 제외) 오로지 추상 메소드와 상수만을 포함할 수 있다.
인터페이스 끼리 상속이 가능하며 상속관계가 있는 인터페이스를 구현하는 클래스에서는 부모 자식 인터페이스의 추상 메소드를 모두 오버라이딩 해야한다.
interface Hunting { public abstract void hunt(); }
interface Animal extends Hunting { public abstract void cry(); }
class Cat implements Animal {
public void cry() {
System.out.println("냐옹냐옹~");
}
public void hunt() {
System.out.println("물고기 사냥!");
}
}
class Dog implements Animal {
public void cry() {
System.out.println("멍멍!");
}
public void hunt() {
System.out.println("토끼 사냥!");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Cat();
animal.cry(); // 냐옹냐옹~ 출력
animal.hunt(); // 물고기 사냥! 출력
animal = new Dog();
animal.cry(); // 멍멍! 출력
animal.hunt(); // 토끼 사냥! 출력
}
}