모던 자바 인 액션 04. 람다(3)
JAVA/모던 자바 인 액션

모던 자바 인 액션 04. 람다(3)

형식검사 (Type)

람다를 잘 이해하기 위해서는 람다의 구성요소(파라미터, 바디)를 이용해서 람다가 어떤 함수형 인터페이스를 구현하고 있는지 검사할 수 있어야 합니다. 이를 형식검사이라고 합니다!

 

예를 들어 형식검사를 해봅시다.

List<Student> maleStudentsV3 = studentFilterByPredicate(students, 
	(Student student) -> student.getSex().equals(Sex.Male)
});


public List<Student> studentFilterByPredicate(List<Student> students, StudentPredicate p) {
	List<Student> result = new ArrayList<>;
    
    // 전략들이 참인 경우에만 List에 추가
	for(Student student : students) {
	    if(p.test(student)) result.add(student);
	}
    
	return result;
}
먼저 람다의 바디가 boolean 타입을 반환하고 있기 때문에 boolean 타입을 반환하는 함수형 인터페이스로 좁힐 수 있습니다. ('Predicate')

두 번째로는 'Student' 클래스를 파라미터로 받아 성별 정보를 얻은 후 'eqaul( )' 연산을 수행하고 있습니다.

두 정보를 통해 이 'studentFilterByPredicate( )'는 'Student'를 받아 'Boolean' 타입으로 반환하는 메서드임을 추론
할 수 있습니다!

마지막으로 'studentFilterByPredicate( )'를 직접 확인해 함수형 인터페이스의 반환 타입을 확인합시다.
Predicate이기 때문에 일치합니다!

이로써 검사가 성공적으로 이루어졌습니다.

그림 1 형식 검사

 

 

형식 추론

자바 컴파일러는 앞서 설명한 형식검사에서 설명한 반환 타입을 추론할 수 있습니다. 이 기능을 이용하면 람다식을 더욱 간략화할 수 있습니다.

 

  // 형식 추론 X
  Comparator<Student> c1 = (Student s1, Student s2) -> s1.getHeight().compareTo(s2.getHeight()); 
  // 형식 추론 O
  Comparator<Student> c2 = (s1, s2) -> s1.getHeight().compareTo(s2.getHeight());

형식 추론을 사용하는 게 좋을 수도, 사용하지 않는 것이 좋을 수도 있으니 판단해서 사용하면 됩니다!

 

 

PLUS. 지역 변수

지금까지는 모두 람다 내 파라미터에서만 변수들을 가져왔습니다. 여기서 람다 외 정의된 변수(자유 변수[각주:1])들도 사용할 수 있지 않을까?라는 의문점이 드는데, 당연히 가능합니다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

이러한 동작을 람다 캡처링이라 합니다.

 

지역 변수 사용 시 주의 사항

자유 변수(인스턴스 변수, 정적 변수)를 사용 시 명시적으로 final로 선언하거나, 그렇게 취급이 되어야 한다.

//Case1 처음부터 final로 선언
final int portNumber1 = 80;
Runnable r1 = () -> System.out.println(portNumber1);
r1.run();		// 결과 = 80

//Case2 지역 변수를 final로 취급
int portNumber2 = 80;
Runnable r2 = () -> System.out.println(portNumber2);
r2.run();

portNumber2 = 443;		// final로 취급해야 하기 때문에 절대 하면 안된다!
r2.run();

만약 final로 취급이 되지 않는다면 컴파일러 에러가 발생합니다.

그림 2 final로 취급되지 않은 지역 변수

이 이유는 멀티스레드와 관련이 있이 있습니다.

지역변수와 멀티 쓰레드

먼저 인스턴스 변수는 힙에 저장이 되지만, 지역 변수는 스택에 저장이 되는 것을 알고 있을 겁니다. 

  • 만약 람다에서 지역변수에 바로 접근이 가능할 때 스레드에서 실행이 되면, 변수를 할당한 스레드가 사라져 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.
  • 그렇게 되면 자유 지역 변수의 복사본을 제공하게 되는데, 복사본의 값이 바뀌어서는 안 되기 때문에 final로 할당을 해야 한다.

 


메서드 참조

메서드 참조를 사용하면 기존 메서드 정의를 재활용해 람다처럼 전달할 수 있습니다. 기본 형태는 다음과 같습니다.

 

Class::Method

 

예시

람다 메서드 참조
(Student student) -> student.getSex( ) Student::getSex
( ) -> Thread.currentThread( ).dumpStack( ) Thread.currentThread( )::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s)  System.out::println
(String s) -> this.isValidNames(s) this::isValidNames

 

Plus. 생성자 참조

인스턴스 생성도 문제없습니다. 메서드 참조와 마찬가지로 ClassName::new 하면 그만입니다.

Supplier<Student> const1 = () -> new Student();
Student student1 = const1.get();
System.out.println(student1);

Supplier<Student> const2 = Student::new;
Student student2 = const2.get();
System.out.println(student2);

/** 
 * Function을 쓰면 각 클래스 파라미터 타입에 해당되는 값을 미리 넣을 수 있다.
 * 단 그 파라미터를 인수로 받는 생성자가 반드시 존재해야 한다.
 */

// height를 받는 생성자가 있다고 전제
public Student(int height) {
    this.height = height;
}

Function<Integer, Student> const3 = Student::new; // (height) -> new Student(height)
Student student3 = const3.apply(180);
System.out.println(student3);	// 결과 : Student{name='null', Sex=null, height=180}

// name를 받는 생성자가 있다고 전제
public Student(String name) {
    this.name = name;
}

Function<String, Student> const4 = Student::new;
Student student4 = const4.apply("학생1");
System.out.println(student4);  // 결과 : Student{name='학생1', Sex=null, height=0}

2개의 인수를 받게 하려면 'BiFunction'을 사용해서 2개의 인수까지 사용이 가능합니다. 자바에서는 'BiFunction'까지 만을 공식적으로 지원하고 있기 때문에 그 이상부터는 직접 만드셔야 합니다.

 

 

람다 + 메서드 참조

이제 배운 모든 것을 활용해서 다음 코드를 간략화해봅시다.

student.sort((Student s1, Student s2) -> Integer.compare(s1.getHeight(), s2.getHeight()));

 

1st 코드 전달

'sort( )'Comparator <T> 파라미터를 받습니다. 이는 동작 파라미터화 되었다고 볼 수 있습니다.

그러므로 'sort( )'에 전달되는 Comparator <T> 따라 sort의 동작 방식이 달라질 것을 기대할 수 있겠죠?

 

먼저 'Comparator <Student>'를 구현하는 인터페이스를 만들어봅시다!

public static class StudentComparator implements Comparator<Student> {
    @Override
    public int compare(Student s1, Student s2) {
    	return s1.getHeight() - s2.getHeight();
    }
}
    
//Step 1 일반적인 파라미터화
studentList.sort(new StudentComparator());
System.out.println(studentList);

2nd 익명 메서드 사용

익명 클래스를 사용해 'sort( )' 메서드 안에서 'Comparator( )'을 구현합시다.

studentList.sort(new Comparator<Student>() {
        @Override
        public int compare(Student s1, Student s2) {
                return s1.getHeight() - s2.getHeight();
        }
});

3rd 람다식으로 변환

studentList.sort((s1, s2) -> s1.getHeight() - s2.getHeight());

4th 메서드 참조 사용

studentList.sort(comparing(Student::getHeight));

 

요약

  • 형식 추론을 이용하면 람다식의 파라미터에서 클래스를 명시적으로 사용하지 않아도 된다.
    • 단 이는 명시적으로 사용해 가독성을 높이는 방향과 적절히 Trade-Off를 하자.

  • 람다식에서 파라미터 외의 변수를 사용할 때는 항상 상수(final) 취급이 되어야 한다.

  • 메서드 참조를 이용해서 람다식을 더욱 간략화할 수 있다.
    • (ClassName var) -> var.Method() ===> ClassName::Method

참고 자료

  • 모던 자바 인 액션
  • JAVA Reference

 

주석

  1. 자유 변수 : 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수. [본문으로]