JavaRush /Java Blog /Random-KO /다형성과 그 친구들
Viacheslav
레벨 3

다형성과 그 친구들

Random-KO 그룹에 게시되었습니다
다형성은 객체지향 프로그래밍의 기본 원칙 중 하나입니다. 이를 통해 Java의 강력한 타이핑 기능을 활용하고 사용 가능하고 유지 관리 가능한 코드를 작성할 수 있습니다. 그에 대해 많은 말이 있지만 모두가 이 리뷰에서 새로운 것을 얻을 수 있기를 바랍니다.
다형성과 그 친구들 - 1

소개

나는 우리 모두가 Java 프로그래밍 언어가 Oracle에 속한다는 것을 알고 있다고 생각합니다. 따라서 우리의 경로는 www.oracle.com 사이트에서 시작됩니다 . 메인 페이지에 "메뉴"가 있습니다. 그 안에는 "문서" 섹션에 "Java" 하위 섹션이 있습니다. 언어의 기본 기능과 관련된 모든 내용은 "Java SE 문서"에 속하므로 이 섹션을 선택합니다. 최신 버전에 대한 설명서 섹션이 열리지만 지금은 "다른 릴리스를 찾고 계십니까?" JDK8 옵션을 선택해 보겠습니다. 페이지에는 다양한 옵션이 표시됩니다. 그러나 우리는 언어 학습: " Java 자습서 학습 경로 "에 관심이 있습니다. 이 페이지에는 " Java 언어 학습 " 이라는 또 다른 섹션이 있습니다 . 이것은 Oracle의 Java 기본에 대한 튜토리얼인 가장 신성한 것입니다. Java는 객체지향 프로그래밍 언어(OOP)이므로 오라클 웹사이트에서도 언어 학습은 " 객체지향 프로그래밍 개념 "이라는 기본 개념에 대한 논의부터 시작됩니다. 이름 자체에서 Java가 객체 작업에 중점을 두고 있음이 분명합니다. " 객체란 무엇입니까? " 하위 섹션을 보면 Java의 객체는 상태와 동작으로 구성되어 있음이 분명합니다. 은행 계좌가 있다고 상상해 보세요. 계좌에 있는 금액이 상태이고, 이 상태를 다루는 방법이 행동입니다. 객체는 어떻게든 설명되어야 하며(어떤 상태와 동작을 가질 수 있는지 알려줌) 이 설명은 클래스 입니다 . 우리가 어떤 클래스의 객체를 생성할 때, 우리는 이 클래스를 지정하고 이를 " 객체 유형 "이라고 합니다. 따라서 Java는 " 4장. 유형, 값 및 변수 " 섹션의 Java 언어 사양에 명시된 대로 강력한 유형의 언어라고 합니다 . Java 언어는 OOP 개념을 따르고 확장 키워드를 사용하여 상속을 지원합니다. 확장하는 이유는 무엇입니까? 상속을 통해 하위 클래스는 상위 클래스의 동작과 상태를 상속하고 이를 보완할 수 있습니다. 기본 클래스의 기능을 확장합니다. Implements 키워드를 사용하여 클래스 설명에 인터페이스를 지정할 수도 있습니다. 클래스가 인터페이스를 구현할 때 이는 클래스가 일부 계약, 즉 클래스가 특정 동작을 가지고 있음을 나머지 환경에 선언하는 프로그래머를 준수한다는 것을 의미합니다. 예를 들어 플레이어에는 다양한 버튼이 있습니다. 이 버튼은 플레이어의 동작을 제어하기 위한 인터페이스이며 동작에 따라 플레이어의 내부 상태(예: 볼륨)가 변경됩니다. 이 경우 설명으로서의 상태와 동작은 클래스를 제공합니다. 클래스가 인터페이스를 구현하는 경우 이 클래스에 의해 생성된 객체는 클래스뿐만 아니라 인터페이스에 의한 유형으로도 설명될 수 있습니다. 예를 살펴보겠습니다:
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
유형은 매우 중요한 설명입니다. 이는 우리가 객체를 어떻게 사용할 것인지를 알려줍니다. 우리가 객체로부터 어떤 행동을 기대하는지. 행동은 방법입니다. 따라서 방법을 이해합시다. Oracle 웹 사이트의 메소드에는 Oracle Tutorial: " 메소드 정의 "에 자체 섹션이 있습니다. 이 기사에서 가장 먼저 알아야 할 사항: 메서드 서명은 메서드 이름과 매개 변수 유형입니다 .
다형성과 그 친구들 - 2
예를 들어, public void method(Object o) 메소드를 선언할 때 서명은 메소드 이름과 매개변수 Object 유형이 됩니다. 반환 유형은 서명에 포함되지 않습니다. 그건 중요해! 다음으로 소스 코드를 컴파일해 보겠습니다. 우리가 알고 있듯이, 이를 위해서는 클래스 이름과 확장자가 java.dll인 파일에 코드를 저장해야 합니다. Java 코드는 " javac " 컴파일러를 사용하여 JVM(Java Virtual Machine)에서 실행할 수 있는 중간 형식으로 컴파일됩니다 . 이 중간 형식은 바이트코드라고 하며 .class 확장자를 가진 파일에 포함되어 있습니다. 컴파일 명령을 실행해 보겠습니다. javac MusicPlayer.java Java 코드가 컴파일된 후 이를 실행할 수 있습니다. " java " 유틸리티를 사용하여 시작하면 Java 가상 머신 프로세스가 시작되어 클래스 파일에 전달된 바이트코드를 실행합니다. 애플리케이션을 시작하는 명령을 실행해 보겠습니다 java MusicPlayer. println 메소드의 입력 매개변수에 지정된 텍스트를 화면에서 볼 수 있습니다. 흥미롭게도 .class 확장자를 가진 파일에 바이트코드가 있으면 " javap " 유틸리티를 사용하여 이를 볼 수 있습니다. <ocde>javap -c MusicPlayer 명령을 실행해 보겠습니다.
다형성과 그 친구들 - 3
바이트코드에서 클래스가 지정된 유형의 객체를 통해 메소드를 호출하는 것은 를 사용하여 수행되고 invokevirtual컴파일러는 어떤 메소드 서명을 사용해야 하는지 계산했음을 알 수 있습니다. 왜 invokevirtual? 가상 메서드 호출(호출은 호출로 번역됨)이 있기 때문입니다. 가상 방법이란 무엇입니까? 프로그램 실행 중에 본문을 재정의할 수 있는 메서드입니다. 특정 키(메서드 서명)와 메서드 본문(코드) 사이의 대응 목록이 있다고 간단히 상상해 보십시오. 그리고 키와 메서드 본문 간의 이러한 대응 관계는 프로그램 실행 중에 변경될 수 있습니다. 따라서 이 방법은 가상입니다. 기본적으로 Java에서는 정적이 아니고 최종이 아니며 비공개가 아닌 메서드가 가상입니다. 덕분에 Java는 다형성이라는 객체 지향 프로그래밍 원칙을 지원합니다. 이미 이해하셨겠지만 이것이 오늘 우리의 리뷰에 관한 것입니다.

다형성

Oracle 웹사이트의 공식 튜토리얼에는 " 다형성 "이라는 별도의 섹션이 있습니다. Java Online Compiler를 사용하여 Java에서 다형성이 어떻게 작동하는지 살펴보겠습니다. 예를 들어, Java에서 숫자를 나타내는 추상 클래스 Number가 있습니다 . 그것은 무엇을 허용합니까? 그는 모든 상속자가 갖게 될 몇 가지 기본 기술을 가지고 있습니다. Number를 상속받은 사람은 문자 그대로 "나는 숫자입니다. 당신은 숫자로서 나와 함께 일할 수 있습니다."라고 말합니다. 예를 들어, 후속 작업에 대해 intValue() 메서드를 사용하여 해당 Integer 값을 가져올 수 있습니다. Number에 대한 Java API를 보면 메소드가 추상적이라는 것을 알 수 있습니다. 즉, Number의 각 후속 항목은 이 메소드 자체를 구현해야 합니다. 그러나 이것이 우리에게 무엇을 주는가? 예를 살펴보겠습니다:
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
예제에서 볼 수 있듯이 다형성 덕분에 모든 유형의 인수를 입력으로 받아들이는 메서드를 작성할 수 있습니다. 이는 Number의 자손이 될 것입니다(Number는 추상 클래스이기 때문에 얻을 수 없습니다). 플레이어 예제의 경우와 마찬가지로 이 경우에는 Number와 같은 작업을 원한다고 말하고 있습니다. 우리는 숫자인 사람은 누구나 정수 값을 제공할 수 있어야 한다는 것을 알고 있습니다. 그리고 그것은 우리에게 충분합니다. 우리는 특정 객체의 구현에 대해 자세히 설명하고 싶지 않고 Number의 모든 자손에 공통적인 방법을 통해 이 객체를 사용하고 싶습니다. 우리가 사용할 수 있는 메소드 목록은 컴파일 타임에 유형에 따라 결정됩니다(앞서 바이트코드에서 본 것처럼). 이 경우 유형은 Number입니다. 예제에서 볼 수 있듯이 다양한 유형의 다양한 수를 전달합니다. 즉, summ 메서드는 Integer, Long 및 Double을 입력으로 받습니다. 그러나 이들 모두의 공통점은 추상 Number의 자손이므로 intValue 메소드에서 해당 동작을 재정의한다는 것입니다. 각 특정 유형은 해당 유형을 Integer로 변환하는 방법을 알고 있습니다. 이러한 다형성은 소위 재정의(English Overriding)를 통해 구현됩니다.
다형성과 그 친구들 - 4
재정의 또는 동적 다형성. 이제 다음 내용으로 HelloWorld.java 파일을 저장하는 것부터 시작하겠습니다.
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
javac HelloWorld.java다음과 같이 해보자 javap -c HelloWorld:
다형성과 그 친구들 - 5
보시다시피, 메서드 호출이 있는 줄의 바이트코드에는 호출하는 메서드에 대한 동일한 참조가 표시됩니다 invokevirtual (#6). 해보자 java HelloWorld. 보시다시피 parent 및 child 변수는 Parent 유형으로 선언되지만 구현 자체는 변수에 할당된 개체(즉, 개체 유형)에 따라 호출됩니다. 프로그램 실행 중에(런타임에서도 말함) JVM은 객체에 따라 동일한 서명을 사용하여 메서드를 호출할 때 다른 메서드를 실행했습니다. 즉, 해당 서명의 키를 사용하여 먼저 하나의 메서드 본문을 받은 다음 다른 메서드 본문을 받았습니다. 변수에 어떤 개체가 있는지에 따라 다릅니다. 프로그램 실행 시 어떤 메소드를 호출할지 결정하는 것을 후기 바인딩(Late Binding) 또는 동적 바인딩(Dynamic Binding)이라고도 합니다. 즉, 메소드가 호출되는 객체에 따라 시그니처와 메소드 본문 간의 일치가 동적으로 수행됩니다. 당연히 클래스의 정적 멤버(클래스 멤버)는 물론 액세스 유형이 private 또는 final인 클래스 멤버도 재정의할 수 없습니다. @Override 주석은 개발자에게도 도움이 됩니다. 이는 컴파일러가 이 시점에서 조상 메서드의 동작을 재정의할 것임을 이해하는 데 도움이 됩니다. 메소드 서명에 실수가 있으면 컴파일러는 즉시 이에 대해 알려줍니다. 예를 들어:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
다음 오류와 함께 컴파일되지 않습니다. 오류: 메서드가 상위 유형의 메서드를 재정의하거나 구현하지 않습니다.
다형성과 그 친구들 - 6
재정의는 " 공분산 " 이라는 개념과도 연관되어 있습니다 . 예를 살펴보겠습니다:
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
명백히 난해함에도 불구하고, 그 의미는 재정의할 때 조상에 지정된 유형뿐만 아니라 더 구체적인 유형도 반환할 수 있다는 사실로 귀결됩니다. 예를 들어, 조상은 Number를 반환했고 우리는 Number의 자손인 Integer를 반환할 수 있습니다. 메서드의 throw에 선언된 예외에도 동일하게 적용됩니다. 상속자는 메서드를 재정의하고 발생한 예외를 구체화할 수 있습니다. 하지만 확장할 수는 없습니다. 즉, 부모가 IOException을 던지면 더 정확한 EOFException을 던질 수 있지만 예외는 던질 수 없습니다. 마찬가지로 범위를 좁힐 수도 없고 추가적인 제한을 가할 수도 없습니다. 예를 들어 정적을 추가할 수 없습니다.
다형성과 그 친구들 - 7

숨김

' 은폐 ' 라는 것도 있습니다 . 예:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
이것은 곰곰히 생각해보면 너무나 당연한 일이다. 클래스의 정적 멤버는 클래스에 속합니다. 변수의 유형에. 따라서 child가 Parent 유형인 경우 메소드가 child가 아닌 Parent에서 호출되는 것이 논리적입니다. 앞서 했던 것처럼 바이트코드를 살펴보면 Invokestatic을 사용하여 정적 메서드가 호출되는 것을 볼 수 있습니다. 이는 Invokevirtual 또는 Invokeinterface처럼 메소드 테이블이 아닌 유형을 확인해야 함을 JVM에 설명합니다.
다형성과 그 친구들 - 8

오버로딩 방법

Java Oracle Tutorial에서 또 무엇을 볼 수 있나요? 이전에 연구한 " 메소드 정의 " 섹션에는 오버로딩에 관한 내용이 있습니다. 그것은 무엇입니까? 러시아어에서는 이를 "메서드 오버로딩"이라고 하며 이러한 메서드를 "오버로드"라고 합니다. 즉, 메소드 오버로딩입니다. 언뜻보기에 모든 것이 간단합니다. 예를 들어 tutorialspoint online java 컴파일러와 같은 온라인 Java 컴파일러를 열어 보겠습니다 .
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
그래서 여기서는 모든 것이 단순 해 보입니다. Oracle 튜토리얼에 명시된 대로 오버로드된 메서드(이 경우 say 메서드)는 메서드에 전달되는 인수의 수와 유형이 다릅니다. 동일한 이름과 동일한 수의 동일한 유형의 인수를 선언할 수 없습니다. 컴파일러는 이들을 서로 구별할 수 없습니다. 매우 중요한 점을 바로 주목할 가치가 있습니다.
다형성과 그 친구들 - 9
즉, 오버로드할 때 컴파일러는 정확성을 확인합니다. 그건 중요해. 하지만 컴파일러는 특정 메서드를 호출해야 하는지 실제로 어떻게 결정합니까? 이는 Java 언어 사양: " 15.12.2.5. 가장 구체적인 방법 선택 "에 설명된 "가장 구체적인 방법 " 규칙을 사용합니다. 작동 방식을 보여주기 위해 Oracle Certified Professional Java Programmer의 예를 들어 보겠습니다.
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
여기에서 예를 들어보세요: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... 보시다시피, 우리는 통과하고 있습니다 메소드에 null입니다. 컴파일러는 가장 구체적인 유형을 결정하려고 시도합니다. 객체가 적합하지 않은 이유는 다음과 같습니다. 모든 것은 그에게서 물려받은 것입니다. 계속하세요. 예외에는 2가지 클래스가 있습니다. java.io.IOException을 살펴보고 "Direct Known Subclasses"에 FileNotFoundException이 있는지 살펴보겠습니다. 즉, FileNotFoundException이 가장 구체적인 유형임이 밝혀졌습니다. 따라서 결과는 "FileNotFoundException" 문자열의 출력이 됩니다. 그러나 IOException을 EOFException으로 바꾸면 유형 트리에서 동일한 계층 구조 수준에 두 개의 메서드가 있는 것으로 나타납니다. 즉, 두 메서드 모두에 대해 IOException이 부모입니다. 컴파일러는 호출할 메서드를 선택할 수 없으며 컴파일 오류가 발생합니다 reference to method is ambiguous. 또 하나의 예:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
1이 출력됩니다. 여기에는 질문이 없습니다. int... 유형은 vararg https://docs.oracle.com/javase/8/docs/technotes/guides/언어/varargs.html이며 실제로 "구문 설탕"에 지나지 않으며 실제로는 int입니다. .. 배열은 int[] 배열로 읽을 수 있습니다. 이제 메소드를 추가하면:
public static void method(long a, long b) {
	System.out.println("2");
}
그러면 1이 아닌 2가 표시됩니다. 우리는 2개의 숫자를 전달하고 있으며 2개의 인수가 하나의 배열보다 더 잘 일치합니다. 메소드를 추가하면:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
그런 다음 여전히 2를 볼 수 있습니다. 이 경우 기본 요소는 Integer의 boxing보다 더 정확하게 일치하기 때문입니다. 그러나 실행하면 method(new Integer(1), new Integer(2));3이 인쇄됩니다 . Java의 생성자는 메소드와 유사하며 서명을 얻는 데에도 사용할 수 있으므로 오버로드된 메소드와 동일한 "오버로딩 해결" 규칙이 적용됩니다. Java 언어 사양은 " 8.8.8. 생성자 오버로딩 "에서 이를 알려줍니다. 메서드 오버로드 = 초기 바인딩(일명 정적 바인딩) 정적 바인딩 또는 동적 바인딩이라고도 알려진 초기 바인딩과 후기 바인딩에 대해 자주 들을 수 있습니다. 그들 사이의 차이점은 매우 간단합니다. 빠른 것은 컴파일이고, 늦은 것은 프로그램이 실행되는 순간입니다. 따라서 초기 바인딩(정적 바인딩)은 컴파일 시 누구에게 어떤 메서드가 호출될지 결정하는 것입니다. 글쎄, 후기 바인딩(동적 바인딩)은 프로그램 실행 시 어떤 메서드를 직접 호출할지 결정하는 것입니다. 앞에서 본 것처럼(IOException을 EOFException으로 변경했을 때) 메서드를 오버로드하여 컴파일러가 어디에서 어떤 호출을 해야 하는지 이해할 수 없게 되면 컴파일 타임 오류가 발생합니다. 메서드에 대한 참조가 모호합니다. 영어로 번역된 모호한 단어는 모호하거나 불확실하고 부정확함을 의미합니다. 과부하는 초기 바인딩이라는 것이 밝혀졌습니다. 확인은 컴파일 타임에 수행됩니다. 결론을 확인하기 위해 " 8.4.9. 오버로딩 " 장에서 Java 언어 사양을 열어보겠습니다 .
다형성과 그 친구들 - 10
컴파일하는 동안 인수의 유형과 수(컴파일 시 사용 가능)에 대한 정보가 메서드의 서명을 결정하는 데 사용됩니다. 메소드가 객체의 메소드 중 하나(예: 인스턴스 메소드)인 경우 실제 메소드 호출은 런타임 시 동적 메소드 조회(예: 동적 바인딩)를 사용하여 결정됩니다. 더 명확하게 하기 위해 앞서 논의한 것과 유사한 예를 들어보겠습니다.
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
이 코드를 HelloWorld.java 파일에 저장하고 다음을 사용하여 컴파일해 보겠습니다. javac HelloWorld.java 이제 다음 명령을 실행하여 컴파일러가 바이트코드에 작성한 내용을 살펴보겠습니다 javap -verbose HelloWorld.
다형성과 그 친구들 - 11
언급한 대로 컴파일러는 일부 가상 메서드가 나중에 호출될 것이라고 결정했습니다. 즉, 메소드 본문은 런타임에 정의됩니다. 그러나 컴파일 시 세 가지 방법 중 컴파일러는 가장 적합한 방법을 선택하여 숫자를 표시했습니다."invokevirtual #13"
다형성과 그 친구들 - 12
이것은 어떤 종류의 메소드 참조입니까? 방법에 대한 링크입니다. 대략적으로 말하면 이는 런타임 시 Java Virtual Machine이 실행할 메소드를 실제로 결정할 수 있는 몇 가지 단서입니다. 자세한 내용은 상위 기사 " JVM이 메소드 오버로딩 및 내부 재정의를 어떻게 처리합니까? "에서 찾을 수 있습니다.

요약

그래서 우리는 객체지향 언어인 Java가 다형성을 지원한다는 것을 알게 되었습니다. 다형성은 정적(정적 바인딩) 또는 동적(동적 바인딩)일 수 있습니다. 초기 바인딩이라고도 하는 정적 다형성을 사용하면 컴파일러는 호출할 메서드와 호출 위치를 결정합니다. 이를 통해 과부하와 같은 메커니즘을 사용할 수 있습니다. 동적 다형성(후기 바인딩이라고도 함)을 사용하면 이전에 계산된 메서드 시그니처를 기반으로 런타임 시 어떤 개체가 사용되는지(즉, 어떤 개체의 메서드가 호출되는지)에 따라 메서드가 계산됩니다. 이러한 메커니즘이 어떻게 작동하는지 바이트코드를 사용하여 확인할 수 있습니다. 오버로드는 메서드 시그니처를 확인하고 오버로드를 해결할 때 가장 구체적인(가장 정확한) 옵션이 선택됩니다. 재정의는 사용 가능한 메서드를 결정하기 위해 유형을 살펴보고 메서드 자체는 개체에 따라 호출됩니다. 주제에 관한 자료뿐만 아니라 : #비아체슬라프
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION