개요

Effective Java 7장에서 등장하는 개념을 공부하기 위한 포스팅이다.


1-1) 익명 클래스(Anonymous Class)

프로그램에서 일시적으로 한번만 사용되고 버려지는 객체다. 

분명히 Java Spring을 공부할 때는 코드의 재사용성을 고려하며 프로그래밍을 해야 좋은 개발자라고 배웠다.

그렇다면 재사용이 불가능하고 확장성도 좋지 않은 익명클래스를 왜 사용하는 걸까?

 

    1. 프로그램 내에서 일시적으로 사용되는 객체 처리에 사용한다.
    2. 확장성을 활용하는 것이 유지보수에서 더 불리할 경우에 사용한다.

해당 블로그 에서 이를 이해하기 위한 아주 좋은 예시를 들고 있다.

일부를 발췌해서 이해해보자.

 


1-2) 확장성이 필요없는 극단적인 예시 (출처)

곤충이라는 부모클래를 상속받는 거미 구현 클래스가 있다. 

거미는 전 세계에 약 3만 종이 알려져 있고, 이 3만종 거미들은 공통적인 특징도 없다. 심지어 프로그램 로직에서는 단 한번만 등장하고 사라진다고 하자. 

 

이때 개발자가 할 수 있는 선택은 다음과 같다.

  1. 3만종의 거미클래스를 만들어 유지보수한다.
  2. 익명 클래스를 활용하여 한번 쓰고 버린다!

 

이러한 경우에 클래스를 별도로 만들 필요없이, 코드에서 익명 객체를 생성/정의하여 단 한번의 쓰임을 다 하고 소멸하게 하는 것이다.

단 하나의 예시로 바로 이해할 수 있도록 좋은 글을 남겨주신 Lim-Ky(출처)님 감사합니다😃

 


1-3) 익명 클래스 예제 (출처 )

보통 상속을 정의하고 사용하기 위해서는 부모 클래스 정의 > 자식 클래스 정의 > 상속 > 객체 인스턴스 초기화와 같은 과정을 거친다.

이를 코드로 나타내면 다음과 같다.

// Animal이라는 부모 클래스
class Animal {
    public String bark() {
        return "동물이 우는 소리";
    }
}

// Dog라는 자식 클래스(Animal을 상속)
class Dog extends Animal {
    @Override
    public String bark() {
        return "개가 짖는 소리";
    }
}

public class Main {

    public static void main(String[] args) {
        Animal dog = new Dog();
        System.out.println(dog.bark());  // 개가 짖는 소리
    }
}

 

 

위 코드를 통해 일반적인 상속 예제에서는 부모 클래스와 그를 상속 받는 자식 클래스를 정의하고, 객체 인스턴스를 생성한 뒤에 Override된 메소드를 사용하는 것을 알 수 있다. 하지만 익명 클래스는, 클래스의 정의와 동시에 객체를 생성할 수 있다.

 

// Animal이라는 부모 클래스
class Animal {
    public String bark() {
        return "동물이 우는 소리";
    }
}

public class Main {

    public static void main(String[] args) {
        // 객체 생성과 동시에 클래스 정의
       Animal dog = new Animal() {
           @Override
           public String bark() {
               return "개가 짖는 소리";
           }
       };  // 익명 클래스의 끝에 세미콜론을 반드시 붙여 주어야 한다.
   
        dog.bark();
    }
}

이렇게 bark를 Override한 메소드 활용이 단발성이라면, 위와 같이 익명 클래스를 선언해서 쓰는 것이 좋다.

익명 클래스라는 기법을 통해 긴 상속 과정을 압축하고, 익명으로 한방에 선언하고 날릴 수 있기 때문이다.


1-4) 익명 클래스와 람다

이러한 익명 클래스는 1997년 JDK 1.1이 등장하면서 함수 객체를 만드는 주요 수단으로 썼다.

Java 8에서는 함수형 인터페이스, 람다, 메서드 참조 등 개념이 등장하면서 함수 객체를 더 쉽게 만들 수 있게 되었다. 

이펙티브 자바에서는 이러한 익명 클래스의 인스턴스를 함수 객체로 사용하는 대신, Lambda를 사용하는 것이 좋은 기법이라고 소개하고 있다. 


2-1) 람다(Lambda)

람다식은 메소드를 하나의 식으로 표현한 것이다. 익명 클래스와 비슷한 개념이지만 코드는 훨씬 간결하다. 

표현식은 다음과 같다.

 

(매개변수목록) -> {함수 몸체}

 

메소드의 매개변수를 전달할 수 있고, 메소드의 결과값으로 반환도 가능하다.

따라서 람다 표현식을 사용하면, 기존의 불필요한 코드를 줄여주고 작성된 코드의 가독성을 높여준다.

 

아래 예제를 통해 이해해보자. 

첫번째 코드는 익명 클래스의 인스턴스를 함수 객체로 사용한 예제이다.

 

   List<String> words = new ArrayList<>();

        words.add("apple");
        words.add("banana");
        words.add("cherry");

        // words를 sort하는 class method 
        Collections.sort(words, new Comparator<String>() {
            // 익명 클래스 정의
            public int compare(String s1, String s2) {
                return Integer.compare(s1.length(), s2.length());
            }
        });

        for (String word : words) {
            System.out.println(word);
        }

 

위 익명 클래스를 다음과 같이 Lambda로 간결화할 수 있다.

 

   Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

 

 

익명 클래스와 비교해서 코드가 매개변수와 반환값 관련해서 훨씬 간결해졌다. 이렇게 람다를 사용할 때는 매개변수의 타입을 생략해준다. 어차피 컴파일 과정에서 컴파일러가 타입 추론 원리로 타입을 추론해주기 때문이다. (100% 가능한 것은 아니고, 여기서 자세히 얘기하진 않는다. 이는 타입 추론 관련한 포스팅에서 알아본다.)


 

🍒 여기서 끝아님.

람다 표현식에 Java의 method reference를 쓰면 코드를 훨씬 더 간결하게 쓸 수 있다.

2-2) 메서드 참조(method reference) 

❗️ 메서드 참조는 여기(포스팅 첨부) 서 더 자세히 설명한다. 

 

위 람다 표현식은 다음과 같이 수정할 수 있다.

 

Collections.sort(words, comparingInt(String::length));

*문자열의 길이를 정렬하는 함수 length가 있고, String class를 정렬..


2-3) 람다(Lambda) 사용 시 주의할 점

1. 람다는 이름이 없고 문서화도 불가능하므로, 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 쓰지 말아야 한다.

  • 코드 줄 수가 많다는 기준 - 한 줄이 가장 좋고 길어야 세 줄!

 

2. 람다가 익명 클래스보다 훨씬 간결한 것은 맞지만, 대체 불가능한 경우가 있다.

  • 람다는 함수형 인터페이스에서만 쓰이므로, 추상 클래스의 인스턴스를 만들 때는 익명 클래스를 써야 한다.
  • 람다는 자기자신을 참조할 수 없다.(this X) 함수 객체가 자신을 참조해야 한다면 익명 클래스를 써야 한다.
  • 람다를 직렬화하는 일은 극히 삼가야 한다.
sebinChu