JDK1.5 부터 지네릭스의 등장
JDK1.7 부터 람다식의 등장 : 객체지향언어 + 함수형 언어
메서드 ➡️ < 하나의 식 : 반환값, 메서드의 이름이 없어짐 = 익명함수 >
int[] arr = new int[5];
Arrays.setAll(arr,(i)->(int)(Math.random()*5)+1);
위에서 '(i)->(int)(Math.random()*5)+1'이 바로 람다식이다.
람다식 말고 메서드로 표현하면 아래와 같다.
int mathod() {
return (int)(Math.random()*5)+1);
}
람다식이 더 간결하면서도 이해하기 쉽다.
람다식은 메서드의 매개변수, 메서드의 반환값으로 사용될 수 있다
= 변수처럼 다루는 게 가능하다.
예를 들면 아래와 같다.
이제 몇 가지 규칙들을 알아보자
✔️ 반환값이 있는 경우 return값 대신에 식으로 대신할 수 있다.
이때는 문장이 아닌 식이므로 끝에 ;을 붙이지 않는다.
✔️ 람다식에 매개변수의 타입은 추론이 가능한 경우 생략, 대부분의 경우 생략
(int a,b)->a>b?a:b와 같이 하나만 생략하는 것은 불가능하다. 무조건 둘다 생략이거나 생략이 아니거나
✔️ 매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있다.
단 타입이 있으면 괄호를 생략할 수 없다.
✔️ {}괄호 한에 문장이 하나일 경우 {}생략 가능하며 문장의 끝에 ; 붙이지 않기
✔️ 그러나 return문의 경우 {}괄호 생략이 불가능하다.
몇 개의 예시
int roll() {
return (int) (Math.random()*5);
}
➡️ 람다식으로 바꿔보기
()->{return (int) (Math.random()*5);}
()->(int) (Math.random()*5)
int sumArr(int[] arr) {
int sum =0;
for(int i:arr)
sum+=i;
return sum;
}
➡️ 람다식으로 바꿔보기
(int[] arr) -> {
int sum =0;
for(int i:arr)
sum+=i;
return sum;
}
람다식은 익명 클래스와 동등하다.
중요한 것은 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 것인가?
참조변수를 통해서 호출한다.
그렇다면 참조변수의 타입은 무엇으로 해야하나?
참조변수의 타입은 클래스나 인터페이스여야 하는데 람다식은 어떻게 참조변수의 타입을 결정할 것인가?
타입 f=(int a, int b)->a>b? a:b
타입을 무엇으로 해야할까?
먼저 익명클래스의 메서드 구현을 먼저 살펴보자
interface MyFunction {
public abstract int max(int a, int b) ;
}
My Function f = new MyFunction() {
public int max(int a, intb){
return a>b?a:b;
}
}
int big = f.max(5,3) ;
자 위 인터페이스를 보면 정의된 max(int a, int b) 메서드는 람다식(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인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수, 반환값이 일치한다.
이 MyFunction인터페이스처럼 인터페이스를 통해 람다식을 다루는 인터페이스를 우리는 '함수형 인터페이스(functional interface)'라고 부르자.
@FunctionalInterface
interface MyFunction {
public abstract int max(int a, int b) ;
}
이 함수형 인테퍼이스는
✔️ 오직 하나의 추상 메서드만 정의되어 있어야 한다. -> 람다식과 인터페이스 1:1 연결
✔️ static메서드와 default메서드의 개수에는 제약이 없다.
① 함수형 인터페이스 타입의 매개변수
@FunctionalInterface
interface MyFunction {
void myMethod() ;
}
void aMethod(MyFunction f) {
f.myMethod();
}
...
MyFunction f = ()->System.out.println("야호");
aMethod(f);
또는
aMethod(()->System.out.println("야호"));
② 메서드의 반환타입이 함수형 인터페이스 타입
MyFunction muMethod() {
Myfunction f=()->{}; // 추상메서드와 1:1의 람다식
return f; //의 참조변수
}
MyFunction muMethod() {
return ()->{};; //람다식 자체를 반환
}
예제를 통해 자세히 알아보자
@FunctionalInterface
interface MyFunction {
void run();
}
public class LamdaEx1 {
static void execute(MyFunction f) {
f.run();
}
static MyFunction getMyFunction() {
MyFunction f = ()->System.out.println("f3.run()");
return f;
}
public static void main(String[] args) {
MyFunction f1= ()->System.out.println("f1.run");
MyFunction f2 = new MyFunction() { //익명클래스로 객체 생성
public void run() {
System.out.println("f2.run");
}
};
MyFunction f3=getMyFunction();//반환값
f1.run();
f2.run();
f3.run();
execute(f1);
execute(()->System.out.println("run()"));
}
}
함수형 인터페이스를 통해서 우리는 람다식을 참조할 수 있었다.
이 람다식을 함수형 인터페이스의 메서드를 호출함으로써 구현할 수 있는 것이다.
그러나 람다식의 타입과 함수형 인터페이스의 타입이 일치하는 것은 아니다.
따라서 우리는 람다식의 타입을 지정해줘야 한다.
MyFunction f =(MyFunction)(()->{});
이렇게 표현해야 한다.
하지만 이 형변환은 생략이 가능하기 때문에
우리는 이전 예시들에서 생략해서 사용하고 있었던 것이다.
람다식은 오직 함수형 인터페이스로만 형변환이 가능하다
Object obj =(Object)(()->{});
이는 불가능하다.
따라서 굳이 Object타입으로 형변환하려면
Object obj =(Object)(MyFunction)(()->{});
String str = (Object)(MyFunction)(()->{}).toString();
@FunctionalInterface
interface MyFunction_2 {
void myMethod();
}
class LamdaEx2 {
public static void main(String[] args) {
MyFunction_2 f = ()->{};
Object obj = (MyFunction_2)(()->{});
String str = ((Object)(MyFunction_2)(()->{})).toString();
System.out.println(f);
System.out.println(obj);
System.out.println(str);
System.out.println((MyFunction_2)(()->{}));
System.out.println(((Object)(MyFunction_2)(()->{})).toString());
}
}
👆 컴파일러는 람다식을 '외부클래스이름 $$ Lanmbda $ 번호'와 같은 형식으로 만들어낸다.
이 예시에서 외부 변수는
내부 클래스의 지역변수라고 볼 수 있다.
@FunctionalInterface
interface MyFunction2{
void myMethod();
}
class Outer2 {
int iv = 50; // Outer.this.iv
class InstanceInner{
int iv =100; //this.iv
void method(int i) {//void method(final int i)
int iv =200;//final int iv=200;
// i=10;//에러, 상수의 값을 변경할 수 없다.
MyFunction2 f = ()->{
System.out.println(" i : "+iv);
System.out.println(" iv : "+iv);
System.out.println(" this.iv : "+this.iv);
System.out.println("Outer.this.iv : "+Outer2.this.iv);
};
f.myMethod();
}
}
}
class LambdaEx3 {
public static void main(String[] args) {
Outer2 outer = new Outer2();
Outer2.InstanceInner inner = outer.new InstanceInner();
inner.method(100);
}
}
👆 외부클래스의 인스턴스 변수, 내부 클래스의 인스턴스 변수,내부 클래스의 지역변수를 각각 어떻게 표현하는지는 chapter7 내부클래스에서 본 부분이다.
람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 상수로 간주한다.
람다식 내에서 지역변수는 i와 iv을 참조하고 있으므로
람다식 내에서나 다른 어느 곳에서도 이 변수들의 값을 변경하는 일을 허용되지 않는다.
사실 우리는 내부클래스에서 외부클래스의 지역변수를 참조할 때 지역변수 중에서 상수만 참조만 가능하다는 것을 떠올릴 수 있어야한다.(복습 중요!!)
👆위 예시는 내가 수정해보았다.
람다식에서 지역변수 i를 참조하지 않았더니 상수로 인식되지 않아서 i의 값을 변경해도 에러가 발생하지 않는다.
사실 우리는 매번 람다식을 사용할 때 함수형 인터페이스를 만들 수는 없다.
왜냐하면 함수형 인터페이스는 사실 람다식의 반환,매개변수 값의 형태만 맞춰주는 메서드만 넣으면 되는 아주 간단한 형태이기 때문이다.
즉, 선언부만 함수형 인터페이스랑 람다식이 1:1 연결되도록만 해주면 된다.
따라서 java.util.function패키지는 다양한 형태(보편적으로 사용되는)의 함수형 인터페이스를 미리 정의해놓았다.
함수형 인터페이스 | 메서드 | 실행 |
---|---|---|
java.lang.Runnable | void run() | 매개변수❌반환값❌ |
Supplier<T> | T get()-->T | 매개변수❌반환값⭕, 제공자 |
Consumer<T> | T--->T void accept(T t) | 매개변수⭕반환값❌, 소비자 |
Function<T,R> | T---> R apply(T t)-->R | 한 개의 매개변수⭕반환값⭕, f(x)=y |
Predicat<T> | T-->boolean test(T t)--->boolean | 조건식 표현, 매개변수 하나⭕반환값 true or false⭕ |
🔍java.util.function 패키지의 주요 함수형 인터페이스
Predicate<String> isEmptyStr = s -> s.length ==0 ;
String s = "";
if(isEmptyStr.test(s))
System.out.println("This is an empty String.");
함수형 인터페이스 | 메서드 | 실행 |
---|---|---|
BiConsumer<T,U> | T,U--->T void accept(T t,U u) | 두 개의 매개변수⭕반환값❌, 소비자 |
Function<T,U,R> | T,U---> R apply(T t,U u)-->R | 두 개의 매개변수⭕반환값⭕, f(x)=y |
Predicat<T,U> | T,U-->boolean test(T t,U u)--->boolean | 조건식 표현, 매개변수 e두개⭕반환값 true or false⭕ |
두 개 이상의 매개변수를 갖는 함수형 인터페이스는 직접 만들어야 한다/
Function의 변형
매개변수의 타입과 반환타입이 모두 일치
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
UnaryOperator<T> | T--->T apply(T t)-->T | Function의 자손, 매개변수 타입 = 반환타입 |
BinaryOperator<T> | T,T--->T apprly(T t,T t)--->T | BiFunction의 자손, 매개변수 타입 = 반환 타입 |
다수의 디폴트 메서드 추가, 그 중의 일부는 함수형 인터페이스를 사용한다.
매개변수로 함수형 인터페이스의 객체가 들어간다.
인터페이스 | 메서드 | 설명 |
---|---|---|
Collection | boolean removeIf(Predicate<E> filter) | 조건에 맞는 요소를 삭제 |
List | void replaceAll(UnaryOperator<E> operator) | 모든 요소를 반환하여 대체 |
Iterator | void forEach(Consumer<T> action) | 모든 요소에 작업 action을 수행 |
Map | V compute(K key,BiFunction<K,V,V> f) | 지정된 키의 값에 작업 f를 수행 |
V computeIfAbsent(K key,Fucntion<K,V> f) | 키가 없으면, 작업 f를 수행 후 추가 | |
V computeIfPresent(K key, BiFunction<K,V,V> f) | 지정된 키가 있을 때 f를 수행 | |
V merge(K key, V value, BiFunction<V,V,V> f) | 모든 요소에 병합작업 f를 수행 | |
void forEach(BiConsumer<K,V> action) | 모든 요소에 작업 action을 수행 | |
void replaceAll(BiFunction<K,V,V> f) | 모든 요소에 치환작업 f를 수행 |
예시를 통해서 알아보자.
import java.util.*;
class LambdaEx4 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for(int i=0;i<10;i++)
list.add(i);
//list의 모든 요소를 출력
//forEach(Consumer<T> action) : 각 요소에 직접 action을 수행
//모든 요소를 출력
list.forEach(i->System.out.print(i+","));
System.out.println();
//list에서 2또는 3의 배수를 제거한다.
// boolean removeIf(Predicate<E> filter)
list.removeIf(x->x%2==0||x%3==0);
System.out.println(list);
//void replaceAll(UnaryOperator<E> operator)
list.replaceAll(i->i*10);
System.out.println(list);
Map<String,String> map = new HashMap<>();
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
//map의 모든 요소를 {k,v}의 형식으로 출력한다.
//Map-void forEach(BiConsumer<K,V> action)
map.forEach((k,v)->System.out.print("{"+k+","+v+"}"));
System.out.println();
}
}
import java.util.function.*;
import java.util.*;
class LambdaEx5 {
public static void main(String[] args) {
//함수형 인터페이스
Supplier<Integer> s =()->(int)(Math.random()*100)+1; //1부터 100까지
Consumer<Integer> c= i->System.out.print(i+", ");
Predicate<Integer> p= i->i%2==0;
Function<Integer,Integer> f =i->i/10*10;
List<Integer> list = new ArrayList<>();
makeRandomList(s,list);//1부터 100까지의 값을 10개 갖는 list생성
System.out.println(list);
printEvenNum(p,c,list);//2의 배수만 출력
List<Integer> newList= dosomething(f,list); // 10으로 나눈 그 몫에 곱하기 10한 새로운 리스트
System.out.println(newList);
}
static <T> List<T> dosomething(Function<T,T> f, List<T> list){
List<T> newList = new ArrayList<T>(list.size());
for(T i:list) {
newList.add(f.apply(i));//10으로 나눈 그 몫에 곱하기 10
}
return newList;
}
static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
System.out.print("[");
for(T i: list) {
if(p.test(i))//list의 요소가 2의 배수라면
c.accept(i);//출력
}
System.out.println("]");
}
static<T> void makeRandomList(Supplier<T> s,List<T> list) {
for(int i=0;i<10;i++) {
list.add(s.get());//list에 10개의 랜던값을 넣는다. 그 값은 1부터 100까지
}
}
}
앞서 배운 함수형 인터페이스는 매개변수와 반환타입이 모두 지네릭 타입
기본형도 래퍼 클래스 이용하여 비효율적
따라서 기본형을 바로 이용하는 함수형 인터페이스를 알아보자.
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
DoubleToIntFunction | double->int applyAsInt(double d)--->int | AToBFunction은 입력이 A타입, 출력이 B타입 |
ToIntFunction<T> | T---> int applyAsInt(T value)--->int | ToBFunction은 출력이 B타입. 입력은 지네릭 |
IntFunction<R> | int, int---> R apply(T t,U u)--->R | AFunction은 입력이 A타입, 출력은 지네릭 |
ObjIntConsumer<T> | T,int---> void Consumer(T t, U u) | ObjAFunction은 입력이 T,A타입 ,출력은 없음 |
import java.util.function.*;
import java.util.*;
class LambdaEx6 {
public static void main(String[] args) {
IntSupplier s=()->(int)(Math.random()*100)+1;//1~100
IntConsumer c = i->System.out.print(i+", ");
IntPredicate p = i->i%2==0;
IntUnaryOperator op = i->i/10*10;
int[] arr = new int[10];
makeRandomList(s,arr);
System.out.println(Arrays.toString(arr));
printEvenNum(p,c,arr);
int[] newArr = dosomething(op,arr);
System.out.println(Arrays.toString(arr));
}
static int[] dosomething(IntUnaryOperator op, int[] arr){
int[] newArr = new int[arr.length];
for(int i=0;i<arr.length;i++) {
newArr[i]=op.applyAsInt(arr[i]);//십의 자리만 남기기
}
return newArr;
}
static void printEvenNum(IntPredicate p, IntConsumer c, int[] arr) {
System.out.print("[");
for(int i:arr) {
if(p.test(i))//list의 요소가 2의 배수라면
c.accept(i);//출력
}
System.out.println("]");
}
static void makeRandomList(IntSupplier s,int[] arr) {
for(int i=0;i<arr.length;i++) {
arr[i]=s.getAsInt();//list에 10개의 랜던값을 넣는다. 그 값은 1부터 100까지
}
}
}
앞서 배웠지만 java.util.function에는 함수형 인터페이스가 소개되어져 있다.
하지만 그 인터페이스에는 추상메서드 외에도 디폴트 메서드와 static메서드가 정의되어져 있다.
우리는 그 중에서 Function과 Predicate를 알아보자
Function
defalut<V> Function<T,V> andThen(Fucntion<? super R,? super V> after)
🔍 T--->Function<T,R>--->R--->Function<R,V>--->V
defalut<V> Function<V,R> compose(Fucntion<? super V,? super T> before)
🔍 V--->Function<V,T>--->T--->Function<T,R> ---->R
static Function<T,T> identify()
Predicate
default Predicate and (Predicat<? super T>other)
default Predicate or (Predicat<? super T>other)
default Predicate negate()
static Predicat isEqual(Object targerRef)
f.andThen(g) : f먼저 적용하고 g
f.compose(g) : g먼저 적용하고 f
Function<String,Integer> f = (s)->Integer.parseInt(s,16);
//16진수인 수 -> 10진수
Function<Integer,String> g = (i)->Integer.toBinaryString(i);
//2진수의 String
Function<String,String> h= f.andThen(g);
//f->g 문자열로 된 16진수-> 10진수의 정수->2진수의 문자열
System.out.println(h.apply("FF"));
이 함수의 합성을 보자
f: String을 넣고 결과가 Integer
g: Integer을 넣고 결과가 String
따라서 f.andThen(g) 경우 String을 넣어서 결과가 String으로 반환된다.
이 예제는 "FF"를 넣었다.
f: "FF"를 16진수의 숫자로 인식->10진수의 숫자 255로 반환
g : 255를 -> 2진수의 문자열로 반환 "11111111"
Function<String,Integer> f = (s)->Integer.parseInt(s,16);
//16진수인 수 -> 10진수
Function<Integer,String> g = (i)->Integer.toBinaryString(i);
//2진수의 String
Function<Integer,Integer> h2 = f.compose(g);
//g->f 어떤 숫자->2진수의 문자열, 2진수의 문자열을 16진수로 인식-> 10진수의 숫자
System.out.println(h2.apply(2));
g: 숫자 2를->2진수의 문자열 "10"
f: 문자열"10"를 16진수로 인식-> 10진수 16으로 반환
identify()함수는 그냥 항등함수이다.
잘 사용되지 않지만 map()에서 변환작업할 때 사용됨
Function<String,String> f =x->x;
//Function<String,String> f = Predicate.identify();
여러개의 조건문을 합치는 것!
논리연산자 &&의 역할-> and()
논리연산자 ||의 역할-> or()
논리연산자 !의 역할->negate()
위 결합들은 예제를 통해서 확실히 배워보자
import java.util.function.*;
class LambdaEx7 {
public static void main(String[] args) {
Function<String,Integer> f = (s)->Integer.parseInt(s,16);//16진수인 수 -> 10진수
Function<Integer,String> g = (i)->Integer.toBinaryString(i);//2진수의 String
Function<String,String> h= f.andThen(g); //f->g 문자열로 된 16진수-> 10진수의 정수->2진수의 문자열
Function<Integer,Integer> h2 = f.compose(g);//g->f 어떤 숫자->2진수의 문자열, 2진수의 문자열을 16진수로 인식-> 10진수의 숫자
System.out.println(h.apply("FF"));
System.out.println(h2.apply(2));
Function<String,String> f2 = x->x; //Function.identify();
System.out.println(f2.apply("AAA"));//항등함수
Predicate<Integer> p = i-> i<100;
Predicate<Integer> q = i -> i <200;
Predicate<Integer> r = i -> i%2==0;
Predicate<Integer> notP = p.negate();//i>=100
Predicate all = notP.and(q.or(r));
System.out.println(all.test(150));
String str1 ="abc";
String str2="abc";
Predicate<String> p2 = Predicate.isEqual(str1);//static 메서드
boolean result =p2.test(str2);
System.out.println(result);
}
}
우리는 메서드를 람다식으로 바꿈으로써 코드를 간결하게 만들었다.
예를 들어
Integer wrapper(String s){
return Integer.parseInt(s);
}
라는 메서드를
Function<String,Integer> f = (s)->Integer.parseInt(s);
위와 같은 람다식으로 간단하게 만들었다.
하지만 더 나아가 메서드 참조(method reference)를 통해서 코드를 더 간결하게 만들어 보도록 하자. 이 메서드 참조는 항상 가능한 것은 아니고, 람다식이 하나의 메서드만 호출하는 경우에만 가능하다.
일단 우리는 앞서 배웠지만 메서드가 다른 클래스에서 메서드를 참조할 때는
우리는 static인지 인스턴스인지에 따라 호출하는 방법이 다르다.
static : 클래스이름.메서드();
인스턴스 : 클래스의 객체 . 메서드() ;
마찬가지로 메서드 참조도 비슷하다.
종류 | 람다식 | 메서드 참조 |
---|---|---|
static메서드 참조 | (x)->ClassName.method(x) | ClassName::method |
인스턴스메서드 참조 | (obj,x)->obj.method(x) | ClassName::method |
👆 | 여기서 매개변수로 아에 객체를 받아버림 | 따라서 어떤 클래스의 객체인지 알기 위해 |
특정 객체 인스턴스메서드 참조 | (x)->obj.method(x) | obj::method |
👆 | 이전 코드에서 객체를 미리 생성하고 그 객체를 이용 | 따라서 그 객체를 언급 |
예시를 통해서 이해해보자
✔️ 먼저 static메서드를 참조하는 경우
Function<String,Integer> f =(String s)->Integer.parseInt(s);
⬇️
Function<String,Integer> f =Integer::parseInt;
이렇게 생략이 가능한 이유
① 컴파일러가 메서드의 선언부(parseInt)
② 함수형 인터페이스의 지네릭 타입(<String,Integer>)
을 통해서 컴파일러가 생략된 부분을 쉽게 알아낼 수 있음.
✔️ 인스턴스 메서드 참조하는 경우 : 객체 자체가 매개변수
BiFunction <String,String,Boolean> f = (s1,s2)->s1.equals(s2);
⬇️
BiFunction <String,String,Boolean> f = String :: equals;
String의 경우
① String str1="abc";
② String str2 = new String("abc");
이기 때문에 둘 다 매개변수로 들어가는 것 가능
✔️ 특정 객체 인스턴스메서드 참조
MyClass obj = new MyClass();
Function<String,Boolean> f = (x) -> obj.equals(x);
Function<Stirng,Boolean> f = obj :: equals;
람다식으로 생성자를 호출하는 경우
✔️ 매개변수가 없는 경우
Supplier s = ()->new MyClass();
⬇️
Supplier s = MyClass :: new;
✔️ 매개변수가 있는 경우
Function<Integer,MyClass> s = (i)-> new MyClass();
⬇️
Function<Integer,MyClass> s = MyClass :: new ;
✔️ 배열의 생성의 경우
Function<Integer,int[]> s = (i)->new int[i];
⬇️
Function<Integer,int[]> s = int[]::new;