java.util.function패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다.
매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의되어 있고, Function 의 변형으로 Predicate가 있다.
Runnable : 매개변수도 없고, 반환값도 없음
Supplier<T> : 매개변수는 없고 반환값만 있는 공급자
Consumer<T> : 매개변수만 있고, 반환값은 없는 소비자
Function<T,R> : 하나의 매개변수와 하나의 결과를 반환하는 일반적인 함수
Predicate<T> : 조건식을 표현하는데 사용, 하나의 매개변수와 boolean 타입의 반환값
Predicate는 반환값이 boolean이라는 것만 제외하면 Function과 동일하다.
Predicate는 조건식을 함수로 표현히는데 시용된다.
Predicate는 Function의 변형으로, 반환타입이 boolean이라는 것만 다르다. Predicate 는 조건식을 람다식(함수)으로 표현하는데 사용된다
java.util.function 패키지 Quiz
1~100 범위의 숫자를 반환만 하므로 공급자 > Supplier<Integer>
매개변수만 있고 반환값을 없으므로 수요자 > Consumer<Integer>
조건식이 있으므로 Predicate<Integer, Boolean>
매개변수 하나와 반환값이 있으므로 Function<Integer, Integer>
매개변수가 2개인 함수형 인터페이스
매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 "Bi’가 붙는다
BiSupplier 는없다. 함수의 반환값은 0개 혹은 1개만 가능하기 때문이다. 반환값이 2개일 수는 없다.
람다식의 도입으로 인해, 이제 자바는 객체지향언어(OOP)인 동시에 함수형 언어가 되었다.
람다식(Lambda expression)은 메서드를 하나의 ‘식(expression)’으로 표현한 것이다.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 ‘익명 함수(anonymous function)’이라고도 한다.
모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 이 메서드를 호출할 수 있다.
그러나 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 이 메서드의 역할을 대신할 수 있다.
람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다.
람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.
Q. 메서드와 함수의 차이
A. 전통적으로 프로그래밍에서 함수라는 이름은 수학에서 따온 것입니다. 수학의 함수와 개
념이 유사하기 때문이죠. 그러나 객체지향개념에서는 함수(function)대신 객체의 행위나 동
작을 의미하는 메서드(method)라는 용어를 사용합니다. 메서드는 함수와 같은 의미이지만,
특정 클래스에 반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미의 다른
용어를 선택해서 사용한 것입니다. 그러나 이제 다시 람다식을 통해 메서드가 하나의 독립적
인 기능을 하기 때문에 함수라는 용어를 사용하게 되었습니다.
즉 함수는 클래스에 독립적이나 메서드는 반드시 클래스에 작성해야하므로 종속적이다.
람다식 작성하기
람다식은 메서드에서 이름과 반환타입을 제거하고
매개변수 선언부와 몸 통{ } 사이에
‘ -> ’를 추가 한다.
이때는 ‘문장(statement)’이 아닌 ‘식’이므로 끝에 ‘;’을 붙이지 않는다.
람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있는데, 대부분의 경우에 생략가능하다.
람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문이다.
‘(int a. b) -> a > b ? a : b’와 같이 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않는다.
선언된 매개변수가 하나뿐인 경우에는 괄호( ) 를 생략할 수 있다.
단, 매개변수의 타입이 있으면 괄호()를 생략할 수 없다.
괄호{ } 안의 문장이 하나일 때는 괄호{ }를 생략할 수 있다.
이 때 문장의 끝에 ';'(세미콜론) 을 붙이지 않아야 한다는 것에 주의한다.
그러나 괄호{ } 안의 문장이 return문일 경우 괄호를 생략할 수 없다.
익명객체 람다식
자바에서 모든 메서드는 클래스내에 포함되어야 한다.
람다식이 메서드와 동등한 것처럼 설명했지만, 사실 람다식은 익명 클래스의 객체와 동등하다.
람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을 것인가? 이미 알고 있는 것 처럼 참조변수가 있어야 객체의 메서드를 호출 할 수 있으니 이 익명 객체의 주소를 f라는 참조변수에 저장해 본다.
타입 f = (int a,int b) -> a>b ? a:b;
참조변수 f의 타입은 어떤 것이어야 할까?
참조형이니까 클래스 또는 인터페이스 가 가능하다.
그리고 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.
그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.
아래와 같이 max()라는 메서드가 정의된 MyFunction인터페이스가 정의되어 있다고 가정하자
이 인터페이스를 구현한 익명 클래스의 객체는 다음과 같이 생성할 수 있다.
MyFunction인터페이스에 정의된 메서드 max()는 람다식 ‘(int a, int b) -〉a 〉b ? a : b’과 메서드의 선언부가 일치한다. 그래서 위 코드의 익명 객체를 람다식으로 아래와 같이 대체할 수 있다
MyFunction f = (int a,int b) -> a>b ? a:b;
int big = f.max(5,3);
MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는
MyFunction인터페이스를 구현한 익명 객체의 메서드 max()와
람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다
그러므로 람다식을 다루기 위한 인터페이스를 ‘함수형 인터페이스(functional interface)’라고 한다.
멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.
이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지
다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 바로 ‘임
계 영역(critical section)’과 ‘잠금(락,lock)’이다
공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가
가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한
다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해
야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다
이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 ‘쓰레드의 동기화(synchronization)’라고 한다.
동기화하려면 간섭받지 않을 문장들을 하나의 영역, 즉 임계 영역으로 묶으면 된다.
이 묶는 행위는 synchronized 라는 키워드로 할 수 있다.
임계영역은 락이 걸려 하나의 영역에 하나의 쓰레드만 출입 가능하게 된다.
synchronized 를 이용한 동기화
synchronized 로 임계영역을 설정하는 방법 두가지는
1. 메서드 전체를 임계영역으로 지정 (voide 앞에 synchronized 키워드 넣기)
2. 특정 영역을 임계영역으로 지정하는 방법이 있다. synchronized(참조변수)
첫 번째 방법은 메서드 앞에 synchronized를 붙이는 것인데, synchronized를 붙이면 메서드 전체가 임계 영역으로 설정된다
두 번째 방법은 메서드 내의 코드 일부를 블럭{} 으로 감싸고 블럭 앞에 ‘synchronized (참조변수)’를 붙이는 것인데, 이때 참조변수는 탁을 걸고자하는 객체를 참조하는 것이어 야 한다.
블럭을 synchronized 블럭이라고 부르며, 이 블럭의 영역 안으로 들어가면서 부터 쓰레드는 지정된 객체의 lock을 얻게 되고, 이 블럭을 벗어나면 lock을 반납한다.
임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 락을 거는 것보다 synchronized블럭으로 임계 영역을 최소화해서 보다 효율적인 프로그 램이 되도록 노력해야한다.
즉, 임계영역은 한번에 한 쓰레드만 들어갈 수 있는 만큼 그 영역 최소화 될 수록 좋다.
두 방법 모두 lock의 획득과 반납이 모두 자동적으로 이루어지므로 우리가 해야 할 일은
그저 임계 영역만 설정해주는 것뿐이다.
synchronized 하지않았을때는 한 쓰레드에 다른 쓰레드가 영향을 미쳐 마이너스 잔고가 뜨게 된다.
그 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들 어서 출금을 먼저 했기 때문이다.
즉, synchronized키워드가 없을 때 이런 결과가 출력될 수 있다.
withdraw메서드에 synchronized키워드를 붙이기만 하면 간단히 동기화가 된다.
그러나 synchronized 키워드를 적어 해당 메소드를 임계영역으로 만들어주면
잔고가 마이너스로 출력되는 결과가 해결된다.
예제에서도 synchronized 키워드가 없으면 한쓰레드가 다른 쓰레드에 영향을 미쳐 잔고가 마이너스로 출력된다.
① 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐 (queue)와 같은 구조로 먼저 실행대기 열에 들어온 쓰레드가 먼저 실행된다.
② 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.
③ 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
④ 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데,이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 된다.
⑤ 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), in te rrup t)가 호출되면 일시 정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
⑥ 실행을 모두 마치거나 stop( )이 호출되면 쓰레드는 소멸된다.
쓰레드의 실행제어 메서드 종류
유일하게 static 메서드인 sleep() 과 yield() : 쓰레드 자기 자신에게만 호출이 가능하다.
이는 자기 자신에게만 호출이 가능함을 의미한다.
즉, 자거나 양보하는건 자기 스스로에게만 적용되는 것
쓰레드의 스케줄링을 잘하기 위해서는 쓰레드의 상태와 관련 메서드를 잘 알아야 한다.
sleep(long millis)
일정시간동안 쓰레드를 엄추게 한다.
sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면(InterruptedException발생), 잠에서 깨어나 실행대기 상태가 된다.
그래서 sleepO을 호출할 때는 항상 try-catch문으로 예외를 처리해줘야 한다.
static 메서드로 현재 쓰레드에서만 동작한다.
interrupt()와 interrupted()
쓰레드의 작업을 취소한다.
진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야할 때 사용한다.
interrupt()는 쓰레드에게 작업을 멈추라고 요청한다.
단지 멈추라고 요청 만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다.
interrupt()는 그저 쓰레드의 interrupted상태(인스턴스 변수)를 바꾸는 것일 뿐이다.
suspend( ), resume( ), stop ()
suspend()는 sleep()처럼 쓰레드를 멈추게 한다.
suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 된다.
stop()은 호출되는 즉시 쓰레드가 종 료된다.
이 메서드들은 교착상태 (deadlock)를 일으키기 쉽게 작성되어있으므로 사용이 권장되지 않는다.
그래서 이 메서드들은 모두 ‘deprecated’되었다. (권장이지 강제는 아님)
yield()
다른 쓰레드에게 양보한다.
yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다.
static 메서드로 현재 쓰레드에서만 동작한다.
join()
다른 쓰레드의 작업을 기다린다.
쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도 록할 때 join()을 사용한다
시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다.
join()도 sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출 되는 부분을 try-catch문으로 감싸야 한다.
join( )은 여러모로 sleep( )과 유사한 점이 많다.
sleep()과 다른 점은 join()은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static메서드가 아니라는 것이다.