Java 8

8 minute read

노력하는 사람에게 복이 있나니.


Functional Interface

Functional Interface는 (Object 클래스의 메소드를 제외하고) 단 하나의 추상 메소드만을 가진 인터페이스를 의미하며, 그런 이유로 단 하나의 기능적 계약을 표상하게 된다.

  • @FunctionalInterface : 작성한 인터페이스가 Functional Interface임을 확실히 하기 원할 때에만 사용하면 됩니다.

@FunctionalInterface로 지정되어 있으면서 실제로는 Functional Interface가 아닌 인터페이스를 선언한 경우 컴파일 타임 에러가 발생한다.

선언 방식


@FunctionalInterface 
interface func {
  void apply(String value);
}


@FunctionalInterface
public interface  Operator<T, U, R> {
    public R operate(T a, U b);
}     

  • 함수형 인터페이스에서 함수의 의미

메서드와 함수의 큰 차이는 메서드는 객체에 종속되어 있다는 것이다. 함수란 인풋에 의해서만 아웃풋이 달라져야하는데 메서드는 객체에 종속적이기 때문에 인풋이 달라져야 하며, 그것을 지원하기 위해 람다 표현식으로 구현할 때는 객체는 상태를 가질 수 없다. 즉 상태가 없는 객체(Statelet Object)를 의미한다.

함수형 기본 샘플

  • Function : 인자, 리턴 타입

Consumer<? super Object> consumer = System.out::println;

Function function = Integer::parseInt;

Integer result = function.apply("123123");

consumer.accept(result);

  • Consumer : 인자, void

Consumer<? super Object> consumer = System.out::println;

consumer.accept("테스트 함수");

  • Predicate : 인자, boolean

Predicate<String> predicate = String::isEmpty;

boolean isRight = predicate.test("");

consumer.accept(isRight);

  • Supplier : 인자없음, 리턴타입

Supplier<String> supplier = () -> "Hello Supplier";

String get = supplier.get();

System.out.println(get);

  • UnaryOperator : 동일한 타입의 인자, 리턴 타입

UnaryOperator<String> unaryOperator = str -> str + "isEmpty";

String unaryValue = unaryOperator.apply("haha");

consumer.accept(unaryValue);

  • Comparator : 동일한 비교 인자

Comparator<String> comparator = String::compareTo;

consumer.accept(comparator.compare("123123","123123"));

함수형 인터페이스 - 샘플 예제


package STUDY.JAVA8STUDY.basic02;

import java.util.Comparator;
import java.util.function.*;

public class FunctionalInterfaceList {

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("value");

        runnable.run();

        Supplier<String> supplier = () -> "Hello Supplier";

        String get = supplier.get();

        System.out.println(get);

        Consumer<? super Object> consumer = System.out::println;

        consumer.accept("테스트 함수");

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

        Integer result = function.apply("123123");

        consumer.accept(result);

        Predicate<String> predicate = String::isEmpty;

        boolean isRight = predicate.test("");

        consumer.accept(isRight);

        UnaryOperator<String> unaryOperator = str -> str + "isEmpty";

        String unaryValue = unaryOperator.apply("haha");

        consumer.accept(unaryValue);

        BinaryOperator<String> binaryOperator = ( str1 , str2) -> str1 +  str2;

        consumer.accept(binaryOperator.apply("asdf", "asdfsa"));

        BiPredicate<String, Integer> biPredicate = ( str, num) -> str.equals(Integer.toString(num));

        consumer.accept(biPredicate.test("adfsafd", 31231));

        BiConsumer<String, Integer> biConsumer = ( str, num) -> System.out.println(str + "::" + num);

        biConsumer.accept("숫자", 40);

        BiFunction<Integer, String, String> biFunction = ( num, str) -> String.valueOf(num) + str;

        String biFunctionResult = biFunction.apply(50, "678");

        consumer.accept(biFunctionResult);

        Comparator<String> comparator = String::compareTo;

        consumer.accept(comparator.compare("123123","123123"));

    }
}        

Lambda Expression 사용시 유의 사항


// statement 

either
  (parameters) -> expression            //1
or
  (parameters) -> { statements; }       //2
or 
  () -> expression                      //3

// sample code 

(int a, int b) ->    a * b               // takes two integers and returns their multiplication
 
(a, b)          ->   a - b               // takes two numbers and returns their difference
 
() -> 99                                 // takes no values and returns 99
 
(String a) -> System.out.println(a)      // takes a string, prints its value to the console, and returns nothing 
 
a -> 2 * a                               // takes a number and returns the result of doubling it
 
c -> { //some complex statements }       // takes a collection and do some procesing

  • Functional Interface에 대해서 너무 많은 Default 메소드를 사용하지 말 것

@FunctionalInterface                                
public interface FunctionExecute {
  public void execute();
  default void executeWithDefault();
}

Interface에서 자바명세어 따르면 Default 메소드에 대해서 몇개를 만들든 상관이 없으며, 이는 설계적인 관점에서 절대 좋은 방식은 아니다.

Functional Interface에서 동일한 Overloading은 정상적으로 동작하지 않는다.


@FunctionalInterface
public interface FunctionExecute {
  // 아래와 같이 메소드 자체를 구분해서 사용하는 것을 가장 추천 한다. 
  public void executeWithCallable(Callable<String> callable);

  public void executeWithSupplier(Supplier<String> supplier);

  /*
    또는 메소드 하나에 대해서 오버로딩을 사용하되 호출 시점에 타입 캐스팅을 통해서 실행할 수 있으나 
    이는 좋은 방식은 아니다. 첫번째 안으로 고려하여 처리할 필요가 있다. 
  */
}
    

  • 가능하면 코드 블록 {} 을 사용하지 않을 수 있도록 긴 코드를 사용하지말고 별도의 함수를 선언하여 이를 람다식에 적용하라. 하지만 2~3줄 짜리의 람다식에 대해서이를 함수로 빼내는 것이 옳은 일인지도 판단해야한다.

  • 람다 표현식 상에 정확한 타입을 선언하여 사용하지 마라.

  • 람다 표현식 내부에 반환문 ( return )을 표현하지 않고 사용할 수 있도록 하라.

  • 람다식을 사용할 때 Method Reference를 사용할 수 있다면 사용하라.

  • 람다식을 사용할 때 effectively final를 적절하게 고려하라.

  • 람다식을 사용하는 주된 이유는 병렬 프로그래밍을 하기 위함이다. 따라서 람다식 내의 함수에서 외부함수를 사용하면 변형/변수오염이 일어나며, 스레드 안전성을 위반하게 될 수 있다.


Stream

스트림이란 ‘데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소’

스트림을 활용하면 멀티 스레드 환경에 필요한 코드를 작성하지 않고도 데이터를 병렬로 처리할 수 있다.

filter, sorted, map, collect 같은 여려 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프 라인을 만들 수 있다. 여러 연산을 파이프라인으로 연결해도 여전히 가독성/명확성이 유지된다. filter 메서드의 결과는 sorted 메서드로, 다시 sorted의 결과는 map 메서드로, map 메서드의 결과는 collect로 연결이 된다. filter (또는 sorted, map, collect ) 같은 연산을 high-level building block 으로 이루어져 있으며, 특정 스레딩 모델을 제한되지 않고 자유롭게 어떤 상황에서든 사용할 수 있다. 결과적으로 우리는 데이터 처리과정을 병렬화하면서 스레드와 락을 걱정할 필요가 없다.

  • 스트림은 데이터 소스를 변경하지 않는다.
  • 스트림 한번 사용하면 다시 사용할 수 없다.
  • 스트림은 작업을 내부 반복으로 처리한다.
  • 스트림은 데이터 흐름 속에 중간 연산과 최종연산이 존재한다.

Stream<Integer> intStreams = Arrays.Stream(values).filter( item -> item.contains("0"));
result = intStreams.count();

List<String> lists = intStreams.collect(Collectors.toList()); // 에러 , 사용불가 

Stream 요소

  • Mapping
    • map

    스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다. 이 과정은 기존의 값을 ‘고친다:modify’라는 개념보다는 ‘새로운 버전을 만든다’라는 개념에 가까우므로 ‘변환:transforming’에 가까운 ‘매핑:mapping’이라는 단어를 사용한다.


stream().map

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

  • flatMap

/*
  .flatMap은 Array나 Object로 감싸져 있는 모든 원소를 단일 언소 스트림으로 반환해준다.
  스트림 평면화, 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결,
  flatMap은 먼저 매핑 함수를 사용해 각 엘리먼트에대해 map을 수행 후, 결과를 새로운 배열로 평평화한다.
*/
stream().flatMap

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

  • filter

stream().filter

Stream<T> filter(Predicate<? super T> predicate);

  • peek

// ‘peek’ 은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer 를 인자로 받습니다. 
// 따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않습니다.                                
stream().peek           

  • Search & Matching

    • anyMatch

stream().anyMatch

boolean anyMatch(Predicate<? super T> predicate);

  • allMatch

stream().allMatch

boolean allMatch(Predicate<? super T> predicate);

  • noneMatch

stream().noneMatch

boolean noneMatch(Predicate<? super T> predicate);

  • Element Searching

    • findAny

stream().findAny

Optional<T> findAny();

  • findFirst

stream().findFirst

Optional<T> findFirst();

  • Reducing

    • reduce

stream().reduce

T reduce(T identity, BinaryOperator<T> accumulator);

Integer sum = integers.reduce(0, Integer::sum);

Integer sum = integers.reduce(0, (a, b) -> a+b);

  • Number Type Stream

    • mapToInt

stream().mapToInt

IntStream mapToInt(ToIntFunctio<? super T> mapper);

  • mapToDouble

stream().mapToDouble

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

  • mapToLong

stream().mapToLong

LongStream mapToLong(ToLongFunction<? super T> mapper);

  • boxed

intStream.boxed

Stream<type> boxed();

  • Range

    • range

IntStream.range

  • rangeClosed

IntStream.rangeClosed

  • Collectors

    • collect

Collectors에서 제공하는 메서드의 기능은 크게 세가지(스트림 요소를 하나의 값으로 리듀스하고 요약/요소 그룹화/요소 분할)로 구분할 수 있다.

  Collectors.maxBy

  Collectors.minBy

  Collectors.toList

  Collectors.averagingInt

  Collectors.averagingLong

  Collectors.averagingDouble

  reducing

  groupingBy : 그룹화 / 다수준 그룹화

  toList

  • Other

    • of

// Stream.of()로 생성하려는 객체를 입력하면, 스트림 객체로 생성이 된다.      

  • empty

// Stream.empty()는 비어있는 스트림을 생성한다.   

  • generate

//  Stream.generate()는 반환되는 인자를 무한히 생성합니다. 
//  Random 함수등과 같이 사용하여 난수 생성시 활용할 수 있습니다.      

  • Arrays.stream

// Array 또한 stream 함수를 이용하여 Stream을 생성할 수 있습니다.      

  • concat

// 두개의 Stream 을 concat을 통해 하나로 만들 수 있다.  

  • builder

 
// 빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다. 마지막에 build 메소드로 스트림을 반환한다. 

Stream<String> builderStream = Stream.<String>builder()
  .add("Build1")
  .add("Build2")
  .add("Build3")
  .build();

builderStream.map(item -> item + "Value").forEach(System.out::println);

  • distinct

// 동일값이 존재하는 경우 stream에서 distinct가 호출되면 동일 값은 하나로 보이게 된다.                               
List<String> stringList = Arrays.asList("google", "google", "value" , "value1" , "value2" , "value3");

Stream<String> stream1 = stringList.stream();
Stream<String> stream2 = stream1.distinct();

stream2.forEach(System.out::println);    

  • limit

// Stream에서 흘러가는 데이터에 대한 제한을 걸어야 할 경우, 사용한다. 
                              
List<Integer> listInteger = Stream.iterate(0, a -> a + 10).limit(10).collect(Collectors.toList());  

  • skip

 
# skip은 Stream Data에서 선택된 갯수만큼을 제외하고 다음 값부터 처리할  있게 필터링 된다. 

List<Integer> listInteger = Stream.iterate(0, a -> a + 10).limit(10).collect(Collectors.toList());

List<Integer> listInteger2 = listInteger.stream()
        .map(value -> value - 2)
        .peek(value -> System.out.println("테스트 1 : " + value) )
        .filter(value -> value > 0 )
        .peek(value -> System.out.println("테스트 2 : " + value) )
        .collect(Collectors.toList());

listInteger2.forEach(value -> System.out.println("before filter ["+value+ "]"));

System.out.println("");

List<Integer> listInteger3 = listInteger2.stream()
        .skip(4)
        .collect(Collectors.toList());

listInteger3.forEach(value -> System.out.println("after filter ["+value+ "]"));    

  • iterate

// 반복적으로 반복 데이터를 생성할 수 있다. 기본 시작값 및 반복될 때 계산될 값을 지정할 수 있다. 

List<Integer> listInteger = Stream.iterate(0, a -> a + 10).limit(10).collect(Collectors.toList());

  • generate

// limit 등의 제한 함수가 선언되기 전까지는 무한히 데이터를 생성할 때 사용한다.       

Stream<String> stream = Stream.generate(() -> "Builder");

  • Sorted

// 정렬된 새로운 스트림을 반환
List<String> stringList = Stream.iterate(0, a -> a + 10).map(item -> item + " Value").limit(20).collect(Collectors.toList());

Stream<String> sortedStream = stringList.stream().sorted(Comparator.comparing(String::length).reversed());

sortedStream.forEach(s -> {
  log.info("Sorted Values : {}  ", s);
});

  • count

long count = stringList.stream().count();

log.info("Count Value : {}" , count);                              

  • min

String stringMin = stringList.stream().min(Comparator.comparing(String::valueOf)).get();

log.info("min Value : {}" , stringMin);        

  • max
 
String stringMax = stringList.stream().max(Comparator.comparing(String::valueOf)).get();

log.info("max Value : {}" , stringMax);                             
                            
  • sum

List<Integer> longStream = Stream.iterate(0, a -> a + 1).limit(50).collect(Collectors.toList());

Integer sum = longStream.stream().mapToInt(Integer::intValue).sum();

log.info("총합계 {}", sum); 

  • average

OptionalDouble average = longStream.stream().mapToInt(Integer::intValue).average();

log.info("Average Value : {}" , average.getAsDouble());   

  • foreach

List<String> stringList = Stream.iterate(0, a -> a + 10).map(item -> item + " Value").limit(20).collect(Collectors.toList());

Stream<String> sortedStream = stringList.stream().sorted(Comparator.comparing(String::length).reversed());

sortedStream.forEach(s -> {
  log.info("Sorted Values : {}  ", s);
});                                    


Oprtional

Optional은 존재할 수 도 있지만 안할 수 도 잇는 객체. 즉, “null이 될 수도 있는 객체”을 감싸고 있는 일존의 래퍼 클래스입니다.

  • Null Point Exception을 유발할 수 있는 null을 다루지 않아도 된다.
  • 별도의 null 체크가 필요 없다.
  • 명시적으로 해당 변수가 null일 수 있다고 표현할 수 있다.

Optional.empty();

Optional.of(value);

Optional.ofNullable(value);

Optional.get()

Optional.orElse(new Value())

Optional.orElseGet(() => { return new Value() })

Optional.orElseThrow(() => { return exceptions.getMessage() })

Optional을 사용하는 방식

  • ofNullable과 Stream API 처럼 map을 활용하여 데이터 가져오기
  • ofNullable과 Stream API 처럼 filter을 활용하여 데이터 가져오기
  • ofNullable과 Stream API 처럼 map을 활용하여 문자열의 길이를 체크하는 방식
  • 정적 메소드를 활용하여 인덱스 범위를 체크하고 그 뒤 그 해당값을 사용하는 방식
  • ifPresent를 이용하여 함수형 인자를 통해 데이터를 처리하는 경우

package studysample.java8.optionalSample;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class OptionalSample {

    public static void main(String[] args) {

        Map<String, String> testMap = new HashMap<>();

        testMap.put("test1", "1번 테스트 데이터");
        testMap.put("test2", "2번 테스트 데이터");
        testMap.put("test3", "3번 테스트 데이터");

        Optional<String> testData = Optional.ofNullable(testMap.get("test4"));

        if(testData.isPresent()){
            System.out.println(testData.get());
        }else{
            System.out.println("데이터가 없어요!");
        }

        int length = testData.map(String::length).orElse(0);
        System.out.println("length: " + length);

        List<Integer> listData = Stream.of(1,2,3,4,5,7,8,9,10).collect(Collectors.toList());
        Optional<Integer> optionalValue =  getAsOptional(listData, 30);
        int intValue = optionalValue.map( integer ->  integer + 1 ).orElse(0);
        System.out.println(intValue);

        List<Integer>  listData2 = Stream.iterate(0, i -> i +1).limit(10).collect(Collectors.toList());
        Optional<Integer> optionalValue2 =  getAsOptional(listData, 32);
        optionalValue2.ifPresent(value -> {
            System.out.println("값을 출력하세요~!" + value);
        });

        Map<String, Object> paramMap = new HashMap<>();

        paramMap.put("put", "value");

        Optional<Map> valueMap = getMapData(paramMap, "get");

        valueMap.ifPresent(
            map -> System.out.println(map.toString())
        );
    }

    public static <T> Optional<T> getAsOptional(List<T> list, int index) {
        try {
            return Optional.of(list.get(index));
        } catch (IndexOutOfBoundsException e) {
            return Optional.empty();
        }
    }

    public static Optional<Map> getMapData(Map mapdata, String key){
        return Optional.ofNullable(mapdata)
                .filter(map -> map.get(key) == "Value1")
                .map(Converter::converter);
    }
}

class Converter {
    public static Map converter(Map mapdata){
        return mapdata;
    }
}