함수형 프로그래밍
https://dinfree.com/lecture/language/112_java_9.html
자바
자바에서 함수형 프로그래밍 구문인 람다 표현식의 사용법과 함수형 인터페이스 그리고 스트림 API 활용법을 배우기 위한 강좌 입니다.
dinfree.com
잘 이해가 안되어서 여기 사이트 참조 많이 했음.
https://www.hanbit.co.kr/store/books/look.php?p_code=B1795688037
이것이 자바다(3판)
최신 JAVA 21 버전 반영! 9년 동안 꾸준히 사랑받은 자바 베스트셀러, 『이것이 자바다』 3판!
www.hanbit.co.kr
이 책에서 설명하길 함수형 프로그래밍을 위해 람다식을 지원한다고 했는데.. 내가 알고 있는 함수형 프로그래밍은 저것만이 아닌 더 복잡한 내용이었음.
1. 함수는 같은 인풋에 항상 같은 아웃풋을 보장해야한다. : 순수 함수(pure function)
결론 적으로 이 내용을 전혀 람다식은 보장해주지 못함... 람다식도 결과론적인 정체는 객체이고, 그럼 매서드 안에 람다식을 사용하게 되면 호출될때마다 new 객체 해주기 때문에 안에서 무슨 짓거리를 하든 결국 아웃풋을 보장한다는건가? 라는 생각을 했지만 결과는 아니었음. 매서드 호출마다 new 객체를 하는건 메모리와 속도상 불가능하다. 컴파일러는 람다를 사용하는 객체 내부에 인스턴스 하나만 생성해두고 걔를 반복 사용하기에 만약 내부에서 "순수성을 해치는 순간" 함수형 프로그래밍의 결과는 보장되지 못한다. 는게 결론 즉. 개발자가 잘해라.
2. 함수는 Input Output에 전부 사용 가능하다. :고차함수(High Order Function)
수학에서 f(x)도 있고 f(g(x)) = h(x) 도 있듯이 함수 안에 함수. 그 결과는 또 함수 등등 이 가능하다.
기존 객체 지향에서는 불가했냐? 불가했다. 왜냐? 매개변수의 결과는 반드시 정해져있는 값이나 객체 스태틱 타입이어야 했다. 함수를 쓰는것 같고, return 함수 하는거 같아도. 결국 내부적으로는 정해진 값을 컴파일러 단계에서 충분히 추론 가능했기에 사용이 가능했던 것이다.
예를 들어
public void process (Strategy strategy){
strategy.execute();
}
//이런식으로 매개변수에 값이 아니면 객체를 전달하여 해당 객체의 함수를 동작 시켰다.
//객체는 그냥 단순 함수만 존재하는게 아니다. 필드도 있고,
//다른 매서드도 있는데 객체 하나만 보고서 뭘 믿고 동작이 예측이 가능한가???
//그래서 추가된 개념이 함수형인터페이스와 람다식이다.
public class ModernJava {
// 진짜 함수를 매개변수로 받을 수 있음
public void process(Function<Integer, Integer> func) {
int result = func.apply(10);
}
// 함수를 반환할 수도 있음
public Function<Integer, Integer> createFunction() {
return x -> x * 2;
}
public static void main(String[] args) {
ModernJava mj = new ModernJava();
// 함수를 변수에 할당
//함수형인터페이스인 Function<T, R> 와 추상매서드인 R apply(T);의 합작품
Function<Integer, Integer> func = x -> x + 1;
// 함수를 매개변수로 전달
//실상 객체를 던진다는 객체지향 언어의 한계를 벗어날 순 없지만
//함수형 인터페이스로 제한을 두어 어차피 함수 한개만 던진다는 개념으로
//개념적으로 함수형 프로그래밍을 보장하는 것.
mj.process(func);
// 함수를 반환값으로 받음
// 이또한 실상 함수형 인터페이스 인스턴스를 새로 생성하여 할당하는 것이지만
// 개념적으로 어차피 함수 apply하나빼고는 암것도 없는 놈이라는 것으로 개념적 함수형 프로그래밍 구현
Function<Integer, Integer> newFunc = mj.createFunction();
}
}
3. 비상태 Stateless 불변성 Immutability 선언형 Expressions
객체지향 언어와 함수형 언어의 큰 차이가 존재하는데 바로 뭐 원론적인 내용 다 갖다버리고 바로 눈앞에 보이는건 "성능" 차이이다. 왜 현대의 모든 언어들이 "객체지향"이냐면 결국 성능이다.
상태를 유지하지 않으며, 불변해야하기에 절대적으로 메모리 효율성이 극도로 낮아지고 선언형이어야하기에 반드시 무슨 행동을 할지만을 선언하여 모든 프로그래밍을 동작시키려면 메모리 용량도 용량인데 쓰기 읽기 동작이 굉장히 비효율적으로 작동할 수 밖에 없게 된다.
그럼에도 불구하고 다시 "함수형 프로그래밍"을 지원하여 과거로 돌아가려는 이유는
"객체지향 언어"의 근간으로 성능은 확보하고 "함수형 프로그래밍"의 장점인 불변성과 높은 추상화로 방대해진 코드를 분할하여 생각하게 하기 위해서이다.
객체지향을 체화하고 이를 함수형으로 리팩토링 하는 방식으로 발전해야 할 것 같다. 이 방식이 익숙해지면 처음부터 객체지향과 함수형을 적절히 사용하는 경지에 오를 듯
그래서 람다식
일단 식이다. 문이 아니라 식이므로 한줄 표현이 가능하고 사실 2줄 3줄도 가능한데 "이펙티브 자바" 라는 책에선 3줄 이상을 넘어가면 해당 로직을 다시 생각해보라고 한다.
람다식으로 표현가능한 건 함수형 인터페이스를 만족할때 이다.
함수형 인터페이스: 추상 매서드가 하나뿐인 인터페이스
//매개변수가 없는 경우
()->{}
//매개변수가 있는 경우
(인자) -> {}
//리턴 값이 있는 경우
() -> 값
() -> {return 값;}
메소드 참조
어떤 클래스의 메소드를 람다식의 구현부로 대체하고 싶을 경우에 람다식을 더 간략히 줄일 수 있음
(a, b) -> Math.max(a, b);
//위 식을 아래처럼 사용해버리는게 가능하다는거
Math :: max;
//저 :: 을 기준으로 클래스와 매서드를 분리하고 해당 매서드의 인자로
//람다식 구현부를 작성해버리겠다는 뜻 즉 맨 위와 같이 변환해서 사용됨.
Math m = new Math();
m :: min;
//이것도 가능함.
1. 컴파일러의 추론
실제 값을 보고 제네릭 타입이 결정되어짐. 메소드와 일치시켜야되는건 반환타입과 매개변수 타입 and 개수
2. 함수형 인터페이스와 매칭
// Function<T,R> 인터페이스와 매칭되는 경우
Function<Integer, Integer> squareFunction = StaticMethodReferenceExample::square;
// Predicate<T> 인터페이스와 매칭되는 경우
Predicate<Integer> positiveCheck = StaticMethodReferenceExample::isPositive;
3. 메서드 시그니처(반환, 매개변수) 일치해야 에러가 아님
// 컴파일 에러! 매개변수 개수가 맞지 않음
Function<Integer, Integer> wrong = Math::max; // max는 매개변수가 2개라서 불가능
// 정상 동작
BiFunction<Integer, Integer, Integer> correct = Math::max;
4. 타입 변환이 필요하다면 명시적으로
List<Integer> numbers = Arrays.asList(1, 2, 3);
// 컴파일 에러! Double을 받는 메소드에 Integer를 전달할 수 없음
numbers.stream().map(Math::sin); // sin은 double을 받음
// 해결방법: 명시적 형변환이 필요한 경우 람다식 사용
numbers.stream().map(x -> Math.sin(x.doubleValue()));
매개변수의 메서드 참조
여기가 좀 많이 어려웠네.
클래스명 :: 인스턴스메서드명
-> 왜? 스태틱메서드명은 아니지? 실제로 스태틱으로 지정된 매서드를 사용하게 될경우 에러가 날 가능성이 매우 높다.
매개변수 메서드 참조는 규칙을 가지고 있는데 순서가 그 규칙이다.
클래스::인스턴스 메서드명 == (a)->a.인스턴스메서드명() or (a, b)-> a.인스턴스메서드명(b) or (a, b, c) -> a.인스턴스메서드명(b, c) 이런식으로 규칙적으로 매핑하여 해석한다. 이런 상황이 아니라면 명시적인 람다식을 사용할것.
클래스::스태틱메서드명 == (a) -> 클래스.스태틱메서드(a) or (a, b) -> 클래스.스태틱메서드(a, b)
매개변수 메서드 참조의 예시
class Person {
String getName() { return name; }
int compareByAge(Person other) { return this.age - other.age; }
void sendEmailTo(String address, String content) { /* ... */ }
}
// 매개변수의 메소드 참조 규칙:
// - 첫 번째 매개변수가 항상 메소드의 수신자(receiver)가 됨
// - 나머지 매개변수들이 순서대로 메소드의 매개변수가 됨
// 1. 매개변수 1개
Function<Person, String> f1 = Person::getName;
// 해석: (person) -> person.getName()
// 2. 매개변수 2개
Comparator<Person> f2 = Person::compareByAge;
// 해석: (person1, person2) -> person1.compareByAge(person2)
// 3. 매개변수 3개
TriFunction<Person, String, String, Void> f3 = Person::sendEmailTo;
// 해석: (person, addr, content) -> person.sendEmailTo(addr, content)
스태틱 메서드 참조의 예시
class Calculator {
static int add(int a, int b) { return a + b; }
static int multiply(int a, int b, int c) { return a * b * c; }
}
// 정적 메소드 참조 규칙:
// - 모든 매개변수가 순서대로 스태틱 메소드의 매개변수로 전달됨
// 1. 매개변수 2개
BiFunction<Integer, Integer, Integer> f1 = Calculator::add;
// 해석: (a, b) -> Calculator.add(a, b)
// 2. 매개변수 3개
TriFunction<Integer, Integer, Integer, Integer> f2 = Calculator::multiply;
// 해석: (a, b, c) -> Calculator.multiply(a, b, c)
컴파일러 입장이되어 생각해보는 프로그래머가 미운 상황
class Example {
static void staticMethod(String s) { }
void instanceMethod(String s) { }
void test() {
// 에러! 스태틱 메소드를 매개변수의 메소드 참조로 사용
Function<String, Void> f1 = String::staticMethod; // 컴파일 에러
// 에러! 매개변수 순서가 규칙과 맞지 않음
BiFunction<String, String, String> f2 = String::substring;
// substring은 (int, int)를 매개변수로 받기 때문에 불가능
}
}
//=============모호한 케이스================
public class AmbiguousCase {
public void process(String s) { }
public static void process(Object o) { }
public static void main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c");
// 컴파일 에러! process가 인스턴스 메소드인지 스태틱 메소드인지 모호함
// list.forEach(AmbiguousCase::process);
// 해결방법 1: 명시적 람다식 사용
AmbiguousCase obj = new AmbiguousCase();
list.forEach(s -> obj.process(s)); // 인스턴스 메소드 사용
list.forEach(s -> AmbiguousCase.process(s)); // 스태틱 메소드 사용
// 해결방법 2: 타입을 명확히 지정
Consumer<String> consumer1 = obj::process; // 인스턴스
Consumer<Object> consumer2 = AmbiguousCase::process; // 스태틱
}
}
//=========그냥 메소드 참조용으로 설계가 안되는 경우=======
public class MethodReferenceLimit {
// 1. 매개변수 순서가 다른 경우
public void example1() {
TriFunction<String, String, String, Integer> func1
= (a, b, c) -> b.indexOf(c + a); // 람다로는 가능
// String::indexOf // 불가능!
}
// 2. 추가 매개변수가 있는 경우
public void example2() {
BiFunction<String, String, String> func2
= (a, b) -> a.substring(1, b.length()); // 람다로는 가능
// String::substring // 불가능!
}
// 3. 매개변수를 여러번 사용하는 경우
public void example3() {
BiFunction<String, String, String> func3
= (a, b) -> a.replace(b, b.toUpperCase()); // 람다로는 가능
// String::replace // 불가능!
}
}
번외 Comparable Comparator... Sort는 대체 정체가 뭐야??
먼저 대표적으로 함수형 인터페이스를 사용하는 녀석인 sort를 보면
// Collections.sort의 두 가지 형태
public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T> void sort(List<T> list, Comparator<? super T> c)
즉 오버로딩을 통해 2가지 방식을 구현하고 있음.
컴파일러는 제네릭과 람다식의 구현부를 자세히보면서 매개변수 사용방식을 검사하여 어떤 sort를 사용할지 결정함.
public class SortingExample {
// Case 1: Comparable 구현
public static class Person implements Comparable<Person> {
private String name;
private int age;
@Override
public int compareTo(Person other) {
return this.age - other.age;
}
}
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
// 방법 1: Comparable 사용 (compareTo)
Collections.sort(people); // Person의 compareTo 사용
// 방법 2: Comparator 제공 (compare)
Collections.sort(people, (p1, p2) -> p1.name.compareTo(p2.name));
// 방법 3: 둘 다 있는 경우
// → Comparator가 우선! (명시적으로 제공된 것이 우선)
Collections.sort(people, (p1, p2) -> p2.age - p1.age); // 나이 역순
}
}
//=즉 클래스가 Comparable을 상속하는 녀석이라 compare함수가 있는지 없는지 여부가 중요
public class SortingError {
public static class BadPerson { // Comparable 구현 안 함
private int age;
}
public static void main(String[] args) {
List<BadPerson> people = new ArrayList<>();
// 컴파일 에러!
// BadPerson은 Comparable을 구현하지 않았고,
// Comparator도 제공하지 않았기 때문
Collections.sort(people); // 에러!
// 해결방법: Comparator 제공
Collections.sort(people, (p1, p2) -> p1.age - p2.age); // OK
}
}
생성자 참조
클래스명 :: new
메서드 참조랑 너무 다른 규칙을 가지고 있다는걸 보여 주는 예시 일반적인 반환예시가 마지막 값인걸 이용하여
구현되어 있는 듯
class Person {
Person() { } // 생성자 1
Person(String name) { } // 생성자 2
Person(String name, int age) { } // 생성자 3
}
public class ConstructorRules {
public static void main(String[] args) {
// 1. 매개변수 없는 생성자
Supplier<Person> c1 = Person::new;
// 해석: () -> new Person()
// 2. 매개변수 1개 생성자
Function<String, Person> c2 = Person::new;
// 해석: (name) -> new Person(name)
// 3. 매개변수 2개 생성자
BiFunction<String, Integer, Person> c3 = Person::new;
// 해석: (name, age) -> new Person(name, age)
}
}
'Learn > 이것이 자바다' 카테고리의 다른 글
Stream Interface (1) | 2024.12.12 |
---|---|
[이것이 자바다 확인문제] chapter 16 (0) | 2024.12.11 |
[이것이 자바다 확인문제] chapter 15 (0) | 2024.12.10 |
컬렉션, List, Set, Map, 검색, Stack,Queue, Synchronizaton, immodified (1) | 2024.12.10 |
[이것이 자바다] chapter 13 확인문제 (0) | 2024.12.04 |