람다 표현식 (+함수형 인터페이스/프로그래밍, 일급 객체)
함수형 인터페이스(Functional Interface)
- 추상 메소드를 딱 하나만 가지고 있는 인터페이스
- 두 개의 메소드가 있다면 함수형 인터페이스가 아님
- SAM(Single Abstract Method) 인터페이스
- 함수형 인터페이스의 어노테이션: @FunctionalInterface
- 인터페이스의 선언 앞에 붙이면, 컴파일러는 해당 인터페이스를 함수형 인터페이스로 인식
- Java 컴파일러는 이렇게 명시된 함수형 인터페이스에 두 개 이상의 메소드가 선언되면 오류를 발생시킴
- Java는 java.util.function 패키지를 통해 여러 상황에서 사용할 수 있는 다양한 함수형 인터페이스를 미리 정의하여 제공
- 예시 1. @FunctionalInterface 어노테이션을 가지고 있는 인터페이스
- static 메소드와 default메소드와 상관없이, 추상 메소드가 1개만 있어야 한다는 게 중요
@FunctionalInterface
public interface RunSomething {
void doIt();
//void doItAgain();
static void printName(){
System.out.println("catsbi");
}
default void printAge(){
System.out.println("33");
}
}
- 예시 2. Runnable 인터페이스
- 스레드를 만들 때 사용하는 Runnable 인터페이스의 경우, run( )이라는 추상 메소드를 하나만 가지고 있음
Runnable 인터페이스를 이용해 스레드를 만드는 코드
- 스레드를 실행하려면 start( )메소드 내부의 run( )메소드가 실행되어아 함
- run( )메소드를 실행하려면 run( )메소드를 갖고 있는 Runnable 인터페이스를 객체로 생성해 매개변수에 Runnable 인터페이스를 전달해야 함
- 1) main 스레드에서 Thread 객체 생성
- 2) Thread의 생성자에 Runnable 인터페이스를 객체로 생성해 매개변수로 삽입
- Runnable 인터페이스를 객체로 만들 때 Ctrl+Enter 눌러서 자동완성 시켜주면, 자동으로 run( )메소드가 오버라이딩되어 나옴
- 3) Runnable 인터페이스의 run( ) 메소드를 구현
- 4) Thread 생성자를 닫고 Thread가 갖고 있는 start( )메소드로 Thread 실행시키기
- new Thread(new Runnable( ) {…}).start( );의 의미
- 스레드 생성자 안에 넣은 Runnable의 run( )메소드가 실행되게 해라
- new Thread(new Runnable( ) {…}).start( );의 의미
public class LambdaExam1 {
public static void main(String[] args) {
new Thread(new Runnable(){
public void run(){
for(int i = 0; i < 10; i++){
System.out.println("hello");
} //for
} //run()
}).start(); //Thread
} //main 스레드
} //class
- 스레드가 실행되면 스레드 생성자 안에 넣은 run( )메소드가 실행됨
- 자바는 메소드만 매개변수로 전달할 방법이 없음. 인스턴스만 전달할 수 있음
- 그렇기 때문에 run( )메소드를 가지고 있는 Runnable 인스턴스를 만들어서 전달
람다 표현식은 위처럼 클래스를 작성해 객체를 생성해야만 메소드를 사용할 수 있는 문제점을 해결
- 람다 표현식 이전에는 클래스를 작성해 객체를 생성해야만 클래스 안에 있는 메소드를 사용할 수 있었음 (메소드만 매개변수로 전달할 수 있는 방법이 없었음. only 인스턴스만 전달 가능)
- 람다 표현식 이후로, 클래스를 작성해 객체를 생성하지 않아도 메소드를 사용할 수 있기 때문에 좀 더 편리한 프로그래밍이 가능해짐 (메소드를 정의할 필요도 없음)
위의 코드를 람다식을 이용해서 수정한 코드
public class LambdaExam1 {
public static void main(String[] args) {
new Thread(()->{
for(int i = 0; i < 10; i++){
System.out.println("hello");
} //for
}).start(); //람다식으로 만든 스레드
} //main 스레드
} //class
- new Thread( ( )->{ ..... }).start( ); 에서 ( )->{ ..... } 부분이 람다식(=익명 메소드)임
- JVM은 Thread생성자를 보고 ( )->{ } 이 무엇인지 대상을 추론
- Thread생성자 API를 보면, Runnable인터페이스를 받아들이는 것을 알 수 있음
- JVM은 Thread생성자에 Runnable인터페이스를 구현한 것이 와야 함을 알게 되고 람다식을 Runnable을 구현하는 객체로 자동으로 만들어서 매개변수로 넣어줌
람다 표현식(lambda expression)
개념
- 간단히 말해, 메소드를 하나의 식으로 표현한 것
//메소드를 사용하면
int min(int x, int y) {
return x < y ? x : y;
}
//람다 표현식을 사용하면
(x, y) -> x < y ? x : y;
- 위의 예제처럼 메소드를 람다 표현식으로 표현하면, 클래스를 작성하고 객체를 생성하지 않아도 메소드를 사용할 수 있음
- 함수형 인터페이스의 인스턴스를 만드는 방법으로 사용될 수 있음
- 예: Runnable 인터페이스의 run( )메소드를 사용하는 스레드를 만들기 위해 스레드의 생성자 안에 Runnable 인터페이스의 인스턴스 생성
- 메소드 매개변수, 리턴타입, 변수로 만들어 사용할 수도 있음 (람다 표현식 = 일급객체)
- 람다 표현식은 익명 클래스라고도 함
- Java에서는 클래스의 선언과 동시에 객체를 생성하므로, 단 하나의 객체만을 생성할 수 있는 클래스를 익명 클래스라고 함
- But, 익명 클래스보다는 코드가 짧음
문법
(매개변수목록) -> {함수몸체}
- 매개변수 목록
- 매개변수가 없을 때: ( )
- 매개변수가 한 개일 때: (one) 또는 one > 괄호 생략 가능
- 매개변수가 여러 개일 때 : (one, two) > 괄호 생략 불가능
- 매개변수의 타입은 컴파일러가 추론(infer)할 수 있는 경우엔 생략 가능 But, 명시할 수도 있음 (Integer one, Integer two)
- 함수몸체(실행문)
- 화살표 오른쪽에 함수 본문을 정의
- 함수의 몸체가 여러 줄의 명령문으로 이루어진 경우
- 중괄호 { } 를 사용해서 묶음
- 함수의 몸체가 한 줄인 경우
- 중괄호 { } 생략 가능
- return 키워드도 생략 가능
- But, return문으로만 구성되어 있는 경우엔 중괄호 { } 생략 불가능
- <가능> Supplier<Integer> get20 = ( ) -> { return 20; };
- <불가능> Supplier<Integer> get20 = ( ) -> return 20; //오류 발생
public class Foo {
public static void main(String[] args) {
//함수의 몸체가 한 줄인 경우
Supplier<Integer> get10 = () -> 10;
Supplier<Integer> get20 = () -> {
return 20;
};
UnaryOperator<Integer> plus10 = i -> i + 10;
UnaryOperator<Integer> plus20 = (i) -> i + 20;
BinaryOperator<Integer> plus30 = (i, j) -> i + j + 30;
BinaryOperator<Integer> plus40 = (Integer i, Integer j) -> i + j + 40;
}
}
예시
Compare 인터페이스
- 2개의 값을 비교하여 어떤 값이 더 큰지 구하는 compareTo( )메소드를 가지고 있음
- 2개의 값을 받아들인 후, 정수를 반환하는 메소드를 선언
public interface Compare{
public int compareTo(int value1, int value2);
}
CompareExam 클래스
- Compare 인터페이스를 이용
- Compare 인터페이스를 매개변수로 받아들인 후, 해당 인터페이스를 이용하는 exec메소드
- compareTo메소드가 어떻게 구현되어 있느냐에 따라서 출력되는 값이 다름
public class CompareExam {
public static void exec(Compare compare){
int k = 10;
int m = 20;
int value = compare.compareTo(k, m);
System.out.println(value);
}
public static void main(String[] args) {
exec((i, j)->{
return i - j;
}); //결과: -10
}
}
- JVM은 exec( )메소드를 찾아보고 매개변수 2개(i, j)를 받아들이는 인터페이스(=Compare)를 받아들이는 메소드(=exec( )메소드)가 뭔지를 찾아봄
- exec( )메소드가, 해당 Compare 인터페이스를 받아들이는 알맞은 메소드임을 찾게 됨
- JVM은 람다식을 Compare 인터페이스를 구현하는 익명 객체로 만들어서 exec( )a메소드에 전달함
장단점
- 람다의 장점
- 코드가 훨씬 간결해지고 가독성도 좋아짐
- 코드의 간결성 - 람다를 사용하면 불필요한 반복문의 삭제가 가능하며 복잡한 식을 단순하게 표현할 수 있습니다.
- 지연연산 수행 - 람다는 지연연상을 수행 함으로써 불필요한 연산을 최소화 할 수 있습니다.
- 병렬처리 가능 - 멀티쓰레디를 활용하여 병렬처리를 사용 할 수 있습니다.
- 람다의 단점
- 람다식의 호출이 까다로움
- 람다 stream 사용 시 단순 for문 혹은 while문 사용 시 성능이 떨어짐
- 불필요하게 너무 사용하게 되면 오히려 가독성을 떨어뜨릴 수 있음
함수형 프로그래밍
- 함수를 일급 객체(First class object)로 사용할 수 있음
RunSomething runSomething = () -> System.out.println("Hello");
- Java에서는 이런 형태를 특수한 형태의 Object라고 볼 수 있음
- 함수형 인터페이스를 인라인 형태로 구현한 Object라 볼 수 있는데, 자바는 객체지향언어(OOP)이기 때문에 이 함수를 메소드 매개변수, 리턴타입, 변수로 만들어서 사용할 수 있음
고차 함수(Higher-Order Function)
- 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있음
- Java에서는 함수가 특수한 형태의 오브젝트일 뿐이기에 가능
순수 함수(Pure Function)
개념
- 수학적인 함수에서 가장 중요한 것은 입력받은 값이 동일할 때 결과가 같아야 한다는 것
- 매개변수로 1을 넣었으면 몇 번을 호출하든 11이 나와야 함. 이런 결과를 보장하지 못하거나 못할 여지가 있다면 함수형 프로그래밍이라고 할 수 없음.
@FunctionalInterface
public interface RunSomething {
int doIt(int number);
}
public class Foo {
public static void main(String[] args) {
RunSomething runSomething = (number) -> {
return number + 10;
};
System.out.println(runSomething.doIt(1));//11
System.out.println(runSomething.doIt(1));//11
System.out.println(runSomething.doIt(1));//11
}
}
특징
- side-effect가 없음 (함수 밖에 있는 값을 변경하지 않음)
- 상태가 없음 (함수 밖에 있는 값을 사용하지 않음)
public class Foo {
public static void main(String[] args) {
//int baseNumber = 10; //---(1)
RunSomething runSomething = new RunSomething() {
//int baseNumber = 10; //---(2)
@Override
public int doIt(int number) {
return number + baseNumber;
}
};
}
}
- (1), (2) 위치에 있는 변수는 둘 다 외부값이라 할 수 있고, 접근하거나 변경하려 하면 순수함수라 할 수 없음
- 물론, 문법적으로 (1) 위치에 있는 지역변수를 참조할 수는 있지만 참조하게 될 경우 다른 곳에서 해당 변수를 변경할 수 없음 (final 변수 취급)
일급 객체(First class object)
- 메소드 매개변수, 리턴타입, 변수로 만들어 사용할 수도 있음 (람다 표현식 = 일급객체)
1. 변수나 데이터에 담을 수 있음
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
// 람다식을 인터페이스 타입 변수에 할당
Consumer<String> c = (t) -> System.out.println(t);
c.accept("Hello World");
}
}
2. 함수의 파라미터로 전달할 수 있음
import java.util.function.Consumer;
public class Main {
// 메소드 매개변수로 람다 함수를 전달
public static void print(Consumer<String> c, String str) {
c.accept(str);
}
public static void main(String[] args) {
print((t) -> System.out.println(t) ,"Hello World");
}
}
3. 함수의 리턴값으로 사용할 수 있음
import java.util.function.Consumer;
public class Main {
public static Consumer<String> hello() {
// 람다 함수 자체를 리턴함
return (t) -> {
System.out.println(t);
};
}
public static void main(String[] args) {
Consumer<String> c = hello();
c.accept("Hello World");
}
}
출처 1: https://catsbi.oopy.io/e980e4b7-fde3-4ceb-91f9-181ce2e7b507#11c9ff0d-0077-4abe-aac2-b209b72dfefa
출처 2: https://inpa.tistory.com/entry/CS-👨💻-일급-객체first-class-object#자바의_람다_함수의_일급_객체
람다, 내부클래스, 익명클래스 실습
* 실습 이전에 내부클래스 복습하기 *
실습 1
- 문제 설명
- 내부클래스, 익명클래스, 람다를 왜 사용하는지 자바를 처음 시작할 때는 이해하기 어려울 수 있습니다. 지금은 예제를 보면서 "저렇게도 쓸 수 있구나!" 정도로 이해하면 됩니다. 내부클래스, 익명클래스, 람다를 이용해서 같은 작업을 어떻게 다르게 할 수 있는지 살펴보면서 각각이 어떻게 쓰이는지 눈여겨보세요. 다음 코드는 앞으로 예제에서 사용할 Car클래스입니다.
- Car 클래스
public class Car{
//이어지는 예제에서 사용할 Car클래스입니다.
//이름, 탑승인원, 가격, 사용년수를 필드로 가집니다.
public String name;
public int capacity;
public int price;
public int age;
//각각의 필드를 생성자에서 받아서 초기화합니다.
public Car(String name, int capacity, int price, int age){
this.name = name;
this.capacity = capacity;
this.price = price;
this.age = age;
}
//Car 객체를 문자열로 출력하면 이름을 출력합니다.
public String toString(){
return name;
}
public static void main(String args[]){
Car car = new Car("new model", 4, 3000, 0);
}
}
실습 2 - List 생성
- 문제 설명
- main에서는 다양한 조건의 Car 객체를 만들어서 cars라는 리스트에 넣습니다. 이 cars라는 리스트에 있는 차를 검색해서 조건에 맞는 차를 출력하는 예제들을 살펴볼 텐데요. 첫 번째로 가격이 2000보다 싼 차량을 검색해서 이름을 출력하는 printCarCheaperThan 이라는 함수가 있습니다.
- Car 클래스 (실습 2 ~ 5 동안 변경 없이 계속 쓰이므로 이하의 실습에선 생략)
public class Car{
public String name;
public int capacity;
public int price;
public int age;
public Car(String name, int capacity, int price, int age){
this.name = name;
this.capacity = capacity;
this.price = price;
this.age = age;
}
public String toString(){
return name;
}
}
- CarExam 클래스
import java.util.*;
public class CarExam{
public static void main(String[] args){
//Car객체를 만들어서 cars에 넣습니다.
List<Car> cars = new ArrayList<>();
cars.add( new Car("작은차",2,800,3) );
cars.add( new Car("봉고차",12,1500,8) );
cars.add( new Car("중간차",5,2200,0) );
cars.add( new Car("비싼차",5,3500,1) );
printCarCheaperThan(cars, 2000);
}
public static void printCarCheaperThan(List<Car> cars, int price){
for(Car car : cars){
if(car.price < price){
System.out.println(car);
}
}
}
}
- 결과
- 작은차
봉고차
- 작은차
실습 3 - 내부 클래스 활용
- 문제 설명
- 이번에는 조건이 더 복잡한 경우입니다. 내부클래스를 이용해서 CheckCar라는 인터페이스를 만들고, 그걸 구현하는 CheckCarForBigAndNotExpensive클래스를 만들어서 4명 이상이 탈 수 있고, 가격이 2500 이하인 차를 검색합니다.
- CarExam 클래스
import java.util.*;
public class CarExam{
public static void main(String[] args){
List<Car> cars = new ArrayList<>();
cars.add( new Car("작은차",2,800,3) );
cars.add( new Car("봉고차",12,1500,8) );
cars.add( new Car("중간차",5,2200,0) );
cars.add( new Car("비싼차",5,3500,1) );
printCar(cars, new CheckCarForBigAndNotExpensive());
}
public static void printCar(List<Car> cars, CheckCar tester){
for(Car car : cars){
if (tester.test(car)) {
System.out.println(car);
}
}
}
interface CheckCar{
boolean test(Car car);
}
//내부클래스를 만들어서 사용합니다.
static class CheckCarForBigAndNotExpensive implements CheckCar{
public boolean test(Car car){
return car.capacity >= 4 && car.price < 2500;
}
}
}
- 결과
- 봉고차
중간차
- 봉고차
- 중첩 인터페이스
- 클래스의 멤버로 선언된 인터페이스
- 해당 클래스와 긴밀한 관계를 맺는 구현 클래스를 만들기 위해 사용
- 인스턴스 멤버 인스턴스와 정적 멤버 인터페이스 모두 가능
- 인스턴스 멤버 인터페이스는 외부 클래스의 객체가 있어야 사용 가능하고 정적 멤버 인터페이스는 외부 클래스의 객체 없이 외부 클래스만으로 바로 접근할 수 있음
- 주로 정적 멤버 인터페이스를 많이 사용: UI 프로그래밍에서 이벤트를 처리할 목적으로 많이 활용됨
실습 4 - 익명 클래스 활용
- 문제 설명
- 같은 검색조건에 대해 익명 클래스를 이용하면 별도 클래스를 만들 필요가 없으므로 코드가 조금 더 짧아집니다.
import java.util.*;
public class CarExam{
public static void main(String[] args){
List<Car> cars = new ArrayList<>();
cars.add( new Car("작은차",2,800,3) );
cars.add( new Car("봉고차",12,1500,8) );
cars.add( new Car("중간차",5,2200,0) );
cars.add( new Car("비싼차",5,3500,1) );
printCar(cars,
//인터페이스 CheckCar를 구현하는 익명클래스를 만듭니다.
new CheckCar(){
public boolean test(Car car){
return car.capacity >= 4 && car.price < 2500;
}
}
);
} //main 메소드
public static void printCar(List<Car> cars, CheckCar tester){
for(Car car : cars){
if (tester.test(car)) {
System.out.println(car);
}
}
}
interface CheckCar{
boolean test(Car car);
}
}
- 결과
- 봉고차
중간차
- 봉고차
실습 5 - 람다 활용
- 문제 설명
- 같은 검색조건에 대해 람다를 이용하면 메소드 지정도 필요 없으므로 더 간결하게 표현할 수 있습니다.
- CarExam 클래스
import java.util.*;
public class CarExam{
public static void main(String[] args){
//Car객체를 만들어서 cars에 넣습니다.
List<Car> cars = new ArrayList<>();
cars.add( new Car("작은차",2,800,3) );
cars.add( new Car("봉고차",12,1500,8) );
cars.add( new Car("중간차",5,2200,0) );
cars.add( new Car("비싼차",5,3500,1) );
CarExam carExam = new CarExam();
carExam.printCar(cars,
//인터페이스 CheckCar의 test메소드에 대응하는 람다를 만듭니다.
(Car car) -> { return car.capacity >= 4 && car.price < 2500; }
);
}
public void printCar(List<Car> cars, CheckCar tester){
for(Car car : cars){
if (tester.test(car)) {
System.out.println(car);
}
}
}
interface CheckCar{
boolean test(Car car);
}
}
- 결과
- 봉고차
중간차
출력: 내부클래스, 익명클래스, 람다가 불필요하게 복잡해 보일 수 있지만 다양한 기능을 구현하다 보면 람다를 쓰는 게 더 효율적일 수 있습니다.
이것으로 모든 자바 강의를 마쳤습니다. 수고하셨습니다.
- 봉고차
'☕ Java 웹 프로그래밍 > Java' 카테고리의 다른 글
[프로그래머스] Java(자바) 중급 | Part 7. 스레드 (0) | 2023.05.18 |
---|---|
[프로그래머스] Java(자바) 중급 | Part 6. 어노테이션 (0) | 2023.05.16 |
[프로그래머스] Java(자바) 중급 | Part 5. IO (0) | 2023.05.15 |
[프로그래머스] Java(자바) 중급 | Part 4. 날짜와 시간 (2) | 2023.05.13 |
[프로그래머스] Java(자바) 중급 | Part 3. java.util 패키지 (0) | 2023.05.13 |