자바 다형성과 그 유형

다형성 은 일부 엔티티가 다른 형태로 발생하는 능력을 나타냅니다. 그것은 애벌레에서 번데기, 성충으로 변하는 나비로 널리 표현됩니다. 다형성은 다양한 피연산자, 인수 및 개체에 대한 단일 인터페이스를 만들 수있는 모델링 기술로서 프로그래밍 언어에도 존재합니다. Java 다형성은 더 간결하고 유지하기 쉬운 코드를 생성합니다.

이 튜토리얼은 하위 유형 다형성에 초점을 맞추고 있지만 알아야 할 몇 가지 다른 유형이 있습니다. 4 가지 유형의 다형성에 대한 개요부터 시작하겠습니다.

다운로드 코드 받기이 자습서에서 예제 응용 프로그램의 소스 코드를 다운로드합니다. JavaWorld를 위해 Jeff Friesen이 만들었습니다.

자바의 다형성 유형

Java에는 네 가지 유형의 다형성이 있습니다.

  1. 강제 변환은 암시 적 형식 변환을 통해 여러 형식을 제공하는 작업입니다. 예를 들어, 정수를 다른 정수로 나누거나 부동 소수점 값을 다른 부동 소수점 값으로 나눕니다. 한 피연산자가 정수이고 다른 피연산자가 부동 소수점 값이면 컴파일러 정수를 부동 소수점 값으로 강제 변환 (암시 적으로 변환)하여 형식 오류를 방지합니다. (정수 피연산자와 부동 소수점 피연산자를 지원하는 나누기 연산은 없습니다.) 또 다른 예는 메서드의 수퍼 클래스 매개 변수에 하위 클래스 개체 참조를 전달하는 것입니다. 컴파일러는 작업을 수퍼 클래스의 작업으로 제한하기 위해 하위 클래스 유형을 수퍼 클래스 유형으로 강제 변환합니다.
  2. 오버로딩 은 다른 컨텍스트에서 동일한 연산자 기호 또는 메서드 이름을 사용하는 것을 의미합니다. 예를 들어, +피연산자의 유형에 따라 정수 더하기, 부동 소수점 더하기 또는 문자열 연결을 수행 하는 데 사용할 수 있습니다 . 또한 동일한 이름을 가진 여러 메서드가 선언 및 / 또는 상속을 통해 클래스에 나타날 수 있습니다.
  3. 파라 메트릭 다형성은 클래스 선언 내에서 필드 이름이 다른 유형과 연관 될 수 있고 메소드 이름이 다른 매개 변수 및 리턴 유형과 연관 될 수 있음을 규정합니다. 그러면 필드와 메서드가 각 클래스 인스턴스 (객체)에서 서로 다른 유형을 취할 수 있습니다. 예를 들어 필드는 유형 Double( double값 을 래핑하는 Java의 표준 클래스 라이브러리의 구성원 )이고 메서드는 Double하나의 객체에서를 반환 할 수 있으며 동일한 필드는 유형일 수 있으며 String동일한 메서드 String는 다른 객체에서 a 를 반환 할 수 있습니다. . Java는 제네릭을 통해 파라 메트릭 다형성을 지원합니다. 이에 대해서는 향후 기사에서 설명하겠습니다.
  4. 하위 유형 은 유형이 다른 유형의 하위 유형으로 사용될 수 있음을 의미합니다. 하위 유형 인스턴스가 상위 유형 컨텍스트에 나타날 때 하위 유형 인스턴스에서 상위 유형 작업을 실행하면 해당 작업의 하위 유형 버전이 실행됩니다. 예를 들어 임의의 모양을 그리는 코드 조각을 생각해보십시오. 메소드가 있는 Shape클래스를 도입하여이 그리기 코드를보다 간결하게 표현할 수 있습니다 draw(). 도입하여 Circle, Rectangle오버라이드 (override)하는 것을, 그리고 다른 서브 클래스 draw(); Shape요소가 Shape하위 클래스 인스턴스에 대한 참조를 저장 하는 유형 의 배열을 도입 합니다. 각 인스턴스에서 Shapedraw()메소드를 호출 합니다. 를 호출 draw()하면 Circle's, Rectangle's 또는 다른 Shape인스턴스의draw()호출되는 메서드. Shapedraw()방법 에는 여러 형태가 있다고합니다 .

이 튜토리얼에서는 하위 유형 다형성을 소개합니다. 업 캐스팅 및 후기 바인딩, 추상 클래스 (인스턴스화 할 수 없음) 및 추상 메서드 (호출 할 수 없음)에 대해 배우게됩니다. 또한 다운 캐스팅 및 런타임 유형 식별에 대해 배우고 공변 반환 유형을 먼저 살펴 봅니다. 향후 튜토리얼을 위해 파라 메트릭 다형성을 저장하겠습니다.

임시 다형성 대 범용 다형성

많은 개발자와 마찬가지로 필자는 강제 및 과부하를 임시 다형성으로 분류하고 매개 변수 및 하위 유형을 범용 다형성으로 분류합니다. 귀중한 기술이기는하지만 강압과 과부하가 진정한 다형성이라고는 생각하지 않습니다. 유형 변환 및 구문 설탕과 더 비슷합니다.

하위 유형 다형성 : 업 캐스팅 및 후기 바인딩

하위 유형 다형성은 업 캐스팅 및 후기 바인딩에 의존합니다. Upcasting은 당신이 슈퍼에 하위 유형에서 상속 계층 구조를 주조 주조의 한 형태이다. 하위 유형이 상위 유형의 전문화이므로 캐스트 연산자가 포함되지 않습니다. 예를 들어, Shape s = new Circle();에서 업 캐스팅 CircleShape. 이것은 원이 일종의 모양이기 때문에 의미가 있습니다.

upcasting 후 CircleShape, 당신은 호출 할 수 없습니다 Circle예와 같은 특이 방법, getRadius()방법을 그 반환 원의 반지름 때문에 Circle특이 방법의 일부가 아닌 Shape의 인터페이스를 제공합니다. 하위 클래스를 수퍼 클래스로 좁힌 후 하위 유형 기능에 대한 액세스 권한을 잃는 것은 무의미 해 보이지만 하위 유형 다형성을 달성하는 데 필요합니다.

메서드 를 Shape선언 draw()하고 Circle하위 클래스가이 메서드를 재정의 Shape s = new Circle();하고 방금 실행했으며 다음 줄에서를 지정 한다고 가정 합니다 s.draw();. 어떤 draw()방법이 호출됩니다 Shapedraw()방법 또는 Circledraw()방법은? 컴파일러는 draw()호출 할 메서드를 모릅니다 . 할 수있는 일은 메서드가 수퍼 클래스에 존재하는지 확인하고 메서드 호출의 인수 목록과 반환 유형이 수퍼 클래스의 메서드 선언과 일치하는지 확인하는 것입니다. 그러나 컴파일러는 런타임시 s올바른 draw()메서드 를 호출하기 위해 참조가 무엇이든 가져오고 사용하는 명령을 컴파일 된 코드에 삽입합니다 . 이 작업을 후기 바인딩이라고 합니다.

늦은 바인딩과 초기 바인딩

지연 바인딩은 final인스턴스 가 아닌 메서드에 대한 호출에 사용됩니다 . 다른 모든 메서드 호출의 경우 컴파일러는 호출 할 메서드를 알고 있습니다. 변수의 값이 아니라 변수의 유형과 관련된 메서드를 호출하는 명령을 컴파일 된 코드에 삽입합니다. 이 기술을 초기 바인딩이라고 합니다.

업 캐스팅 및 후기 바인딩 측면에서 하위 유형 다형성을 보여주는 응용 프로그램을 만들었습니다. 이 응용 프로그램의 구성은 Shape, Circle, Rectangle, 그리고 Shapes각 클래스 자체 소스 파일에 저장되어있는 클래스. 목록 1은 처음 세 개의 클래스를 보여줍니다.

Listing 1. 셰이프 계층 선언

class Shape { void draw() { } } class Circle extends Shape { private int x, y, r; Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; } // For brevity, I've omitted getX(), getY(), and getRadius() methods. @Override void draw() { System.out.println("Drawing circle (" + x + ", "+ y + ", " + r + ")"); } } class Rectangle extends Shape { private int x, y, w, h; Rectangle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } // For brevity, I've omitted getX(), getY(), getWidth(), and getHeight() // methods. @Override void draw() { System.out.println("Drawing rectangle (" + x + ", "+ y + ", " + w + "," + h + ")"); } }

목록 2는 메소드가 애플리케이션을 구동하는 애플리케이션 Shapes클래스를 보여줍니다 main().

Listing 2. 하위 유형 다형성의 업 캐스팅 및 후기 바인딩

class Shapes { public static void main(String[] args) { Shape[] shapes = { new Circle(10, 20, 30), new Rectangle(20, 30, 40, 50) }; for (int i = 0; i < shapes.length; i++) shapes[i].draw(); } }

shapes배열 의 선언은 업 캐스팅을 보여줍니다. CircleRectangle참조에 저장됩니다 shapes[0]shapes[1]및 업 캐스팅은 입력 할 수 있습니다 Shape. 각 shapes[0]과는 shapes[1]A와 여겨진다 Shape예 : shapes[0]A와 간주되지 않는다 Circle; shapes[1]으로 간주되지 않습니다 Rectangle.

늦은 바인딩은 shapes[i].draw();표현에 의해 입증됩니다 . 때 i와 동일 0, 컴파일러가 생성 한 명령어 원인 Circledraw()메소드가 호출된다. 그러나 i같으면 1이 명령어는 Rectangledraw()메서드가 호출되도록합니다. 이것이 아형 다형성의 본질입니다.

가정하면 네 개의 소스 파일 ( Shapes.java, Shape.java, Rectangle.java,과 Circle.java), 현재 디렉토리에있는 다음 명령 줄 중 하나를 통해 그들을 컴파일하고 있습니다 :

javac *.java javac Shapes.java

결과 애플리케이션을 실행하십시오.

java Shapes

다음 출력을 관찰해야합니다.

Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)

추상 클래스 및 메서드

클래스 계층 구조를 디자인 할 때 이러한 계층 구조의 위쪽에 가까운 클래스가 아래쪽에있는 클래스보다 더 일반적이라는 것을 알 수 있습니다. 예를 들어, Vehicle수퍼 클래스는 Truck서브 클래스 보다 더 일반적 입니다. 마찬가지로 Shape수퍼 클래스는 Circle또는 Rectangle서브 클래스 보다 더 일반적 입니다.

제네릭 클래스를 인스턴스화하는 것은 의미가 없습니다. 결국 Vehicle객체는 무엇을 설명할까요? 비슷하게, 어떤 모양이 Shape물체 로 표현 됩니까? 에서 빈 draw()메서드를 코딩하는 대신 Shape두 엔터티를 추상으로 선언하여이 메서드가 호출되고이 클래스가 인스턴스화되는 것을 방지 할 수 있습니다.

Java는 abstract인스턴스화 할 수없는 클래스를 선언하기 위해 예약어를 제공합니다 . 이 클래스를 인스턴스화하려고하면 컴파일러에서 오류를보고합니다. abstract본문없이 메서드를 선언하는데도 사용됩니다. 이 draw()방법은 추상적 인 모양을 그릴 수 없기 때문에 본문이 필요하지 않습니다. 목록 3은이를 보여줍니다.

Listing 3. Shape 클래스와 그 draw () 메서드 추상화

abstract class Shape { abstract void draw(); // semicolon is required }

추상주의

The compiler reports an error when you attempt to declare a class abstract and final. For example, the compiler complains about abstract final class Shape because an abstract class cannot be instantiated and a final class cannot be extended. The compiler also reports an error when you declare a method abstract but don't declare its class abstract. Removing abstract from the Shape class's header in Listing 3 would result in an error, for instance. This would be an error because a non-abstract (concrete) class cannot be instantiated when it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all of the abstract methods, or else the extending class must itself be declared to be abstract; otherwise, the compiler will report an error.

An abstract class can declare fields, constructors, and non-abstract methods in addition to or instead of abstract methods. For example, an abstract Vehicle class might declare fields describing its make, model, and year. Also, it might declare a constructor to initialize these fields and concrete methods to return their values. Check out Listing 4.

Listing 4. Abstracting a vehicle

abstract class Vehicle { private String make, model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } abstract void move(); }

You'll note that Vehicle declares an abstract move() method to describe the movement of a vehicle. For example, a car rolls down the road, a boat sails across the water, and a plane flies through the air. Vehicle's subclasses would override move() and provide an appropriate description. They would also inherit the methods and their constructors would call Vehicle's constructor.

Downcasting and RTTI

Moving up the class hierarchy, via upcasting, entails losing access to subtype features. For example, assigning a Circle object to Shape variable s means that you cannot use s to call Circle's getRadius() method. However, it's possible to once again access Circle's getRadius() method by performing an explicit cast operation like this one: Circle c = (Circle) s;.

This assignment is known as downcasting because you are casting down the inheritance hierarchy from a supertype to a subtype (from the Shape superclass to the Circle subclass). Although an upcast is always safe (the superclass's interface is a subset of the subclass's interface), a downcast isn't always safe. Listing 5 shows what kind of trouble could ensue if you use downcasting incorrectly.

Listing 5. The problem with downcasting

class Superclass { } class Subclass extends Superclass { void method() { } } public class BadDowncast { public static void main(String[] args) { Superclass superclass = new Superclass(); Subclass subclass = (Subclass) superclass; subclass.method(); } }

Listing 5 presents a class hierarchy consisting of Superclass and Subclass, which extends Superclass. Furthermore, Subclass declares method(). A third class named BadDowncast provides a main() method that instantiates Superclass. BadDowncast then tries to downcast this object to Subclass and assign the result to variable subclass.

이 경우 컴파일러는 수퍼 클래스에서 동일한 유형 계층 구조의 서브 클래스로 다운 캐스트하는 것이 합법적이기 때문에 불평하지 않습니다. 즉, 할당이 허용되면 실행하려고 할 때 응용 프로그램이 충돌합니다 subclass.method();. 이 경우 JVM은를 Superclass선언하지 않기 때문에 존재하지 않는 메서드를 호출하려고합니다 method(). 다행히 JVM은 캐스트 작업을 수행하기 전에 캐스트가 합법적인지 확인합니다. Superclass선언하지 않는 것을 감지 method()하면 ClassCastException객체를 던질 것 입니다. (다음 기사에서 예외에 대해 논의 할 것입니다.)

다음과 같이 목록 5를 컴파일하십시오.

javac BadDowncast.java

결과 애플리케이션을 실행하십시오.

java BadDowncast