최근 들어 함수적 프로그래밍이 다시 부각되고 있는데, 병렬처리와 이벤트 지향 프로그래밍에 적합하기 때문이다. 그래서 객체 지향 프로그래밍과 함수적 프로그래밍을 혼합함으로써 더훅 효율적인 프로그래밍이 될 수 있도록 프로그램 개발 언어가 변하고 있다.
함수적 스타일의 람다식을 작성하는 방법은 다음과 같다
(타입 매개변수, ...) -> { 실행문; ... }
: (타입 매개변수, ...)는 중괄호 {} 블록을 실행하기 위해 필요한 값을 제공하는 역할을 한다.
: -> 기호는 매개 변수를 이용해 중괄호 {}를 실행한다는 뜻으로 해석하면 된다
예를 들어, int 매개변수 a의 값을 콘솔에 출력하기 위해 람다식을 작성한다면
(int a) -> { System.out.println(a); }
: 람다식이 하나의 메소드를 정의하기 때문에 두 개 이상의 추상 메소드가 선언된 인터페이스는 람다식을 이용해서 구현 객체를 생성할 수 없다.
원래 인터페이스 사용법은 아래와 같았다.
// 인터페이스를 선언한 뒤
interface A1 {
void aMethod();
}
// (1)
// 일반 클래스에 implements A 로 연결해서 오버라이드 후
class AA implements A{
@Override
public void aMethod() {}; {};
}
// 메인에서 객체화해 사용
AA aa = new AA();
// (2)
// 아니면 메인에서 객체 생성하면서 익명 구현 객체로 생성해 사용
A1 ac = new A1() {
@Override
public void aMethod() {
int a = 5;
a++;
}
};
: 인터페이스 선언시 @FunctionalInterface
어노테이션을 붙이면 함수적 인터페이스를 작성할 때 두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹해준다.
@FunctionalInterface
interface A {
void aMethod();
}
public class A_LambdaEx {
public static void main(String[] args) {
A1 ac = () -> {
int a = 5;
a++;
};
// 람다식으로 익명구현 객체 만들고
B1 bc = () -> {
// 메소드 재 정의한 부분
int b = 5;
b++;
};
// 메소드 호출해서 값 넣어주기
bc.bMethod();
}
}
interface A1 {
void aMethod();
}
interface B1 {
void bMethod();
}
-----------------------------------------------------------
public class B_LambdaEx {
public static void main(String[] args) {
//인터페이스 구현방법
// 1. 클래스에 implements로 연결하고 재정의 후 메인에서 해당 클래스를 객체 생성해 사용
// 2. 익명 구현 객체로 메인에 생성
A2 a2 = new A2() {
@Override
public void a2Method() {
System.out.println("람다식 연습");
}
};
// 3. 람다식으로 생성
A2 aa2 = () -> {System.out.println("람다식 연습");};
A2 aaa2 = () -> System.out.println("람다식 연식");
aa2.a2Method();
aaa2.a2Method();
}
}
// 인터페이스 하나에 추상메소드도 하나
// = 함수적 인터페이스
interface A2 {
void a2Method();
}
아래 코드를 보면 알 수 있지만 람다식의 기본 모양은
인터페이스 매개변수 = () -> {실행문}
의 형태이지만 이걸 더 줄여 사용할 수도 있다. 예를 들자면 이런 식이다.
// 근데 이제 이런식으로 작성해도 된다
// 들어가는 값이 하나이고 실행문도 하나라면 아래 식들은 모두 같은 식이다
D1 d1 = (int a) -> { System.out.println(a); };
D1 d2 = (a) -> { System.out.println(a); };
D1 d3 = a -> System.out.println(a);
public class C_MyFunctionalInterfaceEx2 {
public static void main(String[] args) {
MyFunctionalInterface2 mi;
mi = (int x) -> {
int result = x * 5;
System.out.println(result);
};
mi.method(2);
mi = (x) -> {System.out.println(x * 5);};
mi.method(2);
mi = x -> {System.out.println(x * 5);};
mi.method(2);
mi = x -> System.out.println(x * 5);
mi.method(2);
// 근데 이제 이런식으로 작성해도 된다
// 들어가는 값이 하나이고 실행문도 하나라면 아래 식들은 모두 같은 식이다
D1 d1 = (int a) -> { System.out.println(a); };
D1 d2 = (a) -> { System.out.println(a); };
D1 d3 = a -> System.out.println(a);
}
}
@FunctionalInterface
interface MyFunctionalInterface2 {
void method(int x);
}
interface D1 {
void dMethod(int a);
}
// 인터페이스를 이럴식으로 작성했다고 했을 때
@FunctionalInterface
interface C1 {
int cMethod(int a, int c);
}
// 사용은 아래와 같이 할 수 있다.
// 만약 들어가는값이 여러개이고 실행문이 하나라면
C1 c1 = (int x, int y) -> { return x+y; };
C1 c2 = (x, y) -> { return x+y; };
// 매개값이 두개일때는 괄호 제거시 에러
// C1 c3 = x,y -> { return x+y; };
// 실행문이 리턴이라면 괄호 제거할때 "return" 작성하지 않아도 괜찮다
C1 c4 = (x, y) -> x+y;
람다식의 실행 블록에는 클래스의 멤버(필드와 메소드) 및 로컬 변수를 사용할 수 있다. 클래스의 멤버는 제약 사항 없이 사용 가능하지만, 로컬 변수는 제약 사항이 따른다
람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조이다.
public class D_UsingThisEx {
public static void main(String[] args) {
UsingThis usingthis = new UsingThis();
UsingThis.Inner inner = usingthis.new Inner();
inner.method();
}
}
@FunctionalInterface
interface MyFunctionalInterface4 {
void method();
}
class UsingThis {
int outterfield = 10;
int a = 5;
class Inner {
int ineerField = 20;
int a = 10;
void method() {
// 람다식
MyFunctionalInterface4 fi = () -> {
// 호출 방식의 차이이지 결국 같은 것릉 불러오는 것
System.out.println("outterField : " + outterfield);
System.out.println("outterField : " + UsingThis.this.outterfield + "\n");
// 호출 방식의 차이이지 결국 같은 것을 불러오는 것
System.out.println("innerField : " + ineerField);
System.out.println("innerField : " + this.ineerField + "\n");
// this를 사용한 호출 방식의 차이
System.out.println("UsingThis에서 불러오는 a값 : " + UsingThis.this.a);
System.out.println("가장 가까이 있는 a 값 : " + this.a);
};
fi.method();
}
}
}
실행 모습
람다식에서 바깥 클래스의 필드나 메소드는 제한 없이 사용할 수 있으나, 메소드의 매개 변수 또는 로컬 변수를 사용하면 이 두 변수는 final 특성을 가져야한다.
class UsingLocalVariable {
void method(int arg) { //arg는 final 특성을 가짐
int localVar = 40; //localVar는 final 특성을 가짐
// arg = 31; //final 특성 때문에 수정 불가
// localVar = 41; //final 특성 때문에 수정 불가
//람다식
// = 익명 구현 객체로 작성할 것을 간단히 작성
MyFunctionalInterface5 fi= () -> {
//로컬변수 사용
// 내부에서 원래 값을 호출할 수 있지만
// 변경된 값을 가져오는 것은 xx
// 내부에서 값을 변경하는 것도 xx
// arg = 30;
System.out.println("arg: " + arg);
System.out.println("localVar: " + localVar + "\n");
};
fi.method();
}
}
자바 8부터는 빈번하게 사용되는 함수적 인터페이스(functional interface)는 java.util.function 표준 API 패키지로 제공한다. 이 패키지에서 제공하는 함수적 인터페이스의 목적은 메소드 또는 생성자의 매개 타입으로 사용되어 람다식을 대입할 수 있도록 하기 위해서이다.
함수적 인터페이스는 크게 Consumer, Supplier, Function, Operator, Predicate로 구분된다. 구분기준은 인터페이스에 선언된 추상 메소드의 매개값과 리턴 값의 유무!
특징은 리턴값이 없는 accept() 메소드를 가지고 있다는 것이다. accept() 메소드는 단지 매개값을 소비하는 역할만 한다.
Consumer<T>
인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.
Consumer<타입> consumer = t -> { t를 소비하는 실행문; };
public class A_ConsumerEx {
public static void main(String[] args) {
// 람다식으로 익명구현 객체 클래스를 재정의함
// consumer인터페이스를 String타입으로 사용하겠다
// (String s)에서 String 생략 가능
// 실행문이 하나이기 때문에 중괄호 {}도 생략
Consumer<String> consumer1 = s -> System.out.println(s + " 8");
// 사용하기
consumer1.accept("Java");
Consumer<Integer> consumer2 = t -> System.out.println(t + 10);
consumer2.accept(10);
Consumer<Integer> consumer3 = t -> { t++; System.out.println(t); };
consumer3.accept(10);
Consumer<Double> consumer4 = t -> { t = t-2.2; System.out.println(t); };
consumer4.accept(3.2);
// true를 넣어 판단하기
Consumer<Boolean> consumer5 = b -> {
if (b) {
System.out.println("참 입니다");
} else {
System.out.println("거짓입니다");
}
};
consumer5.accept(true);
consumer5.accept(false);
}
}
public class A_ConsumerEx2 {
public static void main(String[] args) {
BiConsumer<String, String> biConsumer1 = (s1, u1) ->{
System.out.println(s1 + u1);
};
biConsumer1.accept("java", " Lambda");
BiConsumer<Integer, Double> biConsumer2 = (s1, u1) -> {
System.out.println(s1 + "*" + u1 + "=" +s1*u1);
};
biConsumer2.accept(10, 80.9);
System.out.println();
// Double 컨슈머 사용해보기
DoubleConsumer dc = v -> System.out.println(v);
dc.accept(10.9);
System.out.println();
// "홍길동" 85.3을 넣어 소비하고 싶다면
ObjDoubleConsumer<String> objDouble= (s, d) -> {
System.out.println(s + d);
};
objDouble.accept("홍길동", 85.3);
//520L long숫자 넣어 소비하기
LongConsumer lc = l -> System.out.println(l);
lc.accept(520L);
// 정수 100 넣어서 소비하기
IntConsumer ic = i -> System.out.println(i);
ic.accept(100);
}
}
Supplier 함수적 인터페이스의 특징은 매개 변수가 없고 리턴값이 있는 getXXX() 메소드를 가지고 있다는 것이다. 이 메소드들은 실행 후 호출한 곳으로 데이터를 리턴하는 역할을 한다.
리턴 타입에 따라 아래와 같은 함수적 인터페이스가 있다.
람다식은 다음과 같이 작성할 수 있다.
Supplier<T> supplier = () -> {...; return T값; }
get() 메소드가 매개값을 가지지 않으므로 람다식도 ()를 사용한다. 람다식의 중괄호 {} 는 반드시 한 개의 T객체를 리턴하도록 해야한다.
ublic class A_SupplierEx {
public static void main(String[] args) {
// 입력은 없고, 정수값만 출력하기
// 람다식으로 메소드를 재정희한 것
// int getAsInt();
IntSupplier iS = () -> {
int num = (int) (Math.random() * 6) + 1;
return num;
};
int number = iS.getAsInt();
System.out.println(number);
// 참/거짓을 출력하기
// boolean getAsBoolean
BooleanSupplier iS2 = () -> {
int num = (int) (Math.random() * 2);
if(num == 1) {
return true;
} else {
return false;
}
};
boolean result = iS2.getAsBoolean();
System.out.println(result);
Student st = new Student();
st.setName("홍길동");
st.setScore(90);
Supplier<Student> iS3 = () -> {
return st;
};
System.out.println(iS3.get().getName() + " " + iS3.get().getScore());
// 컨슈머도 사용해보기
Consumer<Student> cS = (t) -> {
st.set("김자바", 100);
System.out.println(st.getName() + " " + st.getScore());
};
cS.accept(st);
}
}
class Student {
String name;
int score;
public Student() { }
void set(String name, int score) {
this.name = name;
this.score = score;
}
void setName(String name) {
this.name = name;
}
void setScore(int score) {
this.score = score;
}
int getScore() {
return score;
}
String getName() {
return name;
}
}
public class A_SupplierConsumerTest {
public static void main(String[] args) {
Car car = new Car();
// 입력 Consumer
Consumer<Car> co = (t) -> {
t.set("소나타", 1000, true);
};
co.accept(car);
// 출력 Supplier
Supplier<Car> sp = () -> {
return car;
};
System.out.println(sp.get().carName + " " + sp.get().price + " " + sp.get().newOld);
}
}
class Car {
String carName;
int price;
boolean newOld;
void set(String carName, int price, boolean newOld) {
this.carName = carName;
this.price = price;
this.newOld = newOld;
}
String getCarName() {
return carName;
}
int getPrice() {
return price;
}
boolean getNewOld() {
return newOld;
}
}
매개값과 리턴값이 있는 applyXXX() 메소드를 가지고 있는 것이 특징이다. 이 메소드들은 매개값을 리턴값으로 매핑(타입 변환)하는 역할을 한다.
Function 인터페이스를 타겟 타입으로 하는 람다식은 다음과 같이 작성할 수 있다.
// '객체' 가 들어가면 '해당 타입'으로 나온다
// 객체에 있는 Name을 가져오겠다
Function< 객체, 타입 > function = t -> { return t.getName(); }
또는
// 실행할 내용이 리턴문 하나라면
// 중괄호 제거와 return 제거가 가능하다
Functin < 객체, 타입> function = t -> t.getName;
public class A_FuncionEx1 {
// 배열로 만들어서 리스트 만들기
private static List<Student> list = Arrays.asList(
new Student("홍길동", 90, 96, true),
new Student("신용권", 95, 93, false)
);
// Student가 들어가면 String이 나온다라는 function
public static void printString( Function<Student, String> function ) {
// list에 저장된 항목 수 만큼 반복
for(Student student : list) {
// 람다식 실행
System.out.println(function.apply(student) + " ");
}
System.out.println();
}
// ToIntFunction<Student> function
// printInt( t -> t.getMathScore());
// t가 function에 들어가 int로 매핑된다
public static void printInt( ToIntFunction<Student> function ) {
for(Student student : list) {
// ToIntFunction의 추상 메소드는 applyAsInt(T value)
System.out.println(function.applyAsInt(student) + " ");
}
System.out.println();
}
// 성별 넣기
public static void printBoolean( Function<Student, Boolean> function) {
for(Student student : list) {
// 값을 가져와서 gender에 넣는데 만약 true라면 여성, false라면 남성이 출력되도록 지정
boolean gender = function.apply(student);
if (gender == true) {
System.out.println("여성");
} else {
System.out.println("남성");
}
}
System.out.println();
}
public static void main(String[] args) {
System.out.println("[학생 이름]");
printString(t -> t.getName());
System.out.println("[영어 점수]");
printInt( t -> t.getEngScore());
System.out.println("[수학 점수]");
printInt( t -> t.getMathScore());
System.out.println("[학생 성별]");
printBoolean(t -> t.isGender());
}
}
class Student {
private String name;
private int englishScore;
private int mathScore;
private boolean gender;
public Student(String name, int englishScore, int mathScore, boolean gender) {
this.name = name;
this.englishScore = englishScore;
this.mathScore = mathScore;
this.gender = gender;
}
public String getName() { return name; }
public int getEngScore() { return englishScore; }
public int getMathScore() { return mathScore; }
public boolean isGender() { return gender; }
}
요구사항 : 값을 두 개 넣고 1개로 출력하고 싶다
이 경우 방법은 두 가지가 될 수 있다.
1. Function<객체, 타입>
2. BiFunction<객체, 타입>
비슷해보이지만 사용하는 방법이 다르니 편한 걸로 사용하자
public class A_FunctionEx2 {
private static List<Student_> list = Arrays.asList(
// 1학년
new Student1("홍길동", 90, 96, true),
new Student1("신용권", 95, 93, false),
// 2학년
new Student2("이순신", 100, 95, true),
new Student2("김자바", 98, 90, false)
);
private static List<Student_> list1 = Arrays.asList(
// 1학년
new Student1("홍길동", 90, 96, true),
new Student1("신용권", 95, 93, false)
);
private static List<Student_> list2 = Arrays.asList(
// 2학년
new Student2("이순신", 100, 95, true),
new Student2("김자바", 98, 90, false)
);
// 값 두개 넣고 1개 출력하는 함수적 인터페이스를 찾기
// 방법 1
public static void printString1( Function<Student_, String> function) {
for(Student_ st : list) {
System.out.println(function.apply(st) + " ");
}
System.out.println();
}
// 방법 2
public static void printString2( BiFunction<Student1, Student2, String> function1 ) {
// 객체를 넣었기 때문에 size나 길이 혹은 향상된 for문으로 돌릴 시
// 에러남. 길이를 안다면 그 길이대로 돌리는 것이 안전하다
for(int i=0; i<2; i++) {
Student1 st1 = (Student1) list1.get(i);
Student2 st2 = (Student2) list2.get(i);
System.out.println(function1.apply(st1, st2));
}
System.out.println();
}
public static void main(String[] args) {
System.out.println("[학생 이름]");
printString1(t -> t.getName());
System.out.println();
System.out.println("[학생 이름]");
printString2((t, u)-> t.getName() + "\n" + u.getName());
}
}
// 상위 클래스
class Student_{
String name;
public Student_() {
}
public String getName() {
return name;
}
}
// 1학년
class Student1 extends Student_{
String name;
private int englishScore;
private int mathScore;
private boolean gender;
public Student1(String name, int englishScore, int mathScore, boolean gender) {
this.name = name;
this.englishScore = englishScore;
this.mathScore = mathScore;
this.gender = gender;
}
public String getName() { return name; }
public int getEngScore() { return englishScore; }
public int getMathScore() { return mathScore; }
public boolean isGender() { return gender; }
}
// 2학년
class Student2 extends Student_ {
String name;
private int englishScore;
private int mathScore;
private boolean gender;
public Student2(String name, int englishScore, int mathScore, boolean gender) {
this.name = name;
this.englishScore = englishScore;
this.mathScore = mathScore;
this.gender = gender;
}
public String getName() { return name; }
public int getEngScore() { return englishScore; }
public int getMathScore() { return mathScore; }
public boolean isGender() { return gender; }
}