참조링크에 아주 잘 정리되어 있는데 핵심 골자는 내 생각에는 메소드에서 사용한 로컬 변수는 스택 메모리에 생성되고, 메소드가 끝나면서 회수당하기 때문이다. 만일, 해당 메소드가 끝나고도 존재하는 스레드를 람다로 구현했는데,
또한 스택에 있는 메모리는 스레드끼리 공유가 되지 않는다.
위와 같은 특성 때문에 애초에 람다에서는 로컬 지역 변수 참조가 불가능하다 그렇다면 도대체 왜 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은 이미 캡쳐링된 값이며, 실제 메소드에서 사용하던 지역 변수는 메소드가 끝나며 스택 메모리에서 반환된지 오래일 것이다.
표준 API의 함수적 인터페이스
자바 8부터 java.util.function 표준 API 패키지에는 자주 사용되는 함수적 인터페이스를 제공한다.
위와 같은 형식으로 사용할 수 있으며, LongConsumer, DoubleConsumer, ObjDoubleConsumer<T>, ObjIntConsumer<T> 등 오브젝트와 숫자 타입에 관한 Consumer는 미리 정의되어 있는 것도 꽤 있다. 그런데 가독성과 통일성 측면에서 봤을 때 그냥 Consumer<T>와 BiConsumer<T, U>를 사용하지 않을 이유는 모르겠다.
Supplier
매개값: 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들도 존재한다.
Function
매개값: 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가지 중에 아무거나 랜덤 배정된다.
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의 경우에도 제네릭 타입은 하나만 받는다.
Predicate
매개값: O, 리턴값: O
리턴 값은 오직 boolean 타입
매개값을 조사하여 true/false를 반환한다.
.test() 혹은 .testXXX() 메소드를 통하여 동작한다.
2개의 요소를 쓸 때는 다른 함수적 인터페이스들과 같이 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 함수적 인터페이스를 통해 작성해보았다.
위와 같은 형식으로 테스트할 수 있다. .isEqualTo()는 추상 메소드가 아닌 정적 메소드임에 유의하자.
BinaryOperator 함수적 인터페이스의 minBy()와 maxBy()
BinaryOperator<T> minBy(Comparator<? super T> comparator)
BinaryOperator<T> maxBy(Comparator<? super T> comparator)
컬렉션의 .sort() 메소드처럼 해당 타입의 Comparator를 구현하면 그에 따라 최소값 혹은 최대값을 구해준다.
예제
PersonalHundredMeterRecord 클래스
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() 메소드를 통해 정렬도 가능하다.