3주간 스터디를 진행하게 됐다. 안그래도 이전에 쭉 정리했던 자바 인 액션 책을 다시 읽어보려던 참이었는데 타이밍 맞게 스터디가 있어 참여하게 됐다. 나름 간단하게 정리하려고 노력했다.
람다 표현식 정리
람다 표현식은 익명 함수를 단순화한 것
Comparator<Apple> byWeight = new Comparator<Apple> () {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
=>
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
다음과 같이 익명 함수를 단순화 시킨다.
어디에서 람다를 사용할 수 있을까? => 함수형 인터페이스라는 문맥에서 사용할 수 있다.
함수형 인터페이스는 뭔데? => 오직 하나의 추상 메서드를 가지고 있는 인터페이스.
public interface Adder {
int add(int a, int b);
} //이것 처럼!!!
public interface SmartAdder extends Adder {
int add(double a, double b);
} // 얘는 결국엔 2개의 메서드이므로 탈락!! 아예 메서드가 없어도 안된다.
함수 디스크립터 : 람다 표현식의 시그니처(함수형 인터페이스의 추상 메서드 시그니처)
예를들어, 위의 Adder는 (int, int) -> int
가 람다 표현식의 시그니처이다.
자바 8에 추가된 함수형 인터페이스는 다음과 같다.
함수형 인터페이스 | 함수 디스크립터 | 기본형 특화 |
---|---|---|
Predicate |
T -> boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer |
T -> void | IntConsumer, LongConsumer, DoubleConsumer |
Function<T, R> | T -> R | IntFunction |
Supplier |
() -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator |
T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator |
(T, T) -> T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate<L, R> | (T, U) -> boolean | |
BiConsumer<T, U> | (T, U) -> void | ObjIntConsumer |
BiFunction<T, U, R> | (T, U) -> R | ToIntBiFunction<T, U>, ToLongBiFunction<T, U>, ToDoubleBiFunction<T, U> |
예시 코드를 들어보자면 다음과 같다.
public class test {
static class Fruit {
String name;
int cnt;
public Fruit(String name, int cnt) {
this.name = name;
this.cnt = cnt;
}
public <T> String getSellableFruitName(T t, Predicate<T> isFruitExist) {
if (isFruitExist.test(t)) {
return this.name;
}
return "can't sell";
}
}
public static void main(String[] args) {
Fruit apple = new Fruit("apple", 0);
Predicate<Fruit> isFruitExist = (Fruit f) -> f.cnt != 0;
String result = apple.getSellableFruitName(apple, isFruitExist);
System.out.println(result); //can't sell
}
}
(T) -> boolean
의 시그니처를 가지는 Predicate
public <T> void printFruitName(T t, Consumer<T> c) {
c.accept(t);
}
Consumer<Fruit> printFruitNameConsumer = (Fruit f) -> System.out.println(f.name);
apple.printFruitName(apple, printFruitNameConsumer); //apple
(T) -> ()
의 시그니처를 가지는 Consumer
public <T, R> R cntPlusOne(T t, Function<T, R> f) {
return f.apply(t);
}
Function<Integer, Integer> plusOneFunction = (Integer i) -> i + 1;
Integer result = apple.cntPlusOne(apple.cnt, plusOneFunction); //1
(T) -> (R)
의 시그니처를 가지는 Function
public <T> T supplyApple(Supplier<T> s) {
return s.get();
}
Supplier<String> appleSupplier = () -> "supply Apple!!";
String result = apple.supplyApple(appleSupplier);
() -> (T)
의 시그니처를 가지는 Supplier
이외의 함수형 인터페이스들은 시그니처를 보고 이해하면 되겠다.
Predicate<Integer> applePrediacte = (Integer i) -> i != 0;
IntPredicate applePrediacte2 = (int i) -> i != 0;
박싱을 하는 것에 소모되는 비용을 막기위해 기본형 특화의 함수형 인터페이스 또한 제공해준다.
위의 예시의 람다 표현식은 인수를 자신의 바디 안에서만 사용했다. 이를 외부에서 정의된 변수(자유변수)를 이용할 수도 있다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
다음과 같이 말이다. 하지만 이 때에는 해당 변수가 불변취급 되어야 한다. 위의 코드에서는 이후 코드에 portNumber에 대한 값이 변경이 되지 않았지만 만약 변경된다면 Runnable 라인에서 컴파일 에러가 발생할 것이다. 고로, final
을 그냥 붙여주는 것이 속편하다.
왜 이렇게 동작하는지는 다음과 같다고 한다.
람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있따. 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에서는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다. 또한 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴에 제동을 걸 수 있다.
메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이다. 가독성을 높일 수 있다.
(Apple apple) -> apple.getWeight() Apple::getWeight
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println
(String s) -> this.isValidName(s) this::isValidName
다음과 같이 표기가 가능하다. 메서드 참조는 다음과 같이 세 가지 유형이 있다.
정적 메서드 참조
Integer의 parseInt를 Integer::parseInt로 표현
다양한 형식의 인스턴스 메서드 참조
String의 length 메서드는 String::length로 표현
기존 객체의 인스턴스 메서드 참조
Fruit 객체의 getName을 Fruit::getName로 표현
(객체 객) -> 객.객체메서드()
이런 경우에 객체:객체메서드
로 표현이 가능하다.
여기서 이렇게도 사용할 수가 있다.
static class Fruit{
String name;
int cnt;
public Fruit(String name, int cnt) {
this.name = name;
this.cnt = cnt;
}
public int getCnt() {
return cnt;
}
}
public static void main(String[] args) {
List<Fruit> fruits = new ArrayList<>(List.of(
new Fruit("apple", 3),
new Fruit("orange", 1),
new Fruit("strawberry", 2)));
fruits.sort(Comparator.comparing(Fruit::getCnt));
for (Fruit fruit : fruits) {
System.out.println(fruit.cnt);
}
}
보통 객체 정렬을 할 때에 Comparator를 많이 사용하게 된다. 람다를 배우기 전이였으면 익명함수로 Comparator를 나타냈을 것이다. 그리고 메서드 참조이전에는 다음과 같이 나타내줬을 것이다.
fruits.sort((f1, f2) -> f1.getCnt() - f2.getCnt());
이것을 비교의 주체가 cnt이므로 이를 메서드로 던져주는 것이다. fruits.sort(Comparator.comparing(Fruit::getCnt));
comparing을 이용한 Comparator 제공은 오름차순을 기준으로 되어있으며 역방향을 원한다면 .reversed()
를 붙여주면 되겠다.
만약 동일한 값에 대한 메서드를 추가로 붙이고 싶다면 .thenComparing(기준이 될 Comparator)
를 체이닝하면 된다.
생성자 또한 메서드 참조로 대체할 수 있다.
public Fruit(String name, int cnt) {
this.name = name;
this.cnt = cnt;
}
BiFunction<String, Integer, Fruit> constructorMethodReference = Fruit::new;
//(s, i) -> new Fruit(s,i) 이것과도 같음
람다 표현식을 입맛대로 조합할 수도 있다.
Predicate<Apple> notRedApple = redApple.negate(); //결과의 반전
Predicate<Apple> redAndHeavyApple =
redApple.and(apple -> apple.getWeight() > 150); //redApple 에다가 and 뒤의 predicate 까지도 추가
//위의 코드에 .or(apple -> GREEN.equals(a.getColor())); 를 붙여서 '또는 predicate' 을 추가할 수도 있다.
Predicate 말고 Function도 비슷한 것이 존재한다. andThen
과 compose
이다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
f.andThen(g)
은 f 후에 g를 실행한다.
f.compose(g)
은 g 후에 f를 실행한다.
'공부 기록들 > 우테코' 카테고리의 다른 글
[모던 자바 인 액션 스터디] 4장 스트림 소개 (0) | 2021.03.14 |
---|---|
순차적 스트림, 병렬 스트림 그리고 findAny와 findFirst 에 대해 (0) | 2021.03.07 |
함수형 인터페이스를 이용할 때 착각했던 점 (0) | 2021.03.05 |
로또 구현 피드백 정리 (0) | 2021.03.01 |
자동차 경주 1차 피드백 정리 (0) | 2021.02.11 |