DevOps

Java의 유형 종속성, 2-2 (Collections API, 제네릭, 람다식 활용)

IT오이시이 2017. 6. 14. 10:36
728x90

Type dependency in Java, Part 2

Using covariance and contravariance in your Java programs


원 문 : http://www.itworld.com/article/3197118/learn-java/type-dependency-in-java-part-2.html?page=2


메소드는 매개 변수의 유형에 따라 다릅니다. 입력 매개 변수와 함께 메서드의 반환 유형도 출력 매개 변수로 간주합니다. 그러나 리턴 유형은 메소드의 특성에 속하지 않으며 입력 (원시) 매개 변수 유형에만 속합니다.

이름이 다르거 나 매개 변수 수가 다른 메서드는 호환되지 않습니다. 호환성 의 문제 는 동일한 이름과 동일한 수의 매개 변수를 가진 메소드에서만 발생합니다.

메소드 선언과 정의의 호환성

클래스의 본문에있는 메서드 호출은 메서드 정의 (클래스)와 선언 (추상 클래스 및 인터페이스)과 호환되거나 호환되지 않을 수 있습니다. 메서드 선언의 경우 질문은 다른 선언과의 호환성입니다.이 정보는 메서드를 재정의 하거나 과부하 할 것인지 결정하는 데 도움이 됩니다.

Java 선언 및 정의 중 서명과 관련된 차이는 없습니다. 메소드가 다른 메소드를 겹쳐 쓰면 서명이 동일해야합니다. 그러나 결과 유형 에 대한 공분산이 있습니다 .


interface SuperType {
	void procedure(SuperType parameter);
	SuperType function();
 }
interface SubType extends SuperType {
	void procedure(SubType parameter); // overloaded: different signature
	@Override
	SubType function(); // overridden: same signature, different result type
}

서명과 관련한 Java의 엄격한 규칙을 준수 procedure()하면이 경우 오버로드되지 않으며 오버라이드되지 않습니다. 주석 @Override을 추가하면 두 인터페이스 에 SubType.procedure()대해 매개 변수 유형이 같지 않기 때문에 컴파일러에 오류 메시지가 표시됩니다 procedure().

호출이 선언 또는 정의와 호환되는지 여부는 서명을 기반으로 결정됩니다. 여기에는 공분산이 있습니다. 매개 변수의 상위 호환성은 호출의 호환성을 의미합니다. 그러나 우리가 호출 대상 객체에 관심이 있다면, 우리는 분산 에 대해서가 아니라 다형성 에 대해서 말합니다 :


SuperType superParameter = ... ;
SubType subParameter = ... ;
superReference.procedure(subParameter); // call is covariant concerning signature
subReference.procedure(superParameter); // polymorph

서명과 관련하여 선언 및 정의에 대한 호출은 공 변 (covariant)입니다. 아래의 마지막 것은 불변입니다.

Java의 유형 종속성 : 메소드의 차이
그림 1. 서명 관련 메소드의 차이

함수의 차이

Java의 메소드 선언 및 정의는 결과 유형과 관련하여 공 변합니다. 매개 변수와 달리 함수 의 결과 유형은  서명에 속하지 않습니다. 결과 형식은 다른 방식으로는 변경할 수 없지만 재정의 된 버전에서 위쪽으로 확장 될 수 있습니다.

함수 결과를 호출 할 때 분산을 찾지는 않지만 대신 상향 호환성을 찾습니다.


SuperType superResult = subReference.function(); // upward compatible
SubType subResult = superReference.function();
	// type error: no downward compatibility

그러나 위의 코드에서 나타난 상향 호환성은 공분산 형태로 간주 될 수 있습니다.

함수 결과와 마찬가지로, 액세스 보호 ( privatepublic등) 및 예외 스펙 ( throws)은 서명에 속하지 않습니다. (그러나 메서드가 재정의 된 경우 위쪽으로 확장 될 수 있습니다.) Appending final은 다음과 같이 오버라이드를 더 이상 방지 합니다 .


class SuperClass {
	void method() throws SuperException { ... }
}
class SubClass extends SuperClass {
	@Override
	final void method() throws SubException { ... }
}

대체로 메소드 편차는 간단합니다. 메소드 서명의 선언과 정의는 서로 불변합니다. 결과 유형, 예외 스펙 및 선언 및 정의에 대한 호출과 같은 다른 모든 요소는 공 변 (covariant)입니다. 표 2는 방법 차이를 요약 한 것이다.

표 2. 방법 차이

선언 정의

요구

서명

불변량

공변량

결과 형 
예외 지정

공변량

공변량

람다 식에서의 분산 사용 람다 식은 익명 메서드입니다. 모든 익명의 언어 요소 (클래스 및 객체 포함)와 마찬가지로, 이들은 일회용으로 가장 적합합니다. Lambda는 코드 가독성의 원칙을 충족시켜 소프트웨어 품질을 만족시킵니다. 우리는 그 원칙을 다음과 같이 요약 할 수 있습니다. 정의가 사용법에 가까울수록 좋을 것 입니다.

람다 표현식을 사용하면 익명의 프로그램 요소가 사용되는 것처럼 정의 할 수 있습니다. 이렇게 :


method(new MyClass()); // anonymous object without reference, for one-time usage
new Interface() { ... };
	// anonymous class implementing the interface, for one-time usage
procedure(parameter -> { ... }); // lambda
	// anonymous method passed as parameter to procedure

이 코드는 다음 선언을 가정합니다.


@FunctionalInterface
interface Functionalinterface {
	void method(Type parameter);
}
...
void procedure(Functionalinterface functionalinterface) { ... }

Java 8 이전에는 호출 procedure()이 더 복잡해 보였습니다.


class Implementation implements Functionalinterface {
	public void method(Type parameter) { ... } }
...
Functionalinterface fi = new Implementation();
procedure(fi);

익명의 클래스와 객체로 구현 된 다소 간단한 예제 :


procedure(new Functionalinterface() {
	public void method(Type parameter) { ... } } );

Java 8 이전에는 콜백이 다음과 같이 구현되었습니다.

나는Java에서 유형 종속성 : 콜백
그림 2. 콜백

전통적인 콜백 프로 시저에서 메서드 a 는 b 를 호출하는 동안 매개 변수 로 메서드 c 를 전달 하므로 b 가 c를 호출 합니다. b 프로그래밍 을하는 경우 그림 2의 빨간색 원으로 표시된 위치에서 메소드를 호출하지만 호출 할 메소드를 알지 못합니다.

Lambda는 콜백을 구현하는 훨씬 간단한 방법을 제공합니다.


procedure(parameter -> { ... }); // lambda

람다 (lambdas)로 통화 단순화

함수를 통합하기위한 알고리즘을 프로그래밍하려는 경우 어떤 함수 (예 : 사인 ) 를 통합할지 모른 채 작성해야합니다 . 그림 3은 사인의 적분을 계산하는 알고리즘을 보여줍니다.

유형 dependency-integral1
Java-integral2의 유형 종속성위키피디아
그림 3. 사인의 적분

그런 다음 알고리즘을 호출 할 때 통합 알고리즘 사용자가 해당 함수를 ( a 및 b 와 함께 ) 전달합니다. 아래에서, 0 과 π 사이 의 사인 의 적분을 계산하는 데 사용 된 알고리즘을 보았습니다.이  은 1.0 입니다.


double result = integral((double x) -> Math.sin(x), 0, Math.PI);

Java에서 콜백의 좋은 예는 리스너입니다.


button.addActionListener( // pre-Java 8 version
	new ActionListener() {
		public void actionPerformed(ActionEvent e) {
			System.out.println("Button pushed"); }});

수업을 프로그래밍한다고 가정 해보십시오 Button. 버튼 누름에 반응하도록 버튼을 프로그래밍해야하지만 동작은 정의되지 않습니다. 클래스의 사용자 만 응용 프로그램이 요구하는 정확한 작업을 알 수 있습니다 (이 경우 해당 작업이 수행됩니다 System.out.println()).

제네릭 단추를 개발하려면 ActionListener개체를 만들고 이를 Button호출 하여 개체에 전달합니다 addActionListener(). 그런 다음 작업이 프로그래밍되는 ActionListener메서드 actionPerformed()를 구현합니다. (더 복잡한 예제에서는 ActionEvent매개 변수도 사용할 수 있습니다 .) Button버튼을 누르면 사용자 메서드가 클래스 에 의해 "콜백"됩니다 .

lambdas를 도입하면이 프로그램이 간단 해집니다.


button.addActionListener(
  e -> System.out.println("Button pushed") // lambda expression
);

즉시 사용할 수있는 메서드 참조를 사용하면 System더욱 간단하게 처리 할 수 ​​있습니다.


button.addActionListener(System.out::println);

Math메서드 참조를 내보내 므로 sin호출 할 때이 메서드 를 사용할 수도 있습니다 integral().


result = integral(Math::sin, 0, Math.PI);

그럼 당신은 선언 했음에 틀림 없습니다.


double integral(Function<Double, Double> function, double a, double b)

여기서 Function두 가지 유형 매개 변수가있는 일반 인터페이스가 있습니다.

기능 인터페이스의 Lambdas

당신은 선언 후 람다 표현식을 사용할 수 있습니다  기능 인터페이스를 . 정확히 하나의 (추상적 인) 비 제네릭 메소드를 선언하는 한, 모든 인터페이스를 사용할 수 있습니다. 컴파일러는 인터페이스가 주석으로 표시되어 있으면 이러한 기준을 확인합니다  @FunctionalInterface.

파라미터의 타입 Button.addActionListener(), 즉 java.awt.event.ActionListener, 기준이 만족; 따라서 매개 변수로 람다 식을 사용하여 호출 할 수 있습니다. 람다 표현식은 하나 개의 파라미터와 익명 방식 (이하 '람다의 값 ")을 나타내고, e화살표 전에 ->, 그리고 이후에있어서 본체 (A 블록).

Java 8에서 람다 식은 객체처럼 동작하며 매개 변수로 전달 될 수 있습니다. 이 개체는 "람다 형식"으로 간주됩니다. 예제의 경우 객체는 ActionListener기능 인터페이스 유형  입니다.

람다 식에 대한 참조를 정의 할 수도 있습니다.


ActionListener actionListener = e -> System.out.println("Button pushed");

통화에서 나중에 사용하십시오.


button.addActionListener(actionListener);

동일한 청취자를 둘 이상의 단추에 지정하려는 경우 유용 할 수 있습니다.

또 다른 예는 Runnable다음과 같습니다.


class OldRunnable implements Runnable {
  public void run() {
    System.out.println("Old");
  }
};
Runnable old = new OldRunnable();
new Thread(old).start();

이 호출은 람다 표현식을 사용하면 훨씬 간단 해집니다.


new Thread(() -> System.out.println("New")).start();

람다 식은 해당 기능 인터페이스 유형을 갖습니다. 다음 예에서 우리는 유형 (지정할 수 있습니다 String두 개의 매개 변수)를 left하고 right있지만, 컴파일러는 추론에 의한 유형을 확인할 수 있습니다 :


Comparator<String> c;
c = (String left, String right) -> left.compareTo(right);
c = (left, right) -> left.compareTo(right); // equivalent

이 참조에서는 다음과 같이 람다 값 (메서드 본문)을 호출합니다.


System.out.println(c.compare("Hallo", "World"));

전화를 단순화하는 것이 람다의 유일한 장점은 아닙니다. 람다 표현식은 객체 지향 프로그래밍과 기능 스타일 프로그래밍을 혼합하여 Java에서 새로운 프로그래밍 패러다임을 가능하게합니다. 람다를 사용하여 많은 알고리즘을보다 간결하고 더 읽기 쉽게 작성할 수 있으며 많은 경우 프로그램의 정확성을 향상시키는 데 도움이됩니다.

람다 표현의 공분산

람다는 일반적이어서는 안됩니다. 그러므로 그들은 편차를위한 여지가 거의 없다. 종종 컴파일러의 추론 알고리즘이 람다 식의 유형을 결정하는 것은 어렵습니다. 따라서 람다 식의 매개 변수 유형은 기능 인터페이스의 유형에 정확히 맞아야합니다. 메서드 매개 변수에 대한 일반적인 호환성 규칙이 적용됩니다.


@FunctionalInterface
interface SuperFI {
	void method(SuperType parameter);
}
@FunctionalInterface
interface SubFI {
	void method(SubType parameter);
}
...
void covariantProcedure(SuperFI fi, SuperType superParameter {
	fi.method(superParameter);
}
void contravariantProcedure(SubFI sfi, SuperType subParameter) {
	sfi.method((SubType)subParameter); // run time error: ClassCastException
}
...
covariantProcedure(parameter -> {}, new SubType());
	// SubType is compatible to SuperType
contravariantProcedure(parameter -> {}, new SuperType());

컴파일러는 SuperType마지막 행에서 적합하지 않은 객체를 수락 하지만 본문의 형 변환 은 런타임에 contravariantProcedure()ClassCastException를 발생시킵니다 . 따라서 람다 표현식 (메소드 호출과 마찬가지로)은 그들의 서명과 관련하여 공변 적이라고 말할 수 있습니다.

Lambda는 결과 유형 및 예외 사양과 관련하여 메소드처럼 작동합니다. 이 경우 그들은 공변이다 :


@FunctionalInterface
interface SuperInterface {
	SuperType function() throws SuperException;
}
@FunctionalInterface
interface SubInterface {
	SubType function() throws SubException;
}
...
SuperType superFunction(SuperInterface superParameter) throws SuperException {
	return superParameter.function(); // or do something more interesting
}
SubType subFunction(SubInterface subParameter) throws SubException {
	return subParameter.function(); // similarly
}
...
SuperType superVariable = superFunction(() -> new SuperType()); // normal
superVariable = subFunction(() -> new SubType()); // simply compatible
superVariable = superFunction(() -> new SubType()); // covariant
SubType subVariable = (SubType)superFunction(() -> new SuperType()); // error
subVariable = (SubType)superFunction(() -> new SuperType()); // run time error

위의 마지막 두 줄에있는 주물이 a ClassCastException. 주조는 명백한 반공 변이 없기 때문에 도움이되지 않습니다. 그러나, (단지 방법 등) 람다 표현되는 내재적 공변 그들의 함수 결과 예외 사양의 종류에 대하여 (상기 코드의 셋째 줄 참조).

여기, 우리의 람다 식을 통과 한 SubType의 매개 변수 SuperType. 이것은 람다 (lambda)를위한 특별한 경우이다 : superFunction매개 변수 유형은 SuperInterface이다; 일반적으로이 호출은 매개 변수를 취할 것이며이 매개 변수 는 하위 유형 SubInterface이 아닙니다 . 그러나 람다는 암묵적으로 공변 적이므로 작동합니다.

추론은 속임수입니다. 두 가지 버전의 function()구별 가능한 서명 (서로 다른 유형의 매개 변수를 사용하는 의미)을 정의하고 전통적으로 구현하면 SubInterface매개 function()변수 유형을 명시 적으로 지정해야합니다 . 그러나 람다 표현식에 매개 변수 유형이 지정되지 않은 경우 컴파일러는 가장 적합한 피팅을 찾고 유연합니다.


superVariable = superFunction(parameter -> new SubType());
	// unspecified parameter type: inference changes SubType to SuperType

이 유연성은 추가적인 공분산을 보증 합니다. 람다 표현식은 전통적인 방법이 아닌 경우에도 상향 호환됩니다.

728x90
반응형