본문 바로가기
  • Where there is a will there is a way.
개발/java

자바 람다식이란?

by 소확행개발자 2018. 10. 6.

자바 람다식이란?

자바는 함수적프로그래밍을 위해 자바 8부터 람다식을 지원하면서 기존의 코드 패턴이 많이 달라졌다. 람다식은 anonymous function을 생성하기 위한 식으로 객체지향 언어보다는 함수지향 언어에 가깝다.


( 이 내용은 이것이 자바다 에서 람다부분을 참조해서 작성했습니다. ) 


자바에서 람다식을 수용한 이유는 자바 코드가 간결해자고 , 주로 사용하는 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있기 때문이다. 

람다식의 형태는 매개 변수를 가진 코드 블록이지만, 런타임 시에는 익명 구현 객체를 생성한다.


예를들어 

// 익명 구현개체
Runnable runnable = new Runnable() {
@Override
public void run() {

}
};

// 위의 코드에서 익명 구현 객체를 람다식으로 표현하면 다음과 같다.
Runnable runnable2 = () -> {};
@Test
public void ramdaTestforNotReturnAndNotParam(){

OvndFunctionalInterface ovndFunctionalInterface;

ovndFunctionalInterface = () -> {
String str = "method call1";
System.out.println(str);
};

// 메소드의 호출은 람다식의 중괄호 {} 를 실행시킨다.
ovndFunctionalInterface.method();
}


람다식은 (매개변수) -> {실행코드} 형태로 작성되는데, 마치 함수 정의 형태를 띠고 있지만 런타임 시에 인터페이스의 익명 구현 객체로 생성된다. 


어떤 인터페이스를 구현할 것인가는 대입되는 인터페이스가 무엇이냐에 달려있다. 


(int a) -> {System.out.println(a)};


매개변수 타입은 런타임 시에 대입되는 값에 따라 자동으로 인식될 수 있기 때문에 람다식에서는 매개 변수의 타입을 일반적으로 언급하지 않는다. 


(a) -> {System.out.println(a)}


만약 매개 변수가 하나밖에 없다면 괄호도 생략해서


a -> System.out.println(a) 로 표현이 가능하다. 


만약 실행코드에 return 이 있다면 


( x, y) -> { return x+y; };


그리고 return 문만 있는 경우에는 


(x , y) -> x+y 


타겟인터페이스

람다식이 대입될 인터페이스를 람다식의 타겟 타입이라고 하는데 


모든 인터페이스를 타겟으로 사용할 수 없다. 람다식이 하나의 메소드를 정의하기 때문에 두 개 이상의 추상 메소드가 선언된 인터페이스는 람다식을 이용해서 구현 객체를 생성할 수 없다. 


정리하자면 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟 타입이 될 수 있는데, 이러한 인터페이스를 함수적 인터페이스라고 한다. 


두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹해주는 기능이 있는데, 인터페이스 선언시


@FunctionalInterface


어노테이션을 붙이면 된다. 



@FunctionalInterface
public interface OvndFunctionalInterface {
// 람다형 메소드는 한개만 존재해야 한다.
public void method();

}


@Test
public void ramdaTestforNotReturnAndNotParam(){

OvndFunctionalInterface ovndFunctionalInterface;

ovndFunctionalInterface = () -> {
String str = "method call1";
System.out.println(str);
};

// 메소드의 호출은 람다식의 중괄호 {} 를 실행시킨다.
ovndFunctionalInterface.method();
}



디폴트 및 정적 메소드는 추상 메소드가 아니기 때문에 함수적 인터페이스에 선언되어도 여전히 함수적 인터페이스의 성질을 잃지 않는다. 


 


@FunctionalInterface
public interface OvndFunctionalInterface3 {

int[] scores = {1,2,3};

static int maxOrMix(IntBinaryOperator operator){

int result = scores[0];
for(int score : scores){
result = operator.applyAsInt(result, score);
}

return result;

}

public int method(int x, int y);

}

@Test
public void ramdaTestForOperator(){

int max = OvndFunctionalInterface3.maxOrMix(
(a,b) -> {
if(a>b){
return a;
}else{
return b;
}
}
);

int min = OvndFunctionalInterface3.maxOrMix(
(a,b) -> {
if(a<b)
return a;
else
return b;
}
);

OvndFunctionalInterface3 ovndFunctionalInterface3 = (left, right) -> Math.max(left, right);
int value1 = ovndFunctionalInterface3.method(3,1);

System.out.println(max);
System.out.println(min);
System.out.println(value1);
}


생성자 참조

생성자가 오버로딩 되어 여러개가 있을 경우, 컴파일러가 함수적 인터페이스의 추상 메소드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다.


@Test
public void methodReferenceTest(){

OvndFunctionalInterface3 ovndFunctionalInterface3 = (left, right) -> {
return Math.max(left, right);
};
int value1 = ovndFunctionalInterface3.method(3,4);

OvndFunctionalInterface3 ovndFunctionalInterface31 = Math::max;
int value2 = ovndFunctionalInterface31.method(3,4);


System.out.println(value1);
assertEquals("expect two return value is equal", value1,value2);
}

@Test
public void methodReferenceTest2(){

Member member = new Member();
member.setName("derek");

Supplier<String> consumer = member::getName;
System.out.println(consumer.get());
}

표준 API 함수적 Interface

자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능하다.

종류

종류 

추상 메소드 특징 

Consumer 

파라미터 O , 리턴 X 

Supplier 

파라미터 X , 리턴 O 

Function 

파라미터 O , 리턴 O  ( 파라미터를 리턴으로 매핑 :: )

Operator

파라미터 O , 리턴 O ( 파라미터가 연산 과정을 거쳐 리턴 ) 

Predicate 

파라미터 O , 리턴 O ( boolean return ) 


List<Student> lists;


@Before
public void setup() {

lists = Arrays.asList(
new Student("male", 50),
new Student("female", 80),
new Student("male", 90),
new Student("female", 60)
);
}


@Test
public void averageTestForPredicate() {

double result = femaleAvg(t -> t.getSex().equals("female"));
System.out.println(result);

}

private double femaleAvg(Predicate<Student> predicate) {

int count = 0, sum = 0;

for (Student student : lists) {
if (predicate.test(student)) {
sum += student.getScore();
count++;
}
}

return (double) sum / count;
}

스트림 Stream

스트림은 자바 8부터 추가된 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자이다.


@Test
public void lamdaExpressTest(){

List<Member> list = new ArrayList<>();
list.add(new Member("derekpark","sanghoo9112@naver.com"));
list.add(new Member("johnkoo","johnkoo@overnodes.com"));

Stream<Member> stream = list.stream();
stream.forEach(s -> System.out.println(s.getName()));

}


Java 8 Stream 의 장점

내부 반복자를 사용하므로 병렬처리가 쉽다는 점이 있다.


외부 반복자는 Iterator나 index를 말한다. 







Iterator는 컬렉션 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만, 스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어난다. 스트림을 이용하면 코드도 간결해지지만, 무엇보다도 요소의 병렬 처리가 컬렉션 내부에서 처리되므로 일석이조의 효과를 가져온다. 



@Test
public void steamThreadTest(){

List<String> list = Arrays.asList(
"derekpark",
"johnkoo",
"ryankim",
"tedkim"
);

//순차처리
Stream<String> stream = list.stream();
stream.forEach(PrintThread::print);

System.out.println("================================");

//병렬처리
Stream<String> parallelStream = list.parallelStream();
parallelStream.forEach(PrintThread::print);

}

derekpark:main

johnkoo:main

ryankim:main

tedkim:main

================================

johnkoo:ForkJoinPool.commonPool-worker-1

derekpark:ForkJoinPool.commonPool-worker-3

tedkim:ForkJoinPool.commonPool-worker-1

ryankim:main


스트림은 중간 처리와 최종처리가 가능하다.


( Rx에서도 사용되는 개념 )


스트림은 컬렉션 요소에 대해 중간처리와 최종 처리를 수행할 수 있는데, 중간 처리에서는 매핑, 필터링, 정렬을 수행하고 최종 처리에서는 반복, 카운팅, 평균, 총합등의 집계 처리를 수행한다.


@Test
public void mapAndReduceTest(){

List<Score> list = Arrays.asList(
new Score("derekpark",90),
new Score("johnkoo", 80),
new Score("ryankim",50),
new Score("tedkim", 70)
);

double avg = list.stream()
// 중간처리
.mapToInt(Score::getScore)
// 최종처리
.average().getAsDouble();

System.out.println("평균점수 : " + avg);
}


스트림 파이프라인

대량의 데이터를 가공해서 축소하는 것을 리덕션이라고 하고 데이터 합계 , 평균값 , 카운팅 , 최댓값 등이 대표적인 리덕션의 결과물이라고 볼 수 있다. 그러나 컬렉션의 요소를 리덕션의 결과물로 바로 집계할 수 없을 경우에는 집계하기 좋도록 필터링, 매핑, 정렬, 그룹핑 등의 중간 처리가 필요하다. 


중간 처리와 최종 처리

스트림은 중간 처리와 최종 처리를 통해 리덕션 하는 것을 파이프라인 방법으로 해결한다. 

파이프라인은 여러 개의 스트림이 연결되어 있는 구조를 말한다. 


파이프라인에서 최종 처리는 모두 중간 처리를 제외하고는 모두 중간 처리 스트림이라고 말한다.


중간 스트림이 생성될 때 요소들이 바로 처리되는 것이 아니라 최종 처리가 시작되기 전까지는 중간 처리는 지연 된다. 즉 최종처리가 시작되면 비로소 컬렉션의 요소가 하나씩 중간 스트림에서 처리되고 최종 처리까지 오게 된다. 


@Test
public void streamPipelimeTest() {

List<OvndEntity> lists = Arrays.asList(
new OvndEntity("derekpark@overnodes.com", "박상후", 90),
new OvndEntity("johnkoo@overnodes.com", "구장회", 85),
new OvndEntity("tedkim@overnodes.com", "박상후", 75)
);

double parkAvg = lists.stream()
.filter(s -> s.getName() == "박상후")
.mapToInt(OvndEntity::getScore)
.average()
.getAsDouble();

System.out.println("상후의 평균 점수 : " + parkAvg);

}


@Test
public void streamFlatMapTest() {

List<String> lists = Arrays.asList(

"java8 ramda",
"rxjava1 rxjava2"
);

lists.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.forEach(s -> System.out.println(s));

}

spring react 5.0 에서 발췌한 이미지



댓글