상세 컨텐츠

본문 제목

[JAVA] 자바 스터디 6주차 - 상속

개발 공부 (etc)/JAVA

by letprogramming 2021. 2. 22. 05:50

본문

반응형

 

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

 

 


1. 자바 상속의 특징

상속(Inheritance)이란 부모로 부터 자식이 물려받는 것이다.

자바의 상속도 마찬가지이다.

부모 클래스로 부터 자식 클래스가 물려받는 것을 의미한다.

public class Animal{
    protected int age;
    
    public void setAge(int age){
        this.age = age;
    }
    
    public int getAge(){
    	System.out.println(this.age);
        return this.age;
    }
}

예를 들기 위해 Animal이라는 클래스를 정의했다.

Animal 클래스는 age라는 int 형 변수를 가진다.

 

setAge() 라는 메소드는 age라는 데이터를 수정할 수 있는 메소드이며,

getAge() 라는 메소드는 age라는 데이터를 조회할 수 있는 메소드이다.

 

이 Animal 클래스를 상속받는 Dog 라는 클래스를 정의해보자

public class Dog extends Animal{
    
}

extends 키워드는 상속을 할 때 사용하는 키워드이다.

클래스를 정의하면서 옆에 extends 키워드와 상속 받을 클래스 이름을 명시하면 상속을 받을 수 있다.

 

현재 Dog 클래스 내부에는 아무것도 구현되어 있지 않다.

하지만 Animal 클래스를 상속받았기 때문에 Animal 클래스의 setAge, getAge 메소드를 사용할 수 있다.

 

Dog dog = new Dog();
dog.setAge(5);
dog.getAge();

main 메소드 내부에서 위의 코드를 수행하면

5 라는 값이 출력되는 것을 확인할 수 있다.

 

Dog 클래스가 Animal 클래스를 상속받으면서 Animal 클래스의 메소드들을 사용할 수 있게 된 것이다.

 

상속의 범위

그렇다면 부모의 모든 데이터와 메소드들을 자식 클래스가 상속받아서 사용할 수 있을까?

이전에 배운 것처럼 자바에서는 자료에 대한 캡슐화, 정보 은닉을 위해서 접근 지정자를 제공한다.

이 데이터를 직접 참조할 수 있는 범위를 지정하는 것이다.

 

위의 Dog 클래스에서 Animal 클래스에서 선언한 age 변수를 참조할 수 있는 이유는

protected로 선언되었기 때문이다.

만약 Animal 클래스의 age가 private int age; 였다면 age 변수를 상속받아서 사용할 수 없다.

 

protected 지정자는 default 와 public 사이의 허용 범위를 갖는 지정자이다.

내부 클래스, 같은 패키지, 부모 - 자식 간에는 참조가 가능하지만

외부 클래스, 즉 다른 클래스에서는 참조가 불가능하다.

 

따라서 위에서는 protected 로 선언된 age 와 public 으로 선언된 setAge(), getAge()를 Dog 클래스에서 사용할 수 있다.

 

IS-A 관계

위에서 상속관계에 있는 Animal클래스와 Dog 클래스를 표현할 때,

"Dog(개)는 Animal(동물)이다." ("Dog is a Animal") 이라고 표현할 수 있다.

 

자바에서는 상속 관계를 가지고 있는 자식 객체를 선언할 때 자료형으로 부모 클래스를 갖도록 할 수 있다.

Animal dog = new Dog();

위와 같은 코드가 오류가 나지 않고 가능하다.

 

그러나 반대의 경우는 불가능하다.

Dog dog = new Animal();

위의 코드를 실행하면 컴파일 오류가 발생한다.

즉, "개는 동물이다"는 성립하지만

"동물은 개다."는 성립하지 않는 것이다.

 

다중 상속

자바에서는 다중 상속이 불가능하다.

다중 상속이랑 2개 이상의 부모 클래스로부터 상속을 받는 것을 의미한다.

 

자바에서는 부모는 무조건 하나만 존재해야 한다.


2. super 키워드

super 키워드는 상속관계에 있는 클래스들 중에서 부모 클래스에 접근할 때 사용한다.

this와 유사한 super 키워드는 자식 클래스에서 부모 클래스의 변수나 메소드를 구분하여 사용해야할 때 사용한다.

Animal클래스와 Dog 클래스에 생성자를 추가해보자.

public class Animal{
    protected int age;
    
    public Animal(int age){
    	this.age = age;
    }
    
    public void setAge(int age){
        this.age = age;
    }
    
    public int getAge(){
    	System.out.println(this.age);
        return this.age;
    }
}

public class Dog extends Animal{
	private int age;
	public Dog(int age){
    	super.age = age;
        this.age = age;
    }
}

위에서 age 변수가 부모 클래스인 Animal 클래스에도 존재하고 Dog 클래스에도 존재한다면 이 두 변수를 구분하고

부모 클래스의 age에 접근할 수 있는 키워드가 super 키워드이다.

 

this() 와 같이 super() 메소드도 존재한다.

만약 Dog 클래스를 인스턴스화해서 Animal 클래스가 가지고 있는 getAge() 메소드를 호출하고 싶다면

부모 클래스인 Animal 클래스의 age 변수를 초기화해야 한다.

 

이전에 공부했듯이 클래스의 데이터를 초기화하는 메소드는 생성자이다.

그렇다면 Dog 클래스에서 Animal 클래스의 생성자를 호출해야하는 상황이 벌어진다.

이 때 super() 메소드를 사용할 수 있다.

 

super() 메소드는 부모의 생성자를 의미한다.

super() 메소드를 호출할 때 부모 클래스에 구현되어 있는 생성자와 파라미터를 맞추어 호출하면

해당하는 부모 클래스의 생성자가 호출된다.

public class Animal{
    protected int age;
    
    public Animal(int age){
    	this.age = age;
    }
    
    public void setAge(int age){
        this.age = age;
    }
    
    public int getAge(){
    	System.out.println(this.age);
        return this.age;
    }
}

public class Dog extends Animal{
	public Dog(int age){
    	super(age);
    }
}

위처럼 정의하고

Dog dog = new Dog(5);

Dog 클래스를 인스턴스화하는 순간 Dog의 생성자가 수행된다.

Dog 생성자 내부의 super() 메소드를 호출하면 Animal 내부에 정의한 생성자가 호출되면서 age를 초기화한다.


3. 메소드 오버라이딩 (Overriding)

메소드 오버라이딩이란 상위 클래스의 메소드를 하위 클래스에서 재정의하는 것을 의미한다.

예를 들어 Animal 클래스를 상속 받는 Dog 클래스와 Cat 클래스에서 오버라이딩을 구현해보자

public class Animal{
    public void bark(){
    	System.out.println("동물이 짖어요");
    }
}

public class Dog extends Animal{
    public void bark(){
    	System.out.println("멍멍");
    }
}

public class Cat extends Animal{
    public void bark(){
    	System.out.println("냐옹");
    }
}

bark() 라는 동일한 이름의 메소드를 Animal 클래스를 상속받는 Dog, Cat 클래스에서 각각 다시 구현했다.

Animal animal = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();

animal.bark():
dog.bark():
cat.bark():

위의 코드를 수행했을 때, 만약 오버라이딩을 통해 bark 메소드를 다시 정의하지 않았다면

모두 Animal 클래스에 있는 bark 메소드가 수행되면서 "동물이 짖어요"가 출력되었을 것이다.

 

하지만 위처럼 재정의한다면

"동물이 짖어요"

"멍멍"

"냐옹"

 

각각의 클래스에서 정의한대로 bark() 메소드가 수행될 것이다.


4. 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)

다이나믹(Dynamic)의 의미는 런타임이고 메소드 디스패치란 메소드를 결정하는 것이다.

즉, 다이나믹 메소드 디스패치는 런타임에 수행할 메소드를 결정하는 행위이다.

 

위의 오버라이딩을 공부할 때 Animal, Dog, Cat 클래스마다 bark 메소드를 오버라이딩하여 각각 객체를 선언해 호출했다.

Animal animal = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();

animal.bark():
dog.bark():
cat.bark():

위 코드 처럼 수행하면 각각의 객체의 bark 메소드가 수행되는 것을 확인했다.

 

만약

Animal animal = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();

animal.bark():
dog.bark():
cat.bark():

위와 같이 모든 객체를 Animal 클래스를 자료형으로 한다면 수행 결과는 어떻게 될까

출력 결과는 위와 같이

"동물이 짖어요"

"멍멍"

"냐옹"

이다.

자바에서는 컴파일을 할 때가 아닌 런타임에 어떤 자식 클래스의 오버라이딩된 메소드를 수행할지를 결정하기 때문에

위와 같이 각각의 객체에서 구현한 메소드가 수행될 수 있다.


5.  추상 클래스 (Abstract Class)

추상클래스란 추상 메소드(abstract method)를 하나 이상 포함하는 클래스이다.

추상 메소드란 선언은 되어 있지만 메소드의 내용, 본체는 없는 메소드이다.

 

abstract class Animal{
    protected int age;
    
    abstract void bark();
}

위의 코드에서 Animal 은 추상 클래스이고

bark() 는 추상 메소드이다.

추상 클래스와 추상 메소드를 정의할 때는 abstract라는 키워드를 사용한다.

public abstract class Animal{
    protected int age;
    
    abstract void bark();
}

public class Dog extends Animal{
    public void bark(){
    	System.out.println("멍멍");
    }
}

Animal 추상 클래스를 Dog 클래스가 상속 받아 bark() 추상 메소드를 재정의했다.

만약 Animal 클래스를 상속받은 자식 클래스에서 bark() 메소드를 구현하지 않는다면

컴파일 에러가 발생한다.

 

추상 클래스는 일반적인 클래스와 거의 모든 것이 유사하지만

객체를 생성할 수 없다는 특징을 갖는다.

 

위의 코드에서 Animal을 상속 받은 Dog는 일반적인 클래스이기 때문에 객체를 생서할 수 있다.

그러나 추상 클래스인 Animal 은

Animal animal = new Animal();

이렇게 객체를 생성할 수 없다.

 

객체를 생성하지 못하는 추상 클래스를 사용하는 이유는 여러 가지가 있다.

 

우선 추상 클래스를 사용하면 클래스가 가지고 있는 변수들과 메소드의 이름이 통일된다.

코드의 양이 많지 않다면 문제가 되지 않지만 만약에 거대한 프로젝트를 진행하고

수많은 사람들이 참여한다고 가정을 해보자.

A 라는 이름의 클래스를 상속받아 수많은 사람들이 각자 개발을 진행한다면 아마 각기 다른 이름의 변수와 메소드가

쏟아질 것이고 의사소통을 하는 것도 비용이 많이 소비될 것이다.

 

이 때 추상클래스를 이용하면 이러한 문제점을 해결할 수 있다.

추상클래스에 변수와 메소드를 미리 정의해놓으면 이 클래스를 상속받는 사람들은 해당 이름을 변경할 수 없다.

결과적으로 모든 사람이 반드시 미리 정의 해놓은 이름을 사용해야 하기 때문에 자연스럽게 통일이 된다.

이는 이후에 프로젝트를 유지 보수하고 개선하는 작업을 할 때도 일을 효율적으로 할 수 있게 해준다.

 

추상클래스를 이용하는 다른 이유는 설계와 구현의 분리이다.

거대한 클래스를 구현해야 할 때 일반적인 클래스로 구현을 한다면 설계와 구현을 동시에 해야하기 때문에

집중하기도 어렵고 시간이 오래걸릴 것이다.

하지만 추상클래스를 이용하면 설계와 구현을 따로 분리해서 효율적으로 일할 수 있다.

설계를 맡은 사람이 필요한 것들을 조사하고 추상클래스로 정의해놓으면

구현하는 사람이 그 본체를 구현하면 된다.

 

위에서 언급한 내용들과 유사한 장점으로

추상클래스는 클래스의 일정한 규격, 제한을 할 수 있다.

추상클래스의 추상메소드들은 반드시 상속받은 자식 클래스에서 재정의(오버라이딩)해야 한다.

재정의하지 않으면 컴파일 에러가 발생하고 해당 클래스를 사용할 수 없다.

추상클래스를 사용하면 상속받은 사람이 생각하지 않은 방향으로 사용하는 것을 방지하고 제어할 수 있다.


6.  final 키워드

final 키워드는 상수를 표현하기 위한 키워드이다.

상수란 수학의 상수와 마찬가지로 수식에서 변하지 않는 값이다.

 

지금까지 코딩을 할 때 데이터를 저장하기 위해서 주로 변수(Variable)를 사용했다.

변수는 상수와 반대되는 개념으로 값이 항상 변한다.

 

이러한 변수의 특성때문에 변하면 안되는 값이 변할 때가 있다.

절대 변하면 안되는 값을 미리 선언해놓을 때 final 키워드를 사용한다.

 

public class Math{
    static final double PI = 3.141592;
    final String URL = "https://letsbegin.tistory.com";
}

예시를 위해서 Math 라는 클래스를 정의했다.

 

PI 는 실수형을 가지는 상수이고 URL은 문자열 String을 값으로 가지는 상수이다.

상수는 반드시 선언과 동시에 초기화해야 한다.

 

위에서 final 앞에 static을 붙인 PI는 프로그램 전체에서 사용할 수 있다.

일반적으로 상수의 경우 프로그램 전반적으로 주로 사용하는 값들을 상수로 미리 정의하는 경우가 많기 때문에

static 키워드와 함께 사용하여 정적으로 선언하면 더 편하게 사용할 수 있다.


7.  Object 클래스

Object 클래스는 자바 클래스 중에서 가장 최상위계층에 있는 클래스이다.

최상위계층에 있다는 뜻은 모든 클래스들의 부모를 찾아 올라가다 보면 마지막에 결국 Object 클래스가 나온다는 것이다.

 

Object 클래스는 상속을 사용하지 않는 클래스들도 자동으로 상속받도록 하는 클래스이다.

public class A{}

만약에 위와 같이 아무것도 상속받지 않는 클래스 A를 선언했더라도

public class A extends Object{}

이는 위와 같이 Object 클래스를 자동으로 상속받는 것이다.

 

앞서 배웠던 클래스 상속의 IS-A 관계에 의해서 모든 클래스 객체들은

Object A = new Animal();
Object B = new Car();
Object C = new Human();

이런 식으로 Object 클래스를 자료형으로 사용할 수 있다.

 

Object 클래스는 기본적으로 클래스가 가지고 있어야 하는 기능들을 가지고 있기 때문에

자바에서는 최소한의 기능을 모든 클래스에 제공하고자 이 Object 클래스를 무조건 상속받도록 해놓았다.

 

대표적인 Object 클래스의 메소드들은 toString(), equals(), hashCode(), clone(), finalize(), getClass() 등이 있다.

간단하게 정리해보면

 

먼저 toString()은 객체의 정보를 문자열로 리턴할 때 주로 사용한다.

public class Human{
    private String name;
    private int age;
    
    public Human(String name, int age){
    	this.name = name;
        this.age = age;
    }
    
    public static void main(String[] args){
    	Human human = new Human("kim", 10);
        System.out.println(human);
    }
}

위의 예시를 보자

Human이라는 클래스를 선언하고 내부의 main 메소드에서 객체를 생성하고

객체 자체를 출력해보았다.

 

이 때

패키지 이름 + 클래스 이름 + @ + 해쉬 코드

예) com.kim.default.Human@87aa3b21

 

이런 형태로 출력되는 것을 확인할 수 있다.

이 형태를 리턴해주는 것이 Object 클래스의 toString() 메소드이다.

 

객체를 출력하는 순간 Object 클래스의 toString()이 호출되고

toString() 내부에서

getClass().getName() + '@' + Integer.toHexString(hashCode())

위 메소드들은 조합하여 String 값을 리턴해주는 것이다.

 

이 toString() 메소드를 오버라이딩하여 출력하고 싶은 형태로 바꾸면

public class Human{
    private String name;
    private int age;
    
    public Human(String name, int age){
    	this.name = name;
        this.age = age;
    }
    
    public String toString(){
    	return "name : " + name + " age : " + age;
    }
    
    public static void main(String[] args){
    	Human human = new Human("kim", 10);
        System.out.println(human);
    }
}

"name : kim age : 10" 이라는 출력 결과를 볼 수 있다.

 

toString() 함수에서 보았던 getClass() 메소드는 Object 내에 있는 함수로 객체의 클래스 정보를 확인할 수 있다.

 

hashCode() 메소드는 인스턴스화된 객체가 현재 가리키고 있는 메모리의 주소를 변환한 코드이다.

실제 메모리 주소를 반환해주지 않지만 hashCode()를 오버라이드하여 객체들의 동일성을 판별할 때 주로 사용한다.

 

이전에 공부했던 것처럼 객체는 생성할 때마다 메모리에 올라가는 주소를 참조하는 변수이다.

따라서 우리가 논리적으로 만들어낸 name이나 age 등과 같은 값들이 같은 객체끼리도

객체의 주소 입장에서 보면 완전히 다른 객체인 것을 hashCode() 메소드를 출력하면 알 수 있다.

 

따라서 정해놓은 값들이 같다면 동일한 객체로 판별하기 위해서 hashCode()와 equals() 메소드를 오버라이딩해서 사용한다.

public class User(){
    private int id;
    
    public User(int id){
    	this.id = id;
    }
    
    public boolean equals(Object obj){
    	if (obj instanceof User){
        	return this.hashCode() == ((User)obj).hashCode();
        } else {
        	return false;
        }
    }
    
    public int hashCode(){
    	return this.id;
    }
    
    public static void main(String[] args){
    	User user1 = new User(1234);
        User user2 = new User(1234);
        User user3 = new User(5678);
        
        System.out.println(user1.equals(user2));
        System.out.println(user1.equals(user3));
        
        System.out.println(user1.hashCode());
        System.out.println(user2.hashCode());
        System.out.println(user3.hashCode());
    }
}

예를 들어 User 라는 클래스가 id 라는 변수를 이용해서 유저들을 구분한다고 가정하자

위의 코드에서 private으로 선언된 id를 조회하기 위해 hashCode() 메소드를 오버라이딩했다.

 

또한 equals 메소드를 이용해 두 객체간의 값을 비교하도록 했다.

먼저 이 객체가 User 클래스의 객체인지 확인하고

두 객체의 id 값이 같다면 true, User 클래스가 아니거나 id 가 다를 경우 false를 리턴한다.

 

결과적으로 위의 코드의 출력 결과는

true

false

1234

1234

5678

true
false
1234
1234
5678

이다.

 

만약 오버라이딩하지 않고 그대로 Object 클래스의 hashCode() 메소드나 equals() 메소드 원형을 사용했다면

위의 결과는 달랐을 것이다.

세 개의 객체가 모두 다른 값을 가지기 때문에 false가 나오고

hashCode()는 위에서 이야기 했던 패키지, 클래스, 주소 값이 조합된 값이 출력되었을 것이다.

 

Object 클래스의 다양한 메소드들과 특징을 이용하면 기본적인 클래스의 형태를 미리 잘 정의할 수 있다.

반응형

관련글 더보기