본문 바로가기
☕️ JAVA/🦋 Effective Java

자바의 'enum'과 'lambda'를 클래스로 이해해보기 🔎

by kukim 2022. 1. 21.

잘못된 내용이 있다면 편하게 말씀해주세요 🙏🏻

 

 자바의 'enum'과 'lambda'가 클래스로 어떻게 구현되는지 살펴보고 비교해보자

 

목차

enum과 클래스

lambda, 사용 방법과 주의사항

enum, enum + lambda, 클래스 비교


enum

보통 enum(열거형)은 일정 개수의 상수 값을 정의하고 이 값을 문자열로 대체하여 하드 코딩을 아름답게(?) 만들어 준다. 자바enum은 어떨까? 사실 조금 특별하다. 아래 예는 Operation 이름의 enum이다. 

enum Operation {
    PLUS("+"),
    MINUS("-"),
    TIMES("*"),
    DIVIDE("/");

    private final String op;

    Operation(String op) {
        this.op = op;
    }
}

위 enum을 이해하기 어렵다면 클래스로 바꿔보자.

class Operation {
    public static final Operation PLUS = new Operation("+");
    public static final Operation MINUS = new Operation("-");
    public static final Operation TIMES = new Operation("*");
    public static final Operation DIVIED = new Operation("/");

    private String op;

    private Operation(String op) {
        this.op = op;
    }
}


public class enumTest {
    public static void main(String[] args) {
        Operation op = Operation.PLUS;
        Operation op2 = Operation.PLUS;
        System.out.println(op == op2); // true
    }
}

그렇다. 자바에서 enum은 단순히 클래스이다. enum에서 말하는 상수는, 상수 하나당(PLUS, MINUS...) 자신의 public static final 형태의 인스턴스를 가지고 있다. 이는 == 연산이 가능한 것이다. 해당 각 상수는 유일한 인스턴스 값이기 때문에 비교할 수 있다. 또한 enum 자체를 밖에서 생성할 수 없도록 private 생성자로 되어있다. 이는 'singleton(싱글턴) 타입'이다. 

다른 언어의 enum은 값 하나만 가지고 있지만 자바의 enum 클래스라 특별하다. 이는 특정 상수에 하나의 값만 가지고 있는 것이 아니라 다양한 값들(여기서는 PLUS, MINUS, TIMES, DIVIED 기본 이름과 추가로 "+", "-",... 이 있음)을 가지고 있다. 또한 메서드도 가지고 있다. 아래 예 '코드 1'은 enum Operation에 메서드를 넣은 것이다. apply 메서드를 abstract로 만들고 이를 오버 라이딩하고 있다. 이를 클래스로 다시 표현한 것은 '코드 2'에서 볼 수 있다.

 

// 코드 1
enum Operation {
    PLUS("+") { public double apply(double x, double y) { return x + y; }},
	
    // ...

    private final String op;
    Operation(String op) {
        this.op = op;
    }
    public abstract double apply(double x, double y);
}

// 코드 2
class Operation {
    public static final Operation PLUS = new Operation("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    };
    
    // ...
    
    
    private String op;
    private Operation(String op) {
        this.op = op;
    }
    public double apply(double x, double y) {
        return 0;
    }
}


// 사용 방법
public class lambda_enum {
    public static void main(String[] args) {
        System.out.println(Operation.PLUS.apply(20, 10)); // 30.0
        System.out.println(Operation.MINUS.apply(20, 10)); // 10.0
        System.out.println(Operation.TIMES.apply(20, 10)); // 200.0
        System.out.println(Operation.DIVIDE.apply(20, 10)); // 2.0
}

'코드 1'은 실제 '코드 2'로 구현되어 있다고 볼 수 있다. private 생성자를 통해 new Operation, 인스턴스를 생성하고 apply 함수를 오버라이딩 한다. 그 예는 메인 문을 보면 알 수 있다. (Operation.PLUS.apply(20, 10))

 


lambda, 사용 방법과 주의사항

자바에서 메서드는 항상 클래스 밑에 만들어진다. 1990년대 당시 개발자들은 함수만으로 존재할 수 없을까? 란 생각으로 함수 객체란 것을 만들어졌다. 이 구현체는 '추상 메서드를 하나만 담은 인터페이스'와 '익명 클래스'를 조합하여 함수만 있는 것처럼 사용했다.

// 추상 메서드 하나만 있는 인터페이스
interface myFunction {
    public abstract int max(int a, int b);
}

// 인터페이스를 익명 클래스로 생성하여 해당 메서드를 오버라이딩하여 함수 객체를 생성함
MyFunction f = new MyFunction() {
            @Override
            public int max(int a, int b) {
                return a > b ? a : b;
            }
        };

System.out.println(f.max(3, 5));

 

이는 자주 사용되었다. 하지만 익명 클래스 방식은 코드가 너무 길기 때문에 사용하기에 힘들었다.

시간이 흘러 자바 8에서 위 방식을 '람다식(lambcda expression)'라는 손쉬운 방법으로 풀어냈다. 람다는 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하다. 

@FunctionalInterface
interface MyFunction {
	void run();
}


class LambdaEx {
	public static void main(String[] args) {
		// 람다식으로 MyFunction의 run()을 구현
		MyFunction f1 = ()-> System.out.println("람다");

		MyFunction f2 = new MyFunction() {  // 익명클래스로 run()을 구현
			public void run() {   // public을 반드시 붙여야 함
				System.out.println("익명 클래스)");
			}
		};

		f1.run(); // 람다
		f2.run(); // 익명 클래스
	}
}

 

아래 예는 Compare의 예이다. 

public class lambda {
    @Test
    void 익명클래스() {
        List<String> word = new ArrayList<>(List.of("1","333","22"));

        System.out.println(word);
        Collections.sort(word, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return Integer.compare(o1.length(), o2.length());
            }
        });

        System.out.println(word);
    }

    @Test
    void 람다() {
        List<String> word = new ArrayList<>(List.of("1","333","22"));
        System.out.println(word);

        Collections.sort(word, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
        System.out.println(word);
    }

    @Test
    void 람다_타입추론에러() {
        // List뒤에 <> 의 타입을 넣어주지 않으면 타입 추론이 불가
        List word = new ArrayList<>(List.of("1","333","22"));
        System.out.println(word);

//        Collections.sort(word, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
        System.out.println(word);
    }

    @Test
    void 람다에_타입명시() {
        // List뒤에 <> 의 타입을 넣어주지 않으면 타입 추론이 불가
        List word = new ArrayList<>(List.of("1","333","22"));
        System.out.println(word);

        Collections.sort(word, (String s1, String s2) -> Integer.compare(s1.length(), s2.length()));
        System.out.println(word);
    }

}

특별한 점은 람다는 컴파일러가 타입을 추론하여, 타입 명시를 생략해도 된다. 하지만 타입에 대한 정보가 적다면 타임을 직접 명시해줘야 하는 경우도 있다. 

 

람다 사용과 주의 사항

✅ 람다 작성 시 모든 매개변수 타입은 생략하여 간편하게 쓰자. 단, 타입 명시해야 하는 코드가 더 이해가 쉽거나 컴파일러가 타입을 찾지 못할 땐 작성한다.

✅ 람다는 되도록 한 줄, 최대 세 줄 안에 끝내자. 세 줄이 넘어간다면 줄여보거나 안 쓰는 쪽을 고려하자.

 

🧐 람다는 함수 인터페이스로 만들어진다. 익명 클래스처럼 추상 클래스의 인스턴스는 불가하다.

🧐 인터페이스의 추상 메서드가 2개 이상이라면 람다로 표현이 불가능하다 (@Funcionalinterface 어노테이션이 하나만 가능하게 체크)

🧐 람다는 자기 자신 참조가 불가능하다. (익명 클래스는 자신 참조하지만 람다는 바깥을 참조한다)

@FunctionalInterface
interface FunctionalInterfaceTest {
    String name = "kuku";
    void testMethod();
}

public class lambda_this_error {
    String name = "yeye";

    public void test() {
        FunctionalInterfaceTest f1 = new FunctionalInterfaceTest() {
            @Override
            public void testMethod() {
                System.out.println(this.name); // "kuku" - 익명 클래스의 this는 인스턴스 자신, 따라서 인터페이스의 "kuku"
            }
        };

        FunctionalInterfaceTest f2 = () -> System.out.println(this.name); // "yeye" - 람다의 this는 자신을 못가르키고 바깥 인스턴스, "yeye"
        f1.testMethod(); // "kuku"
        f2.testMethod(); // "yeye"
    }
}

🧐 람다, 익명 클래스는 모두 가상 머신 별 직렬화 구현이 다르다. 따라서 직렬화는 사용을 자제하고, 직렬화 해야 하는 함수 객체가 있다면 private 정적 중첩 클래스를 사용하자.

 

enum, enum + lambda, 클래스 비교

이제 처음 살펴봤던 enum Operation을 람다로 표현하고 이 람다를 클래스로 변경하여 이해해보자.

 

먼저 enum과 enum + lambda 코드를 살펴보자.

코드 1(enum)과 코드 2(람다)의 차이점은 apply 메서드를 정의하는 데 있다. 코드 1은 각 PLUS, MINUS,... 의 apply() 메서드를 익명 클래스와 오버라이딩을 통해 사용하고 있다면 코드 2는 람다(함수)를 통째로 넘겨 간결하게 표현하고 있다. 이때 함수를 저장하기 위해 java.util.Function 에 이미 구현되어 있는 함수형 인터페이스 BiFunction를 사용하여 함수를 저장하고 사용한다.

// 코드 1 : enum
enum Operation {
    PLUS("+") { public double apply(double x, double y) { return x + y; }},
    MINUS("-") { public double apply(double x, double y) { return x - y; }},
    TIMES("*") { public double apply(double x, double y) { return x * y; }},
    DIVIDE("/") { public double apply(double x, double y) { return x / y; }};

    private final String op;

    Operation(String op) {
        this.op = op;
    }

    public abstract double apply(double x, double y);
}


// 코드 2 : enum + 람다
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

enum Operation {
    PLUS("+", (x, y) -> x + y),
    MINUS("-", (x, y) -> x - y),
    TIMES("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String op;
    private final BiFunction<Double, Double, Double> fuc;

    Operation(String op, BiFunction<Double, Double, Double> fuc) {
        this.op = op;
        this.fuc = fuc;
    }

    public double apply(double x, double y) {
        return fuc.apply(x, y);
    }
}

 

코드 2(enum + lambda)를 클래스로 표현하면 아래와 같다. Operation을 private 생성자로 생성하고 있고, BiFunction 인터페이스를 익명 클래스로 생성하여, 그 안의 apply 메서드를 오버라이딩하여 사용하고 있다.

class Operation {
    public static final Operation PLUS = new Operation("+",
            new BiFunction<Double, Double, Double>() {
                @Override
                public Double apply(Double x, Double y) {
                    return x + y;
                }
            });
	
    // ... MINUS, TIMES, DIVIDE 생략
    
    private String op;
    private BiFunction<Double, Double, Double> fuc;

    private Operation(String op, BiFunction<Double, Double, Double> fuc) {
        this.op = op;
        this.fuc = fuc;
    }

    public double apply(double x, double y) {
        return fuc.apply(x, y);
    }
}

 


⛓ Reference

자바의 정석 3판 - ch 12 열거형

이펙티브 자바 3판 - item 34 : int 상수 대신 열거 타입을 사용하라 

이펙티브 자바 3판 - item 42 : 익명 클래스보다는 람다를 사용하라

https://www.baeldung.com/java-singleton

댓글