(101)

람다식(Lambda)

[ 람다함수 ] 프로그래밍 언어에서 사용되는 개념으로 익명 함수(Anonymous functions)를 지칭하는 용어 익명 함수들은 어느 언어에서나 일급 객체라는 특징을 가짐 일급 객체 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체 를 가리킨다. 보통 함수에 인자로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다. [ 특징 ] 람다 대수는 이름을 가질 필요가 없고 두 개 이상의 입력이 있는 함수는 1개의 입력만 받는 람다 대수로 단순화 될 수 있다 - 커링 람다 실행블록에는 클래스의 필드와 메서드를 제약없이 사용 가능하다 람다식 내에서 사용되는 지역변수는 final이 없어도 상수로 간주된다 람다식으로 선언된 변수명은 다른 변수명과 중복될 수 없다 람다 대..

향상된 for문

조건 JDK 1.5 이상 사용 가능 사용할 변수는 지역변수로 인식된다 대상은 배열이거나 여러 원소를 포함한 자료형이어야 한다 → 배열은 향상된 for문을 입력해도 컴파일러가 기존 for문으로 변환시킨다 사용하기 for(자료형 변수명 : 배열명){ 문장 } int[] arr = {1,2,3,4,5,6,7,8,9}; for(int i : arr){ System.out.print(i); } // console // 123456789 // 다차원 배열도 활용가능 int[][] arr = {{1,2},{3,4},{5,6},{7,8}}; for(int[]arr2 : arr) { for(int i : arr2) { System.out.print(i); } } // Console // 12345678 예제 class ..

스택(Stack)

후입선출(LIFO - Last In First Out)의 구조이다 ex) 음료수 진열대 사용하기 import java.util.Stack; Stack stack = new Stack(); // 값 추가하기 stack.push(1); stack.push(2); stack.push(3); // 맨 위에 있는 데이터(top)를 가져옴(제거) stack.pop(); // top 반환 stack.peek(); // 초기화 stack.clear(); // 그 외 메서드들 stack.size(); // stack의 크기 출력 stack.empty(); // stack이 비어있는지 stack.contains() // stack에 포함여부 stack.search(1) // 1이 있는 인덱스를 반환 참조 https://c..

우선순위 큐(Priority Queue)

Priority Queue 기본 큐의 구조(FIFO : First In First Out)를 가지면서 데이터의 우선순위를 정해 우선순위가 높은 순서대로 나간다 우선순위 힙을 기반으로 구현된다 사용되는 생성자에 따라 자연 순서에 따라 또는 큐 생성 시 제공되는 Comparator에 따라 순서가 지정된다 데이터를 삽입할 때 우선순위의 최대, 최소를 구성하여 데이터가 빠지면 중간을 계속해서 채워넣는 방식 특징 높은 우선순위의 요소를 먼저 꺼내서 처리하는 구조이다 비교할 수 없는 객체는 큐를 만들 수 없다 (비교 가능한 기준이 있어야한다) 우선순위 큐는 값을 비교해야하므로 null을 허용지 않는다 내부구조는 이진트리 힙으로 구성되어있다 (내부구조가 힙으로 구성되어 있기에 시간 복잡도는 O(NLogN)이다) Ab..

객체지향 설계 원칙 - SOLID

[ 객체 지향 설계 원칙 ( SOLID ) ] 컴퓨터 프로그래밍에서 SOLID란 로버트 C. 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다. 프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다. SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다. 이 원칙들은 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적 전략의 일부다. SOLID 원칙들은 결국 자기 자신 클래스 안에 응집도는 내부적으로 높이고, 타 클래스들 간..

이차원 배열의 정렬 - Arrays.sort(arr, Comparator) / JAVA

1차원배열의 정렬import java.util.Arrays;// 오름차순Arrays.sort(arr);// 내림차순 기본타입에 선언 불가!! 참조타입에 가능Arrays.sort(arr, Collections.reverseOrder());//참조타입 변환(ex - int[])Integer[] refArr = Arrays.stream(arr).boxed().toArray(Integer[]::new);//변환 후 정렬Arrays.sort(refArr, Collections.reverseOrder());2차원배열의 정렬import java.util.Arrays;import java.util.Comparator;// Comparator는 인터페이스이기때문에 오버라이딩Arrays.sort(arr, new Compa..

Stream(2)

동작 순서 다음 스트림에서는 최종 작업인 findFirst 메소드를 호출합니다. 과연 출력 결과는 어떨까요? list.stream() .filter(el -> { System.out.println("filter() was called."); return el.contains("a"); }) .map(el -> { System.out.println("map() was called."); return el.toUpperCase(); }) .findFirst(); 요소는 3개인데 결과는 다음처럼 filter 두 번, map 이 한 번 출력됩니다. filter() was called. filter() was called. map() was called. 여기서 스트림이 동작하는 순서를 알아낼 수 있습니다. 모든..

Stream(1)

스트림 Streams 자바 8에서 추가한 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나입니다. 자바 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for 또는 foreach문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이었습니다. 간단한 경우라면 상관없지만 로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고, 메소드를 나눌 경우 루프를 여러 번 도는 경우가 발생합니다. 스트림은 '데이터의 흐름’입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다. 또 하나의 장점은 간단하게 병렬처..

람다식(Lambda)

Tech/Java & Spring 2022. 12. 22. 09:43
728x90
728x90

[  람다함수  ]


프로그래밍 언어에서 사용되는 개념으로 익명 함수(Anonymous functions)를 지칭하는 용어

익명 함수들은 어느 언어에서나 일급 객체라는 특징을 가짐

 

일급 객체
다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체
를 가리킨다. 보통 함수에 인자로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다.

 

 

[  특징  ]


람다 대수는 이름을 가질 필요가 없고

두 개 이상의 입력이 있는 함수는 1개의 입력만 받는 람다 대수로 단순화 될 수 있다 - 커링

 

람다 실행블록에는 클래스의 필드와 메서드를 제약없이 사용 가능하다

람다식 내에서 사용되는 지역변수는 final이 없어도 상수로 간주된다

람다식으로 선언된 변수명은 다른 변수명과 중복될 수 없다

 

람다 대수
추상화와 함수 적용 등의 논리 연산을 다루는 형식 체계로 함수를 보다 단순하게 표현하는 방법이다

 

커링
f(a,b,c) → f(a)(b)(c)
다중 인수를 갖는 함수를 단일 인수를 갖는 함수들의 함수열로 바꾸는 것을 말한다.단일 호출로 처리하는 함수를 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 것이다

 

 

[  장점  ]


코드의 간결함

가독성의 향상 : 개발자의 의도가 명확히 드러난다

생산성의 향상 : 함수를 만드는 과정없이 한번에 처리할 수 있다

병렬처리에 용이 : 멀티쓰레드 활용

 

 

[  단점  ]


호출의 까다로움

불필요한 람다의 남발 시 가독성이 떨어진다

디버깅이 어렵다

람다 stream 사용 시 단순 for문 혹은 while문 사용 시 성능이 떨어진다

 

 

[  사용하기  ]


매개변수 -> 함수 몸체

int a(int x, int y) {
    return x < y ? x : y;
}

(x, y) -> x < y ? x : y;
//함수 몸체가 단일 실행문이면 중괄호{} 생략 가능
//return문으로만 구성되어 있으면 생략 불가능
(int x) -> x+1
(x) -> x+1
x -> x+1
(int x) -> { return x+1; }
x -> { return x+1; }

//기존 자바 문법
new Thread(new Runnable() {
    public void run() {
        System.out.println("전통적인 방식의 일회용 스레드 생성");
    }
}).start();

 
//람다식 문법
new Thread(()->{
    System.out.println("람다 표현식을 사용한 일회용 스레드 생성");
}).start();

 

 

[  함수형 인터페이스( Functional Interface )  ]


Java는 기본적으로 객체지향 언어이기 때문에 순수 함수와 일반 함수를 다르게 취급하고 있으며 Java에서는 이를 구분하기 위해 함수형 인터페이스가 등장하게 되었다

 

함수형 인터페이스란 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다. 함수형 인터페이스를 사용하는 이유는 Java의 람다식이 함수형 인터페이스를 반환하기 때문이다

 

하지만 함수형 인터페이스의 등장으로 우리는 함수를 변수처럼 선언할 수 있게 되었다

함수형 인터페이스를 구현하기 위해서는 인터페이스를 개발하여 그 내부에는 1개 뿐인 abstract 함수를 선언하고, 위에는 @FunctionalInterface 어노테이션을 붙여주면 된다

 

람다식으로 생성된 순수 함수는 함수형 인터페이스로만 선언이 가능하다. 또한 @FunctionalInterface는 해당 인터페이스가 1개의 함수만을 갖도록 제한하기 때문에, 여러 개의 함수를 선언하면 컴파일 에러가 발생한다 

@FunctionalInterface
public interface LambInter {
	void test(int a);	
}
public class Lambda {

	public static void main(String[] args) {
		
		LambInter lam = null;
		
		lam = (a) ->{
			int result = a*30;
			System.out.println("람다" + result);
		};		
		lam.test(30);
	}
}

//Console
//람다900

 

[  JAVA에서 제공하는 함수형 인터페이스  ]


자바8부터는 빈번하게 사용되는 함수형 인터페이스(Functional Interface)는 java.util.function 표준 API 패키지로 제공된다

이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서이다 

  • Consumer
  • Supplier
  • Function
  • Operator
  • Predicate

 

1. Consumer

파라미터만 있고 리턴값이 없는 추상 메서드를 가진다

추상메서드 accept를 호출하여 사용

 

인터페이스 추상 메서드 설명
Consumer<T> void accept(T t) 객체 T를 받아 소비
BiConsumer<T, U> void accept(T t, U u) 객체 T와 U를 받아 소비
DoubleConsumer void accept(double value) double 값을 받아 소비
IntConsumer void accept(int value) int 값을 받아 소비
LongConsumer void 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 값을 받아 소비

 

import java.util.function.Consumer;
import java.util.function.BiConsumer;
import java.util.function.DoubleConsumer;
import java.util.function.ObjIntConsumer;

public class Lambda {	

	public static void main(String[] args) {	
    
		Consumer<String> consumer = (t) ->{
			System.out.println("디파일러 컨슘" + t);
		};		
		consumer.accept("ㅇㅇㅇㅇ");;		
		
		BiConsumer<String, String> bi = (a,b) -> {
			System.out.println("ㅋㅋㅋㅋ" + a + "ㅋㅋㅋㅋ" + b);
		};
		bi.accept("어제 길을 가는데", "누가 넘어짐");		
		
		DoubleConsumer dc = (dd) ->{
			System.out.println("내가 지금 사용하는 자바 버전은? " + dd);
		};
		dc.accept(1.8);
        
        	ObjIntConsumer<String> obc = (i, j) ->{
			System.out.println(i + j);
		};
		obc.accept("자바", 8);
	}
}
//Console
디파일러 컨슘 지이이잉
ㅋㅋㅋㅋ 어제 길을 가는데 ㅋㅋㅋㅋ 누가 넘어짐
내가 지금 사용하는 자바 버전은? 1.8
자바8

 

 

2. Supplier

파라미터는 없고 리턴값만 있다

get....() 메서드를 호출하여 실행한 후 호출한 곳으로 데이터를 리턴한다

 

인터페이스 추상 메서드  설명
Supplier<T> T get() T 객체를 리턴
BooleanSupplier Boolean getAsBoolean() Boolean 값을 리턴
DoubleSupplier double getAsDouble() double 값을 리턴
IntSupplier int getAsInt() int 값을 리턴
LongSupplier long getAsLong() long 값을 리턴

 

import java.util.function.Supplier;
import java.util.function.IntSupplier;

public class Lambda {

	public static void main(String[] args) {

		Supplier<String> supplier = () ->{
			String study = " 나는 오늘도 열심히 자바 공부를 한다";
			return study;
		};
		System.out.println(supplier.get());
				
		IntSupplier lotto = () ->{
			int num = (int)(Math.random()*45)+1;
			return num;		
		};
        
	}
       
}
//Console
나는 오늘도 열심히 자바 공부를 한다
로또 가즈아 : 9

 

 

 

3. Function

매개값과 리턴값이 있는 apply....()메서드를 사용

매개값을 리턴으로 매핑한다

 

인터페이스 추상 메서드 설명
Function<T, R> R apply(T t) 객체 T를 객체 R로 매핑
BiFunction<T, U, 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로 매핑
IntToDoubleFunction double applyAsDouble(int value) int를 double로 매핑
IntToLongFunction long applyAsLong(int value) int를 long으로 매핑
LongToDoubleFunction double applyAsDouble(long value) long을 double로 매핑
LongToIntFunction int 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 t) 객체 T를 int로 매핑
ToLongBiFunction<T, U> long applyAsLong(T t, U u) 객체 T와 U를 long으로 매핑
ToLongFunction<T> long applyAsLong(T t) 객체 T를 long으로 매핑

 

Function<Student, String> function = t -> {
	return t.getName();
};


Function<Student, String> function = t -> t.getName();

 

 

 

4. Operator

파라미터, 리턴값이 모두 있는 추상 메서드 apply....()를 가지고 있다

파라미터 값을 연산하고 그 결과를 리턴 시 사용할 수 있다

 

인터페이스  추상 메서드  설명
BinaryOperator<T> BiFunction<T,U,R>의 하위 인터페이스 T와 U를 연산 후 R 리턴
UnaryOperator<T> Function<T, R>의 하위 인터페이스 T를 연산한 후 R 리턴
DoubleBinaryOperator double applyAsDouble(double, double) 두 개의 double을 연산
DoubleUnaryOperator double applyAsDouble(double) 한 개의 double을 연산
IntBinaryOperator int applyAsInt(int, int) 두 개의 int를 연산
IntUnaryOperator int applyAsInt(int) 한 개의 int를 연산
LongBinaryOperator long applyAsLong(long, long) 두 개의 long을 연산
LongUnarayOperator long applyAsLong(long) 한 개의 long을 연산

 

import java.util.function.BinaryOperator;

public class Lambda {
	
	static int[] arr = {13, 15, 100, 1, 37, 76};

	public static void main(String[] args) {

                IntBinaryOperator operMin = (a, b) ->{
                return Math.min(a, b);
		};
		int min = maxOrMin(operMin);
		
		IntBinaryOperator operMax = (a, b) ->{
			return Math.max(a, b);
		};        
		int max = maxOrMin(operMax);
		
		System.out.printf("최소값 : %d, 최대값 : %d", min, max);		
		
	}
	
	public static int maxOrMin(IntBinaryOperator oper){
		int result = arr[0];
		for(int num : arr) {
			result = oper.applyAsInt(result, num);
		}
		return result;
	}
    
}
//Console
최소값 : 1, 최소값 : 100

 

 

5.Predicate

파라미터를 조사하여 truse / false값을 리턴하는 test....()메서드를 가지고 있다

 

인터페이스 추상 메서드 설명
Predicate<T> Boolean test(T t) 객체 T를 조사
BiPredicate<T, U> Boolean test(T t, U u) 객체 T와 U를 비교 조사
DoublePredicate Boolean test(double value) double 값을 조사
IntPredicate Boolean test(int value) int 값을 조사
LongPredicate Boolean test(long value) long 값을 조사

 

더보기
package java1222;

public class Ourclass {
	
	private String name;
	private String gender;
	private int score;
	
	public Ourclass(String name, String gender, int score) {
		this.name = name;
		this.gender = gender;
		this.score = score;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

	public int getScore() {
		return score;
	}

	public void setScore(int score) {
		this.score = score;
	}
	
	
	
	
	
}

 

import java.util.ArrayList;
import java.util.function.Predicate;

public class PredicateExam {
	
	public static ArrayList<Ourclass> arrayList = new ArrayList<>();

	public static void main(String[] args) {
		
		arrayList.add(new Ourclass("김범수", "남자", 30));
		arrayList.add(new Ourclass("나얼", "남자", 50));
		arrayList.add(new Ourclass("박효신", "남자", 60));
		arrayList.add(new Ourclass("이수", "남자", 55));
		arrayList.add(new Ourclass("아이유", "여자", 42));
		arrayList.add(new Ourclass("김연우", "남자", 30));
		arrayList.add(new Ourclass("박화요비", "여자", 25));
		arrayList.add(new Ourclass("이선희", "여자", 20));
		arrayList.add(new Ourclass("이승기", "남자", 95));
		arrayList.add(new Ourclass("허각", "남자", 80));
		
		
		Predicate<Ourclass> female = t ->{
			System.out.print(t.getName());
			System.out.println(t.getScore());
			return t.getGender().equals("여자");
		};
		
		Predicate<Ourclass> male = t ->{
			System.out.print(t.getName());
			System.out.println(t.getScore());
			return t.getGender().equals("남자");
		};
		
		double fe = avg(female);
		System.out.println("여성 평균 : " + fe);
		
		double ma = avg(male);
		System.out.println("남성 평균 : " + ma);
		
		
	}
	
	public static double avg(Predicate<Ourclass> score){
		int count = 0;
		int sum = 0;
		
		for(Ourclass clsMate : arrayList) {
			if(score.test(clsMate)) {
				count++;
				sum += clsMate.getScore();
			}
		}
		return (double)sum/count;		
	}

}
//Console
여성 평균 : 29.0
남성 평균 : 57.142857142857146

 

 

 

참조

https://palpit.tistory.com/673
https://mangkyu.tistory.com/113
https://khj93.tistory.com/entry/JAVA-%EB%9E%8C%EB%8B%A4%EC%8B%9DRambda%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%82%AC%EC%9A%A9%EB%B2%95

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

향상된 for문

Tech/Java & Spring 2022. 12. 21. 23:07
728x90
728x90

조건


  • JDK 1.5 이상 사용 가능
  • 사용할 변수는 지역변수로 인식된다
  • 대상은 배열이거나 여러 원소를 포함한 자료형이어야 한다
    → 배열은 향상된 for문을 입력해도 컴파일러가 기존 for문으로 변환시킨다

 

 

사용하기


for(자료형 변수명 : 배열명){
	문장
}

int[] arr = {1,2,3,4,5,6,7,8,9};

for(int i : arr){
	System.out.print(i);
}

// console
// 123456789

// 다차원 배열도 활용가능

int[][] arr = {{1,2},{3,4},{5,6},{7,8}};

for(int[]arr2 : arr) {
	for(int i : arr2) {
		System.out.print(i);
	}
}

// Console
// 12345678

 

 

예제


class Solution{
    public String solution(String s) {
            String answer = "";
            String[] arr = s.toLowerCase().split("");
            boolean bl = true;

            for(String str : arr){
                answer += bl ? str.toUpperCase() : str;
                bl = str.equals(" ") ? true : false;
            }
            return answer;
    }
}

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

스택(Stack)

Tech/Java & Spring 2022. 12. 21. 09:33
728x90
728x90

후입선출(LIFO - Last In First Out)의 구조이다

  • ex) 음료수 진열대

동작원리 (출처 : https://coding-factory.tistory.com/601)

 

 

 

사용하기


import java.util.Stack;

Stack<Integer> stack = new Stack<>();

// 값 추가하기
stack.push(1);
stack.push(2);
stack.push(3);

출처 :&nbsp;https://coding-factory.tistory.com/601

 


 

// 맨 위에 있는 데이터(top)를 가져옴(제거)
stack.pop();

// top 반환
stack.peek();

// 초기화
stack.clear();

 

출처 :&nbsp;https://coding-factory.tistory.com/601

 


 

// 그 외 메서드들

stack.size();      // stack의 크기 출력
stack.empty();     // stack이 비어있는지
stack.contains()   // stack에 포함여부
stack.search(1)    // 1이 있는 인덱스를 반환

 


 

 

참조

https://coding-factory.tistory.com/601

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

우선순위 큐(Priority Queue)

Tech/Java & Spring 2022. 12. 20. 23:28
728x90
728x90

Priority Queue


  • 기본 큐의 구조(FIFO : First In First Out)를 가지면서 데이터의 우선순위를 정해 우선순위가 높은 순서대로 나간다
  • 우선순위 힙을 기반으로 구현된다
  • 사용되는 생성자에 따라 자연 순서에 따라 또는 큐 생성 시 제공되는 Comparator에 따라 순서가 지정된다
  • 데이터를 삽입할 때 우선순위의 최대, 최소를 구성하여 데이터가 빠지면 중간을 계속해서 채워넣는 방식

 

 

특징


  • 높은 우선순위의 요소를 먼저 꺼내서 처리하는 구조이다
  • 비교할 수 없는 객체는 큐를 만들 수 없다 (비교 가능한 기준이 있어야한다)
  • 우선순위 큐는 값을 비교해야하므로 null을 허용지 않는다
  • 내부구조는 이진트리 힙으로 구성되어있다 (내부구조가 힙으로 구성되어 있기에 시간 복잡도는 O(NLogN)이다)
  • AbstractQueue , AbstractCollection , Collection 및 Object클래스에서 메서드를 상속한다

 

 

선언하기


import java.util.Collections;
import java.util.PriorityQueue;

//낮은 숫자가 우선 순위인 int형 큐 선언
PriorityQueue<Integer> queue = new PriorityQueue<>();

//높은 숫자가 우선 순위인 int형 큐 선언
PriorityQueue<Integer> queue2 = new PriorityQueue<>(Collections.reverseOrder());

 

같은 기능의 메서드지만 예외를 던지거나, null또는 false가 반환될 수도 있다

 

  예외 발생 값 리턴
추가(enqueue) add(e) offer(e)
삭제(dequeue) remove() poll()
검사(peek) element() peek()

.

 

추가하기


// 큐에 여유 공간이 없어 삽입에 실패하면 IllegalStateException을 발생

//add, offer로 삽입
queue.add(1);
queue.add(2);
queue.offer(3);

queue2.add(1);
queue2.add(2);
queue2.offer(3);

Queue에 데이터를 추가 했을 때, 아래 그림과 같은 과정을 통해 즉시 정렬된다

 

출처 : https://coding-factory.tistory.com/603

 

 

삭제하기


// 첫번째 값을 반환하고 제거한다
// 비어있다면 null을 반환
queue.poll();

// 첫번째 값을 제거한다
// 비어있다면 예외 발생
queue.remove(); 

// 첫번째 값을 반환만 하고 제거 하지는 않는다.
// 큐가 비어있다면 null을 반환
queue.peek();

// 첫번째 값을 반환만 하고 제거 하지는 않는다.
// 큐가 비어있다면 예외 발생
queue.element();

// 초기화시킨다
queue.clear();

 

값을 제거할 시 우선순위가 가장 높은 값이 제거된다
poll() 함수는 우선순위 큐가 비어있으면 null을 반환한다 
pop을 하면 가장 앞쪽에 있는 원소의 값이 아래 그림과 같이 제거된다
queue의 모든 요소를 제거하려면 clear() 메서드를 사용한다

 

출처 : https://coding-factory.tistory.com/603


참조

https://coding-factory.tistory.com/603

https://velog.io/@gillog/Java-Priority-Queue%EC%9A%B0%EC%84%A0-%EC%88%9C%EC%9C%84-%ED%81%90

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

객체지향 설계 원칙 - SOLID

Tech/Java & Spring 2022. 12. 20. 19:26
728x90
728x90

[  객체 지향 설계 원칙 ( SOLID )  ]


컴퓨터 프로그래밍에서 SOLID란 로버트 C. 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스가 두문자어 기억술로 소개한 것이다.

프로그래머가 시간이 지나도 유지 보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용할 수 있다.

SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스 코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링하여 코드 냄새를 제거하기 위해 적용할 수 있는 지침이다.

이 원칙들은 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적 전략의 일부다.

 

SOLID 원칙들은 결국 자기 자신 클래스 안에 응집도는 내부적으로 높이고, 타 클래스들 간 결합도는 낮추는 High Cohesion - Loose Coupling 원칙을 객체 지향의 관점에서 도입한 것이다.

왜 그랬을까? 간단하다. 좋은 소프트웨어는 응집도가 높고 결합도가 낮기 때문이다.

결국 모듈 또는 클래스 당 하나의 책임을 주어 더욱더 독립된 모듈(클래스)을 만들기 위함이다.

이렇게 설계된 소프트웨어는 재 사용이 많아지고, 수정이 최소화 되기 때문에 결국 유지 보수가 용이해진다. 


객체 지향 설계의 5대 원칙

  • SRP(Single Responsibility Principle) : 단일 책임의 원칙
  • OCP(Open Closed Principle) : 개방 폐쇄의 원칙
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle) : 의존 역전 원칙

 

 

[  SRP(Single Responsibility Principle) : 단일 책임의 원칙  ]


어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다 - 로버트 C. 마틴

 

클래스에 역할과 책임을 너무 많이 부여하지 마라

한 클래스는 하나의 책임만 가져야 한다

클래스를 설계할 때 경계를 확실히 정하고 추상화를 통해 경계 안에서 필요한 속성과 메서드를 선택하여 설계해야 한다

 

 

[  OCP(Open Closed Principle) : 개방 폐쇄의 원칙  ]


소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다

- 로버트 C. 마틴

 

자신의 확장에는 열려있고 주변의 변화에는 닫혀 있어야 한다

 

 

[  LSP(Liskov Substitution Principle) : 리스코프 치환 원칙  ]    


서브타입은 언제나 자신의 기반타입으로 교체할 수 있어야 한다 - 로버트 C. 마틴

 

 

하위 클래스의 인스턴스는 상위 객체 참조 변수에 대입해 상위 클래스 인스턴스 역할에 문제가 없어야 한다(올바른 상속관계 설정)
객체 지향은 인간이 실세계를 보면서 느끼고 논리적으로 이해한 것과 똑같이 프로그래밍 하는게 목적이기 때문에 논리적으로 맞아 떨어져야 하는것이 기본이다

 

 

ISP(Interface Segregation Principle) : 인터페이스 분리 원칙  ]


클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다 - 로버트 C. 마틴

 

상황과 관련 있는 메서드만 제공 할 것

 

 

DIP(Depndency Inversion Principle) : 의존 역전 원칙


고차원 모듈은 저차원 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화 된 것에 의존해야 한다

추상화 된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화 된 것에 의존해야 한다

자주 변경되는 구체 클래스에 의존하지 마라 - 로버트 C. 마틴

 

자주 변경되는 클래스에 의존하지 않아야 한다
추상 클래스나 상위 클래스는 하위 클래스에게 의존적이 되어서는 안된다

 

 

 

 

 

 

참조

https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84) 

https://limkydev.tistory.com/77

 

 

 

 

 

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

이차원 배열의 정렬 - Arrays.sort(arr, Comparator) / JAVA

Tech/Java & Spring 2022. 12. 19. 23:13
728x90
728x90

1차원배열의 정렬

import java.util.Arrays;

// 오름차순
Arrays.sort(arr);

// 내림차순 기본타입에 선언 불가!! 참조타입에 가능
Arrays.sort(arr, Collections.reverseOrder());

//참조타입 변환(ex - int[])
Integer[] refArr = Arrays.stream(arr).boxed().toArray(Integer[]::new);
//변환 후 정렬
Arrays.sort(refArr, Collections.reverseOrder());

2차원배열의 정렬

import java.util.Arrays;
import java.util.Comparator;

// Comparator는 인터페이스이기때문에 오버라이딩
Arrays.sort(arr, new Comparator<int[]>() {

    @Override
    public int compare(int[] o1, int[] o2) {
        return o1[0]-o2[0]; // 첫번째 숫자 기준 오름차순 {1,30}{2,10}{3,50}{4,20}{5,40}
        return o2[0]-o1[0]; // 첫번째 숫자 기준 내림차순 {5,40}{4,20}{3,50}{2,10}{1,30}
        return o1[1]-o2[1]; // 두번째 숫자 기준 오름차순 {2,10}{4,20}{1,30}{5,40}{3,50}
        return o2[1]-o1[1]; // 두번째 숫자 기준 내림차순 {3,50}{5,40}{1,30}{4,20}{2,10}
    }


});

 

// 람다식 활용
Arrays.sort(arr, (o1, o2) -> o1[1] - o2[1]);

 

//String 배열 정렬
Arrays.sort(arr, (o1, o2) -> o1[1].compareTo(o2[1]));
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

Stream(2)

Tech/Java & Spring 2022. 12. 17. 19:46
728x90
728x90

동작 순서

다음 스트림에서는 최종 작업인 findFirst 메소드를 호출합니다. 과연 출력 결과는 어떨까요?

list.stream()
  .filter(el -> {
    System.out.println("filter() was called.");
    return el.contains("a");
  })
  .map(el -> {
    System.out.println("map() was called.");
    return el.toUpperCase();
  })
  .findFirst();

요소는 3개인데 결과는 다음처럼 filter 두 번, map 이 한 번 출력됩니다.

filter() was called.
filter() was called.
map() was called.

여기서 스트림이 동작하는 순서를 알아낼 수 있습니다. 모든 요소가 첫 번째 중간 연산을 수행하고 남은 결과가 다음 연산으로 넘어가는 것이 아니라, 한 요소가 모든 파이프라인을 거쳐서 결과를 만들어내고, 다음 요소로 넘어가는 순입니다.

 

좀 더 자세히 살펴보면,

  • 처음 요소인 “Eric” 은 “a” 문자열을 가지고 있지 않기 때문에 다음 요소로 넘어갑니다. 이 때 “filter() was called.” 가 한 번 출력됩니다.
  • 다음 요소인 “Elena” 에서 "filter() was called."가 한 번 더 출력됩니다. "Elena"는 "a"를 가지고 있기 때문에 다음 연산으로 넘어갈 수 있습니다.
  • 다음 연산인 map 에서 toUpperCase 메소드가 호출됩니다. 이 때 "map() was called"가 출력됩니다.
  • 마지막 연산인 findFirst 는 첫 번째 요소만을 반환하는 연산입니다. 따라서 최종 결과는 “ELENA” 이고 다음 연산은 수행할 필요가 없어 종료됩니다.

위와 같은 과정을 통해서 수행됩니다.

성능 향상

위에서 살펴봤듯이 스트림은 한 요소씩 수직적으로(vertically) 실행됩니다. 여기에 스트림의 성능을 개선할 수 있는 힌트가 숨어있습니다. 다음 예제를 살펴보시죠.

list.stream()
  .map(el -> {
    wasCalled();
    return el.substring(0, 3);
  })
  .skip(2)
  .collect(Collectors.toList());

System.out.println(counter); // 3

첫 번째 요소 "Eric"은 먼저 문자열을 잘라내고, 다음 skip 메소드 때문에 스킵됩니다. 다음 요소인 "Elena"도 마찬가지로 문자열을 잘라낸 후 스킵됩니다. 마지막 요소인 “Java” 만 문자열을 잘라내어 “Jav” 가 된 후 스킵되지 않고 결과에 포함됩니다. 여기서 map 메소드는 총 3번 호출됩니다.

 

여기서 메소드 순서를 바꾸면 어떨까요? skip 메소드가 먼저 실행되도록 해봅시다.

List<String> collect = list.stream()
  .skip(2)
  .map(el -> {
    wasCalled();
    return el.substring(0, 3);
  })
  .collect(Collectors.toList());

System.out.println(counter); // 1

그 결과 스킵을 먼저 하기 때문에 map 메소드는 한 번 밖에 호출되지 않습니다. 이렇게 요소의 범위를 줄이는 작업을 먼저 실행하는 것이 불필요한 연산을 막을 수 있어 성능을 향상시킬 수 있습니다. 이런 메소드로는 skip, filter, distinct 등이 있습니다.

스트림 재사용

종료 작업을 하지 않는 한 하나의 인스턴스로서 계속해서 사용이 가능합니다. 하지만 종료 작업을 하는 순간 스트림이 닫히기 때문에 재사용은 할 수 없습니다. 스트림은 저장된 데이터를 꺼내서 처리하는 용도이지 데이터를 저장하려는 목적으로 설계되지 않았기 때문입니다.

Stream<String> stream = 
  Stream.of("Eric", "Elena", "Java")
  .filter(name -> name.contains("a"));

Optional<String> firstElement = stream.findFirst();
Optional<String> anyElement = stream.findAny(); // IllegalStateException: stream has already been operated upon or closed

위 예제에서 findFirst 메소드를 실행하면서 스트림이 닫히기 때문에 findAny 하는 순간 런타임 예외(runtime exception)이 발생합니다. 컴파일러가 캐치할 수 없기 때문에 Stream 이 닫힌 후에 사용되지 않는지 주의해야 합니다.

 

위 코드는 아래 코드처럼 바꿀 수 있습니다. 데이터를 List 에 저장하고 필요할 때마다 스트림을 생성해 사용합니다.

List<String> names = 
  Stream.of("Eric", "Elena", "Java")
  .filter(name -> name.contains("a"))
  .collect(Collectors.toList());

Optional<String> firstElement = names.stream().findFirst();
Optional<String> anyElement = names.stream().findAny();

지연 처리 Lazy Invocation

스트림에서 최종 결과는 최종 작업이 이루어질 때 계산됩니다. 호출 횟수를 카운트하는 예제입니다.

private long counter;
private void wasCalled() {
  counter++;
}

다음 예제에서 리스트의 요소가 3개이기 때문에 총 세 번 호출되어 결과가 3이 출력될 것으로 예상됩니다. 하지만 출력값은 0입니다.

List<String> list = Arrays.asList("Eric", "Elena", "Java");
counter = 0;
Stream<String> stream = list.stream()
  .filter(el -> {
    wasCalled();
    return el.contains("a");
  });
System.out.println(counter); // 0 ??

왜냐하면 최종 작업이 실행되지 않아서 실제로 스트림의 연산이 실행되지 않았기 때문입니다. 다음 예제처럼 최종 작업인 collect 메소드를 호출한 결과 3이 출력됩니다.

list.stream().filter(el -> {
  wasCalled();
  return el.contains("a");
}).collect(Collectors.toList());
System.out.println(counter); // 3

Null-safe 스트림 생성하기

NullPointerException 은 개발 시 흔히 발생하는 예외입니다. Optional 을 이용해서 null에 안전한(Null-safe) 스트림을 생성해보겠습니다.

public <T> Stream<T> collectionToStream(Collection<T> collection) {
    return Optional
      .ofNullable(collection)
      .map(Collection::stream)
      .orElseGet(Stream::empty);
  }

위 코드는 인자로 받은 컬렉션 객체를 이용해 옵셔널 객체를 만들고 스트림을 생성후 리턴하는 메소드입니다. 그리고 만약 컬렉션이 비어있는 경우라면 빈 스트림을 리턴하도록 합니다.

 

제네릭을 이용해 어떤 타입이든 받을 수 있습니다.

List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("a", "b", "c");

Stream<Integer> intStream = 
  collectionToStream(intList); // [1, 2, 3]
Stream<String> strStream = 
  collectionToStream(strList); // [a, b, c]

이제 null 로 테스트를 해보겠습니다. 다음과 같이 리스트에 null 이 있다면 NPE 가 날 수 밖에 없는 상황입니다. 외부에서 인자로 받은 리스트로 작업을 하는 경우에 일어날 수 있는 상황입니다.

List<String> nullList = null;

nullList.stream()
  .filter(str -> str.contains("a"))
  .map(String::length)
  .forEach(System.out::println); // NPE!

하지만 우리가 만든 메소드를 이용하면 NPE 가 발생하는 대신 빈 스트림으로 작업을 마칠 수 있습니다.

collectionToStream(nullList)
  .filter(str -> str.contains("a"))
  .map(String::length)
  .forEach(System.out::println); // []

줄여쓰기 Simplified

스트림 사용 시 다음과 같은 경우에 같은 내용을 좀 더 간결하게 줄여쓸 수 있습니다. IntelliJ 를 사용하면 다음과 같은 경우에 줄여쓸 것을 제안해줍니다. 그 중에서 많이 사용되는 것만 추렸습니다.

collection.stream().forEach() 
  → collection.forEach()
  
collection.stream().toArray() 
  → collection.toArray()

Arrays.asList().stream() 
  → Arrays.stream() or Stream.of()

Collections.emptyList().stream() 
  → Stream.empty()

stream.filter().findFirst().isPresent() 
  → stream.anyMatch()

stream.collect(counting()) 
  → stream.count()

stream.collect(maxBy()) 
  → stream.max()

stream.collect(mapping()) 
  → stream.map().collect()

stream.collect(reducing()) 
  → stream.reduce()

stream.collect(summingInt()) 
  → stream.mapToInt().sum()

stream.map(x -> {...; return x;}) 
  → stream.peek(x -> ...)

!stream.anyMatch() 
  → stream.noneMatch()

!stream.anyMatch(x -> !(...)) 
  → stream.allMatch()

stream.map().anyMatch(Boolean::booleanValue) 
  → stream.anyMatch()

IntStream.range(expr1, expr2).mapToObj(x -> array[x]) 
  → Arrays.stream(array, expr1, expr2)

Collection.nCopies(count, ...) 
  → Stream.generate().limit(count)

stream.sorted(comparator).findFirst() 
  → Stream.min(comparator)

하지만 주의점이 있습니다. 특정 케이스에서 조금 다르게 동작할 수 있습니다.

 

예를 들면 다음의 경우 stream 을 생략할 수 있지만,

collection.stream().forEach() 
  → collection.forEach()

다음 경우에서는 동기화(synchronized)는 차이가 있습니다.

// not synchronized
Collections.synchronizedList(...).stream().forEach()
  
// synchronized
Collections.synchronizedList(...).forEach()

다른 예제는 다음과 같이 collect 를 생략하고 바로 max 메소드를 호출하는 경우입니다.

stream.collect(maxBy()) 
  → stream.max()

하지만 스트림이 비어서 값을 계산할 수 없을 때의 동작은 다릅니다. 전자는 Optional 객체를 리턴하지만, 후자는 NullPointerExcpetion 이 발생할 가능성이 있습니다.

collect(Collectors.maxBy()) // Optional
Stream.max() // NPE 발생 가능

 

 

출처

https://futurecreator.github.io/2018/08/26/java-8-streams-advanced/

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

split() - JAVA 문자열 자르기

Tech/Java & Spring 2022. 12. 17. 15:53
728x90
728x90

Split

특정 문자를 기준으로 문자열을 나누어 배열에 저장한다

String phoneNum = "010-0101-0101";
String[] strArr = phoneNum.split("-");


for(int i=0; i<strArr.length; i++) {
    System.out.println(strArr[i]);			
}

//Console
//010
//0101
//0101
// split(String regex, int limit)
// limit 만큼의 배열크기 지정

String phoneNum = "010-0101-0101-0101-010";
String[] strArr = phoneNum.split("-", 3);


for(int i=0; i<strArr.length; i++) {
    System.out.println(strArr[i]);			
}

//Console
//010
//0101
//0101-0101-010
728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

Stream(1)

Tech/Java & Spring 2022. 12. 17. 13:39
728x90
728x90

스트림 Streams


자바 8에서 추가한 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나입니다. 자바 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for 또는 foreach문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이었습니다. 간단한 경우라면 상관없지만 로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고, 메소드를 나눌 경우 루프를 여러 번 도는 경우가 발생합니다.

 

스트림은 '데이터의 흐름’입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.

 

또 하나의 장점은 간단하게 병렬처리(multi-threading)가 가능하다는 점입니다. 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을 병렬 처리(parallel processing)라고 합니다. 즉 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있습니다.

스트림에 대한 내용은 크게 세 가지로 나눌 수 있습니다.

 

  1. 생성하기 : 스트림 인스턴스 생성.
  2. 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations).
  3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations).
전체 -> 맵핑 -> 필터링 1 -> 필터링 2 -> 결과 만들기 -> 결과물

 

 

 

생성하기

보통 배열과 컬렉션을 이용해서 스트림을 만들지만 이 외에도 다양한 방법으로 스트림을 만들 수 있습니다. 하나씩 살펴보겠습니다.

배열 스트림

스트림을 이용하기 위해서는 먼저 생성을 해야 합니다. 스트림은 배열 또는 컬렉션 인스턴스를 이용해서 생성할 수 있습니다. 배열은 다음과 같이 Arrays.stream 메소드를 사용합니다.

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> streamOfArrayPart = 
  Arrays.stream(arr, 1, 3); // 1~2 요소 [b, c]

 

컬렉션 스트림

 

컬렉션 타입(Collection, List, Set)의 경우 인터페이스에 추가된 디폴트 메소드 stream 을 이용해서 스트림을 만들 수 있습니다.

public interface Collection<E> extends Iterable<E> {
  default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
  } 
  // ...
}


그러면 다음과 같이 생성할 수 있습니다.

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림

비어 있는 스트림

비어 있는 스트림(empty streams)도 생성할 수 있습니다. 언제 빈 스트림이 필요할까요? 빈 스트림은 요소가 없을 때 null대신 사용할 수 있습니다.

public Stream<String> streamOf(List<String> list) {
  return list == null || list.isEmpty() 
    ? Stream.empty() 
    : list.stream();
}

Stream.builder()

빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있습니다. 마지막에 build 메소드로 스트림을 리턴합니다.

Stream<String> builderStream = 
  Stream.<String>builder()
    .add("Eric").add("Elena").add("Java")
    .build(); // [Eric, Elena, Java]

 

Stream.generate()

generate메소드를 이용하면 Supplier<T> 에 해당하는 람다로 값을 넣을 수 있습니다. Supplier<T>  는 인자는 없고 리턴값만 있는 함수형 인터페이스죠. 람다에서 리턴하는 값이 들어갑니다.

public static<T> Stream<T> generate(Supplier<T> s) { ... }

이 때 생성되는 스트림은 크기가 정해져있지 않고 무한(infinite)하기 때문에 특정 사이즈로 최대 크기를 제한해야 합니다.

Stream<String> generatedStream = 
  Stream.generate(() -> "gen").limit(5); // [el, el, el, el, el]

5개의 “gen” 이 들어간 스트림이 생성됩니다.

Stream.iterate()

iterate 메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만듭니다. 다음 예제에서는 30이 초기값이고 값이 2씩 증가하는 값들이 들어가게 됩니다. 즉 요소가 다음 요소의 인풋으로 들어갑니다. 이 방법도 스트림의 사이즈가 무한하기 때문에 특정 사이즈로 제한해야 합니다.

Stream<Integer> iteratedStream = 
  Stream.iterate(30, n -> n + 2).limit(5); // [30, 32, 34, 36, 38]

기본 타입형 스트림

물론 제네릭을 사용하면 리스트나 배열을 이용해서 기본 타입(int, long, double) 스트림을 생성할 수 있습니다. 하지만 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수도 있습니다. range  rangeClosed 는 범위의 차이입니다. 두 번째 인자인 종료지점이 포함되느냐 안되느냐의 차이입니다.

IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

제네릭을 사용하지 않기 때문에 불필요한 오토박싱(auto-boxing)이 일어나지 않습니다. 필요한 경우 boxed메소드를 이용해서 박싱(boxing)할 수 있습니다.

Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();

Java 8 의 Random 클래스는 난수를 가지고 세 가지 타입의 스트림(IntStream, LongStream, DoubleStream)을 만들어낼 수 있습니다. 쉽게 난수 스트림을 생성해서 여러가지 후속 작업을 취할 수 있어 유용합니다.

DoubleStream doubles = new Random().doubles(3); // 난수 3개 생성

문자열 스트링

스트링을 이용해서 스트림을 생성할수도 있습니다. 다음은 스트링의 각 문자(char)를 IntStream 으로 변환한 예제입니다. char 는 문자이지만 본질적으로는 숫자이기 때문에 가능합니다.

IntStream charsStream = 
  "Stream".chars(); // [83, 116, 114, 101, 97, 109]

다음은 정규표현식(RegEx)을 이용해서 문자열을 자르고, 각 요소들로 스트림을 만든 예제입니다.

Stream<String> stringStream = 
  Pattern.compile(", ").splitAsStream("Eric, Elena, Java");
  // [Eric, Elena, Java]

파일 스트림

자바 NIO 의 Files 클래스의 lines 메소드는 해당 파일의 각 라인을 스트링 타입의 스트림으로 만들어줍니다.

Stream<String> lineStream = 
  Files.lines(Paths.get("file.txt"), 
              Charset.forName("UTF-8"));

병렬 스트림 Parallel Stream

스트림 생성 시 사용하는 stream 대신 parallelStream 메소드를 사용해서 병렬 스트림을 쉽게 생성할 수 있습니다. 내부적으로는 쓰레드를 처리하기 위해 자바 7부터 도입된 Fork/Join framework 를 사용합니다.

// 병렬 스트림 생성
Stream<Product> parallelStream = productList.parallelStream();

// 병렬 여부 확인
boolean isParallel = parallelStream.isParallel()

따라서 다음 코드는 각 작업을 쓰레드를 이용해 병렬 처리됩니다.

boolean isMany = parallelStream
  .map(product -> product.getAmount() * 10)
  .anyMatch(amount -> amount > 200);

다음은 배열을 이용해서 병렬 스트림을 생성하는 경우입니다.

Arrays.stream(arr).parallel();

컬렉션과 배열이 아닌 경우는 다음과 같이 parallel 메소드를 이용해서 처리합니다.

IntStream intStream = IntStream.range(1, 150).parallel();
boolean isParallel = intStream.isParallel();

다시 시퀀셜(sequential) 모드로 돌리고 싶다면 다음처럼 sequential 메소드를 사용합니다. 뒤에서 한번 더 다루겠지만 반드시 병렬 스트림이 좋은 것은 아닙니다.

IntStream intStream = intStream.sequential();
boolean isParallel = intStream.isParallel();

스트림 연결하기

Stream.concat 메소드를 이용해 두 개의 스트림을 연결해서 새로운 스트림을 만들어낼 수 있습니다.

Stream<String> stream1 = Stream.of("Java", "Scala", "Groovy");
Stream<String> stream2 = Stream.of("Python", "Go", "Swift");
Stream<String> concat = Stream.concat(stream1, stream2);
// [Java, Scala, Groovy, Python, Go, Swift]

가공하기

전체 요소 중에서 다음과 같은 API 를 이용해서 내가 원하는 것만 뽑아낼 수 있습니다. 이러한 가공 단계를 중간 작업(intermediate operations)이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있습니다.

List<String> names = Arrays.asList("Eric", "Elena", "Java");

아래 나오는 예제 코드는 위와 같은 리스트를 대상으로 합니다.

Filtering

필터(filter)은 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업입니다. 인자로 받는 Predicate 는 boolean 을 리턴하는 함수형 인터페이스로 평가식이 들어가게 됩니다.

Stream<T> filter(Predicate<? super T> predicate);

간단한 예제입니다.

Stream<String> stream = 
  names.stream()
  .filter(name -> name.contains("a"));
// [Elena, Java]

스트림의 각 요소에 대해서 평가식을 실행하게 되고 ‘a’ 가 들어간 이름만 들어간 스트림이 리턴됩니다.

Mapping

맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해줍니다. 이 때 값을 변환하기 위한 람다를 인자로 받습니다.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

스트림에 들어가 있는 값이 input 이 되어서 특정 로직을 거친 후 output 이 되어 (리턴되는) 새로운 스트림에 담기게 됩니다. 이러한 작업을 맵핑(mapping)이라고 합니다.

간단한 예제입니다. 스트림 내 String 의 toUpperCase 메소드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴합니다.

Stream<String> stream = 
  names.stream()
  .map(String::toUpperCase);
// [ERIC, ELENA, JAVA]

다음처럼 요소 내 들어있는 Product 개체의 수량을 꺼내올 수도 있습니다. 각 ‘상품’을 ‘상품의 수량’으로 맵핑하는거죠.

Stream<Integer> stream = 
  productList.stream()
  .map(Product::getAmount);
// [23, 14, 13, 23, 13]

map 이외에도 조금 더 복잡한 flatMap 메소드도 있습니다.

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

인자로 mapper를 받고 있는데, 리턴 타입이 Stream 입니다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야합니다. flatMap 은 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 합니다. 이러한 작업을 플래트닝(flattening)이라고 합니다.

 

다음과 같은 중첩된 리스트가 있습니다.

List<List<String>> list = 
  Arrays.asList(Arrays.asList("a"), 
                Arrays.asList("b"));
// [[a], [b]]

이를 flatMap을 사용해서 중첩 구조를 제거한 후 작업할 수 있습니다.

List<String> flatList = 
  list.stream()
  .flatMap(Collection::stream)
  .collect(Collectors.toList());
// [a, b]

이번엔 객체에 적용해보겠습니다.

students.stream()
  .flatMapToInt(student -> 
                IntStream.of(student.getKor(), 
                             student.getEng(), 
                             student.getMath()))
  .average().ifPresent(avg -> 
                       System.out.println(Math.round(avg * 10)/10.0));

위 예제에서는 학생 객체를 가진 스트림에서 학생의 국영수 점수를 뽑아 새로운 스트림을 만들어 평균을 구하는 코드입니다. 이는 map 메소드 자체만으로는 한번에 할 수 없는 기능입니다.

Sorting

정렬의 방법은 다른 정렬과 마찬가지로 Comparator 를 이용합니다.

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

인자 없이 그냥 호출할 경우 오름차순으로 정렬합니다.

IntStream.of(14, 11, 20, 39, 23)
  .sorted()
  .boxed()
  .collect(Collectors.toList());
// [11, 14, 20, 23, 39]

인자를 넘기는 경우와 비교해보겠습니다. 스트링 리스트에서 알파벳 순으로 정렬한 코드와 Comparator 를 넘겨서 역순으로 정렬한 코드입니다.

List<String> lang = 
  Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");

lang.stream()
  .sorted()
  .collect(Collectors.toList());
// [Go, Groovy, Java, Python, Scala, Swift]

lang.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
// [Swift, Scala, Python, Java, Groovy, Go]

Comparator 의 compare 메소드는 두 인자를 비교해서 값을 리턴합니다.

int compare(T o1, T o2)

기본적으로 Comparator 사용법과 동일합니다. 이를 이용해서 문자열 길이를 기준으로 정렬해보겠습니다.

lang.stream()
  .sorted(Comparator.comparingInt(String::length))
  .collect(Collectors.toList());
// [Go, Java, Scala, Swift, Groovy, Python]

lang.stream()
  .sorted((s1, s2) -> s2.length() - s1.length())
  .collect(Collectors.toList());
// [Groovy, Python, Scala, Swift, Java, Go]

Iterating

스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드로는 peek 이 있습니다. ‘peek’ 은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer 를 인자로 받습니다.

Stream<T> peek(Consumer<? super T> action);

따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않습니다. 다음처럼 작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있습니다.

int sum = IntStream.of(1, 3, 5, 7, 9)
  .peek(System.out::println)
  .sum();

결과 만들기

가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계입니다. 따라서 스트림을 끝내는 최종 작업(terminal operations)입니다.

Calculating

스트림 API 는 다양한 종료 작업을 제공합니다. 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있습니다.

long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();

만약 스트림이 비어 있는 경우 count  sum 은 0을 출력하면 됩니다. 하지만 평균, 최소, 최대의 경우에는 표현할 수가 없기 때문에 Optional 을 이용해 리턴합니다.

OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();

스트림에서 바로 ifPresent 메소드를 이용해서 Optional 을 처리할 수 있습니다.

DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
  .average()
  .ifPresent(System.out::println);

 

이 외에도 사용자가 원하는대로 결과를 만들어내기 위해 reduce  collect 메소드를 제공합니다. 이 두 가지 메소드를 좀 더 알아보겠습니다.

Reduction

스트림은 reduce라는 메소드를 이용해서 결과를 만들어냅니다. 람다 예제에서 살펴봤듯이 스트림에 있는 여러 요소의 총합을 낼 수도 있습니다.

다음은 reduce메소드는 총 세 가지의 파라미터를 받을 수 있습니다.

  • accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
  • identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
  • combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);

// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);

// 3개 (combiner)
<U> U reduce(U identity,
  BiFunction<U, ? super T, U> accumulator,
  BinaryOperator<U> combiner);

먼저 인자가 하나만 있는 경우입니다. 여기서 BinaryOperator<T> 는 같은 타입의 인자 두 개를 받아 같은 타입의 결과를 반환하는 함수형 인터페이스입니다. 다음 예제에서는 두 값을 더하는 람다를 넘겨주고 있습니다. 따라서 결과는 6(1 + 2 + 3)이 됩니다.

OptionalInt reduced = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce((a, b) -> {
    return Integer.sum(a, b);
  });

이번엔 두 개의 인자를 받는 경우입니다. 여기서 10은 초기값이고, 스트림 내 값을 더해서 결과는 16(10 + 1 + 2 + 3)이 됩니다. 여기서 람다는 메소드 참조(method reference)를 이용해서 넘길 수 있습니다.

int reducedTwoParams = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce(10, Integer::sum); // method reference

마지막으로 세 개의 인자를 받는 경우입니다. Combiner 가 하는 역할을 설명만 봤을 때는 잘 이해가 안갈 수 있는데요, 코드를 한번 살펴봅시다. 그런데 다음 코드를 실행해보면 이상하게 마지막 인자인 combiner 는 실행되지 않습니다.

Integer reducedParams = Stream.of(1, 2, 3)
  .reduce(10, // identity
          Integer::sum, // accumulator
          (a, b) -> {
            System.out.println("combiner was called");
            return a + b;
          });

Combiner 는 병렬 처리 시 각자 다른 쓰레드에서 실행한 결과를 마지막에 합치는 단계입니다. 따라서 병렬 스트림에서만 동작합니다.

Integer reducedParallel = Arrays.asList(1, 2, 3)
  .parallelStream()
  .reduce(10,
          Integer::sum,
          (a, b) -> {
            System.out.println("combiner was called");
            return a + b;
          });

결과는 다음과 같이 36이 나옵니다. 먼저 accumulator 는 총 세 번 동작합니다. 초기값 10에 각 스트림 값을 더한 세 개의 값(10 + 1 = 11, 10 + 2 = 12, 10 + 3 = 13)을 계산합니다. Combiner 는 identity 와 accumulator 를 가지고 여러 쓰레드에서 나눠 계산한 결과를 합치는 역할입니다. 12 + 13 = 25, 25 + 11 = 36 이렇게 두 번 호출됩니다.

combiner was called
combiner was called
36

병렬 스트림이 무조건 시퀀셜보다 좋은 것은 아닙니다. 오히려 간단한 경우에는 이렇게 부가적인 처리가 필요하기 때문에 오히려 느릴 수도 있습니다.

Collecting

collect 메소드는 또 다른 종료 작업입니다. Collector 타입의 인자를 받아서 처리를 하는데요, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있습니다.

이번 예제에서는 다음과 같은 간단한 리스트를 사용합니다. Product 객체는 수량(amout)과 이름(name)을 가지고 있습니다.

List<Product> productList = 
  Arrays.asList(new Product(23, "potatoes"),
                new Product(14, "orange"),
                new Product(13, "lemon"),
                new Product(23, "bread"),
                new Product(13, "sugar"));

Collectors.toList()

스트림에서 작업한 결과를 담은 리스트로 반환합니다. 다음 예제에서는 map 으로 각 요소의 이름을 가져온 후 Collectors.toList 를 이용해서 리스트로 결과를 가져옵니다.

List<String> collectorCollection =
  productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]

Collectors.joining()

스트림에서 작업한 결과를 하나의 스트링으로 이어 붙일 수 있습니다.

String listToString = 
 productList.stream()
  .map(Product::getName)
  .collect(Collectors.joining());
// potatoesorangelemonbreadsugar

Collectors.joining 은 세 개의 인자를 받을 수 있습니다. 이를 이용하면 간단하게 스트링을 조합할 수 있습니다.

  • delimiter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
  • prefix : 결과 맨 앞에 붙는 문자
  • suffix : 결과 맨 뒤에 붙는 문자
String listToString = 
 productList.stream()
  .map(Product::getName)
  .collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>

Collectors.averageingInt()

숫자 값(Integer value )의 평균(arithmetic mean)을 냅니다.

Double averageAmount = 
 productList.stream()
  .collect(Collectors.averagingInt(Product::getAmount));
// 17.2

Collectors.summingInt()

숫자값의 합(sum)을 냅니다.

Integer summingAmount = 
 productList.stream()
  .collect(Collectors.summingInt(Product::getAmount));
// 86

IntStream 으로 바꿔주는 mapToInt 메소드를 사용해서 좀 더 간단하게 표현할 수 있습니다.

Integer summingAmount = 
  productList.stream()
  .mapToInt(Product::getAmount)
  .sum(); // 86

Collectors.summarizingInt()

만약 합계와 평균 모두 필요하다면 스트림을 두 번 생성해야 할까요? 이런 정보를 한번에 얻을 수 있는 방법으로는 summarizingInt 메소드가 있습니다.

IntSummaryStatistics statistics = 
 productList.stream()
  .collect(Collectors.summarizingInt(Product::getAmount));

이렇게 받아온 IntSummaryStatistics 객체에는 다음과 같은 정보가 담겨 있습니다.

IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
  • 개수 getCount()
  • 합계 getSum()
  • 평균 getAverage()
  • 최소 getMin()
  • 최대 getMax()

이를 이용하면 collect 전에 이런 통계 작업을 위한 map 을 호출할 필요가 없게 됩니다. 위에서 살펴본 averaging, summing, summarizing 메소드는 각 기본 타입(int, long, double)별로 제공됩니다.

Collectors.groupingBy()

특정 조건으로 요소들을 그룹지을 수 있습니다. 수량을 기준으로 그룹핑해보겠습니다. 여기서 받는 인자는 함수형 인터페이스 Function 입니다.

Map<Integer, List<Product>> collectorMapOfLists =
 productList.stream()
  .collect(Collectors.groupingBy(Product::getAmount));

결과는 Map 타입으로 나오는데요, 같은 수량이면 리스트로 묶어서 보여줍니다.

{23=[Product{amount=23, name='potatoes'}, 
     Product{amount=23, name='bread'}], 
 13=[Product{amount=13, name='lemon'}, 
     Product{amount=13, name='sugar'}], 
 14=[Product{amount=14, name='orange'}]}

Collectors.partitioningBy()

위의 groupingBy 함수형 인터페이스 Function 을 이용해서 특정 값을 기준으로 스트림 내 요소들을 묶었다면, partitioningBy 은 함수형 인터페이스 Predicate 를 받습니다. Predicate 는 인자를 받아서 boolean 값을 리턴합니다.

Map<Boolean, List<Product>> mapPartitioned = 
  productList.stream()
  .collect(Collectors.partitioningBy(el -> el.getAmount() > 15));

따라서 평가를 하는 함수를 통해서 스트림 내 요소들을 true 와 false 두 가지로 나눌 수 있습니다.

{false=[Product{amount=14, name='orange'}, 
        Product{amount=13, name='lemon'}, 
        Product{amount=13, name='sugar'}], 
 true=[Product{amount=23, name='potatoes'}, 
       Product{amount=23, name='bread'}]}

Collectors.collectingAndThen()

특정 타입으로 결과를 collect 한 이후에 추가 작업이 필요한 경우에 사용할 수 있습니다. 이 메소드의 시그니쳐는 다음과 같습니다. finisher 가 추가된 모양인데, 이 피니셔는 collect 를 한 후에 실행할 작업을 의미합니다.

public static<T,A,R,RR> Collector<T,A,RR> collectingAndThen(
  Collector<T,A,R> downstream,
  Function<R,RR> finisher) { ... }

다음 예제는 Collectors.toSet 을 이용해서 결과를 Set 으로 collect 한 후 수정불가한 Set 으로 변환하는 작업을 추가로 실행하는 코드입니다.

Set<Product> unmodifiableSet = 
 productList.stream()
  .collect(Collectors.collectingAndThen(Collectors.toSet(),
                                        Collections::unmodifiableSet));

Collector.of()

여러가지 상황에서 사용할 수 있는 메소드들을 살펴봤습니다. 이 외에 필요한 로직이 있다면 직접 collector 를 만들 수도 있습니다. accumulator 와 combiner 는 reduce 에서 살펴본 내용과 동일합니다.

public static<T, R> Collector<T, R, R> of(
  Supplier<R> supplier, // new collector 생성
  BiConsumer<R, T> accumulator, // 두 값을 가지고 계산
  BinaryOperator<R> combiner, // 계산한 결과를 수집하는 함수.
  Characteristics... characteristics) { ... }

코드를 보시면 더 이해가 쉬우실 겁니다. 다음 코드에서는 collector 를 하나 생성합니다. 컬렉터를 생성하는 supplier 에 LinkedList 의 생성자를 넘겨줍니다. 그리고 accumulator 에는 리스트에 추가하는 add 메소드를 넘겨주고 있습니다. 따라서 이 컬렉터는 스트림의 각 요소에 대해서 LinkedList 를 만들고 요소를 추가하게 됩니다. 마지막으로 combiner 를 이용해 결과를 조합하는데, 생성된 리스트들을 하나의 리스트로 합치고 있습니다.

Collector<Product, ?, LinkedList<Product>> toLinkedList = 
  Collector.of(LinkedList::new, 
               LinkedList::add, 
               (first, second) -> {
                 first.addAll(second);
                 return first;
               });

따라서 다음과 같이 collect 메소드에 우리가 만든 커스텀 컬렉터를 넘겨줄 수 있고, 결과가 담긴 LinkedList 가 반환됩니다.

LinkedList<Product> linkedListOfPersons = 
  productList.stream()
  .collect(toLinkedList);

Matching

매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다.

  • 하나라도 조건을 만족하는 요소가 있는지(anyMatch)
  • 모두 조건을 만족하는지(allMatch)
  • 모두 조건을 만족하지 않는지(noneMatch)
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);

간단한 예제입니다. 다음 매칭 결과는 모두 true 입니다.

List<String> names = Arrays.asList("Eric", "Elena", "Java");

boolean anyMatch = names.stream()
  .anyMatch(name -> name.contains("a"));
boolean allMatch = names.stream()
  .allMatch(name -> name.length() > 3);
boolean noneMatch = names.stream()
  .noneMatch(name -> name.endsWith("s"));

Iterating

foreach 는 요소를 돌면서 실행되는 최종 작업입니다. 보통 System.out.println 메소드를 넘겨서 결과를 출력할 때 사용하곤 합니다. 앞서 살펴본 peek 과는 중간 작업과 최종 작업의 차이가 있습니다.

 

names.stream().forEach(System.out::println);

출처

https://futurecreator.github.io/2018/08/26/java-8-streams/

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록