[Java] 타입변환(Casting)

박세진·2021년 1월 28일
7
post-thumbnail

오늘은 어떤 주제로 포스팅을 할 까 고민하다가, 언니에게 들은 얘기를 듣고 아이디어가 떠올랐다.
언니가 본 회사의 면접에서 equlals메소드에 관한 질문을 받았다는 얘기를 듣고
' 아! 오늘은 이거에 대해 포스팅을 해보아야 겠다 ' 고 생각을 했다.

toString, equals 등등 Object 클래스의 메소드가 여러가지가 있어서 이것에 대해 정리해보려고 하던 참이었는데, 이것저것 찾아보며 공부를 하다가 Casting에 관한 부분에서 내가 잘 모르고 있다는 것을 깨달았다.
그래서 오늘은 Casting에 관해서 글을 써볼까 한다.

Object 클래스에 관한 글은 다음 포스팅에 적으려고 한다.


Casting 이란?

자바에서 캐스팅은 타입을 변환하는 것 이다. (=형변환 !!! )
이 때, 상속 관계에 있는 부모와 자식 클래스 간에는 서로 형변환이 가능하다.
캐스팅의 종류는 2가지이다

  • 업 캐스팅 : 자식 클래스가 부모 클래스 타입으로 캐스팅 되는 것.
class Human{
// 생략
}

class Student extends Human{
// 생략
}
public class CastingTest {
	public static void main(String[] args) {
    
    	Student student = new Student();
        Human human = student; 
        // 자식 클래스(Student)가 부모 클래스(Human)타입으로 캐스팅
  	}
}
  • 다운 캐스팅 : 부모 클래스가 자식 클래스 타입으로 캐스팅 되는 것.
class Human{
// 생략
}

class Student extends Human{
// 생략
}
public class CastingTest {
	public static void main(String[] args) {
    
    	Human human = new Human();
        Student student = (Student) human;
        // 부모 클래스(Human)이 자식 클래스(Student)타입으로 캐스팅
        // 위의 구문은 오류지만, 이런 식으로 된다는 걸 보여주기 위한 것이니 참고만 하자.
        // 실제로 다운캐스팅은 업 캐스팅이 선행된 후 되어야 한다.
  	}
}

이제부터 각각에 대해 자세히 살펴보자.
시작하기에 앞서, 부모클래스인 상속 관계의 상위 클래스를 수퍼 클래스, 그리고 자식 클래스인 하위 클래스를 서브 클래스라고 정의한다.


업 캐스팅

Java에서, 서브 클래스는 수퍼 클래스의 모든 특성을 상속받는다.
그렇기 때문에 서브 클래스는 수퍼 클래스로 취급될 수 있다.

ex) 수퍼 클래스 : 생물, 서브 클래스 : 사람 일 때
사람은 생물이다. 라고 할 수 있음 !

여기서 업 캐스팅이란 서브 클래스의 객체가 수퍼 클래스 타입으로 형변환 되는 것을 말한다.

즉, 수퍼 클래스의 참조변수가 서브클래스로 객체화된 인스턴스를 가리킬 수 있게 된다. 이해가 잘 안된다면 아래의 예제를 보고 코드로 이해하자.

package study.test.java;


class Person{
	String name;

	public Person(String name) {
		this.name = name;
	}
}

class Student extends Person{
	
	String age;
	
	public Student(String name) {
		super(name); // super 키워드에 대해서는 따로 설명하겠다.
	}
}
public class CastingTest {
	public static void main(String[] args) {
    
    	// student 참조변수를 이용하면 age, name에 접근 가능하다.
    	Student student = new Student("도리도리"); 
	
    	// person 참조변수를 이용하면 Student 객체의 멤버 중에서 Person 클래스의 멤버에만 접근이 가능하다.
	Person person = student; // "업 캐스팅"
    
	person.name = "세진세진";
	person.age = "24"; // 컴파일 오류 ~!
	
	}
}

위의 코드에서 , person 참조변수는 Student 객체를 가리킨다.
하지만 Person 타입이므로 자신의 클래스에 속한 멤버만 접근 가능하다.
(컴파일 시점에서 오류가 남)
person.age를 했을 때 컴파일 오류가 나는것이 바로 그 예시이다.

이와 같이 업 캐스팅을 하게 되면 객체(여기서는 Student 객체를 말함) 내의 모든 멤버에 접근할 수 없다. 멤버 필드 뿐 아니라 메서드도 마찬가지 이다.

또한, 명시적인 타입 캐스팅 선언을 하지 않아도 된다.
서브 클래스인 Student는 Person 타입 이기도 하기 때문이다.
무슨 말인지 잘 모르겠다면 아래의 코드를 참고하자.

Student student = new Student();
Person person = (Person) student; // 이렇게 앞에 (Person) 붙여주는거 안해도 된다는 것.

[참고]

super 키워드는 뭐지? super, super()의 차이점은?😶


super

  • 자식 클래스가 부모 클래스로부터 상속받은 멤버를 참조할 때 사용하는 참조 변수.
  • 클래스 내의 멤버변수와 지역변수의 이름이 같을 경우 구분하기 위해 this를 사용하는 것 처럼, 부모 클래스와 자식 클래스의 멤버의 이름이 같을 경우 super를 사용한다.
  • 아래의 예제를 참고하자.
class Parent{
   int x = 10;
}
class Child extends Parent{
   int x = 20;
   void childMethod(){
      System.out.println("x=" + x);
      System.out.println("this.x=" + this.x);
      System.out.println("super.x=" + super.x); // 부모클래스의 멤버변수 x를 말하는것. 출력결과는 10 이다.
   }
}
class Main{
	public static void main(String[] args) {
    Child child = new Child();
    child.childMethod();
    }
}

super()

  • 부모 클래스의 생성자를 호출하는 메서드.
  • 자식 클래스의 인스턴스를 생성하면, 해당 인스턴스에는 자식 클래스의 고유 멤버 뿐 아니라 부모 클래스의 모든 멤버까지 포함되어 있음.
    따라서 부모 클래스의 멤버를 초기화하기 위해서는 자식 클래스의 생성자에서 부모 클래스의 생성자까지 호출해야 한다.
cf) 상속을 받았더라도 자식조차 접근할 수 없는 private field의 경우는 자식 생성자로 
초기화 할 수 없다. 따라서 부모의 생성자를 호출하는 super() 필요.
  • 자바 컴파일러는 부모 클래스의 생성자를 명시적으로 호출하지 않는 모든 자식클래스의 생성자 첫줄에 자동으로 super()를 추가하여, 부모 클래스의 멤버를 초기화 할 수 있도록 해준다.
  • 하지만 자바 컴파일러는 컴파일시 클래스에 생성자가 하나도 정의되어 있지 않아야만 자동으로 기본 생성자를 추가해준다.
  • 만약 아래의 예제처럼 부모 클래스에 매개변수를 가지는 생성자를 하나라도 선언했다면, 부모 클래스에는 기본 생성자가 자동으로 추가되지 않을 것이다.
  • 따라서 Parent 클래스를 상속받은 자식 클래스에서 super()를 사용하여 부모 클래스의 기본 생성자를 호출하게 되면, 오류가 발생한다.
class Parent{
   int a;
   Parent(int n){
      a = n;
   }
}
class Child extends Parent{
   int b;
   Child(){
      super();
      b = 20;
   }
}

Why 오류 발생 ? 😵

  • 부모 클래스인 Parent 클래스에 매개변수를 가진 생성자가 선언되어 있어, 자바 컴파일러에서 자동으로 기본 생성자를 추가해주지 않기 때문이다.
  • 그래서 자식 클래스에서 super()를 통해 부모 클래스의 기본생성자를 호출하게 되면 오류가 발생하는 것.

그럼 어떻게 해야 오류가 안나지?

  • 매개변수를 가지는 생성자를 선언해야 하는 경우 기본 생성자까지 명시적으로 선언하는게 좋다.
    (자식클래스에서 super() 이용해서 부모 클래스의 기본 생성자를 호출했을 때, 부모 클래스에는 기본 생성자가 없어서 오류가 발생할 수 있으니까.)

업 캐스팅 하는 이유? 😶

-> 다형성과 관련 있음 !!
(좀 더 공통적으로 할 수 있는 부분을 간단하게 만들기 위해.)
이해를 돕기 위해 예제를 참고하자.

class 해장국{
   public void 간맞추기(){
   // 뭐든 Ok
   }	
}

class 뼈해장국 extends 해장국{
   @Override
   public void 간맞추기(){
   // 뼈해장국에는 들깨가루 ~!
   }
}

class 콩나물해장국 extends 해장국{
   @Override
   public void 간맞추기(){
   // 콩나물해장국에는 고춧가루 ~!
   }
}

class 취객 {
   public void 해장국먹기(해장국 어떤해장국){
   어떤해장국.간맞추기();
   }
}

public class CastingTest{
   public static void main(String[] args){
      취객 취객1 = new 취객();
      해장국 해장국한그릇 = new 뼈해장국(); 
      // 해장국(상위 클래스) 타입에 뼈해장국(하위 클래스)를 담음. 업캐스팅 !
      취객1.해장국먹기(해장국한그릇);
   }
}

위의 예제에서, 취객 클래스의 해장국 먹기라는 메소드를 호출할 때는
그 해장국이 뼈해장국이든 or 콩나물해장국이든 그냥 해장국 타입의 해장국한그릇 만 넘겨주면 취객은 아무런 걱정없이 (= 타입검사 없이) 먹으면 된다.

하지만 만약 이 코드에서 업캐스팅을 사용하지 않는다면?

  • 각각의 해장국 객체의 메서드를 호출해야 한다면 해장국 한그릇이 뼈해장국인지, 콩나물해장국인지 검사하는 조건문이 추가된 이후에야 각 조건에 맞는 객체의 메서드가 호출될 것이다.

아래의 코드를 보자.(해장국 객체 생성)

해장국 해장국한그릇 = new 해장국(); // 업캐스팅 안했을 때는 이런식으로 될 것.

그다음 해장국 먹기 메소드를 보자.

public void 해장국먹기(해장국 어떤해장국){
   if(뼈해장국 타입){
      뼈해장국.간맞추기();
   }
   else if(콩나물해장국 타입){
      콩나물해장국.간맞추기();
   }
   // 해장국 메뉴가 더 추가된다면 추가로 더 써주어야 한다.
}

다운 캐스팅

다운캐스팅은 자신의 고유한 특성을 잃은 서브 클래스의 객체를 복구시키는 것 이다.
(= 업캐스팅 된 것을 다시 원상태로 돌리는 것)

업캐스팅 과의 다른점?

  • 명시적으로 타입 지정해주어야 한다.
  • 업캐스팅이 선행 된 후 진행되어야 한다.
// 업 캐스팅 선행
Person person = new Student();

// 그 후 다운 캐스팅 해줘야 함. and (Student) 처럼 명시적으로 타입 지정
Student student = (Student) person;

아래의 코드를 보자.

package study.test.java;


class Person{
	String name;
	
	public Person(String name) {
		this.name = name;
	}
}

class Student extends Person{
	
	String age;
	
	public Student(String name) {
		super(name); 
	}
}

public class CastingTest2 {
	public static void main(String[] args) {
		// 업 캐스팅 선행
		Person person = new Student("박세연");
		
		// 다운 캐스팅
		Student student = (Student) person;
		
		// name에 접근. student 타입이니까 전부 접근 가능
		student.name = "세욘이";
		
		// age에 접근
		student.age = "27";
	}
}

위의 코드에서는 다운캐스팅을 하면서 형변환할 대상을 지정했다.
하지만 무분별한 다운캐스팅은 커파일 시점에는 오류가 발생하지 않아도,
런타임 오류를 발생시킬 가능성이 있다.

다음의 예시가 그러하다.

// 오류 발생 (ClassCastException)
Student student2 = (Student) new Person("박세진"); 

// 형변환 타입 명시해서 컴파일 오류는 사라졌지만 
// 실제 코드 수행시 ClassCastException 발생 

instanceof 연산자

객체의 타입을 구분하기 위해 instanceof 연산자를 사용할 수 있다.

ex) 업 캐스팅 했을 때 참조변수가 가리키는 객체의 타입이 어떤 것인지 구분하기 어려울 때 -> 유용하다 !

참조 변수의 타입과 참조변수가 가리키는 인스턴스의 타입은 항상 같지는 않으므로,
참조변수가 가리키는 인스턴스의 타입이 어떤 것인지 확인하기 위해 사용하는게 instanceof인것.

다음의 코드를 보자.

class Unit {
    // 생략
}

class Zealot extends Unit {
    // 생략
}

class Marine extends Unit {
    // 생략
}

class Zergling extends Unit {
    // 생략
}

public class CastingTest {
    public static void main(String[] args) {
        Unit unit;
        unit = new Unit();
        unit = new Zealot(); // 업캐스팅
        unit = new Marine(); // 업캐스팅
        unit = new Zergling(); // 업캐스팅
    }
}

클래스 Zealot, Marine, Zergling은 모두 Unit 클래스를 상속하고 있다.
따라서 위 코드에서의 업 캐스팅 코드는 컴파일 오류 없이 정상적으로 수행 된다.

이때 unit 참조 변수가 어떤 객체를 가리키고 있다고 가정하자.
가리키는 객체의 실제 클래스 타입을 구분하기 위해서는 어떻게 해야 할까?

// 적 공격하기.
public void attackEnemy(Unit unit) {
  // unit이 가리키는 객체가 Unit일 수도 있고
  // Zealot, Marine, Zergling일 수도 있다.
}

위와 같은 메서드가 있다고 할 때, 파라미터로 넘어오는 객체의 실제 클래스 타입을 구분하려면 어떻게 해야할까?

앞에서 언급한 instanceof 연산자를 사용하면 객체의 타입을 쉽게 구별할 수 있다.

public class CastingTest4 {
	public static void main(String[] args) {
		Unit unit1 = new Unit();
		
		Unit unit2 = new Zealot(); // 업캐스팅 
		Unit unit3 = new Marine(); // 업캐스팅
		Unit unit4 = new Zergling(); // 업캐스팅
		
		if (unit1 instanceof Unit) { // true
			System.out.println("unit1은 Unit 타입 ");
		}
		if (unit1 instanceof Zealot) { // false
			System.out.println("unit1은 Zealot 타입이당."); 
		}
                if (unit2 instanceof Zealot) { // true
            		System.out.println("unit2는 Zealot 타입이다.");
        	}
       		if (unit2 instanceof Zergling) { // false
            		System.out.println("unit2는 Zergling 타입이다.");
        }
	}
}

if (unit2 instanceof Zealot ) 은, unit2가 Zealot 타입으로 형변환 될 수 있는지를 묻는 것이다.
앞에서 Unit unit2 = new Zealot(); 을 보면, Zealot 객체를 Unit 타입에 담아서 업 캐스팅을 했음을 알 수 있다. 그러면 unit2는 다시 Zealot 타입으로 다운 캐스팅을 할 수 있으므로, 위의 if문의 결과는 true가 된다.

객체가 실제로 어떤 타입인지 비교할 수 있으므로, 실행 시점에 발생할 수 있는 형변환 오류를 줄일 수 있다.

참고

자바의 데이터 타입은 크게 기본형/참조형의 2가지로 나뉜다.
위에서 다룬 것은 참조형 변수의 형변환이고, 기본형 변수의 타입변환은 아래에 간단하게 다뤘다.

기본 타입간의 타입변환

  • boolean 타입을 제외한 나머지 타입들은 서로 타입변환이 가능하다
  1. 자동 타입 변환 : 작은 크기를 가지는 타입이 큰 크기를 가지는 타입에 저장될 때 발생. () 생략.
    (작은 vs 큰 의 구분 : 메모리 크기)
int i = 3;
double d = 1.0 + i; // double d = 1.0 + (double)i; 에서 형변환이 생략된 것.

2.강제 타입 변환 : 큰 크기의 타입을 작은 데이터 타입으로 쪼개어 저장할 때. 캐스팅 연산자 ()을 사용한다.

double doubleValue = 3.14;
int intValue = (int) doubleValue; // 정수부분인 3만 저장된다.

[주의할 점]
사용자로부터 입력받은 값을 변환할 때 값의 손실이 발생하면 안된다.
강제 타입 변환 하기 전 안전하게 값이 보존될 수 있는지 검사해서 올바른 타입 변환이 되도록 해야함 !


글을 마치면서

자바에 대해 다시금 공부하면서 개념을 정리하는게 이렇게 재밌는 일인 줄 몰랐다.
이럴 줄 알았으면 배울 때 조금이라도 더 열심히 배울걸, 하는 후회도 남지만. 앞으로 열심히 블로그를 포스팅하면서 만회해보려고 한다.

profile
계속해서 기록하는 개발자. 조금씩 성장하기!

3개의 댓글

comment-user-thumbnail
2022년 3월 9일

와 어느 블로그를 봐도 업캐스팅 개념이 이해가 잘 안 됐는데 여기 비유랑 설명이 너무 너무 찰떡이라 바로 이해했네요 감사합니다 ;))

답글 달기
comment-user-thumbnail
2022년 10월 30일

해장국 비유가 재미있었습니다. 감사합니다ㅎㅎ

답글 달기
comment-user-thumbnail
2024년 3월 10일

Wow, no matter how many blogs I read, I struggled to grasp the concept of upcasting. However, the analogy and explanation provided here were so perfect that I immediately understood it. Thank you! delta executor website :)

답글 달기