람다의 이해와 활용을 공부하기 위해서 모던 자바 인 액션의 3장과 9장을 요약해 포스트를 작성하고자 한다.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
함수형 인터페이스는 오직 하나의 추상 메서드만 갖는 인터페이스를 말한다. 함수형 인터페이스는 람다 표현식으로 추상 메서드를 직접 전달할 수 있기 때문에 전체 표현식을 함수형 인터페이스의 인스턴스로 취급
할 수 있다.
자바 API는 함수형 인터페이스로 여러가지를 제공하고 있다.
위 함수형 인터페이스들 외에도 원시 타입을 지원하기 위해 존재하는 IntPredicate
,IntConsumer
,IntFunction
같은 함수형 인터페이스들도 존재한다. 원시 타입을 레퍼런스 타입으로 변환하면서 생기는 박싱 비용을 절감하기 위해 위 함수형 인터페이스 대신 사용할 수 있다.
람다 표현식의 형식을 대상 형식
이라고 부른다. 이런 람다 표현식의 형식을 적합한지를 판단하는 것을 말한다.
List<Integer> list = Arrays.asList(1,2,3,4,5);
list.stream().filter(int -> int > 2).collect(Collectors.toList());
위와 같은 코드가 있을 때 형식 확인 과정은 다음과 같은 순서로 이루어진다.
1. filter
메서드의 선언을 확인한다
2. filter
의 파라미터가 Predicate<? super T>
라는 것을 확인한다
3. Predicate<? super T>
는 T 클래스를 받아서 boolean 으로 리턴하는 함수형 인터페이스임을 인지한다
4. filter
의 파라미터로 전달되는 인수가 Predicate<? super T>
의 추상 메서드를 묘사할 수 있는지 확인한다.
람다는 형식 추론을 통해 람다 표현식을 더 단순화할 수 있도록 한다.
List<Integer> list = Arrays.asList(1,2,3,4,5);
list.stream().filter(int -> int > 2).collect(Collectors.toList()); // 형식 추론
list.stream().filter((Integer int) -> int > 2).collect(Collectors.toList()); // 형식 추론 X
람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수
를 활용할 수 있다. 이와 같은 동작은 람다 캡처링
이라고 부른다. 하지만 람다 캡처링에 사용되는 변수는 변수가 변하지 않는다는 명시적 혹은 암시적
전제 조건이 붙어야만 한다. 이는 근본적으로 지역 변수
는 스택에 인스턴스 변수
는 힙에 저장되는데에서 기인한다.
멀티스레드의 환경 등에서 스레드에서 람다가 실행될 때, 변수를 할당했던 스레드가 사라져서 변수 할당이 해제 되어도 람다는 해당 변수에 접근하려고 할 수 있기 때문에 변수를 할당할 때 local variable의 복사본을 제공한다.
따라서 이 복사본의 값이 변하지 않음을 보장하기 변수에 값을 단 한 번만 할당해야 한다는 제약이 생긴 것이다.
Integer num = 3; // 지역 변수
List<Integer> list = Arrays.asList(1,2,3,4,5);
list.stream().map(n -> n + num).collect(Collectors.toList());
num = 4; // 컴파일 에러 발생 !
자유 변수?
람다를 호출한 메서드가 종료되어도 람다는 메서드의 종료 여부와 관계없이 자유롭게 데이터가 유지되기 때문에 자유변수라고 부른다.
전략 패턴을 적용한 다음과 같은 코드가 있다고 가정해보자.
public interface ValidationStrategy {
boolean execute(String s);
}
public class IsAllLowerCase implements ValidationStrategy{
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
public class IsNumeric implements ValidationStrategy{
@Override
public boolean execute(String s) {
return s.matches("\\d+");
}
}
public class Validator {
private final ValidationStrategy strategy;
public Validator(ValidationStrategy strategy) {
this.strategy = strategy;
}
public boolean validate(String s) {
return strategy.execute(s);
}
}
public static void main(String[] args) {
Validator numericValidator = new Validator(new IsNumeric());
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
}
위 전략 패턴에 Predicate
함수형 인터페이스를 적용하면 위와 같이 새로운 클래스를 구현할 필요가 없어진다.
public static void main(String[] args) {
Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
Validator lowerValidator = new Validator((String s) -> s.matches("\\d+"));
}
다음과 같이 게임의 템플릿 메서드가 존재한다고 가정해보자.
public abstract class Game {
public void login(int id) {
Member member = Database.getMemberWithId(id);
execute(member);
}
abstract void execute(Member member);
}
위에 함수형 인터페이스인 Consumer
를 적용하면 다음과 같아진다.
public abstract class Game {
public void login(int id, Consumer<Member> execute) {
Member member = Database.getMemberWithId(id);
execute.accept(member);
}
}
public static void main(String[] args) {
new Game().login(1234, (Member m) -> System.out.println("Hello, " + m.getName()));
}
Game 클래스를 상속받은 클래스를 구현하지 않고도 직접 람다 표현식을 전달해서 다양한 동작을 추가할 수 있다.
맨유, 맨시티, 리버풀의 경기 결과를 알려주는 옵저버가 있다고 가정해보자. 다양한 옵저버를 그룹화할 Observer
인터페이스와 새로운 트윗이 발생했을 때 호출할 Subject
클래스가 있다.
public interface Observer {
void notify(String tweet);
}
public class MUresult implements Observer{
@Override
public void notify(String tweet) {
if(tweet!=null && tweet.contains("manchester united")) {
System.out.println("MU news published !" + tweet);
}
}
}
public class LFCresult implements Observer{
@Override
public void notify(String tweet) {
if(tweet!=null && tweet.contains("liverpool")) {
System.out.println("LFC news published !" + tweet);
}
}
}
public class MCYresult implements Observer{
@Override
public void notify(String tweet) {
if(tweet!=null && tweet.contains("manchester city")) {
System.out.println("MCY news published !" + tweet);
}
}
}
public interface Subject {
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
public class Feed implements Subject{
private List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer o) {
this.observers.add(o);
}
@Override
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
public class Main {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(new MUresult());
f.registerObserver(new LFCresult());
f.registerObserver(new MCYresult());
f.notifyObservers("mancheter united beat liverpool 3-0");
}
}
위와 같은 옵저버 패턴의 구조에 람다 표현식을 사용해보자
@FunctionalInterface
public interface Observer {
void notify(String tweet);
}
public class Main {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("mancherster united")) {
System.out.println("MU new published !" + tweet);
}
});
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("mancherster city")) {
System.out.println("MCY new published !" + tweet);
}
});
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("liverpool")) {
System.out.println("LFC new published !" + tweet);
}
});
f.notifyObservers("mancheter united beat liverpool 3-0");
}
}
의무 체인 패턴은 한 객체가 처리할 작업을 다른 객체로 전달하고, 다른 객체는 또 다른 객체로 전달하는 식의 패턴을 말한다. 따라서 객체의 정보를 유지하는 필드를 포함하는 작업 처리 추상 클래스로 의무 체인 패턴을 구성하게 된다.
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor;
public void setSuccessor(ProcessingObject<T> successor) {
this.successor = successor;
}
public T handle(T input) {
T r = handleWork(input);
if(successor != null) {
return successor.handle(r);
}
return r;
}
abstract protected T handleWork(T input);
}
// header text 를 넣는 일을 담당
public class HeaderTextProcessing extends ProcessingObject<String>{
@Override
protected String handleWork(String text) {
return "This is Header !" + text;
}
}
// 스펠링 체크를 담당
public class SplleCheckerProcessing extends ProcessingObject<String>{
@Override
protected String handleWork(String text) {
return text.replaceAll("APPEL", "APPLE");
}
}
public class Main {
public static void main(String[] args) {
ProcessingObject<String> headerProcessor = new HeaderTextProcessing();
ProcessingObject<String> spellCheckProcessor = new SplleCheckerProcessing();
headerProcessor.setSuccessor(spellCheckProcessor);
String result = headerProcessor.handle("I LIKE APPEL");
System.out.println(result);
}
}
public class Main {
public static void main(String[] args) {
UnaryOperator<String> headerProcessor =
(String text) -> "This is Header !" + text;
UnaryOperator<String> spellCheckProcessor =
(String text) -> text.replaceAll("APPEL", "APPLE");
Function<String, String> pipeLine = headerProcessor.andThen(spellCheckProcessor);
String result = pipeLine.apply("I LIKE APPEL");
}
}
경우에 따라서 다양한 객체를 return 해줘야 하는 경우에 팩토리 패턴을 생각해볼 수 있다.
public class ProductFactory {
public static Product createProduct(String name) {
switch (name) {
case "loan": return new Loan();
case "stock": return new Stock();
case "bond": return new Bond();
default: throw new IllegalArgumentException("No such product" + name);
}
}
}
위와 같이 이름에 맞는 은행 상품을 만들어주는 ProductFactory
가 있을 때 메서드 참조
와 Map
을 활용해서 코드를 재구현할 수 있다.
public class ProductFactoryLambda {
private final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new);
map.put("bond", Bond::new);
}
public static Product createProduct(String name) {
Supplier<Product> p = map.get(name);
if(p != null) return p.get();
throw new IllegalArgumentException("No such product " + name);
}
}
Supplier
라는 함수형 인터페이스를 활용해서 메서드 참조를 사용했다. 인수로 들어오는 값이 여러 개인 경우에도 함수형 인터페이스를 새롭게 구현함으로써 해결할 수 있다.
@FunctionalInterface
public interface TriFunction<T,U,V,R> {
R apply(T t, U u, V v);
}
public class ProductFactoryTriFunction {
private final static Map<String, TriFunction<Integer, String, String, Product>> map = new HashMap<>();
static {
map.put("SmartPhone", (qty, address, notice) -> new SmartPhone(qty, address, notice));
map.put("Car", (qty, address, notice) -> new Car(qty, address, notice));
map.put("Notebook", (qty, address, notice) -> new Notebook(qty, address, notice));
}
public static Product createProduct(int qty, String address, String notice, String name) {
TriFunction<Integer, String, String, Product> p = map.get(name);
if(p != null) return p.apply(qty, address, notice);
throw new IllegalArgumentException("No such product " + name);
}
}
인수가 여러 개가 필요하면 새로운 함수형 인터페이스를 만들어서 사용하면 된다