람다와 스트림

주8·2023년 1월 31일
0

함수형 프로그래밍(Functional Programming)

  • 소프트웨어 규모가 커짐에 따라 복잡한 코드를 유지보수 하기가 힘들어졌고, 이를 해결하기 위해 함수형 프로그램밍에 관심을 가지게 되었다. 선언형 프로그래밍이다.
  • 계산을 수학적 함수의 평가로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임이다.
  • 함수형 프로그래밍은 프로그램을 오직 순수 함수(pure function)들로만 작성되어진다.
  • 순수 함수(Pure function)은 모든 입력이 이력으로만, 모든 출력이 출력으로만 사용된다.
  • 함수형 프로그래밍은 대입문이 없는 프로그래밍이다.
  • 객체지향은 동작하는 부분을 캡슐화해서 이해할 수 있게 하고, 함수형 프로그래밍은 동작하는 부분을 최소화해서 코드 이해를 돕는다.
  • 함수 실행 동안 변수 할당 값은 불변: 변수에는 새로운 값만 설정할 수 있으며 값이 설정이 되면 변경할 수 없다.
  • 1급 객체(First object, First class citizen): 1급 함수라고도 하며, 함수를 변수나 자료 구조 안에 할당할 수 있고, 인자 전달, 리턴 값으로 사용할 수 있는 객체를 의미한다.
  • 고차 함수(Higher-order function): 함수의 인자로 함수를 넘기거나, 함수를 리턴하는 함수, 1급 함수를 지원한다는 의미는 고차함수를 사용할 수 있다는 의미이다.
  • 익명함수(Anonymous function): Java에서 람다(Lambda)는 익명함수이며, 파라미터 리스트와 함수의 body만을 가지는 함수이다.
  • 코드를 간결하게, 가독성을 높여 로직에 집중: 람다(Lambda), 스트림(Stream) API를 통해 보일러 플레이트(Boilerpalte code)를 제거하고, 내부에 직접적인 함수 호출을 통해 가독성을 높인다. 보일러 플레이트는 ‘별 수정 없이 반복적으로 사용되는 코드’를 의미한다.
  • 동시성 작업을 보다 쉼고 안전하게 구현할 수 있다.
  • 단일 추상 메서드를 가지는 인터페이스로 함수형 인터페이스 지정을 위해서 @FunctoinalInterface 어노테이션이 도입되었다.
  • 단일 메서드 구현으로 람다 표현식을 이용해 함수로 구현할 수 있게 된다.
  • Default static method를 정의할 수 있지만 추상 메서드는 하나만 포함할 수 있다.
  • @FunctionalInterface 어노테이션이 아니더라도 추상 메서드가 하나인 인터페이스는 함수형 인터페이스로 처리된다.
@FunctionalInterface
public interface Runnable{
	public abstract void run();
}

@FUnctionalInterface
interface MyFunction{
	void run(); //public abstract void run();
}

람다식(lambda expression) 개요

  • Java 8에서의 가장 큰 변화는 람다식이며 람다는 원래는 수학기호로 함수형 언어의 특징에서 나온 것으로 한 번 이상 실행할 수 있는 코드블록을 말하며 실제 구현에는 익명 함수 형태로 사용된다.
  • 식을 사용하여 하나의 메서드 인터페이스를 나타내는 명확하고 간결한 방법을 제공하며, 컬렉션 라이브러리에서 매우 유용하게 사용할 수 있다.
  • 컬렉션에서 데이터를 반복, 필터링 및 추출하는 데 도움이 된다.
  • 람다식은 함수를 간결하게 표현하며, 이미 많은 언어에서 지원하고 있다.
  • 람다식은 함수형 인터페이스 구현을 제공하며, 람다식을 통해 인스턴스화 될 수 있고, 구현 코드만 작성하면 된다.
Runnalbe r = () -> {System.out.println("funmctional interface");}
  • 함수의 구조로 → 와 같은 화살표 형태의 기호를 이용해서 매개변수를 함수 바디로 전달하는 형태를 취한다.
    • 예) (int x) → x + 1 → ‘x라는 인수로 호출하여 x+1을 반환’하는 동작을 수행하는 코드를 구현할 수 있다.

람다식 특성

  • 메서드처럼 특정 클래스에 종속되지 않고 parameter, body, return 값을 포함한다.
  • 람다식을 메서드의 인자로 전달하거나 변수 값으로 저장할 수 있다.
  • 적은 코딩으로 구현을 할 수 있으며, 일반적으로 다중 CPU를 활용하는 형태로 구현되어 병렬처리에 이득이다.
  • 디버깅시 함수 콜스택 추적이 다소 어렵다.
  • 람다식은 함수로 취급되며, 컴파일러는 .class 파일을 생성하지 않는다.
  • Syntax: (Argument list) → {body}
    • Argument list: 매개변수는 비어 있거나 비어 있지 않을 수 있다.
    • → (Arrow-token): 파라미터 리스트와 experssion 본문을 연결하는데 사용된다.
    • Body: 람다식을 위한 표현식과 명령문을 포함한다.

람다식(lambda experssion) 개요

  • 기존 Comparator 코드
Comparator<Apple> weight = new Comparator<Apple>(){
	@Override
	public int compare(Apple o1, Apple o2){
		return o1.getWeight().compareTo(o2.getWeight());
	}
}
  • 람다를 이용한 Comparator 코드
Comparator<Apple> weight =
			(Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight());

람다식 예

  • No parameter return void: () → {}
  • No parameter, express body: () → 10, () → null
  • No parameter, body, return 값: () → { return 10; }
  • No parameter, return void: () → {System.out.println(”value”);}
  • One Parameter list: (int x) → x+1
  • Multiple Pamrameter list: (int x, int y) → x+y
  • Type 추론 parameter list: (x) → x+1, x → x+1
  • 람다식의 Parameter, Return 타입 추론: 추상 메서드와 리턴타입에 대해서 타입 추론을 수행한다.
  • Boolean 표현식: (List<String> list) → list.isEmpty()
  • 객체 생성: () → new Apple();
  • 객체 소비: (Apple a) → {System.out.println(a.getWeight());}
  • 객체 선택/추출: (String s) → s.length();
  • 값의 조합: (int a, int b) → a*b
  • 객체 비교: (Apple a, Apple b) → a.getWeight().compareTo(b.getWeight());

  • 람다식을 이용한 스레드 생성 예제
public class LambdaCreateThreadEx1{
	public static void main(String[] args) {
		// 람다 없이 Thread 생성 
		Runnable r1=new Runnable(){
			public void run(){
				System.out.println("Thread1 is running...");
			} 
		};

		Thread t1=new Thread(r1); t1.start();
		// 람다를 사용한 Thread 생성 
		Runnable r2=()->{
			System.out.println("Thread2 is running..."); 
		};
		Thread t2=new Thread(r2);
		t2.start(); 
	}
}
  • 컬렉션 프레임워크에서 람다식을 사용할 수 있고, 데이터를 iteration, filtering 등의 간결한 방법을 제공한다.

  • Comparator 예제
public class LambdaProduct{ 
int id;
String name;
int price;
public LambdaProduct(int id, String name, int price) {
	this.id = id; 
	this.name = name; 
	this.price = price;
}

public int getId() {
	return id;
}
public String getName() {
	return name; 
}
public int getPrice() { 
	return price;
}
public class LambdaCollectionComparator { 
	public static void main(String[] args) {
		List<LambdaProduct> list=new ArrayList<>();

		list.add(new LambdaProduct(1,"노트북",25000)); 
		list.add(new LambdaProduct(3,"키보드",300)); 
		list.add(new LambdaProduct(2,"마우스",150));

		System.out.println("이름 기준 정렬");

		// lambda expression 구현 
		Collections.sort(list,(p1, p2)->{
			return p1.name.compareTo(p2.name);
		});

		for(LambdaProduct p:list){ 
			System.out.println(
							p.id+" "+p.name+" "+p.price);
		}
	}
}

[결과]
이름 기준 정렬 
1 노트북 25000 
2 마우스 150 
3 키보드 300

메서드 레퍼런스(Method Reference)

  • Java 8부터 제공되는 기능으로 함수형 인터페이스의 메서드를 참조하는데 사용된다.
  • 람다식으로 간결하게 사용할 수 있는 Syntax
  • 클래스(객체)명과 메서드명 사이에 구분자(::)를 붙여 사용한다.
  • 이 기능을 활용해 매개변수 정보 및 리턴 타입을 알아내어 람다식에서 불필요한 매개변수를 제거하는 것이 목적이다.
  • 다음 유형의 메서드 레퍼런스가 있다.
종류형식
정적 메서드 참조클래스명::정적메서드명
객체 메서드 참조객체변수::메서드명
람다인자 객체 메서드 참조클래스명::메서드명
생성자 참조클래스명::New
(apple) -> apple.getWeight();
Apple::getWeight;

  • 정적 메서드 참조(Static method reference)

    • 클래스에 정의된 정적 메서드를 참조할 수 있다.
    • 예) Integer::parseInt
  • 인스턴스 메서드 참조(Instance method reference)

    • 인스턴스 메서드를 참조할 수 있다. 클래스 객체 및 익명 객체로 메서드를 참조할 수 있다.
    • System.out::println
  • 생성자 참조(Constructor reference)

    • new 키워드를 사용하여 클래스의 생성자를 간결하게 호출하여 참조할 수 있다. (String::new)
    • (인자) → new 클래스(인자)
    • () → new 클래스()
    • 클래스::new
  • 정적 메서드 참조 예제: 미리 정의된 Runnable 인터페이스를 사용하여 정적 메서드 참조를 한다.

public class StaticMethodReferenceEx2 { 
	public static void ThreadStatus(){
		System.out.println("Thread is running"); 
	}
	public static void main(String[] args) {
		Thread t2=new Thread(StaticMethodReferenceEx2::ThreadStatus); 
		t2.start();
	} 
}

[결과]
Thread is running

  • 인스턴스 메서드 참조 예제
interface SayableEx{ 
	void say();
}
public class InstanceMethodReferenceEx1 {
	public void saySomething(){ 
		System.out.println("안녕, 인스턴스 메서드");
	}
	public static void main(String[] args) {
		InstanceMethodReferenceEx1 methodReference = new InstanceMethodReferenceEx1(); 
		// 참조를 사용한 인스턴스 메서드 참조
		SayableEx sayable = methodReference::saySomething;
		// 인터페이스 메서드 호출
		sayable.say();
		// 익명 객체를 이용한 인스턴스 메서드 참조
		Sayable sayable2 = new InstanceMethodReferenceEx1()::saySomething; // 익명 객체를 사용할 수 있다
		sayable.say();
	}
}

[결과]
안녕, 인스턴스 메서드 
안녕, 인스턴스 메서드

  • 생성자 메서드 참조 예제
interface Messageable{
	Message getMessage(String msg);
}
class Message{
	Message(String msg){ 
		System.out.print(msg);
	} 
}
public class ConstructorReferenceEx1 { 
	public static void main(String[] args) {
		Messageable hello = Message::new;
		hello.getMessage("안녕"); 
	}
}

[결과] 
안녕

함수형 인터페이스의 사용

  • 함수형 인터페이스의 추상 메서드는 람다 표현식의 시그니처를 표현하며 추상 메서드 시그니처를 함수 디스크립터(function descriptor)라고 한다.
  • 함수 디스트립터 == 람다 표현식의 시그니처
  • 다양한 람다식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다.
  • 자바 API는 Comparable, Runnable, Callable 등의 다양한 함수형 인터페이스를 포함하여 java.util.function 패키지로 제공한다.
  • Predefined-Functional interface
    • 람다 및 메서드 참조를 사용하여 함수형 프로그래밍을 처리하기 위해 미리 정의된 함수형 인터페이스를 제공한다.
    • 사용자가 정의한 함수형 인터페이스를 사용할 수도 있다.
  • java.util.function 패키지에 정의된 함수형 인터페이스
  • 함수형 인터페이스의 타입 파라미터: T (첫번째 인자 Type) / U (두번째 인자 Type) / R (리턴타입)
종류추상 메서드 특징
Function인자 있고, 리턴값 있음, 주로 인자값을 연산하고 결과를 리턴
Consumer인자 있고, 리턴값 없음
Supplier인자 없고, 리턴값 있음
Operator인자 있고, 리턴값 있음. 주로 인자값을 연산하고 결과를 리턴
Predicate인자 있고, 리턴값은 boolean, 인자값을 조사하고 true/false를 리턴
  • Descriptor
    • Function: T → R
    • Consumer: T → void
    • Supplier: () → T
    • Operator: T → T
    • Predicate: T → boolean
			  //T          R
Function<BufferedReader, String> f = (BufferedReader b) -> {
	//구현부
};
			//T      U       R
BiFunction<String, String, String> func1 = (s1, s2) -> {
	String s3 = s1 + s2;
	return s3;
};
String result = func1.apply("Hello", "World");

  • Function<T, R>은 대표적인 예이다. T 형식의 객체를 R 형식의 객체로 변환할 때 사용한다.
    • 예) Function<Apple, Integer>는 사과의 무게 정보가 Integer 객체로 변환된다.
  • 람다식에서 Checked Exception이 발생할 경우 람다식 내부에서 try~catch로 처리해주지 않으면 컴파일 에러가 발생한다.
  • 아래 예제처럼 Checked Exception을 Runtime Exception으로 감싸서 throw하고 람다를 사용하는 곳에서 Runtime Exception을 핸들링한다.
Function<BufferedReader, String> f = (BufferedReader b) -> {
	try{
		return b.readLine();
	}catch(IOException e){
		throw new RuntimeException(e);
	}
}

Predicate 함수형 인터페이스

  • Function Predicate 인터페이스는 test 추상 메서드를 정의한다.
  • test 메서드는 제너릭 형식 T의 객체를 인수로 받아 boolean으로 리턴한다.
  • T 타입의 객체를 사용하는 boolean 표현식이 필요한 상황에서 Predicate 인터페이스를 사용하면 된다.
  • Argument type과 개수에 따라 인터페이스를 분류할 수 있다.
인터페이스 명추상 메서드설명
Predicate<T>boolean test(T t)객체 T를 조사
BiPredicate<T, U>boolean test(T t, U u)객체 T와 U를 비교 조사
DoublePredicateboolean test(double value)double 값을 조사
IntPredicateboolean test(int value)int 값을 조사
LongPredicateboolean test(long value)long 값을 조사
@FunctionalInterface
public interface Predicate<T>{
	boolean test(T t);
	...
}

  • Predicate를 구현
public <T> List<T> filter(List<T> list, Predicate<T> p) { 
	List<T> results = new ArrayList<>();
	for(T t: list){
		if(p.test(t)) { 
			results.add(t);
		} 
	}
	return results; 
}
  • 구현한 코드를 람다식으로 호출
Predicate<String> predicate = (String s) -> !s.isEmpty(); 
List<String> nonEmpty = filter(lists, predicate);

Consumer 함수형 인터페이스

  • Consumer<T> 인터페이스는 제네릭 타입 T 객체를 받고 리턴 타입이 없는 accept(T) 추상 메서드를 정의한다.
  • 예를 들어 Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있다.
  • Argument type과 개수에 따라 분류할 수 있다.
인터페이스 명추상 메서드설명
Consumer<T>void accept(T t)객체 T를 받아 소비
BiConsumer<T,U>void accept(T t, U u)객체 T와 U를 받아 소비
DoubleConsumervoid accept(double value)double 값을 받아 소비
IntConsumervoid accept(int value)int 값을 받아 소비
LongConsumervoid accept(long value)long 값을 받아 소비
ObjDoubleConsumer<T>void accept(T t, double value)객체 T와 double 값을 받아 소비
ObjIntConsumer<T>void accept(T t, int value)객체 T와 int 값을 받아 소비
ObjLongConsumer<T>void accept(T t, long value)객체 T와 long 값을 받아 소비
@FunctionalInterface
public interface Consumer<T> {
	void accept(T t);
	...
}

  • Consumer를 구현
public <T> void forEach(List<T> list, Consumer<T> c){
	for(T t:list){
		c.accept(t);
	}
}
  • Consumer의 accept 메서드를 구현한 람다식
forEach(
	Arrays.asList(1,2,3,4,5),
	(Integer i) -> System.out.println(i)
);

[결과]
1
2
3
4
5

Function 함수형 인터페이스

  • Function<T, R> 인터페이스는 제네릭 타입 T를 인수로 받아 R 객체로 리턴하는 추상 메서드 apply를 정의한다.
  • 입/출력을 매핑하는 람다식을 정의시 Function 인터페이스를 활용할 수 있다.
  • Argument type과 Return type에 따라 분류할 수 있다.
@FunctionalInterface
public interface Function<T, R>{
	R apply(T t);
	...
}
인터페이스명추상메서드설명
Function<T,R>R apply(T t)객체 T를 객체 R로 매핑
BiFunction<T,R>R apply(T t, U u)객체 T와 U를 객체 R로 매핑
DoubleFunction<R>R apply(double value)double을 객체 R로 매핑
IntFunction<R>R apply(int value)int를 객체 R로 매핑
IntToDoubleFunctiondouble applyAsDouble(int value)int를 double로 매핑
IntToLongFunctionlong applyAsLong(int value)int를 long으로 매핑
LongToDoubleFunctiondouble applyAsDouble(long value)long을 double로 매핑
LongToIntFunctionint applyAsInt(long value)long을 int로 매핑
ToDoubleBiFunction<T,U>double applyAsDouble(T t, U u)객체 T와 U를 double로 매핑
ToDoubleFunction<T>double applyAsDouble(T value)객체 T를 double로 매핑
ToIntBiFunction<T,U>int applyAsInt(T t, U u)객체 T와 U를 int로 매핑
ToIntFunction<T>Int applyAsInt(T value)객체 T를 int로 매핑
ToLongBiFunction<T,U>long applyAsLong(T t, U u)객체 T와 U를 long으로 매핑
ToLongFunction<T>long applyAsLong(T value)객체 T를 long으로 매핑

  • Function 인터페이스를 구현
public <T, R> List<R> map(List<T> list, Function<T, R> f){ 
	List<R> result = new ArrayList<>();
	for(T t: list){
		result.add(f.apply(t)); 
	}
	return result; 
}
  • Function의 apply 메서드를 구현한 람다식
List<Integer> resultlist = f.map( 
	Arrays.asList("람다", "홍길동", "함수형 인터페이스"), 
	(String s) -> s.length()
);

[결과] 
[2, 3, 9]

Operator 함수형 인터페이스

  • Argument와 Return 값이 모두 있는 추상 메서드를 가지며, 주로 Argument 값을 연산하고 그 결과를 리턴할 경우에 사용한다.

Argument 값 → Operator → 리턴 값

UnaryOperator<String> u = str -> str + " operator"; 
String result = u.apply("test"); // test operator
  • Argument와 개수에 따라 분류할 수 있다.
인터페이스명추상메서드설명
BinaryOperator<T>BiFunction<T,U,R>의 하위 인터페이스T와 U를 연산한 후 R 리턴
UnaryOperator<T>Function<T,R>의 하위 인터페이스T를 연산한 후 R 리턴
DoubleBinaryOperatordouble applyAsDouble(double, double)두 개의 double 연산
DoubleUnaryOperatordouble applyAsDouble(double)한 개의 double 연산
IntBinaryOperatorint applyAsInt(int, int)두 개의 int 연산
IntUnaryOperatorint applyAsInt(int)한 개의 int 연산
LongBinaryOperatorlong applyAsLong(long, long)두 개의 long 연산
LongUnaryOperatorlong applyAsLong(long)한 개의 long 연산

Supplier 함수형 인터페이스

  • Supplier<T> 인터페이스는 추상 메서드 get()을 람다의 Body 영역에 정의하며, 인수 없이 T 객체를 리턴한다.
@FunctionalInterface
public interface Supplier<T>{
	T get();
}
Supplier<String> s = () -> "supplier";
String result = s.get(); //supplier
  • 리턴 타입에 따라 분류할 수 있다.
인터페이스명추상메서드설명
Supplier<T>T get()객체를 리턴
BooleanSupplierboolean getAsBoolean()boolean 값을 리턴
DoubleSupplierdouble getAsDouble()double 값을 리턴
IntSupplierint getAsInt()int 값을 리턴
LongSupplierlong getAsLong()long 값을 리턴

함수형 인터페이스 예제

static <T> List<T> doSomething(Function<T, T> f, List<T> list) { 
	System.out.print("Function : ");
	List<T> newList = new ArrayList<T>(list.size());
	for(T i : list) {
		newList.add(f.apply(i));
	}
	return newList; 
}
static <T, R> List<R> doSomething2(Function<T, R> f, List<T> list) { 
	System.out.print("Function : ");
	List<R> newList = new ArrayList<R>(list.size());
	for(T i : list) {
		newList.add(f.apply(i)); // i/10
	}
	return newList; 
}
static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) { 
	System.out.print("Predicate, Consumer : [");
	for(T i : list) {
		// i%2==0
		if(p.test(i)) // boolean return (짝수 여부)
			c.accept(i); // T 객체 소비 System.out.print(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()); // T 객체 리턴
	}
}
public static void main(String[] args) {
	// Supplier<T> -> T get()
	Supplier<Integer> s = ()-> (int)(Math.random()*100)+1; // 랜덤 값
	// Consumer<T> -> void accept(T t)
	Consumer<Integer> c = i -> System.out.print(i+", ");
	// Predicate<T> -> boolean test(T t)
	Predicate<Integer> p = i -> i%2==0; // 짝수 구하기
	// Function<T, R> -> R apply(T t)
	Function<Integer, Integer> f = i -> i/10;
	// 문자열 표시
	Function<Integer, String> f2 = i -> "\"" + Integer.toString(i/10) + "\"";
	
	List<Integer> list = new ArrayList<>();
	makeRandomList(s, list); // Supplier 
	System.out.println("Supplier : " + list);
	printEvenNum(p, c, list); // Predicate, Consumer 
	List<Integer> newList = doSomething(f, list); // function 
	System.out.println(newList);
	List<String> newList2 = doSomething2(f2, list);
	System.out.println(newList2); 
}

[결과]
Supplier : [86, 10, 92, 60, 32, 29, 3, 44, 29, 94] 
Predicate, Consumer : [86, 10, 92, 60, 32, 44, 94, ] 
Function:[8,1,9,6,3,2,0,4,2,9]
Function : ["8", "1", "9", "6", "3", "2", "0", "4", "2", "9"]

스트림(Stream) 개요

  • 스트림(Stream)은 Java 8 부터 java.util.stream이라는 추가 패키지를 통해 제공되는 API로 요소(Element)를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자이며, 선언형(질의로 데이터를 처리)으로 컬렉션 데이터를 처리할 수 있다.
  • 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 병렬로 처리할 수 있다.
  • 제공 기능
    • 스트림은 요소를 저장하지 않는다. 컴퓨팅 작업의 파이프라인(pipeline)을 통해 데이터 구조, 배열 또는 I/O 채널과 같은 데이터 소스이 요소를 전달한다.
    • 스트림은 본질적으로 기능적이며, 스트림에서 수행되는 작업은 데이터 소스를 수정하지 않는다. 예를 들어 컬렉션에서 얻어온 스트림을 필터링하면 소스 컬렉션에서 요소를 제거하는 대신 필터링된 새 스트림이 생성된다.
    • 스트림의 요소는 스트림의 실행동안 한번만 접근된다. Iterator와 마찬가지로 소스의 동일한 요소를 다시 접근하려면 새 스트림을 생성해야 한다.
  • 스트림을 사용하여 하나의 데이터 구조에서 다른 데이터 구조로 filter, collect, print 및 변환할 수 있다.
//기존의 for문을 이용한 방식
int count = 0;
for(String w : words){
	if(w.length() > 12) count++;
}

//Stream을 이용한 방식
long count = words.stream()
		.filter(w -> w.length()>12)
		.count();

//병렬처리를 수행하는 Stream을 이용한 방식
long count = words.parallelStram()
		.filter(w -> w.length()>12)
		.count();
  • Java 8 이전 코드에서 Stream API를 이용한 코드를 비교해보면 간결하고 짧은 코드로 쉽게 구현할 수 있다.
  • 칼로리를 기준으로 요리를 정렬하는 자바 코드이다.
List<Dish> lowCaloricDishes = new ArrayList<>(); 
for(Dish d: dishes){
	if(d.getCalories() > 400){ 
		lowCaloricDishes.add(d);
	} 
}
List<String> lowCaloricDishesName = new ArrayList<>(); 
Collections.sort(lowCaloricDishes, new Comparator<Dish>() { //익명클래스compare로정렬
	public int compare(Dish d1, Dish d2){
		return Integer.compare(d1.getCalories(), d2.getCalories());
	} 
});
for(Dish d: lowCaloricDishes){ 
	lowCaloricDishesName.add(d.getName());
}
List<String> lowCaloricDishesName = 
	dishes.stream()
				.filter(d -> d.getCalories() > 400) 
				.sorted(comparing(Dish::getCalories)) 
				.map(Dish::getName) 
				.collect(toList());
  • filet, sorted, map, collect와 같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다.
  • fileter 메서드의 결과 → sorted 메서드 결과 → map 메서드 결과 → collection으로 연결되어 수행

스트림 필터링 수집 예

  • 스트림을 사용하여 데이터를 필터링 할 수 있다. 코드가 최적화되고 빠른 실행을 제공한다.
public class StreamFilterCollectEx1 { 
	public static void main(String[] args) {

		List<LambdaProduct> list = new ArrayList<LambdaProduct>();

		list.add(new LambdaProduct(1,"삼성",17000)); 
		list.add(new LambdaProduct(3,"아이폰",65000)); 
		list.add(new LambdaProduct(2,"소니",25000)); 
		list.add(new LambdaProduct(4,"노키아",15000)); 
		list.add(new LambdaProduct(6,"레노바",19000));;

		List<Integer> productPriceList = list.stream() 
						.filter(p -> p.price > 20000) // 데이터 필터링 
						.map(p -> p.price) // 가격 얻어오기 
						.collect(Collectors.toList());// collecting list
		System.out.println(productPriceList); 
	}
}

[결과]
[65000, 25000]

스트림 컬렉션의 reduce() 메서드

  • 일련의 입력 요소를 가져와 반복 작업을 통해 단일 요약 결과를 결합할 수 있다. 예를 들면 숫자의 합을 찾거나 목록에서 요소를 누적시킨다.
public class StreamReduceEx01 {
	public static void main(String[] args) {
		List<LambdaProduct> productsList = new ArrayList<>(); 
		productsList.add(new LambdaProduct(1,"삼성",17000)); 
		productsList.add(new LambdaProduct(3,"아이폰",65000)); 
		productsList.add(new LambdaProduct(2,"소니",25000)); 
		productsList.add(new LambdaProduct(4,"노키아",15000)); 
		productsList.add(new LambdaProduct(6,"레노바",19000));
		
		// 데이터 필터링의 간결한 접근 방식
		int totalPrice = productsList.stream()
						.map(product->product.price)
						.reduce(0,(sum, price)-> sum + price); // 누적 가격 
		System.out.println(totalPrice);
	}
}

[결과] 
141000

List를 Map으로 변환

  • 예제
public class StreamConverListToMapEx01{ 
	public static void main(String[] args) {
		List<LambdaProduct> productsList = new ArrayList<>(); 
		productsList.add(new LambdaProduct(1,"삼성",17000)); 
		productsList.add(new LambdaProduct(3,"아이폰",65000)); 
		productsList.add(new LambdaProduct(2,"소니",25000)); 
		productsList.add(new LambdaProduct(4,"노키아",15000)); 
		productsList.add(new LambdaProduct(6,"레노바",19000));

		// Product List를 Map으로 변환 
		Map<Integer,String> productPriceMap =
				productsList.stream() 
							.collect(Collectors.toMap(p->p.id, p->p.name));
		System.out.println(productPriceMap); 
	}
}

[결과]
{1=삼성, 2=소니, 3=아이폰, 4=노키아, 6=레노바}

리덕션과 파이프라인

  • 리덕션(Reduction)은 대량의 데이터를 가공해 축소 하는 것이며 데이터의 합계, 평균값, 카운팅, 최대값, 최소값을 구한다.
  • 파이프라인(Pipeline)
    • 여러 개의 스트림이 연결되어 있는 구조이다.
    • 파이프라인에서 최종 처리를 제외하고 모두 중간 처리 스트림이다.
  • 스트림의 처리는 중간 처리와 최종 처리로 구분된다.
    • 중간처리: 매핑, 필터링, 소팅을 수행
    • 최종처리: 반복, 카운팅, 평균, 총합 등의 집계 처리를 수행

스트림 특징

  • 스트림은 Iterator와 비슷한 역할의 반복자이다.
  • 스트림 API는 메서드들을 연경하여, 복잡한 연산을 처리하는 로직을 쉽고, 유연하게 작성할 수 있게 도와준다.
  • 여러 연산을 파이프라인으로 연결해도 가독성이 유지되며 filter 같은 연산은 고수준 빌딩 블록으로 이루어져 있으므로 특정 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서든 사용할 수 있어 (멀티코어 아키텍처를 최대한 투명하게 활용할 수 있게 구현되어 있다) 데이터 처리 과정을 병렬화하면 스레드와 락에 대해 걱정할 필요가 없다.
//순차 필터링
long count = values.stream().filter().count();
//병렬 필터링
long count1 = values.parallelStream().filter().count();

1612104
순차 필터링 소요 시간: 168 ms

1612104
병렬 필터링 소요 시간: 36 ms

  • 스트림의 중간 연산
연산형식반환 형식
filter중간 연산Stream<T>
map중간 연산Stream<R>
limit중간 연산Stream<T>
sorted중간 연산Stream<T>
distinct중간 연산Stream<T>

  • 스트림의 최종 연산
연산형식반환 형식목적
forEach최종 연산void스트림의 각 요소를 소비하면서 람다를 적용한다.
count최종 연산long (generic)스트림의 요소 개수를 반환한다.
collect최종 연산스트림을 리듀스에서 리스트, 맵, 합수 형식의 컬렉션을 만든다.

스트림 종류

  • java.util.stream 패키지의 API로 BaseStream 인터페이스에 공통 메서드가 정의되어 있다.
  • Stream은 객체 요소를 처리하고 IntStream, DoubleStream 등은 기본형인 int, long, double 요소를 처리할 때 이용한다.
  • Stream은 컬렉션과 배열에서 주로 얻어낸다.
리턴 타입메서드(매개 변수)소스
Stream<T>java.util.Collection.stream(
java.util.Collection.parallelStream()
Stream<T>
IntStream
LongStream
DoubleStream
Arrays.stream(T[ ]), Stream.of(T[ ])
Arrays.stream(int[ ]), IntStream.of(int[ ])
Arrays.stream(long[ ]),
LongStream.of(long[ ])
Arrays.stream(double[ ]),
DoubleStream.of(double[ ])
배열
IntStreamIntStream.range(int, int)
IntStream.rangeClosed(int, int)int 범위
LongStreamLongStream.range(long, long)
LongStream.rangeClosed(long, long)
long 범위
Stream<Path>Files.find(Path, int, BiPredicate, FileVisitOption)
Files.list(Path)
디렉토리
Stream<String>Files.lines(Path, Charset)
BufferedReader.lines()
파일
DoubleStream
IntStream
LongStream
Random.doubles(…)
Random.ints()
Random.longs()
랜덤 수
  • 다양한 방식의 스트림 생성 방법을 제공한다.
  • Collection: 콜렉션객체.stream(), parallelStream()
  • Files: Stream<String> Files.lines()
  • Arrays: Arrays.stream(*)
  • Stream.of(*)

컬렉션과 스트림

  • 컬렉션과 스트림은 모두 연속된 요소 형식의 값을 저장하는 자료 구조의 인터페이스를 제공한다.
  • 둘 다 순서에 따라 순차적으로 요소에 접근한다.
  • 컬렉션
    • 각 계산식을 만날 때마다 데이터가 계산되며, 자료 구조이므로 데이터에 접근, 변경, 저장 같은 연산이 주 기능이다.
    • 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조로, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 하고 메모리 사용량이 늘어난다.
  • 스트림
    • 최종 연산이 실행될 때에 데이터가 계산되며, filter, sorted, map처럼 계산식(람다)를 표현하는 것이 주요 관심사로 계산을 JVM에 위임한다.
    • 요청할 때만 요소를 계산하는 고정된 자료구조로 스트림에 요소를 추가하거나 제거할 수 없다.
    • 스트림의 탐색된 요소는 소비되며, 한 번 탐색한 요소를 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다.
  • 데이터 계산이 언제냐에 따라 컬렉션과 스트림으로 나뉘는 가장 큰 차이가 있다.

외부 반복과 내부 반복

  • 외부 반복(External Iterator): 개발자가 직접 컬렉션 요소를 반복해서 처리하는 방식
  • 내부 반복(Internal Iterator): 컬렉션 내부에서 요소를 반복시키고, 개발자는 요소별로 처리해야 할 코드만 제공하는 방식

  • 외부 반복 코드를 스트림을 이용한 내부 반복 코드로 변경
List<String> highCaloricDish = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()){
	Dish dish = iterator.next();
	if(dish.getCalories() > 300){
		highCaloricDish.add(dish.getName());
	}
}
List<String> highCaloricDish =
		menu.stream()
				.flter(d -> d.getCalories() > 300)
				.collect(toList());

스트림의 연산

  • 연결할 수 있는 스트림 연산을 중간 연산(intermediate operation)
  • 스트림을 닫는 연산을 최종 연산(terminal operation)
  • filter나 sorted와 같은 중간 연산은 다른 스트림을 반환하기 때문에 여러 중간 연산을 연결해서 질의를 만들 수 있다.
List<String> names =
				menu.stream() //객체 리스트에서 스트림 얻기
						.filter(dish -> dish.getCalories() > 300)
						.map(Dish::getName) //중간 연산
						.limit(3) //중간 연산
						.collect(toList()); //스트림을 리스트로 변환

Filter

  • 중간 연산으로 Predicate(boolean을 반환하는 함수)를 인수로 받아서 Predicate와 일치하는 모든 요소를 포함한 스트림을 반환한다.
  • Predicate는 함수형 인터페이스라서 람다식을 전달할 수 있다.
  • 아래 예와 같이 채식 요리인지 확인하는 메서드 참조를 통해 필터링해서 메뉴를 만들 수 있다.
  • Stream filter() method signature
Stream<T> filter(Predicate<? super T> predicate);

List<Dish> vegetarianMenu =
				menu.stream()
						.filter(Dish::isVegetarian)
						.collect(toList());

distinct

  • 중간 연산으로 고유한 유일 값을 반환하며 유일한 값인지는 스트림에서 만든 객체의 hashCode, equals로 결정된다.
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 2, 4);
				numbers.stream()
						.filter(i -> i%2 == 0)
						.distinct()
						.forEach(System.out::println);

limit

  • 중간 연산으로 지정된 값 이하의 크기를 갖는 새로운 스트림을 반환한다.
  • 아래 예제는 filter predicate와 일치하는 처음 3개 요소를 반환한다.
List<Dish> dishes =
	specialMenu.stream()
						.filter(dish -> dish.getCalories() > 300)
						.limit(3)
						.collect(toList());

map

  • 중간 연산으로 특정 데이터를 선택하는 기능을 제공하며, 스트림은 함수를 인수로 받는 map 메서드를 지원한다.
  • 함수를 적용한 결과가 새로운 요소로 매핑되는데 기존의 값이 아닌 새로운 버전을 만드는 매핑 과정과 같다.
  • 예를 들면 아래의 예제에서 getName은 문자열을 반환하므로 map 메서드의 스트림은 Stream<String> 타입이 된다.
  • 요리명의 길이를 추출하고 싶은 경우에는 아래 예제처럼 map 메서드를 chaining할 수 있다.
List<String> dishNames = menu.stream()
						.map(Dish::getName)
						.collect(toList());

List<Integer> dishNames = menu.stream()
						.map(Dish::getName)
						.map(String::length)
						.collect(toList());

flatMap

  • map 메서드를 리스트에서 고유 문자로 이루어진 리스트로 반환하는 기능을 개발할 때 사용한다.
  • {”Hello”, “World”] 리스트가 있다면 결과로 [”H”, “e”, “l”, “o”, “W”, “r”, “d”] 리스트가 반환된다.
  • 우리가 원하는 반환 스트림은 배열이 아닌 Stream<String> 타입이다.
  • map을 이용하면 배열 형태가 되는데 이런 중첩 구조를 한 단계 제거하기 위한 중간 연산이 필요하고, 이것이 flatmap이다.
  • flatMap은 스트림 평면화라는 이름으로도 불리며 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉 map(Arrays::Stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다. (중복된 스트림을 1차원으로 평면화)
  • 스트림의 각 값을 다른 스트림으로 생성 ⇒ 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행
// [[a], [b]]
List<List<String>> list = Arrays.asList(Arrays.asList("a"), Arrays.asList("b"));

// [a, b]
List<String> flatList = list.stream() 
					.flatMap(Collection::stream)
          .collect(Collectors.toList());
  • flatMap을 이용한 단어 리스트의 고유 문자 찾기 기능 구현
  • “Hello”와 “World”가 split 메서드에 의해 분리되고, map에 의해 [”H”,”e”,”l”,”l”,”o”]와 [”W”,”o”,”r”,”l”,”d”]로 변환된다.
  • Arrays:stream (Arrays.stream(T[] array))을 사용해 [”H”,”e”,”l”,”l”,”o”]와 [”W”,”o”,”r”,”l”,”d”]를 각각 Stream<String>으로 변환한다.
  • flatMap()을 사용하여 여러 개의 Stream<String>을 1개의 Stream<String>으로 평면화하여 ”H”,”e”,”l”,”l”,”o”,”W”,”o”,”r”,”l”,”d”]가 된다.
  • distinct()로 중복된 소스(”l”,”o”)가 제거된 후 [”H”,”e”,”l”,”o”,”W”,”r”,”d”]가 collect(toList())로 수집된다.
  • flatMap(Arrays::stream)은 String 리스트를 스트림으로 변환하는 것이 아닌 String을 감싸는 구성요소를 만들어주는 것이다.
List<String> words = Arrays.asList("Hello", "World");
List<String> uniqueCharacters =
			words.stream()
					.map(word -> word.split("")) //단어를 개별 문자 배열로 반환
					.flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화
					.distinct()
					.collect(toList());

[결과]
[H, e, l, o, W, r, d]

anyMatch

  • 최종 연산으로 Predicate가 적어도 한 요소와 일치하는지 확인한다.
  • 아래 예제는 menu에 채식 메뉴가 있는지 확인하는 예이다.
if(menu.stream().anyMatch(Dish:isVegetarian)){
	System.out.println("Vegetarian menu");
}

allMatch

  • 최종 연산으로 Predicate가 모든 요소와 일치하는지 확인한다.
  • 아래 예제는 모든 메뉴가 1000칼로리 이하인지 확인한다.
boolean isHealthy = menu.stream()
	.allMatch(dish -> dish.getCalories() < 1000);

noneMatch

  • 최종 연산으로 Predicate가 모든 요소와 불일치하는지 확인한다.
  • 아래 예제는 모든 메뉴가 1000칼로리 이하가 아닌지 확인한다.
boolean isHealthy = menu.stream()
	.noneMatch(dish -> dish.getCalories() >= 1000);

reduce

  • 최종 연산으로 모든 스트림 요소를 처리해서 값으로 반환하는 연산을 리듀싱 연산이라고 한다.
  • 리듀스 연산을 이용해서 “메뉴의 모든 칼로리의 합계를 구하기”와 같은 복잡한 질의를 처리할 수 있다.
  • 아래 연산 과정을 보면 스트림이 하나의 값으로 줄어들 때까지 람다는 각 요소를 반복해서 조합한다.

  • reduct를 이용해서 최대값, 최소값을 구할 수 있다.
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
profile
웹퍼블리셔의 백엔드 개발자 도전기

0개의 댓글