이것이 자바다 정리 #14 람다식
이것이 자바다 책을 참고하였습니다.
수학자 Alonzo Church가 발표한 람다 계산법에서 사용된 식으로, John McCarthy가 프로그래밍 언어에 도입했다.
Runnable runnable = new Runnable() {
@Override
public void run() { ... }
}
Runnable runnable = () -> { ... }
람다식은 익명의 Runnable
객체를 생성한다.
(타입 매개변수, ...) -> { 실행문; ... }
->
기호는 매개변수를 이용해서 중괄호 {}
를 실행한다는 뜻으로 해석하면 된다.
(int a) -> { System.out.println(a); }
(a) -> { System.out.println(a); }
일반적으로 타입은 인터페이스의 추상 메소드를 통해 자동으로 인식되기 때문에 언급하지 않는다.
(a) -> System.out.println(a)
일반적으로 실행문이 1줄일 때는 실행문을 축약시킨다.
(a) -> a + "원"
return
할 값에 따로 2줄 이상의 코드가 필요하지 않다면, 위와 같이 작성하면 자동으로 return
된다.
람다식은 기본적으로 익명 구현 클래스를 생성하고 객체화한다. 람다식은 대입될 인터페이스의 종류에 따라 작성 방법이 달라지기 때문에 람다식이 대입될 인터페이스를 람다식의 타겟 타입(target type)이라고 한다.
@FunctionalInterface
애노테이션을 붙이면, 두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹을 해준다.
@FunctionalInterface
애노테이션은 어디까지나 선택사항이다.
메소드를 2개 이상 만들면 위와 같은 컴파일 에러가 뜬다. 단, 위의 설명을 보면 디폴트 메소드
나 Object
타입의 기본 메소드를 오버라이드하는 것은 상관없다고 한다.
상기 항목들은 상관없다.
Object
클래스의 메소드this
의 사용에 주의해야 한다. this
는 바깥 객체가 아닌, 람다식에서 만든 익명 객체를 가리킨다.this
를 사용하고 싶다면, 외부 객체명.this
를 이용하면 된다.@FunctionalInterface
public interface MyFunctionalInterface {
void method();
}
public class UsingThis {
public int outerField = 10;
class Inner {
int innerField = 20;
void method() {
MyFunctionalInterface mfi = () -> {
System.out.println("outerField: " + outerField);
System.out.println("outerField: " + UsingThis.this.outerField);
System.out.println("innerField: " + innerField);
System.out.println("innerField: " + Inner.this.innerField);
};
mfi.method();
}
}
}
public class UsingThisExample {
public static void main(String[] args) {
UsingThis usingThis = new UsingThis();
UsingThis.Inner inner = usingThis.new Inner(); // 중첩 클래스 생성하기
inner.method();
}
}
중첩클래스를 생성하고 싶을 때는
상위인스턴스.new 중첩클래스명()
과 같은 문법으로 생성하면 된다.
위 내용을 실행시켜보면 정상적으로 실행되는 것을 볼 수 있다.
final
특성을 가져야 한다.참조 링크
참조링크에 아주 잘 정리되어 있는데 핵심 골자는 내 생각에는 메소드에서 사용한 로컬 변수는 스택 메모리에 생성되고, 메소드가 끝나면서 회수당하기 때문이다. 만일, 해당 메소드가 끝나고도 존재하는 스레드를 람다로 구현했는데,
또한 스택에 있는 메모리는 스레드끼리 공유가 되지 않는다.
위와 같은 특성 때문에 애초에 람다에서는 로컬 지역 변수 참조가 불가능하다 그렇다면 도대체 왜 final
로 된 로컬 지역 변수에는 접근이 가능할까? 그 이유는 바로 람다에서는 해당 지역 변수를 참조하는 것이 아닌 해당 지역 변수를 복사해서 사용하기 때문이다. 이와 같은 행위를 람다 캡처링이라고 한다.
이전의 UsingThis
클래스의 내용을 아래와 같이 살짝 바꿔보면 이해가 쉽다.
public class UsingThis {
public int outerField = 10;
class Inner {
int innerField = 20;
void method() {
int localFieldVariable = 11;
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
System.out.println("thread implemented by lambda expression is running");
System.out.println("localFieldVariable is " + localFieldVariable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
System.out.println("Method is already over");
}
}
}
위의 경우에 메소드가 끝난 이후에도 thread
는 계속 실행된다. 또한 실행되며 지역 변수인 localFieldVariable
을 계속 참조한다. 하지만 실제 localFieldVariable
은 이미 캡쳐링된 값이며, 실제 메소드에서 사용하던 지역 변수는 메소드가 끝나며 스택 메모리에서 반환된지 오래일 것이다.
java.util.function
표준 API 패키지에는 자주 사용되는 함수적 인터페이스를 제공한다.매개값: O, 리턴값: X
.accept()
를 통해 실행됨public class ConsumerTest {
public static void main(String[] args) {
Consumer<String> printer = s -> {
System.out.println("전달받은 문자열: " + s);
};
printer.accept("printerTest");
Consumer<Number> printerForNumber = arg -> {
System.out.println("전달받은 인자: " + arg);
};
printerForNumber.accept(123123);
BiConsumer<String, Number> printerStringNumberPair = (s, n) -> {
System.out.println("전달받은 문자열: " + s);
System.out.println("전달받은 숫자: " + n);
};
printerStringNumberPair.accept("지금 몇분이야?", 51);
}
}
위와 같은 형식으로 사용할 수 있으며, LongConsumer
, DoubleConsumer
, ObjDoubleConsumer<T>
, ObjIntConsumer<T>
등 오브젝트와 숫자 타입에 관한 Consumer
는 미리 정의되어 있는 것도 꽤 있다. 그런데 가독성과 통일성 측면에서 봤을 때 그냥 Consumer<T>
와 BiConsumer<T, U>
를 사용하지 않을 이유는 모르겠다.
매개값: X, 리턴값: O
.get()
혹은 타입에 따라 미리 정의된 .getXXX()
메소드를 통해 실행된다.public class SupplierTest {
public static void main(String[] args) {
Supplier<Integer> randomIntSupplier = () -> (int) Math.floor(Math.random() * 10);
System.out.println("randomIntSupplier.get() = " + randomIntSupplier.get());
}
}
위는 0~9
까지의 수를 랜덤으로 출력하는 예제이다. IntSupplier
와 같이 제네릭이 아닌 타입이 앞에 붙은 Supplier
들도 존재한다.
매개값: O, 리턴값: O
.apply()
혹은 타입에 따라 정의된 .applyXXX()
메소드를 통해 실행된다.public class MiddleSchoolStudent {
private String studentName;
private String elementarySchoolName;
private String middleSchoolName;
public String getStudentName() {
return studentName;
}
public void setStudentName(String studentName) {
this.studentName = studentName;
}
public String getElementarySchoolName() {
return elementarySchoolName;
}
public void setElementarySchoolName(String elementarySchoolName) {
this.elementarySchoolName = elementarySchoolName;
}
public String getMiddleSchoolName() {
return middleSchoolName;
}
public void setMiddleSchoolName(String middleSchoolName) {
this.middleSchoolName = middleSchoolName;
}
}
public class HighSchoolStudent extends MiddleSchoolStudent{
private String highSchoolName;
public HighSchoolStudent(MiddleSchoolStudent middleSchoolStudent, String highSchoolName) {
setMiddleSchoolInfo(middleSchoolStudent);
this.setHighSchoolName(highSchoolName);
}
public void setMiddleSchoolInfo(MiddleSchoolStudent middleSchoolStudent) {
this.setStudentName(middleSchoolStudent.getStudentName());
this.setElementarySchoolName(middleSchoolStudent.getElementarySchoolName());
this.setMiddleSchoolName(middleSchoolStudent.getMiddleSchoolName());
}
public String getHighSchoolName() {
return highSchoolName;
}
public void setHighSchoolName(String highSchoolName) {
this.highSchoolName = highSchoolName;
}
}
public class FunctionTest {
public static void main(String[] args) {
List<String> highSchoolList = new ArrayList<>();
highSchoolList.add("똘똘고등학교");
highSchoolList.add("아무고등학교");
highSchoolList.add("개똥고등학교");
highSchoolList.add("소똥고등학교");
Function<MiddleSchoolStudent, HighSchoolStudent> middleSchoolStudentToRandomHighSchoolStudent = (middleSchoolStudent) ->
new HighSchoolStudent(middleSchoolStudent, highSchoolList.get((int) (Math.random() * highSchoolList.size())));
MiddleSchoolStudent middleSchoolStudent1 = new MiddleSchoolStudent();
middleSchoolStudent1.setStudentName("김똘똘");
middleSchoolStudent1.setElementarySchoolName("똘똘초등학교");
middleSchoolStudent1.setMiddleSchoolName("똘똘중학교");
MiddleSchoolStudent middleSchoolStudent2 = new MiddleSchoolStudent();
middleSchoolStudent2.setStudentName("김똘망");
middleSchoolStudent2.setElementarySchoolName("똘망초등학교");
middleSchoolStudent2.setMiddleSchoolName("똘망중학교");
HighSchoolStudent highSchoolStudent1 = middleSchoolStudentToRandomHighSchoolStudent.apply(middleSchoolStudent1);
System.out.println("%s, %s".formatted(highSchoolStudent1.getStudentName(), highSchoolStudent1.getHighSchoolName()));
HighSchoolStudent highSchoolStudent2 = middleSchoolStudentToRandomHighSchoolStudent.apply(middleSchoolStudent2);
System.out.println("%s, %s".formatted(highSchoolStudent2.getStudentName(), highSchoolStudent2.getHighSchoolName()));
}
}
위는 중학생에 대한 정보에 고등학교 정보를 추가하여 고등학생으로 만드는 코드를 작성해본 것이다. 고등학교는 4가지 중에 아무거나 랜덤 배정된다.
public class FunctionTest {
public static void main(String[] args) {
List<String> highSchoolList = new ArrayList<>();
highSchoolList.add("똘똘고등학교");
highSchoolList.add("아무고등학교");
highSchoolList.add("개똥고등학교");
highSchoolList.add("소똥고등학교");
BiFunction<MiddleSchoolStudent, String, HighSchoolStudent> middleSchoolStudentToRandomHighSchoolStudent = (middleSchoolStudent, highSchoolName) ->
new HighSchoolStudent(middleSchoolStudent, highSchoolName);
;
MiddleSchoolStudent middleSchoolStudent1 = new MiddleSchoolStudent();
middleSchoolStudent1.setStudentName("김똘똘");
middleSchoolStudent1.setElementarySchoolName("똘똘초등학교");
middleSchoolStudent1.setMiddleSchoolName("똘똘중학교");
MiddleSchoolStudent middleSchoolStudent2 = new MiddleSchoolStudent();
middleSchoolStudent2.setStudentName("김똘망");
middleSchoolStudent2.setElementarySchoolName("똘망초등학교");
middleSchoolStudent2.setMiddleSchoolName("똘망중학교");
HighSchoolStudent highSchoolStudent1 = middleSchoolStudentToRandomHighSchoolStudent.apply(middleSchoolStudent1, highSchoolList.get((int) (Math.random() * highSchoolList.size())));
System.out.println("%s, %s".formatted(highSchoolStudent1.getStudentName(), highSchoolStudent1.getHighSchoolName()));
HighSchoolStudent highSchoolStudent2 = middleSchoolStudentToRandomHighSchoolStudent.apply(middleSchoolStudent2, highSchoolList.get((int) (Math.random() * highSchoolList.size())));
System.out.println("%s, %s".formatted(highSchoolStudent2.getStudentName(), highSchoolStudent2.getHighSchoolName()));
}
}
위는 단순히 BiFunction
형태로 바꾸어본 것이다.
매개값: O, 리턴값: O
Function
과 동일하게 .apply()
혹은 .applyXXX()
메소드를 통해 실행된다.public class OperatorTest {
public static int minOrMax(IntBinaryOperator intBinaryOperator, int[] scores) {
int result = scores[0];
for (int score : scores) {
result = intBinaryOperator.applyAsInt(result, score);
}
return result;
}
public static void main(String[] args) {
int max = minOrMax((a, b) -> {
if (a > b) {
return a;
}
return b;
}, new int[]{15, 10, 12, 100});
System.out.println("max = " + max);
int min = minOrMax((a, b) -> {
if (a > b) {
return b;
}
return a;
}, new int[]{15, 10, 12, 100});
System.out.println("min = " + min);
}
}
Operator
는 예외적으로 Operator
자체 인터페이스는 존재하지 않고, BinaryOperator<T>
, UnaryOperator<T>
로 나뉜다. 같은 타입을 리턴하기 때문에 BinaryOperator
의 경우에도 제네릭 타입은 하나만 받는다.
매개값: O, 리턴값: O
boolean
타입true/false
를 반환한다..test()
혹은 .testXXX()
메소드를 통하여 동작한다.BiPredicate
를 사용하면 된다.public class Student {
private String name;
private int score;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}
public class PredicateTest {
public static void main(String[] args) {
Student student1 = new Student();
student1.setName("김똘똘");
student1.setScore(75);
Student student2 = new Student();
student2.setName("김똘만");
student2.setScore(85);
Student[] students = {student1, student2};
Predicate<Student> isPassedStudent = (student) -> student.getScore() > 80;
for (Student student : students) {
boolean test = isPassedStudent.test(student);
if(test) {
System.out.println(student.getName() + "님은 시험에 합격하였습니다.");
}else {
System.out.println(student.getName() + "님은 시험에 불합격하였습니다.");
}
}
}
}
Student
라는 객체를 만들고, 점수를 부여하여 시험에 합격했는지 알아보는 과정을 Predicate
함수적 인터페이스를 통해 작성해보았다.
위와 같은 관점에서 자바 표준 API의 함수적 인터페이스도 몇가지 정적 메소드를 갖고 있다.
.andThen()
.compose()
위 두개의 메소드는 첫번째 처리 결과를 두번째 매개값으로 제공해서 최종 결과값을 얻을 때 사용한다.
Consumer
,Function
,Operator
가 제공하는 함수적 인터페이스 중 부분적인 것들에서만 사용 가능하다.
public class ConsumerAndThenTest {
public static void main(String[] args) {
Student student1 = new Student();
student1.setName("김똘똘");
student1.setScore(75);
Student student2 = new Student();
student2.setName("김똘만");
student2.setScore(85);
Student[] students = {student1, student2};
Consumer<Student> studentName = (student) -> {
System.out.println("이름: " + student.getName());
};
Consumer<Student> studentScore = (student) -> {
System.out.println("점수: " + student.getScore());
};
Consumer<Student> studentNameAndScore = studentName.andThen(studentScore);
for (Student student : students) {
studentNameAndScore.accept(student);
}
}
}
Function
은 함수적 인터페이스의 결과로 다른 타입을 반환했다. 연결되는 함수적 인터페이스에서 또 그 타입을 다른 타입으로 변환시킬 수 있다.
public class FunctionAndThenTest {
public static void main(String[] args) {
Function<Member, Address> memberToAddress = (member) -> member.getAddress();
Function<Address, String> addressToCityString = (address) -> address.getCity();
Function<Member, String> memberToCityString = memberToAddress.andThen(addressToCityString);
Address address = new Address();
address.setCity("대전시");
address.setStreet("봉명로");
Member member = new Member();
member.setName("김똘똘");
member.setAddress(address);
String city = memberToCityString.apply(member);
System.out.println("city: " + city);
memberToCityString = addressToCityString.compose(memberToAddress);
city = memberToCityString.apply(member);
System.out.println("city: " + city);
}
}
위는 Member
객체의 멤버인 Address
객체에서 City
문자열만 빼오는 예제이다. .compose()
의 경우에는 호출 순서만 역순으로 하면 .andThen()
과 결과가 같다.
메소드 이름과 같이 각각 논리 연산자 and, or, not에 해당한다고 보면 된다.
public class PredicateDefaultMethodTest {
public static void main(String[] args) {
IntPredicate isMultipleOfTwo = (num) -> num % 2 == 0;
IntPredicate isMultipleOfThree = (num) -> num % 3 == 0;
IntPredicate isMultipleOfTwoAndThree = isMultipleOfTwo.and(isMultipleOfThree);
boolean test1 = isMultipleOfTwoAndThree.test(6); // true
System.out.println("test1 = " + test1);
IntPredicate isMultipleOfTwoOrThree = isMultipleOfTwo.or(isMultipleOfThree);
boolean test2 = isMultipleOfTwoOrThree.test(5); // false
System.out.println("test2 = " + test2);
IntPredicate isNotMultipleOfTwo = isMultipleOfTwo.negate();
boolean test3 = isNotMultipleOfTwo.test(5); // true
System.out.println("test3 = " + test3);
Predicate<String> isEqualToMyName = Predicate.isEqual("Jake Seo");
boolean test4 = isEqualToMyName.test("Jake Seo");
System.out.println("test4 = " + test4);
}
}
위와 같은 형식으로 테스트할 수 있다. .isEqualTo()
는 추상 메소드가 아닌 정적 메소드임에 유의하자.
BinaryOperator<T> minBy(Comparator<? super T> comparator)
BinaryOperator<T> maxBy(Comparator<? super T> comparator)
컬렉션의 .sort()
메소드처럼 해당 타입의 Comparator
를 구현하면 그에 따라 최소값 혹은 최대값을 구해준다.
public class PersonalHundredMeterRecord implements Comparator<PersonalHundredMeterRecord>, Comparable<PersonalHundredMeterRecord> {
String name;
LocalTime record;
public PersonalHundredMeterRecord(String name, LocalTime record) {
this.name = name;
this.record = record;
}
public String getName() {
return name;
}
public LocalTime getRecord() {
return record;
}
@Override
public int compare(PersonalHundredMeterRecord o1, PersonalHundredMeterRecord o2) {
return o1.getRecord().compareTo(o2.getRecord());
}
@Override
public int compareTo(PersonalHundredMeterRecord o) {
return this.compare(this, o);
}
@Override
public String toString() {
return "TournamentResult{" +
"name='" + name + '\'' +
", record=" + record +
'}';
}
}
위와 같이 개인의 100미터 달리기 기록을 저장하는 객체가 있다고 가정하자. 각각 이름과 100미터 달리기 기록을 저장한다. 기록은 LocalTime
객체를 이용해서 저장한다.
Comparator
의 상속을 받아서 .compare()
를 구현하고 Comparable
의 상속을 받아서 .compareTo()
도 구현해놨다.
public class MinByMaxByTest {
public static void main(String[] args) {
List<PersonalHundredMeterRecord> personalRecords = new ArrayList<>();
PersonalHundredMeterRecord record1 = new PersonalHundredMeterRecord("우사인볼트", LocalTime.of(0, 0, 9));
PersonalHundredMeterRecord record2 = new PersonalHundredMeterRecord("손흥민", LocalTime.of(0, 0, 10));
PersonalHundredMeterRecord record3 = new PersonalHundredMeterRecord("허경영", LocalTime.of(0, 0, 2));
PersonalHundredMeterRecord record4 = new PersonalHundredMeterRecord("빅현배", LocalTime.of(0, 0, 50));
personalRecords.add(record1);
personalRecords.add(record2);
personalRecords.add(record3);
personalRecords.add(record4);
BinaryOperator<PersonalHundredMeterRecord> binaryOperator = BinaryOperator.minBy(PersonalHundredMeterRecord::compareTo);
// 우사인볼트 vs 손흥민은? winner = 우사인볼트
PersonalHundredMeterRecord winner = binaryOperator.apply(record1, record2);
System.out.println("winner = " + winner.getName());
Collections.sort(personalRecords);
System.out.println("personalRecords = " + personalRecords);
}
}
위는 각각 선수들의 기록 객체를 만들고, personalRecords
라는 배열 리스트에 기록을 넣어놓은 부분이다. BinaryOperator.minBy()
메소드를 이용해 두개의 기록을 넣으면, 둘 중 누가 더 빨리(min
) 뛰었는지 알 수 있다. 메소드를 .maxBy()
로 바꾸면 둘 중 누가 느리게 뛰었는지 알 수도 있다. .compareTo()
메소드는 클래스 단에서 구현한 것을 이용했다. Comparable
인터페이스를 상속하면, compareTo()
를 오버라이드하여 구현할 수 있다.
위 클래스의 .compare()
도 구현해 놨기 때문에 Collections.sort()
메소드를 통해 정렬도 가능하다.
winner = 우사인볼트
personalRecords = [PersonalHundredMeterRecord{name='허경영', record=00:00:02}, PersonalHundredMeterRecord{name='우사인볼트', record=00:00:09}, PersonalHundredMeterRecord{name='손흥민', record=00:00:10}, PersonalHundredMeterRecord{name='빅현배', record=00:00:50}]
결과는 위와 같이 잘 나온다. .toString()
도 구현해놓아서 클래스의 내용이 잘 보인다.
메소드를 참조해서 불필요한 매개변수를 제거하여 코드를 간결히 보여주는 것이 목적이다.
// 클래스::메소드
// Math.max(a, b)
operator = Math::max
// 참조변수::메소드
// object.instanceMethod(a, b)
operator = object::instanceMethod
// 클래스::instanceMethod
// personalRecord1.compareTo(personalRecord2)
operator = PersonalRecord::compareTo
// 클래스::new
// (a, b) -> { return new Pair(a, b) }
function = Pair::new