[윤성우의 열혈 Java 프로그래밍] Chapter 26 - 네스티드 클래스와 람다(Lambda)의 소개

Update:     Updated:

카테고리:

태그:

26-1. 네스티드(Nested) 클래스와 이너(Inner) 클래스

class Outer {
  class Nested {...}
}

클래스 내에 정의된 클래스를 가리켜 네스티드 클래스(Nested Class)라 하고,
이를 감싸는 클래스를 외부 클래스(Outer Class)라 한다.

- 네스티드 클래스의 구분

image
static이냐, non static이냐로 나뉘고,
non static 네스티드 클래스는 다시 멤버 클래스, 로컬 클래스, 익명 클래스로 나뉜다.

- Static 네스티드 클래스 (Static Nested Class)

class Outer {
  private static int num = 0;

  static class Nested1 {
    void add(int n) {
        num += n;   // Outer 클래스의 static 변수를 공유한다.
    }
  }

  static class Nested2 {
    int get() {
      return num; // Outer 클래스의 static 변수를 공유한다.
    }
  }
}

public class StaticNested {
  public static void main(String[] args) {
    // 인스턴스 생성방법
    Outer.Nested1 nst1 = new Outer.Nested1();
    Outer.Nested2 nst2 = new Outer.Nested2();

    // static 변수 num을 공유한다.
    nst1.add(5);
    System.out.println(nst2.get()); // 5가 출력됨
  }
}

Static 네스티드 클래스의 인스턴스 생성은 외부 클래스의 인스턴스 생성과 무관하다.
따라서, 다음도 유추해볼 수 있다.

Static 네스티드 클래스 내에서 외부 클래스의 인스턴스 변수와 메소드에 접근 불가능하다.
즉, Static 네스티드 클래스 내에서는 외부 클래스에 static으로 선언된 변수와 메소드에만 접근이 가능하다.

- 이너(Inner) 클래스의 구분

네스티드 클래스 중에서 static 선언이 붙지 않은 클래스를 가리켜 ‘이너 클래스’라 한다.

  • 멤버 클래스 (Member Class)
  • 로컬 클래스 (Local Class)
  • 익명 클래스 (Anonymous Class)

이들 중 다음 둘은 정의된 위치에 따라서 구분이 된다.

class Outer {
  /**
  * 멤버 클래스
  * 인스턴스 변수, 인스턴스 메소드와 동일한 위치에 정의
  */
  class MemberInner {...} 
  
  /**
  * 로컬 클래스
  * 중괄호 내에, 특히 메소드 내에 정의
  */
  void method() {
    class LocalInner {...}
  }
}

- 멤버 클래스(Member Class)

image

Member 클래스 내에서는 Outer 클래스의 인스턴스 변수에 접근이 가능하다.
그리고 코드에서 볼 수 있듯이

멤버 클래스의 인스턴스는 외부 클래스의 인스턴스에 종속적이다.

- '멤버 클래스'를 언제 사용하는가?

클래스의 정의를 감추어야 할 때 유용하게 사용이 된다.

interface Printable {
  void print();
}

class Paper {
  private String con;

  public Paper(String con) {
    this.con = con;
  }

  public Printable getPrinter() {
    return new Printer();   // 멤버 클래스 인스턴스 생성 및 반환
  }

  private class Printer implements Printable {    // 멤버 클래스의 정의
    public void print() {
      System.out.println(con);
    }
  }
}

public class UseMemberInner {
  public static void main(String[] args) {
    Paper p = new Paper("서류 내용: 행복합니다.");
    Printable prn = p.getPrinter();
    prn.print();    // 서류 내용: 행복합니다.
  }
}

Printer 클래스는 private으로 선언되었기 때문에 이 클래스를 감싸는 클래스 내에서만 인스턴스 생성이 가능하다. 이 때문에 Papers 클래스의 외부에서는 getPrinter 메소드가 어떤 인스턴스의 참조 값을 반환하는지 알지 못한다.
다만, 반환되는 참조 값의 인스턴스가 Printable을 구현하고 있어서 Printable의 참조 변수로 참조할 수 있다는 사실만 안다.
이러한 상황을 ‘클래스의 정의가 감추어진 상황’이라 한다.

클래스의 정의를 감추면 getPrinter 메소드가 반환하는 인스턴스가 다른 클래스의 인스턴스로 변경되어도 Papers 클래스 외부의 코드는 수정할 필요가 없다.
즉, 코드에 유연성이 부여되었다.

public static void main(String[] args) {
  List<String> list = new ArrayList<>();
  Iterator<String> itr = list.iterator();
}
public class ArrayList<E> implements List<E> {
  ....
  public Iterator<E> iterator() {
    return new Itr(); // 멤버 클래스의 인스턴스 생성 및 반환
  }

  private class Itr implements Iterator<E> {  // 멤버 클래스의 정의
    ...
  }
}

전에 보았던 iterator 메소드도 보다싶이 inner class로 정의되어있다.
이를 통해 반복자가 Outer Class의 멤버에 직접 접근이 가능했던 이유를 알 수 있다.

- 로컬 클래스 (Local Class)

‘로컬 클래스’는 ‘멤버 클래스’와 매우 유사하다.
다만, 클래스의 정의 위치가 if문이나 while문 또는 메소드 몸체와 같은 블록 안에 정의된다는 점에서 ‘멤버 클래스’와 구분된다.

interface Printable {
  void print();
}

class Paper {
  private String con;

  public Paper(String con) {
    this.con = con;
  }

  public Printable getPrinter() {
    class Printer implements Printable {    // 로컬 클래스의 정의
      public void print() {
        System.out.println(con);
      }
    }
    return new Printer();   // 로컬 클래스 인스턴스 생성 및 반환
  }
}

public class UseLocalInner {
  public static void main(String[] args) {
    Paper p = new Paper("서류 내용: 행복합니다.");
    Printable prn = p.getPrinter();
    prn.print();    // 서류 내용: 행복합니다.
  }
}

멤버 클래스보다도 클래스를 더 깊이, 특정 블록 안으로 감추는 효과가 있다.

- 익명 클래스 (Anonymous Class)

익명 클래스의 이해를 돕기 위해 위의 클래스를 익명 클래스의 형태로 재정의해보자.
image

실제로 전에 Collection 공부할 당시 쓰였던 코드를 익명 클래스의 정의 형태로 다시 만들어보았다.

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class SSComp implements Comparator<String> {
  @Override
  public int compare(String s1, String s2) {
    return s1.length() - s2.length();
  }
}

public class SortComparator {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("ROBOT");
    list.add("APPLE");
    list.add("BOX");

    SSComp cmp = new SSComp();      // 정렬 기준
    Collections.sort(list, cmp);    // 정렬 기준 변경해서 정렬 진행
    System.out.println(list);
  }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class AnonymousComparator {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("ROBOT");
    list.add("APPLE");
    list.add("BOX");

    Comparator<String> cmp = new Comparator<String>() {
      @Override
      public int compare(String o1, String o2) {
        return o1.length() - o2.length();
      }
    };

    Collections.sort(list, cmp);
    System.out.println(list);
  }
}

26-2. 람다(Lambda)의 소개

- 람다의 이해

람다를 사용하면 코드를 줄일 수 있고, 그렇게 만들어진 코드는 가독성도 뛰어나다.
다음 예시를 보고 람다를 이해해보자.

1) 기존 방식

// 인터페이스 Printable을 구현하는 인스턴스의 생성을 위해..
interface Printable {
  void print(String s);
}

// 1. Printable 인터페이스를 구현하는 Printer 클래스를 정의함
class Printer implements Printable {
  public void print(String s) {
    System.out.println(s);
  }
}

public class Lambda1 {
  public static void main(String[] args) {
    // Printer 클래스를 통해 인스턴스를 생성함
    Priatable prn = new Printer();
    
    prn.print("What is Lambda?");
  }
}


2) 익명 클래스 활용

// 인터페이스 Printable을 구현하는 인스턴스의 생성을 위해..
interface Printable {
  void print(String s);
}

public class Lambda2 {
  public static void main(String[] args) {
    // 2. 익명 클래스의 적용을 통해 기존 Printer 클래스의 정의를 하나 줄여줌
    Priatable prn = new Printer() {
      public void print(String s) {
        System.out.println(s);
      }
    };
    
    prn.print("What is Lambda?");
  }
}


3) 람다 활용

// 인터페이스 Printable을 구현하는 인스턴스의 생성을 위해..
interface Printable {
  void print(String s);
}

public class LambdaExample {
  public static void main(String[] args) {
    // 위의 예시를 람다 기반으로 수정함
    Printable prn = (s) -> { System.out.println(s); };
    
    prn.print("What is Lambda");
  }
}


익명 클래스와 람다는 분명 다르지만 둘 다 인스턴스의 생성으로 이어지고, 람다식이 익명 클래스의 정의를 일부 대체하기 때문에 익명 클래스의 정의를 기반으로 람다식을 이해해보자.

image
핵심은 참조변수의 type 정보를 보고 컴파일러가 어디까지 유추할 수 있는가에 있다.
컴파일러가 참조변수의 type(Printable)을 보고

  1. 아! 오른쪽에 Printable 인터페이스를 구현하는 인스턴스가 와야하는 구나
  2. Printable 인터페이스에 추상 메소드가 하나 밖에 없으니 몸체에 오는 건 그 메소드의 내용이겠구나

위 두 가지 정보를 유추할 수 있다. 따라서 생략도 가능하다.

image

- 람다식의 인자 전달

람다식을 메소드의 인자로 전달할 수도 있다.

interface Printable {
  void print(String s);
}

class Show {
  public static void ShowString(Printable p, String s) {
    p.print(s);
  }
}

public class LambdaExample {
  public static void main(String[] args) {
    showString((s) -> { System.out.println(s); }, "What is Lambda?");
  }
}

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기