인프런에서 주최하는 Warming-up 클럽 0기 백엔드 스터디에 참여하고 있다.
스터디에 참여하면서 배우게 된 내용을 전체적으로 정리하고 과제로 수행했던 내용들을 정리해 보고자 한다.
(1) 3일 차 : 2024-02-21(Wed)
(2) 과제 수행 GitHub : https://github.com/twojun/java8_core_study
1. 익명 클래스(Anonymous Class)와 람다식
1-1. 익명 클래스란?
(1) 익명 클래스는 의미 그대로 이름이 없는 클래스를 말한다.
1-2. 익명 클래스의 특징
(2) 일반적으로는 특정 클래스를 상속받아 재정의해서 사용하기 위해서는 자식 레벨의 클래스를 만들고 부모 클래스를 상속받아서 기능들을 재정의하고 해당 자식 클래스의 인스턴스를 생성해 사용하게 된다.
(3) 하지만 익명 클래스의 경우 클래스 정의와 동시에 객체 생성이 가능하다.
(4) 익명 클래스는 새로운 클래스를 익명으로 사용하는 것이 아닌, 이미 정의된 클래스의 멤버들을 재정의해서 사용하고 싶을 때, 일회성으로 진행되야 할 때 사용할 수 있다. (부모의 자원을 일회성으로 재정의하여 사용하기 위한 용도)
(5) 한 가지 주의해야 할 점은, 익명 클래스의 인스턴스를 생성하면 부모에서 오버라이딩한 메서드만 사용 가능하고 새롭게 정의한 메서드는 호출할 수 없다.
(6) 아래와 같이 예시 코드를 확인해 보자.
1-3. 예시 코드
class Creature {
public void bark() {
System.out.println("동물이 울거나 짖는다.");
}
public void bark2(Creature creature) {
creature.bark();
}
}
public class CreatureMain {
public static void main(String[] args) {
Creature cat = new Creature() {
@Override
public void bark() {
System.out.println("고양이가 운다.");
}
public void barkkkkkk() {
System.out.println("고양이가 또 우네?."); // 자식 레벨에서 정의한 메서드 호출 불가 (사용X)
}
};
cat.bark(); // 고양이가 운다.
Creature dog = new Creature();
dog.bark2(new Creature() {
@Override
public void bark() {
System.out.println("개가 짖기도 한다.");
}
});
}
}
// 출력 결과
// 고양이가 운다.
// 개가 짖기도 한다.
1-4. 결론
(1) 실무에서는 멀쩡한 클래스들을 놔두고 굳이 익명 클래스를 만들어서 사용하는 일은 거의 없다. 익명 클래스의 힘은, 아래의 예시처럼 자바의 인터페이스를 익명 객체로 선언하여 사용할 때 발휘된다.
public interface MyRunnable {
public void run();
}
(1) 위와 같은 인터페이스가 정의되어 있다
public class MyRunnableMain {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable() {
@Override
public void run() {
System.out.println("MyRunnable run()");
}
};
myRunnable.run(); // MyRunnable run()
}
}
(2) myRunnable 인터페이스를 구현한 익명 클래스의 인스턴스에서 오버라이딩된 메서드를 사용할 수 있지만 기존에 부모 레벨에서 정의되지 않고 새롭게 자식 레벨에서 생성한 메서드는 호출 불가능하다.
public class RunnableExecute {
public void execute(MyRunnable myRunnable) {
myRunnable.run();
}
}
(3) 위와 같이 RunnableExecute라는 새로운 클래스를 생성했다.
(4) 해당 클래스의 내부 메서드는 MyRunnable 객체를 전달인자로 받는 메서드이다.
public class MyRunnableMain2 {
public static void main(String[] args) {
/** MyRunnable을 구현하고 있는 익명 클래스를 myRunnable에 참조하도록 함 */
MyRunnable myRunnable = new MyRunnable() {
@Override
public void run() {
System.out.println("hello!");
}
};
RunnableExecute runnableExecute = new RunnableExecute();
runnableExecute.execute(myRunnable); // hello!
}
}
(5) 그렇게 되면 위와 같이 execute() 메서드를 호출하면 hello!가 출력된다.
public class MyRunnableMain3 {
public static void main(String[] args) {
RunnableExecute runnableExecute = new RunnableExecute();
runnableExecute.execute(new MyRunnable() {
@Override
public void run() {
System.out.println("hello Main3!");
}
});
}
}
(6) 위와 같이 익명 객체를 직접 파라미터로 넘길 수도 있다.
1-5. 이러한 익명 객체를 람다식(Lambda expression)을 통해 편하게 처리할 수 있다.
public class MyRunnableMain4WithLambda {
public static void main(String[] args) {
RunnableExecute runnableExecute = new RunnableExecute();
runnableExecute.execute( () -> {
System.out.println("hello Main3!");
}
);
}
}
(1) 위처럼 람다식을 사용해 익명 객체를 항상 만들 수 있는 것은 아니고 아래의 두 가지 조건을 만족시켜야 한다.
- 인터페이스로만 만들 수 있다.
- 하나의 추상 메서드가 선언되어 있는 인터페이스(함수형 인터페이스)만 가능하다. (default 메서드 제외)
2. 함수형 프로그래밍(Functional Programming, FP) & 람다 표현식
2-1. 함수형 프로그래밍?
(1) 함수형 프로그래밍이란 함수를 일급 객체(First-class)로 취급하고 이러한 함수들 간의 조합으로 프로그램을 구성하는 프로그래밍 방법론을 의미한다.
(2) 위에서 잠시 언급된 람다식은 이러한 함수형 프로그래밍을 지원하기 위한 문법 중 하나로 함수를 간결하고 효율적으로 표현할 수 있는 문법적 표현을 말한다. 자바에서는 메서드를 간결하고 효율적으로 표시하기 위한 문법이 된다.
2-2. 람다 표현식(람다식, Lambda Expression)
(1) 위에서 본 것처럼 람다식은 자바 8에서 생긴 변화 중 가장 큰 변화가 람다 표현식의 도입이다.
(2) 람다식은 자바의 메서드를 간결한 형태의 함수식으로 표현한 형태를 말하며, 람다식을 사용하면 익명 클래스로 처리했던 부분들을 간소화하고 가독성 있게 처리할 수 있다. 람다식으로 표현되는 메서드(함수)는 일급 객체로 정의된다.
(3) 이러한 람다식은 함수 자체를 전달인자에 보내거나 변수에 저장하는 행위(일급 객체의 특성)가 가능한데 이때 데이터 타입은 무엇으로 할 것인가에 대한 질문(데이터 타입을 전달인자로 보내거나, 변수에 저장할 건데?)이 생기게 된다. 따라서 자바는 아래와 같이 결정하기로 한다.
(4) 자바의 데이터 타입은 Primitive Type, Reference Type 두 가지가 존재한다. 자바에서는 아무런 클래스의 메서드를 람다식으로 적용할 수는 없게 했고, 인터페이스 타입을 가능하도록 했는데, 이때 인터페이스가 가지는 조건은 하나의 추상 메서드를 갖는 함수형 인터페이스만 가능하도록 했고, 따라서 함수형 인터페이스만이 람다식을 담을 수 있도록 했다.
@FunctionalInterface
public interface FunctionalInterfaceSample {
public abstract void testMethod();
}
(5) 함수형 인터페이스의 예시인데 위의 코드처럼, 우리가 직접 함수형 인터페이스를 구현할 일은 별로 없다. 자바에서 이미 효율적으로 함수형 인터페이스를 모두 만들어 놓았기 때문이다. 기본적으로 제공하는 함수형 인터페이스를 다양한 상황과 용도에 맞게 사용해서 코드를 간결하고 가독성 있게 작성할 수 있게 된다.
(6) 결과적으로, 람다식은 함수형 인터페이스의 추상 메서드를 구현하기 위한 간결한 문법이다.
2-3. 자바의 대표적인 함수형 인터페이스
함수형 인터페이스 | 추상 메서드 | 기능 |
Predicate<T> | boolean test(T t) | 람다식을 수행하는데 파라미터로 별도의 값을 넘긴다.이후 True, false를 반환한다. 주로 데이터들에 대해 필터링하고 조건을 검사한 후 참/거짓을 반환하는 역할을 수행한다. |
Consumer<T> | void accept(T t) | 람다식을 수행하는데 파라미터로 별도의 값을 넘긴다. 수행 후 반환값은 받지 않는다. |
Function<T, R> | R apply(T t) | 람다식을 수행하는데 파라미터로 별도의 값을 넘긴다. 수행 후 별도의 반환 결과를 얻을 수 있다. |
Supplier<T> | T get() | 람다식을 수행하는데 파라미터로 별도의 값을 넘기지 않는다 수행 후 반환 결과를 얻을 수 있다. |
2-4. 기본형 타입을 지원하기 위한 함수형 인터페이스
함수형 인터페이스 | 기본형에 특화된 함수형 인터페이스 |
Predicate<T> | IntPredicate, LongPredicate, DoublePredicate ... |
Consumer<T> | IntConsumer, LongConsumer, DoubleConsumer ... |
Function<T, R> | IntFunction, IntToDoubleFunction ... |
Supplier<T> | BooleanSupplier, IntSupplier ... |
(1) 특화 인터페이스가 존재하는 이유
- 기본형에 특화된 인터페이스는 기본형 타입을 참조 타입의 래퍼 클래스로 바꾸는 오토박싱, 참조 타입을 기본형 타입으로 변환하는 오토 언박싱 기능이 제외되어 있다.
- 연산 과정에서 이러한 오토박싱, 오토 언박싱 기능을 수행하면 성능 저하의 이슈가 있을 수 있기 때문에 위와 같이 박싱 기능이 빠진 별도의 인터페이스가 존재한다.
2-4. 람다 표현식 기본적인 작성 방법
(1) 파라미터의 타입이 추론 가능하면 타입을 지정하지 않아도 된다.
(2) 파라미터 개수가 2개 이상인 경우가 아니라면 소괄호를 적지 않아도 된다.
list.stream().
forEach((String str) -> System.out.println(str));
// AFTER
list.stream().
forEach(str -> System.out.println(str));
2-5. 메서드 참조(Method Reference)
(1) 메서드 참조를 적용하면 아래와 같이 코드를 줄일 수 있다.
(2) 메서드 참조에 대한 부분은 아래에서 별도로 정리하겠다.
list.stream().
forEach(System.out::println);
3. 메서드 참조(Method Reference)
3-1. 메서드 참조
(1) 메서드 참조는 기존의 람다식을 더 간결하게 표현해 주는 문법이다.
(2) 실행하려는 람다식의 메서드를 참조해서 파라미터의 정보, 반환 타입을 추론한 뒤 람다식에서 선언이 불필요한 부분을 제거하고 람다식을 더 간단히 표현하는 것이다.
(3) 메서드 참조의 경우 클래스가 메서드를 참조하는 기호를 .에서 ::으로 변환하여 클래스명::메서드명으로 표기한다.
public class LambdaTypeExam {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Java");
list.add("TypeScript");
list.add("Python");
list.stream().
forEach(System.out::println); // 메서드 참조
}
}
(4) 위의 예제에서 사용된 람다식이지만 형태를 살펴보면 반환값 없이 System 클래스의 출력 메서드를 호출하고 있을 뿐이다.
(5) 또한 람다식의 파라미터 역시 출력 메서드의 파라미터로 그대로 들어가기 때문에 코드의 중복으로 간주하고 이 부분을 간략화 한 것이 메서드 참조이다.
4. Stream API
4-1. 스트림이란?
(1) 스트림은 다양한 데이터 소스를 표준화된 방법으로 다루는 것을 말하며 컬렉션, 배열 등의 데이터 요소들을 함수형 프로그래밍(람다식)의 스타일로 처리할 수 있는 다양한 기능을 제공하고 있다.
(2) 스트림은 컬렉션 프레임워크를 통해 관리되는 데이터를 효율적으로 처리하기 위해 주로 사용된다.
4-2. 스트림 활용 예시
public class StreamExam001 {
public static void main(String[] args) {
List<Customer> customerList = new ArrayList<>();
customerList.add(new Customer("A", 23));
customerList.add(new Customer("B", 21));
customerList.add(new Customer("C", 11));
customerList.add(new Customer("D", 35));
customerList.add(new Customer("E", 56));
customerList.add(new Customer("F", 12));
customerList.add(new Customer("G", 45));
customerList.add(new Customer("H", 23));
customerList.add(new Customer("I", 89));
List<String> customersBySorted = customerList.stream()
.filter(customer -> customer.getAge() > 30)
.sorted(Comparator.comparing(Customer::getAge))
.map(customer -> customer.getName())
.collect(Collectors.toList());
for (String name : customersBySorted) {
System.out.println(name);
}
}
}
(1) 스트림 객체를 얻어온다.
(2) filter, sorted, map 연산들은 중간 연산이고 반환 시 스트림 객체를 반환했다.
(3) collect를 통해 최종 연산을 수행한다.
4-3. 스트림의 파이프라인 연산 - 중간 연산(Intermediate operation)
(1) 스트림의 연산은 중간 연산, 최종 연산이 존재한다.
(2) 중간 연산의 경우 filter, map과 같은 연산으로 스트림을 반환한다.
(3) 중간 연산의 경우 메서드 체이닝으로 코드를 작성할 수 있다.
(4) 최종 연산이 실행되어야 중간 연산이 처리된다. 따라서 최종 연산이 없는 경우 메서드 체이닝은 실행되지 않는다.
중간 연산 메서드 | 반환 형식 | 연산에 필요한 인수 |
filter() | Stream<T> | Predicate<T> |
map() | Stream<T> | Function<T, R> |
limit() | Stream<T> | |
sorted() | Stream<T> | Comparator<T> |
distinct() | Stream<T> | |
peek() | Stream<T> | Consumer<T> |
skip() | Stream<T> |
4-4. 스트림의 파이프라인 연산 - 최종 연산(Terminal operation)
(1) 최종 연산의 경우 forEach, collect()과 같은 연산으로 반환을 하지 않거나 컬렉션 타입을 반환할 수 있다.
(2) 중간 연산을 통해 가공된 스트림들은 마지막 최종 연산을 거쳐야 각 요소를 소모하며 결과를 출력한다.
(3) 최종 연산 이후에는 스트림은 종료되고 재사용이 불가능하다. 추가 연산이 필요할 경우 스트림을 재생성해야 한다.
최종 연산 메서드 | 반환 형식 |
forEach() | 스트림의 각 요소를 소비하며 람다식을 적용한다. (반환결과 없음) |
count() | 스트림의 요소 수를 Long 형태로 반환한다. |
collect() | List, Map의 형태로 반환한다. |
sum() | 스트림의 모든 요소에 대한 합을 반환한다. |
reduce() | 스트림의 요소를 하나씩 줄여가며 연산 수행 후 결과를 반환한다. (Optional 형태로 반환된다.) |
5. 개인 회고
(1) 하이버네이트와 같은 ORM 기술을 사용하면서 람다와 스트림이 자주 사용되고, 코드의 가독성을 올려주는 데 효과적이라고 생각했었으며 이번 계기로 이 부분을 다시 한 번 정리해 볼 수 있었다.
(2) 위와 같은 내용들을 무작정 외우기 보다는, 코드를 많이 쳐보면서 스트림, 람다의 코드 스타일을 본인의 것으로 만드는 과정이 더 중요하다고 생각했다.
※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.
개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다!
'기록, 회고 > InFlearn Warming-up 0기 BE' 카테고리의 다른 글
[4일 차] - 과제 수행 : API 개발 연습 (0) | 2024.02.21 |
---|---|
[4일 차] - 내용 정리, 개인 회고 (0) | 2024.02.21 |
[3일 차] - 내용 정리, 개인 회고 (0) | 2024.02.20 |
[2일 차] - 과제 수행 : GET, POST API 설계 (4) | 2024.02.19 |
[2일 차] - 내용 정리, 개인 회고 (2) | 2024.02.19 |
댓글