도서/자바의 신

[도서/자바의 신] #33 Java 8에서 변경된 것들은?

yulee_to 2023. 1. 2. 23:24

자바의 신

✔️이 글은 [자바의 신 - 이상민 지음] 도서를 바탕으로 정리한 글입니다. 


Lambda 표현식(expression)

Java8부터 추가된 람다 표현식은 익명클래스의 가독성이 떨어진다는 단점을 보완하기 위해 만들어졌다.

대신 람다 표현식은 인터페이스에 메소드가 "하나"인 것들에만 적용이 가능하다. 

람다 표현식은 익명 클래스로 전환이 가능하며, 익명 클래스는 람다 표현식으로 전환이 가능하다.

 

메소드가 하나인 인터페이스에는 대표적으로 Runnable, Comparator, FileFilter 등이 있다. 사용자가 구현한 인터페이스에서도 람다 표현식을 사용할 수 있다.

 

람다 표현식

매개 변수 목록 화살표 토큰(Arrow Token)  처리 식
(int x, int y) ->  x+y

 

예제

interface Calculate {
	int operation(int a, int b);
}
private void calculateClassic() {
	Calculate calculateAdd = new Calculate() {
    	@Override
        public int operation(int a, int b) {
        	return a+b;
        }
    };
    System.out.println(calculateAdd.operation(1, 2));
 }

이 코드를 람다 표현식으로 바꾸면 아래와 같다. 

private void calculateLambda() {
	Calculate calculateAdd = (a, b) -> a+b;
    System.out.println(calculateAdd.operation(1, 2));
}

 

메소드가 하나만 있는 인터페이스를 Functional(기능적) 인터페이스라고 부른다. 

누군가 이 Functional 인터페이스에 메소드를 하나 추가한다거나 없애는 것을 막기 위해 명시적으로 @FunctionalInterface라는 어노테이션을 써주면 이 인터페이스에는 내용이 없는 "하나"의 메소드만 선언할 수 있다. 하나만 선언하지 않으면 컴파일 오류가 발생한다.

 

매개변수 목록에는 아무것도 없어도 가능하고, 화살표 뒤의 처리식이 여러 줄일 경우엔 처리식을 중괄호로 묶어주면 된다. 

 

java.util.function 패키지

Java 8에서 제공하는 주요 Functional 인터페이스는 java.util.function 패키지에 다음과 같이 있다.

  • Predicate
    • test() 메소드로 두 객체를 비교할 때 사용하고 boolean 값을 리턴
    • 추가로 and(), negate(), or()라는 default 메소드가 구현되어 있으며, isEqual()이라는 static 메소드가 존재
  • Supplier
    • get() 메소드로 리턴 값은 generic으로 선언된 타입을 리턴
    • 추가적인 메소드 x
  • Consumer
    • accept()라는 매개변수를 하나 갖는 메소드가 있으며, 리턴값이 없음
    • 출력을 할 때처럼 작업을 수행하고 결과를 받을 일이 없을 때 사용
    • andThen()이라는 default 메소드가 있는데 순차적인 작업을 할 때 사용됨
  • Function
    • apply()라는 매개변수를 하나 갖는 메소드가 있으며, 리턴 값도 존재
    • 이 인터페이스는 Function<T, R>로 정의되어 있음. T는 입력 타입, R은 리턴 타입을 의미
    • 변환할 필요가 있을때 이 인터페이스 사용
  • UnaryOperator : A unary operator from T->T
    • apply()라는 매개변수를 하나 갖는 메소드가 있으며, 리턴 값도 존재
    • 단, 한 가지 타입에 대하여 결과도 같은 타입일때 사용
  • BinaryOperator : A binary operator from (T,T) -> T
    • apply()라는 매개변수를 두개 갖는 메소드가 있으며, 리턴 값도 존재
    • 단, 한 가지 타입에 대하여 결과도 같은 타입일 경우에 사용

 

stream

자바의 스트림은 "뭔가 연속된 정보"를 처리하는 데 사용한다.

연속된 정보로 가장 기본적인 것은 배열이고, 컬렉션도 포함되는데 컬렉션은 스트림을 사용가능하지만, 배열은 스트림을 사용할 수 없다. 

배열에 스트림을 사용하기 위해서 컬렉션의 List로 바꿔 사용해 줄 수 있다.

 

배열에 스트림 사용하기 위한 방법

Integer[] values = {1, 3, 5};
List<Integer> list1 = new ArrayList<Integer>(Arrays.asList(values)); //방법 1
List<Integer> list2 = Arrays.stream(values).collect(Collectors.toList()); //방법 2

 

스트림의 구조

list.stream().filter(x-> x>10).count()

list 뒤에 나오는 메소드마다

  • 스트림 생성 : 컬렉션의 목록을 스트림 객체로 변환. 스트림 객체는 java.util.stream.Stream 인터페이스를 의미. stream() 메소드는 Collection 인터페이스에 선언되어 있음
  • 중개 연산(intermediate operation) : 생성된 스트림 객체를 사용하여 중개 연산 부분에서 처리. 아무런 결과를 리턴하지 못함. 없어도 되는 부분이고 여러개의 중개 연산도 가능 
  • 종단 연산(terminal operation) : 마지막으로 중개 연산에서 작업된 내용을 바탕으로 결과 리턴

 

stream() 메소드는 Collection 인터페이스에 선언되어 있고, 순차적으로 데이터를 처리한다. stream()를 더 빠르게 처리하려면 parallelStream()을 사용하면 되는데, 이는 병렬로 처리하기 때문에 CPU도 많이 사용하고 몇개의 쓰레드로 처리될지 보장되지 않아 일반적인 웹 프로그램에선 stream()만 사용하는 것을 권장한다. 

 

stream forEach()

Stream에서 제공하는 연산자 중에서 주로 사용하는 forEach() 메소드는 종단 연산으로 for 루프를 수행하는 것처럼 각각의 항목을 꺼낼 때 사용한다.

 

예시1

List<StudentDTO> students를 매개변수로 받는 메소드에서

students.stream().forEach(student -> System.out.println(student.getName()));

를 사용해 students라는 List 객체에 담겨 있는 StudentDTO객체 하나를 가리키는 student의 이름을 출력해준다.

 

예시2

students.stream().map(student->student.getName()).forEach(name->System.out.println(name));

이 문장의 중간에 map()메소드는 데이터를 특정 데이터로 변환해준다. 즉 map(student->student.getName()) 메소드를 사용하면 stream()에서 StudentDTO객체가 아닌 student.getName()의 결과인 String 값을 사용한다는 말이 된다. 그래서 map() 이후론 List<String>의 스트림을 처리한다고 생각하면 된다. 

 

메소드 참조

앞 절의 예제에서 forEach의 출력 문장은 다음과 같이 처리할 수 있다. 

forEach(System.out::println)

더블콜론(::)은 Java8부터 추가된 것으로 Method Reference(메소드 참조)라고 부른다.

 

메소드 참조의 종류

  • static 메소드 참조 
    • static한 메소드를 참조할 때 사용
    • 정적 메소드가 포함된 클래스::정적 메소드 명
    • Function<String, Integer> f = (String s) -> Integer.parseInt(s); // 람다식
    • Function<String, Integer> f = Integer::parseInt; // 메소드 참조
  • 특정 객체의 인스턴스 메소드 참조 
    • System.out::println의 out 변수에 있는 println() 메소드 호출하는 것처럼 "변수에 선언된 메소드 호출"을 의미
    • 메소드가 포함된 객체::메소드 명
  • 특정 유형의 임의의 객체에 대한 인스턴스 메소드 참조 
    • static이 아닌 메소드를 참조, 메소드 참조가 실행될 때마다 인자로 넘어온 객체가 다를 수 있어 임의의 객체라고 표현
    • 타입::메소드명
  • 생성자 참조 (예 : ClassName::new)
    • 생성자를 임의의 인터페이스를 통해 만들어 줌
    • 클래스명::new
    • Function<Integer, MyClass> s = (i) -> new MyClass(i); // 람다식
    • Function<Integer, MyClass> s = MyClass::new; // 메소드 참조

 

배열과 메소드 참조 예시

Function<Integer, int[]> f = x->new int[x]; // 람다식
Function<Integer, int[]> f2 = int[]::new; // 메소드 참조 

 

stream map()

스트림의 중개 연산에는 대표적으로 map()과 filter()가 있다. 

map() 메소드는 앞에서 본 것처럼 스트림의 값 자체를 바꿔준다. 스트림에서 처리하는 값들을 중간에 변경해야 할 때 사용한다.

 

예시

List<StudentDTO> 타입의 studentList 객체에 이름과 숫자 3개가 저장되어 있다고 하자.

List<String> nameList = studentList.stream()
  .map(student->student.getName()).collect(Collectors.toList());

이렇게 해주면 studentList에 저장된 각 객체(student)의 String인 이름값만 사용해 collect 메소를 수행한다. 

collect()는 모든 값들을 한 곳으로 모으는 종단 연산이다. 

 

stream filter()

filter() 메소드는 필요 없는 데이터나 웹 요청들을 걸러낼 때 사용한다. 

filter() 메소드 안에서 조건식을 통과한 데이터만 종단 연산으로 넘어간다.

 

예시

점수가 80점 이상인 학생들만 뽑아낼 때

studentList.stream().filter(student -> student.getScore() > 80).forEach(student -> System.out.println(student.getName()));

 

Stream을 다시 한번 정리해 보자

스트림은 Collection과 같이 목록을 처리할 때 유용하게 사용된다.

 

스트림 생성 - 중개 연산 - 종단 연산으로 구분된다.

생성은 stream() 메소드를 호출해 Stream 타입을 생성

중개 연산은 데이터 가공시 사용되며, 연산 결과로 Stream 타입 리턴, 여러개 가능

종단 연산은 스트림 처리를 마무리하기 위해 사용되며, 숫자값을 리턴하거나 목록형 데이터를 리턴

 

중개 연산 종류

  • filter()
  • map(), mapToInt(), mapToLong(), mapToDouble()
  • flatMap(), flatMapToInt(), flatMapToLong(), flatMapToDouble()
  • distinct()
  • sorted()
  • peek()
  • limit()
  • skip()

 

종단 연산 종류

  • forEach(), forEachOrdered()
  • toArray()
  • reduce()
  • collect()
  • min(), max(), count()
  • anyMatch(), allMatch(), noneMatch()
  • findFirst(), findAny()

 

728x90