티스토리 뷰

JAVA/JAVA기본

JAVA - 람다식이란?

realizers 2022. 2. 10. 20:22
728x90
반응형

람다 표현식(Lambda Expressions)


  • 람다식이란 메서드를 하나의 식으로 표현한 것입니다.
  • 람다식으로 표현하면 return이 없어지므로 람다식을 익명 함수라고도 합니다.
람다식의 장점
  • 코드가 간결해집니다.
  • 가독성이 향상됩니다.
  • 멀티 쓰레드 환경에서 용이합니다.
  • 함수를 만드는 과정없이 한번에 처리하기에 생산성이 높아집니다.
람다식의 단점
  • 람다로 인한 무명 함수는 재사용이 불가능합니다.
  • 디버깅이 까다롭습니다.
  • 람다를 무분별하게 사용하게 되면 코드가 지져분해 집니다.
  • 재귀로 만드는 경우 부적합해집니다.

 

람다를 사용하지 않은 예제


interface MyFunction {
    void init();
}

public class Example {
    public static void main(String[] args) {
        MyFunction myFunction = new MyFunction() {
            @Override
            public void init() {
                System.out.println("나의 함수 초기화");
            }
        };

        myFunction.init(); // 나의 함수 초기화
    }
}

람다를 사용한 예제


  • 확실히 코드가 간결해진것을 확인할 수 있습니다.
interface MyFunction {
    void init();
}

public class Example {
    public static void main(String[] args) {
        MyFunction myFunction = () -> {
            System.out.println("나의 함수 초기화");
        };

        myFunction.init();
    }
}

 

함수형 인터페이스


  • 하나의 추상 메서드를 가지고 있는 인터페이스@FunctionalInterface 어노테이션이 작성된 인터페이스를 말합니다.
  • 추상 메서드가 하나라는 의미는 default 메서드나 static 메서드는 여러개 존재해도 상관없다는 의미입니다.
  • @FunctionalInterface은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사를 해줍니다. 해당 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는데 문제는 없지만 인터페이스 검증과 유지보수를 위하여 선언하는게 좋습니다.

 

함수형 인터페이스 생성 방법


  • 위에서 언급한거처럼 추상 메서드는 하나여야하고 2개 이상을 선언하는 경우 컴파일 에러가 발생하게 됩니다.
  • default 메서드나 static 메서드는 여러개를 선언해도 괜찮습니다.
@FunctionalInterface
public interface UserInterface<T> {

    T init();

    default void defaultMethod(){
        System.out.println("This is default Method");
    }

    static void staticMethod(){
        System.out.println("This is static Method");
    }
}

public class Example {
    public static void main(String[] args) {
        UserInterface<String> userInterface = () -> "Hello User";

        String text = userInterface.init();
        // output -> Hello User
        System.out.println(text);

        // output -> This is default Method
        userInterface.defaultMethod();

        // output -> This is static Method
        UserInterface.staticMethod();
    }
}

 

자바에서 제공하는 함수형 인터페이스


Suppliers
  • Suppliers<T>는 인자를 받지 않고 T 타입의 객체를 반환합니다.
@FunctionalInterface
public interface Supplier<T> {

    T get();
}
예제
class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Example {
    public static void main(String[] args) {
        User user = new User("홍길동", 25);
        Supplier<User> supplier = () -> user;
        User getSupplier = supplier.get();

        // User{name='홍길동', age=25}
        System.out.println(getSupplier);
    }
}

 

Consumer
  • Consumer<T>는 T 타입의 객체를 인자로 받고 반환하는 값은 없습니다.
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}
예제
class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Example {
    public static void main(String[] args) {
        User users = new User("홍길동", 25);

        Consumer<User> consumer = (user) -> {

            // User{name='홍길동', age=25}
            System.out.println(user);
        };
        consumer.accept(users);
    }
}

 

Function
  • Function<T, R>은 T타입의 인자를 받아 R 타입의 객체로 반환합니다.
@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}
예제
public class Example {
    public static void main(String[] args) {
        Function<Integer, Integer> add = (value) -> value + value;
        Integer result = add.apply(10);

        // 20
        System.out.println(result);
    }
}

 

Predicate
  • Predicate<T>는 T타입의 인자를 받고 결과를 boolean으로 반환합니다.
@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

    @SuppressWarnings("unchecked")
    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>)target.negate();
    }
}
예제
public class Example {
    public static void main(String[] args) {
        Predicate<Integer> isSmallerThan = num -> num < 10;
        System.out.println(isSmallerThan.test(5)); // true

        Predicate<String> isEqual = Predicate.isEqual("Hello");
        System.out.println(isEqual.test("Hello")); // true
    }
}

 

Variable Capture


  • 람다의 Body에서 인자로 넘어온 것 이외의 변수에 접근하는 것을 Variable Capture라고 합니다.
  • 람다식의 실행 블록 내부에서 인스턴스 변수, 스태틱 변수, 지역 변수에 접근이 가능합니다. 하지만 지역 변수에 접근할 때는 Variable Capture라는 특별한 작업이 수행되기 때문에 한가지 제약 사항이 발생합니다. 밑에 있는 예시를 보면서 살펴보겠습니다.
지역 변수의 값 변경 전
  • 지역 변수인 loalVariable등 User의 스태틱 변수, 지역 변수에 접근이 가능한 것을 볼 수 있습니다.
@FunctionalInterface
interface UserInterface {

    void init();
}

class User {
    static String name = "홍길동";
    int age;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Example {
    public static void main(String[] args) {
        User users = new User();
        users.age = 20;

        int localVariable = 40;
        
        UserInterface userInterface = () -> {
            System.out.println("static variable = " + User.name);
            System.out.println("instance variable = " + users.age);
            System.out.println("local variable = " + localVariable);
        };

        userInterface.init();
    }
}

 

지역 변수의 값 변경 후
  • localVariable이라는 변수에 20을 할당 후 100이라는 값을 재할당하게 되면 컴파일 오류가 발생하게 됩니다.
  • 오류가 발생하는 이유는 클래스 내부에 선언된 람다식이 지역 변수를 참조할 때는 그 값을 복사해서 사용하기 때문입니다. 이를 Variable Capture라고 하는데 쉽게 말하자면 Variable Capture란 객체 외부에 선언된 변수를 객체 내부로 복사하는 행위입니다. 지역 변수 뿐만 아니라 파라미터로 전달된 변수 또한 외부에서 선언된 변수이므로 해당 규칙이 적용됩니다.
@FunctionalInterface
interface UserInterface {

    void init();
}

class User {
    static String name = "홍길동";
    int age;

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Example {
    public static void main(String[] args) {
        User users = new User();
        users.age = 20;

        int localVariable = 40;

        // 값 변경
        User.name = "이순신";
        users.age = 50;
        localVariable = 100;

        UserInterface userInterface = () -> {
            System.out.println("static variable = " + User.name);
            System.out.println("instance variable = " + users.age);
            System.out.println("local variable = " + localVariable); // 컴파일 오류 발생
        };

        userInterface.init();
    }
}

 

void setParam(int i){
    i = 10;

    UserInterface userInterface = () -> {
        System.out.println("local variable = " + i); // 컴파일 오류 발생
    };
}

 

 

메서드, 생성자 레퍼런스


  • 위에서는 람다식을 사용하여 간결하게 코드를 작성했는데 람다식보다 더 간결하게 표현하는 방법이 있습니다. 이를 레퍼런스라고 부르는데 사용방법은 아래와 같습니다.
Static Method Reference
  • example1은 람다식을 사용하여 작성한 것이고 example2는 메서드 레퍼런스를 사용하여 작성을 하였습니다.
interface Eatable {

    void eating(String eat);
}

class User {

    static void somethingEat(String eat){
        System.out.println(eat);
    }
}

public class Example {
    public static void main(String[] args) {
        Eatable example1 = eat -> {
            User.somethingEat(eat);
        };
        
        Eatable example2 = User::somethingEat;
        
        example1.eating("딸기");
        example2.eating("복숭아");
    }
}

 

  • Consumer를 사용하여 더욱 간단한 예제도 만들 수 있습니다.
class User {

    static void somethingEat(String eat){
        System.out.println(eat);
    }
}

public class Example {
    public static void main(String[] args) {
        Consumer<String> consumer = User::somethingEat;
        consumer.accept("딸기 냠냠");
    }
}

 

Instance Method Reference
  • Instance Method Reference의 메서드는 static이 아니고 객체의 메서드를 의미합니다.
class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void myInfo(){
        System.out.println("Name : " + name + ", Age : " + age);
    }
}

public class Example {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(new User("홍길동", 25),
                                         new User("이순신", 30),
                                         new User("유관순", 27));

        // 람다식을 사용한 예제
        users.stream().forEach(user -> {
            user.myInfo();
        });

        // 메서드 레퍼런스를 사용한 예제
        users.stream().forEach(User::myInfo);
    }
}

// 실행 결과
// Name : 홍길동, Age : 25
// Name : 이순신, Age : 30
// Name : 유관순, Age : 27

 

Constructor Method Reference
  • Constructor Method Reference는 생성자를 생성해주는 코드입니다.
class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public void myInfo(){
        System.out.println("Name : " + name);
    }
}

public class Example {
    public static void main(String[] args) {
        List<String> users = Arrays.asList("홍길동", "이순신", "유관순");

        // 람다식을 사용한 예제
        users.stream()
                .map(name -> new User(name))
                .forEach(user -> {
                    user.myInfo();
                });

        // 생성자 메서드 레퍼런스를 사용한 예제 
        users.stream()
                .map(User::new)
                .forEach(user -> {
                    user.myInfo();
                });


    }
}

// 실행 결과
// Name : 홍길동
// Name : 이순신
// Name : 유관순
728x90
반응형