이전 내용
[Java] JDBC : 데이터베이스
이전 내용 [java] 컬렉션 프레임워크, 제네릭이전 내용 [java] 스레드 제어이전 내용 [java] java 스레드(thread) - 멀티, 메인, 작업 / 동기화 메소드이전 내용 [java] java.lang 패키지이전 내용 [java] 예외 (클
puppy-foot-it.tistory.com
Lambda 표현식
자바의 람다 표현식은 Java 8에서 도입된 기능으로, 코드의 간결함과 가독성을 높이기 위해 사용된다. 람다 표현식을 통해 함수형 프로그래밍 스타일을 사용할 수 있으며, 이를 통해 일회성 기능을 정의할 수 있다.
1. 람다 표현식의 기본 문법
(매개변수1, 매개변수2, ...) -> { 실행 블록 }
- 매개변수: 입력 값으로 사용할 변수들.
- ->: 매개변수와 실행 블록을 구분하는 구문.
- 실행 블록: 메서드를 구현하는 구문으로, 하나 이상의 명령을 포함할 수 있다.
[람다 표현식 특징]
- 메소드와 달리 이름이 없다.
- 메소드와 달리 특정 클래스에 종속되지 않지만, 매개변수, 반환 타입, 본체를 가지며, 심지어 예외도 처리할 수 있다.
- 메소드의 인수로 전달될 수도 있고 변수에 대입될 수도 있다.
- 익명 구현 객체와 달리 메소드의 핵심 부분만 포함한다.
[람다 표현식 규칙]
- 선언부의 타입은 생략 가능
- 매개변수가 1개이면 괄호 생략 가능
- 실행문이 하나라면 중괄호, 세미콜론, return문 생략 가능
- return문이 있으면 {} 생략 불가
@FunctionalInterface
interface Negative {
int neg(int x);
}
@FunctionalInterface
interface Printable {
void print();
}
public class Lambda2Demo {
public static void main(String[] args) {
Negative n;
n = (int x) -> {
return -x;
};
n = (x) -> {
return -x;
};
n = x -> {
return -x;
};
n = (x) -> -x;
n = x -> -x;
Printable p;
p = () -> {
System.out.println("안녕!");
};
p = () -> System.out.println("안녕!");
p.print();
}
}
[람다 표현식 예제]
구분 | 일반 문법 | 람다 표현식 적용 |
a와 b 중에 a가 크면 a를 출력하고, 그렇지 않을 경우 b 출력 | int max(int a, int b) { return a > b ? a : b; } |
(a, b) -> a > b ? a : b; |
문자열 타입의 name과 정수형 타입의 i를 받아서 name과 i를 더한 값을 출력 | void printVar(String name, int i) { System.out.println(name + "=" + i); } |
(name, i) - > System.out.println(name + "=" + i); |
정수형 x를 받아서 제곱 출력 | int square(int x) { return x * x; } |
(x) -> x * x; |
임의의 정수 6개 출력 | int roll() { return (int) (Math.random() * 6); } |
() -> (int) Math.random() * 6; |
2. Functional Interface
람다 표현식은 주로 함수를 처리하는 인터페이스인 Functional Interface와 함께 사용된다.
Functional Interface는 하나의 추상 메서드를 가진 인터페이스를 의미하며, 그 예로 Runnable, Callable, Comparator 등이 있다.
- @Functional Interface: 함수형 인터페이스의 조건을 갖추었는지에 대한 검사를 컴파일러에게 요청한다.
3. 람다 표현식의 장점
- 간결함: 코드의 양이 줄어들어 가독성이 높아진다.
- 표현력: 복잡한 익명 클래스 대신 쉽게 이해할 수 있는 형태로 대체할 수 있다.
- 지연 실행: 많은 경우, 실행을 나중으로 미루어 성능을 개선할 수 있다.
4. 유의할 점
- 람다 표현식은 스코프가 제한적이다.
- 람다 표현식 외부에서 선언된 변수와 동일한 이름의 변수를 람다 표현식에서 선언할 수 없다.
- 외부 변수를 사용할 수 있지만, 해당 변수가 final이거나 사실상 final이어야 한다. (람다 표현식에 사용된 지역변수는 final 이다.)
- 추상 메서드가 여러 개인 경우, 람다 표현식을 사용할 수 없다.
- 람다 표현식의 this 키워드는 람다 표현식을 실행한 외부 객체를 의미한다.
람다 표현식과 함께 자주 사용되는 개념
1. 메소드 참조
메소드 참조(Method Reference)는 특정 메소드를 참조하여 람다 표현식을 간결하게 표현하는 방식으로, 전달한 동작을 수행하는 메소드가 이미 정의된 경우에 표현할 수 있는 람다 표현식의 축약형이다.
[주요 유형]
◆ 정적 메소드 참조: 클래스 이름을 사용하여 정적 메소드 참조
클래스이름::정적메소드
class Utility {
static void print(String message) {
System.out.println(message);
}
}
List<String> messages = Arrays.asList("Hello", "World");
messages.forEach(Utility::print); // Utility 클래스의 print 메소드 참조
◆ 인스턴스 메소드 참조: 인스턴스의 메소드 참조
객체이름::인스턴스메소드
클래스이름::인스턴스메소드
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println); // System.out 인스턴스의 print 메소드 참조
◆ 특정 객체의 인스턴스 메소드 참조: 특정 객체의 메소드 참조
String text = "Hello";
Function<String, Integer> getLength = text::length; // 특정 객체의 length 메소드 참조
System.out.println(getLength.apply("")); // 출력: 5
◆ 생성자 참조: 객체 생성 참조
클래스이름::new
배열타입이름::new
아래의 ConstructorRefDemo 클래스는 자바에서 메소드 참조를 활용하여 객체와 배열 생성에 대한 인터페이스를 구현하는 예제이다.
interface NewObject<T> {
T getObject(T o);
}
interface NewArray<T> {
T[] getArray(int size);
}
public class ConstructorRefDemo {
public static void main(String[] args) {
NewObject<String> s;
NewArray<Integer> i;
s = x -> new String(x);
s = String::new;
String str = s.getObject("사과");
i = x -> new Integer[x];
i = Integer[]::new;
Integer[] array = i.getArray(2);
array[0] = 10;
array[1] = 20;
}
}
- NewObject: 제네릭 인터페이스로, 입력받은 객체를 기반으로 새로운 객체를 생성하는 메소드 getObject를 정의.
- NewArray: 제네릭 인터페이스로, 주어진 크기의 배열을 생성하는 메소드 getArray를 정의.
s = x -> new String(x);
s = String::new;
String str = s.getObject("사과");
- 첫 번째 줄에서는 람다 표현식을 사용하여 String 객체를 생성. 입력된 문자열 x에 대해 새로운 String 객체를 생성.
- 두 번째 줄에서는 생성자 참조를 사용하여 같은 기능을 수행. String::new는 새로운 String을 생성하는 생성자를 참조하는 방식.
- s.getObject("사과")를 호출하여 "사과"라는 문자열로 새로운 String 객체 str을 생성.
i = x -> new Integer[x];
i = Integer[]::new;
Integer[] array = i.getArray(2);
array[0] = 10;
array[1] = 20;
- 첫 번째 줄에서는 람다 표현식을 사용하여 주어진 크기의 Integer 배열을 생성. new Integer[x]를 통해 Integer 타입의 배열을 만듦.
- 두 번째 줄에서는 생성자 참조를 사용하여 배열을 생성. Integer[]::new가 새로운 Integer 배열 생성자의 참조.
- i.getArray(2) 메소드를 호출하여 크기가 2인 Integer 배열 array를 생성.
- 이후 array의 각 요소에 값을 할당하는 과정 진행.
[메소드 참조 예]
@FunctionalInterface
interface Mathematical {
double calculate(double d);
}
@FunctionalInterface
interface Pickable {
char pick(String s, int it);
}
@FunctionalInterface
interface Computable {
int compute(int x, int y);
}
class Utils {
int add(int a, int b) {
return a + b;
}
}
public class MethodRefDemo {
public static void main(String[] args) {
Mathematical m;
Pickable p;
Computable c;
m = d -> Math.abs(d);
m = Math::abs;
System.out.println(m.calculate(-50.3));
// p = (a, b) -> a.charAt(b);
p = String::charAt;
System.out.println(p.pick("안녕, 인스턴스 메소드 참조!", 4));
Utils utils = new Utils();
// c = (a, b) -> utils.add(a, b);
c = utils::add;
System.out.println(c.compute(20, 30));
}
}
- 첫번째: Math::abs - m.calculate: 입력된 값의 절대값 출력 ▶ 50.3
- 두번째: String::charAt - p.pick: 입력된 문자열에서 특정 위치의 문자 출력 ▶ 인
- 세번째: utills:add - c.compute: 입력된 두 값을 더한 값 출력 ▶ 50
2. 함수형 인터페이스(Functional Interface) 유형
함수형 인터페이스는 람다 표현식을 사용할 수 있도록 설계된 인터페이스이다.
[주요 유형]
◆ Consumer: 주어진 입력을 사용하지만 결과를 반환하지 않는 함수형 인터페이스
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello!");
◆ Supplier: 인수를 받지 않고 값을 반환하는 인터페이스
Supplier<String> stringSupplier = () -> "Hello World!";
System.out.println(stringSupplier.get()); // 출력: Hello World!
◆ Function<T, R>: 입력을 받아서 결과를 반환하는 인터페이스
Function<Integer, String> intToString = Object::toString;
System.out.println(intToString.apply(5)); // 출력: "5"
◆ Predicate: 입력을 받아서 불리언 값을 반환하는 인터페이스
Predicate<String> isEmpty = String::isEmpty;
System.out.println(isEmpty.test("")); // 출력: true
◆ 람다 표현식과 제네릭의 결합
람다 표현식은 제네릭과 함께 사용할 수 있으며, 함수형 인터페이스를 제네릭으로 정의하면 다양한 타입에 대해 사용할 수 있다.
@FunctionalInterface
interface Calculate<T> { // 제네릭 기반의 함수형 인터페이스
T cal(T a, T b);
}
public class LambdaGeneric {
public static void main(String[] args) {
Calculate<Integer> ci = (a, b) -> a + b;
System.out.println(ci.cal(3, 4));
Calculate<Double> cd = (a, b) -> a + b;
System.out.println(cd.cal(4.32, 3.45));
}
}
3. Comparable 인터페이스
Comparable 인터페이스는 객체의 자연 순서를 정의하기 위해 사용된다. 이 인터페이스는 compareTo(T o) 메소드를 반드시 구현해야 하며, 두 객체를 비교하여 정수값을 반환한다.
public 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);
}
}
넓이와 높이 입력값을 받으면 사각형의 폭을 계산해 주는 Rectangle 클래스가 있다.
이를 Comparable<RecLambda> 인터페이스를 구현하는 코드로 변경하면,
public class RecLambda implements Comparable<RecLambda> {
private int width, height;
public RecLambda(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 int compareTo(RecLambda o) {
return o.findArea() - findArea(); // desc
// return findArea() - o.findArea() //asc/
}
}
Comparable 인터페이스의 compareTo 메소드를 구현하여 두 사각형 객체를 면적에 따라 비교.
- return o.findArea() - findArea();은 내림차순 정렬을 의미하며, 큰 면적이 먼저 오게 된다.
- return findArea() - o.findArea();은 오름차순 정렬을 나타내며, 작은 면적이 먼저 오게 된다.
이를 출력하는 ComparableDemo 클래스를 생성하여 출력해보면
import java.util.Arrays;
public class ComparableDemo {
public static void main(String[] args) {
RecLambda[] rectangles = { new RecLambda(3, 5), new RecLambda(2, 10), new RecLambda(5, 5) };
Arrays.sort(rectangles);
for (RecLambda r : rectangles) {
System.out.println(r);
}
}
}
▶ 사각형의 넓이가 큰 순서대로 출력하게 된다.
4. Comparator 인터페이스
Comparator 인터페이스는 두 객체를 비교하는 방법을 정의하고, compare(T o1, T o2) 메소드를 제공한다. 이를 통해 사용자 정의 정렬 기준을 구현할 수 있다.
import java.util.Arrays;
import java.util.Comparator;
public class ComparatorDemo {
public static void main(String[] args) {
String[] strings = { "로마에 가면 로마법을 따르라", "시간은 금이다.", "펜은 칼보다 강하다." };
Arrays.sort(strings, new Comparator<String>() {
public int compare(String first, String second) {
return first.length() - second.length();
}
});
for (String s : strings)
System.out.println(s);
}
}
- Arrays.sort() 메소드는 두 번째 매개변수로 Comparator를 사용하여 정렬 기준 설정.
- 익명 클래스(Anonymous Class)를 사용하여 Comparator<String>을 구현.
- compare(String first, String second) 메소드는 두 문자열의 길이를 비교하여 반환하며, 길이가 짧은 문자열이 먼저 오도록 정렬.
- first.length() - second.length()의 결과가 양수이면 first가 second보다 길다는 의미로, 이 경우 first가 뒤로 밀리게 된다. 음수이면 반대로 오름차순으로 정렬.
- 정렬된 문자열 배열을 출력. for-each 루프를 사용하여 배열의 각 요소를 순회하고 출력.
[프로그램의 동작 방식]
- 초기 문자열 배열:
"로마에 가면 로마법을 따르라"
"시간은 금이다."
"펜은 칼보다 강하다."
- Arrays.sort()를 호출하여 문자열 자료를 길이에 따라 오름차순으로 정렬. 이 경우 문자열의 길이는 다음과 같다:
"로마에 가면 로마법을 따르라" (16자)
"시간은 금이다." (6자)
"펜은 칼보다 강하다." (7자)
- 정렬 후 출력된 결과:
"시간은 금이다."
"펜은 칼보다 강하다."
"로마에 가면 로마법을 따르라"
이를 람다 표현식으로 바꾸면,
import java.util.Arrays;
public class LambdaDemo {
public static void main(String[] args) {
String[] strings = { "로마에 가면 로마법을 따르라", "시간은 금이다.", "펜은 칼보다 강하다." };
Arrays.sort(strings, (first, second) -> {
return first.length() - second.length();
});
for (String s : strings)
System.out.println(s);
}
}
람다 표현식의 this 키워드
- 람다 표현식에서 this 키워드는 일반 클래스에서의 this와 약간 다른 의미를 가진다.
- 람다 표현식 내에서 사용된 this는 해당 람다 표현식을 포함하는 외부 클래스의 인스턴스를 참조한다. 즉, 람다 표현식의 this는 해당 람다 표현식이 정의된 클래스의 this와 동일하다.
- 람다 표현식의 this는 해당 람다 표현식을 포함하는 외부 클래스의 인스턴스를 참조하는 반면, 익명 내부 클래스에서는 익명 내부 클래스의 this가 그 자신을 참조하게 된다.
아래의 UseThisDemo 클래스는 두 가지 방법, 즉 익명 내부 클래스와 람다 표현식을 사용하여 this 키워드의 동작 방식을 보여주는 예제이다.
interface UseThis {
void use();
}
public class UseThisDemo {
public void lambda() {
String hi = "Hi!";
UseThis u1 = new UseThis() {
public void use() {
System.out.println(this); // 익명 구현체의 this 익명 구현체 자기 자신
// hi = hi + " Lambda. "; // 람다식의 변수는 외부에서 선언된 변수와 동일한 이름의 변수 사용 못함
}
};
u1.use();
UseThis u2 = () -> {
System.out.println(this); // 람다 this: 람다를 실행한 외부 객체
// hi = hi + " Lambda.";
};
u2.use();
}
public String toString() {
return "UseThisDemo";
}
public static void main(String[] args) {
int one = 1;
new UseThisDemo().lambda();
// Comparator<String> c/= (one, two) -> one.length() - two.length();
}
}
[코드 상세 설명]
interface UseThis {
void use();
}
UseThis: use()라는 메소드를 포함하는 간단한 인터페이스. 이 인터페이스를 구현하는 클래스나 객체는 use() 메소드를 구현해야한다.
UseThis u1 = new UseThis() {
public void use() {
System.out.println(this); // 익명 구현체의 this: 익명 구현체 자기 자신
//hi = hi + " Lambda."; // 람다식의 변수는 외부에서 선언된 변수와 동일한 이름의 변수 사용 못함
}
};
u1.use();
- UseThis 인터페이스를 구현하는 익명 내부 클래스 u1을 생성.
- use() 메소드 내부에서 System.out.println(this);를 호출. 여기서 this는 익명 내부 클래스 자체를 참조.
UseThis u2 = () -> {
System.out.println(this); // 람다 this: 람다를 실행한 외부 객체
// hi = hi + " Lambda.";
};
u2.use();
- UseThis 인터페이스를 구현하는 람다 표현식 u2를 생성.
- use() 메소드 내부에서 System.out.println(this);를 호출하는데, 이 this는 UseThisDemo 클래스를 참조. 즉, 람다 표현식이 정의된 외부 클래스의 인스턴스를 나타낸다.
public static void main(String[] args) {
int one = 1;
new UseThisDemo().lambda();
// Comparator<String> c = (one, two) -> one.length() - two.length();
}
- new UseThisDemo().lambda();를 호출하여 lambda() 메소드를 실행.
- 이 호출로 인해 위에서 설명한 익명 내부 클래스와 람다 표현식의 동작을 확인할 수 있다.
'Java > Java 기초' 카테고리의 다른 글
[Java] JDBC : 데이터베이스 (0) | 2025.04.15 |
---|---|
[java] 컬렉션 프레임워크, 제네릭 (0) | 2025.04.11 |
[java] 스레드 제어 (0) | 2025.04.10 |
[java] java 스레드(thread) - 멀티, 메인, 작업 / 동기화 메소드 (1) | 2025.04.10 |
[java] java.lang 패키지 (0) | 2025.04.09 |