java

람다식 ②

코드지우개 2023. 6. 5. 11:25
반응형

 

 
클래스 멤버와 로컬 변수 사용

람다식의 실행 블록에는 클래스의 멤버(필드와 메소드) 및 로컬 변수(지역변수)를 사용할 수 있다.
클래스의 멤버는 제약 사항 없이 사용 가능하지만, 로컬 변수는 제약 사항이 따른다.


클래스의 멤버 사용

람다식 실행 블록에는 클래스의 멤버인 필드와 메소드를 제약 사항 없이 사용할 수 있다. 하지만 this 키워드를 사용할 때에는 주의가 필요하다.
일반적으로 익명 객체 내부에서 this는 익명 객체의 참조이지만, 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조이다.

public interface MyFunctionalInterface {
  public void method();
}
 
public class UsingThis {
  public int outterField = 10;

  class Inner{
    int innerField = 20;

    void method(){
      MyFunctionalInterface fi = () -> {
        System.out.println("outterField : " + outterField);
        System.out.println("outterField : " + UsingThis.this.outterField); // 바깥 객체의 참조를 얻기 위해서는 클래스명.this를 사용

        System.out.println("innerField : " + innerField);
        System.out.println("innerField : " + this.innerField); //람다식 내부에서 this는 Inner 객체를 참조
      };
      fi.method();
    }
  }
}
 
public class UsingThisExample {
  
  public static void main(String[] args) {
    UsingThis usingThis = new UsingThis();
    UsingThis.Inner inner = usingThis.new Inner();
    inner.method();
    
    //  출력
    //  outterField : 10
    //  outterField : 10
    //  innerField : 20
    //  innerField : 20
  }
}
 
 

 

로컬 변수 사용

람다식은 메소드 내부에서 주로 사용되기 때문에 로컬 익명 구현 객체를 생성시킨다고 봐야 한다.
람다식에서 바깥 클래스의 필드나 메소드는 제한 없이 사용할 수 있으나, 메소드의 매개 변수 또는 로컬 변수를 사용하면 이 두 변수는 final 특성을 가져야 한다.

그 이유는 메소드 내에서 생성된 익명 객체는 메소드 실행이 끝나도 힙 메모리에 존재해서 계속 사용할 수 있다.
매개 변수나 로컬 변수는 메소드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 사용할 수 없게 되므로 문제가 발생한다.


따라서 매개 변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만 람다식 내부 또는 외부에서 변경할 수 없다.

public interface MyFunctionalInterface {
  public void method();
}
 
public class UsingLocalVariable {

  void method(int arg){ //람다식에서 사용하고 있기 때문에 arg는 final 특성을 가짐
    int localVar = 40;  //람다식에서 사용하고 있기 때문에 localVar는 final 특성을 가짐

    //arg = 31;           //final 특성 때문에 수정 불가
    //localVar = 41;      //final 특성 때문에 수정 불가

    //람다식
    MyFunctionalInterface fi = () ->{
      //로컬 변수 읽기
      System.out.println("arg : " + arg);
      System.out.println("localVar : " + localVar);
    };
    fi.method();
  }
  
}
 
public class UsingLocalVariableExample {
  public static void main(String[] args) {
    UsingLocalVariable ulv = new UsingLocalVariable();
    ulv.method(20);
  }
}
 

표준 API의 함수적 인터페이스

자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능하다.
자바 8부터는 빈번하게 사용되는 함수적 인터페이스는 java.util.function 표준 API 패키지로 제공되는데 이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서이다.
java.util.function 패키지의 함수적 인터페이스는 크게 Consumer, Supplier, Function, Operator, Predicate로 구분된다.
구분 기준은 인터페이스에 선언된 추상 메소드의 매개값과 리턴 값의 유무이다.

종류
추상메소드 특징
Consumer
- 매 개값은 있고, 리턴 값은 없음
Supplier
- 매 개값은 없고, 리턴 값은 있음
Function
- 매 개값도 있고, 리턴 값도 있음
- 주로 매 개값을 리턴 값으로 매핑(타입 변환)
Operator
- 매 개값도 있고, 리턴 값도 있음
- 주로 매 개값을 연산하고 결과를 리턴
Predicate
- 매 개값은 있고, 리턴 타입은 boolean
- 매 개값을 조사해서 true/false를 리턴

 


Consumer 함수적 인터페이스

Consumer 함수적 인터페이스의 특징은 리턴 값이 없는 accrpt() 메소드를 가지고 있다. accept() 메서드는 단지 매 개값을 소비하는 역할만 한다.
여기서 소비한다는 말은 사용만 할 뿐 리턴 값이 없다는 뜻이다.

인터페이스명
추상 메소드
설명
Consumer<T>
void accept(T t)
객체 T를 받아 소비
BiConsumer<T,U>
void accept(T t, U u)
객체 T와 U를 받아 소비
DoubleConsumer
void accept(double value)
double 값을 받아 소비
IntConsumer
void accept(int value)
int 값을 받아 소비
LongConsumer
void accept(long value)
long 값을 받아 소비
ObjDoubleConsumer<T>
void accept(T t, double value)
객체 T와 double 값을 받아 소비
ObjIntConsumer<T>
void accept(T t, int value)
객체 T와 int 값을 받아 소비
ObjLongConsumer<T>
void accept(T t, long value)
객체 T와 long 값을 받아 소비

 

  • Consumer<T> : <String>이므로 매 개값 t는 String 타입
Consumer<String> consumer = t -> {t를 소비하는 실행문;}
 
  • BiConsumer<T,U> : <String, String> 이무로 매 개값 t와 u는 모두 String 타입
BiConsumer<String, String> consumer = (t,u) -> {t와 u를 소비하는 실행문;}
 
  • ObjIntConsumer<T> : <String>이므로 매 개값 t는 String 타입이고, 매 개값 i는 int 타입 (고정)
ObjIntConsumer<String> consumer = (t, i) -> {t와 i를 소비하는 실행문;}
 

 

Example
public class ConsumerExample {
  public static void main(String[] args) {
    Consumer<String> consumer = t -> System.out.println( t + "8");
    consumer.accept("java"); //출력 : java8

    BiConsumer<String, String> biConsumer = (t, u) -> System.out.println( t + u );
    biConsumer.accept("java", "8"); //출력 : java8

    DoubleConsumer doubleConsumer = d -> System.out.println("java" + d);
    doubleConsumer.accept(8.0); //출력 : java8.0

    ObjIntConsumer<String> objIntConsumer = (t, i) -> System.out.println( t + i );
    objIntConsumer.accept("java", 8); //출력 : java8
  }
}
 

 
Supplier 함수적 인터페이스

Supplier 함수적 인터페이스의 특징은 매개 변수가 없고 리턴 값이 있는 getXXX() 메소드를 가지고 있다. 이 메소드들은 실행 후 호출한 곳으로 데이터를 리턴(공급) 하는 역할을 한다.

인터페이스명
추상 메소드
설명
Supplier<T>
T get()
T 객체를 리턴
BooleanSupplier
boolean getAsBoolean()
boolean 값을 리턴
DoubleSupplier
double getAsDouble()
double 값을 리턴
IntSupplier
int getAsInt()
int 값을 리턴
LongSupplier
long getAsLong()
long 값을 리턴

 

  • Supplier<T> : <String>이므로 리턴 값은 String 타입(문자열)
Supplier<String> supplier = () -> { ...; return "문자열" }
 
  • IntSupplier : 리턴 값은 반드시 int 타입(고정)
IntSupplier supplier = () -> { ...; return int 값; }
 
Example
public class SupplierExample{
  public static void main(String[] args) {
    IntSupplier intSupplier = () -> {
        int num = (int) (Math.random() * 6) + 1;
        return num;
    }
    int num = intSupplier.getAsInt();
    System.out.println( "눈의 수: " + num ); 출력 : 눈의수: 4
  }
}
 

 
Function 함수적 인터페이스

Function 함수적 인터페이스의 특징은 매개변수와 리턴 값이 있는 applyXXX() 메소드를 가지고 있다. 이들 메소드는 매 개값을 리턴 값으로 매핑(타입 변환) 하는 역할을 한다.

인터페이스명
추상 메소드
설명
Function<T,R>
R apply(T t)
객체 T를 객체 R로 매핑
BiFunction<T,U,R>
R apply(T t, U u)
객체 T와 U를 객체 R로 매핑
DoubleFunction<R>
R apply(double value)
double을 객체 R로 매핑
IntFunction<R>
R apply(int value)
int를 객체 R로 매핑
IntToDoubleFunction
double applyAsDouble(int value)
int를 double로 매핑
IntToLongFunction
long applyAsLong(int value)
int를 long으로 매핑
LongToDoubleFunction
double applyAsDouble(long value)
long을 double로 매핑
LongToIntFunction
int applyAsInt(long value)
long을 int로 매핑
ToDoubleBiFunction<T,U>
double applyAsDouble(T t, U u)
객체 T와 U를 double로 매핑
ToDoubleFunction<T>
double applyAsDouble(T t)
객체 T를 double로 매핑
ToIntBiFunction<T,U>
int applyAsInt(T t, U u)
객체 T와 U를 int로 매핑
ToIntFunction<T>
int applyAsInt(T t)
객체 T를 int로 매핑
ToLongBiFunction<T,U>
long applyAsLong(T t, U u)
객체 T와 U를 long으로 매핑
ToLongFunction<T>
long applyAsLong(T t)
객체 T를 long으로 매핑

 

  • Function<T,R> : <Student, String>이므로 매 개값 t는 Student 타입이고 리턴 값은 String 타입 Studnet 객체를 String으로 매핑
Function<Student, String> function = t -> { return t.getName(); }
또는
Function<Student, String> function = t -> t.getName();
 
  • ToIntFunction<T> : <Studnet>이므로 매 개값 t는 Student 타입이고 리턴 값은 int 타입(고정). Student 객체를 int로 매핑
ToIntFunction<Student> function = t -> { return t.getScore(); }
또는
ToIntFunction <Stdent> function = t -> t.getScore();
 
Example
public class Student{
    private String name;
    private int englishScore;
    private int mathScore;

    public Student(String name, int englishScore, int mathScore){
        this.name = name;
        this.englishScore = englishScore;
        this.mathScore = mathScore;
    }
    
    public String getName(){
        return name;
    }

    public int getEnglishScore(){
        return englishScore;
    }

    public int getMathScore(){
        return mathScore;
    }

}
 
public class FunctionExample1{
    private static List<Student> list = Arrays.asList(
        new Student("유재석", 90, 96),
        new Student("강호동", 95, 93)
    );

    public static void printString(Function<Student, String> function){
        for(Student student : list){
            System.out.print( function.apply(student) + " " ); //Student가 String으로 매핑돼서 리턴
        }
        System.out.println();
    }

    public static void printInt(ToIntFunction<Student> function){
        for(Student student : list){
            System.out.print( function.applyAsInt(student) + " " ); //Student가 int으로 매핑돼서 리턴
        }
        System.out.println();
    }
    //메인
    public static void main(String[] args){
        System.out.println( "[학생 이름]" );
        printString( t -> t.getName() ); //학생 객체를 넣으면 학생 이름을 리턴 Student -> String
        //출력 : 유재석, 강호동


        System.out.println( "[영어 점수]" );
        printInt( t -> t.getEnglishScore() ); //학생 객체를 넣으면 학생 영어 점수를 리턴 Student -> int
        //출력 : 90, 95

        System.out.println( "[수학 점수]" );
        printInt( t -> t.getMathScore() ); //학생 객체를 넣으면 학생 수학 점수를 리턴 Student -> int     
        //출력 : 96, 93
    } 

}
 

Operator 함수적 인터페이스

Operator 함수적 인터페이스의 특징은 Function과 동일하게 매개변수와 리턴 값이 있는 applyXXX() 메소드를 가지고 있다. 하지만 Opertor 메소드는 매 개값을 리턴 값으로 매핑(타입 변환) 하는 역할보다는 매 개값을 이용해서 연산을 수행한 후 동일한 타입으로 리턴 값을 제공하는 역할을 한다.

인터페이스명
추상 메소드
설명
BinaryOperator<T>
BiFunction<T,U,R>의 하위 인터페이스
T와 U를 연산 후 R 리턴
UnaryOperator<T>
Function<T,R>의 하위 인터페이스
T를 연산한 후 R 리턴
DoubleBinaryOperator
double applyAsDouble(double, double)
두 개의 double 연산
DoubleUnaryOperator
double applyAsDouble(double)
한 개의 double 연산
IntBinaryOperator
int applyAsInt(int int)
두 개의 int 연산
IntUnaryOperator
int applyAsInt(int)
한 개의 int 연산
LongBinaryOperator
long applyAsLong(long long)
두 개의 long 연산
LongUnaryOperator
long applyAsLong(long)
한 개의 long 연산

 

  • IntBinaryOperator : 매 개값 a, b는 모두 int 타입이고, 연산 후, 리턴 값도 int 타입
IntBinaryOperator operator = (a,b) -> { ...; return int 값; }
 
  • IntUnaryOperator : 매 개값 a는 int 타입이고, 연산 후, 리턴 값도 int 타입
IntUnaryOperator operator = a -> { ...; return int 값; }
 
Example
public class OperatorExample{

    private static int[] scores = { 92, 95, 87 };

    public static int maxOrMin(IntBinaryOperator operator){
        int result = scores[0];
        for(int score : scores){
            result = operator.applyAsInt(result, score);
        }
        return result;
    }

    public static void main(String[] args){
        //최대값 얻기
        int max = maxOrMin(
            (a,b) -> {
                if(a>=b) return a;
                else return b;
            }
        );
        System.out.println("최대값 : " + max); //출력 : 95

        //최소값 얻기
        int min = maxOrMin(
            (a,b) -> {
                if(a<=b) return a;
                else return b;
        )
        System.out.println("최소값 : " + min); //출력 : 92
    }


}
 

Predicate 함수적 인터페이스

Predicate 함수적 인터페이스의 특징은 매개변수와 boolean 리턴 값이 있는 testXXX() 메소드를 가지고 있다. 이 메소드는 매 개값을 조사해서 true 또는 false를 리턴하는 역할을 한다.

인터페이스명
추상 메소드
설명
Predicate<T>
boolean test(T t)
객체 T를 조사
BiPredicate<T,U>
boolean test(T t, U u)
객체 T와 U를 비교 조사
DoublePredicate
boolean test(double value)
double 값을 조사
IntPredicate
boolean test(int value)
int 값을 조사
LongPredicate
boolean test(long value)
long 값을 조사

 

  • Predicate<T> : 매 개값 t는 Student 타입이고, 리턴 값은 boolean타입(고정)
Predicate<Student> predicate = t -> { return t.getSex().equals("남자"); }
또는
Predicate<Student> predicate = t -> t.getSex().equals("남자");
 
Example
public class Student{
    private String name;
    private String sex;
    private int score;

    public Student(String name, String sex, int score){
        this.name = name;
        this.sex = sex;
        this.score = score;
    }

    public String getSex(){
        return sex;
    }

    public int getScore(){
        return score;
    }
}
 
public class PredicateExample{

    private static List<Student> list = Arrays.asList(
        new Student("유재석", "남자", 90),
        new Student("이효리", "여자", 90),
        new Student("강호동", "남자", 95),
        new Student("아이유", "여자", 92)
    );

    public static double avg(Predicate<Student> predicate){
        int count, sum = 0;
        for(Student student : list){
            if(predicate.test(student)){
                count ++;
                sum += student.getScore();
            }
        }
        return (double)sum/count;
    }

    public static void main(String[] args){
        //남자 평균 점수
        double maleAvg = avg( t -> t.getSex().equals("남자") );
        System.out.println( "남자 평균 점수 : " +  maleAvg ); //출력 : 92.5

        //여자 평균 점수
        double femaleAvg = avg( t -> t.getSex().equals("여자") );
        System.out.println( "여자 평균 점수 : " +  femaleAvg ); //출력 : 91.0
    }
}
 

 

 

반응형