210106 람다식과 스트림
2021-01-06
람다식
(매개변수)->{실행문;}
- 자바 8부터 함수형 프로그래밍 방식을 지원
함수형 프로그래밍
- 함수를 기반으로 자료를 입력받아 구현한다
- 함수의 구현과 호출만으로 프로그램을 만들 수 있다
- 순수 함수(pure function)을 구현, 호출하여 외부 자료에 부수적인 영향을 주지 않도록 구현한다
- 순수 함수: 매개변수만을 사용하여 만드는 함수
- 함수 내부에서 함수 외부에 있는 변수를 사용하지 않아, 함수가 수행되더라도 외부 자료에는 영향이 없다
함수형 프로그래밍의 장점
- 여러 자료를 동시에 처리하는 병렬 처리에 적합하다
- 함수가 입력받은 자료 외 외부 자료에 영향을 미치지 않기 때문
- 안정되고 확장성 있는 프로그램을 개발할 수 있다
- 함수 기능이 자료에 독립적임을 보장한다
- 동일한 입력에 대해 동일한 출력 보장
- 다양한 자료에 같은 기능을 수행할 수 있다
람다식 문법
- 함수 이름이 없는 익명 함수를 만든다
// 1. 매개변수 자료형을 생략 가능하며, 매개변수가 하나인 경우에는 괄호도 생략 가능하다.
str -> {System.out.println(str);}
(x,y) -> {System.out.println(x+y);}
// 2. 중괄호 내부의 구현 부분이 한 문장인 경우, 중괄호를 생략할 수 있다
// 하지만 return문은 중괄호를 생략할 수 없다
str -> System.out.println(str);
str -> { return str.length();}
// 3. 중괄호 안의 구현 부분이 return문 하나라면, 중괄호와 return을 모두 생략하고 식만 쓴다
(x , y)-> x + y;
str -> str.length();
람다식 사용
// 함수형 인터페이스 선언
public interface MyNumber {
int getMax(int num1, int num2);
}
// 람다식 구현 및 호출
public class TestMyNumber {
public static void main(String[] args){
MyNumber Max = (x,y) -> (x>=y) ? x : y;
System.out.println(max.gerMax(10,20));
}
}
함수형 인터페이스
- 람다식은 메서드 이름이 없다
- 메서드를 실행하는데 필요한 매개변수와 매개변수를 활용한 실행 코드를 구현
- 자바에서는 참조 변수 없이 메서드를 호출할 수 없다
- 람다식을 구현하기 위해 함수형 인터페이스를 만들고, 인터페이스에 람다식으로 구현할 메서드를 선언한다.
- 람다식은 오직 하나의 메서드만 선언한 인터페이스를 구현할 수 있다
- 하나의 메서드를 구현하여 인터페이스형 변수에 대입
- 이름이 없는 익명 함수로 구현하기 때문에 인터페이스에 메서드가 여러 개 있다면 어떤 메서드를 구현한 것인지 모호해진다
@FunctionalInterface 애너테이션
-
함수형 인터페이스라는 의미를 명시적으로 표현한다
-
람다식으로 구현한 인터페이스에 실수로 다른 메서드를 추가하는 것을 막는다
익명 객체
- 익명 내부 클래스는 클래스 이름 없이 인터페이스 자료형 변수에 바로 메서드 구현부를 생성하여 대입할 수 있다
- 람다식 코드에서 외부 메서드의 지역 변수를 수정할 수 없다
- 지역 변수는 메서드 호출이 끝나면 메모리에서 사라진다
- 익명 내부 클래스에서 사용하는 경우에는 지역 변수가 상수로 변한다
- 람다식 역시 익명 내부 클래스가 생성, 외부 메서드의 지역 변수를 사용하면 변수는 상수(final)가 되어 오류 발생
함수를 변수처럼 사용하는 람다식
interface PrintString{
void showString(String str);
}
public class TestLambda{
public static void showMyString(PrintString p){
p.showString("Hello2");
}
public static PrintString returnString(){
/*
PrintString str = s->System.out.println(s+" world");
return str;
*/
return s->System.out.println(s+" world");
}
public static void main(String args[]){
// 1) 인터페이스형 변수에 람다식 대입
PrintString lambdaStr = s-> System.out.println(s);
lambdaStr.showString("Hello");
// 2) 함수의 매개변수로 람다식 전달
showMyString(lambdaStr);
// 3) 반환값으로 람다식 사용
PrintString reStr=returnString();
reStr.showString("Hello3");
}
}
스트림
int[] arr = {1,2,3,4,5};
Arrays.stream(arr).forEach(n->System.out.println(n));
- 여러 자료의 처리에 대한 기능을 구현해놓은 클래스
- 배열, 컬렉션 등의 자료를 일관성있게 처리할 수 있다
- 처리해야 하는 자료가 무엇인지와 상관없이 같은 방식으로 메서드를 호출한다
- 자료를 추상화했다
- 처리해야 하는 자료가 무엇인지와 상관없이 같은 방식으로 메서드를 호출한다
스트림 연산
중간 연산
- 자료를 거르거나 변경하여 또 다른 자료를 내부적으로 생성
filter()
sList.stream().filter(s -> s.length() >= 10).forEach(s -> System.out.println(s));
- 조건을 넣고 true인 경우에만 자료 추출
map()
customerList.stream().map(c->c.getName()).forEach(s -> System.out.println(s))
- 클래스가 가진 자료 중 한가지만 추출
- 요소들을 순회하면서 다른 형식으로 변환하는 경우에도 사용
최종 연산
- 생성된 내부 자료를 소모해 가면서 연산을 수행
- 마지막에 한 번만 호출된다
- 최종 연산이 수행되고 나면 해당 스트림은 더이상 사용할 수 없다
forEach()
- 요소를 하나씩 꺼낸다
count()
int count = (int) Arrays.stream(arr).count();
- 통계: 배열 요소의 개수를 출력
sum()
int sum = Arrays.stream(arr).sum();
- 통계: 배열 요소의 합계를 구함
Collection에서 스트림 생성/사용
public class ArrayListStreamTest{
public static void main(String[] args){
List<String> sList = new ArrayList<>();
sList.add("A");
sList.add("B");
sList.add("C");
Stream<String> stream = sList.stream();
stream.forEach(s -> System.out.println(s));
System.out.println();
sList.stream().sorted().forEach(s -> System.out.println(s));
}
}
- Collection 인터페이스의 stream() 메서드 사용, 스트림 생성
- 이 스트림은 내부적으로 해당 Collection 구현체의 모든 요소를 가지고 있다
sorted()
- 요소의 정렬을 위한 중간 연산
- 사용하는 자료 클래스가 Comparable 인터페이스를 구현해야 한다
- 만약 구현되어 있지 않다면 메서드의 매개변수로 Comparator 인터페이스를 구현한 클래스를 지정할 수 있다
스트림의 특징
자료의 대상과 관계없이 동일한 연산을 수행한다
- 컬렉션의 여러 자료 구조에 대해 요소 출력, 조건에 따른 자료 추출, 합계나 평균 등의 작업을 일관성 있게 처리할 수 있는 메서드를 제공한다
한 번 생성하고 사용한 스트림은 재사용할 수 없다
- 스트림의 요소들은 최종 연산시 ‘소모된다’
- 소모된 요소들은 재사용할 수 없으며, 다른 기능을 호출하고 싶다면 스트림을 새로 생성해야 한다
스트림의 연산은 기존 자료를 변경하지 않는다
- 정렬한다거나 합을 구하는 등의 여러 연산을 수행한다고 해서 기존 배열이나 컬렉션이 변경되지는 않는다
- 스트림 연산을 위해 사용하는 메모리 공간이 별도로 존재한다
스트림의 연산은 중간 연산과 최종 연산이 있다
- 중간 연산은 여러 개가 적용될 수 있으며, 최종 연산은 맨 마지막에 한 번 적용된다.
- 만약 중간 연산이 여러 개 호출되더라도 최종 연산이 호출되어야 스트림의 중간 연산이 모두 적용된다
- 지연 연산(lazy evaluation)
reduce() 연산
T reduce(T identify, BinaryOperator<T> accumulator)
- 내부적으로 스트림의 요소를 하나씩 소모하면서 프로그래머가 직접 정의한 기능을 수행한다
- T identify : 초기값
- BinaryOperator
인터페이스 : 두 매개변수로 람다식을 구현, 각 요소별 기능 수행 - 매개변수로 람다식을 직접 작성해도 되고, 길이가 길다면 인터페이스를 구현한 클래스를 생성하여 대입해도 된다
- 함수형 인터페이스이므로 apply() 메서드를 반드시 구현해야 한다
- apply() : 두 개의 매개변수와 한 개의 반환값을 가진다(셋 다 같은 자료형)
class CompareString implements BinaryOperator<String> {
@Override
public String apply(String s1, String s2){
if(s1.getBytes().length >= s2.getBytes().length) return s1; else return s2;
}
}
public class ReduceTest {
public static void main(String[] args){
String[] greetings = {"안녕하세요","hello","반갑습니다"};
// 1. 매개변수로 람다식 직접 작성하기
System.out.println(Arrays.stream(greetings).reduce("",(s1,s2)->{
if(s1.getBytes().length >= s2.getBytes().length) return s1; else return s2;});
// 2. BinaryOperator 인터페이스를 구현한 클래스 사용하기
String str = Arrays.stream(greetings).reduce(new CompareString()).get();
System.out.println(str);
}
}
궁금증: 스트림의 forEach()와 for-loop의 성능 차이?
- 요소를 순회하기 위해 스트림의 forEach() 최종 연산을 사용할때와 for-loop를 사용할 때 성능 차이가 발생할까?
- 기본 for-loop을 사용했을 때 가장 빠르다
- 기본 for-loop은 오버헤드가 없는 단순 인덱스 기반 메모리 접근이므로 스트림보다 빠르다
- 비교적 최근에 도입된 스트림보다 for-loop의 컴파일러 최적화가 더 정교하다
출처
Do it! 자바 프로그래밍
https://madplay.github.io/post/mistakes-when-using-java-streams