Java 8
노력하는 사람에게 복이 있나니.
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;
}
}