인터페이스와 람다 표현식

 

 

핵심 내용 정리

  1. 인터페이스는 구현 클래스에서 반드시 구현해야 하는 메소드를 명시
  2. 인터페이스는 해당 인터페이스를 구현하는 모든 클래스의 슈퍼타입
  3. 인터페이스는 정적 메소드를 포함할 수 있다. 인터페이스의 모든 변수는 자동으로 public static final이다.
  4. 인터페이스는 구현 클래스에서 상속하거나 오버라이드할 수 있는 기본 메소드를 포함할 수 있다.
  5. 인터페이스는 구현 클래스에서 호출하거나 오버라이드할 수 없는 비공개 메소드를 포함할 수 있다.
  6. 함수형 인터페이스는 단일 추상 메소드를 가진 인터페이스이다.
  7. 람다 표현식은 나중에 실행할 수 있는 코드 블록이다.
  8. 람다 표현식은 함수형 인터페이스로 변환된다.
  9. 메소드 참조와 생성자 참조는 메소드와 생성자를 호출하지 않고 참조한다.
  10. 람다 표현식과 지역 클래스는 자신을 감싸는 유효 범위에 있는 사실상 최종 변수에 접근할 수 있다.

 

 

3. 인터페이스

public interface IntSequence {
	boolean hasNext();
	int	next();
}
  • 기본 구현을 작성하지 않고 선언만 한 메소드를 추상(abstract) 메소드라고 한다.
  • 인터페이스의 모든 메소드는 자동으로 public이 된다. 그러므로 hasNext와 next를 public으로 선언할 필요 없다.
  • 인터페이스를 구현하는 클래스는 인터페이스의 메소드를 반드시 public으로 선언해야한다. 그렇지 않으면 클래스의 메소드는 기본적으로 패키지 접근이 된다. 하지만 인터페이스는 공개 접근을 요구하므로 컴파일로가 오류 발생
  • 클래스가 인터페이스의 메소드 중 일부만 구현한다면 해당 클래스는 반드시 abstract 제어자로 선언해야 한다.
  • 인터페이스는 또 다른 인터페이스를 extend해서 원래 있던 메소드 외의 추가 메소드를 요구하거나 제공할 수 있다.
  • 클래스는 인터페이스를 몇 개든 구현할 수 있다. (2개를 implements 하면, Super type 을 두개 둔다.)
  • 인터페이스에 정의한 변수는 자동으로 public static final이 된다.

 

3.2.1 정적메소드

팩토리 메소드는 인터페이스에 아주 잘 맞는다.

public interface IntSequenct {
	static IntSequence digitsOf(int n){
		return new DigitSequenct(n);
	}
}

3.2.2 기본 메소드

기본 메소드에는 반드시 default 제어자를 붙여야한다.

public interface IntSequence {
	default boolean hasNext() { return true;}
}

이 인터페이스를 구현하는 클래스는 hasNext 메소드를 오버라이드하거나 기본 구현을 상송하는 방법 중 하나를 선택할 수 있다.

 

* 기본 메소드 덕분에 자바 API의 Collection/AbstractCollenction이나 WindowListener/WindowAdapter처럼 인터페이스와 해당 인터페이스의 메소드를 대부분 또는 모두 구현한 동반 클래스를 제공하던 고전적인 패턴에 종지부를 찍을 수 있었다.

 

3.2.3 기본 메소드의 충돌 해결

 

클래스가 두 개의 인터페이스를 구현한다. 그런데 한 인터페이스에는 기본 메소드가 있고, 다른 한 인터페이스에는 이 메소드와 이름, 매개변수 타입이 같은 메소드가 있다면 반드시 충돌을 해결해야한다.

public interface Person{
	default int getId() { return 0 ; }
}

public interface Identified{
	default int getId() { return Math.abs(hashCode();}
}

public class Employee implements Person, Identified {
	…
}

위의 경우 컴파일러가 하나를 우선해서 선택하지 못한다. 따라서 이 문제를 해결하려면 Employee 클래스에 getId 메소드를 추가한 후 고유의 ID 체계를 구현하거나, 다음과 같이 충돌한 메소드 중 하나에 위임해야 한다.

public class Employee implements Person, Identified {
	public int getId() { return Person.super.getId();}
}

* 두 인터페이스 모두 공유 메소드의 기본 구현을 제공하지 않으면 충돌이 일어나지 않는다. 이 경우 구현 클래스에 메소드를 구현하거나 메소드를 구현하지 않고 클래스를 abstract로 선언하면 된다.

 

3.2.4 비공개 메소드

 

자바 9부터 인터페이스에 비공개 메소드를 만들 수 있다. 비공개 메소드는 static이나 인스턴스 메소드는 될 수 있지만, default 메소드 (오버라이드가 가능하므로) 될 수 없다.

비공개 메소드는 인터페이스 자체에 있는 메소드에서만 쓸 수 있으므로, 인터페이스 안에 있는 다른 메소드의 헬퍼 메소드로만 사용할 수 있다.

 

예를 들어 IntSequence 인터페이스가 다음 메소드를 제공한다고 하자.

static of(int a)
static of(int a, int b)
static of(int a, int b, int c)

이 메소드들은 다음 헬퍼 메소드를 호출할 수 있다.

private static IntSequence makeFiniteSequence(int … values) { … } 

 

3.3.1 Comparable 인터페이스

 

어떤 클래스의 객체를 정렬하려면 해당 클래스가 Comparable 인터페이스를 구현해야 한다. 이 인터페이스와 관련해 기술적으로 중요한 점이 하나 있다. 정렬을 수행할 때 문자열 대 문자열, 직원 대 직원 식으로 비교한다. 그래서 Comparable 인터페이스는 타입 매개변수를 받는다.

public interface Comparable<T> {
	int comparaTo(T other);
}

x.compareTo(y) 를 호출하면 compareTo 메소드는 x와 y 중 어느 것이 앞에 오는지 나타내는 정수 값을 반환한다.

 반환 값이 양수인 경우 x가 y 다음에 온다. 반환 값이 음수면 y가 x 다음에 온다. x, y 값이 같으면 반환 값은 0이다.

 

* Comparable이나 ArrayList처럼 타입 매개변수를 받는 타입은 제네릭(generic)이다.

 

 

3.3.2 Comparator 인터페이스

 

문자열을 사전 순사가 아닌 길이가 증가하는 순서로 비교한다면, String 클래스는  comparableTo 메소드를 두 가지 방법으로 구현하지 못한다. 그리고 String 클래스는 우리가 소유한 클래스가 아니므로 수정할 수도 없다.

 

이런 상황을 다룰 수 있는 Arrays.sort 메소드의 두 번째 버전이 있다. 배열과 비교자(comparator)를 매개 변수로 받는다. (비교자는 Comparator 인터페이스를 구현하는 클래스의 인스턴스다.)

public interface Comparator<T> {
	int compare(T first, T second);
}

문자열을 길이로 비교하려면 Comparator<String>을 구현하는 클래스를 정의해야한다.

class LengthComparator implements Comparator<String>{ 
	public int compare(String first, String second){
		return first.length() - second.length();
	} 
}

Comparator<String> comp = new LengthComparator();
if (comp.compare(word[i], word[j]) > 0) …

word[i].compareTo(word[j])와 비교하면 compare 메소드는 문자열 차제가 아니라 비교자 객체로 호출한다.

 

 

3.3.3 Runnable 인터페이스

 

태스크를 정의하려면 Runnable 인터페이스를 구현해야 한다. Runnable 인터페이스에는 메소드가 한 개만 있다.

class HelloTask implements Runnable{
	public void run(){
    	for( int i = 0; i < 1000; i++){
        	System.out.println("Hello, World!");
		}
	}
}

 

이 태스크를 새 스레드에서 실행하려면 Runnable로 스레드를 생성하고 시작해야 한다.

 

Runnable task = new HelloTask();

Thread thread = new Thread(task);

thread.start();

Runnable task = new HelloTask();
Thread thread = new Thread(task);
thread.start();

 

3.3 람다 표현식

 

람다표현식은 나중에 한 번 이상 실행할 수 있게 전달하는 코드 블록이다. 

 

자바에는 함자바는 거의 모든 것이 객체인 객체 지향 언어이다. 자바에는 함수 타입이 없다. 그 대신 객체(특정 인터페이스를 구현하는 클래스의 인스턴스)로 함수를 표현한다. 람다 표현식은 이런 인스턴스를 생성하는 아주 편리한 문법을 제공한다.

 

3.4.1 람다 표현식 문법

(String first, String second) -> first.length() - second.length()

람다 표현식은 쉽게 말해 코드 블록으로, 해당 코드에 전달해야 하는 변수의 명세까지 갖춘 것이다.

  • 람다 표현식의 바디에서 표현식 하나로는 표현할 수 없는 계산을 수행한다면 메소드를 쓸 때처럼 작성하면 된다.
(String first, String second) -> 
{
	int difference = first.length() - second.length();
	if(difference < 0) return -1;
	else if(difference > 0) return 1;
	else return 0;
}
  • 람다 표현식에 매개변수가 없으면 매개변수가 없는 메소드처럼 빈 괄호를 붙여야 한다.
Runnable task = () -> { for (int i = 0; i < 1000; i++) doWork(); }
  • 람다 표편식의 매개변수 타입을 추론할 수 있다면 다음과 같이 매개변수 타입을 생략할 수 있다.

Comparator<String> comp = (first, second) -> first.length() - second.length();

Comparator<String> comp = (first, second) -> first.length() - second.length();
  • 메소드에 매개변수가 한 개만 있고, 이 매개변수의 타입을 추론할 수 있다면 괄호도 생략할 수 있다.
EventHandler<ActionEvent> listener = event -> System.out.println("Oh");

람다 표현식의 결과 타입은 명시하지 않는다. 하지만 컴파일러는 람다 표현식 바디에서 결과 타입을 추론한 후 기대하는 타입과 일치하는지 검사한다.

 

 

3.4.2 함수형 인터페이스

 

람다 표현식은 단일 추상 메소드를 가진 인터페이스(즉, 추상 메소드가 한 개만 있는 인터페이스) 자리에 사용할 수 있다. 이런 인터페이스를 함수형 인터페이스라고 한다.

 

함수형 인터페이스로 변환하는 것을 알아보기 위해 Arrays.sort 메소드를 생각해 보자. 이 메소드의 두 번째 배개변수는 Comparator의 인스턴스를 요구한다.(Comparator 인터페이스에는 메소드가 하나만 있다.)

이 매개 변수에 다음과 같은 람다를 전달해 보자.

 

Arrays.sort(words, (first, second) -> first.length() - second.length());

Arrays.sort(words, (first, second) -> first.length() - second.length());

내부에서 Arrays.sort 메소드의 두 번째 매개변수는 Comparator<String> 을 구현한 클래스의 객체를 받는다.

이 객체로 compare 메소드를 호출하면 람다 표현식의 바디가 실행된다. 이런 객체와 클래스를 관리하는 일은 순전히 구현체의 몫이며 고도로 최적화되어 있다.

 

함수 리터럴을 지원하는 거의 모든 프로그래밍 언어에서 (String, String) -> int 처럼 함수 타입을 선언하고, 이 함수 타입으로 변수를 선언한 후 함수를 변수에 저장해 호출할 수 있다.

하지만 자바에서는 이 중 하나만 람다 표현식으로 할 수 있다. 

 

바로 람다 표현식을 함수형 인터페이스 타입 변수에 저장해서 해당 인터페이스의 인스턴스로 변환하는 것이다.

 

* 자바에서 Object 타입은 모든 클래스의 Super타입이지만, 람다 표현식은 Object 타입 변수에 저장할 수 없다. -> Object는 함수형 인터페이스가 아니라 클래스이기 때문이다. 

 

3.5.1 메소드 참조

Arrays.sort(strings, (x, y) -> x.compareToIgnoreCase(y));

이 코드 대신 다음 메소드 표현식을 전달할 수 있다.

Arrays.sort(strings, String::compareToIgnoreCase);

 

표현식은 String::compareToIgnoreCase는 람다 표현식 (x,y) -> x.compareToIgnoreCase(y)에 대응하는 메소드 참조다.

list.forEach(x -> System.out.println(x));
list.forEach(System.out::println);

list.removeIf(x -> Objects.isNull(x));
list.removeIf(Object::isNull);

::연산자는 클래스 이름과 메소드 이름을 분리하거나 객체의 이름과 메소드 이름을 분리한다.

  1. Class::instanceMethod
    • 첫 번째 매개변수가 메소드의 수신자가 되고, 나머지 매개변수는 메소드에 전달한다.(String::compareToIgnoreCase == (x,y) -> x.compareToIgnoreCase(y))
  2. Class::staticMethod
    • 모든 매개변수가 정적 메소드로 전달된다. (Objects::isNull == x -> Objects.isNull(x))
  3. object::instanceMethod
    • 주어진 객체로 메소드를 호출하며, 매개변수는 인스턴스 메소드로 전달된다. (System.out::println == x -> System.out.println(x))

 

메소드 참조에서 this 매개변수를 캡처할 수 있다. 예를 들어 this::equals는 x -> this.equals(x)와 같다.

 

*내부 클래스에서 EnclosingClass.this::method로 자신을 감싸는 클래스의 this 참조를 캡처할 수 있다.

*생성자 참조는 Employee::new로 할 수 있다 (names.stream().map(Employee::new)

'Java' 카테고리의 다른 글

자바 클래스 관련 내용 정리  (1) 2020.08.24
Mac에 Java 설치하기  (1) 2017.03.05

+ Recent posts