Coding/Java

Java Lambda Expression(람다식)

heyoon2j 2020. 9. 18. 01:17

■ Java Lambda Expression(람다식)

 - Java 1.8에서 추가된 함수형 프로그래밍(Functional Programming)을 구현하는 방식

 - 클래스를 생성하지 않고 함수의 호출만으로 기능을 수행

 - 병렬 처리가 가능함

 - Lambda를 이용하면, 함수를 변수처럼 사용할 수 있다 (Use funcation like variable)

 - Java에서 '메소드'를 '함수'처럼 사용하는 방식

   * Java에는 '함수'라는 게 없음.

   * 함수형 프로그래밍을 하려면 '1급 함수'라는 개념이 필요


• Functional Programming

 - 순수 함수(Pure Function)를 구현하고 호출

  * 순수 함수 : 외부의 상태를 변경하지 않으면서, 동일한 인자에 대해 항상 똑같은 값을 리턴하는 함수

 - 매개 변수만을 사용하도록 만든  함수로 외부 자료에 부수적인 영향(Side Effect)이 발생하지 않도록 한다.

 - 입력받은 자료를 기반으로 수행되고 외부에 영향을 미치지 않기 때문에, 병렬 처리 등이 가능하고 안정적으로 확장이 가능한 프로그래밍 방식이다.


• Lambda 사용 가능한 경우

 - Lambda 식은 하나의 Method를 정의하기 때문에, 하나의 Abstract Method가 선언된 Interface만이 Lambda 식의 타겟 타입이 될 수 있다.

 - Lambda를 위한 Interface를 작성하는 경우, Annotation으로 @FunctionalInterface를 써주면 Method를 하나만 작성하도록 체크해 준다.

 - Lambda 식의 내부적인 동작은 Anonymous Object 동작이다.


• Lambda 문법

// 기본 형태
// (타입 매개 변수, ...) -> { 실행문 }
// (x, y) -> {return x + y}


// 1. 매개 변수가 하나인 경우, 괄호 생략 가능
// str -> { System.out.println(str); }


// 2. 매개 변수가 없는 경우, 괄호 생략 불가능
// () -> { System.out.println("NULL"); }


// 3. 중괄호 안의 구현부가 한문장인 경우 중괄호 생략
// str -> System.out.println(str)


// 4. return 문 하나인 경우, 중괄호와 세미콜론 생략 가능
// str -> return str.length();	// Error
// str -> str.length() // Possible

• Lambda 예제

class ComparatorTest implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        return o1.substring(1).compareTo(o2.substring(1));
    }
}

class Test{

    public static void main(String[] args) {
        String[] test = {"Java", "Study", "Yoon", "Church"};
        Arrays.sort(test);
        System.out.println(Arrays.toString(test));

        // 1. Comparator 구현
        Arrays.sort(test, new ComparatorTest());
        System.out.println(Arrays.toString(test));

        // 2. 익명 내부 클래스
        Arrays.sort(test, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.substring(2).compareTo(o2.substring(2));
            }
        });
        System.out.println(Arrays.toString(test));

        // 3. Lambda 구현
        Arrays.sort(test,(o1, o2) -> o1.substring(3).compareTo(o2.substring(3)));
        System.out.println(test);
	}
}

 

* Lambda의 경우, 실제 객체가 만들어지는 것이 아니기 때문에 this를 이용해 익명 구현 객체처럼 자기 자신 객체를 호출할 수 없다.

functionalMethod(()-> System.out.println(this));	// Error

• 표준 함수형 인터페이스

 - 자주 사용되는 함수형 인터페이스를 정의해 둔 API

계 열 입 력 출 력 메소드 설명
Consumer O X void accept(T) 입력을 소비
Supplier X O T get() 출력을 공급
Function O O T apply(R) 입력 -> 출력 함수 매핑
Operation O O T apply(T) 입력을 연산하여 동일 타입의 출력으로 리턴
Predicate O boolean boolean test(T) 입력을 판단

 

1. Consumer 계열

- Parameter 입력을 받아서 그것을 소비하는 Functional Interface

- accept(T) 메소드 사용 : 리턴 타입이 void인 특징

 

1) Consumer<T>, BiConsumer<T, U>

- 제네릭 타입을 입력 타입으로 받는다.

- Bi는 입력 값 2개를 받는다.

class Test {
	public static void main(String[] args){
		Consumer<String> consumer = (s) -> System.out.println(s);
        consumer.accept("Hello");

        // 입력 값이 두 개
        BiConsumer<String, String> biConsumer = (t, u) -> System.out.println(t+u);
        biConsumer.accept("StringA", "StringB");
	}
}

 

2) PConsumer

- P : Primitive Type

- IntConsumer, LongConsumer, DoubleConsumer

- 입력으로 P Type을 받음

class Test {
	public static void main(String[] args){
		// IntConsumer, LongConsumer, DoubleConsumer
        IntConsumer intConsumer = (x) -> System.out.println(x);
        intConsumer.accept(10);
	}
}

 

3) ObjPConsumer<T>

- P : Primitive Type

- ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer

- 입력으로 Object와 P Type 2개를 받음

class Test {
	public static void main(String[] args){
		ObjIntConsumer<String> objIntConsumer = (t, x) -> System.out.println(t + " : "+x);
        objIntConsumer.accept("x",1023);
        // ObjLongConsumer, ObjDoubleConsumer
	}
}

2. Supplier 계열

- 아무런 입력을 받지 않고, 값을 하나 반환하는 Functional Interface

- 자료를 공급하는 역할을 한다.

- T get() 메소드 사용 : return Type으로 T 반환

 

1) Supplier<T>

class Test{
	public static void main(String[] args){
    	Supplier<String> supplier = () -> "A String";
        System.out.println(supplier.get());    
    }
}

 

2) PSupplier

- P : Primitive Type

- BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier

- Return Type으로 P 반환

- P getAsP() 메소드 사용

class Test{
	public static void main(String[] args){
    	// Supplier는 P Type 계열에서 getAsP 메소드로 정의
        BooleanSupplier boolSup = () -> true;
        System.out.println(boolSup.getAsBoolean());
        // IntSuplier, LongSupplier, DoubleSupplier

        IntSupplier rollDice = () -> (int)(Math.random()*6);

        int x = 4;
        IntSupplier intSupp = () -> x;
        // Lambda로 동작한다. Local, Parameter 의 경우, 내부로 final로 저정됨
	}
}

3. Function 계열

- Mapping : 입력 -> 출력의 Mapping을 하는 Functional Interface

- 입력 타입과 출력 타입은 다를 수 있다.

- apply() 메서드 사용

 

1) Function<T, R>, BiFunction<T, U, R>

class Test{
	public static void main(String[] args){
    	Function<String, Integer> func = s -> s.length();
        func.apply("Four");

        // Bi가 붙으면 '입력'을 2가지를 받을 수 있다는 의미
        BiFunction<String, String, Integer> funcTwo = (s, u) -> s.length();
    }
}

 

2) PFunction<R>

- P Type을 입력으로 받고, R 타입으로 반환

class Test{
	public static void main(String[] args){
    	// P Type Function은 입력을 P Type으로 받는다.
        IntFunction<String> funcTree = value -> "" + value;
        System.out.println(funcTree.apply(512));    
	}
}

 

3) PtoQFunction

- P Type을 입력으로 받고, Q 타입으로 반환

- P applyAsQ(p value) 메소드 사용

// PtoQFunction : P -> Q로 매핑하는 함수
// IntToIntFunction은 없다. 동일한 형태는 없다.
IntToDoubleFunction funcFive;

 

4) ToPFunction<T>, ToPBiFunction<T, U>

- P 타입으로 리턴

- p applyAsP() 메소드 사용

// ToP Type Function은 출력을 P 타입으로 한다.
ToIntFunction<String> funcFour = (s) -> s.length();

// 출력이 P타입인 경우에는 AsP가 들어간다.
funcFour.applyAsInt("ABCDE");

4. Operator 계열

- 입력받은 타입과 동일한 타입의 출력을 하는 Functional Interface

- Function 계열과 달리 입출력 타입이 다를 수 없다.

- apply() 메소드 사용

 

1) UnaryOperator<T>, BinaryOperator<T>

 

class Test{
	public static void main(String[] args){
    	// 기본 Operator가 없다, 입력이 1개인 것을 Unary로 표현
        UnaryOperator<String> operator = s -> s + ".";
        System.out.println(operator.apply("왔다"));

        BinaryOperator<String> operatorTwo = (s1, s2) -> s1 + s2;
        System.out.println(operatorTwo.apply("나", "왔다"));
    }
}

 

2) PUnaryOperator, PBinaryOperator

- P Type을 입력으로 받고, P Type으로 리턴

class Test{
	public static void main(String[] args){
    	IntUnaryOperator op = value -> value * 10;
        System.out.println(op.applyAsInt(5));
        // LongUnaryOperator, DoubleUnaryOperator

        IntBinaryOperator op2 = (v1, v2) -> v1 * v2;
        System.out.println(op2.applyAsInt(5, 10));
        // LongBinaryOperator, DoubleBinaryOperator
	}
}

5. Predicate 계열

- 논리 판단을 해주는 Functional Interface

- 입력을 받아서 boolean 타입 출력으로 반환

- test() 메소드 사용

 

1) Predicate<T>, BiPredicate<T, U>

class Test{
	public static void main(String[] args){
    	Predicate<String> pred = (s) -> s.length() == 4;
        System.out.println(pred.test("abcd"));
        System.out.println(pred.test("abcde"));

        BiPredicate<String, Integer> pred2 = (s, v) -> s.length() == v;
        System.out.println(pred2.test("abcd", 3));
    }
}

 

2) PPredicate

- P Type을 입력받는다.

- IntPredicate, LongPredicate, DoublePredicate

class Test{
	public static void main(String[] args){
		IntPredicate pred3 = (i) -> i > 0;
        System.out.println(pred3.test(3));
        // LongPredicate, DoublePredicate
	}
}

 


• 표준형 함수형 인터페이스의 메소드

1. andThen(), compose()

- 2개 이상의 함수형 인터페이스를 연결하기 위해 사용

 

1) A.andThen(B)

 - A를 먼저 실행하고 B를 실행.

 - Consumer, Funcation, Operator 계열의 default method 구현

Consumer<String> c0 = s -> System.out.println("c0:"+s);
Consumer<String> c1 = s -> System.out.println("c1:"+s);
Consumer<String> c2 = c0.andThen(c1);
c2.accept("STRING");

 

2) A.compose(B)

 - B를 먼저 실행하고 A를 실행.

 - Funcation, Operator 계열의 default method 구현

// Function 계열은 입력 -> 출력 이 연쇄가 되어야 된다.
Function<String, Integer> func1 = s -> s.length();
Function<Integer, Long> func2 = value -> (long)value;
Function<String, Long> func3 = func1.andThen(func2);
System.out.println(func3.apply("Four"));

2. and(), or(), negate, isEqual()

- Predicate 계열 기본 메소드/ 클래스 메소드

 

1) and(), or(), negate()

 - and() : A && B 

 - or() : A || B

 - negate() : !A

DoublePredicate p0 = (y) -> y > 0.5;
DoublePredicate p1 = (y) -> y < 0.7;
DoublePredicate p2 = p0.and(p1);    // and
System.out.println(p0.test(0.9));
System.out.println(p1.test(0.9));
System.out.println(p2.test(0.6));

DoublePredicate p3 = p0.or(p1);     // or
DoublePredicate p4 = p0.negate();   // not

 

2) isEqual()

 - isEqual() : A == B

 - 클래스 메소드이다.

// Class Method
Predicate<String> eq = Predicate.isEqual("String");
System.out.println(eq.test("String"));
System.out.println(eq.test("String!"));

3. minBy, maxBy()

- BinaryOperator 클래스의 클래스 메소드

- Compartor<T>를 입력받아 min, max 출력

BinaryOperator<String> minBy = BinaryOperator.minBy((o1, o2) -> {
    return len(o1) > len(o2) ? 1 : -1;
});

BinaryOperator<Integer> maxBy = BinaryOperator.maxBy((val1, val2) -> {
    return val1.compareTo(val2);
});

 


• 람다식에 기존 메소드/생성자 사용

- 람다식에 기존에 구현되어 있는 내용을 재사용하고자 할 때

- 생성자를 andThen(), compose와 같이 사용 가능

 

1. ClassName::instanceMethod

// 클래스::인스턴스_메소드
String[] strings = {"fast", "campus", "best"};
Arrays.sort(strings, String::compareTo);
// Comparator 인터페이스는 2개의 입력 o1, o2를 받음
// o1.인스턴스_메소드(o2)로 호출되는 메소드가 사용됨

 

2. ClassName::classMethod

Function<String, Integer> func0 = Integer::parseInt;

 

3. instance::instanceMethod

- 주어진 객체의 메소드를 호출

String s = "String";
Predicate<String> preq = s::equals;
pred.test("String");    // True
pred.test("String2");   // False

 

4. ClassName::new

Function<Integer, Integer> fnc = Integer::new;

 

5. ClassName[]::new

IntFunction<String[]> fnc2 = String[]::new;
String[] strings2 = fnc2.apply(100);

 

 

 

'Coding > Java' 카테고리의 다른 글

Java Stream API  (0) 2020.09.28
Java Comparator & Comparable  (0) 2020.09.22
Java Inner Class(내부 클래스)  (0) 2020.09.18
Java Handling Exception(예외 처리)  (0) 2020.09.17
Java Generic(제네릭)  (0) 2020.09.17