람다식의 도입으로 인해, 자바는 객체지향언어인 동시에 함수형 언어가 되었다..!
람다 표현식
- 람다는 익명 함수를 말한다. 즉, 이름이 없는 간단한 함수를 작성하는 방식
- 반환 타입과 메서드 이름을 제거하고 매개변수 옆에 -> 를 추가한다.
//기존 코드
반환타입 메서드이름 (매개변수 선언) {
문장들
}
//람다식 도입
(매개변수 선언) -> {문장들}
예제1
//기존 코드
int max(int a, int b){
return a > b ? a : b;
}
//람다식 도입
(int a, int b) -> { return a > b ? a : b}
//return문 대신 식으로 대신 할 수 있다. 식의 연산 결과가 자동적으로 변환값이 된다.
(int a, int b) -> a > b ? a : b
//람다식에 선언된 매개변수의 타입은 추론이 가능한 경우 생략이 가능하다. 대부분 생략함
(a, b) -> a > b ? a : b
주의!) 매개변수가 하나일 때는 괄호() 생략 가능 (타입이 없을 때만), 문장이 하나 뿐일 때는 { } 생략 가능!
예제 2
//기존 코드
void printVar (String name, int i) {
System.out.println(name + " = " + i);
}
//람다 도입
(name, i) -> System.out.println(name + " = " + i)
예제 3
//기존 코드
int square (int x) {
return x * x;
}
//람다식 도입
x -> x * x
예제 4
//기존 코드
int roll() {
return (int)(Math.random() * 6);
}
//람다식 도입
() -> (int)(Math.random() * 6)
예제 5
//기존 코드
int sumArr (int[] arr) {
int sum = 0;
for(int i : arr) {
sum += i;
}
return sum;
}
//람다식 도입
(int[] arr) -> {
int sum = 0;
for(int i : arr) {
sum += i;
}
return sum;
}
그래서 자바는 람다식은 익명 함수?가 아니라 익명 객체이다 왜냐면 자바에서는 메서드가 독립적으로 존재할 수 없기 때문이다.
//익명 객체 (객체의 생성과 선언을 동시에)
Object = new Object() {
int max(int a, int b) {
retunr a > b ? a : b;
}
}
//람다식 표현 => 따라서 람다식 자체는 객체이다!!
??? obj = (a, b) -> a > b ? a : b
그럼 람다식 자체가 객체이면 참조 변수가 필요할텐데 참조 변수 타입은 뭐인가?
그럼 바로 위 코드처럼 Object 타입으로 변수를 받으면?
Object obj = (a,b) -> a > b ? a : b;
int value = obj.max(3,5); //에러남
왜 에러가 나냐..? => 바로 최상위 부모 클래스인 Object에는 당연히도 max() 메서드가 없기 때문이다!
그럼 이 문제를 해결할 수 있는게 바로 함수형 인터페이스이다.
함수형 인터페이스
함수형 인터페이스란? 추상 메서드가 딱 1개만 있는 인터페이스 (단, default 메서드나 static 메서드는 존재 가능)
자바에서 모든 메서드는 클래스 내에 포함되어야 하는데, 람다식은 어떤 클래스에 포함되는 걸까 ?
=> 람다식은 익명 클래스의 객체와 동등하다.
//함수형 인터페이스 생성
@FunctionalInterface //이 애노테이션을 적어주면 함수형 인터페이스를 잘 적었는지 확인해준다
interface MyFunction {
public abstract int max(int a, int b);
}
//익명 클래스 선언과 생성 동시에
MyFunction f = new Myfunction() {
public int max(int a, int b) {
return a > b ? a : b;
}
};
int value = f.max(3,6) // Ok 해결
//익명 클래스를 람다식으로
MyFunction f = (a, b) -> a > b ? a : b
"f.max(3,6)을 하면 추상 메서드와 람다식을 연결시켜준다고 생각하면 된다"
저렇게 긴 익명 클래스를 람다식으로 짧게 바꿔주었다.. 매우 편하다
java.util.function
일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓은 것.
(T는 Type, R은 Return Type)
java.lang.Runnable : 매개변수도 업소, 반환값도 없음
Supplier<T> : 매개변수는 없고, 반환값만 있음 (공급자라 반환(공급)만 해준다고 생각)
Supplier<Double> randomSupplier = () -> Math.random(); //매개변수 없음
System.out.println(randomSupplier.get()); // e.g., 0.345234
Consumer<T> : Supplier와 반대로 매개변수만 있고, 반환값이 없음 (소비자라 대입(소비)만 해준다고 생각)
Consumer<String> printer = s -> System.out.println("Hello, " + s);
printer.accept("world"); // "Hello, World"
Function<T,R> : 일반적인 함수. 한개의 매개변수를 받아서 결과를 반환
Function<Integer, String> intToStr = i -> "Number : " + i;
System.out.println(intToStr.apply(5)); // "Number : 5"
"
추가 메서드:
- andThen() : 변환 후 추가 작업 연결.
- compose() : 변환 전에 작업 추가.
"
Predicate<T> : 조건식을 표현하는데 사용됨. 매개변수는 하나, 반환 타입은 boolean
Predicate<String> isLong = s -> s.length() > 5;
System.our.println(isLong.test("Lambda")); //true
"
추가 메서드 :
- and() : 두 조건을 AND로 결합.
- or() : 두 조건을 OR로 결합.
- negate() : 조건의 반대를 반환
"
BiFunction : 두 개의 매개변수를 받아 값을 반환
BiFunction<Integer, Integer, String> sumToString = (a, b) -> "Sum : " + (a + b);
System.out.println(sumToString.apply(3,7)); // "Sum : 10"
메서드 참조 (클래스 이름 :: 메서드 이름)
하나의 메서드만 호출하는 람다식은 '메서드 참조'로 간단히 할 수 있다.
| 종류 | 람다 | 메서드 참조 |
| static 메서드 참조 | (x) -> ClassName.method(x) | ClassName::method |
| 인스턴스 메서드 참조 | (obj.x) -> obj.method(x) | ClassName::Method |
static 메서드 참조
Integer method(String s) { //그저 Integer.parseInt(String s)만 호출
return Integer.parseInt(s);
}
//람다식으로 변환
Function<String, Integer> f = (String s) -> Integer.parseInt(s)
"Function 인터페이스는 입력 값으로 String, 출력 값으로 Integer 라는 것을 아니깐 s를 지워줘도 된다.
어차피 우리가 입력 값으로 뭘 주는지 추론이 가능하기 때문이다.
"
// 메서드 참조 변환
Function<String, Integer> f = Integer::parseInt;
"이해가 안갈 수 있다. 이럴 때는 메서드 참조에서 람다식으로 역순으로 해보자!
1. 먼저 Function 함수형 인터페이스를 보자. 입력이 String이고 출력이 Integer다.
2. 그리고 메서드가 parseInt니깐.
3. 그럼 입력에 String을 받아서 parseInt로 변화해줄 값이 필요하니 s.
"
생성자의 메서드 참조
"생성자와 메서드 참조"
Supplier<MyClass> s = () -> new MyClass(); //람다식
Supplier<MyClass> s = MyClass::new;
Function<Integer, MyClass> s = (i) -> new MyClass(i);
Function<Integer, MyClass> s = MyClass::new;
"배열과 메서드 참조"
Function<Integer, int[]> f = (i) -> new int[i]; //람다식
Function<Integer, int[]> f = int[]::new;
이렇게 보면 정의된 함수형 인터페이스들이 <입력 타입, 출력 타입> 또는 <입력 타입>, <출려 타입> 이렇게만 알면 쉽게 구현할 수 있을 것 같다!!!
스트림
실무에서 꼭 알아야할 스트림!!
스트림이란? 스트림은 데이터 흐름을 다루는 API 이다.
- 데이터 소스를 변경하지 않고 처리 : 컬렉션(ArrayList 등) 을 조작하지 않고 데이터의 복사본을 사용.
- 중간 연산과 최종 연산으로 구성 : 필터링, 매핑, 정렬 같은 중간 작업 후 데이터를 최종적으로 출력, 집계 등으로 처리 한다.
스트림 기능
1. filter : 조건에 맞는 데이터만 걸러내기
"사용 예 : 특정 조건에 맞는 데이터만 추출"
List<String> names = List.of("Alice", "Bob", "Charile");
List<String> filteredNames = names.stream()
.filter(name -> name.startWith("A")
.collect(Collectors.toList());
System.out.println(filteredNames); // ["Alice"]
//람다식 풀어서 쓴 코드
Predicate<String> Predicate = new Predicate<String>() {
@Override
public boolean test(String e) {
return e.startsWith("A");
}
};
// 람다식으로
(String e) -> e.startsWith("A")
e -> e.startsWith("A)
2. map : 데이터 변환
"사용 예 : 리스트에서 값 변환"
List<String> names = List.of("Alice", "Bob", "Charile");
List<Integer> nameLength = names.Stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(nameLengths); //[5, 3, 7]
"String::length는 메서드 참조로, 각 요소(name)의 문자열을 길이를 반환"
.map(name -> name.length)
3. forEach : 요소에 대해 작업 수행
"사용 예 : 데이터를 하나씩 출력하거나 처리"
List<String> items = List.of("A", "B", "C");
items.stream()
.forEach(item -> System.out.println("Item: " + item);
//출력 : Item: A, Item: B, Item: c
4. Collect : 결과를 모아 새로운 데이터 구조로 변환
"사용 예 : 필터링 후 결과를 리스트로 모으기"
List<String> names = List.of("Alice", "Bob", "Charile");
List<Integer> upperCaseNames = names.Stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseNames); //["ALICE", "BOB", "CHARILE"]
5.reduce : 값을 누적해 하나의 결과 만들기
"사용 예: 총합 계산"
List<Integer> numbers = List.of(1,2,3,4);
int sum = numbers.stream()
.reduce(0, Integer::sum); //초기값 0, 각 요소를 더함
System.out.println(sum); //10
실무에서 자주 활용하는 패턴
1. 데이터 필터링과 정렬
"예 : 직원 목록에서 이름이 "A"로 시작하는 직원만 정렬"
List<String> employees = List.of("Alice", "Bob", "Adam", "Charile");
List<String> filteredAndSorted = employess.stream()
.filter(e -> e.startWith("A"))
.sorted()
.collect(Collectors.toList());
System.out.println(filteredAndSorted); // ["Adam", "Alice"]
2. 데이터 그룹화
"예 : 직원 목록을 부서별로 그룹화"
class Employee {
String name;
String department;
Employees(String name, String department) {
this.name = name;
this.department = department;
}
public String getDepartment() {
return department;
}
public String getName() {
return name;
}
}
List<Employee> employees = List.of(
new Employee("Alice", "HR");
new Employee("Bob", "Engineering");
new Employee("Charile", "HR");
};
Map<String, List<Employee>> groundByDepartment = employess.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
//Employee.getDepartment
System.out.println(groundByDepartment);
//{HR=[Alice, Charile], Engineering=[Bob]}
"
1. empolyees.stream() -> 직원 목록(employees)의 스트림 생성
2. collect(Collectors.groupingBy(Employee::getDepartment))
->직원들의 부서 이름(getDepartment)을 기준으로 그룹화하고, 결과를 Map에 저장
"
3. 데이터 중복 제거(distinct)
"예 : 리스트에서 중복 제거"
List<Integer> numbers = List.of(1,2,2,3,4,4,5);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(collectors.toList());
System.out.println(distinctNumbers); // [1,2,3,4,5]
4. 데이터 제한 (limit, skip)
"예 : 데이터 일부만 처리"
List<Integer> numbers = List.of(1,2,3,4,5);
List<Integer> limitNumbers = numbers.stream()
.skip(2) // 앞의 2개 생략
.limit(2) // 이후 2개만 처리
.collect(Collectors.toList());
System.out.println(limitedNumbers); //[3,4]
스트림 종료 메서드 (collect, reduce, forEach)
'Back-End > JAVA' 카테고리의 다른 글
| 래퍼 클래스 (0) | 2024.11.19 |
|---|---|
| 불변 String 클래스와 가변 StringBuilder (1) | 2024.11.19 |
| 불변 객체 (0) | 2024.11.18 |
| 최상위 부모인 Object 클래스 (0) | 2024.11.18 |
| 자바 메모리 구조와 static (0) | 2024.11.16 |