본문 바로가기
프로그래밍 언어

Java 8 람다(Lambda) / 스트림(Stream) / double colon(::)

by 내기록 2022. 8. 15.
반응형

람다 함수란?

람다 함수는 익명 함수(Anonymous functions)를 지칭하는 용어로 함수를 보다 단순하게 표현한다.

함수를 하나의 식(expression)으로 표현한 것으로 함수를 람다식으로 표현하면 메소드의 이름이 없기 때문에 익명 함수(Anonymous Function)의 한 종류라고 볼 수 있다.

 

! 람다 식의 도입으로 익명 클래스를 생략할 수 있게 하여 boilerplate code를 크게 줄이고 가독성을 향상시켰다.

 

** 익명 함수(Anonymous functions) : 이름이 없는 함수로, 익명 함수들은 모두 일급 객체이다.

** 일급 객체 : 일급 객체인 함수는 변수처럼 사용이 가능하며 매개변수로 전달이 가능하다.

람다식으로 선언된 함수는 일급 객체이기 때문에 Stream API의 매개변수로 전달이 가능하다. (아래 예제 확인)

 

람다의 특징

익명 함수(Anonymous functions) : 람다 대수는 이름을 가질 필요가 없다. 익명 함수들은 공통적으로 1급 시민(First Class Citizen)이라는 특징을 가진다.

커링 (Curring) : 여러 개의 매개변수를 가진 함수를 한개의 매개변수를 가진 여러 함수의 연결로 나타내는 방법이다.

 

1급 개체(First class citizen)?

3가지 조건을 충족한다. 1) 변수나 데이터에 할당할 수 있어야 한다.

2) 객체의 인자로 넘길 수 있어야 한다.

3) 객체의 리턴값으로 리턴할 수 있어야 한다.

 

람다의 장단점

장점

  1. 코드 간결성 : 람다를 사용하면 불필요한 반복문의 삭제가 가능하여 복잡한 식을 단순하게 표현할 수 있다.
  2. 지연연산 수행 : 람다는 지연연산을 수행함으로써 불필요한 연산을 최소화할 수 있다.
  3. 병렬처리 : 멀티스레드를 사용하여 병렬처리를 할 수 있다.

단점

  1. 람다식 사용이 까다롭다.
  2. 람다 stream 에서 단순 for/while문 사용 시 성능이 떨어진다.
  3. 불필요하게 많이 사용하게 되면 오히려 가독성을 떨어뜨릴 수 있다.

 

예제

기존 자바 문법

new Thread(new Runnable() {
   @Override
   public void run() { 
      System.out.println("Hello world!"); 
   }
}).start();

 

람다 문법

new Thread(()->{
      System.out.println("Hello world!");
}).start();

 

함수형 인터페이스

람다식으로 순수 함수를 선언할 수 있게 되었지만 Java는 기본적으로 객체지향 언어익기 때문에 순수 함수와 일반 함수를 다르게 취급하고 있다. Java에서는 이를 구분하기 위해 함수형 인터페이스가 등장했다.

함수형 인터페이스란 함수를 1급 객체처럼 다룰 수 있는 어노테이션으로 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다.

함수형 인터페이스를 사용하는 이유는 Java의 람다식이 함수형 인터페이스를 반환하기 때문이다.

 

@FunctionalInterface

Functional Interface는 '구현해야 하는 추상 메소드가 하나만 정의된 인터페이스'를 의미한다.

자바 컴파일러는 @FunctionalInterface annotation이 붙은 함수형 인터페이스에 두 개 이상의 메소드가 선언되면 오류를 발생시킨다.

함수형 인터페이스를 사용하면 함수를 변수처럼 선언할 수 있다.

 

예제)

함수형 Interface 선언

@FunctionalInterface
public interface Math {
    public int Calc(int first, int second);
}

추상 메소드 구현 및 함수형 인터페이스 사용

public static void main(String[] args){

   Math plusLambda = (first, second) -> first + second;
   System.out.println(plusLambda.Calc(4, 2));

   Math minusLambda = (first, second) -> first - second;
   System.out.println(minusLambda.Calc(4, 2));

}

실행 결과

6
2

 

Java에서 제공하는 함수형 인터페이스

  • Supplier<T>
  • Consumer<T>
  • Function<T,R>
  • Predicate<T>
  • BiFunction<T,U,R>

 

간단하게 설명하면,

1) Supplier<T>

인자를 받지 않고 Type T 객체를 리턴하는 함수형 인터페이스이다.

// 정의
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

// 사용 예시
Supplier<LocalDateTime> s = () -> LocalDateTime.now();
LocalDateTime time = s.get();

System.out.println(time);

 

2) Consumer<T>

객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스이다.

// 정의
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

// 예시
import java.util.function.Consumer;

public class Java8Consumer1 {

    public static void main(String[] args) {
    
        Consumer<String> print = x -> System.out.println(x);
        print.accept("java");   // java
        
    }
}

3) Function<T,R>

인수(타입 T의 객체)를 사용하고 객체(타입 R의 객체)를 반환한다. 인수와 출력은 다른 유형일 수 있다.

  • T – Type of the input to the function.
  • R – Type of the result of the function.
// 정의
@FunctionalInterface
public interface Function<T, R> {

      R apply(T t);

}


// 예시
import java.util.function.Function;

public class JavaMoney {

    public static void main(String[] args) {

        Function<String, Integer> func = x -> x.length();

        Integer apply = func.apply("hello");   // 5

        System.out.println(apply);

    }

}

 

4) Predicate<T>

객체 T를 매개변수로 받아 처리한 후 Boolean을 반환한다.

보통 객체들의 모음을 위한 필터에 적용된다. filter()는 매개변수로 predicate를 받는다.

// 정의
@FunctionalInterface
public interface Predicate<T> {
  boolean test(T t);
}

// 예시
// Predicate in filter()

public class Java8Predicate {

    public static void main(String[] args) {

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        List<Integer> collect = list.stream().filter(x -> x > 5).collect(Collectors.toList());

        System.out.println(collect); // [6, 7, 8, 9, 10]

    }

}

// 결과
[6, 7, 8, 9, 10]

 

5) BiFunction<T,U,R>

2개의 매개변수를 받고 객체를 반환한다.

 

  • T – Type of the first argument to the function.
  • U – Type of the second argument to the function.
  • R – Type of the result of the function.
// 정의
@FunctionalInterface
public interface BiFunction<T, U, R> {

      R apply(T t, U u);

}

// 사용
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;

public class Java8BiFunction1 {

    public static void main(String[] args) {

        // takes two Integers and return an Integer
        BiFunction<Integer, Integer, Integer> func = (x1, x2) -> x1 + x2;

        Integer result = func.apply(2, 3);

        System.out.println(result); // 5

        // take two Integers and return an Double
        BiFunction<Integer, Integer, Double> func2 = (x1, x2) -> Math.pow(x1, x2);

        Double result2 = func2.apply(2, 4);

        System.out.println(result2);    // 16.0

        // take two Integers and return a List<Integer>
        BiFunction<Integer, Integer, List<Integer>> func3 = (x1, x2) -> Arrays.asList(x1 + x2);

        List<Integer> result3 = func3.apply(2, 3);

        System.out.println(result3);

    }

}

// 결과
5
16.0
[5]

 


자바 스트림(Stream)

자바 스트림(stream)은 Java 8부터 지원하기 시작한 기능으로 컬렉션에 저장되어 있는 element들을 하나씩 순회하면서 처리할 수 있는 코드패턴이다.

람다식과 함께 사용되어 컬렉션에 들어있는 데이터에 대한 처리를 간결하게 작성할 수 있다.

또한 내부 반복자를 사용하기 때문에 병렬처리가 쉽다.

 

이전에는 컬렉션의 엘리멘트들을 순회하기 위해 Iterator 객체나 foreach문을 사용했다.

Java 8부터는 스트림을 사용하여 조금 단순하게 코드를 작성할 수 있다.

 

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));

list.stream()
.filter("b"::equals)
.forEach(System.out::print);

 

스트림 생성

//// 컬렉션
List<String> list = Arrays.asList("a","b","c");
Stream<String> steam = list.steam();

//// 배열
String[] array = new Sting[]{"a","b","c"};
Stream<String> stream1 = Arrays.stream(array); 
// 인덱스 1 포함, 3 제외 -> "b","c"
Stream<String> stream2 = Arrays.stream(array,1,3);

 

//// builder

Stream.Builder<String> builder = Stream.builder();
  
// Adding elements in the stream of Strings
Stream<String> stream = builder.add("Geeks")
    .add("for")
    .add("Geeks")
    .add("GeeksQuiz")
    .build();
  
// Displaying the elements in the stream
stream.forEach(System.out::println);
//// Generate

// using Stream.generate() method 
// to generate 5 random Integer values
Stream.generate(new Random()::nextInt)
    .limit(5).forEach(System.out::println);

이 외에도 Iterator, Empty, 기본 타입(Primitive type)으로 생성할 수 있다.

 

스트림 데이터 가공

//// Filter

List<Customer> customersWithMoreThan100Points = customers
  .stream()
  .filter(c -> c.getPoints() > 100)
  .collect(Collectors.toList());
  
  
// we added the 'hasOverHundrredPoints' method to our Customer class
List<Customer> customersWithMoreThan100Points = customers
  .stream()
  .filter(Customer::hasOverHundredPoints)
  .collect(Collectors.toList());

 

map()은 스트림에서 나오는 데이터를 변환한다.

map() 메소드는 값을 변환해주는 람다식을 인자로 받아서 새로운 데이터를 생성한다.

//// Map
List<Staff> staff = Arrays.asList(
  new Staff("mkyong", 30, new BigDecimal(10000)),
  new Staff("jack", 27, new BigDecimal(20000)),
  new Staff("lawrence", 33, new BigDecimal(30000))
);

//Java 8
List<String> collect = staff.stream().map(x -> x.getName()).collect(Collectors.toList());
System.out.println(collect); //[mkyong, jack, lawrence]

이 외에도 flatMap, Sorted, Peek 등이 있다.

 

참고 : 자바 스트림(Stream) 사용법 및 예제

 

 

Java 8 method references, double colon (::) operator (메서드 참조)

double colon (::) operator는 method references 라 불린다.

Method references(메서드 참조)는 람다 식의 특수한 유형으로 간단한 람다 식을 만드는데 자주 사용된다.

 

Anonymous class to print a list

List<String> list = Arrays.asList("node", "java", "python", "ruby");
list.forEach(new Consumer<String>() {       // anonymous class
    @Override
    public void accept(String str) {
        System.out.println(str);
    }
});

 

Anonymous class -> Lambda expressions

List<String> list = Arrays.asList("node", "java", "python", "ruby");
list.forEach(str -> System.out.println(str)); // lambda

 

Lambda expressions -> Method references

List<String> list = Arrays.asList("node", "java", "python", "ruby");
list.forEach(System.out::println);          // method references

 

람다 식 또는 메서드 참조는 모두 기존 메서드를 호출하는 다른 방법일 뿐이다. 메소드 참조를 사용하면 더 나은 가독성을 얻을 수 있다.

 

method references 4가지

  • Reference to a static method 
    ClassName::staticMethodName
  • Reference to an instance method of a particular object
    object::instanceMethodName
  • Reference to an instance method of an arbitrary object of a particular type --> 예제로 확인!
    ContainingType::methodName
  • Reference to a constructor
    ClassName::new

1. Reference to a static method

// Lambda expression
(args) -> ClassName.staticMethodName(args)

// Method Reference
ClassName::staticMethodName

 

예제)

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class Java8MethodReference1a {

    public static void main(String[] args) {

        List<String> list = Arrays.asList("A", "B", "C");

        // anonymous class
        list.forEach(new Consumer<String>() {
            @Override
            public void accept(String x) {
                SimplePrinter.print(x);
            }
        });

        // lambda expression
        list.forEach(x -> SimplePrinter.print(x));

        // method reference
        list.forEach(SimplePrinter::print);

    }

}

class SimplePrinter {
    public static void print(String str) {
        System.out.println(str);
    }
}

 

2. Reference to an instance method of a particular object

instance의 메소드 호출

// Lambda expression
(args) -> object.instanceMethodName(args)

// Method Reference
object::instanceMethodName

 

예제)

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;

public class Java8MethodReference2 {

    public static void main(String[] args) {

        List<Employee> list = Arrays.asList(
                new Employee("mkyong", 38, BigDecimal.valueOf(3800)),
                new Employee("sunrise", 5, BigDecimal.valueOf(100)),
                new Employee("min", 25, BigDecimal.valueOf(2500)),
                new Employee("unknown", 99, BigDecimal.valueOf(9999)));

        // anonymous class
        /*list.sort(new Comparator<Employee>() {
            @Override
            public int compare(Employee o1, Employee o2) {
                return provider.compareBySalary(o1, o2);
            }
        });*/

        ComparatorProvider provider = new ComparatorProvider();

        // lambda
        // list.sort((o1, o2) -> provider.compareBySalary(o1, o2));

        // method reference
        list.sort(provider::compareBySalary);

        list.forEach(x -> System.out.println(x));

    }

}

class ComparatorProvider {

    public int compareByAge(Employee o1, Employee o2) {
        return o1.getAge().compareTo(o2.getAge());
    }

    public int compareByName(Employee o1, Employee o2) {
        return o1.getName().compareTo(o2.getName());
    }

    public int compareBySalary(Employee o1, Employee o2) {
        return o1.getAge().compareTo(o2.getAge());
    }

}

 

3. Reference to an instance method of an arbitrary object of a particular type

위 문장으로 정리하기엔 모호한 느낌이 있다. 아래 예제로 알아보자.

 

**java arbitary number of arguments

int sum(int ... values){...};

 

//// Lambda expression
// example, assume a and b are String
(a, b) -> a.compareToIgnoreCase(b)

//// Method Reference
// example, a is type of String
String::compareToIgnoreCase

 

(String a, String b)에서 a와 b는 arbitrary name(임의 이름)이며 String은 arbitrary type(임의 유형)이다.

  String[] stringArray = { "Barbara", "James", "Mary", "John",
                "Patricia", "Robert", "Michael", "Linda" };
  Arrays.sort(stringArray, String::compareToIgnoreCase);
  
  
  // Arrays.sort method
  public static <T> void sort(T[] a, Comparator<? super T> c) {
  }

매개변수 타입의 instance method를 참조한다고 생각한다..

(String a, String b) -> a.compareToIgnoreCase(b) // return int

// a is type of String
// method reference
String::compareToIgnoreCase

 

첫 번째 매개변수(f)의 타입이 InvoiceCalculator이다.

따라서 우리는 arbitrary object(f)의 인스턴스 메소드(normal of promotion)을 참조할 수 있다.

* InvoiceCalculator class는 normal, promotion method를 가진다.

(f, o) -> f.normal(o))
(f, o) -> f.promotion(o))

InvoiceCalculator::normal
InvoiceCalculator::promotion

 

참고 : double colon (::) operator - mkyong

 

 

4. Reference to a constructor

//// Lambda expression
(args) -> new ClassName(args)

//// Method Reference
ClassName::new

예제) 생성자를 참조할 때 ClassName::new 를 사용한다.

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

public class Java8MethodReference4a {

    public static void main(String[] args) {

        // lambda
        Supplier<Map> obj1 = () -> new HashMap();   // default HashMap() constructor
        Map map1 = obj1.get();

        // method reference
        Supplier<Map> obj2 = HashMap::new;
        Map map2 = obj2.get();

        // lambda
        Supplier<Invoice> obj3 = () -> new Invoice(); // default Invoice() constructor
        Invoice invoice1 = obj3.get();

        // method reference
        Supplier<Invoice> obj4 = Invoice::new;
        Invoice invoice2 = obj4.get();

    }

}

class Invoice {

    String no;
    BigDecimal unitPrice;
    Integer qty;

    public Invoice() {
    }

    //... generated by IDE
}

 

 

 

References

[Java] 람다식과 함수형 인터페이스

https://khj93.tistory.com/entry/JAVA-%EB%9E%8C%EB%8B%A4%EC%8B%9DRambda%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%82%AC%EC%9A%A9%EB%B2%95

https://hbase.tistory.com/171

https://mkyong.com/java8/java-8-streams-map-examples/

https://mkyong.com/java8/java-8-method-references-double-colon-operator/

Java 8 - Supplier 예제

https://mkyong.com/java8/java-8-consumer-examples/ ( 외 다수의 예제 페이지. 추천하는 사이트 )

 

반응형

댓글