- 백기선 - JAVA STUDY/WEEK152021년 03월 06일 20시 16분 33초에 업로드 된 글입니다.작성자: jCurve728x90반응형
목표
자바의 람다식에 대해 학습하세요.
학습할 것 (필수)
- 람다식 사용법
- 함수형 인터페이스
- Variable Capture
- 메소드, 생성자 레퍼런스
람다식이란 ?
람다식 사용에 앞서 익명 구현 객체에 대해 알아보자
익명 구현 객체는 인터페이스나 클래스의 객체를 생성해서 사용할 때, 재사용하지 않는 경우 보통 사용한다.
특정 인터페이스를 사용하기 위해 그 인터페이스의 구현체로 클래스를 구현하는 상황에 만약 해당 클래스가 구현체로만 사용하고 클래스 자체는 재사용되지 않는다면 클래스 파일까지 생성해서 관리하는게 부담이다.
그래서 다음과 같이 별도의 클래스를 작성하지 않고 인터페이스를 바로 구현해서 사용하는 방법이 있는데 , 이를 익명 구현 객체라고 부른다.
inteface AnonymousInf { void doSomething(); } public static void main(String[] args){ AnonymousInf anony = new Anonymousinf(){ @Override public void doSomething(){ System.out.println("do something!!"); } }; anony.doSomething(); }
식별자 없이 실행 가능한 함수
- 메소들르 하나의 식으로 표현하는 것이라고 볼 수 있다.
- 람다식으로 표현하면 return 이 없어지므로 람다식을 anonymous function이라고도 한다.
람다식의 장점과 단점
장점
- 코드를 간결하게 만들 수 있다.
- 가독성이 향상된다.
- 멀티쓰레드 환경에서 용이하다.
- 함수를 만드는 과정 없이 한번에 처리하기에 생산성이 높아진다.
단점
- 람다로 인한 익명함수는 재사용이 불가능하다.
- 디버깅이 많아 까다롭다
- 람다를 무분별하게 사용하면 코드가 클린하지 못하다
- 재귀로 만들 경우 부적합하다
람다식 사용법
(매개변수) -> 표현바디 (매개변수) -> { 표현바디 } () -> { 표현바디} () -> 표현바디
람다식을 사용할 때 타켓 타입이란 것이 존재한다.
타켓 타입이란 , 람다식은 기본적으로 인터페이스 변수에 담기는데, 이 람다식이 담기는 인터페이스를 타켓 타입이라고 한다.
람다식을 사용할 때 타켓 타입이 중요한 이유는, 람다식만 보고는 이게 어떤 함수형 인터페이스를 구현한 것인지 유추할 수 없기 때문이다.
@FunctionalInterface (Single Abstract Method -> 줄여서 SAM이라고도 불린다)
- Java 8 이전에는 자바에서 값이나 객체가 아닌 하나의 함수를 변수에 담아두는 것은 허용되지 않았다.
- Java 8에서 람다식이 추가되고 하나의 변수에 하나의 함수를 매핑할 수 있다.
@FunctionalInterface public interface Funtional { public int calc(int a, int b); }
@FunctionalInterface 애너테이션을 지정하게되면 이 인터페이스가 함수형 인터페이스라고 명시를 해주고 컴파일러가 SAM여부를 체그할 수 있도록 한다.
사용 예제
Functional add = (int a, int b) -> { return a+b;}; Functional add1 = (int a, int b) -> a+b; Functional add2 = Interger::sum;
위의 addX 참조 변수에 할당된 함수는 모두 같은 결과를 반환한다.
이 변수에 할당된 함수를 사용하는 방법은 아래와 같다.
int result = add.calc(1,1) + add1.calc(2,2) + add2.calc(3,3); //결과 12
자바에서 람다를 내부적으로 익명클래스로 컴파일 하지 않는 이유: java8 이전 버전이나 kotlin같은 언어에서는 컴파일 시점에 람다를 단순히 익명클래스로 치환이 된다. 하지만 익명 클래스로 사용하면 항상 새 인스턴스로 할당해야하고 람다식 마다 클래스가 하나씩 생기게되는 문제가 발생할 수 있다.
함수형 인터페이스
함수형 인터페이스란?
함수형 인터페이스는 1개의 추상 메서드만 가지고 있는 인터페이스이다.
함수형 인터페이스를 사용하는 이유
자바의 람다식은 함수형 인터페이스로만 접근이 가능하다.
기본 함수형 인터페이스
- Runnable
- Supplier
- Consumer
- Function<T,R>
- Predicate
- 등등
Runnable
인자를 받지 않고 리턴값도 없는 인터페이스로 멀티쓰레드 스터디 블로깅에서 언급했던 내용이다.
Suppliers
Supplier<T>는 인자를 받지 않고 T 타입의 객체를 리턴한다.
public interface Supplier<T>{ T get(); }
Consumer
Consumer<T>는 T타입의 객체를 인자로 받고 리턴 값은 없다.
public interface Consumer<T> { void accept(T t); defualt Consumer<T> andThen(Consumer<? super T> after){ Objects.requireNonNull(after); return (T t) -> { accept(t); after.accpet(t);}; } }
andThen()을 사용하면 두 개 이상의 Consumer를 사용할 수 있다.
Function
Function<T,R>는 T 타입의 인자를 받아, R 타입의 객체로 리턴한다.
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; } }
Predicate
Predicate<T>는 T타입 인자를 받고 결과로 boolean으로 리턴한다.
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); } }
Variable Capture
- 람다식은 특정 상황에서 람다 함수 본문 외부에 선언된 변수에 접근이 가능하다.
- Java8 버전 이전에서는 익명의 내부 클래스가 이를 둘러싼 메서드에 대한 로컬 변수를 캡처할때 이 문제가 발생했다. 컴파일러가 만족할 수 있도록 로컬 변수 앞에 final 키워드를 추가해야만 했었다.
- 우리가 변수를 final로 선언하면 컴파일러가 변수를 사실상 final로 인식할 수 있다.
클래스의 멤버 메소드의 매개변수와 이 메소드 실행 블록 내부의 지역 변수는 JVM의 stack 영역에 생성되고 메소드 실행이 끝나며 stack에서 사라진다.
new 연산자를 사용해서 생성한 객체는 JVM의 heap 영역에 객체가 생성되고 gc에 의해 관리되며, 더 이상 사용하지 않는 객체에 대해 필요한 경우 메모리에서 제거한다.
heap에 생성된 객체가 stack의 변수를 사용하려고 하는데 , 사용하려는 시점에 stack에 더 이상 해당 변수가 존재하지 않을 수 있다. stack은 메소드 실행이 끝나면 매개변수나 지역변수에 대해 제거하기 때문에, 그래서 더 이상 존재하지 않는 변수를 사요하려 할 수 있기 때문에 오류가 발생하는데 자바는 이 문제를 Variable Capture라는 값 복사를 사용해서 해결하고 있다.
컴파일 시점에 멤버 메소드의 매개변수나 지역 변수를 멤버 메소드 내부에서 생성한 객체가 사용 할 경우 객체 내부로 값을 복사해서 사용한다.
이때 제약이 존재하는데 final 키워드로 작성 되었거나 final 성격을 가져야 한다.(effectively final)
Local variable Capture
- Local varible은 조건이 final 또는 effectively final 이여야 한다
- effectively final이란?
- Java8에 추가된 syntactic sugar의 일종으로, 초기화된 이후 값이 한번도 변경되지 않았다는 것
- effectively final 변수는 final 키워드가 붙어있지 않지만 final 키워드를 붙인 것과 동일하게 컴파일러에서 처리한다.
- 결론적으로 초기화하고 값이 변경되지 않은 것을 의미한다.
- 왜 이런 제약조건이 있을까?
- JVM 메모리 구조를 되짚어보자.
- 지역변수는 쓰레드끼리 공유가 안된다.
- JVM에서 인스턴스 변수는 힙 영역에 생성된다.
- 인스턴스 변수는 쓰레드끼리 공유가 가능하다.
- JVM 메모리 구조를 되짚어보자.
- 결론적으로
- 지역 변수가 스택에 저장되기 때문에 람다식에서 값을 바로 참조하는 것에 제약이 있어 복사된 값을 사용하는데 이때 멀티 쓰레드 환경에서 변경이 되면 동시성에 대한 이슈를 대응하기가 힘들다.
메소드, 생성자 레퍼런스
Method Reference
메소드를 간결하게 지칭할 수 있는 방법으로 람다가 쓰이는 곳에서 사용이 가능하다.
일반 함수를 람다 형태로 사용할 수 있도록 해준다.
람다식을 더 간략하게 표현 할 수 있게 해준다.
콜론 두 개 :: 를 사용한다.
interface MethodReferenceInterface{ void multiply(int value); } public class MethodReferenceSample{ publi static void main(String[] args){ MethodReferenceInterface methodReferenceInterface = MethodReferenceSample::multiplyPrint; methodReferenceInterface.multiply(13); } public static void multiplyPrint(int value){ System.out.print(value*2); } }
메소드 참조 방법
- Default Use
- 매개변수의 타입 클래스 이름 :: 메소드 이름
- Constructor Reference
- 클래스 이름 :: new
- Static Method Reference
- 클래스 이름 :: 메소드 이름
- Instance Method Reference
- 인스턴스 변수 :: 메소드 이름
public static void main(String[] args){ IntBinaryOperator op; //static method 참조 op = MyReference::add_static; // op = (num01,num02) -> MyReference.add_static(num01,num02); 와 동일 //instance method 참조 MyReference mr = new MyReference(); op = mr :: add_instance; // op = (num01,num02) -> mr.add_instance(num01,num02); 와 동일 //constructor method 참조 Function<String,ConstructorRefTest> f; f = ConstructorRefTest :: new; System.out.println(f.apply("hibs").toString()); }
반응형'JAVA' 카테고리의 다른 글
생성자에 매개변수가 많다면 빌더를 고려하라 (0) 2021.03.15 생성자 대신 정적 팩터리 메서드를 고려 (0) 2021.03.14 백기선 - JAVA STUDY/WEEK14 (0) 2021.03.06 백기선 - JAVA STUDY/WEEK13 (0) 2021.03.06 백기선 - JAVA STUDY/WEEK12 (0) 2021.03.06 댓글