본문 바로가기
IT/자바

Java8 람다(Lambda)를 이용한 프로그래밍

by 모띠 2022. 1. 23.

함수형 인터페이스

람다를 이용한 프로그래밍 

  1. 람다를 받는다(함수형인터페이스를 찾던가 만들던가)
  2. 람다를 호출해야하는지 검사
  3. 필요할때 람다를 호출
  • 모든 람다의 핵심은 지연실행이다. 코드를 나중에 실행할 이유는 다음과 같다.
    • 별도의 스레드에서 코드실행
    • 코드를 여러번 실행
    • 알고리즘에서 코드를 적절한 시점에서 실행(정렬 비교연산 등)
    • 어떤일이 발생했을 때 코드 실행(이벤트 등..)
    • 필요할때만 코드 실행

각 픽셀에 Color → Color 함수를 적용하여 이미지를 변환한다고 가정.

public static Image transform(Image image, UnaryOperator<Color> f){ // 고정된 값만큼 모든 픽셀의 밝기를 올리는 메서드
//	.....
	f.apply(image.getColor);
}

Image brightenedImage = transform(image, Color::brighter);

여기서 람다를 받을 방법은 2가지인데,

// 1. 알맞는 함수형인터페이스를 찾던가
T타입을 받아서 T타입을 리턴하므로 UnaryOperator<T>를 사용

// 2. 내가 직접 만들던가
@FunctionalInterface
public interface ColorTransformer{
	Color apply(Color color); // 명칭을 apply로 하는 이유는 
}

함수 리턴

함수형프로그램에서는 함수를 인자로 받을 수 있을뿐만아니라, 함수자체를 리턴할 수 도 있다.

Image brightenedImage = transform(image, Color::brighter); // 고정된 값으로 이미지를 밝게하지만 내가 원하는 밝기만큼 밝게해주고싶다면?

// 1. 함수 오버로딩
Image brightenedImage = transform(image, (c, factor) -> c.deriveColor(0,1,factor,1), 1.2); // 이렇게 변수를 하나 더받도록 오버로딩해야함
public static <T> Image transform(Image in, BiFunction<Color, T> f, T val){ ... }

// 2. 함수 자체를 리턴하도록함
public static UnaryOperator<Color> setBright(double factor){ // 새롭게 하나 만들고
	return c -> c.deriveColor(0,1,factor,1); 
}

Image brightenedImage = transform(image, setBright(1.2)); // setBright(1.2)가 이미 UnaryOperator<Color>함수를 리턴하기때문에 setBright의 결과값을 transform의 인자로 사용할 수 있다.

여기서 setBright메서드의 결과가 UnaryOperator<Color>함수를 리턴하기때문에 setBright의 결과값을 transform의 인자로 사용가능한 것은 맞는데, setBright를 미리 계산하여 그 결과값을 transform에 넣을생각하지말고(어차피 c를 받아야해서 계산도못함) 그 리턴값(함수)를 그대로 들고와서 대입하면 쉽다.

리턴값을 계산하여 가져오지말고, 리턴 함수 그자체를 통째로 가져와서 대입하도록 패러다임을 바꿔야함.

위에서는 c -> c.deriveColor(0,1,factor,1); 를 들고와서 transform의 f.apply()가 실행될때 c -> c.deriveColor(0,1,factor,1);를 실행하면된다. c값은 image.getColor가 됨.

합성

이미지 조작을 한다. 먼저 밝게한다음 흑백을 적용한다.

Image image1 = new Image("abc.jps");
Image image2 = transform(image, Color::brighter); 
Image image3 = transform(image2, Color::gray); // 이렇게하면되긴하는데, image2라는 쓸때없는 중간변수가 생겨남

합성을 적용하여 중간변수를 만들지않고 한번에 결과를 뽑는다.

public static <T> UnaryOperator<T> compose(UnaryOperator<T> op1, UnaryOperator<T> op2){
	return t -> op2.apply(op1.apply(t));
}

Image image3 = transform(image, compose(Color::brighter, Color::gray);

예외처리

람다 표현식에서 예외를 던지면, 해당 예외는 호출자에 전파된다.

public static void do(Runnable first, Runnable second){
	first.run(); // <- 여기서 에러가 발생하면 do메서드는 종료되며 이 메서드를 호출한 곳으로 예외를 전달
	second.run(); // 실행x
}
// 비동기 실행
public static void do(Runnable first, Runnable second){
	Thread t = new Thread(){
		public void run(){
			first.run(); // <- 여기서 에러가 발생하면 do메서드는 종료되지만, 예외를 전달하지않고 종료됨
			second.run(); // 실행x
		}
	};
	t.start();
}

// 아래처럼 예외처리기에 전달하는 것이 좋음
public static void do(Runnable first, Runnable second, **Consumer<Throwable> handler**){
	Thread t = new Thread(){
		public void run(){
			try{
				first.run(); // <- 여기서 에러가 발생하면 do메서드는 종료
				second.run(); // 실행x
			} catch(Throwable t){
				**handler.accept(t); // 예외처리**
			}
		}
	};
	t.start();
}

// 이런식으로 first가 생성하는 값을 second가 소비한다고해도 처리기 사용가능
public static <T> void do3(**Supplier<T>** first, **Consumer<T>** second, Consumer<Throwable> handler) {
	Thread t = new Thread() {
		public void run() {
			try {
				**T result = first.get(); // first가 만든값을 second가 소비, 예외처리도 가능
				second.accept(result);** 
			} catch (Throwable t) {
				handler.accept(t);
			}
		}
	};
	t.start();
}

위의 예제는 first.get()을 진행하는도중 예외가 발생했을시 예외를 처리하기위해서 try / catch로 잡았다**.**

Supplier의 get()은 예외검사를 안하기때문에 try/catch를 안넣어줘도 컴파일 에러는 없었다.

(다만 예외처리는 못하겠지만)

미연의 방지를 위하여 예외 처리를 강제하고 싶다면 Callable의 call() 메서드를 사용한다. call()메서드는 throws exception이 붙어있어서 반드시 예외처리를 해줘야한다.

public static <T> void do4(Callable<T> first, Consumer<T> second, Consumer<Throwable> handler) {
	Runnable r = () -> {
		try {
			T result = first.call(); // Supplier였을때는 예외처리를 안해줘도 됐지만, Callable은 예외처리가 필수임(throws Exception이 붙어있기때문에)
			second.accept(result);
		} catch (Throwable t) {
			handler.accept(t);
		}
	};
}

public static void repeatMessage(String text, int count) {
		Runnable r = () -> {
			for (int i = 0; i < count; i++) {
//			Thread.sleep(20); 예외 처리를 해줘야함. try catch로 잡던가 / throw할수있는 함수형인터페이스로 받아주던가(근데 Runnable의 run은 throws Exception이 없음)
			}
		};

		Callable<String> a = () -> {
			Thread.sleep(20); // Callable의 call메서드가 exception을 받아주기때문에 try catch로 예외를 잡을필요가없음
			return text;
		};
		new Thread(r).start();
}

제네릭과 람다

List<Employee> staff = ...;

List<Person> tenants = staff; ← 불가능 Person의 후손이 Employee이긴하지만 List<Person>의 후손이 Employee<Person>은 아니므로..

  • 인자타입 : <? super T>
  • 리턴타입 : <? extends T>
  • BinaryOperator<T> 같은 인터페이스는 인자도 T, 리턴도 T 이므로 상쇄되므로 그냥 <T>만 입력
public static <T> void do3(Supplier<? extends T> first, **Consumer<? super T>** second, Consumer<**? super** Throwable> handler) { // 이런식으로 변환
	Thread t = new Thread() {
		public void run() {
			try {
				T result = first.get(); 
				second.accept(result); 
			} catch (Throwable t) {
				handler.accept(t);
			}
		}
	};
	t.start();
}

'IT > 자바' 카테고리의 다른 글

classpath  (0) 2023.03.26
ExecutorService  (0) 2023.03.26
Java8 스트림(Stream)연산을 사용해보자  (0) 2022.01.23
Java8 람다(Lambda)를 사용해보자  (0) 2022.01.23
Java8 옵셔널(Optional<T>)객체 올바르게 사용하기  (0) 2022.01.23

댓글