본문 바로가기
Programming/Languages (Java, etc)

[Java] Java 8 사용하기 : 람다와 스트림으로 리팩터링

by kghworks 2023. 9. 23.

목차

  • 람다로 대체할 수 있는 코드
  • method reference로 한번 더 리팩터링
  • Streams를 활용한 declarative programming
  • 람다와 OOP 디자인 패턴

 

https://kghworks.tistory.com/140

 

[Java] Java 8 사용하기 : 가독성과 유연성, 람다와 Streams API

목차 Java 8로 향상되는 가독성과 유연성 익명 클래스 -> 람다 표현식 람다 표현식 -> 메서드 참조 명령적 데이터 가공 -> Streams API 함수형 인터페이스 Java 8 이상의 버전을 사용하면서 legacy를 Java 8

kghworks.tistory.com

 

 앞선 포스팅에서 Java 8이 제공하는 람다와 스트림에 대한 유용함을 간단히 알아보았다. 이번에는 기존 코드들을 람다로 리팩터링 하고, 적절한 OOP 패턴들에도 적용해 보겠다. 거듭 설명하듯, 람다와 스트림 자체에 대해서는 사전에 정확한 사용법, 예시를 숙지하길 바란다. 구현예시들을 살펴봐도 좋고, Oracle tutorial도 아주 괜찮다.

 

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

 

Lambda Expressions (The Java™ Tutorials > Learning the Java Language > Classes and Objects)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 

 


람다로 대체할 수 있는 코드

 

함수형 인터페이스 @FunctionalInterface

 람다를 사용하기 위해 알아야할 기본 개념이 함수형 인터페이스이다. 함수형 인터페이스는 단 하나의 추상 메서드만을 가지고, 람다에서 사용될 목적으로 작성된 인터페이스를 말한다. @FunctionalInterface 가 붙은 클래스들이다. (@은 생략 가능) Java에서 first-class로 method를 승격시키기 위해 도입한 개념이다.  대표적인 함수형 인터페이스로 Predicate, Consumer, Function들이 있다.

 

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

 

오직 함수형 인터페이스만 람다 표현식으로 구현할 수 있다.

함수형 인터페이스의 추상 메서드가 람다 익명 메서드로 구현된다.

 

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

...

// anonymous class
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
};

// lamda
Runnable r2 = () -> System.out.println("Hello World");

 

  Runnable은 함수형 인터페이스로, 단 하나의 추상 메서드만을 가진다. 따라서 위와 같이 람다로 대체할 수 있다.

 

 

람다를 사용할 수 없는 경우

  • this, super 키워드
  • this : anonymous class의 인스턴스 자신, lamda에선 lamda를 감싸는 클래스의 인스턴스
  • enclosing class와 동일한 변수명 사용
  • overloading
int a = 10;
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        int a = 20;
        System.out.println("Hello World! : "+ a); // Hello World! : 20
    }
};

Runnable r2 = () -> {
    int a = 30; // compile error : Variable 'a' is already defined in the scope
    System.out.println("Hello World");
};
public interface Task {
    public void execute();
}

public static void doSomething(Runnable r) { r.run(); }
public static void doSomething(Task a) { a.execute(); }

// anonymous class
doSomething(new Task() {
    @Override
    public void execute() {
        System.out.println("Danger danger!!");
    }
});

// lamda
doSomething(() -> System.out.println("Danger danger!!")); // compile error : reference to doSomething is ambiguous
doSomething((Task)() -> System.out.println("Danger danger!!")); // OK

method reference로 한번 더 리팩터링

 

[method의 class]::[method name]

Member::getAge // (Member m) -> m.getAge()
Member::getAgeLevel // (Member m) -> m.getAgeLevel()

 

 

 람다 body에서 사용하는 메서드가 하나인 경우에만, method reference로 간략하게 대체할 수 있다. 위에서 보듯 때로는 람다보다 가독성이 더 좋을 수 있기 때문에 본인의 경우 대체할 수 있는 상황이면 거의 무조건 적용하는 편이다. (특히 복잡한 stream 코드에서 유용)

 

List<String> memberLsit=  Arrays.asList("karina", "winter", "gisele", "hani", "minzi");
memberList.sort((String s1, String s2) -> s1.compareToIgnoreCase(s2)); // lamda
memberList.sort(String::compareToIgnoreCase); // method reference


// lamda
Map<Member.AgeLevel, List<Member>> byTeam = memberList.stream()
    .collect(groupingBy(member -> {
        member.getAgeLevel();
    }));
    
// method reference
Map<Member.AgeLevel, List<Member>> byTeam = memberList.stream()
    .collect(groupingBy(Member::getAgeLevel));
    
// lamda
memberList.sort((Member m1, Member m2) -> 
	m1.getAge().compareTo(m2.getAge()));

// method reference
memberList.sort(comparing(Member::getAge));

// lamda
int totalAge = memberList.stream()
	.map(Member::getAge).reduce(0, (a, b) -> a + b);
  
// method reference + reduction operation + built-in helper method
int totalAge = memberList.stream()
	.collect(summingInt(Member::getAge));

// Constructor references
Supplier<Member> member = () -> new Member(); // lamda
Supplier<Member> member = Member::new; // constructor reference

Function<String, Member> memberFunction1 = (name) -> new Member(name); // lamda
Member karina = memberFunction1.apply("karina");

Function<String, Member> memberFunction2 = Member::new; // constructor reference
Member karina = memberFunction2.apply("karina");

 

리팩터링 예시 : 익명 클래스 -> 람다 표현식 (타입 명시 -> 타입 추론) -> method reference

// step 1. 일반 코드
public class MemberComparator implements Comparator<Member> {
  @Override
  public int compare(Member m1, Member m2) {
    return m1.getAge().compareTo(m2.getAge());
  }
}

memberList.sort(new MemberComparator());

// step 2. 익명 클래스
memberList.sort(new Comparator<Member>() {
  @Override
  public int compare(Member m1, Member m2) {
    return m1.getAge().compareTo(m2.getAge());
  }
});

// step 3. 람다 표현식
memberList.sort((Member m1, Member m2) -> 
		m1.getAge().compareTo(m2.getAge())); // 타입 명시

memberList.sort((m1, m2) -> 
		m1.getAge().compareTo(m2.getAge())); // 타입 추론

import static java.util.Comparator.comparing;

memberList.sort(comparing(member -> member.getAge())); // comparing static method 

// step 4. method reference
memberList.sort(comparing(Member::getAge));

 


Sreams를 활용한 declarative programming

 

 declarative programmingHow보다 What에 집중하는 것을 말한다. 한국어로 뭐라하는지 모름. 대표적으로 SQL Query를 보면, 우리는 DBMS에 질의할 때 무슨 (What) 데이터가 필요한지를 SQL에 명시하지, 어떻게 (How) 데이터를 탐색할지는 명시하지 않는다. (SQL hint를 통해 인위적으로 옵티마이징 할 수 있으나, 이건 포스팅 영역을 벗어남) Stream API는 람다 표현식과 함께 사용해서 이 declarative를 완성시켜 코드가 정말 간결해진다.

 

// imperative data processing
List<String> memberAespa = new ArrayList<>();
for(Member member : memberList) {
    if(member.getTeam().equals(Aespa)) {
        memberAespa.add(member.getName());
    }
}

// Streams API
memberList.stream()
    .filter(member -> member.getTeam().equals(Aespa))
    .map(Member::getName)
    .collect(toList());

 

 단점으로는, 제어 흐름 (break, continue, return)을 하지 못한다는 것이 있는데, 그 외 장점으로는 코드의 의도가 명확하고, 가독성이 높아진다는 것이 있다. 또한 이미 Stream API에 구현 (최적화)되어있는 메서드를 사용하기 때문에 코드의 안정성도 올라간다. 


람다와 OOP 디자인 패턴

 

OOP의 대표적인 디자인 패턴

  • Strategy (전략)
  • Template method
  • Observer
  • Chain of reposibility (책임 연쇄)
  • Factory

 

위 패턴들을 구현할 때 람다를 어떻게 유용하게 사용이 가능할지 알아보자

 


패턴 1.  Strategy

runtime에 사용할 알고리즘을 선택할 수 있다는 특징을 가진 패턴이다. 클라이언트는 사용할 전략을 사전에 구현체에 만들어 두고 사용해야 하지만, 함수형 인터페이스를 사용한다면, 미리 정의할 것 없이 사용 시점에 람다를 통해 구현하고, 사용할 수 있게 한다. 

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 v) {
        this.strategy = v;
    }

    public boolean validate(String s) {
        return strategy.execute(s);
    }
}

// client
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaaa"); // false

Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("bbbb"); // true

 

ValidationStrategy를 함수형 인터페이스로 선언하고, 람다를 통해 구현체 IsAllLowerCase, IsNumeric 은 생략된다. 

 

 

 

@FunctionalInterface
public interface ValidationStrategy {
    boolean execute(String s);
}

Validator numericValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b1 = numericValidator.validate("aaaa"); // false

Validator lowerCaseValidator = new Validator((String s) -> s.matches("[a-z]+"));
boolean b2 = lowerCaseValidator.validate("bbbb"); // true

 

패턴 2.  Tempalte method

 클라이언트가 사용할 알고리즘을 전략 패턴보다 더 자유롭게 커스터마이징이 가능해진다. Java는 이미 API에 Consumer와 같은 함수형 인터페이스를 선언해 두었다.

abstract class OnlineBanking {
    public void processCustomer(int customerId) {
        Customer c = Database.getCustomerWithId(customerId);
        makeCustomerHappy(c);
    }

    abstract void makeCustomerHappy(Customer c); // subclass에서 구현하세요~
}

// Consumer를 사용해 subclass 구현 생략
public class OnlineBankingWithConsumer {
    public void processCustomer(int customerId, Consumer<Customer> makeCustomerHappy) {
        Customer c = Database.getCustomerWithId(customerId);
        makeCustomerHappy.accept(c);
    }
}

new OnlineBankingWithConsumer().processCustomer(1337, 
	(Customer c) -> System.out.println("Hello " + c.getName()));

 

패턴 3.  Observer

object (subject)의 event가 발생하면 다른 objects에게 알리는 패턴이다. observer의 세부 내용이 복잡하다면 람다 작성이 힘들겠지만, 간단한 경우 적용해 볼 수 있다. 

 

interface Observer {
    void notify(String tweet);
}

class FanAespa implements Observer {
    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("Aespa")) {
            sendTweet("To. Aespa Fans " + tweet);
        }
    }
}


class FanNewJeans implements Observer {
    @Override
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("NewJeans")) {
            sendTweet("To. NewJeans Fans " + tweet);
        }
    }
}

interface Subject {
    void registerObserver(Observer o);

    void notifyObservers(String tweet);
}

class Feed implements Subject {
    private final 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));
    }
}

@Test
public void testFeed(){

    Feed feed = new Feed();
    feed.registerObserver(new FanAespa());
    feed.registerObserver(new FanNewJeans());
    feed.notifyObservers("Aespa is the best!!");
}

 

 람다를 사용해서 직접 Object 인터페이스를 구현해서 사용할 수 있다. 미리 FanAespa와 같은 구현체를 만들어두지 않아도 된다.

@Test
@DisplayName("람다로 대체")
public void testFeedLamda(){

    Feed feed = new Feed();

    // fansAespa
    feed.registerObserver((String tweet) -> {
        if (tweet != null && tweet.contains("Aespa")) {
            sendTweet("To. Aespa Fans " + tweet);
        }
    });

    // fansNewJeans
    feed.registerObserver((String tweet) -> {
        if (tweet != null && tweet.contains("NewJeans")) {
            sendTweet("To. NewJeans Fans " + tweet);
        }
    });

    feed.notifyObservers("Aespa is the best!!");
}

 

패턴 4. Chain of responsibility

 추상 클래스로 선언하여 후임자에게 추상 메서드를 구현하여 사용하도록 하는 패턴이다. 

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);
}

public class HeaderTextProcessing extends ProcessingObject<String> {
    @Override
    protected String handleWork(String text) {
        return "그룹명은 소문자에서 대문자로 변환됩니다. \n" + text;
    }
}

public class ToUpperCaseProcessing extends ProcessingObject<String> {
    @Override
    protected String handleWork(String text) {
        return text.replaceAll("aespa", "AESPA");
    }
}


@Test
@DisplayName("Chain of Responsibility")
public void tst() {
    ProcessingObject<String> headerTextProcessing = new HeaderTextProcessing();
    ProcessingObject<String> toUpperCaseProcessing = new ToUpperCaseProcessing();
    headerTextProcessing.setSuccessor(toUpperCaseProcessing);
    String result = headerTextProcessing.handle("aespa is the best!");
    System.out.println(result);
}

 

 마찬가지로 람다로 아래와 같이 대체가 가능하다. 함수형 인터페이스 java.util.function.UnaryOperator, java.util.function.Function을 사용했다.

 

@Test
@DisplayName("Chain of Responsibility lamda")
public void tstLamda(){
    UnaryOperator<String> headerProcessing = (String text) -> 
    		"그룹명은 소문자에서 대문자로 변환됩니다. \n" + text;
    UnaryOperator<String> toUpperCaseProcessing = (String text) -> 
    		text.replaceAll("aespa", "AESPA");
            
    Function<String, String> pipeline = headerProcessing.andThen(toUpperCaseProcessing);
    String result = pipeline.apply("aespa is the best!");
    
    System.out.println(result);
}

 

 

패턴 5. Factory

public static Idol getIdol(String name) {
    switch (name) {
        case "Aespa":
            return new Aespa();
        case "NewJeans":
            return new NewJeans();
        default:
            throw new RuntimeException("No such idol " + name);
    }
}

static class Idol {
    public String getName() {
        return "Idol";
    }
}

static class Aespa extends Idol {
    public String getName() {
        return "Aespa";
    }
}

static class NewJeans extends Idol {
    public String getName() {
        return "NewJeans";
    }
}

@Test
@DisplayName("팩토리로 객체 생성")
public void testFactory() {
    Idol idol = Fact.getIdol("Aespa");
    System.out.println(idol.getName());
}

@Test
@DisplayName("팩토리로 객체 생성 (람다)")
public void testFactoryLamda() {
    Supplier<Idol> newJeans = () -> new NewJeans(); // lamda
    Supplier<Idol> aespa = Aespa::new; // method reference
}

참고 (예제 코드)

https://github.com/gihyeon6394/modern-java-in-action/tree/master

 

GitHub - gihyeon6394/modern-java-in-action: book : Modern Java IN ACTION

book : Modern Java IN ACTION. Contribute to gihyeon6394/modern-java-in-action development by creating an account on GitHub.

github.com

https://product.kyobobook.co.kr/detail/S000001810171

 

모던 자바 인 액션 | 라울-게이브리얼 우르마 - 교보문고

모던 자바 인 액션 |

product.kyobobook.co.kr

 

댓글