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