메서드 레퍼런스
다른 코드에 전달하려고 하는 액션을 수행하는 메서드가 이미 존재할 수도 있다.
예) 버튼을 클릭할때마다 단순히 이벤트 객체를 출력하고 싶을 경우
button.setOnAction(event -> System.out.println(event)); |
하지만 setOnAction 메서드에 println 메서드만 전달할 수 있다면 더 좋을것이다.
button.setOnAction(System.out::println); |
System.out::println 표현식은 람다 표현식 x -> System.out.println(x) 에 해당하는 메서드 레퍼런스(method reference)이다
예) 대소문자를 가리지 않고 문자열을 정렬하고 싶은 경우
Arrays.sort(strings, String::compareToIgnoreCase)
:: 연산자는 객체 또는 클래스와 메서드 이름을 구분한다.
Object::instanceMethod
Class::staticMethod
Class::instaceMethod
처음 두 경우에서 메서드 레퍼런스는 메서드의 파라미터를 제공하는 람다 표현식에 해당한다.
System.out.::println 는 x-> System.out.println(x)에 해당
Math::pow은 (x, y) -> Math.pow(x, y)에 해당
3번째 경우에서는 첫번째 파라미터가 해당 메서드의 대상이 된다.
String::compareToIgnoreCase는 (x, y) -> x.compareToIgnoreCase(y)와 같다.
이름이 같은 여러 메서드가 오버로드되어 있을 때는 컴파일러가 의도한 문맥을 찾으려할 것이다.
예) Math.max 메서드는 정수와 부동소수점 수를 받는 버전이다.
이중 어느 버전이 선택되는지는 Math::max가 변환되는 대상 함수형 인터페이스의 메서드 파리미터에 의존한다.
람다 표현식과 마찬가지로, 메서드 레퍼런스는 독립적으로 존재하지 않고 항상 함수형 인터페이스의 인스턴스로 변환된다.
메서드 레퍼런스에서 this 파라미터를 캡처할 수 있다.
예) this::equals는 x-> this.equals(x)와 같다.
super도 사용할 수 있다.
this를 대상으로 해서 주어진 메서드의 슈퍼클래스 버전을 호출한다.
super::instanceMethod
class Greeter { public void greet() { System.out.println("Hello, world!"); } } class ConcurrentGreeter extends Greeter { public void greet() { Thread t = new Thread(super::greet); t.start(); } } |
스레드가 시작할 때 해당 스레드의 Runnable이 호출되고 super::greet가 실행된다
이때 슈퍼클래스의 greet 메서드가 호출된다.
이너 클래스에서는 바깥쪽 클래스의 this 레퍼런스를 EnclosingClass.this::method 또는 EnclosingClass.super::method로 캡처할 수 있다.
생성자 레퍼런스
생성자 레퍼런스 constructor reference는 메서드의 이름이 new이라는 점을 제외하면 메서드 레퍼런스와 유사하다.
예) Button:new는 Button 생성자를 가리키는 레퍼런스다.
그런데 어느 생성자를 가리킬까? 실제 가리키는 생성자는 문맥에 따라 다르다.
문자열 리스트가 있다고 할때, 다음과 같은 호출을 이용해 각 문자열을 대상으로 생성자를 호출함으로써 문자열 리스트를 버튼 배열로 변환할 수 있다.
List<String> lables = ...; Stream<Button> stream = labels.stream().map(Button::new); List<Button> buttons = stream.collection(Collectors.toList()); |
map 메서드가 리스트의 각 요소를 대상으로 Button(String) 생성자를 호출한다는 점이 중요하다.
Button 생성자는 여러 개지만, 컴파일러는 문자열로 생성자가 호출되는 문맥으로부터 추정해서 String 파리미터 한개를 받는 생성자를 선택한다.
배열타입으로도 생성자 레퍼런스를 만들수 있다.
예) int []::new는 파라미터가 한개(배열의 길이)인 생성자 레퍼런스다. 이 레퍼런스는 람다 표현식 x -> new int[x] 에 해당한다.
배열 생성자 레퍼런스는 자바의 한계를 극복하는데 유용하다.
자바에서 제네릭 타입 T의 배열을 생성할 수 없다. 다시 말해, 표현식 new T[n]은 new Object[n]으로 소거되기 때문에 오류이다.
이 점은 라이브러리 제작자에게 문제가 된다.
예를들어, 버튼의 배열을 원한다고 하자. Stream 인터페이스는 Object 배열을 리턴하는 toArray 메서드를 포함한다.
Object [] buttons = stream.toArray(); |
하지만 이 코드는 충분하지 않다. 사용자는 Object의 배열이 아니라 버튼의 배열을 원한다.
스트림 라이브러리는 이문제를 생성자 레퍼런스를 이용해 해결한다.
toArray 메서드에 Button::new를 전달해보자
Button [] button = stream.toArray(Button[]::new); |
toArray 메서드는 이 생성자를 호출해 올바른 타입의 배열을 얻는다. 그리고 나서 해당 배열을 채워넣어 리턴한다.
변수 유효 범위
람다 표현식에서 해당 표현식을 감싸고 있는 메서드나 클래스에 있는 변수에 접근하고 싶을때가 있다.
public static void repeatMessage(String text, int count) { Runnable r = () -> { for(int i=0; i<count; i++) { System.out.printlnt(text); Thread.yield(); } }; new Thread(r).start(); } |
repeatMessage("Hello", 1000); //별도의 스레드에서 Hello를 1000번 출력한다.
이제 람다 표현식 내부에 있는 count와 text 변수를 보자
이 변수들은 람다 표현식 안에 정의되어 있지 않다.
두 변수는 repeatMessage 메서드의 파라미터 변수이다.
람다 표현식의 코드는 repeatMessage 메서드 호출이 리턴하여 파라미터 변수들이 사라지고도 한참 후에 실행 될수 있다.
그런데 text와 count는 어떻게 남았을까?
람다 표현식에 관한 이해
1. 코드블록
2. 파라미터
3. 자유변수(파라미터도 아니고 코드 내부에도 정의되지 않는 변수)의 값
람다 표현식이 자유변수 두개 (text, count)를 포함한다.
람다 표현식을 나타내는 자료 구조는 이들 변수의 값(여기서는 "Hello"와 1000)을 저장해야 한다
이 경우 람다 표현식이 이들 값을 캡처 capture했다고 말한다.
캡처 방법은 구현의 세부 사항이다. 예를들어 어떤 구현에서 람다 표현식을 단일 메서드를 갖춘 객체로 변환하고, 자유 변수들의 값을 해당 객체의 인스턴스 변수에 복사한다.
람다 표현식은 해당 표현식을 감싸고 있는 유효범위에 있는 변수의 값을 캡처할 수 있다.
자바에서 캡처한 값이 잘 정의되어 있음을 보장하기 위한 중요한 제약이 있다.
람다 표현식에서는 값이 변하지 않는 변수만 참조할 수 있다.
예) 잘못된 참조
public static void repeatMessage(String text, int count) { Runnable r = () -> { while(count > 0) { count --; //오류: 캡처한 변수는 변경할수 없다. System.out.println(text); Thread.yield(); } }; new Thread(r).start(); } |
람다 표현식에서 변수를 변경하는 작업은 스레드에 안전하지 않다.
일련의 병행 작업이 있고, 각 작업에서 공유 카운터를 업데이트하는 경우를 생각해보자
int matches = 0; for(Path p: files) new Thread( () -> { if(p가어떤 프로퍼티를 포함하면) matches++;}).start(); //matches를 변경하는 일은 규칙에 어긋난다. |
만일 이 코드가 규칙에 맞는 것이었다면, 아주 나쁜 결과를 초래 했을 것이다.
다시 말해, 증가 연산 matches++가 원자적이지 않기 때문에 여러 스레드에서 이 증가 연산을 동시에 실행하면 무슨일이 일어날지 알수 있는 방법이 없다.
이너 클래스 역시 자신을 감싸고 있는 유효 범위에 있는 값을 캡처할 수 있다.
자바 8 이전에는 이너 클래스가 final 지역 변수만 접근할수 있었다.
지금은 이 규칙이 람다 표현식과 일치하도록 완화되었다.
이너 클래스는 사실상 final인 모든 지역변수(즉, 값이 변하지 않는 모든 지역 변수)를 접근할 수 있다.
컴파일러가 모든 동시 접근 오류를 잡아 낼것으로 기대하지 말자.
변경 금지는 오직 지역 변수에만 해당한다. 만일 matches가 람다를 감싸고 있는 클래스의 인스턴스 변수 또는 정적 변수라면(결과가 정의되어 있지 않더라도) 오류가 보고되지 않는다.
또한 공유 객체 변경은 말이 안되는것 같지만 규칙에 맞는 일이다.
예)
List<Path> matches = new ArrayList<>(); for(Pah: matches) { new Thread( ()-> { if(p가어떤 프로퍼티를 포함하면) matches.add(p); }).start(); // matches를 변경하는 일이 규칙에 맞지만 안전하지 않다. } |
변수 matches는 사실상 final effectively final임을 주목하기 바란다.
(사실상 final 변수란 초기화된 후 새로운 값을 전혀 대입받지 않는 변수를 말한다.)
예에서는 matches가 항상 같은 ArrayList 객체를 참조한다.
하지만 해당 ArrayList 객체 자체는 변경되었기 때문에 스레드에 안전하지 않다.
따라서 여러 스레드에서 add를 호출하면 결과는 예측할 수 없다.
동시에 값을 세고 모으는 안전한 매커니즘이 있다.
다른 상황에서는 스레드에 안전한 카운터와 컬렉션을 사용하려고 할 것이다.
이너 클래스와 마찬가지로, 람다 표현식에서 자신을 감싸고 있는 지역 유효범위에 있는 카운터를 업데이트 할 수 있게 하는 탈출구가 있다.
int [] counter = new int[1]; button.setOnAction(event -> counter[0]++); |
물론 이와 같은 코드는 스레드에 안전하지 않다.
버튼 콜백인 경우 문제가 되지 않지만 일반적으로는 이 트릭을 사용하기 전에 한번 더 생각해보기 바란다.
람다 표현식의 몸체는 중첩 블록과 동일한 유효 범위를 가진다.
따라서 중첩 블록과 동일한 이름 충돌 및 가리기 규칙이 적용된다.
지역 변수와 이름이 같은 파라미터나 다른 지역 변수를 람다 내부에 선언하는 것은 규칙에 어긋난다.
Path first = paths.get("/usr/bin"); Comparator<String> comp = (first, second) -> Interger.compare(first.length(), second.length()); //오류: 변수 first가 이미 정의되어있다. |
메서드 내부에서 이름이 같은 두 지역 변수를 보유할 수 없기 때문에 람다 표현식 내부에도 이러한 변수를 도입할 수 없다.
람다 표현식에서 this 키워드를 사용하면, 결국 해당 람다를 생성하는 메서드의 this 파라미터를 참조하는 결과가 된다.
예)
public class Application() { public void doWork() { Runnable runner = () -> { ...; System.out.println(this.toString()); ...}; } } |
표현식 this.toString()은 Runnable 인스턴스가 아닌, Application 객체의 toString 메서드를 호출한다.
람다 표현식에서 this 레퍼런스 사용과 관련해 특별한 점은 없다.
람다 표현식의 유효범위는 doWork 메서드 내부에 중첩되고, 이 메서드 내부의 어느 곳에서도 this의 의미는 같다.
'JAVA > JAVA 8' 카테고리의 다른 글
자바 8 #스트림(Stream) 관련 메서드 (0) | 2018.02.26 |
---|---|
자바 8 #스트림(Stream) (0) | 2018.02.23 |
자바8 #인터페이스 디폴트 메서드와 정적메서드 (0) | 2018.02.23 |
자바8 람다 #함수형 인터페이스 (0) | 2018.02.21 |
자바8 람다 (0) | 2018.02.18 |