잘못된 내용이 있다면 편하게 말씀해주세요 🙏🏻
자바의 '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판 - item 34 : int 상수 대신 열거 타입을 사용하라
'☕️ JAVA > 🦋 Effective Java' 카테고리의 다른 글
자바의 동시성 프로그래밍, 가변 데이터를 동기화하는 3가지 방법(+a. 자바 기본 타입의 원자성에 대하여) (0) | 2022.01.24 |
---|---|
자바 enum에서 ordinal 메서드 사용하지마...(세요) 🚫 (1) | 2022.01.21 |
정확한 답이 필요하다면 float와 double은 피하자 🏃♂️ (0) | 2022.01.18 |
표준 라이브러리를 익히고 사용하자 ㉿ (0) | 2022.01.18 |
지역 변수의 범위를 줄여 쉬운 코드 작성하기 (feat. while 보다는 for) (1) | 2022.01.18 |
댓글