이번 글에서 다룰 주제는 Java SE 8버전에서 추가된 Lambda(람다) 표현식과 Stream API에 대해서다.
나는 애석하게도 Lambda 에 대한 것이나 Stream 을 배워본 적이 없어서 활용해보질 못했었다.
그렇기에 더욱 더 제대로 공부해야겠다고 생각이든다.
이번 글에서 설명할 람다 표현식과 스트림을 이용하면 많은 장점이 있다.
기존의 불필요한 코드를 줄여주고, 가독성을 높여주며 효율 면에서도 좋은 효과를 줄 수 있다.
대체 어떤 방식이길래 그러는지 정리해보겠다.
1. Lambda 표현식
람다 표현식은 식별자 없이 실행이 가능한 함수를 만들 때 쓰인다.
이 한줄을 보면 식별자가 없는데 어떻게 실행을 하지? 라는 생각이 들 수도 있다.
자바스크립트를 해봤다면 변수에 함수를 넣어서 익명함수(식별자(이름)이 없는 함수)를 만들어 본적이 있을 것이다.
그 익명함수와 같은 역할을 하는 것이 바로 이 람다 표현식이다.
(타입 변수명, ...) -> {실행문; }
람다식은 위와 같이 표현할 수 있다.
어떻게 활용하는지도 알아보겠다. 일단 더하기를 위한 기능을 만들기 위해 인터페이스를 만든다.
public interface Add {
public int getAdd(int x, int y);
}
public class Main{
public static void main(String[] args) {
Add add = new Add(){
@Override
public int getAdd(int x, int y) {
return x + y;
}
};
System.out.println(add.getAdd(6, 9));
}
}
확실한 비교를 위해서 상속이 아닌 선언해서 구현해보았다.
자바의 특성상 코드가 조금 길다고는 생각이든다. 그리고 프로그래머가 아니라면 해석하기도 조금 힘들것 같다는 생각이든다.
그러면 이번엔 람다식을 사용해보겠다.
public class Main {
public static void main(String[] args) {
Add add = (x, y) -> { return x + y; };
System.out.println(add.getAdd(6, 9));
}
}
이렇게 코드를 작성하면 같은 값을 받을 수 있다.
엄청나게 코드가 간결해졌고 코드 작성시간도 적어졌으며 직접적인 작성으로 이해하기도 편한것 같다.(가독성)
익명함수는 보통 일회성 함수에 사용된다.
만약 함수의 재사용이 필요한 상황이라면 람다를 이용하는게 아니라 선언해서 사용하는게 더 옳을 것이다.
그리고 람다를 남발하거나 재귀함수에 람다를 이용하면 부적합한 면이 존재한다고 하니 주의하는 것도 괜찮을 것 같다.
2. Stream
이번에는 Java SE 8에서 나온 다른 기능인 Stream에 대해서 알아볼려고 한다.
스트림은 배열, 컬렉션 인스턴스(arrays, list 등 : 나중에 자세히 다룰 예정)를 활용해 연산 할 수 있는 기능이다.
기존의 방법(반복문 : for, foreach, while)을 사용하는 것은 코드가 짧을 때는 괜찮지만, 구현할 내용이 길어지거나 복잡해지면,
문제가 생길수도 있기에 이런 api가 나온것 같다.( 에러가 난다던지... ex)무한 루프 / 가독성이 떨어진다던지... 등)
심지어 스트림은 위에서 배운 람다를 활용할 수 있기도 해서, 잘 활용만 한다면 코딩에 있어서 좋은 효율을 가져다줄 것이다.
그러면 바로 어떻게 사용되는지 알아보자.
List<String> names = Arrays.asList("GG", "BB", "ppusda", "java");
for(int i = 0; i < names.size(); i++){
if(names.get(i).toString().contains("a")){
System.out.println(names.get(i).toString());
}
}
위의 코드를 보면, 일반적으로 우리가 사용하는 for문과 if문을 사용해 원하는 값을 출력하게끔 코드를 작성했다.
이 코드를 stream으로 작성해보겠다.
List<String> names = Arrays.asList("GG", "BB", "ppusda", "java");
Stream<String> nmSt = names.stream().filter(x -> x.contains("a"));
nmSt.forEach(x -> System.out.println(x));
위의 코드처럼 사용할 수 있다.
for문과 if문이 사라지고 두줄로 해결할 수 있게 되었다!
위처럼 컬렉션에서 원하는 결과를 얻기위해 데이터를 가공해 처리하는 데 사용되는 것이 바로 stream이다.
보통 코드는 아래와 같이 이루어진다.
컬렉션(배열) = 값;
스트림 = 컬렉션.스트림생성().중간연산().최종연산();
위에 작성했던 코드를 예로 들어보면 컬렉션(배열)은 names, 스트림은 nmSt, 스트림생성은 stream(), 중간연산은 filter(), 최종연산은 forEach()가 된다.
코드가 꽤나 간결해진 걸 보면 나중에 프로젝트에 적용해보고 싶다는 생각이 든다.
그렇다면 다른 연산들은 무엇이 있는지 자세히 살펴보자
1. 중간연산()
Filter() - 위에서 사용했던 것 처럼 if문 같이 사용가능하다.
.filter(x -> x > 5); // 등등
Map() - 각 데이터에 연산을 한다.
.map(x -> x.concat("s")); // 뒤에 s 붙히기 .map(x -> x*2); // 각 값에 2 곱하기 // 등등
Peek() - foreach()와 같이 반복 행동을 한다.
// but peek는 중간연산이기 때문에 최종연산이 없으면 값이 출력되지 않는다는 것을 알고있어야한다.
// peek() 내에서 값을 바꾸는 걸 시도하려고 한다면 최종연산이 있더라도 모든 내용이 실행되지 않을 수도 있으니 주의
해당 주의 내용에 대한 출처 : https://ramees.tistory.com/46
.peek(x -> System.out.println("name : " + x.name)); // 이름 출력
Sorted() - 말그대로 컬렉션 값들을 정렬해준다.
// 단순히 sorted()만 쓴다면 오름차순으로 정렬(정순), Comparator.reverseOrder()을 넣어준다면 내림차순으로 정렬(역순)
.sorted(); .sorted(Comparator.reverseOrder());
Limit() - 스트림에 제한을 걸어 길이를 조절한다.
.limit(1); // 값 1개만 .limit(5); // 5개만 들어감
Skip() - limit와 반대라고 생각하면 된다. 말그대로 값 건너뛰기
.skip(3); // 3개 건너 뜀 .skip(1); // 1개 건너 뛰고 작업
Distinct() - 중복제거의 기능을 한다. SQL에서도 쓰이니 기억해두면 좋을 듯
.distinct()
MapToInt, MapToLong, MapToDouble - 각 타입에 맞춰서 스트림을 반환해준다.
// ex) "1", "2", "3" 이 있으면 MapToInt를 통해 1, 2, 3으로 바꿀 수 있음
// Int로 바꾸기 위해서는 IntStream에 담아야 한다는 사실도 알아두자
List<String> name = Arrays.asList("3", "6", "9");
IntStream intStream = name.stream().mapToInt(Integer::parseInt);
intStream.forEach(System.out::println);
*** 괄호안에 람다식이 들어가있지 않고 :: 이 들어가 있는 것은 생략 방식 중에 하나인데,
파라미터(예 : x) 선언하지 않고 바로 값을 전달해줄 때 쓸 수 있다. 선언하고 넣는 것 조차 코드의 낭비라고 생각해 줄인 것이다.
.mapToInt(x -> Integer.parseInt(x)); // 원래라면 이렇게 써야하지만 바로 값을 전달해서 Integer::parseInt로 줄일 수 있는 것
2. 최종연산()
count(), min(), max(), sum(), average() - 최종연산이기 때문에 앞에서 가져온 값들을 토대로 개수를 세거나(count), 최소값(min), 최대값(max), 더하기(sum), 평균(average)을 구해 값을 처리할 수 있다.
int[] nums = {3, 6, 9};
int tot = Arrays.stream(nums)
.filter(x-> x>5)
.sum();
System.out.println(tot);
reduce() - 누적된 값을 계산하는 함수
int[] nums = {3, 6, 9, 10};
int tot = Arrays.stream(nums).reduce((x, y) -> x + y).getAsInt();
System.out.println(tot);
foreach() - 우리가 아는 for문과 같다. 각 요소를 돌면서 처리한다.
int[] nums = {3, 6, 9, 10};
IntStream tot = Arrays.stream(nums).filter(x-> x>5);
tot.forEach(System.out::println);
collect() - 스트림으로 바꾼 것을 다시 컬렉션으로 바꾸는 기능이다.
List<Integer> nums = Arrays.asList(3, 6, 9, 10);
List<Integer> tot = nums.stream()
.filter(x -> x>5)
.collect(Collectors.toList());
tot.forEach(System.out::println);
Iterator() - 이름에서 알 수 있다 싶이 Iterator 객체를 반환한다.
List<Integer> nums = Arrays.asList(3, 6, 9, 10);
Iterator<Integer> tot = nums.stream()
.filter(x -> x>5)
.iterator();
while(tot.hasNext()){
System.out.println(tot.next());
}
noneMatch, anyMatch, allMatch - 모든 것을 만족하지 않는지(noneMatch), 하나라도 만족하는지(anyMatch), 모든 것을 만족하는지(allMatch)를 판단해서 boolean 값으로 반환한다.
List<Integer> nums = Arrays.asList(3, 6, 9, 10);
boolean tot = nums.stream()
.peek(x -> x++).anyMatch(x -> x == 10);
System.out.println(tot);
이렇게 람다 표현식과 그것을 사용할 수 있는 스트림, 그리고 컬렉션을 활용하는 법들에 대해서 알아보았다.
예제코드가 많아서 살짝 글 길어졌지만 그래도 하나하나씩 활용 방법에 대해 보게되서 확실히 이해할 수 있게 된 것같다.
다음에는 컬렉션 프레임워크에 대해서 집중적으로 정리해 볼 것이다.
*** 알아두면 좋은 것
스트림은 재사용이 불가능하다.
병렬 스트림(parallelStream)을 사용하면 여러 쓰레드를 사용할 수 있다.
중간연산은 최종연산과 같이 실행된다.
참고 :
https://coding-factory.tistory.com/265
https://jeong-pro.tistory.com/165
'언어 > Java' 카테고리의 다른 글
[Java] Static import (0) | 2022.03.20 |
---|---|
[Java] 컬렉션 프레임워크 (0) | 2022.03.20 |
[Java] 자바 프로그램의 구조 (0) | 2022.03.20 |
[Java] - 자바란? (0) | 2022.03.20 |
[Java] Overriding, Overloading (0) | 2022.03.20 |