본문 바로가기

Java/생활코딩

자바(Java) 개념 정리 - 상속 #17

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
}
public class InheritanceApp {
       public static void main(String[] args) {
              Cal c = new Cal();
              System.out.println(c.sum(2, 1));
       }
}

위와 같은 계산기 클래스가 있다. 만일 sum 뿐만 아니라 minus 메소드를 만들고 싶을 때, 아래와 같이 만들어줘야 한다. Cal 클래스를 복제해서 minus 메소드를 추가한 Cal2이다.

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
}
class Cal2{
       public int minus(int v1, int v2) {
              return v1 - v2;
       }
}

자 그런데, 이상태로 만족하지 못하는 경우가 있다. 만약 오리지널 sum(Cal) 메소드의 버그가 있어서 수정을 하려고 할 때, sum(Cal2)도 같은 내용이니깐 같이 수정이 되었으면 한다. 하지만 일일이 작업을 해야 한다. 한 번의 수정으로 같은 메소드의 오류를 수정할 수 있을까? 중복으로 인한 고통을 받고 싶지 않다.

 

그러면 다음으로 상속하는 방법이다.

 

Cal3라는 클래스가 Cal을 상속하는 코드는 extends를 쓴다. Cal 클래스를 확장(상속)한다는 뜻이다. Cal3는 Cal 클래스를 확장해서 Cal 클래스가 가지고 있는 모든 method와 변수를 상속받게 되는 클래스이다. 

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
}
class Cal3 extends Cal {
       
}

public class InheritanceApp {
       public static void main(String[] args) {
              Cal c = new Cal();
              System.out.println(c.sum(2, 1));
              
              Cal3 c3 = new Cal3();
              System.out.println(c3.sum(1, 3));
       }
}

실행했을 때, Cal3에도 sum 메소드가 포함되어 있는 것을 알 수 있다.


기능의 개선과 발전

 

여기서 Cal3는 Cal을 확장하기만 할 뿐 아무런 기능을 추가하지 못했다. 그러면 부모 클래스가 가지고 있는 기능에서 추가를 할 껀지, 기능을 덮어쓰기 할 건지 결정을 해줘야 한다.

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
}
class Cal3 extends Cal {
    public int minus (int v1, int v2) {
    return v1 + v2;
}
}

public class InheritanceApp {
       public static void main(String[] args) {
              Cal c = new Cal();
              System.out.println(c.sum(2, 1));
              
              Cal3 c3 = new Cal3();
              System.out.println(c3.sum(1, 3));
              System.out.println(c3.minus(4, 1));

다음과 같이 Cal3 클래스에 minus 메소드를 추가할 수 있다. 혹시나 sum 메소드가 마음에 들지 않으면 수정할 수 있을까?

 

자, 다음과 같이 자식 클래스에서 메소드를 수정해보자.

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
}
class Cal3 extends Cal {
       public int sum (int v1, int v2) {
              System.out.println("Cal3!!");
              return v1 + v2;
       }
}

이제 둘의 sum 메소드는 분명히 차이가 난다. 이를 출력하면 다음과 같은 결과가 나온다.

public class InheritanceApp {
       public static void main(String[] args) {

              Cal3 c3 = new Cal3();

              System.out.println(c.sum(1, 3));
              System.out.println(c3.minus(4, 1));
              System.out.println(c3.sum(1, 3));
       }
}

c.sum과 c3.sum의 입력값은 같지만 결과는

4
5
Cal3!!
4

이렇게 둘이 다르게 나타난다. 이는 부모가 갖고 있는 기능을 덮어썼다. 올라탔다. 라는 개념이라 오버라이딩(Overriding)이라 불린다.

 

오버라이딩(Overriding) : 부모 클래스의 메소드에 기능을 추가하는 것.


Overriding vs Overloading

 

둘의 개념을 비교해보자.

 

Override : 올라타다.

 

Overload : 과적하다. 너무 많이 탑재하다.

 

자바는 같은 이름의 메소드 여러개를 과적할 수 있다.

다음과 같이 같은 이름의 같은 내용의 메소드는 에러가 난다. 하지만

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
       public int sum (int v1, int v2, int v3) {
              return v1 + v2 + v3;
       }
}

 

이렇게 새로운 변수를 추가하면 같은 이름의 메소드라 할지라도 오류가 나지 않는다.

 

그럼 이 클래스 메소드를 호출하려 하면 어떻게 될까?

              System.out.println(c.sum(1, 3, 4));

이렇게 하면 입력값이 정수이면서 3개인 두 번째 sum 메소드를 호출하게 되는 것이다. 이것이 오버로딩(Overloading)이다. 이 새로운 sum 메소드는 메소드이기 때문에 자식 클래스만이 소유할 수 있다. 하지만 이는 상속과는 무관한 개념이다.


this & super

 

다시 한 번 생각해보자.

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
}

이 클래스에서

class Cal3 extends Cal {
    public int minus (int v1, int v2) {
    return v1 + v2;
}
    // Overriding
       public int sum (int v1, int v2) {
              System.out.println("Cal3!!");
              return v1 + v2;
       }
}

Cal3 클래스가 Cal 클래스를 상속받아 sum 메소드를 오버라이딩을 했다. 하지만 sum 메소드의 내용을 그대로 복사하는 것은 중복이다. 만일 그 내용이 엄청나게 긴 내용일 때, 코드는 복잡해지고 오류가 발생할 가능성이 높아진다. 위의 메소드를 계승받아 발전시킬 수 있을까?

       public int sum (int v1, int v2) {
              System.out.println("Cal3!!");
              return super.sum(v1, v2);
       }

이렇게 super라고 작성해주면 이 클래스의 부모(super)클래스의 sum 메소드를 가르키게 된다. 

 

반면에, this는 자기자신을 가르킨다. 특히나 인스턴스를 가르킨다.

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
       public int sum (int v1, int v2, int v3) {
              return v1 + v2 + v3;
       }

이 개념을

class Cal{
       public int sum (int v1, int v2) {
              return v1 + v2;
       }
       public int sum (int v1, int v2, int v3) {
              return this.sum(v1, v2) + v3;
       }
}

이렇게 바꿔줄 수 있다. 해당 sum은 Cal클래스가 가지고 있는 sum을 가져다 쓴 것임을 지칭하고 있다.


상속과 생성자

 

코드를 다음과 같이 초기화한다.

 

class Cal{

}
class Cal3 extends Cal {
}
public class InheritanceApp {
       public static void main(String[] args) {
              Cal c = new Cal();
              Cal c3 = new Cal3();
       }
}

새로운 클래스를 만들어주자.

class Cal{
       int v1, v2;
       Cal(int v1, int v2) {
              System.out.println("Cal init!!");
              this.v1 = v1; this.v2 = v2;
       }
}

Cal 안에 생성자를 지정해준다. 그리고 this를 써주면 해당 인스턴스의 매개변수를 사용할 수 있게 된다.

하지만 다음과 같은 오류가 나타난다. 우리가 Cal을 제대로 계승했다면 Cal이 인스턴스로 만들어질 때 반드시 해야하는 일들이 생성자에 들어있다. 그럼 그 생성자를 Cal3도 제대로 실행을 시켜야한다. 그래서 자바는 상속받은 부모의 생성자가 있으면 자식 클래스는 반드시 부모 생성자를 실행시키도록 강제하고 있다. 그래서 추천 받은 해결법(Create constructor matiching super)를 클릭하면 다음과 같이 나타난다.

class Cal3 extends Cal {
       Cal3(int v1, int v2) {
              super(v1, v2);
}

여기서 super는 부모 클래스이다. 즉, 부모클래스의 생성자이다. 그리고 Cal3만의 메소드로 오버라이딩하려고 한다면

class Cal3 extends Cal {
       Cal3(int v1, int v2) {
              super(v1, v2);
              System.out.println("Cal3 init!!");
       }
}

이렇게 바꿀 수 있다. 

 

즉, 생성자가 있는 클래스를 상속받았다면 부모 클래스를 반드시 호출해야 한다.

class Cal{
       int v1, v2;
       Cal(int v1, int v2) {
              System.out.println("Cal init!!");
              this.v1 = v1; this.v2 = v2;
       }
}
class Cal3 extends Cal {
       Cal3(int v1, int v2) {
              super(v1, v2);
              System.out.println("Cal3 init!!");
       }
}
public class InheritanceApp {
       public static void main(String[] args) {
              Cal c = new Cal(2, 1);
              Cal c3 = new Cal3(2, 1);
       }
}

출력값은 

Cal init!!
Cal init!!
Cal3 init!!

위와 같이 나타난다.

class Cal{
       int v1, v2;
       Cal(int v1, int v2) {
              System.out.println("Cal init!!");
              this.v1 = v1; this.v2 = v2;
       }
       public int sum() {return this.v1+v2;}
}
class Cal3 extends Cal {
       Cal3(int v1, int v2) {
              super(v1, v2);
              System.out.println("Cal3 init!!");
       }
       public int minus() {return this.v1-v2;}
}
public class InheritanceApp {
       public static void main(String[] args) {
              Cal c = new Cal(2, 1);
              Cal3 c3 = new Cal3(2, 1);
              
              System.out.println(c3.sum());
              System.out.println(c3.minus());
       }
}

정리

 

상속을 하면 기능이 급격하게 늘어난다. 그렇게 되면 클래스간에 호환성이 떨어지고 클래스를 다른 클래스로 교체하는 것이 어려워진다. 이런 맥락에서 자식 클래스를 부모클래스로서 동작하도록 규제하는 것이 '다형성'이다.

 

Polymorphism : 다형성

Parent obj = new ChildClass()

 

public default protected private 이런 키워드들을 접근 제어자(Access Modifiers)라고 부른다.

 

접근 제어자는 클래스, 메소드, 변수를 사용자가 아무거나 건들지 못하게 제한하는 기능이다. 사용자에게 제공하려는 조작장치만 손 델 수 있도록 하는 것이다.

 

Final

 

내가 만든 클래스를 더이상 다른 사람이 상속하지 못하게 만들고 싶을때, 또 메소드를 오버라이딩 하지 못하게, 변수를 마음대로 변경하지 못하게 하는 방법이다.

 

Abstract

 

Abstract Class Incompletion

 

클래스를 상속해서 사용하려는 사람들에게 특정 메소드를 꼭 구현하라고 강제할 수 있는 메소드이다.