Post

기강 자바-04





객체지향 프로그래밍

  • 이번에는 자바에서 지원하는 객체지향 프로그래밍 문법에 대해 알아본다.





상속

  • 자바의 상속이란,
    • 부모 클래스로부터 멤버 변수나 메서드 등의 성질을 물려받는 행위이다.
    • 자식 클래스는 부모 클래스의 기능을 그대려 물려받는 것은 물론, 확장 및 수정도 가능하다.



자바의 상속

  • 자바는 단일 상속을 지원한다.
    • 자식 클래스는 하나의 부모 클래스만 상속 받을 수 있다.
    • 어떤 부모 클래스로부터 상속을 받은 자식 클래스 역시 다른 클래스에 상속을 해줄 수 있다.
    • 부모 클래스는 하나 밖에 가지지 못하지만, 조상 클래스는 여러 개 가질 수 있다.
      • 조상 클래스는 부모 클래스의 부모 클래스와 같이 상속 관계에서 더 위에 있는 클래스들을 의미한다.
      • 후손 클래스는 모든 조상 클래스들의 성질들을 상속 받는다.
      • 따라서 단일 상속이어도 여러 개의 클래스를 상속 받을 수 있는 것이나 마찬가지이다.
  • 자바의 모든 클래스의 최상위 조상 클래스는 Object이다.
    • 소스 파일 상에서 아무 것도 상속 받지 않는 클래스의 경우에도 컴파일 시점에 Object를 상속 받도록 된다.
  • 어떤 클래스의 인스턴스를 생성할 때, 조상 클래스들의 생성자 함수를 차례차례 호출한다.
    • 조상 클래스의 생성자 함수는 super() 메서드로 호출할 수 있다.
    • 후손 클래스의 생성자 함수에서 조상 클래스의 생성자 함수를 명시적으로 적어줘야 한다.(기본 생성자 함수 제외)



  • 상속을 받아도 모든 성질을 다 물려받지는 않는다.
    • 상속 해주는 성질들
      • 멤버 변수, 메서드
      • 생성자 메서드
      • 인스턴스 내부 클래스
        • 상속은 인스턴스 레벨에서 이뤄지기 때문에, 인스턴스 내부 클래스만 상속 해준다.
      • 인터페이스 구현
        • 조상 클래스가 구현하고 있는 인터페이스를 후손 클래스도 구현하고 있는 것으로 간주된다.
    • 상속 해주지 않는 성질들
      • private인 멤버 변수, 메서드
      • default 접근 지시자이면서 후손 클래스와 패키지가 다른 경우의 멤버 변수, 메서드
      • static 변수, 메서드
        • 상속은 인스턴스 레벨에서 이뤄지는 것이다.
        • 따라서 클래스 레벨인 static 변수와 메서드는 상속되지 않는다.
        • 상속을 받는 부모-자식 관계여도 서로 다른 클래스니까..





코드로 짜보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Parent {
    public String firstName;
    public final String lastName = "Park";

    public Parent(String firstName) {
        this.firstName = firstName;
    }

    private void sayFirstName() {
        sout(this.firstName);
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
}

public class Child extends Parent {
    public int age;

    public Child(int age) {
        super("default first name.");  // 부모 클래스에 name을 받는 생성자 함수 밖에 없기 때문에 뭐라도 넣어줘야 함.

        this.age = age;
    }

    public Child(String firstName, int age) {
        super(firstName);

        this.age = age;
    }
}
  • 위는 실제 상속을 받는 클래스를 작성한 코드이다.
    • 부모 클래스인 Parent
    • 자식 클래스인 Child


  • Child는 부모 클래스인 Parent의 특성을 물려 받는다.
    • Parent의 firstName 멤버 필드를 Child도 갖는다.
    • Parent의 setFirstName() 메서드를 Child도 사용한다.
    • lastName을 Child도 갖지만, final 키워드가 붙어있기 때문에 변경은 불가능하다.
  • Child가 가질 수 없는 Parent의 특성도 있다.
    • private으로 설정된 sayFirstName() 메서드는 자식인 Child한테 물려주지 않는다.
    • package-private 접근 지시자 (= default)인 경우에도 같은 패키지가 아니라면 물려주지 않는다.


  • 자식 클래스는 생성 시에 부모 클래스의 생성자를 먼저 호출해야 한다.
    • 위 예시의 Parent는 String name을 파라미터로 갖는 생성자만을 가지고 있다.
    • 따라서 Child는 본인의 생성자 함수의 맨 처음에 Parent의 유일한 생성자인 name 생성자를 먼저 호출해야 한다.
    • 부모 생성자를 나타내는 super()를 통해 Parent의 생성자를 호출한 예시를 볼 수 있다.
      • super() 하나로 부모 클래스의 모든 생성자 함수에 접근할 수 있다.
      • 이는 메서드를 메서드 시그니처로 구분하기 때문에 가능한 것이다.
  • Default 생성자 함수인 경우에는 호출을 생략해도 된다.
    • 위의 경우엔 Parent가 Default 생성자 함수가 없기 때문에 명시적으로 호출 해주었다.
    • 만약 Parent에 Default 생성자 함수가 있다면, Child 생성자 함수에 따로 super()를 호출해주지 않아도 된다.
    • 우리가 아무것도 상속받지 않는(실제로는 Object를 상속 받는) 클래스의 생성자를 정의할 때, super()를 따로 정의하지 않아도 되는 이유이다.(Object는 디폴트 생성자 함수를 갖고 있기 때문에)





오버라이딩

  • 자식 클래스는 부모 클래스의 특성을 가진다.
    • 그대로 물려받지 않고 메서드의 재정의를 할 수도 있다.
    • 부모 클래스의 멤버 메서드를 재정의 하는 것을 오버라이딩(Overriding)이라고 한다.
  • @Override
    • 컴파일러에 해당 메서드가 오버라이딩 된 메서드라고 일러주는 메타 어노테이션이다.
    • 굳이 안 적는다고 컴파일 에러가 나지는 않지만,
      • 부모 메서드와 메서드 시그니처가 다른 경우 컴파일 에러로 알기 쉬움
      • 명시적으로 오버라이딩을 알리기 위함 등의 장점이 있다.
  • 접근 지시자에 규칙이 있는데
    • 접근 지시자가 범위가 넓은 것 => 범위가 좁은 것 순으로 있다고 할 때
      • public => protected => package-private(default) => private
    • 부모의 메서드를 오버라이드 할 때, 해당 메서드의 접근 지시자보다 좁은 범위로 변경이 불가능하다.
      • 예시로, 부모 메서드 protected이면,
      • public, protected로 오버라이드 가능
      • default, private 불가능



코드로 짜보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Parent {
    // 생략 ..

    public void method1() {
    }

    public void method2() {
    }

    public void method3() {
        sout("Hello");
    }
}

public class Child extends Parent {
    // 생략 ..

    public void method1() {     // Parent의 method1()을 재정의 하였다.
        sout("haha!!");         // @Override 어노테이션이 없지만 컴파일 에러는 없다.
    }

    public String method2() {   // 부모로 받은 method2()와 리턴 타입이 다르므로
        return "haha!!";        // Compile Error!!
    }

    @Override
    public void method3() {     // 부모 메서드를 오버라이딩(재정의) 하였다.
        sout("haha!!");         // 클래스가 Child 구현체를 갖는 경우에 Hello가 아닌 haha!!를 출력한다.
    }
}





추상 클래스

  • 여태는 모든게 구체적으로 구현된 클래스만을 사용해 왔지만, 자바에는 추상 클래스도 존재한다.
  • 추상 클래스란,
    • 추상 메서드를 가질 수 있는 클래스이다.
      • 구현된 메서드 역시 가질 수 있다.
      • 오버라이딩(재정의)도 가능하다.
    • 클래스처럼 독자적으로 생성하여 사용할 수 없고, 구현체가 필요하다.
      • 구현체 역할은 클래스만이 할 수 있다.
      • 여태 계속 만들어온 방식처럼 class를 따로 만들어 구현할 수 있으며,
      • 익명 클래스로 구현체를 구현할 수도 있다.
    • abstract 키워드로 추상 클래스를 만들 수 있다.
    • 추상 클래스 역시 클래스이다.
      • 자바의 단일 상속이란 특징 때문에 하나 밖에 상속 받을 수 없다.



추상 메서드

  • 추상 메서드란,
    • 구현체가 없는 메서드이다.
    • 사용하기 위해선 구현이 필수이다.
      • 클래스 내에서 재정의
      • 익명 클래스 내에서 재정의
  • 추상 메서드 역시 접근 지시자를 따른다.
    • 위의 오버라이딩에서 언급한 접근 지시자 규칙을 따른다.



코드로 짜보기 - 익명 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public abstract class Abstract1 {
}

public abstract class Abstract2 {
    public void sayHello() {  // 추상 메서드 뿐만 아니라, 구현된 메서드를 가질 수도 있다.
        sout("hello!!!");
    }

    public void sayHello(String greeting) {
        sout(greeting);       // 오버로딩도 가능하다.
    }
}

public abstract class Abstract3 {
    abstract void sayHello();  // 추상 메서드, abstract 키워드로 정의 가능하다.
}


public class Main {
    public static void main(String[] args) {
        Abstract1 abs1 = new Abstract1() {};  // 구현체가 필수이기 때문에, 아무 것도 없어도 익명 클래스 작성이 필요하다.

        Abstract2 abs2 = new Abstract2() {};
        Abstract2 abs22 = new Abstract2() {
            @Override
            public void sayHello() {
                sout("hi...");
            }
        };

        // 오버라이딩, 오버로딩도 가능하다.
        abs2.sayHello();            // Hello!!!
        abs22.sayHello();           // hi...
        abs2.sayHello("안녕..?");     // 안녕..?


        Abstract3 abs3 = new Abstract3() { // 추상 메서드이기 때문에 구현이 필수이다.
            @Override
            public void sayHello() {
                sout("Hi!!");
            }
        };
        Abstract3 abs33 = new Abstract3() { // 추상 메서드이기 때문에 구현이 필수이다.
            @Override
            public void sayHello() {
                sout("Hello.....");
            }
        };

        abs3.sayHello();    // Hi!!
        abs33.sayHello();   // Hello.....
    }
}

  • 구현할 메서드가 따로 없어도, 추상 클래스는 구현체가 필수적으로 필요하다.
  • 클래스이기 때문에 오버라이딩, 오버로딩 다 가능하다.
  • 추상 메서드는 구현이 필수이다.
    • abstract 키워드로 나타내며,
    • 메서드 블럭이 없다.



코드로 짜보기 - 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
abstract class Person {
    private String name;
    private int age;

    // constructors..

    public void sayName() {
        sout(this.name);
    }

    abstract void sayHello();
}

class Park extends Person {
    @Override
    public void sayHello() {
        sout("Hi!!");
    }
}

class Kim extends Person {
    @Override
    public void sayHello() {
        sout("Hello!!");
    }
}


class Main {
    public static void main(String[] args) {
        Park park = new Park("park", 20);
        Kim kim = new Kim("kim", 30);

        park.sayHello();    // Hi!!
        kim.sayHello();     // Hello!!

        park.sayName();     // park
        kim.sayName();      // kim
    }
}

  • 추상 클래스를 상속 받는 클래스를 사용하는 방법도 있다.
    • Person이라는 추상 클래스가 있고
    • Park과 Kim이 이를 상속 받아 구현하는 구현 클래스이다.
  • 각자 sayHello() 추상 메서드를 구현(재정의)하여 사용한다.
  • Person이 미리 구현한 sayName()을 사용할 수도 있다.
    • 물론 미리 구현한 sayName() 메서드를 각자 재정의(오버라이딩)하여 사용할 수도 있다.





인터페이스

  • 추상 클래스보다 좀 더 추상적인 개념이 인터페이스이다.
    • 모든 메서드가 추상 메서드이다.
    • 인터페이스의 추상 메서드는 기본적으로 public이며, public 접근 지시자만을 가질 수 있다.
      • 역시 위에서 언급한 오버라이딩의 접근 지시자 규칙을 따르기 때문에
      • 인터페이스의 추상 메서드를 구현한 메서드는 무조건 public이다.
  • 인터페이스는 상속이 아닌 구현이다.
    • 상속을 나타내는 extends 키워드가 아닌, implements 키워드를 통해 구현한다.
    • 상속이 아니기 때문에 여러 개의 인터페이스를 구현할 수도 있다.
  • jdk8부터 인터페이스도 디폴트 메서드를 가질 수 있게 되었다.
    • default 키워드로 메서드를 정의한다.
    • 디폴트 메서드는 구현체를 가질 수 있으며, 구현체들이 해당 메서드를 따로 구현하지 않고 사용할 수 있다.
    • 추가적으로 jdk8부터 private 메서드를 가질 수 있게 되었다.



코드로 짜보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
interface Person {
    void hello();

    default void bye() {    // default 메서드로, 따로 재정의하지 않으면 해당 구현을 따라 동작함.
        sout("bye~!");
    }
}

interface Cleaner {
    void cleaning();
}

interface Cook {
    void cooking();
}


class Park implements Person, Cook {
    @Override
    public void hello() {
        sout("Hello");
    }

    @Override
    public void cooking() {
        sout("cooking...");
    }
}


class Main {
    public static void main(String[] args) {
        Park park = new Park();
        Cleaner kim = new Cleaner() {
            @java.lang.Override
            public void cleaning() {
                sout("cleaning..");
            }
        };

        park.hello();   // Hello
        park.cooking(); // cooking..
        park.bye();     // bye~!
        kim.cleaning(); // cleaning..
    }
}

  • Park처럼 class로 구현할 수 있다.
    • 이 경우 여러 개의 인터페이스를 구현할 수 있다.
      • Park은 Person과 Cook, 두 인터페이스를 구현했다.
  • kim처럼 Cleaner 인터페이스를 익명 클래스로 구현해 사용할 수도 있다.
    • 이 경우 추상 클래스와 마찬가지로 추상 메서드를 모두 구현해줘야 한다.
  • Person처럼 default 메서드를 가진걸 볼 수 있다.
    • 이는 jdk8부터 도입된 기능이다.
    • 오버라이딩으로 재정의 할 수 있으며,
    • 그냥 사용할 수도 있다.
    • private으로 내부적으로 메서드를 나눌 수도 있다.





This post is licensed under CC BY 4.0 by the author.