공부한 기록/Programming Language

Java - 람다식과 함수형 인터페이스(Predicate, Consumer, Supplier, Function, Operator)

YongE 2024. 4. 17. 20:47

함수형 프로그래밍


 

먼저 프로그래밍 패러다임에 대한 분류를 보자.

 

 

  • 명령형 프로그래밍 : 무엇을 어떻게 하라고 지시함.
  • 선언적 프로그래밍 : 무엇을 하라고만 지시함. 어떻게 하라고 지시하진 않음.

보이다시피 선언적 프로그래밍은 "해야 할 일"에 집중한다. 따라서 ‘무엇을 어떻게 해야할지’ 일일이 명령할 필요가 없고, ‘무엇을’ 하라고만 지시하면 된다. 즉, 짧고 편하게 무언가를 할 수 있다는 것이다.

 

각 패러다임으로 샌드위치를 만든다고 했을 때 예를 들면 다음과 같다.

명령형 프로그래밍 - 샌드위치 재료를 하나하나 언급하며 이를 만들어 달라고 한다.
선언적 프로그래밍 - 각각 정해진대로 샌드위치를 만들어 달라고 한다.

 

이런 선언적 프로그래밍 내에 함수형 프로그래밍이 있고, 이는 명령문을 일일이 읽어들이는 것이 아닌 수식이나 함수 호출로 이뤄진다.

 

 

람다식


 

예를 들어, 기존 모듈에 있지 않은 새로운 클래스의 배열을 생성해 비교한다고 가정해보자.

 

//새로운 클래스
class Rectangle {
	private int width, height;
	public Rectangle(int width, int height) {
		this.width = width;
		this.height = height;
	}
	public int findArea() {
		return width * height;
	}
	public String toString() {
		return String.format("사각형[폭=%d, 높이=%d]", width, height);
	}
}

//새로운 클래스 활용
public class ComparableDemo {
	public static void main(String[] args) {
	Rectangle[] rectangles = { new Rectangle(3, 5),
		new Rectangle(2, 10), new Rectangle(5, 5) };
	Arrays.sort(rectangles); // exception 발생
	for (Rectangle r : rectangles)
		System.out.println(r);
	}
}

 

기존 Arrays의 정적 메소드는 에러를 발생시킨다. ( Array.sort(변수명) )

 

이 에러를 해결하려면 정렬 메서드를 구현해야 한다. flag가 의미하는 비교 기준을 정립하거나 비교기준마다 다른 메서드로 정렬하게 한다.

 

/* flag가 의미하는 비교기준으로 정렬한다고 할 때*/
Object[] sort(Object[] array, int flag){
	if(type == 1){
    	//넓이 비교
    } else {
    	//둘레 비교
    }
}

/* 비교기준마다 다른 메서드로 정렬한다고 할 때*/
Object[] sortByArea(Object[] array){
	//넓이 비교
}

Object[] sortByPerimeter(Object[] array){
	//둘레 비교
}

 

그러나, 이러한 방법은 복잡하고 가독성이 떨어진다. 그리고 그 클래스에 다른 속성이 추가되면 정렬 메소드를 수정하거나 새로운 메서드를 추가해야 한다.

 

여기서 위와 같은 문제를 해결하는 다양한 방식을 알 수 있다.

 

Comparator 인터페이스, 익명 객체


 

예시

Object[] sort(Object[] array, 객체 비교 방식){
	두 번째 파라미터에 면적이나 둘레를 기준으로 비교할 수 있다.
}

구현 예시
-익명 객체를 사용한다.
Arrays.sort(strings, new Comparator<String>() {
	public int compare(String first, String second) {
	return first.length() - second.length();
	}
});

 

위의 익명 객체는 길다. 어떤 익명 객체든 반복되는 부분이 있기 때문이다. 

 

위의 메소드를 '람다식'으로 표현해보자.

 

Arrays.sort(strings, (first, second) -> first.length() - second.length());

 

 

 

동작 매개 변수화 behavior parameterization


 

원하는 동작을 매개변수로 만든다는 의미!

 

빈번한 요구 사항 변경을 처리할 수 있는 소프트웨어 개발 패턴이다. 이는 사용자 요구를 담은 코드 블록을 생성하고 프로그램의 다른 부분에 전달하는 것이다.

 

다시 말해, 어느정도 프로그램을 개발하면 요구사항을 처리할 수 있는 부분을 따로 빼놓고 어떤 요구사항이든 파라미터로 받아서 처리할 수 있도록 하는 패턴이다.

 

동작 매개 변수화 패턴의 시각적 예시

 

이번에도 예시를 들어보자.

 

자동차 영업 사원은 원하는 차종이 제각각인 고객들을 상대해야 한다. 어떤 고객은 흰색과 5000만원 이하의 자동차를 찾고, 또 다른 고객은 검정색, 2000만원 이하, 부품을 전체적으로 교체한 차량만 찾는다고 할 때, 이 불규칙적인 요구 사항을 들어줄 수 있어야 한다.

 

 

첫 번째 방법 - 동작 매개 변수화

자동차의 속성을 검사하여 true, false를 반환하는 함수를 작성하여 메소드로 전달할 수 있다.

 

함수형 인터페이스
public interface CarPred{
	boolean test(Car car);
}

인터페이스 구현 클래스
public class WhiteCheapPred implements CarPred{
	public boolean test(Car car){
    	return "WHITE".equals(car.getColors()) && car.getPrice() <= 5000;
    }
}

동작 전달
List<Car> whitCheapCars = filterCars( carList, new WhiteCheapPred() );

 

 

두 번째 방법 - 익명 클래스 사용

이전에 배웠던 익명 클래스를 다룬다. 익명 클래스는 타입 변수를 선언하지 않고 구현하는 클래스(객체)다.

 

List<Car> whitCheapCars = filterCars( carList, new CarPred(){
	public boolean test(Car car) {
    	return "WHITE".equals(car.getColor);
    }
);

 

 

세 번째 방법 - 람다식 사용

람다식은 익명 클래스를 단순화해 1. 메서드의 인수로 전달하거나 2. 인터페이스 객체를 생성할 수 있는 기능을 제공한다. 

 

List<Car> whiteCars = filterCars( carList, (Car car) -> "White".equals(car.getColors()));

 

 

 

람다식 디테일


 

interface Negative {
		int neg(int x);
}
public class Lambda2Demo {
		public static void main(String[] args) {
				Negative n;
                n = () -> { return 0;} 선언부의 타입은 추론 가능하므로 생략할 수 있다.
				n = (int x) -> { return -x; }; 	
				n = (x) -> { return -x; };		
				n = x -> { return -x; };매개변수가 하나면 괄호도 생략할 수 있다.
				n = (int x) -> -x; 	실행문이 하나면 중괄호와 세미콜론 생략할 수 있다.
				n = (x) -> -x;	실행문이 return문 하나라면 이 또한 생략가능하다.
				n = x -> -x;
		}
}

 

익명 클래스를 람다식으로 변환하는 과정은 다음과 같다.

 

List<Car> whiteCars = filterCars(inventory, new CarPredicate() {
	public boolean test(Car car){
		return "WHITE".equals(car.getColor());
	}
});

//1단계 : 익명 클래스 선언 부분 제거
List<Car> whiteCars = filterCars(inventory,
	public boolean test(Car car){
		return "WHITE".equals(car.getColor());
	}
);

//2단계 : 메서드 선언 부분 제거
List<Car> whiteCars = filterCars(inventory, (Car car){
		return "WHITE".equals(car.getColor());
	}
);

//3단계 : 람다 문법으로 정리(실행문이 하나일 때)
List<Car> whiteCars = filterCars(inventory,
		(Car car) -> "WHITE".equals(car.getColor());
);

 

 

메소드 참조


 

메소드를 ‘호출’하는 게 아니라 ‘참조’하는 것이다. 다시 말해, 전달할 동작을 수행할 메소드가 이미 정의된 경우에, 이를 불러들이는 게 아니라 가르키는 것이다. 이는 람다식의 축약형으로서 코드를 더욱 짧게 해준다.

 

 

아래 코드를 보자.

 

s = String::new;
String str = s.getObject("사과");

 

사과라는 문자열을 호출하면 String 객체가 생성된다.

 

 

 

함수형 인터페이스


 

함수형 인터페이스는 하나의 추상 메소드를 가진 인터페이스라고 한다.  람다식과 함수형 인터페이스는 분리할 수 없는 관계에 있다. 람다식을 컴파일하려면 반드시 함수형 인터페이스가 정의되어 있어야 하기 때문이다.

 

java에서는 이러한 람다식을 사용하기 위한 기본적인 함수형 인터페이스를 제공한다. 물론 추상 메소드 말고도 디폴트 메서드나 정적 메서드도 제공한다. 추상 메서드만 하나다.

 

java.util.function 패키지가 제공하는 함수형 인터페이스 종류

 

Predicate

객체 조사 후 논릿값(boolean)을 반환한다.

 

public class PredicatePrac {
	public static void main(String[] args) {
    	
        정수값을 다루는 Predicate
        매개변수가 짝수인지 홀수인지를 구함.
		IntPredicate even = x -> x % 2 == 0;
		System.out.println(even.test(3) ? "짝수" : "홀수");
        
        1이 아니면 짝수, 홀수인지를 다루는 Predicate의 or 디폴트메서드
		IntPredicate one = x -> x == 1;
		IntPredicate oneOrEven = one.or(even);
		System.out.println(oneOrEven.test(1) ?
			"1 혹은 짝수" : "1이 아닌 홀수");
        
        정적 메소드
		Predicate<String> p = Predicate.isEqual("Java Lambda");
		System.out.println(p.test("Java Lambda"));
		System.out.println(p.test("JavaFX"));
        
        두 매개변수를 받는다는 의미의 Bi-
		BiPredicate<Integer, Integer> bp = (x, y) -> x > y;
		System.out.println(bp.test(2, 3));
	}
}

 

 

Consumer

명칭 그대로 객체를 사용한 후 void 반환한다.

 

public class ConsumerDemo {
	public static void main(String[] args) {
		Consumer<String> c1 = x ->
			System.out.println(x.toLowerCase());
		c1.accept("Java Functional Interface");
    }
}

 

 

Supplier

객체를 반환(공급)한다.

 

public class SupplierDemo {
	public static void main(String[] args) {
		Supplier<String> s1 = () -> "apple";
		System.out.println(s1.get());
    }
}

 

 

Function

매개변수로 받은 T 객체를 R 타입 객체로 반환(매핑)한다.

 

public class Function1Demo {
	public static void main(String[] args) {
		Function<Integer, Integer> add2 = x -> x + 2;
		Function<Integer, Integer> mul2 = x -> x * 2;
		System.out.println(add2.apply(3)); // result = 5
		System.out.println(mul2.apply(3)); // result = 6
	}
}

 

 

Operator

Operator라는 인터페이스는 없고 Binary, Unary, Double, Int, Long을 접두어로 붙인 인터페이스만 있다.

이는 x와 y 객체를 받아서 T 타입의 결과를 반환한다.

 

public class Operator1Demo {
	public static void main(String[] args) {
		IntUnaryOperator add2 = x -> x + 2;
		System.out.println(add2.applyAsInt(3)); // result = 5
    }
}

 

728x90
반응형