본 문서는 2021년 12월 21일 에 기록되었습니다.
Essentials of Java 와 API 문서 와 기타 References 를 참고하였습니다.
함수형 프로그래밍이 뭔지 모른다면, 관련 내용을 구글링 하거나 PP vs OOP vs FP 를 보시길 바랍니다.
또한 인터페이스 와 지네릭스 등에 대한 이해도가 필요합니다.
또한 1등 시민으로서의 함수 에 대한 내용을 선결하여 가볍게 읽고 오시는 것을 추천드립니다.
람다 사용 시에 대한 주의사항도 읽어보시는 것을 추천드립니다.
for-loop 를 Stream.forEach() 로 바꾸지 말아야 할 3가지 이유
이 내용은 다음과 같은 구성으로 이루어져 있습니다.
서론
1.0. Function 이란?
1.1. Functional Interface 이란?
Lamda Expression
Functional Interface
3.1. Function / BiFunction / TriFunction(custom)
3.2. Supplier
3.3. Consumer / BiConsumer
3.4. Predicate
3.5. UnaryOperator
Practice
서론은 다음의 내용으로 구성되어 있습니다.
가볍게 읽어보고 API 문서를 참고하여 간단한 코드 예제를 실천하고
이후 Practice 를 통해 복잡함 문제를 풀어봅시다.
함수란?
어떠한 기능을 달성하기 위한 단위의 일환.
함수의 구성요소는 다음과 같습니다.
Functional Interface 는 기본적으로
단 하나의 abstract method 를 가지는 인터페이스 를 의미합니다.
Lamda 식과 같이 사용하며 코드의 간결함이라는 장점을 가지게 됩니다.
Method Reference 는
기존에 선언되어 있는 메서드를 지정하는 기능 을 의미합니다.
FI + Lamda 보다 더 많은 생략이 이루어지기 때문에,
Method Reference 를 사용하기 위해서는 사용할 메서드의 매개변수 및 리턴 타입을 숙지해야 합니다.
아주 짧고 간단하게 Lamda Expression 을 알아보겠습니다.
이것은 다음과 같은 구조로 생겼습니다.
(Integer x)->{
System.out.print(x);
}
(Integer x)->{
return x+3;
}
메서드와 형태만 다를 뿐 완벽하게 동일합니다.
추가적으로 알면 좋은 부분들은 다음과 같습니다.
x->System.out.print(x);
x->x+3;
Functional Interface 는 매우 많습니다.
하지만 이 중에서도 뿌리가 되거나 주요한 인터페이스 몇 개 를 알아보도록 하였습니다.
알아볼 Functional Interface 는 다음과 같습니다.
또한 사칙연산이나 배열과 연계된 출력문 등의 간단한 예제를 들어서 설명할 것입니다.
이 경우에 용어가 너무 긴 관계로 FI + Lamda 라고 표기하였습니다.
매개변수 T와 리턴값 R
@FunctionalInterface
public interface Function <T, R> {
R apply(T t);
}
Function <Integer, Integer> process1=(Integer number)->{
return number+10;
};
Function <Integer, Integer> process2=(Integer number)->{
return number*5;
};
Function <Integer, Integer> process3=(Integer number)->{
return number/3;
}
Function <Integer, Integer> process1=number->number+10;
Function <Integer, Integer> process2=number->number*5;
Function <Integer, Integer> process3=number->number/3;
System.out.print(process1.apply(50)); // 60
System.out.print(process2.apply(50)); // 250
System.out.print(process3.apply(50)); // 16
매개변수 T,U와 리턴값 R
@FunctionalInterface
public interface BiFunctioanl<T, U, R> {
R apply(T t, U u);
}
BiFunction<Integer, Integer, Integer> process1=(number1, numeber2)=> number1+number2;
BiFunction<Integer, Integer, Integer> process2=(number1, numeber2)=> number1-number2;
BiFunction<Integer, Integer, Integer> process3=(number1, numeber2)=> number1*number2;
BiFunction<Integer, Integer, Integer> process4=(number1, numeber2)=>
(number2==0) number1 : number1/number2;
System.out.print(process1.apply(10,10); // 20
System.out.print(process2.apply(10,10); // 0
System.out.print(process3.apply(10,10); // 100
System.out.print(process4.apply(10,0); // 10
매개변수 T,U,V와 리턴값 R
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, V v, R r);
}
TriFunction<Integer, Integer, Integer, Integer> process=(x,y,z)->x+y+z;
System.out.println(process.apply(100,10,1));
리턴값 T
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier<Integer> process=()->10;
Supplier<String> process=()->"하이";
System.out.print(process.get());
매개변수 T
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
Consumer<Integer> process=number->System.out.print(number);
process.accept(10); // 10
process.accept(30); // 30
2021년 12월 21일 현재 아직도 왜 쓰는지 모르겠지만,
그나마 쓸만한 경우를 찾아보면 다음 정도 밖에 없지 않을까 싶다.
publci class Main {
public static void main(String[] args) {
List<Integer datas=new ArrayList<>(Arrays.asList(10,20,30,40,50));
Consumer<Integer> process=data->System.out.print(data);
}
public static void process(List<Integer> datas, Consumer<Integer> consumer) {
for(Integer data : datas) {
consumer.accept(data);
}
}
}
매개변수 T,U
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
Objects.requireNonNull(after);
return (l, r) -> {
accept(l, r);
after.accept(l, r);
};
}
}
BiConsumer<Integer, String> process=(name, age)->
System.out.printf("%s 이의 나이는 %d 입니다.%n",name,age);
process.accept("unchaptered", 30);
public class Main {
public static void main(String[] args){
List<String> datas=new ArrayList<>(Arrays.asList("김팔순","김덕해","홍영수"));
BiConsumer<Integer, String> process=(index,name)->
System.out.printf("%d 번 사용자의 이름은 %s 입니다.%n",index,name);
}
public static void process(List<String> datas, BiConsumer<Integer, String> process) {
for(int i=0; i<datas.size(); i++){
process.accept(i, list.get(i));
}
}
}
매개변수 T와 리턴값 boolean
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
public class Main {
public static void main(String[] args) {
Predicate<Integer> isPositive=x->x>0;
List<Integer> list=new ArrayList<>(Arrays.asList(10, 3, -5, 4, -2 , 0));
System.out.println("기본 | "+list);
System.out.println();
System.out.println(filter(list,isPositive)); // 10, 4
System.out.println(filter(list,isPositive.negate())); // -5, -2, 0
System.out.println(filter(list,isPositive.or(x->x==0))); // 10, 4, 0
System.out.println(filter(list,isPositive.and(x->x%2==0))); // 10, 4
System.out.println(filter(list,isPositive.and(x->x%2==0 && x%3!=0))); // 10, 4
System.out.println(filter(list,isPositive.and(x->x%2==0).and(x->x%3!=0))); //10,4
}
public static <T> List<T> filter(List<T> list, Predicate<T> condition) {
ArrayList<T> output=new ArrayList<>();
for(T element : list) {
if(condition.test(element)) {
output.add(element);
}
}
output.trimToSize();
return output; // 오토 업캐스팅 이루어짐
}
}
이 인터페이스는 Function을 상속받으며 매개변수와 리턴값이 동일하면 하나의 타입 매개변수만을 사용한다 라는 개념이다.
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
부연설명 | Comparator 는 비교를 위한 인터페이스입니다.
단순한 값의 비교 뿐만 아니라, 객체를 비교하기 위해서도 사용할 수 있습니다.
public class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() { /* 생략 */ } }
public String getName() { /* 생략 */ }
@Override public int hashCode() { /* 생략*/ } }
@Override public boolean equals(Object obj) { /* 생략 */ }
@Override public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}
public class UserTest {
static void main(String[] args) {
List<User> userList=new ArrayList<>(10);
userList.add(new User(3,"ceil"));
userList.add(new User(1,"bellian"));
userList.add(new User(5,"angelica"));
System.out.println(userList+"\n");
Comparator<User> idComparator=(u1, u2)->u1.getId() - u2.getId();
Collections.sort(userList,idComparator);
System.out.println(userList+"\n");
Comparator<User> nameComparator=(u1, u2)->u1.getName().compareTo(u2.getName());
Collections.sort(userList,nameComparator);
System.out.println(userList+"\n");
}
}
람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함이다.
그런데 자바에는 함수 객체를 심지어 람다 보다도 간결하게 만드는 방법이 있으니, 바로 메서드 참조이다. ( :: 연산자를 사용 )
Effective Java 6 | Lamda, Stream
여기서는 다음과 같은 내용을 다루고 있습니다.
Function<String, Integer> strToint=Integer::parseInt;
int number=strToint.apply("403");
System.out.print(number); // 403
String textA="hello";
String textB="world";
Predicate<String> equalsStrToStr=textA::equals
boolean bools=equalsStrToStr.test("hello"); // true
boolean bools2=equalsStrToStr.test(textA); // true
boolean bools3=equalsStrToStr.test(textB); // false
Supplier 사용 시 성능 향상 이유 + Lazy Evaluation
아마 위 사용예시만 보고는 사용 이유를 체감할 수 없을 거라고 생각한다.
그러나 만약에 Supplier 가 어떠한 연산을 담당하고 있다면 이야기가 달라진다.
일반함수를 매개변수로 넘겨주는 경우 즉시 실행 한다.
하지만, Supplier 는 get() 을 통해 실행시점을 결정 할 수 있게 된다.
이러한 것을 활용할 수 있는 것이 바로 Lazy Evaluation 이라는 개념이다.
[Javascript] 지연 평가(Lazy evaluation) 를 이용한 성능 개선
우리는 @Functional Interface 를 활용하여 특정 기능의 실행 시점을 결정할 수 있게 되었다.
이 부분이 이해가 되지 않는 사람이 있다면 아래를 확인해보자.
hello() // 기존의 메서드 방식
hello.get() // Functional Interface 에서 추구하는 방식
일반 메서드에서는 이름() 형태로써 실행 시점을 결정하기 힘들었다.
하지만 @Functional Interface 에서는 이름.실행함수() 의 형태로써 매개변수화 및 실행호출의 분리라는 큰 장점을 가져오게 되었다.
이를 통해서 Strict Evaluation 과 Lazy Evaluation 을 구현할 수 있다.
위 링크를 타고 들어가면 나오는 언어는 JavaScript 를 기반으로 하고 있다.
Java 기반으로 된 설명을 찾아보니 제대로된 펙트를 이야기하는 글을 찾기 힘들고 위 포스트가 더 깔끔하고 정확하여 추가하게 되었다.
const arr = [0, 1, 2, 3, 4, 5]
const result = arr.map(num => num + 10).filter(num => num % 2).slice(0, 2)
console.log(result) // [11, 13]
// 지연평가를 하기 위해서는 지연평가 용 함수(Java 의 Functional Interface) 를 만들어야 한다.
const arr = [0, 1, 2, 3, 4, 5]
const result = _.take(2, L.filter(num => num % 2, L.map(num => num + 10, arr)));
console.log(result) // [11, 13]
해당 내용에 대한 자세한 설명은 아래 포스트를 참고하는 것이 더 낫다고 본다.
[JS] ES6 를 이용한 함수형 프로그래밍 - 12 take