인터페이스와 람다 표현식

 

 

핵심 내용 정리

  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

2.3.3 다른 생성자에서 특정 생성자 호출

  • 또 다른 생성자에서 어느 한 생성자를 호출할 수 있는데, 호출하는 쪽 생성자 바디의 첫 번째 문장으로만 허용한다.
  • 그리고 호출할 생성자 이름이 아니라 this 키워드를 사용한다.
public Employee(double salary){
	
    this("", salary); //Employee(String, double) 호출
    
    //이후에 다른 문장이 올 수 있다.
}

 

2.3.4 기본 초기화

  • 생성자 안에서 인스턴스 변수를 명시적으로 설정하지 않으면 자동으로 변수를 기본 값으로 설정한다. 숫자는 0, bool 값은 false, 객체 참조는 null이 기본 값

 

2.3.5 인스턴스 변수 초기화

  • 객체를 할당하고 나서 생성자가 실행되기 전에 일어난다.
  • 인스턴스 변수를 선언할 때 초기화하는 방법 외에 클래스 안에 임의의 초기화 블록을 넣는 방법도 있다.
  • 인스턴스 변수 초기화와 초기화 블록 클래스 선언에 나타난 순서로 실행하며, 그 다음에 생성자를 실행한다.
public class Employee(){
	
    private String name = " ";
    private int id;
    private double salary;
    
    { //초기화 블록
    	Random generator = new Random();
        id = 1 + generator.nextInt(1_000_000);
    }
    
    ...
}

 

2.4 정적 변수와 정적 메소드

 

2.4.3 정적 초기화 블록

  • 정적 변수에 초기화 작업이 추가로 필요할 때 사용한다.
  • 정적 초기화는 클래스를 처음 로드할 때 일어난다. 인스턴스 변수와 마찬가지로 정적 변수를 명시적으로 다른 값으로 설정하지 않으면 0이나 false, null이 된다.
  • 모든 정젹 변수는 초기화와 정적 초기화 블록은 클래스 선언 안에 나타난 순서로 실행된다.
public class CreditCardForm{
	
    private static final ArrayList<Integer> expiration = new ArrayList<>();
    static{
    	int year = LocalDate.now().getYear();

		for (int i = year; i <= year + 20; i++){
        	expirationYear.add(i);	
        }
    }
    
    ...
}

2.4.4 팩토리 메소드

  • 정적 메소드는 흔히 팩토리 메소드를 만드는데 사용한다.
  • 팩토리 메소드는 클래스의 새 인스턴스를 반환하는 정적 메소드를 의미한다.
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();

double x = 0.1;

System.out.println(currencyFormatter.format(x)); 

-> 왜 생성자 대신 팩토리 메소드를 사용할까? 

  • 생성자를 구별하는 유일한 방법은 생성자의 매개변수. 타입이다. 따라서 매개변수가 없는 생성자를 두개씩 둘 수 없다.
  • 게다가 new NumberFormat(…) 생성자는 NumberFormat을 돌려준다. 하지만 팩토리 메소드는 서브클래스의 객체를 반환할 수 있다.
  • 그리고 팩토리 메소드를 사용하면 불필요하게 새 객체를 생성하는 대신 공유 객체를 반환할 수 있다. 예를 들어 Collections.emptyList()를 호출하면 변경할 수 없는 빈 리스트(공유객체)를 반환한다.

 

2.6 중첩 클래스

  • 클래스를 다른 클래스 내부에 두는 방법이 있다. 이런 클래스를 중첩 클래스(nested class)라고 한다. 중첩 클래스는 가시성을 제한하거나 Element, Node, Item 처럼 일반적인 이름을 사용하면서도 정돈된 상태를 유지할 때 유용하다.
  • 자바에는 작동 방식이 약간 다른 중첩 클래스가 두 종류 있다.

 

2.6.1 정적 중첩 클래스

public class Invoice{

	private static class Item { //Invoice 내부에 Item 중첩했다.
		String description;
		int quantity;
		double unitPrice;

		double price() {return quantity * unitPrice; }
	}

	private ArrayList<Item> items = new ArrayList<>();
    
	public void addItem(String description, int quantity, double unitPrice) {
		
		Item newItem = new Item();
		
		newItem.description = description;
		newItem.quantity = quantity;
		newItem.unitPrice = unitPrice;
		
		items.add(newItem);
	}
}

Invoice.Item Item = new Invoice.Item();
myInvoice.add(newItem);

 

Invoice.Item 클래스와 다른 클래스 외부에 선언한 InvoiceItem 클래스는 근본적으로 차이가 없다. 클래스 중첩은 그저 Item 클래스가 청구서에 들어 있는 물품을 표현한다는 사실을 분명하게 할 뿐이다.

 

2.6.2 내부 클래스

  • static 을 붙이지 않은 클래스를 내부 클래스(이너 클래스 inner class)라고 한다.

각 회원이 다른 회원과 관계를 맺는 소셜 네트워크를 생각해 보자.

 

public class Network {

	public class Member{  //static 제어자를 빼면 근본적인 차이가 하나 생긴다. 예를 들어 Member 객체는 자신이 어느 네트워크에 속하는지 알게 된다.
		private String name;
		private ArrayList<Member> friends;

		public Member(String name) {
			this.name = name;
			friends = new ArrayList();
		}

		public void deactivate() {
			members.remove(this);
		}

		public boolean belongsTo(Network n){
			return Network.this == n;
		}
	}

	private ArrayList<Member> members = new ArrayList()<>;

	public Member enroll(String name) {
		Member newMember = new Member(name);
		members.add(newMember);
		return newMember;
	}
}

 

다음과 같이 멤버를 추가하고 참조를 얻을 수 있다.

Network myFace = new Network();
Network.Member fred = myFace.enroll("Fred");

이제 Fred는 멤버십을 해지하려고 한다.

fred.deactivate();

 

메소드 구현에서 볼 수 있듯이, 내부 클래스의 메소드는 외부 클래스의 인스턴스 변수에 접근할 수 있다.

이 코드에서는 내부 클래스를 생성한 외부 클래스 객체(myFace 네트워크)의 인스턴스 변수 members이다.

 

바로 이 점이 내부 클래스를 정적 중첩 클래스와 구별시키는 요인이다.

내부 클래스의 각 객체는 외부 클래스의 객체에 대한 참조를 포함한다.

 

members.remove(this); // 이 호출은 실제로 다음을 의미한다.
outer.members.remove(this);

 

여기서는 외부 클래스의 숨은 참조를 outer로 나타냈다.

정적 중첩 클래스에는 이런 참조가 없다.(static 메소드에 this 참조가 없는 것과 마찬가지)

 

중첩 클래스의 인스턴스가 자신을 감싸고 있는 클래스의 어느 인스턴스에 속하는지 알 필요가 없을 때 정적 중첩 클래스를 사용하자.

내부 클래스는 이 정보가 중요할 때만 사용하자.

 

2.6.3 내부 클래스용 특수 문법 규칙

 

앞에서는 내부 클래스 객체의 외부 클래스 참조를 outer로 지칭해서 설명했다. 외부 클래스 참조를 나타내는 실제 문법은 조금 더 복잡하다. 다음 표현식은 외부 클래스 참조를 나타낸다.

 

OuterClass.this

 

예를 들어 내부 클래스 Member의 deactivate 메소드는 다음과 같이 작성할 수 있다.

public void deactivate(){ 
	Network.this.members.remove(this)
}

여기서는 Network.this 문법은 필수가 아니다. 그냥 members로만 참조해도 암묵적으로 외부 클래스 참조를 사용한다.

하지만 외부 클래스 참조가 명시적으로 필요할 때도 있다.

public boolean belongsTo(Network n){
	return Network.this == n;
}

 

* 내부 클래스에는 시간 상수 외에 정적 멤버를 선언할 수 없다.

 

 

'Java' 카테고리의 다른 글

인터페이스와 람다 표현식 (1)  (0) 2020.09.09
Mac에 Java 설치하기  (1) 2017.03.05

Mac에 Java 설치하기


제가 사용하는 컴퓨터가 Mac이라서 구분했지만 다른 OS에서도 같은 방법으로 설치할 수 있습니다.


설치하는 방법은 어렵지 않습니다!

아래 링크로 접근하여 최신 버전의 Java를 다운로드 합니다.

Download: http://www.oracle.com/technetwork/java/javase/downloads/index.html

현재 최신버전은 Java Platform (JDK) 8u121입니다.




Accept License Agreement에 체크를 하고, Mac은 Mac OS X를 다운받고 그외에 OS는 자신이 사용하는 OS, Bit 버전에 따라 다운로드 합니다.




다운받은 dmg파일을 실행하면 아이콘을 더블클릭하라는 표시가 뜹니다. 아이콘을 더블클릭 합시다.

 



계속을 눌러 설치를 진행합니다.



대상 디스크를 선택하는 부분이 저는 넘어갔는데 아닌 분들은 디스크를 선택하시면 됩니다.



설치하기 전에 컴퓨터 암호를 입력합니다.



설치완료!



설치가 제대로 되었는지 확인하기 위해 터미널을 열고 java -version을 입력합니다.

아래 스크린 샷과 같이 버전이 나온다면 설치가 제대로 완료된 것입니다.




'Java' 카테고리의 다른 글

인터페이스와 람다 표현식 (1)  (0) 2020.09.09
자바 클래스 관련 내용 정리  (1) 2020.08.24

+ Recent posts