자바스터디 - 6주차

megaseunghan·2022년 3월 6일
0

자바스터디

목록 보기
6/15
post-thumbnail

목표

자바의 상속에 대해 학습하세요.

학습할 것 (필수)

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

1. 자바 상속의 특징

상속이란?

부모 클래스에서 정의된 필드와 메소드를 자식 클래스가 물려받는 것. 하지만 부모의 모든 필드와 메소드를 상속받는 것은 아니다.

  • 상속시 사용하는 키워드 : extends
  • 필요한 이유 :
    • 공통된 특징을 가지는 클래스 사이의 중복적 멤버 선언 등 불필요함을 줄이기 위해
    • 부모 클래스의 멤버를 재사용을함으로 자식 클래스가 간결해진다.
    • 클래스간 계층적 분류 및 관리가 쉬워진다(부모 클래스를 수정하면 상속받은 자식 클래스도 수정되기 때문)

상속의 특징

  1. 자바에서는 다중 상속을 지원하지 않는다. 따라서 extends 뒤에는 단 하나의 부모 클래스만 올 수 있다. 다음은 다중 상속을 코드로 재현한 것이다.
public class ChildClass extends ParentClass1, ParentClass2 { ... } // X !
public class ChildClass extends ParentClass { ... } // O !
  1. 자바에서는 상속의 횟수에 제한을 두지 않는다. 뇌절 예제를 통해 확인해보자
package extendsextendsextends;

// 최상위 부모 클래스
public class AClass {
    int field;
}

// 상위 부모 클래스
public class BClass extends AClass {
}

// 최상위 클래스를 상속받는 부모 클래스를 상속 받는 클래스
public class CClass extends BClass {
}

// 최상위 클래스를 상속받는 부모 클래스를 상속 받는 클래스를 상속받는 하위 클래스
public class DClass extends CClass {
}

// 최상위 클래스를 상속받는 부모 클래스를 상속받는 클래스를 상속받는 하위 클래스를 상속받는 최하위 클래스
public class EClass extends DClass{
}
  1. 자바에서의 최상위 클래스는 언제나 Object 클래스이다. Object 클래스는 유일하게 super class 를 가지고 있지 않다.

2. super 키워드

super 키워드는 부모 클래스로부터 상속 받은 필드나 메소드를 자식 클래스에서 참조하는데 사용하는 참조 변수이다.

부모클래스의 멤버와 자식 클래스의 멤버 이름이 같을 경우 super 키워드를 통해 구별할 수 있다.

super 키워드는 this 키워드와 마찬가지로 참조 변수를 사용할 수 있는 대상은 인스턴스 메소드 뿐이며, 클래스 메소드에서는 사용할 수 없다.

  • 예제
class Parent {
  int a = 10;
}

class Child extends Parent {
  int a = 20;
  void display() {
    System.out.println(a); // 20 출력
    System.out.println(this.a); // 20 출력
    System.out.println(super.a); // 10 출력
  }
}

public class Inheritance02 {
  public static void main(String[] args) {
    Child ch = new Child();
    ch.display();
  }
}

자식 클래스의 인스턴스를 생성하면, 해당 인스턴스에는 자식 클래스의 고유 멤버뿐만 아니라, 부모 클래스의 모든 멤버까지도 포함되어 있다. 따라서 부모 클래스의 멤버를 초기화하기 위해서는 자식 클래스의 생성자에서 부모 클래스의 생성자까지 호출해야만 한다.

자바 컴파일러는 부모 클래스의 생성자를 명시적으로 호출하지 않는 모든 자식 클래스의 생성자 첫 줄에 자동으로 super() 명령문을 추가하여 부모 클래의 멤버를 초기화할 수 있도록 해준다.

하지만, 컴파일 시 클래스에 생성자가 하나도 정의되어 있지 않아야만 자동으로 기본 생성자를 추가해준다.

class Parent {
  int a;
  Parent(int a) {
    a = a;
  }
}

이처럼 매개변수를 가지는 생성자를 하나라도 선언했다면 부모 클래스에는 기본 생성자가 자동으로 추가되지 않는다.

class Parent {
  int a; 
  Parent(int a) {
    a = a;
  }
}

class Child extends Parent {
  int b;
  Child() {
    super();
    b = 20;
  }
}

하지만 이처럼 Parent 클래스를 상속받은 자식 클래스에서 super() 메소드를 사용하여 부모 클래스의 기본 생성자를 호출하게 되면, 오류가 발생한다. 부모 클래스인 Parent 클래스에는 기본 생성자가 추가되지 않았기 때문이다. 따라서 매개변수를 가지는 생성자를 선언할 경우, 기본 생성자를 명시적으로 선언해주는 것이 좋다.

class Parent {
  int a; 
  Parent() {
    a = a;
  }
}

class Child extends Parent {
  int b;
  Child() {
    super();
    b = 20;
  }
}
  • super() 사용 예제
class Parent {
  int a;
  Parent() {
    a = 10;
  }
}

class Child extends Parent {
  int b;
  Child() {
    super(40); // super.a = 40;
    b = 20;
  }
  void display() {
    System.out.println(a);
    System.out.println(b);
  }
}

public class Inheritance {
  public static void main(String[] args) {
    Child c = new Child();
    c.display();
  }
}

/*
실행 결과 :
40
20
*/

3. 메소드 오버라이딩

메소드 오버라이딩이란 부모 클래스로부터 상속 받은 메소드를 자식 클래스에 맞게 재정의 하는 것을 말한다.

사용의 이유는 다음과 같다.

먼저, 상속과 오버라이딩은 긴밀한 관계를 이루고 있는데 자식 클래스는 부모의 멤버 및 메소드를 상속받게 된다. 이 때 부모의 메소드와 다르게 정의할 필요가 있을 때 사용하는 방법이 메소드 오버라이딩이다. 메소드 오버라이딩(@Override)를 사용하게 되면 부모의 메소드는 숨겨지게 된다.

메소드 오버라이딩은 추상 클래스를 상속 받을 때, 또는 인터페이스를 구현할 때 많이 사용하게 된다. 일반 클래스의 상속 관계에서는 많이 사용되는 개념은 아니다.

메소드 오버라이딩은 다형성 런타임에 사용된다.

오버라이딩의 규칙

  1. 부모의 메소드와 동일한 시그니처(리턴 타입, 메소드 이름, 매개 변수 리스트)를 가져야 한다.
  2. 접근 제한을 더 강하게 오버라이딩 할 수 없다.
  3. 새로운 예외를 throws 할 수 없다.

메소드 오버라이딩 예제

public class A {
  public void print() {
    System.out.println("오버라이딩 전!");
  }
}

public class B extends A {
  @Override
  public void print() {
    System.out.println("오버라이딩 후!");
  }
}

public class Main {
  public static void main(String[] args) {
    B b = new B();
    b.print(); // 오버라이딩 후! 출력
  }
}

4. 다이나믹 메소드 디스패치

다이나믹 메소드 디스패치를 알기 전에 메소드 디스패치부터 파악하자.

메소드 디스패치

메소드 디스패치란 어떤 메소드를 호출할지 결정하여 실제로 실행시키는 과정이다. 자바는 런타임시 객체를 생성하고, 컴파일 시에는 생성할 객체 타입에 대한 정보만 공유한다.

  • 메소드 디스패치의 종류
    1. 정적 메소드 디스패치 (Static Method Dispatch)
    2. 동적 메소드 디스패치 (Dynamic Method Dispatch)
    3. 더블 디스패치 (Double Dispatch)

정적 메소드 디스패치 (Static Method Dispatch)

public class Dispatch {
    static class A {
        void run() {
            System.out.println("실행");
        }
    }
    public static void main(String[] args) {
        new A().run();
    }
}

Dispatch 클래스 내의 위치한 static 클래스 A가 main 메소드에서 run() 메소드를 실행하면 우리는 "실행"이 출력될 것을 알고 있다.

컴파일러 역시 컴파일 시점에서 특정 메소드를 호출하면 무엇을 실행시켜야 되는 것을 명확하게 알고 있다. 이것을 정적 메소드 디스패치라고 한다.

동적 메소드 디스패치 (Dynamic Method Dispatch)

public class Dispatch {
    static abstract class A {
        abstract void run();
    }

    static class B extends A {
        @Override
        void run() {
            System.out.println("B가 출력된다.");
        }
    }

    static class C extends A {
        @Override
        void run() {
            System.out.println("C가 출력된다.");
        }
    }

    public static void main(String[] args) {
        A a = new B();
//        A a = new C();
        a.run();
    }
}

Dispatch 클래스 내에 정적 추상 클래스인 A가 정의 되어 있고 추상 메소드 runI()이 선언 되어있다.

그리고 이를 상속받은 B,C가 있다. 각 실체 클래스는 run() 메소드를 오버라이딩하고 있고 출력 구문이 서로 다르다.

그리고 main 메소드에서는 A타입의 객체가 생성되어 있고 어떤 자식 클래스를 업캐스팅 할지는 개발자의 선택에 따라 달렸다. B 클래스를 객체로 만들게 된다면 "B가 출력된다"가 실행될 것이고, 반대로 주석 처리된 C 클래스를 객체로 만들면 "C가 출력된다"가 나올 것이다. 이처럼 정적 메소드 디스패치와 반대의 개념으로, 컴파일러가 어떤 메소드를 호출하는지 모르는 경우를 동적 메소드 디스패치라고 하며, 호출할 메서드를 런타임 시점에서 결정한다.

5. 추상 클래스

  • 추상(Abstract)

    : 사전적 의미로 추상은 실체 간에 공통되는 특성을 추출한 것을 말한다. 예를 들어 새, 사람, 물고기 등의 실체에서 공통되는 특성을 추출해보면 동물이라는 공통점이 있다.

  • 추상 클래스(Abstract Class)

    : 객체를 직접 생성할 수 있는 클래스를 실체 클래스라고 하며 이 클래스들의 공통적인 특성을 추출하여 선언한 클래스를 만들 수 있는데, 이를 추상 클래스라고 한다.

추상 클래스와 실체 클래스 관계

추상 클래스와 실체 클래스는 서로 상속의 관계를 가지고 있다. 추상 클래스는 부모, 실체 클래스는 자식의 관계를 가진다.

추상 클래스의 특성 ?

추상 클래스에서의 특성이란 필드, 메소드들을 말한다

추상 클래스 선언

추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 객체를 직접 생성할 수는 없다. new 키워드로 인스턴스가 되지 못한다는 것이다. 추상클래스는 새로운 실체 클래스를 만들기 위해 부모 클래스로만 사용된다.

  • 추상 클래스 실체화 예제
// 동물이라는 특성을 추출한 추상 클래스 Animal.
public abstract class Animal {
  //필드
  //생성자
  //메소드
}

// Animal 추상 클래스를 상속받은 실체 클래스 Cat
public class Cat extends Animal {
  .
  .
  .
}
  • 선언 방법 예제
public class AbstractExample {
  public static void main(String[] args) {
    // Animal animal = new Animal();  X : 추상 클래스는 new를 통해 생성이 불가능.
    Cat cat = new Cat(); // O : 추상클래스를 실체화한 클래스이기 때문에 사용 가능
  }
}

추상 클래스 용도

1. 실체 클래스들의 공통된 필드와 메소드의 이름을 통일할 목적

실체 클래스를 설계하는 사람이 여러명일 경우, 클래스마다 선언되는 필드, 메소드는 각기 다를 것이다. 이를 통일하기 위해서 추상 클래스를 사용한다. 동일한 데이터와 기능임에도 불구하고 서로 다른 이름을 가진다면, 사용 방법이 달라지게 되므로 추상 클래스를 사용하여 필드와 메소드 이름을 통일시킨다.

2. 실체 클래스를 작성할 때 시간을 절약

공통적인 필드와 메소드는 추상 클래스에 모두 선언하고, 실체 클래스마다 다른 점만 실체 클래스에서 선언하게 되면 실체 클래스를 작성하는데 시간을 절약할 수 있다.

추상 클래스 선언

  • 키워드 : abstract를 사용한다. 이를 통해 new 연산자를 이용해 객체를 생성할 수 없게되고, 상속을 통해서 자식 클래스만 만들 수 있게 된다.

  • 추상 클래스의 멤버

    : 추상 클래스 또한 일반 클래스처럼 필드, 메소드, 생성자를 가질 수 있다. new 연산자로 직접 생성할 수는 없지만 자식 클래스가 생성될 때 상속받은 추상 클래스 또한 자식 클래스 생성자의 super()를 통해 객체가 생성되기 때문에 생성자가 반드시 있어야 한다.

추상 클래스 선언 예제

  • 추상 클래스 Phone
public abstract class Phone {
  	// 필드
    public String owner;
  
		// 생성자
    public Phone(String owner) {
        this.owner = owner;
    }
		
  	// 메소드
    public void turnOn() {
        System.out.println("폰 전원을 켭니다");
    }

    public void turnOff() {
        System.out.println("폰 전원을 끕니다");
    }
}
  • 실체 클래스 SmartPhone
public class SmartPhone extends Phone {

		// 생성자, 
    public SmartPhone(String owner) {
        super(owner); // 추상 클래스의 생성자를 호출하고 있는 모습.
    }
		
  	// 메소드
    public void internetSearch() {
        System.out.println("인터넷 검색을 합니다");
    }
}

추상 메소드와 오버 라이딩

추상 클래스는 실체 클래스가 공통적으로 가져야 할 필드와 메소드들을 정의 해놓은 추상적인 클래스이므로, 실체 클래스의 멤버를 통일화하는데 목적이 있다. 하지만 추상 클래스의 메소드가 실체 클래스에서 사용이 모두 같다면 좋겠지만 아닐 때도 빈번하다. 이런 경우를 위해서 추상 클래스는 추상 메소드를 선언할 수 있다.

  • 추상 메소드

    : 메소드의 선언부만 있고 구현부는 없는 것. 하위 클래스가 반드시 실행 내용을 채우도록 강요하고 싶은 메소드가 있을 경우 , 해당 메소드를 추상 메소드로 선언한다. 자식 클래스는 반드시 추상 메소드를 Overriding해야 하며, 그렇지 않으면 컴파일 에러가 발생한다.

추상 메소드 선언

public | protected abstract 리턴타입 메소드명(매개변수, ...);

일반 메소드와의 차이점은 {}이 없다는 것이다.

6. final 키워드

final 키워드는 클래스, 필드, 메소드 선언 시에 사용할 수 있다. final키워드는 해당 선언이 최종 상태이고 결고 수정될 수 없음을 뜻한다. final키워드가 클래스, 필드, 메소드 선언에 사용될 경우 해석이 조금씩 달라진다.

final 클래스

클래스를 선언할 때 final을 class 앞에 붙이게 되면, 이 클래스는 최종적인 클래스이므로 상속할 수 없는 클래스가 된다. 즉 final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다. final 클래스의 대표적인 예는 자바 표준 API에서 제공하는 String이 있다.

Reference, Oracle - String 명세 살펴보기

  • final 클래스 선언 방법
public final class 클래스 { ... } // 자식 생성 불가 && 부모 클래스 될 수 없음

final 메소드

메소드를 선언할 때 final을 선언하게 되면 이 메소드는 최종적인 의미가 되기 때문에 오버라이딩 할 수 없게 된다. 부모 클래스를 상속해서 자식 클래스를 선언할 때 부모 클래스에 선언된 final 메소드는 재정의 할 수 없다는 것이다.

public final 리턴타입 메소드([매개변수, ...]) { ... }; 

final 필드

먼저, final 필드는 다음과 같이 선언한다.

final 타입 필드 [= 초기값];

final 필드의 초기값을 줄 수 있는 방법은 생성자에서 주거나 선언과 동시에 초기화하는 방법밖에 없다.

final 키워드가 붙은 필드를 초기화하지 않는다면 컴파일 에러가 발생한다.

상수 (constant)

먼저, 상수는 간단하게 말하면 불변의 값이다. 원주율 또는 지구의 무게 및 둘레처럼 변하지 않는 값을 말한다. 자바에서 상수는 static final 키워드로 표현한다.

하지만 final 키워드도 선언 후에 값을 바꿀 수 없다는 점에서 상수가 될 수 있지 않을까? 정답은 아니다. 불변의 값은 객체마다 저장할 필요가 없는 공용성을 띄고 있고, 여러 가지 값으로 초기화 될 수 없다는 점에서 final 필드를 상수로 부르진 않는다고 한다. 상수는 객체마다가 아닌 클래스에만 포함되며, 다음과 같이 선언한다.

static final 타입 상수 [= 초기값];

7. Object 클래스

Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.

java.lang 패키지

자바에서 가장 기본적인 동작을 수행하는 클래스들의 집합이다.

위 패키지의 클래스들은 import 문을 사용하지 않아도 클래스 이름만으로 바로 사용할 수 있다.

java.lang.Object

모든 자바 클래스의 최고 상위 클래스이다. 모든 클래스들은 이 Object 클래스를 상속 받는다.

Object 클래스는 객체 비교, 해시코드 생성, null 여부, 객체 문자열 리턴 등의 연산을 수행하는 정적 메소드로 이루어져 있다.

  • Object의 정적 메소드
리턴 타입메소드(매개 변수)설명
intcompare(T a, T b, Comparator c)두 객체 a,b를 Comparator를 사용하여 비교
booleandeepEquals(Object a, Object b)두 객체의 깊은 비교 (배열의 항목까지 비교)
booleanequals(Object a, Object b)두 객체의 얕은 비교(객체의 번지수만 비교)
inthash(Object... values)매개값이 저장된 배열의 해시코드 생성
inthashCode(Object o)객체의 해시코드 생성
booleanisNull(Obejct obj)객체가 null인지 조사
booleannonNull(Object obj)객체가 null이 아닌지 조사
TrequireNonNull(T obj)객체가 null인 경우 예외 발생
TrequireNonNull(T obj, String message)객체가 null인 경우 예외 발생(주어진 예외 메시지 포함)
TrequireNonNull(T obj, Supplier messageSupplier)객체가 null인 경우 예외 발생(람다식이 만든 예외 메시지 포함)
StringtoStrig(Object o)객체의 toString() 리턴값 리턴
StringtoString(Object o, String nullDefault)객체의 toString() 리턴값 리턴, 첫 번째 매개값이 null일 경우 두 번째 매개값 리턴

잘 모르는 메소드 조금 더 공부하기

  • 객체 비교 (compare(T a, T b, Comparator<T> c))
public interface Comparator<T> {
  int compare(T a, T b);
}
public class ScoreComparator implements Comparator<Student> {
  @Override
  public int compare(Student a, Student b) {
    if(a.score < b.score) {
      return -1;
    } else if( a.score == b.score) {
      return 0;
    } else {
      return 1;
    }
  }
}

인터페이스 Comparator에 정의 되어 있는 compare() 메소드를 확인해보면 a와 b를 비교한다. 이를 구현한 ScoreComparator에는 인터페이스의 compare()가 오버라이딩 되어 있다. 해당 메소드는

a < b 일 때는 -1,
a == b일 때는 0,
a > b일 때는 1 을 리턴한다.

  • 동등 비교(equals(), deepEquals())

    • equals() 비교표
    abObject.equals(a,b)
    not nullnot nulla.equals(b)의 리턴값
    nullnot nullfalse
    not nullnullfalse
    nullnulltrue
    • deepEquals() 비교표
    abObject.deepEquals(a,b)
    not null(not array)not null(not array)a.equals(b)의 리턴값
    not null(array)not null(array)Arrays.deepEquals(a,b)의 리턴값
    not nullnullfalse
    nullnot nullfalse
    nullnulltrue
    • 예제
    package week_6.equals;
    
    import java.util.Arrays;
    import java.util.Objects;
    
    public class EqualsAndDeepEqualsExample {
        public static void main(String[] args) {
            Integer o1 = new Integer(1);
            Integer o2 = new Integer(1);
    
            System.out.println(Objects.equals(o1, o2));
            System.out.println(Objects.deepEquals(o1, o2));
            System.out.println(Objects.equals(o1, null));
            System.out.println(Objects.equals(null, o2));
            System.out.println(Objects.equals(null, null));
    
            System.out.println("===============================");
            Integer[] arr1 = {1, 2};
            Integer[] arr2 = {1, 2};
    
            System.out.println(Objects.equals(arr1, arr2));
            System.out.println(Objects.deepEquals(arr1, arr2));
            System.out.println(Arrays.deepEquals(arr1, arr2));
            System.out.println(Objects.deepEquals(arr1, null));
            System.out.println(Objects.deepEquals(null, arr2));
            System.out.println(Objects.deepEquals(null, null));
        }
    }
    
    /*
    
    true
    true
    false
    false
    true
    ===============================
    false
    true
    true
    false
    false
    true
    
    종료 코드 0(으)로 완료된 프로세스
    
    */

0개의 댓글