자바-15(람다식)

dragonappear·2021년 3월 28일
0

Java

목록 보기
15/22

목표

자바의 람다식에 대해 학습하세요.


학습할 것 (필수)

  1. 람다식 사용법
  2. 함수형 인터페이스
  3. Variable Capture
  4. 메소드, 생성자 레퍼런스

1. 람다식 사용법

람다식이란?

식별자없이 실행가능한 함수

  • 메서드를 하나의 식으로 표현하는 것이라고 볼 수 있다.
  • 람다식으로 표현하면 return이 없어지므로 람다식을 anonumous function(익명함수)라고도 한다.

람다식의 장단점

장점:

  • 코드를 간결하게 만들 수 있다.
  • 가독성이 향상된다.
  • 멀티쓰레드환경에서 용이하다.
  • 함수를 만드는 과정없이 한번에 처리하기에 생산성이 높아진다

단점:

  • 람다로 인한 무명함수는 재사용이 불가능하다
  • 디버깅이 많이 까다롭다
  • 람다를 무분별하게 사용하면 코드가 클린하지 못하다
  • 재귀로 만들 경우 부적합하다

람다식 사용법

(매개변수)-> 표현바디
(매개변수)-> {표현바디}
()-> {표현바디}
()-> 표현바디
  • 매개변수의 타입은 추론이 가능한 경우 생략할 수 있는데, 대부분의 경우 생략이 가능하다.
  • 반환값이 있는 메서드의 경우 return 문 대신 '식'으로 대신할수있다.
  • 문장이 아닌 '식' 이므로 끝에 세미콜론(;)을 붙이지 않는다.
  • 매개변수가 하나일 때는 괄호()를 생략할 수 있다.
  • 단, 매개변수 타입이 있으면 괄호()를 생략할 수 없다.
  • 괄호{}안의 문장이 하나일 떄는 괄호를 생략할 수 있다.
  • 괄호{}안의 문장이 return문일 경우 괄호를 생략할 수 없다.

기본예제

Setting

package me.whiteship.livestudy.week15;

@FunctionalInterface
public interface Setting {
    void setUp();
}

LeagueOfLegend

package me.whiteship.livestudy.week15;

public class LeagueOfLegend {
    public void running(Setting setting){
        setting.setUp();
        System.out.println("LeagueOfLegend running");
    }
}

LambdaSample

package me.whiteship.livestudy.week15;

public class LambdaSample {

    public static void main(String[] args) {
        LeagueOfLegend leagueOfLegend = new LeagueOfLegend();
        leagueOfLegend.running(new Setting() {
            @Override
            public void setUp() {
                System.out.println("leageOfLegend is setup");
            }
        });
    }
}

output

leageOfLegend is setup
LeagueOfLegend running

Process finished with exit code 0

람다예제

lambdaSample

package me.whiteship.livestudy.week15;

public class LambdaSample {

    public static void main(String[] args) {
        LeagueOfLegend leagueOfLegend = new LeagueOfLegend();
        leagueOfLegend.running(()-> System.out.println("leagueOfLegend is setup"));
    }
}

output

leageOfLegend is setup
LeagueOfLegend running

Process finished with exit code 0

@FunctionalInterface

  • 자바8 이전에서는 자바에서 값이나 객체가 아닌 하나의 함수를 변수에 담아두는 것은 허용되지 않는다.
  • 자바8 이후부터 람다식이 추가되고 하나의 변수에 하나의 함사룰 매핑할 수 있다.

예제

package me.whiteship.livestudy.week15;

@FunctionalInterface
public interface Functional {
    int calc(int a,int b);
}
  • 1개의 메소드를 가진 인터페이스를 Functional Interface이다. Single Abstract Method(SAM)이라고 불리기도 한다.
  • @FunctionalInterface를 지정하면 이 인터페이스가 함수형 인터페이스라고 명시해주고 컴파일러가 SAM 여부를 체크할 수 있도록 한다.

사용예제2

various 사용

Functional add = (int a,int b)->{return a+b;};
Functional add1 = (int a,int b)->a+b;
Functional add2 = Integer::sum;
  • 이 예제의 모든 결과는 같다
package me.whiteship.livestudy.week15;

public class App {
    public static void main(String[] args) {
        Functional add = (int a,int b)->{return a+b;};
        Functional add1 = (int a,int b)->a+b;
        Functional add2 = Integer::sum;

        int result = add.calc(1,1);
        int result1 = add.calc(1,1);
        int result2 = add.calc(1,1);

        System.out.println(result+ "," +result1+"," +result2);

    }
}

output

2,2,2

바이트코드

  • 기본예제와 람다예제로 작성하였을시 결과는 동일한데 어떻게 실행되고 바이트코드가 동일한지 찾아보았다.
    1
    기본예제 바이트코드
// class version 52.0 (52)
// access flags 0x21
public class me/whiteship/livestudy/week15/LambdaSample {

  // compiled from: LambdaSample.java
  // access flags 0x8
  static INNERCLASS me/whiteship/livestudy/week15/LambdaSample$1 null null

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lme/whiteship/livestudy/week15/LambdaSample; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    NEW me/whiteship/livestudy/week15/LeagueOfLegend
    DUP
    INVOKESPECIAL me/whiteship/livestudy/week15/LeagueOfLegend.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 8 L1
    ALOAD 1
    NEW me/whiteship/livestudy/week15/LambdaSample$1
    DUP
    INVOKESPECIAL me/whiteship/livestudy/week15/LambdaSample$1.<init> ()V
    INVOKEVIRTUAL me/whiteship/livestudy/week15/LeagueOfLegend.running (Lme/whiteship/livestudy/week15/Setting;)V
   L2
    LINENUMBER 15 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE leagueOfLegend Lme/whiteship/livestudy/week15/LeagueOfLegend; L1 L3 1
    MAXSTACK = 3
    MAXLOCALS = 2
}
  • 기본예제에서 익명클래스인 lambdaSample$1 새로운 클래스를 생성하여 초기화를 해주고 Setting 인터페이스를 실행하는것 같다.
  • 익명클래스는 INVOKESPECIAL이란 OPCODE로 생성자를 호출하고, INVOKEVIRTUAL로 setting을 호출한다.
  • 자바에서는 왜 람다를 내부적으로 익명클래스로 컴파일하지 않을까?
  • 자바8 이전 버전에서 람다를 쓰기위한 retrolambda 같은 라이브러리, kotlin 같은 언어에서는 컴파일 시점에 람다를 단순히 익명클래스로 치환이 된다.
  • 다만, 익명 클래스로 사용할 경우 아래와 같은 문제가 발생할 수 있다.
    • 항상 새 인스턴스로 할당한다.
    • 람다식마다 클래스가 하나씩 생기게된다.

익명 객체를 람다식으로 대체가 가능하다.

람다식의 매개변수 타입과 개수 그리고 반환값이 익명 객체와 일치하기 때문이다.

람다예제 바이트코드

// class version 52.0 (52)
// access flags 0x21
public class me/whiteship/livestudy/week15/LambdaSample {

  // compiled from: LambdaSample.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lme/whiteship/livestudy/week15/LambdaSample; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    NEW me/whiteship/livestudy/week15/LeagueOfLegend
    DUP
    INVOKESPECIAL me/whiteship/livestudy/week15/LeagueOfLegend.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 7 L1
    ALOAD 1
    INVOKEDYNAMIC setUp()Lme/whiteship/livestudy/week15/Setting; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      ()V, 
      // handle kind 0x6 : INVOKESTATIC
      me/whiteship/livestudy/week15/LambdaSample.lambda$main$0()V, 
      ()V
    ]
    INVOKEVIRTUAL me/whiteship/livestudy/week15/LeagueOfLegend.running (Lme/whiteship/livestudy/week15/Setting;)V
   L2
    LINENUMBER 8 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE leagueOfLegend Lme/whiteship/livestudy/week15/LeagueOfLegend; L1 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2

// access flags 0x100A
  private static synthetic lambda$main$0()V
   L0
    LINENUMBER 7 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "leagueOfLegend is setup"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
}
  • 람다예제의 바이트 코드를 보면 새로운 메서드를 static으로 생성해서 메서드를 실행시키는것 같다.
  • 중간쯤에 INVOKEDYNAMIC CALL이 나오는데, INVOKEDYNAMIC CALL가 호출되면 bootstrap 영역의 lambdafactory.metafactory()를 수행하게 된다.
    • lambdafactory.metafactory(): Java Runtime Library의 표준화 method
    • 어떤 방법으로 객체를 생성할지 dynamically를 결정한다.
    • 클래스를 새로 생성, 재사용, 프록시, 래퍼클래스 등등 성능향상을 위한 최적화된 방법을 사용한다.
  • java.lang.invoke.CallSite 객체를 return 한다.
    • LambdaMetafactory~ 부분의 끝에 CallSite 객체를 리턴하게 된다.
    • 해당 lambda의 lambda factory, MethodHandle을 멤버변수로 가지게 된다.
    • 람다가 변환되는 함수 인터페이스의 인스턴스를 반환한다.
    • 한번만 생성되고 재호출시 재사용이 가능하다.

2. 함수형 인터페이스

함수형 인터페이스(Functional Interface)란?

  • 1개의 추상 메서드를 가지고 있는 인터페이스이다.

함수형 인터페이스를 왜 사용하지?

  • 자바의 람다식은 함수형 인터페이스로만 접근이 가능하기 때문이다.
  • 위의 예제를 보면
@FunctionalInterface
public interface Functional {
    int calc(int a,int b);
}

Functional func = (a,b) -> {return a+b};
System.out.println(func.calc(1,2));
// 결과
// f3

이렇게 사용이 가능하다

기본 함수형 인터페이스

오라클링크

  • Runnable
  • Supplier
  • Consumer
  • Funcion<T,R>
  • Predicate

1. Runnable:

  • 인자를 받지 않고 리턴값도 없는 인터페이스이다.
  • 앞에서 쓰레드를 공부할때 Runnable 인터페이스를 실행했던것이라고 보면 된다.

Runnable

Runnable runnable = () -> System.out.println("runnable run");
runnable.run();

output

runnable run
  • Runnable은 Run()을 호출해야 한다. 함수형 인터페이스마다 run()과 같은 실행 메서드 이름이 다르다.

  • 인터페이스 종류마다 만들어진 목적이 다르고, 인터페이스 별 목적에 맞는 실행 메서드 이름을 정하기 때문이다.

2. Suppliers

  • Supplier<T>은 인자를받지않고 T타입의 객체를 리턴한다.

Supplier

@FunctionalInterface
public interface Supplier<T> {
   T get();
}

SupplierSample

package me.whiteship.livestudy.week15;

import java.util.function.Supplier;

public class SupplierSample {
    public static void main(String[] args) {
        Supplier<String> supplier = ()->"Supplier Sample";
        System.out.println(supplier.get());
    }
}

andThen() 메서드와 compose() 디폴트 메서드

참고링크

  • 디폴트 메서드와 static 메서드는 함수적 인터페이스에 선언되더라도 함수적 인터페이스는 그 성질을 유지한다.
  • java.util.Funtion 패키지의 함수적 인터페이스는 하나 이상의 디폴트 및 static 메서드를 가지고 있다.
  • Consumer,Function,Operator 종류의 함수적 인터페이스는 andThen()과 compose() 디폴트 메서드를 가지고 있다.
  • andThen()과 compose() 디폴트 메서드는 두 개의 함수적 인터페이스를 순차적으로 연결하고 첫 번째 처리 결과를 두번째 매개값으로 제공해서 최종결과값을 얻을때 사용한다.
  • andThen() 고 compose()의 차이점은 어떤 함수적 인터페이스부터 처리하느냐이다.
  1. andThen():

인터페이스AB = 인터페이스A.andThen(인터페이스B);
최공결과 인터페이스 AB.method();

  • 인터페이스 AB의 method()를 호출하면 우선 인터페이스 A부터 처리하고 결과를 인터페이스 B의 매개값으로 제공한다.

  • 인터페이스 B는 제공받은 매개값을 가지고 처리한 후 최종결과를 리턴.

  1. compose()

인터페이스AB의 method()를 호출하면 먼저 인터페이스B로부터 처리하고 결과를 인터페이스 A의 매개값으로 제공한다. 인터페이스 A는 제공받은 매개값을 가지고 최종 처리한후 결과를 리턴.

3. Consumer

  • Consumer<T>는 T타입의 객체를 인자로 받고 리턴 값은 없다.

Consumer

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

ConsumerSample

package me.whiteship.livestudy.week15;

import java.util.function.Consumer;

public class ConsumerSample {
    public static void main(String[] args) {
        Consumer<String> print = str -> System.out.println("This is " + str + " interface");
        print.accept("Consumer");
    }
}

output

This is Consumer interface

andThen()을 사용하면 두 개 이상의 Consumer를 사용할수있다.

package me.whiteship.livestudy.week15;

import java.util.function.Consumer;

public class ConsumerSample {
    public static void main(String[] args) {
        Consumer<String> print = str -> System.out.println("This is " + str + " interface");
        Consumer<String> print1 = str -> System.out.println("ok");
        print.andThen(print1).accept("Consumer");

    }
}

output

This is Consumer interface
ok

4.Function

  • Function<T,R> 은 T 타입의 인자를 받아, R 타입의 객체로 리턴한다.

Function

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

    /**
     * Returns a composed function that first applies the {@code before}
     * function to its input, and then applies this function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of input to the {@code before} function, and to the
     *           composed function
     * @param before the function to apply before this function is applied
     * @return a composed function that first applies the {@code before}
     * function and then applies this function
     * @throws NullPointerException if before is null
     *
     * @see #andThen(Function)
     */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     *
     * @see #compose(Function)
     */
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    /**
     * Returns a function that always returns its input argument.
     *
     * @param <T> the type of the input and output objects to the function
     * @return a function that always returns its input argument
     */
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

FunctionSample1

package me.whiteship.livestudy.week15;

import java.util.function.Function;

public class FunctionSample {

    public static void main(String[] args) {
        Function<Integer,Integer> add = (value)->value+value;
        System.out.println(add.apply(5));
    }
}

FunctionSample2

package me.whiteship.livestudy.week15;

import java.util.function.Function;

public class FunctionSample {

    public static void main(String[] args) {
        Function<Integer,Integer> f1 = (value)->value-2;
        Function<Integer,Integer> f2 = (value)->value+5;

        Function<Integer,Integer> addAndSub = f2.compose(f1);
        Integer result = addAndSub.apply(10);

        System.out.println(result);
    }
}

output

13

순차적 연결

  • Function과 Operator 종류의 함수적 인터페이스는 먼저 실행한 함수적 인터페이스의 결과를 다음 함수적 인터페이스의 매개값으로 넘겨주고 최종 처리결과를 리턴한다.

  • 예를 들어서 Function<Member,Address>와 Function<Address,String>을 순차적으로 연결해서 Function<Member,String>을 생성한다고 하면, Function<Address,String>은 매개값으로 제공되는 Address로 부터 String을 리턴한다. 이 둘을 andThen() 이나 compose()로 연결하면
    Function<Member,Address>에서 리턴한 Address를 Function<Address,String>의 매개값으로 넘겨서 최종 String 타입을 리턴하는 Function<Address,String>을 생성해낸다.

  • Address는 두 함수적 인터페이스 간의 전달 데이터이다 Address는 내부적으로 전달되기 때문에 최종 함수적 인터페이스의 형태는 입력 데이터가 Member, 출력데이터가 String이 되는Function<Member,String>이 된다.

Address

package me.whiteship.livestudy.week15.Sequence;

public class Address {
    private String country;
    private String city;

    public Address(String country, String city) {
        this.country = country;
        this.city = city;
    }

    public String getCountry() {
        return country;
    }

    public String getCity() {
        return city;
    }
}

Member

package me.whiteship.livestudy.week15.Sequence;

public class Member {
    private String name;
    private String id;
    private Address address;

    public Member(String name, String id, Address address) {
        this.name = name;
        this.id = id;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public Address getAddress() {
        return address;
    }
}

FunctionMethodExample

package me.whiteship.livestudy.week15.Sequence;

import java.util.function.Function;

public class FunctionMethodExample {
    public static void main(String[] args) {


        Function<Member,Address> f1 = (m) -> m.getAddress();
        Function<Address,String> f2 = (a) -> a.getCity();

        // 1. andThen
        Function<Member,String> f3 = f1.andThen(f2);
        String city = f3.apply(new Member("yyh","5",new Address("Korea","Incheon")));
        System.out.println(city);

        // 2. compose
        Function<Member,String> f4 = f2.compose(f1);
        city = f4.apply(new Member("yyh","5",new Address("USA","New York")));
        System.out.println(city);

    }
}

output

Incheon
New York

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

PredicateExample

package me.whiteship.livestudy.week15;

import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        Predicate<Integer>  isSmallerThan = num -> num<10;
        System.out.println(isSmallerThan.test(5));
    }
}

output

true

another example

package me.whiteship.livestudy.week15;

import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        Predicate<Integer>  isBiggerThan = num -> num>20;
        Predicate<Integer>  isSmallerThan = num -> num<10;
        System.out.println(isBiggerThan.and(isSmallerThan).test(5));
        System.out.println(isBiggerThan.or(isSmallerThan).test(5));

    }
}

output

false
true

another example

  • isEqual()는 static 메서드이고 인자로 전달되는 객체와 같은지 체크하여 객체를 만들어준다.
package me.whiteship.livestudy.week15;

import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        Predicate<String> isEqual = Predicate.isEqual("TheWing");
        System.out.println(isEqual.test("TheWing"));
    }
}

output

true

요약

package me.whiteship.livestudy.week15.useInterface;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class ex1 {
    public static void main(String[] args) {
        Supplier<Integer> s = () -> (int)(Math.random() * 100) + 1;
        Consumer<Integer> c = i -> System.out.print(i + ", ");
        Predicate<Integer> p = i -> i % 2 == 0;
        Function<Integer, Integer> f = i -> i / 10 * 10;

        List<Integer> list = new ArrayList<>();
        makeRandomList(s, list);
        System.out.println(list);
        printEvenNum(p, c, list);
        List<Integer> newList = doSomething(f, list);
        System.out.println(newList);
    }

    static <T> List<T> doSomething(Function<T, T> f, List<T> list) {
        List<T> newList = new ArrayList<>(list.size());

        for (T i : list) {
            newList.add(f.apply(i));
        }

        return newList;
    }

    static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
        System.out.print("[");
        for (T i : list) {
            if (p.test(i)) {
                c.accept(i);
            }
        }
        System.out.println("]");
    }

    static <T> void makeRandomList(Supplier<T> s, List<T> list) {
        for (int i = 0; i < 10; i++) {
            list.add(s.get());
        }
    }
}

output

[6, 88, 93, 57, 62, 10, 89, 54, 94, 63]
[6, 88, 62, 10, 54, 94, ]
[0, 80, 90, 50, 60, 10, 80, 50, 90, 60]

Process finished with exit code 0

매개변수가 두 개인 함수형 인터페이스:

함수형 인터페이스메서드설명
BiConsumer<T,U>void accept(T t , U u)두개의 매개변수,반환값은 없음
BiFunction<T,U,R>R apply(T t , U u)두 개의 매개변수를 받아 결과를 반환
BiPredicateboolean test(T t , U u)조건식을 표현하고 boolean을 반환

UnaryOperator, BinaryOperator:

함수형 인터페이스메서드설명
UnaryOperatorT apply(T t)Function의 자손, Function과 달리 매개변수와 결과타입 같음.
BiryOperatorT apple (T t , T t )BiFunction의 자손. BiFunction과 달리 매개변수와 결과 타입같음.

기본형을 사용하는 함수형 인터페이스

  • 위에서 언급한 함수형 인터페이스는 매개변수화 반환값이 모두 제네릭 타입이었다.
  • 그래서 기본형 타입의 값을 처리할때도 wrapper 클래스를 사용했는데, 이것은 비효율적이다
  • 보다 효유적인 처리를 위해 기본형을 사용하는 함수형 인터페이스도 제공된다.
함수형 인터페이스메서드설명
DoubleToIntFunctionint applyAsInt(double value)AtoB Function 은 A 타입 출력이 B 타입
ToIntFunctionint applyAsInt(T value)ToBFunction은 출력이 B타입, 입력이 제네릭 타입
IntFunctionT apply (T t , U u)AFunction은 입력이 A타입, 출력은 제네릭
ObjIntConsumervoid accept(T t , U u )ObjAFunction은 입력이 T, A 타입이고 출력은 없음
package me.whiteship.livestudy.week15.useInterface;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.*;

public class Ex2 {
    public static void main(String[] args) {
        IntSupplier s = ()->(int)(Math.random() * 100) + 1;
        IntConsumer c=  i -> System.out.print( i + " ");
        IntPredicate p = i -> (i%2) == 0;
        IntUnaryOperator op = i -> i/10 * 10;

        int [] arr = new int[10];
        makeRandomList(s,arr);
        System.out.println(Arrays.toString(arr));
        printEvenNum(p,c,arr);
        System.out.println();

        int [] newArr = doSomething(op,arr);
        System.out.println(Arrays.toString(newArr));


    }

    static int[] doSomething(IntUnaryOperator f,  int[] list) {
        // List<T> newList = new ArrayList<>(list.size());
        int[] newList = new int[list.length];
        for (int i = 0; i < list.length; i++) {
            newList[i] = f.applyAsInt(list[i]);
        }
        return newList;
    }

    static void printEvenNum(IntPredicate p, IntConsumer c, int[] list) {
        for (int i : list) {
            if(p.test(i)){
                c.accept(i);
            }
        }
    }

    static void makeRandomList(IntSupplier s, int[] list) {
        for (int i = 0; i < list.length; i++) {
           list[i] = s.getAsInt();

        }
    }
}

output

[30, 73, 4, 40, 70, 85, 9, 4, 57, 81]
30 4 40 70 4 
[30, 70, 0, 40, 70, 80, 0, 0, 50, 80]

Process finished with exit code 0

3. Variable Capture

  • 람다식의 실행 코드 블록 내에서 클래스의 필드, 메서드 그리고 지역변수를 사용할 수 있다.
  • 클래스의 필드와 메서드는 특별한 제약 없이 사용가능하지만, 지역변수를 사용함에 있어서는 제약이 존재한다.
  • 잠시 람다식이 아닌 다른 얘기를 해보자.
  • 멤버 메서드 내부에서 클래스의 객체를 생성해서 사용할 경우 다음과 같은 문제가 있다.
  • 익명 구현 객체를 포함해서 객체를 생성할 경우 NEW라는 키워드를 사용한다.
  • 이 키워드를 사용한다는 것은 동적 메모리 할당 영역(HEAP)에 객체를 생성한다는 것을 의미한다.
  • 이렇게 생성된 객체는 자신을 감싸고있는 멤버 메서드의 실행이 끝난 이후에도 HEAP영역에 존재하므로 사용할 수 있지만, 이 멤버 메서드에 정의된 매개변수나 지역변수는 런타임 스택영역(이하 STACK)에 할당되어 메서드 실행이 끝나면 해당 영역에서 사라져 더 이상 사용할 수 없게 된다.
  • 그렇기 때문에 멤버 메서드 내부에서 생성된 객체가 자신을 감싸고 있는 메서드의 매개변수나 지역변수를 사용하려 할때 문제가 생길수있다.

조금더 쉽게 설명하면

클래스의 멤버 메서드의 매개변수와 이 메서드 실행 블록 내부의 지역변수는 JVM의 stack에 생성되고 메서드 실행이 끝나면 stack에서 사라진다.

new 연산자를 사용해서 생성한 객체는 JVM의 HEAP 영역에 객체가 생성되고 GC(garbage collector)에 의해 관리되며, 더이상 사용하지 않는 객체에 대해 필요한 경우 메모리에서 제거한다.

heap에 생성된 객체가 stack의 변수를 사용하려고 하는데, 사용하려는 시점에 stack에 더이상 해당 변수가 존재하지 않을 수 있다. 왜냐하면 stack은 메서드 실행 이 끝나면 매개변수나 지역 변수를 제거하기 때문이다. 그래서 더 이상 존재하지 않는 변수를 사용하려고 하기에 오류가 발생한다.

Local Variable Capture:

자바는 위 문제를 Variable Capture 라고 하는 값 복사를 사용해서 해결하고 있다.

  • 즉, 컴파일 시점에 멤버 메서드의 매개변수나 지역 변수를 멤버 메서드 내부에서 생성한 객체가 사용할 경우 객체 내부로 값을 복사해서 사용한다.
  • 하지만, 모든 값을 복사해서 사용할 수 있는 것은 아니다.
  • 여기에도 제약이 존재하는데 final 키워드로 작성되었거나 effective final 이여야한다.
  • effective final 은 final 키워드로 선언된것은 아니지만 값이 초기화된 이후 값이 한번도 변경되지 않아서 final 처럼 쓰이는 것을 뜻한다.
    • final 키워드를 사용하지 않은 변수를 익명 클래스 구현체 또는 람다에서 참조할수있다.
  • 익명 클래스 구현체와 달리 쉐도윙 하지 않는다.
    • 익명 클래스는 새로 scope을 만들지만, 람다는 람다를 감싸고있는 scope와 같다.

즉, 람다에서 Local variable은

  • final이거나 effective final 인 경우에만 참조할수있다.
    • 변수를 변경되도록 수정해보면 이것은 effective final인지 아닌지 확인가능하다.
    • 변경이 되면 effective final이 아니게 되며, 람다에서 사용할수없게 된다.
  • 그렇지 않을 경우 concurrency문제가 생길 수 있어서 컴파일이 불가능하다

예제코드

package me.whiteship.livestudy.week15.VariableCapture;

import java.util.function.Consumer;
import java.util.function.IntConsumer;
import java.util.function.Supplier;

public class Foo {

    public static void main(String[] args) {
        Foo foo = new Foo();
        foo.run();
    }

    private void run() {
        //  참조는 할수있다.
        //  java8부터는 final 키워드를 생략할수있는 케이스가 있다.
        //  이 변수가 사실상 final인 경우이다.(변수를 변경하지 않기때문에)
        int baseNumber = 10;
        // baseNumber = 20; 으로 변경할시 로컬클래스,익명클래스,람다 모두 컴파일 오류 발생
        // 1. 로컬 클래스
        class LocalClass{
            void PrintBaseNumber(){
                System.out.println(baseNumber);
            }
        }

        // 2. 익명클래스
        Consumer<Integer> integerConsumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(baseNumber);
            }
        };

        // 3. 람다

        Supplier<Integer> printInt = ()->{
            return baseNumber;
        };
        System.out.println(printInt.get());
    }
}

로컬클래스와 익명 클래스<> 람다와 다른점 -> 쉐도윙(가려지는 것, 덮어지는 것)

  • 로컬 클래스와 익명 클래스는 메서드내에서 새로운 scope이다.
int baseNumber = 10;

// 1. 로컬 클래스
class LocalClass{
    void PrintBaseNumber(){
        int baseNumber = 11;
        // baseNumber 값은 11이 찍힐 것이다. (scope)
        // run 메소드에서 선언한 baseNumber에 대해 쉐도잉이 발생
        System.out.println(baseNumber);
    }
}

// 2. 익명 클래스
Consumer<Integer> integerConsumer = new Consumer<Integer>() {
    @Override
    public void accept(Integer baseNumber) {
        // 파라미터로 전달받은 baseNumber 가 찍힐 것이다.
        // run 메소드에서 선언한 baseNumber에 대해 쉐도잉이 발생
        System.out.println(baseNumber);
    }
};
  • 람다는 람다를 감싸고 있는 메서드와 같은 scope이다.
    -> 같은 이름의 변수를 선언할 수 없다.
    -> 람다에 들어있는 변수와 람다가 사용되고 있는 클래스의 변수들은 같은 scope이다.
int baseNumber= 10;

// 3. 람다
IntConsumer printInt = (baseNumber) -> {
    System.out.println(baseNumber);
};

위와 같이 선언하게 되는 경우 에러발생
→ Variable 'baseNumber' is already defined in the scope


참고

-> 람다식 내부에서 사용하는 변수는 Variable Capture가 발생하며, 이 값은 final이거나 effective final 처럼 사용해야한다.


4. 메소드, 생성자 레퍼런스

Method Reference

  • 메서드를 간결하게 지칭할 수 있는 방법으로 람다가 쓰이는 곳에서 사용이 가능하다.
  • 일반 함수를 람다 형태로 사용할 수 있도록 해준다.

예제

package me.whiteship.livestudy.week15.reference;

public class MethodReferenceSample {
    public static void main(String[] args) {
        MethodReferenceInterface methodReferenceInterface = MethodReferenceSample::multiplyPrint;
        methodReferenceInterface.multiply(13);
    }

    public static void multiplyPrint(int value){
        System.out.println(value*2);
    }
}

메서드 참조 방법:

  • Default use
  • Constructor Reference
  • Static Method Reference
  • Instance Method Reference

Constructor Reference:

  • 실제로 생성자를 호출해서 인스턴스를 생성하는것이 아닌 생성자 메서드를 참조
클래스이름::new

String::new
() -> new String

Static Method Reference:

  • 메서드 참조는 static method를 직접적으로 가리킬 수 있다.
클래스이름::메서드이름
(매개변수)-> Class.staticMethod(매개변수)

String::valueOf
str -> String.valueOf(매개변수)

Instance Method Reference

  • 특정 인스턴스의 메서드를 참조할 수 있다. 클래스 이름이 아닌 인스턴스 명을 넣어야 한다.
(매개변수)-> obj.instanceMethod(매개변수)
obj::instanceMethod

object::toString
()->object.toString()

정리

// 생성자 참조
String::new // ClassName::new
()->new String();

// static 메서드 참조
String::valueOf // Classname :: staticMethodName
(str) -> String.valueOf(str)

// instance 메서드 참조 클로저
x::toString // instanceName:: instanceMethodName
() -> "TheWing".toString()

// instance 메서드 참조 람다
String :: toString // ClassName :: instanceMethodName
(str) -> str.toString()

예제코드:

package me.whiteship.livestudy.week15.reference;

import java.util.function.BiFunction;
import java.util.function.IntBinaryOperator;

public class Example {
     public static void main(String[] args) {
         // int 타입 두개를 받아 int 타입을 반환하는 표준 api 사용
         IntBinaryOperator op;

         // static method 참조
         op = (num1,num2)-> MyReference.add_static(num1,num2);
         System.out.println(op.applyAsInt(10,20));

         op = MyReference::add_static;
         System.out.println(op.applyAsInt(20,30));

         // instance method 참조
         MyReference mr = new MyReference();

         op = (num1,num2) -> mr.add_instance(num1,num2);
         System.out.println(op.applyAsInt(30,40));

         op = mr::add_instance;
         System.out.println(op.applyAsInt(40,50));

         // 람다식의 매개변수로 접근 가능한 메서드 참조
         // 만약 (x,y) -> x.instanceMethod(y) 인 경우가 있는데
         // 이런 경우 사용할 수 있는 방법은 아래와 같다.

         // 아래 코드는 x 문자열에 y문자열이 포함되어 있는지 결과를 반환하는 예제이다.

         // 이경우 static method 참조와 형태가 매우 유사해 보이지만
         // x의 타입에 속하는 클래스 다음에 :: 연산자를 사용해서 메서드 참조를 한다.

         BiFunction<String,String,Boolean> myBiFunction;
         myBiFunction= (x,y) -> x.contains(y);
         System.out.println(myBiFunction.apply("java study","java"));

         myBiFunction= String::contains;
         System.out.println(myBiFunction.apply("java study","python"));

     }
}

class MyReference{
    // static method
    public static int add_static(int num1,int num2){
        return num1+num2;
    }

    // instance method
    public int add_instance(int num1,int num2){
        return num1+num2;
    }
}

생성자 참조의 예제

package me.whiteship.livestudy.week15.reference;

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

public class Example1 {
    public static void main(String[] args) {
        BiFunction<String,Integer,ConstructorRefTest> bf;
        Function<String,ConstructorRefTest> f;
        Supplier<ConstructorRefTest> s;

        bf = (p1,p2)-> new ConstructorRefTest(p1,p2);
        System.out.println(bf.apply("aaaaa",19).toString());

        System.out.println();

        s= ConstructorRefTest::new;
        System.out.println("기본 생성자 : " +s.get().toString());

        f=ConstructorRefTest::new;
        System.out.println("String 하나를 받는 생성자: " + f.apply("aaaaa").toString());

        bf = ConstructorRefTest::new;
        System.out.println("String,int 두개를 받는 생성자: "+ bf.apply("qweqweqwe",1).toString());
    }
}

class ConstructorRefTest{
    String name;
    int age;

    public ConstructorRefTest() {
    }

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

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

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

output

ConstructorRefTest{NAME='aaaaa',age=19}

기본 생성자 : ConstructorRefTest{NAME='null',age=0}
String 하나를 받는 생성자: ConstructorRefTest{NAME='aaaaa',age=0}
String,int 두개를 받는 생성자: ConstructorRefTest{NAME='qweqweqwe',age=1}

Process finished with exit code 0

1급 시민또는 1급 객체(First-class citizen)

다음의 세가지 조건을 모두 만족하는것들을 말한다.

  1. 변수에 할당할 수 있다.
  2. 매개변수로 사용할수있다.
  3. 반환 값으로 사용할수있다.

자바에서 메소드를 변수에 담아 전달해본적이 있었나?
메서드는 클래스에 종속되어 객체로 전달하거나 객체를 반환한 적은 있어도 메서드 자체를 전달해본적은 없다.

package me.whiteship.livestudy.week15.reference;

public class Example2 {
    public static void main(String[] args) {

        //변수에 저장
        MyInterface mi = ()-> System.out.println("변수에 저장된 람다식");
        mi.print();

        // 매개변수로 전달
        doProc(()->System.out.println("매개변수로 전달된 람다식"));

        //  반환값으로 사용
        getProc().print();
    }

    public static void doProc(MyInterface mi){
        mi.print();
    }

    public static MyInterface getProc(){
        return ()-> System.out.println("반환 값으로 사용된 람다식");
    };

}

interface MyInterface{
    void print();
}

output

변수에 저장된 람다식
매개변수로 전달된 람다식
반환 값으로 사용된 람다식

람다식이 세가지 조건을 만족하기 때문에 1급 시민 객체인것은 알겠으나 뭐가 좋은건가?

  • heap 영역에 생성된 객체가 stack 영역의 변수를 안정적으로 사용하기 위해 final 또는 final 성격을 가져야 한다.

  • 즉, 변할수있는것을 변하지 않도록 제한을 둬야한다. 이것을 불변상태(immutable)로 만든다고 한다.

  • 불변의 상태로 만든다는 것은 조금 더 쉬운 말로 '외부의 상태에 독립적' 이라고 표현할수있다.

  • 불변상태로 만들면 지역 변수에 대해 변하지 않는 상수를 사용하기 때문에 동일한 입력에 대해 동일한 결과를 기대할수있다.

  • 동일한 입력에 대해 일관된 결과를 받아볼 수 있다는 것은 다시 말하면 다수의 쓰레드가 동시에 공유해서 사용한다고 하더라도 일관된 결과를 받아볼 수 있다는 것으로 쓰레드와 관련된 동시성 문제가 생길 원인을 미리 방지할수있다.
    `


참고

참고링크
참고링크
참고링크

0개의 댓글