⛏️/JAVA

[JAVA] 42. 람다식(Lambda Expressions)

defyuil 2023. 11. 24. 14:27

람다식(Lambda Expressions)

- 자바 8에 추가된 가장 큰 특징

- "함수형" 프로그래밍 형태를 받아들인 결과

 

함수형 프로그램이란? (함수형 vs 객체지향)

- 함수형 : 1950년대 , 객체지향 : 1990년대 (역사가 더 오래되었음)

- 기능 위주의 프로그래밍 기법

- 매개변수만을 사용하여 만드는 함수(순수 함수, pure function) 를 구현하고 호출함으로써 외부 자료에 부수적인 영향을 주지 않도록 구현하는 방식

- 객체 지향 : 객체를 기반으로 구현, 클래스에 속성과 기능을 정의

함수형 : 함수를 기반으로 자료를 입력받아 구현, 기능 즉 함수가 따로 존재

 

- 함수 이름이 없는 익명 함수를 만드는 것 메서드의 이름과 리턴타입을 빼고 '->' 를 활용하여 람다식을 구현
ex) public int add(int x, int y) {
return x + y;
}
의 메서드를 람다식으로 표현하면
(int x, int y) -> { return x + y; }

 

< 람다식의 기본 문법 >

(데이터타입 매개변수, ...) -> { 실행문, ... }

 

1. 기본형

(int x) -> { System.out.println(x); }

 

2. 매개변수의 타입을 추론할 수 있는 경우에는 타입 생략 가능

(x) -> { System.out.println(x); }

 

3. 매개변수나 실행문이 하나라면 소괄호() 와 중괄호 {} 를 생략할 수 있음.

(이 때, 세미콜론(;)은 생략!)

x -> System.out.println(x)

주의! 매개변수가 두 개 이상인 경우 소괄호() 를 생략할 수 없다!

 

4. 매개변수가 없을 경우 소괄호() 를 사용한다. (생략 불가!)

() -> System.out.println(x)

 

5. 리턴이 필요한 경우 return 키워드를 사용

(x, y) -> { return x + y; }

주의! return 문은 중괄호를 생략할 수 없다!

 

6. 실행문이 단순히 return 문 하나로 표현되는 경우

표현식만 사용할 수 있으며, 이 때 리턴 값은 표현식의 결과값이 됨

(주의! 이 때, 세미콜론을 붙이지 않는다!)

(x, y) -> x + y

 

// 두 수를 입력받아 큰 수를 출력하는 메서드를 가진 클래스 정의
class MaxPrintNumber {
	public int maxNum(int num1, int num2) {
		if(num1 >= num2) {
			return num1;
		} else {
			return num2;
		}
	}
}

 

 

// MyNumber 인터페이스를 구현한 MyNumberClass 정의
//interface MyNumber {
//	int getMax(int num1, int num2); // 추상 메서드 선언
//}

class MyNumberClass implements MyNumber {
	
	// 두 수를 입력받아 둘 중 큰 숫자를 리턴하는 getMax() 메서드 정의
	@Override
	public int getMax(int num1, int num2) {
		if(num1 >= num2) {
			return num1;
		} else {
			return num2;
		}
	}
}

 

 

위의 getMax() 메서드를 람다식으로 표현하기 위해 함수형 인터페이스 선언한다

=> 자바에서는 참조 변수 없이 메서드를 호출할 수 없음

=> 람다식을 구현하기 위해 함수형 인터페이스를 만들고, 인터페이스에 람다식으로 구현할 메서드를 선언하는 형태로 사용

=> 주의! 람다식은 하나의 메서드를 구현하여 인터페이스형 변수에 대입하므로 인터페이스가 두 개 이상의 메서드를 가지면 안된다!

 

@FunctionalInterface // 함수형 인터페이스 용도로 사용됨을 알림
interface MyNumber {
	int getMax(int num1, int num2); // 추상 메서드 선언
//	int add(int num1, int num2);
}

 

// MaxPrintNumber 클래스 활용
		MaxPrintNumber mpn = new MaxPrintNumber();
		System.out.println(mpn.maxNum(10, 20));
		
		System.out.println("----------------------------");
		
		// 구현클래스를 활용한 getMax 메서드 호출
		MyNumber max = new MyNumberClass(); // 업캐스트 적용
		System.out.println(max.getMax(10, 20));
		
		System.out.println("-----------------------------");
		
		// 람다식 구현과 호출
		MyNumber max2 = (x, y) -> (x >= y) ? x : y ;
		// => 람다식을 인터페이스형 max2 변수에 대입
		System.out.println(max2.getMax(10, 20)); // 인터페이스형 변수로 메서드 호출

 

 

 

 

 

익명 객체를 생성하는 람다식

- 자바는 객체 지향 언어로 객체 없이 메서드를 호출할 수 없다

- 람다식은 객체 없이 인터페이스의 구현만으로 메서드를 호출할 수 있는데 그 이유는 익명 내부 클래스에 있다

- 익명 내부 클래스는 클래스 이름 없이 인터페이스 자료형 변수에 바로 메서드 구현부를 생성하여 대입할 수 있음

- 즉, 람다식으로 메서드를 구현해서 호출하면 컴퓨터 내부에서는 익명클래스가 생성되고, 이를 통해 익명 객체가 생성되는 것

 

 

 

람다식에서 사용하는 지역 변수

- 람다식 코드에서는 외부 메서드의 지역 변수를 수정할 수 없음 그 이유는 지역 변수는 메서드 호출이 끝나면 메모리에서 사라지기 때문에 익명 내부 클래스에서 사용하는 경우에는 지역 변수가 상수로 변하기 때문

 

		int i = 100; // main() 메서드의 지역변수
		
		StringConcat concat2 = (s, v) -> {
//			i = 200; // main() 메서드의 지역변수 i는 상수(final) 이므로 값 변경 불가!
			// => Local variable i defined in an enclosing scope 
			//    must be final or effectively final
			System.out.println(i);
			System.out.println(s + "," + v);
		};
		
		concat2.makeString("itwill", "busan");
		StringConcat concat3 = new StringConcat() {	
			@Override
			public void makeString(String s1, String s2) {
				System.out.println(s1 + "," + s2);
			}
		};
		// => 내부적으로 익명 구현 객체가 생성되어 메서드를 호출할 수 있게 됨
		
		concat3.makeString("Hello", "World");

 

 

 

 

 

 

함수형 인터페이스(functional interface) 또는 타겟타입(target type)

- 람다식은 결과적으로 "인터페이스의 클래스를 손쉽게 구현하는 방법"

- 반드시 하나의 abstract 메서드만 존재

- 만약 abstract 메서드가 없거나 두 개 이상 존재한다면 람다식으로 대체할 수 없음

- 함수형 인터페이스 @FunctionalInterface 어노테이션 선언

 

// 함수형 인터페이스를 사용하는 람다식 유형
// 1. 파라미터와 리턴타입이 없는 경우 (파라미터 : X, 리턴타입 : X)
@FunctionalInterface
interface MyFunc1 {
	// 함수형 인터페이스 어노테이션(@FunctionalInterface) 선언 시 발생!
	// => 함수형 인터페이스는 반드시 하나의 추상메서드(abstract method)를 가져야 함
	public void methodA();
//	public void methodB();
}

// 2. 파라미터가 있는 람다식(파라미터 : O)
@FunctionalInterface
interface MyFunc2 {
	public void methodB(String msg);
}

// 3. 리턴타입이 있는 람다식(파라미터 : O, 리턴타입 : O)
@FunctionalInterface
interface MyFunc3 {
	public String methodC(String msg);
}

 

	// 1. 파라미터와 리턴타입이 없는 경우 (파라미터 : X, 리턴타입 : X)
	public static void useFIMethodA(MyFunc1 fi) {
		fi.methodA();
	}
	
	// 2. 파라미터가 있는 람다식(파라미터 : O)
	public static void useFIMethodB(MyFunc2 fi) {
		fi.methodB("홍길동");
	}
	
	// 3. 리턴타입이 있는 람다식(파라미터 : O, 리턴타입 : O)
	public static String useFIMethodC(MyFunc3 fi) {
		return fi.methodC("이순신");
	}

 

		useFIMethodA(new MyFunc1() {
			@Override
			public void methodA() {
				System.out.println("익명 내부 클래스 형태!");
			}
		});
		
		System.out.println("------------------------------------");
		
		// 1. 파라미터와 리턴타입이 없는 경우 (파라미터 : X, 리턴타입 : X)
		System.out.println("(파라미터 : X, 리턴타입 : X)");
		// 정적 메서드 useFIMethodA의 파라미터가 MyFunc1 의 인터페이스 타입이므로
		// 람다식으로 만들어지는 익명 객체가 파라미터로 사용됨
		
		// 표현방식1
		useFIMethodA( () -> { 
			System.out.println("람다식1!");
		});
		
		// 표현방식2
		useFIMethodA(() -> System.out.println("람다식2!"));
		
		System.out.println("---------------------------------");
		
		// 2. 파라미터가 있는 람다식(파라미터 : O)
		System.out.println("(파라미터 : O, 리턴타입 : X)");
		
		// 표현방식1
		useFIMethodB((String msg) -> {
				System.out.println("람다식1 : " + msg);
			}
		);
		
		// 표현방식2
		useFIMethodB( msg -> System.out.println("람다식2 : " + msg) );
		
		System.out.println("-------------------------------------");
		
		// 3. 리턴타입이 있는 람다식(파라미터 : O, 리턴타입 : O)
		System.out.println("(파라미터 : O, 리턴타입 : O)");
		
		// 표현방식1
		String result = useFIMethodC((String msg) -> {
							return "람다식1 : " + msg;
						});
		System.out.println(result);
		
		// 표현방식2
		System.out.println( useFIMethodC(msg -> "람다식2 : " + msg) );
		

	} // main() 메서드 끝

 

 

 

 

TEST

public class Test {

	public static void main(String[] args) {
		/*
		 * 문자열을 입력받아 두 문자열을 연결하여 출력하는 프로그램 정의
		 * 람다식을 사용하여 구현
		 * ex) Hello 와 World 를 매개변수값으로 전달하면
		 *     "Hello,World" 가 출력
		 */
//	String s1 = "Hello";
//	String s2 = "World";
	Scanner scan = new Scanner(System.in);
	System.out.print("첫 번째 문자열을 입력하세요 : ");
	String s1 = scan.nextLine();
	
	System.out.print("두 번째 문자열을 입력하세요 : ");
	String s2 = scan.nextLine();
	
	// 객체 지향 프로그래밍 방식
	StringConcatImpl sc = new StringConcatImpl();
	sc.makeString(s1, s2);
	
	// ---------------------------------------------
	// makeString() 메서드를 람다식으로 표현
	StringConcat concat = (String s, String v) -> System.out.println(s + ", " + v);
	concat.makeString(s1, s2);
		
	}

}

@FunctionalInterface
interface StringConcat {
	public void makeString(String s1, String s2);
}

class StringConcatImpl implements StringConcat {
	
	@Override
	public void makeString(String s1, String s2) {
		System.out.println(s1 + ", " + s2);
	}
	
}