본문 바로가기

Development/Java

정적 타입 언어(Java)에서 타입 변환(Type Casting)이 필요한 이유

 

 

타입 변환의 개념

 

 

타입 변환(Type Casting 혹은 Type Conversion)은 특정 타입의 값을 다른 타입으로 변환하는 과정이다. 여기에는 변수의 자료형(Data Type)이나 객체의 타입(Object Type)을 다른 타입으로 변경하는 과정이 포함된다.

 

예전 포스팅에서 자료형이 필요한 이유에 대해서 다룬 바 있다(https://hellmir.tistory.com/entry/프로그래밍에서-자료형Data-type이-필요한-이유in-Java).

 

프로그래밍에서 자료형(Data type)이 필요한 이유(in Java)

자료형(Data type)은 데이터를 분류하는 방법이다. 자료형을 통해 해당 자료의 처리되는 데이터 종류, 메모리 할당 과정, 연산 수행 방법을 인지할 수 있다. 예를 들어, Primitive type(기본형)인 int(정

hellmir.tistory.com

 

 

타입 변환의 종류

 

 

정적 타입 언어에서 수행할 수 있는 타입 변환에는 primitive type의 변환과 object type 변환의 두 종류가 있다. 정적 타입 언어와 동적 타입 언어의 차이, primitive type과 object type에 대한 개념 역시 위의 포스팅에 설명되어 있다.

 

 

Primitive Type

 

 

primitive type의 타입 변환은 자동 타입 변환(Automatic Type Conversion)과 명시적 타입 변환(Explicit Type Conversion)으로 나뉜다.

 

  • 자동 타입 변환: 작은 타입에서 큰 타입으로의 타입 변환이다. 큰 타입의 변수는 작은 타입의 자료를 모두 담을 수 있기 때문에 타입 변환의 위험성이 없다. 따라서 Java에서는 따로 명시하지 않아도 타입 변환이 수행된다.
  • 명시적(강제) 타입 변환: 큰 타입에서 작은 타입으로의 타입 변환이다. 작은 타입은 큰 타입의 자료를 모두 담을 수 없기 때문에, 데이터 손실의 위험성을 내포하고 있다. 따라서 개발자는 타입 변환 수행 여부를 명시해, 타입 변환이 실수가 아님을 알린다.

타입의 크기는 byte < short < int < long < float < double 순이다. 큰 타입이 Stack 메모리의 공간을 더 많이 차지하지만, 데이터 저장 기술 발전으로 인해 저장 용량이 비대해진 현재에는 알맞은 타입을 사용해 메모리 공간을 아끼는 것에 아주 큰 의미는 없다.

 

 

Object Type

 

 

마찬가지로 object type의 타입 변환에는 업캐스팅(Upcasting)과 다운캐스팅(DownCasting)의 두 가지 종류가 있다. 이를 통해 객체의 동적 바인딩과 다형성을 구현할 수 있다.

 

  • 업캐스팅: 하위 클래스의 인스턴스(Instance, 생성된 객체)를 상위 클래스 타입으로 변환한다. 상위 클래스 타입은 항상 하나이기 때문에, Java에서는 이를 명시하지 않아도 컴파일러가 정상적으로 인식하고 자동으로 타입을 변환할 수 있다.
  • 다운캐스팅: 하위 클래스의 인스턴스를 담고 있는 상위 클래스의 변수를 하위 클래스 타입으로 재변환한다. 컴파일러는 부모 타입 변수가 어떤 하위 클래스의 타입으로 변환되는지 확인하기 어려우므로, 실수로 인한 변경의 파급효과가 크다. 따라서 개발자는 다운캐스팅을 할 때 타입 변환 대상 하위 타입을 명시해 변환 타입을 알림과 동시에 실수가 아님을 표시해야 하며, 잘못된 다운캐스팅은 ClassCastException의 위험성을 내포하고 있다.

 

타입 변환이 필요한 이유

 

 

정적 타입 언어에서 타입 변환이 필요한 이유에는 여러 가지가 있다.

 

 

다형성(Polymorphism)

 

 

Java에서 객체는 상위 타입의 참조 변수를 통해 참조될 수 있다. 이때 상위 타입의 참조 변수는 자식 객체의 주소를 가리키기 때문에, 자식 객체의 상태 변경을 모두 반영할 수 있으며 자식 객체가 재정의(Overriding)한 자신의 메서드를 사용할 수 있다.

 

public class Parent {
    public void walk() {
        System.out.println("부모가 걷습니다.");
    }
}

 

public class Child extends Parent {

    @Override
    public void walk() {
        super.walk();
        System.out.println("자식도 따라 걷습니다.");
    }

    public void run() {
        System.out.println("자식이 달립니다.");
    }

}

 

public class Main {

    public static void main(String[] args) {

        Parent child = new Child();

        child.walk(); // 부모가 걷습니다.\n 자식도 따라 걷습니다.

    }

}

 

다만 자식 객체의 참조를 부모 타입에 담으면 자식 클래스에서 추가된 메서드는 부모 클래스에 존재하지 않기 때문에, 부모 타입 변수가 자식 객체를 참조하더라도 사용할 수 없다. 이 경우 다운캐스팅을 통해 자식 타입의 변수로 변환하면 자식 클래스의 메서드를 사용할 수 있다.

 

public class Main {

    public static void main(String[] args) {

        Parent child = new Child();

        child.run(); // Compile error

        ((Child) child).run(); // 자식이 달립니다.

    }

}

 

 

메서드 오버로딩(Method Overloading)

 

 

메서드 오버로딩을 통해 같은 이름(책임)을 가진 메서드들이 서로 다른 타입의 매개 변수를 갖도록 할 수 있다. 이를 통해 실행시간에 클라이언트의 입력값에 따라 수행 메서드를 결정할 수 있고, 클라이언트가 원하는 결과를 제공함으로써 다형성을 구현할 수 있다.

 

public class Parent {

    private String name;

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

    public String getName() {
        return name;
    }

}

 

public class Child extends Parent {

    private String name;

    public Child(String name) {
        
        super(name);
        this.name = name;
    }
    

    public String getName() {
        return name;
    }

}

 

public class Runner {

    public void run(Parent parent) {
        System.out.println(parent.getName() + "와 함께 달립니다.");
    }

    public void run(Child child) {
        System.out.println(child.getName() + "과 함께 달립니다.");
    }

}

 

public class Main {

    public static void main(String[] args) {

        Runner runner = new Runner();

        Parent parent = new Parent("부모");
        Parent child = new Child("자식");

        runner.run(parent); // 부모와 함께 달립니다.
        runner.run(child); // 자식와 함께 달립니다.
        runner.run( (Child) child ); // 자식과 함께 달립니다.

    }

}

 

오버로딩이 필요치 않은 경우, 상속과 업캐스팅을 통해 같은 메서드로 다형성을 구현할 수도 있다.

 

public class Runner {

    public void run(Parent parent) {
        System.out.println(parent.getName() + "도 함께 달립니다.");
    }

}

 

public class Main {

    public static void main(String[] args) {

        Runner runner = new Runner();

        Parent parent = new Parent("부모");
        Child child = new Child("자식");

        runner.run(parent); // 부모도 함께 달립니다.
        runner.run(child); // 자식도 함께 달립니다.

    }

}

 

 

인터페이스 구현

 

 

인터페이스 구현 시 인터페이스의 타입에 구현 객체를 담을 수 있다.

 

public interface Service {

    void walk(Parent parent);

    void run(Parent parent);

}

 

public class ServiceImpl implements Service {

    @Override
    public void walk(Parent parent) {
        System.out.println(parent.getName() + "도 함께 걷습니다.");
    }

    @Override
    public void run(Parent parent) {
        System.out.println(parent.getName() + "도 함께 달립니다.");
    }

}

 

public class Main {

    public static void main(String[] args) {

        Service service = new ServiceImpl();

        Parent parent = new Parent("부모");
        Child child = new Child("자식");

        service.walk(parent); // 부모도 함께 걷습니다.
        service.walk(child); // 자식도 함께 걷습니다.

        service.run(parent); // 부모도 함께 달립니다.
        service.run(child); // 자식도 함께 달립니다.

    }

}

 

추상 클래스의 상속과 인터페이스의 구현은 언뜻 보기에 비슷해 보이지만 역할이 다르다. 상속과 인터페이스의 주요한 차이는 다음과 같다.

  1. 상속은 클래스의 확장(extends)에, 인터페이스는 책임(기능)의 구현(implements)에 사용된다.
  2. 부모의 추상 메서드를 공통 특성의 재사용 측면에서 활용하는 상속과 달리, 인터페이스의 모든 추상 메서드의 구현은 일종의 계약이다.  인터페이스의 추상 메서드는 클라이언트에게 약속한 필수적인 책임 목록이므로, 클라이언트는 자신의 책임이 위임을 통해 해결될 수 있는지 여부를 인지할 수 있다.
  3. Java에서는 하나의 부모만을 상속할 수 있지만, 인터페이스는 여러 개를 구현할 수 있다.

 

 

제네릭(Generic)의 타입 변환

 

 

제네릭을 통해 별도의 캐스팅 없이 컴파일러에게 타입 검사를 맡길 수 있다. 이를 통해 유효하지 않은 데이터가 저장되거나, 실행 도중 오류가 발생하는 상황을 방지할 수 있다.

 

List<Integer> list = new ArrayList<Integer>();

 

이는 데이터의 무결성을 보장하기 때문에 편리하지만, 자료를 저장/검색/활용하는 과정에서 타입 변환이 필요할 수 있다.

 

public class Main {

    public static void main(String[] args) {

        List<Integer> list = new ArrayList<Integer>(); // 인터페이스 구현객체의 업캐스팅

        list.add(1);
        list.add(2);
        list.add(3);

        String str = "";

        str += list.get(0);
        str += list.get(1);
        str += list.get(2);

        System.out.println(str); // 자동 타입 변환: 123

        list.add(str); // Compile error

        list.add(Integer.parseInt(str));

        System.out.println(list.get(list.size() - 1)); // 명시적 타입 변환: 123

    }

}

 

 

API(Application Programming Interface)에서의 활용

 

 

API는 서로 다른 애플리케이션을 연결하기 때문에, 상호 교환되는 타입이 일치하지 않는 경우가 많다. 따라서 타입 변환을 통해 호환성을 높이면 상호작용 과정에서 도움을 줄 수 있다.

 

참고 자료
https://songacoding.tistory.com/97
 

나에게.. 다형성이란..?

안녕하세요, 송아지할때 송아 김송아입니다. 오늘은 간단하게(?) 다형성에 대해 얘기를 나눠볼까 합니다. 저도 알아요.. 사실 다형성이ㅋㅋㅋ 간단할 수가.... (먼산😇) 그치만, 제가 드리고 싶은

songacoding.tistory.com