Java Category/Java

[Java] 람다식

ReBugs 2023. 8. 5.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


람다식의 개념

자바는 함수형 프로그래밍을 위해 Java 8부터 람다식을 지원한다.

자바는 객체지향 프로그래밍 언어로써 객체가 없이 함수가 존재할 수 없다.

클래스 내부에 무조건 함수가 있어야 하기 때문에 자바에서는 함수를 함수라 부르지 않고 메소드라고 부른다.

함수형 프로그래밍
함수형 프로그래밍이란 함수를 정의하고 이 함수를 데이터 처리부로 보내 데이터를 처리하는 기법을 가진다.
데이터 처리부는 데이터만 가지고 있을 뿐, 처리 방법이 정해져 있지 않아 외부에서 제공된 함수에 의존한다.
즉, 함수에 어떤 처리코드가 적혀있느냐에 따라 해당 데이터를 가지고 발생하는 실행결과가 달라질 수 있다는 뜻이다.

 

자바는 람다식을 익명 구현 객체로 변환한다.

 

아래와 같은 인터페이스가 정의 되었다고 하자.

@FunctionalInterface
public interface Calculable {
	//추상 메소드
	void calculate(int x, int y);
}

 

람다식은 인터페이스의 익명 구현 객체이므로 인터페이스 타입의 매개 변수에 대입될 수 있다.

public static void action(Calculable calculable) {
    //데이터
    int x = 10;
    int y = 4;
    //데이터 처리
    calculable.calculate(x, y);
}

action() 메소드를 호출할 때 매개값으로 아래와 같이 람다식을 제공할 수 있다.

action((x, y) -> {
    int result = x - y;
    System.out.println("result: " + result);
});

action 메소드에서 calculable.calculate(x, y)를 실행하면 람다식의 중괄호 블록이 실행되면서 데이터가 처리된다.

action() 메소드는 제공된 람다식을 이용해서 내부 데이터를 처리하는 처리부 역할을 한다.

해당 메소드에 어떤 람다식을 이용해서 데이터를 처리하느냐에 따라서 결과는 달라지게 된다.

 

데이터는 똑같고 어떤 람다식을 대입하느냐에 따라 처리 결과가 달라진다.

 

참고사항
-인터페이스의 익명 구현 객체를 람다식으로 표현하려면 인터페이스의 추상 메소드가 단 하나여야 한다.
-인터페이스가 단 하나의 추상 메소드를 가질 때, 이를 함수형 인터페이스라고 한다.
-인터페이스가 함수형 인터페이스임을 보장하기 위해서는 @FunctionalInterface 어노테이션을 붙이면 된다.

 

예제코드

더보기

Calculable.java

@FunctionalInterface
public interface Calculable {
	//추상 메소드
	void calculate(int x, int y);
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		action((x, y) -> {
			int result = x + y;
			System.out.println("result: " + result);
		});

		action((x, y) -> {
			int result = x - y;
			System.out.println("result: " + result);
		});
	}

	public static void action(Calculable calculable) {
		//데이터
		int x = 10;
		int y = 4;
		//데이터 처리
		calculable.calculate(x, y);
	}
}
/*
result: 14
result: 6
*/

 


 

매개변수가 없는 람다식

 

아래의 코드들은 매개변수가 없는 람다식 예제이다.

 

Workable.java

@FunctionalInterface
public interface Workable {
	void work();
}

 

Person.java

public class Person {
	public void action(Workable workable) {
		workable.work();
	}
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		Person person = new Person();

		//실행문이 두 개 이상인 경우 중괄호 필요
		person.action(() -> {
			System.out.println("출근을 합니다.");
			System.out.println("프로그래밍을 합니다.");
		});

		//실행문이 한 개일 경우 중괄호 생략 가능
		person.action(() -> System.out.println("퇴근합니다."));
	}
}
/*
출근을 합니다.
프로그래밍을 합니다.
퇴근합니다.
*/

 


아래의 코드들은 익명 구현 객체를 람다식으로 대체해 버튼의 클릭 이벤트를 처리하는 예제이다.

 

Button.java

public class Button {
	//정적 중첩 인터페이스
	@FunctionalInterface
	public static interface ClickListener {
		//추상 메소드
		void onClick();
	}
	
	//필드
	private ClickListener clickListener;
		
	//메소드
	public void setClickListener(ClickListener clickListener) {
		this.clickListener = clickListener;		
	}
	
	public void click() {
		this.clickListener.onClick();
	}
}

 

ButtonExample.java

public class ButtonExample {
	public static void main(String[] args) {
		//Ok 버튼 객체 생성
		Button btnOk = new Button();

		//Ok 버튼 객체에 람다식(ClickListener 익명 구현 객체) 주입
		btnOk.setClickListener(() -> {
			System.out.println("Ok 버튼을 클릭했습니다.");
		});
		
		//Ok 버튼 클릭하기
		btnOk.click();

		//Cancel 버튼 객체 생성
		Button btnCancel = new Button();

		//Cancel 버튼 객체에 람다식(ClickListener 익명 구현 객체) 주입
		btnCancel.setClickListener(() -> {
			System.out.println("Cancel 버튼을 클릭했습니다.");
		});
		
		//Cancel 버튼 클릭하기
		btnCancel.click();
	}
}
/*
Ok 버튼을 클릭했습니다.
Cancel 버튼을 클릭했습니다.
*/

 


 

매개변수가 있는 람다식

함수형 인터페이스의 추상 메소드에 매개변수가 있을 경우 람다식은 아래와 같이 작성할 수 있다.

매개변수를 선언할 때 타입은 생략할 수 있고, 구체적인 타입 대신에 var를 사용할 수도 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

또한 매개변수가 하나일 경우에는 괄호를 생략할 수도 있다.

이때는 타입 또는 var를 붙일 수 없다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

아래의 예제는 매개변수가 있는 람다식의 사용 예제이다.

Workable.java

@FunctionalInterface
public interface Workable {
	void work(String name, String job);
}

 

Speakable.java

@FunctionalInterface
public interface Speakable {
	void speak(String content);
}

 

Person.java

public class Person {
	public void action1(Workable workable) {
		workable.work("홍길동", "프로그래밍");
	}

	public void action2(Speakable speakable) {
		speakable.speak("안녕하세요");
	}
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		Person person = new Person();

		//매개변수가 두 개일 경우
		person.action1((name, job) -> {
			System.out.print(name + "이 ");
			System.out.println(job + "을 합니다.");
		});
		person.action1((name, job) -> System.out.println(name + "이 " + job + "을 하지 않습니다."));
		
		//매개변수가 한 개일 경우
		person.action2(word -> {
			System.out.print("\"" + word + "\"");
			System.out.println("라고 말합니다.");
		});
		person.action2(word -> System.out.println("\"" + word + "\"라고 외칩니다."));
	}
}
/*
홍길동이 프로그래밍을 합니다.
홍길동이 프로그래밍을 하지 않습니다.
"안녕하세요"라고 말합니다.
"안녕하세요"라고 외칩니다.
*/

 


 

리턴값이 있는 람다식

함수형 인터페이스의 추상 메소드에 리턴값이 있을 경우 람다식은 아래와 같이 작성할  수 있다.

return 문 하나만 있을 경우 중괄호와 함께 return 키워드를 생략할 수 있다.

리턴값은 연산식 또는 리턴값 있는 메소드 호출로 대체할 수 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

Calcuable.java

@FunctionalInterface
public interface Calcuable {
	double calc(double x, double y);
}

 

Person.java

public class Person {
	public void action(Calcuable calcuable) {
		double result = calcuable.calc(10, 4);
		System.out.println("결과: " + result);
	}
}

 

LambdaExample.java

public class LambdaExample {
	public static void main(String[] args) {
		Person person = new Person();

		//실행문이 두 개 이상일 경우
		person.action((x, y) -> {
			double result = x + y;
			return result;
		});

		//리턴문이 하나만 있을 경우(연산식)
		//person.action((x, y) -> {
		// return (x + y);
		//});
		//person.action((x, y) -> (x + y)); //이것도 가능
		person.action((x, y) -> x + y);

		//리턴문이 하나만 있을 경우(메소드 호출)
		//person.action((x, y) -> {
		// return sum(x, y);
		//});
		person.action((x, y) -> sum(x, y));
		
		}
	
	public static double sum(double x, double y) {
		return (x + y);
	}
}
/*
결과: 14.0
결과: 14.0
결과: 14.0
*/

 

 


 

메소드 참조

메소드 참조는 말 그대로 메소드를 참조해서 매개변수의 정보 및 리턴 타입을 알아내 람다식에서 불필요한 매개변수를 제거하는 것을 목적으로 한다.

 

예를 들어 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스으 max() 정적 메소드를 호출하는 람다식은 아래와 같다.

(left, right) -> Math.max(left, right);

위 코드를 더 간결하게 아래의 코드로 줄일 수 있다.

Math::max;

 


 

정적 메소드와 인스턴스 메소드 참조

정적 메소드를 참조할 경우 클래스 이름 뒤에 :: 기호를 붙이고 정적 메소드 이름을 기술한다.

클래스 :: 메소드

 

인스턴스 메소드일 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 :: 기호를 붙이고 인스턴스 메소드 이름을 기술한다.

참조변수 :: 메소드

 

더보기를 누르면 접힌 코드를 볼 수 있다.

더보기
@FunctionalInterface
public interface Calcuable {
	double calc(double x, double y);
}

 

public class Person {
	public void action(Calcuable calcuable) {
		double result = calcuable.calc(10, 4);
		System.out.println("결과: " + result);
	}
}

 

public class Computer {
	public static double staticMethod(double x, double y) {
		return x + y;
	}

	public double instanceMethod(double x, double y) {
		return x * y;
	}
}

MethodReferenceExample.java

public class MethodReferenceExample {
	public static void main(String[] args) {
		Person person = new Person();
		
		//정적 메소드일 경우
		//람다식
		//person.action((x, y) -> Computer.staticMethod(x, y));		
		//메소드 참조
		person.action(Computer :: staticMethod);
		
		//인스턴스 메소드일 경우
		Computer com = new Computer();
		//람다식
		//person.action((x, y) -> com.instanceMethod(x, y));		
		//메소드 참조
		person.action(com :: instanceMethod);
	}
}
/*
결과: 14.0
결과: 40.0
*/
매개변수의 위치를 바꿔야할 경우 메소드 참조를 사용할 수 없다.

 


매개변수의 메소드 참조

아래와 같이 람다식에서 제공되는 a 매개변수의 메소드를 호출해서 b 매개변수를 매개값으로 사용하는 경우도 있다.

(a, b) -> { a.instanceMethod(b); }

 

이를 메소드 참조로 표현하면 아래와 같다.

작성 방법은 정적 메소드 참조와 동일하지만, a의 인스턴스 메소드가 사용된다는 점에서 다르다.

클래스 :: instanceMethod

 

더보기를 누르면 접힌 코드를 볼 수 있다.

더보기
@FunctionalInterface
public interface Comparable {
	int compare(String a, String b);
}

 

public class Person {
	public void ordering(Comparable comparable) {
		String a = "홍길동";
		String b = "김길동";

		int result = comparable.compare(a, b);

		if(result < 0) {
			System.out.println(a + "은 " + b + "보다 앞에 옵니다.");
		} else if(result == 0) {
			System.out.println(a + "은 " + b + "과 같습니다.");
		} else {
			System.out.println(a + "은 " + b + "보다 뒤에 옵니다.");
		}
	}
}

 

MethodReferenceExample.java

public class MethodReferenceExample {
	public static void main(String[] args) {
		Person person = new Person();
		
		person.ordering((a, b) -> {
			return a.compareToIgnoreCase(b);
		});
		
		person.ordering((a, b) -> a.compareToIgnoreCase(b));
		
		person.ordering(String :: compareToIgnoreCase); //매개변수의 메소드 참조
		
		//위 세개의 코드 모두 같은 기능을 동작하는 코드
	}
}
/*
홍길동은 김길동보다 뒤에 옵니다.
홍길동은 김길동보다 뒤에 옵니다.
홍길동은 김길동보다 뒤에 옵니다.
*/
a.compareToIgnoreCase(b)
위 메소드는 String 클래스의 인스턴스 메소드이며, 리턴값은 a 문자열이 b 문자열보다 크면 양의 정수, 같으면 0, 작으면 의 정수 (대소문자 고려사항 무시)이다.

 

 


 

생성자 참조

생성자를 참조한다는 것은 객체를 생성하는 것을 의미한다.

람다식이 단순히 객체를 생성하고 리턴하도록 구성된다면 람다식을 생성자 참조로 대치할 수 있다.

아래의 코드에서 람다식은 단순히 객체를 생성한 후 리턴만 한다.

(a, b) -> {return new 클래스(a,b);}

 

이것을 생성자 참조로 표현하면 아래와 같다.

클래스 :: new

 

생성자가 오버로딩되어 여러 개가 있을 경우, 컴파일러는 함수형 인터페이스의 추상 메소드와 동일한 매개변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다.
만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생한다.

 

Creatable1.java

@FunctionalInterface
public interface Creatable1 {
	public Member create(String id);
}

 

Creatable2.java

@FunctionalInterface
public interface Creatable2 {
	public Member create(String id, String name);
}

 

Member.java

public class Member {
	private String id;
	private String name;
	
	public Member(String id) {
		this.id = id;
		System.out.println("Member(String id)");
	}
		
	public Member(String id, String name) {
		this.id = id;
		this.name = name;
		System.out.println("Member(String id, String name)");
	}
	
	@Override
	public String toString() {
		String info = "{ id: " + id + ", name: " + name + " }";
		return info;
	}
}

 

Person.java

public class Person {
	public Member getMember1(Creatable1 creatable) {
		String id = "winter";
		Member member = creatable.create(id);
		return member;
	}

	public Member getMember2(Creatable2 creatable) {
		String id = "winter";
		String name = "한겨울";
		Member member = creatable.create(id, name);
		return member;
	}
}

 

ConstructorReferenceExample.java

public class ConstructorReferenceExample {
	public static void main(String[] args) {
		Person person = new Person();
		//---------------------------------------------------------
		Member m1 = person.getMember1((id)->{
			Member m = new Member(id);
			return m;
		});
		
		 m1 = person.getMember1((id)->{
				return new Member(id);	
			});
		
		m1 = person.getMember1((id)-> new Member(id));
		
		m1 = person.getMember1( Member :: new );
		System.out.println(m1);
		System.out.println();
        //위 코드는 모두 같은 결과를 내는 코드이다.
		//---------------------------------------------------------
		Member m2 = person.getMember2((id, name)->{
			Member m = new Member(id, name);
			return m;
		});
		
		m2 = person.getMember2((id, name)->{
			return new Member(id, name);
		});
		
		m2 = person.getMember2((id, name)-> new Member(id, name));
		
		m2 = person.getMember2( Member :: new );
		System.out.println(m2);
        //위 코드는 모두 같은 결과를 내는 코드이다.
	}
}
/*
Member(String id)
Member(String id)
Member(String id)
Member(String id)
{ id: winter, name: null }

Member(String id, String name)
Member(String id, String name)
Member(String id, String name)
Member(String id, String name)
{ id: winter, name: 한겨울 }
*/

댓글