JEP 286: Local-Variable Type Inference

JEP 286: Local-Variable Type Inference

Summary

Java 언어를 향상시켜 형식 유추를 이니셜라이저를 사용하여 로컬 변수 선언 에까지 확장합니다.

Goals

우리는 Java 코드 작성과 관련된 의식을 줄임으로써 정적 유형 안전성에 대한 Java의 약속을 유지하면서 개발자가 종종 불필요한 로컬 변수 유형 선언을 생략 할 수 있도록하여 개발자 경험을 향상 시키려고합니다. 이 기능은 예를 들어 다음과 같은 선언을 허용합니다.

1
2
3
4

var list = new ArrayList<String>(); // infers ArrayList<String>
var stream = list.stream(); // infers Stream<String>

이 처리는 초기화 프로그램, 향상된 for루프의 색인 및 기존 for-loop으로 선언 된 지역을 사용하여 로컬 변수로 제한됩니다 .
메소드 포멀, 생성자 포멀, 메소드 리턴 타입, 필드, 포캣 포매터, 또는 다른 종류의 변수 선언에는 사용할 수 없습니다.

Success Criteria

정량적으로, 우리는 실제 코드베이스에서 로컬 변수 선언의 상당 부분이이 유형을 사용하여 변환되어 적절한 유형을 유추하고 싶습니다.

본질적으로, 우리는 지역 변수 유형 유추의 한계와 이러한 제한에 대한 동기를 일반 사용자가 이용할 수 있기를 바랍니다.
(물론 이것은 일반적으로 달성하기가 불가능하며 모든 지역 변수에 대해 합리적인 유형을 추론 할 수 없을뿐만 아니라 유형 유추가 구속 문제 해결 알고리즘이 아닌 마음 독서의 한 형태라고 상상합니다 이 경우에는 설명이 합리적으로 보이지 않을 것입니다.)
하지만 특정 구문이 왜 이상적인지 그리고 컴파일러 진단이 효과적으로 연결될 수있는 방법으로 선을 그려야합니다.
언어의 임의적 인 제한 이라기보다는 사용자 코드의 복잡성에 이르기까지 다양합니다.

Motivation

개발자는 Java에서 필요한 상용구 코딩의 정도에 대해 자주 불평합니다.
지역 주민에 대한 매니페스트 형식 선언은 종종 불필요하거나 그 방법으로 인식됩니다.
좋은 변수 네이밍이 주어지면, 무엇이 계속되고 있는지를 종종 분명하게 알 수 있습니다.

모든 변수에 매니페스트 유형을 제공해야 할 필요가 있기 때문에 실수로 개발자가 지나치게 복잡한 표현을 사용하도록 유도 할 수도 있습니다.
a lower-ceremony declaration syntax을 사용하면 복잡한 체인화 된 또는 중첩 된 표현식을 더 단순한 구문식으로 분리하는 것이 바람직하지 않습니다.

이미 많은 언어들의 지역 변수 유형 추론을 지원하고 있습니다.
C++ (auto), C# (var), Scala (var/val), Go (declaration with :=).
java 는 지역 변수 유형 추론을 채택하지 않은 거의 정적으로 형식화 된 언어입니다.
이 시점에서 더 이상 논란의 여지가 없어야합니다.

중첩 및 연쇄 된 일반 메서드 호출에 대한 확장 된 추론 및 람다 형식에 대한 추론을 포함하여 Java SE 8에서 형식 유추의 범위가 크게 확대되었습니다. 이로 인해 콜 체인 (call chaining)을 위해 설계된 API를 작성하는 것이 훨씬 쉬워졌으며 스트림 (Streams)과 같은 API가 널리 사용되어 개발자가 중간 유형을 유추하는 데 이미 익숙하다는 것을 보여줍니다.

다음과 같은 호출 체인에서 :

1
2
3
4
5
6

int maxWeight = blocks.stream()
.filter(b -> b.getColor() == BLUE)
.mapToInt(Block::getWeight)
.max();

중간 형식 Stream과 IntStream람다 형식의 형식 b이 소스 코드에 명시 적으로 나타나지 않는 사람은 아무도 없습니다 .

지역 변수 유형 추론은 덜 단단하게 구조화 된 API에서 유사한 효과를 허용합니다. 지역 변수의 많은 사용은 본질적으로 체인이며, 다음과 같이 추론을 통해 동등하게 이익을 얻습니다.

1
2
3
4

var path = Paths.get(fileName);
var bytes = Files.readAllBytes(path);

Description

기본 for-loop 에서 선언 된 이니셜 라이저, 향상된 for 및 인덱스 변수가있는 로컬 변수 선언의 경우 예약 유형 이름 var을 매니페스트 유형 대신 사용할 수 있습니다.

1
2
3
4

var list = new ArrayList<String>(); // infers ArrayList<String>
var stream = list.stream(); // infers Stream<String>

식별자 var는 키워드가 아닙니다. 대신 예약 된 유형 이름 입니다. 즉 var, 변수, 메소드 또는 패키지 이름으로 사용하는 코드는 영향을받지 않습니다.
var클래스 또는 인터페이스 이름으로 사용 하는 코드는 영향을받습니다 (그러나이 이름은 일반적인 명명 규칙을 위반하기 때문에 실제로는 드뭅니다)

초기화 프로그램이 없거나 여러 변수를 선언하거나 배열 괄호를 추가하거나 초기화되는 변수를 참조하는 로컬 변수 선언 형식은 허용되지 않습니다.
이니셜 라이저없이 지역 주민을 거부하면 “먼 거리에서의 행동”추론 오류를 피하면서 기능의 범위가 좁혀지며 일반적인 프로그램에서 지역 주민의 일부만 제외됩니다.

추론 프로세스는 실질적으로 변수에 초기 자 표현식의 유형을 제공합니다.

약간의 미묘함 :

  • 이니셜 라이저에는 대상 유형이 없습니다 (아직 추론하지 않았으므로). 람다, 메소드 참조, 배열 이니셜 라이저와 같은 유형이 필요한 폴리 표현식은 오류를 유발합니다.
  • 이니셜 라이저에 null 유형이 있으면 이니셜 라이저가없는 변수와 마찬가지로 오류가 발생합니다.이 변수는 나중에 초기화 될 수 있으므로 원하는 유형을 알 수 없습니다.
  • 캡처 변수 및 중첩 된 캡처 변수 가있는 유형은 캡처 변수가 언급되지 않은 수퍼 유형 으로 투영 됩니다.
    이 맵핑은 캡처 변수를 상위 경계로 대체하고 캡처 변수를 언급하는 유형 인수를 바운드 와일드 카드로 바꾼다 (그리고 다시 반복한다).
    이것은 단일 진술 내에서만 고려되는 캡처 변수의 전통적으로 제한된 범위를 유지합니다.
  • 위의 예외 이외에 익명 클래스 유형 및 교차 유형을 포함하여 명시 할 수없는 유형이 유추 될 수 있습니다. 컴파일러와 도구는 이러한 가능성을 설명해야합니다.
Applicability and impact

로컬 변수 선언을위한 OpenJDK 코드베이스를 살펴보면 var초기화 프로그램이 없거나 이니셜 라이저가 null 타입이거나 (거의) 초기화 프로그램이 타겟 타입을 필요로하기 때문에 13 %를 사용하여 쓸 수 없다는 것을 알게되었습니다 .

나머지 지역 변수 선언들 중에서 :

  • 94 %는 소스 코드에있는 정확한 유형의 이니셜 라이저가 있습니다 (매개 변수화 된 유형이있는 경우의 63 %).
  • 5 %는 좀 더 예리한 denotable 유형 (매개 변수화 된 유형을 가진 경우의 29 %)이있는 초기화 프로그램을 가지고 있습니다.
  • 1 %는 캡처 변수 (매개 변수가있는 유형의 경우 7 %)를 언급하는 유형의 초기화 프로그램이 있습니다.
  • <1 %에는 익명 클래스 유형 또는 교차 유형이있는 초기화 프로그램이 있습니다 (매개 변수화 된 유형이있는 경우와 동일).

Alternatives

로컬 변수 유형의 선언 선언을 계속 요구할 수 있습니다.

지원하는 것보다는 var변수 선언에서 다이아몬드 사용에 대한 지원을 제한 할 수 있습니다. 이것은에 의해 언급 된 사례의 하위 집합을 다룰 것이다 var.

위에서 설명한 디자인은 범위, 구문 및 비 표 현 유형에 대한 몇 가지 결정을 통합합니다. 고려 된 선택 사항에 대한 대안이 여기에 문서화되어 있습니다.

Scope Choices

이 기능의 범위를 정할 수있는 몇 가지 다른 방법이 있습니다. 우리는이 기능을 효과적으로 최종 지역 ( val)으로 제한하는 것을 고려했습니다 . 그러나 우리는 다음과 같은 이유로이 입장에서 물러났다.

  • 초기화 프로그램이있는 로컬 변수의 대다수 (JDK와 더 광범위한 코퍼스 모두에서 75 % 이상)는 이미 효과적으로 이미 불변이었습니다.
    즉,이 기능이 제공 할 수있는 변경 가능성에서 벗어난 모든 “뉘앙스”가 제한되었을 것입니다.
  • lambdas / inner 클래스에 의한 capturability는 이미 최종적인 지역 주민들을 향한 중요한 추진력을 제공합니다;
  • 7 개의 최종 최종 사용자와 2 개의 변경 가능한 코드 블록이있는 코드 블록에서는 변경 가능한 유형에 필요한 유형이 시각적으로 엉망이되어 기능의 많은 이점을 훼손합니다.

다른 한편으로는이 기능을 확장하여 “blank” finals을 포함 할 수 있습니다 (예 : 명확한 할당 분석에 의존하는 이니셜 라이저가 필요하지 않음).
“이니셜 라이저가있는 변수”에 대한 제한을 선택했습니다. 기능의 단순성을 유지하면서 “거리에서의 조치”오류를 줄이면서 후보자의 상당 부분을 차지합니다.

마찬가지로 우리는 이니셜 라이저가 아니라 유형을 유추 할 때 모든 과제를 고려할 수있었습니다.
이것은이 기능을 이용할 수있는 지역 주민의 비율을 더 증가 시켰을지라도 “거리에서의 조치”오류의 위험도 증가합니다.

Syntax Choices

구문론에 대한 의견이 다양했습니다. 자유의 두 가지 주요 도로는 여기 (사용하는 키워드 무엇인가 var, auto등), 및 (불변 지역 주민을위한 별도의 새로운 양식을 가지고 있는지 val, let). 우리는 다음과 같은 구문 옵션을 고려했습니다.

  • var x = expr only (like C#)
  • var, plus val for immutable locals (like Scala, Kotlin)
  • var, plus let for immutable locals (like Swift)
  • auto x = expr (like C++)
  • const x = expr (already a reserved word)
  • final x = expr (already a reserved word)
  • let x = expr
  • def x = expr (like Groovy)
  • x := expr (like Go)

실질적인 입력을 수집 한 후 varGroovy, C ++ 또는 Go 방식보다 선호되었습니다. 불변 지역 주민 제 신택 틱 폼 위에 의견의 상당한 다양성이 있었다 ( val, let);
이는 디자인 의도를 추가로 포착하기위한 추가 의식의 트레이드 오프가 될 것입니다.
결국, 우리는 단지 var를 지원하기로 결심했습니다. 이론적 근거에 대한 자세한 내용은 JEP-286 업데이트 (로컬 변수 유형 추론) 에서 찾을 수 있습니다 .

Non-denotable types

때때로 이니셜 라이저의 유형은 캡처 변수 유형, 교차 유형 또는 익명 클래스 유형과 같이 표기 할 수없는 유형입니다. 그러한 경우에 우리는
i) 유형을 추론할지,
ii) 표현을 거부 할 것인가,
또는 iii) 표상 가능한 상위 유형을 추론 할 것인지를 선택할 수 있습니다.

컴파일러 (그리고 세심한 프로그래머!)는 이미 non-denoteable 유형에 대해 안락한 추론을해야합니다.
그러나 지역 변수의 유형으로 사용하면 컴파일러 / 사양 버그를 드러내고 프로그래머가 더 자주 대응해야하므로 노출이 크게 늘어날 것입니다.
교육적으로, 명시 적으로 형식화 된 선언과 암시 적으로 형식화 된 선언 사이에 간단한 구문 변환이있는 것이 좋습니다.

즉, 선언 할 수없는 유형의 이니셜 라이저를 단순히 거부하는 것이 유익하지 않은 방법입니다
(선언문과 같이 프로그래머를 놀라게하는 경우가 많다 var c = getClass()).
또한 수퍼 유형에 대한 매핑은 예상치 못한 손실을 가져올 수 있습니다.

이러한 고려 사항을 통해 우리는 다른 해답을 얻을 수있었습니다.

  • null-typed 변수는 거의 쓸모가, 그리고 추론 유형에 대한 좋은 대안이 없다, 그래서 우리는이를 거부합니다.
  • 후속 명령문에 캡처 변수를 전달하도록 허용하면 언어에 새로운 표현이 추가되지만이 기능의 목표는 아닙니다.
    대신, 제안 된 투영 연산은 타입 시스템 (예를 들어, JDK-8016196 참조)의 다양한 버그를 처리하기 위해 어쨌든 사용해야 하는 것이므로 여기에 적용하는 것이 합리적입니다.
  • 교차 타입은 슈퍼 타입으로 매핑하기가 특히 어렵습니다. 즉, 순서가 지정되지 않았기 때문에 교차의 한 요소가 본질적으로 다른 요소보다 “더 우수”하지 않습니다. 상위 유형에 대한 안정적인 선택 은 모든 요소 의 Iub 입니다.
    그러나 이는 종종 Object동일하거나 전혀 도움이되지 않을 것입니다. 그래서 우리는 그들을 허용합니다.
  • 익명 클래스 유형은 이름을 지정할 수 없지만 쉽게 이해할 수 있습니다. 클래스는 클래스입니다. 변수에 익명 클래스 유형을 허용하면 로컬 클래스의 싱글 톤 인스턴스를 선언 할 때 유용하게 사용됩니다. 우리는 그들을 허용합니다.

Risks and Assumptions

위험 : Java는 RHS (λ 공식, 일반 메소드 유형 인수, 다이아몬드)에 중요한 유형 유추를 이미 수행했기 때문에 var이러한 표현의 LHS 에서 사용하려고 시도 하면 오류가 발생하고 읽기가 어려울 수 있습니다 오류 메시지.

LHS를 유추 할 때 단순화 된 오류 메시지를 사용하여이를 완화했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Main.java:81: error: cannot infer type for local
variable x
var x;
^
(cannot use 'val' on variable without initializer)

Main.java:82: error: cannot infer type for local
variable f
var f = () -> { };
^
(lambda expression needs an explicit target-type)

Main.java:83: error: cannot infer type for local
variable g
var g = null;
^
(variable initializer is 'null')

Main.java:84: error: cannot infer type for local
variable c
var c = l();
^
(inferred type is non denotable)

Main.java:195: error: cannot infer type for local variable m
var m = this::l;
^
(method reference needs an explicit target-type)

Main.java:199: error: cannot infer type for local variable k
var k = { 1 , 2 };
^
(array initializer needs an explicit target-type)

위험 요소 : 소스 비 호환성 (누군가가 var형식 이름으로 사용 했을 수 있음 )

예약 된 유형 이름으로 완화되었습니다. 같은 이름 var은 형식의 명명 규칙을 따르지 않으므로 형식으로 사용되지 않습니다. 이름 var 은 일반적으로 식별자로 사용됩니다. 우리는 이것을 계속 허용합니다.

위험 : 가독성 감소, 리팩터링시의 놀라움.

다른 언어 기능과 마찬가지로 로컬 변수 유형 추론을 사용하여 명확하고 명확하지 않은 코드를 작성할 수 있습니다. 궁극적으로 명확한 코드 작성 책임은 사용자에게 있습니다. 참고 항목 스타일 가이드 라인을 사용 var하고, 자주 묻는 질문 .

참조