- 자바 스트림(Stream) API 사용하기 -


Java 8에서 추가된 스트림(Steam) API에 대해 알아보자.

 

자바에서 배열이나 컬렉션을 사용할 때 여기에 저장된 데이터에 접근하기 위해서는 반복문이나 반복자(Iterator)를 사용하여 데이터에 접근해야했는데,

그렇게 되면 코드가 너무 길어지고 가독성도 떨어지고, 코드의 재사용이 거의 불가능한 상태의 코드가 탄생한다.

 

이러한 문제점을 극복하기 위해 나온게 스트림 API다.

그러다보니 보다 간결해지고, 데이터 소스에 대한 공통된 접근 방식을 제공하기 때문에 자주 사용된다.

 

이제 스트림API의 특징과 사용법에 대해 하나씩 알아가보자.

 

아 들어가기에 앞서 스트림 API는 람다 표현식을 많이 사용하니 뭔지 알아두기라도 하자.

https://mine-it-record.tistory.com/476

 

[JAVA] 자바_람다식(Lambda Expression) (ft. 함수형 인터페이스, 메서드 참조)

- 자바 람다 표현식(Lambda Expression)이란? - Java 8에서 추가된 람다 표현식(Lambda Expression)과 함수형 인터페이스 그리고 메서드 참조에 대해 알아보자. 1. 람다식(Lambda Expression) - 람다식이란 익명..

mine-it-record.tistory.com


1. Stream API

1-1. 주요 특징과 동작 흐름

  • 스트림은 원본 데이터를 변경하지 않는다.
  • 스트림은 외부 반복을 통해 작업하는 컬렉션과는 달리 내부 반복(internal iteration)을 통해 작업을 수행한다.
  • 스트림은 재사용이 가능한 컬렉션과는 달리 단 한 번만 사용할 수 있다.
  • 스트림의 연산은 필터(filter)-맵(map) 기반의 API를 사용하여 지연(lazy) 연산을 통해 성능을 최적화한다.
  • 스트림은 parallelStream() 메서드를 통한 손쉬운 병렬 처리를 지원한다.
  • 스트림은 [스트림의 생성 -> 스트림의 중개 연산 -> 스트림의 최종 연산]세 가지 단계에 걸쳐서 동작하며, 중개 연산의 경우 Stream형태로 결과를 반환하기 때문에 연속적으로 연결해서 사용할 수 있다.

스트림 API 동작 흐름 (출처 : tcpschool)

 

1-2. 스트림의 생성

스트림 API는 다양한 데이터 소스 (컬렉션, 배열, 가변 매개변수, 지정된 범위의 연속된 정수, 특정 타입의 난수들, 람다 표현식, 파일, 빈 스트림)에서 생성할 수 있다.

 

▷ 1) 컬렉션

자바에서 제공하는 모든 컬렉션의 최고 상위 조상인 Collections 인터페이스에는 stream() 메서드가 정의되어 있다.

따라서 Collection 인터페이스를 구현한 모든 List와 Set 컬렉션 클래스에서도 stream()메서드로 스트림을 생성할 수 있다. 또한, parallelStream() 메서드를 사용하면 병렬 처리가 가능한 스트림을 생성할 수 있다.

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(4, 3, 2, 1);
        Stream<Integer> stream = list.stream();
        stream.forEach(System.out::println);
		
        // Collection 인터페이스를 가지진 않지만 컬렉션 프레임워크 번외로 사용 예제 추가
        // stream api를 활용한 map 출력
        Map<String, Integer> map = new HashMap<>();
        map.put("mine", 50);
        map.put("mine-it", 100);
        map.put("mine-it-record", 200);

        map.entrySet().stream()
            .filter(e -> e.getKey().contains("it"))
            .filter(e -> e.getValue() > 150)
            .forEach(e -> System.out.println(e.getKey() + " : " + e.getValue()));
    }
}

 

▷ 2) 배열

배열에 관한 스트림을 생성하기 위해 Arrays 클래스에는 다양한 형태의 stream() 메서드가 클래스 메서드로 정의되어 있다. 또한, 기본 타입인 int, long, double 형을 저장할 수 있는 배열에 관한 스트림이 별도로 정의되어 있다.

이러한 스트림은 java.util.stream 패키지의 intStream, LongStream, DoubleStream 인터페이스로 각각 제공된다.

public class Main {
    public static void main(String[] args) {

        String[] arr = new String[]{"넷", "둘", "셋", "하나"};

        // 배열에서 스트림 생성
        Stream<String> stream1 = Arrays.stream(arr);
        stream1.forEach(e -> System.out.print(e + " "));
        // stream1.forEach(System.out::println);
        System.out.println(); // 넷 둘 셋 하나

        // 배열의 특정 부분만을 이용한 스트림 생성
        Stream<String> stream2 = Arrays.stream(arr, 3, 4);
        stream2.forEach(e -> System.out.print(e + " ")); // 하나
    }
}

 

▷ 3) 가변 매개변수

Stream 클래스의 of() 메서드를 사용하면 가변 매개변수(variable parameter)를 전달받아 스트림을 생성할 수 있다.

public class Main {
    public static void main(String[] args) {

        // 가변 매개변수에서 스트림 생성
        Stream<Double> stream = Stream.of(4.2, 2.5, 3.1, 1.9);
        stream.forEach(System.out::println);
    }
}

 

▷ 4) 지정된 범위의 연속된 정수

지정된 범위의 연속된 정수를 스트림으로 생성하기 위해 IntStream나 LongStream 인터페이스에는 range()와 rangeClose() 메서드가 정의되어 있다.

  • range(int startInclusive, int endExclusive) : 명시된 시작 정수를 포함하지만, 명시된 마지막 정수는 포함하지 않는 스트림을 생성한다.
  • rangeCloase (int startInclusive, int endExclusive) : 명시된 시작 정수뿐만 아니라 명시된 마지막 정수까지도 포함하는 스트림을 생성한다.
public class Main {
    public static void main(String[] args) {

        // 지정된 범위의 연속된 정수에서 스트림 생성
        IntStream stream1 = IntStream.range(1, 4);
        stream1.forEach(e -> System.out.print(e + " ")); // 1 2 3 
        System.out.println();

        IntStream stream2 = IntStream.rangeClosed(1, 4);
        stream2.forEach(e -> System.out.print(e + " ")); // 1 2 3 4

    }
}

 

▷ 5) 특정 타입의 난수들

특정 타입의 난수로 이루어진 스트림을 생성하기 위해 Random 클래스에는 ints(), longs(), doubles()와 같은 메서드가 정의되어 있다.

  • 이 메서드들은 매개변수로 스트림의 크기를 long 타입으로 전달받을 수 있다.
  • 이 메서드들은 만약 매개변수를 전달받지 않으면 크기가 정해지지 않은 무한 스트림(infinite stream)을 반환한다. (이때에는 limit() 메서드를 사용하여 따로 스트림의 크기를 제한해야 한다.)
public class Main {
    public static void main(String[] args) {

        // 특정 타입의 난수로 이루어진 스트림 생성
        IntStream stream = new Random().ints(4);
        stream.forEach(System.out::println);

    }
}

 

▷ 6) 람다 표현식

람다 표현식을 매개변수로 전달받아 해당 람다 표현식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream 클래스에는 iterate()와 generate() 메서드가 정의되어 있다.

  • iterate(T seed, UnaryOperator<T> f) : 시드로 명시된 값을 람다 표현식에 사용하여 반환된 값을 다시 시드로 사용하는 방식으로 무한스트림을 생성한다.
  • generate(Supplier<T> s) : 매개변수가 없는 람다 표현식을 사용하여 반환된 값으로 무한 스트림을 생성한다.
public class Main {
    public static void main(String[] args) {
        Stream stream = Stream.iterate(2, n -> n + 2); // 2, 4, 6, 8, 10, ...
        stream.forEach(System.out::println);
    }
}

위 예제는 홀수만으로 이루어진 무한 스트림을 생성하는 예제다..

 

▷ 7) 파일

파일의 한 행(line)을 요소로 하는 스트림을 생성하기 위해 java.nio.file.Files 클래스에는 lines() 메소드가 정의되어 있다.

또한, java.io.BufferedReader 클래스의 lines() 메소드를 사용하면 파일뿐만 아니라 다른 입력으로부터도 데이터를 행(line) 단위로 읽어 올 수 있습니다.

public class Main {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("D:\\mine.txt");
        Stream<String> stream1 = Files.lines(path);
        Stream<String> stream2 = Files.lines(path, Charset.forName("UTF-8"));
    }
}

 

▷ 8) 빈 스트림

아무 요소도 가지지 않는 빈 스트림은 Stream 클래스의 empty() 메서드를 사용하여 생성할 수 있다.

public class Main {
    public static void main(String[] args) throws IOException {
        // 빈 스트림 생성
        Stream<Object> stream = Stream.empty();
        System.out.println(stream.count()); // 스트림의 요소의 총 개수를 출력함.
        // 0
    }
}

 

1-3. 스트림의 중개 연산 (스트림의 변환)

- 스트림 API에 의해 생성된 초기 스트림은 중개 연산을 통해 또 다른 스트림으로 변환된다.

- 중개 연산은 스트림을 전달받아 스트림으로 반환하므로, 중개 연산은 연속으로 연결해서 사용할 수 있다.

- 스트림의 중개 연산은 필터(filter)-맵(map) 기반의 api를 사용함으로 지연(lazy) 연산을 통해 성능을 최적화할 수 있다.

- 스트림 API에서 사용할 수 있는 대표적인 중개 연산과 그에 따른 메서드는 다음과 같다.

  • (스트림 필터링) filter(Predicate<? super T> predicate) : 해당 스트림에서 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 반환.
  • (스트림 필터링) distinct() : 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환. (내부적으로 Object 클래스의 equals() 메서드를 사용함)
  • (스트림 변환) map(Functoin<? super T, ? extends R> mapper) : 해당 스트림의 요소들을 주어진 함수에 인수로 전달하여, 그 반환값으로 이루어신 새로운 스트림을 반환.
  • (스트림 변환) flatMap(Functoin<? super T, ? extends Stream<? extends R>> mapper) : 해당 스트림의 요소가 배열일 경우, 배열의 각 요소를 주어진 함수에 인수로 전달하여, 그 반환값으로 이루어진 새로운 스트림을 반환.
  • (스트림 제한) limit(long maxSize) : 해당 스트림에서 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환.
  • (스트림 제한) skip(long n) : 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환.
  • (스트림 정렬) sorted(Comparator<? super T> comparator) : 해당 스트림을 주어진 비교자(comparator)를 이용하여 정렬한다. (비교자를 전달하지 않으면 영문사전 순(natural order)으로 정렬한다.)
  • (스트림 연산 결과 확인) peek(Consumer<? super T> action) : 결과 스트림으로부터 각 요소를 소모하여 추가로 명시된 동작(action)을 수행하여 새로운 스트림을 생성하여 반환함. (주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용한다. 따라서 개발자가 디버깅 용도로 많이 사용한다고 보면된다.)

▷ 예제 1 ) 스트림 중개 연산

public class Main {
    public static void main(String[] args) {
       
        // 중복을 제거하고(distinct) 홀수만을 골라낸다(filter)
        IntStream streamFilter = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
        streamFilter.distinct().filter(n -> n % 2 != 0).forEach(System.out::println);
        // 7 5 1 3
        
        // 배열의 각 요소를 변환하여 다시 하나로 합쳐 새로운 스트림으로 반환 (map)
        Stream<String> streamMap = Stream.of("Int", "Double", "Long");
        streamMap.map(s -> s.concat("Stream")).forEach(System.out::println);
        // IntStream DoubleStream LongStream

        // 여러 문자열이 저장된 배열을 각 문자열에 포함된 단어로 이루어진 스트림으로 변환후 반환(flatMap)
        // flatMap의 매개변수 타입을 보면 Stream타입인것을 명심하자.
        String[] arr = {"I study hard", "You study JAVA", "I am hungry"};
        Stream<String> streamFlatmap = Arrays.stream(arr);
        streamFlatmap.flatMap(s -> Stream.of(s.split(" "))).forEach(System.out::println);
        // I study hard You study JAVA I am hungry
        
        // 첫번째 요소부터 3개를 제외하고 (skip) 첫 번째 요소부터 5개의 요소만으로 이루어진 (limit) 스트림 반환 
        IntStream streamLimit = IntStream.range(0, 10); // 0 1 2 3 4 5 6 7 8 9
        streamLimit.skip(3).limit(5).forEach(n -> System.out.print(n + " "));
        // 3 4 5 6 7
        
        // 오름차순과 내림차순 정렬 (sort)
        Stream<String> streamSort1 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
        Stream<String> streamSort2 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");

        streamSort1.sorted().forEach(s -> System.out.print(s + " ")); // CSS HTML JAVA JAVASCRIPT
        streamSort2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));
        // JAVASCRIPT JAVA HTML CSS 
        
        // 연산과 연산 사이의 결과 확인 (peek)
        IntStream streamPeek = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
        streamPeek.peek(s -> System.out.println("원본 스트림 : " + s))
            .skip(2)
            .peek(s -> System.out.println("skip(2) 실행 후 : " + s))
            .sorted()
            .peek(s -> System.out.println("sorted() 실행 후 : " + s))
            .forEach(System.out::println);
    }
}

 

1-4. 스트림의 최종 연산 (스트림의 사용)

- 스트림 API에서 중개 연산을 통해 변환된 스트림은 마지막으로 최종 연산을 통해 각 요소를 소모하여 결과를 표시한다. 즉, 지연(lazy)되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되는 것이다.

- 최종 연산 시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없다.

- 스트림 API에서 사용할 수 있는 대표적인 최종 연산과 그에 따른 메서드는 다음과 같다.

  • (요소의 출력) forEach(Consumer<? super T> action) : 스트림의 각 요소에 대해 해당 요소를 소모하여 명시된 동작을 수행한다.
  •  
  • (요소의 소모) reduce(T identity, BinaryOperator<T> accumulator) : 처음 두 요소를 가지고 연산을 수행한 뒤, 그 결과와 다음 요소를 가지고 또다시 연산을 수행한다. 이런 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 반환한다. (인수로 초깃값(identity)를 전달하면 초깃값과 해당 스트림의 첫 번째 요소가 먼저 연산을 수행한다. 그리고 초깃값을 사용하냐 안하냐의 차이로 반환 타입이 바뀌니 유의하고 사용해야한다. (예제 참조))
  • (요소의 검색) findFirst() : 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환함.
  • (요소의 검색) findAny() : 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환함. (병렬 스트림일 때 주로 사용)
  • (요소의 검사) anyMatch(Predicate<? super T> predicate) : 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 반환한다.
  • (요소의 검사) allMatch(Predicate<? super T> predicate) : 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 반환한다.
  • (요소의 검사) noneMatch(Predicate<? super T> predicate) : 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 반환한다.
  • (요소의 통계) count() :  해당 스트림의 요소의 개수를 반환한다.
  • (요소의 통계) max(Comparator<? super T> comparator) : 해당 스트림의 요소 중에서 가장 큰 값을 가지는 요소를 참조하는 Optional 객체를 반환한다.
  • (요소의 통계) min(Comparator<? super T> comparator) : 해당 스트림의 요소 중에서 가장 작은 값을 가지는 요소를 참조하는 Optional 객체를 반환한다.
  • (요소의 연산) sum() : 해당 스트림의 모든 요소에 대해 합을 구하여 반환한다.
  • (요소의 연산) average() : 해당 스트림의 모든 요소에 대해 평균값을 구하여 반환한다.
  • (요소의 수집) collect(Collector<? super T,A,R> collector) : 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집한다. (Collector 클래스에는 미리 정의된 다양한 방법이 클래스 메서드로 정의되어 있으며 이 외에도 사용자가 직접 Collector 인터페이스를 구현하여 자신만의 수집 방법을 정의할 수 있다.)

▷ 예제 1 ) 스트림 최종 연산

public class Main {
    public static void main(String[] args) {
       
        // 각 요소를 소모하여 명시된 동작을 수행한다. (forEach)
        IntStream streamEach = IntStream.of(1, 2, 4, 3);
        streamEach.forEach(System.out::println);
        // 1 2 4 3
        
        // 첫번째와 두번째 요소를 가지고 연산 후 세번째 네번째 등 연속으로 수행 (reduce)
        Stream<String> streamReduce1 = Stream.of("넷", "둘", "셋", "하나");
        Optional<String> reduce1 = streamReduce1.reduce((s1, s2) -> s1 + "! " + s2);
        reduce1.ifPresent(System.out::println);
        // 넷! 둘! 셋! 하나
        
        // reduce의 연산 수행에 초깃값 부여
        Stream<String> streamReduce2 = Stream.of("넷", "둘", "셋", "하나");
        String reduce2 = streamReduce2.reduce("시작", (s1, s2) -> s1 + "! " + s2);
        System.out.println(reduce2);
        // 시작! 넷! 둘! 셋! 하나
        
        // 모든 요소를 정렬한 후 첫 번째에 위치한 요소를 출력 (findFirst)
        // 여기서 findAny()를 써도 동일한 결과가 나오나 
        // findAny() 메서드는 병렬 스트림인 경우에 사용해야만 정확한 연산 결과를 반환한다.
        IntStream streamFind = IntStream.of(4, 2, 7, 3, 5, 1, 6);
        OptionalInt findResult = streamFind.sorted().findFirst();
        // OptionalInt findResult = streamFind.sorted().findAny();
        System.out.println(findResult.getAsInt());
        // 1
        
        // 특정 요소를 만족하거나 만족하지 못하거나 (anyMatch, allMatch, noneMatch)
        IntStream streamMatch = IntStream.of(30, 90, 70, 10);
        System.out.println(streamMatch.anyMatch(n -> n > 80)); // true
        // System.out.println(streamMatch.allMatch(n -> n > 80)); // false
        // System.out.println(streamMatch.noneMatch(n -> n > 90)); // true
        
        // 요소의 통계를 뽑아낸다. (개수 : count / 최댓값 : max / 최솟값 : min)
        IntStream streamStat = IntStream.of(30, 90, 70, 10);
        System.out.println(streamStat.count()); // 4
        // System.out.println(streamStat.max().getAsInt()); // 90
        // System.out.println(streamStat.min().getAsInt()); // 10
        
        // 요소의 합을 구한다. (sum)
        IntStream streamSum = IntStream.of(30, 90, 70, 10);
        System.out.println(streamSum.sum()); // 200

        // 요소의 평균을 구한다. (average)
        DoubleStream streamAvg = DoubleStream.of(30.3, 90.9, 70.7, 10.1);
        System.out.println(streamAvg.average().getAsDouble()); // 50.5
        
        // 스트림을 리스트로 수집한다. (collect)
        Stream<String> stream = Stream.of("넷", "둘", "하나", "셋");
        List<String> list = stream.collect(Collectors.toList());
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            System.out.print(iter.next() + " ");
        }
    }
}

참고 : http://www.tcpschool.com/java/java_stream_concept

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

반응형

- 자바 람다 표현식(Lambda Expression)이란? -


Java 8에서 추가된 람다 표현식(Lambda Expression)함수형 인터페이스 그리고 메서드 참조에 대해 알아보자.


1. 람다식(Lambda Expression)

- 람다식이란 익명객체를 생성하기 위한 표현식을 말한다.

- 간단히 말해 메서드를 하나의 간결한 식으로 표현한 것이라고 할 수 있다.

- Java 8부터 사용 가능하며 람다 표현식을 사용하여 자바에서도 함수형 프로그래밍이 가능하게 되었다.

- 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높여준다.

1-1. 람다 표현식 작성하기

자바에서는 화살표(->) 기호를 사용하여 람다 표현식을 작성할 수 있다.

 

▷ 문법

(매개변수목록) -> {함수몸체}

 

▷ 예제 1 ) 기본 문법 예제

// 기존 방식
반환타입 메서드이름 (매개변수타입 변수){
	...
}

// 람다식
(매개변수타입 변수) -> {
	...
}

 

기본 문법에 대해 배워봤고, 그 다음에는 람다식에는 몇가지 특징이 존재하니 알고 넘어가자.

  • 메서드이름과 반환타입의 경우에는 생략할 수 있다.
  • 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있다. (대부분 생략이 가능하다.)
  • 매개변수가 하나인 경우에는 괄호(())를 생략할 수 있다.
  • 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호({})를 생략할 수 있다. (이때 세미콜론은 붙이지 않음)
  • 함수의 몸체가 하나의 return 문으로만 이루어진 경우에는 중괄호({})를 생략할 수 없다.
  • return문 대신 표현식을 사용할 수 있으며, 이때 반환값은 표현식의 결괏값이 된다. (이때 세미콜론은 붙이지 않음)

▷ 예제 2 ) 람다식 작성 예제

// 1. 기존 메서드
int min(int x, int y) {
    return x < y ? x : y;
}

// 2. 메서드명과 반환 타입 생략
(int x, int y) -> {
    return x < y ? x : y;
}

// 3. return문 대신 표현식 사용 그리고 중괄호의 생략
// 이때는 문장이 아니므로 끝에 세미콜론은 생략한다.
(int x, int y) -> x < y ? x : y

// 4. 매개변수의 타입이 추론이 가능한 경우 생략
(x, y) -> x < y ? x : y

 

1-2. 함수형 인터페이스 (Functional Interface)

람다식을 다루기 위한 인터페이스로, 람다 표현식을 하나의 변수에 대입할 때 사용하는 참조 변수의 타입을 함수형 인터페이스라고 부른다.

 

▷ 문법

참조변수의타입 참조변수의이름 = 람다 표현식

 

람다식은 메서드 그 자체로 볼게 아니라 익명 클래스의 객체와 동등하다고 볼 수 있다.

// 람다 표현식
(x, y) -> x < y ? x : y

// 익명 함수
new Object() {
    int min(int x, int y) {
        return x < y ? x : y;
    }
}

 

익명 객체의 메서드와 람다식의 매개변수, 반환값이 일치하면 익명 객체를 람다식으로 대체할 수 있다.

람다식으로 정의된 익명 객체의 메서드를 호출하려면 참조변수가 필요하다.

 

이 때, 참조변수의 타입은 클래스 또는 인터페이스가 가능한데, 람다식과 동등한 메서드가 정의되어 있어야한다.

여기서 우리는 함수형 인터페이스를 사용할 것이다.

 

▷ 예제 1 ) 함수형 인터페이스 사용

public class Main {
	// 1. 함수형 인터페이스의 선언
	@FunctionalInterface
	interface LambdaFunction{
		int min(int x, int y);
	}
	
	public static void main(String[] args) {
		LambdaFunction lambdaFunction = (x, y) -> x < y  ? x : y; 	// 2. 추상 메서드의 구현
		System.err.println(lambdaFunction.min(3, 4)); 	// 3. 함수형 인터페이스의 사용
	}
}

 

함수형 인터페이스는 추상클래스와 달리 단 하나의 추상 메서드만을 가져야한다. (하지만 default와 static 메서드의 개수에는 제약이 없다.)

함수형 인터페이스 라는걸 명시해 주지 않으면 일반 인터페이스로 인식해 추후 코드가 꼬여 에러가 발생할 수 있으나,

이를 방지하기 위해 @FunctionalInterface 를 선언해 주는 것이다.

 

@FunctionalInterface 어노테이션은 해당 인터페이스가 함수형 인터페이스라는것을 선언해주어 저렇게 명시된 함수형 인터페이스에 두 개 이상의 메서드가 선언되면 자바 컴파일러는 오류를 발생시킨다.

 

그리고 2.번으로 가게되면 함수형 인터페이스의 추상 메서드를 람다식을 사용해 간결하게 구현하였으며,

3번으로가 해당 함수형 인터페이스를 사용한 것이다.

 

자바에서는 기본적으로 4가지의 함수형 인터페이스를 지원하고있다.

  • Supplier<T> : 매개변수 없이 반환값 만을 갖는 함수형 인터페이스이다. (T get()를 추상 메서드로 가진다.)
  • Consumer<T>  : 객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스이다. (void accept(T t)를 추상메서드로 가진다.)
  • Function<T, R>  : T를 매개변수로 받아서 처리한 후 R로 반환하는 함수형 인터페이스이다. (R apply(T t)를 추상메서드로 가진다.)
  • Predicate<T> : T를 매개변수로 받아 처리한 후 Boolean을 반환한다. (Boolean test(T t)를 추상 메서드로 가진다.)

자세한 내용은 아래 오라클 공홈과 한글로 잘 설명해놓은 블로그를 참조하자.

 

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

 

java.util.function (Java Platform SE 8 )

Interface Summary  Interface Description BiConsumer Represents an operation that accepts two input arguments and returns no result. BiFunction Represents a function that accepts two arguments and produces a result. BinaryOperator Represents an operation u

docs.oracle.com

 

https://mangkyu.tistory.com/113

 

[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface) (2/5)

1. 람다식(Lambda Expression) 이란? Stream 연산들은 매개변수로 함수형 인터페이스(Functional Interface)를 받도록 되어있다. 그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다. 그렇기 때문

mangkyu.tistory.com

 

1-3. 메서드 참조 (Method Reference)

- 메서드 참조는 람다 표현식이 단 하나의 메서드만을 호출하는 경우에 해당 람다 표현식에서 불필요한 매개변수를 제거하고 사용할 수 있도록 도와준다.

- 함수형 인터페이스를 람다식이 아닌 일반 메서드를 참조시켜 선언하는 방법이다.

- 참조가능한 메서드는 일반 메서드, static 메서드, 생성자가 있다.

- 메서드 참조를 사용하면 람다식과 마찬가지로 함수형 인터페이스로 반환된다.

 

▷ 문법

클래스이름::메소드이름
참조변수이름::메소드이름

 

▷ 예제 1 ) static 메서드 참조 (Math.min)

public class Main {
	@FunctionalInterface
	interface LambdaFunction{
		int min(int x, int y);
	}
	
	public static void main(String[] args) {
		// 1. 람다식 사용
		LambdaFunction lambdaFunction = (x, y) -> Math.min(x, y);
		System.err.println(lambdaFunction.min(3, 4)); 
	   
		// 2.메서드 참조 사용
		LambdaFunction lambdaFunction = Math::min;
		System.err.println(lambdaFunction.min(3, 8)); 
	}
}

 

위 예제를 보면 알겠지만 람다식은 단순히 두개의 값을 Math.min() 메서드의 매개값으로 전달하는 역할만 하는것뿐 별다른 의미는 없다.

 

이럴때 함수형 인터페이스의 매개변수 타입, 개수, 반환형과 사용하려는 메서드의 매개변수 타입, 개수, 반환형이 일치한다면 2번 처럼 메서드 참조를 사용하여 이를 간결하게 표현할 수 있다.

 

▷ 예제 2 ) 일반 메서드 참조

Consumer<String> consumer = System.out::println; 
consumer.accept("Hello World!!");

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.forEach(System.out::println);

System.out.println() 메서드는 반환값이 없고, 파라미터로 String을 받는 메서드라 Consumer라는 함수형 인터페이스에 참조시킬 수 있다.

 

그리고 forEach는 함수형 인터페이스인 Consumer를 매개변수로 받기 때문에 System.out 메서드 참조를 사용할 수 있다.

 

▷ 예제 3 ) 생성자 참조

- 단순히 객체를 생성하고 반환하는 람다 표현식은 생성자 참조로 변환할 수 있다.

Supplier<String> supplier = () -> new String(); // 람다식 표현
Supplier<String> supplier = String::new; // 메서드 참조

Supplier<Object> supplier = () -> new Object(); // 람다식 표현
Supplier<Object> supplier = Object::new; // 메서드 참조

1-4. 기타 예제

▷ 예제 1 ) Thread 표현식

new Thread(new Runnable() {
    public void run() {
        System.out.println("전통적인 방식의 일회용 스레드 생성");
    }
}).start();
 
new Thread(()->{
    System.out.println("람다 표현식을 사용한 일회용 스레드 생성");
}).start();

 

▷ 예제 2 ) 람다 표현식과 메서드 참조의 비교

DoubleUnaryOperator oper;

oper = (n) -> Math.abs(n); // 람다 표현식
System.out.println(oper.applyAsDouble(-5));

oper = Math::abs; // 메소드 참조
System.out.println(oper.applyAsDouble(-5));

 

▷ 예제 3 ) 배열 생성 시 생성자 참조

Function<Integer, double[]> func1 = a -> new double[a]; // 람다 표현식
Function<Integer, double[]> func2 = double[]::new;      // 생성자 참조

참고 : http://www.tcpschool.com/java/java_lambda_concept

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

반응형

Array.prototype.every() 이라는 배열 메서드는 대상 배열에 존재하는 "모든"값이 특정 조건에 만족하는지 확인하는 메서드이다.

 

▷ 구문

arr.every(callback(curentValue, index, array)[, thisArg])

callback : function 안에서 조건을 체크하며, true/false를 반환한다.
curentValue: 현재 요소 (ex. 반복문의 현재 요소)
index : 현재 요소의 인덱스
array :대상 배열
thisArg : callback을 실행할 때 this로 사용하는 값.


▷ 예제1) Array.prototype.every() 기본 사용법

var arr = [0,1,2,3,4]

// result : false
arr.every(function(elem,index,arr){ 
    return elem < 4;
});

// result : true (es6)
arr.every((elem, index, arr) => elem < 5);

 

구문을 통해 Array.prototype.every() 메서드에 대해 알아봤었는데,

예제를 보면 알겠지만 해당 배열안에 있는 값들이 조건을 "모두" 통과해야만 true를 반환해준다.

 

false를 반환한 예제는 단 하나라도 조건과 일치하지 않았기 때문에 false를 반환한 것이다.


지금까지 Array.prototype.every() 메서드에 대해 알아봤는데,

every() 는 "모든" 값들의 조건 통과 여부를 본다면

"단 하나" 라도 만족하는지 여부를 체크하는 some()이라는 메서드 역시 존재한다.

https://mine-it-record.tistory.com/377

 

[JavaScript] arr.some() - 배열에 존재하는 값이 특정 조건을 하나라도 만족하는지 판별하는 함수 (ft.

Array.prototype.some() 이라는 배열 메서드는 대상 배열에 특정 조건에 만족하는게 하나라도 존재하는지 확인하는 메서드이다. ▷ 구문 arr.some(callback(curentValue, index, array)[, thisArg]) callback : ..

mine-it-record.tistory.com

같이 알아두면 좋을것 같다.


참고 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/every

 

Array.prototype.every() - JavaScript | MDN

every() 메서드는 배열 안의 모든 요소가 주어진 판별 함수를 통과하는지 테스트합니다. Boolean 값을 반환합니다.

developer.mozilla.org

반응형

ES6부터 새롭게 생겨난 반복문 for...of 에 대해 알아보고자 한다.

for...of 문반복가능한 객체(Array, Map, Set, String, TypedArray, arguments 객체 등)를 반복하는 문법이다.

 

▷ 구문

for (variable of iterable) {
    statement
}

variable: 각 반복에 서로 다른 속성값이 variable에 할당된다.

iterable : 반복되는 열거가능(enumerable)한 속성이 있는 객체.

 

예제를 통해 쉽게 접해보자.


▷ 예제1) Array에 대한 반복

let arr = [1, 2, 3];

for (let value of arr) {
  console.log(value); //1, 2, 3
}

 

▷ 예제2) String에 대한 반복

let str = 'mine';

for (let value of str) {
  console.log(value); //m, i, n, e
}

 

▷ 예제3) TypedArray에 대한 반복

let iterable = new Uint8Array([0x00, 0xff]);

for (let value of iterable) {
  console.log(value); // 0 , 255
}

 

▷ 예제4) Map에 대한 반복

let animals = new Map();
animals.set("dog", "woof")
       .set("cat", "meow")
       .set("elephant", "toot");

for (let name of animals.keys()) {
    console.log(name); // dog, cat, elephant
}

for (let howling of animals.values()) {
    console.log(howling); //woof, meow, toot
}

for (let [key, value] of animals) {// animals는 animals.entries()와 동일
    console.log(`${key} goes ${value}`);
    // dog goes woof , cat goes meow, elephant goes toot
}

 

▷ 예제5) Set에 대한 반복

let mineSet = new Set([0, 1, 2, 3]);

for (let key of mineSet.keys()) {
    console.log(key); // 0, 1, 2, 3
}

for (let value of mineSet.values()) {
    console.log(value); // 0, 1, 2, 3
}

for (let data of mineSet) {// mineSet mineSet.entries()와 동일
    console.log(data); // 0, 1, 2, 3
}

참고 : https://mine-it-record.tistory.com/473

 

[ES6] Map(), Set() 객체의 특징과 사용법

ES6에서 새롭게 등장한 자료구조인 맵(Map) 과 셋(Set) 객체에 대해 알아보자. 1. 맵(Map) - 맵(Map)은 키가 있는 데이터를 저장한다는 점에서 객체(obj)와 유사하다. - 객체는 키값으로 문자열만 사용 가

mine-it-record.tistory.com

참고 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/for...of

 

for...of - JavaScript | MDN

for...of 명령문은 반복가능한 객체 (Array, Map, Set, String, TypedArray, arguments 객체 등을 포함)에 대해서 반복하고 각 개별 속성값에 대해 실행되는 문이 있는 사용자 정의 반복 후크를 호출하는 루프를

developer.mozilla.org

반응형

ES6에서 새롭게 등장한 자료구조인 맵(Map) 과 셋(Set) 객체에 대해 알아보자.


1. 맵(Map)

- 맵(Map)은 키가 있는 데이터를 저장한다는 점에서 객체(obj)와 유사하다.

- 객체는 키값으로 문자열만 사용 가능하지만 맵(Map)은 다양한 자료형을 허한다.

- 객체는 삽입된 순서를 기억하지 못하지만 맵(Map)은 삽입된 순서를 기억해 반복문 사용시 삽입 순서대로 반된다.

 

1-1. 주요 메서드와 프로퍼티

맵(Map)에서 주요 사용되는 메서드와 프로퍼티는 다음과 같다.

  • new Map(iterable) : 맵 객체를 만든다. (매개변수로 [키, 값] 으로 이루어진 배열이나 이터러블 객체를 전달하면 그안의 값을 복사해 맵에 넣어준다.)
  • set(key, value) : key를 이용해 value를 저장한다.
  • get(key) : key에 해당하는 값을 반환한다. (key가 존재하지 않으면 undefined를 반환한다.)
  • has(key) : key가 존재하면 true, 존재하지 않으면 false를 반환한다.
  • delete(key) : key에 해당하는 값을 삭제한다. (key가 삭제되었으면 true, 키가 없는 등 삭제가 안됐으면 false 반환)
  • clear() : 맵(Map) 안의 모든 요소를 제거한다.
  • size : 맵 객체 안에 있는 요소의 개수를 반환한다.

▷ 예제 1 ) 주요 메서드와 프로퍼티 기본 사용법

let animals = new Map([
    ["wolf", "howling"]
]);

animals.set("dog", "woof");
// use set Chaining
animals.set("cat", "meow")
       .set("elephant", "toot");
       
console.log(animals);
// Map(4) {'wolf' => 'howling', 'dog' => 'woof', 'cat' => 'meow', 'elephant' => 'toot'}

animals.get("dog"); // woof
animals.get("fox"); // undefined

animals.size; // 4

animals.has("elephant"); // true
animals.has("bird"); // false

animals.delete("dog"); // true
animals.delete("bird"); // false

console.log(animals);
// Map(3) {'wolf' => 'howling', 'cat' => 'meow', 'elephant' => 'toot'}

animals.clear();
console.log(animals); // Map(0) {size: 0}

 

1-2. 맵의 요소 반복하기

맵에서는 세 가지 메서드를 통해 맵의 각 요소를 뽑아 반복 작업을 하거나, 배열처럼 내장 메서드인 forEach를 사용한다.

  • keys() : 각 요소의 를 모은 반복 가능한 iterable(이터러블) 객체를 반환한다.
  • values() : 각 요소의 을 모은 반복 가능한 iterable(이터러블) 객체를 반환한다.
  • entries() : 요소의 [키, 값]을 한 쌍으로 하는 iterable(이터러블) 객체를 반환한다.
  • forEach(callFuc(value, key, map)) : 배열과 유사하게 내장 메서드인 forEach 반복문을 지원한다.

▷ 예제 2 ) 반복을 위한 메서드의 기본 사용법

let animals = new Map();
animals.set("dog", "woof")
       .set("cat", "meow")
       .set("elephant", "toot");

for (let name of animals.keys()) {
    console.log(name); // dog, cat, elephant
}

for (let howling of animals.values()) {
    console.log(howling); //woof, meow, toot
}

for (let [key, value] of animals) {// animals는 animals.entries()와 동일
    console.log(`${key} goes ${value}`);
    // dog goes woof , cat goes meow, elephant goes toot
}

animals.forEach((value, key, map) => {
    console.log(`${key} : ${value}`);
    // dog : woof, cat : meow, elephant : toot
});

 

1-3. 객체를 맵으로, 맵을 객체로 변환하기

맵은 키가있는 데이터를 저장한다는 점에서 객체와 유사하다고 하였는데, 그래서 그런지 서로 변환이 가능하다.

 

▷ 예제 3 ) 객체를 맵으로 변환하기

let obj = {
    dog : "woof",
    cat : "meow",
    elephant : "toot"
}

let animals = new Map(Object.entries(obj));
console.log(animals);
// Map(3) {'dog' => 'woof', 'cat' => 'meow', 'elephant' => 'toot'}

 

▷ 예제 4 ) 맵을 객체로 변환하기

let animals = new Map();
animals.set("dog", "woof")
       .set("cat", "meow")
       .set("elephant", "toot");
       
let obj = Object.fromEntries(animals);
console.log(obj); //{dog: 'woof', cat: 'meow', elephant: 'toot'}

맵에서 객체로 변환할때는 모든 key값이 문자열로 바뀌니 주의해야한다.


2. 셋(Set)

- 셋(Set)은 중복을 허용하지 않는 값을 모아놓은 특별한 컬렉션이다.

- 셋(Set)은 키가 없는 값을 저장한다는 점에서 배열(array)과 유사하다.

2-1. 주요 메서드와 프로퍼티

셋(Set)에서 주요 사용되는 메서드와 프로퍼티는 다음과 같다.

  • new Set(iterable) : 셋 객체를 만든다. (배열이나 이터러블 객체를 전달하면 그 안의 값을 복사해 셋에 넣어준다.)
  • add(value) : 값을 추가한다.
  • delete(value) : 값을 제거한다. (삭제되었으면 true, 값이 없는 등 삭제가 안됐으면 false 반환)
  • has(value) : 셋(Set) 내에 값이 존재하면 true, 아니면 false를 반환한다.
  • clear() : 셋(Set) 안의 모든 요소를 제거한다.
  • size : 셋(Set) 객체 안에 있는 요소의 개수를 반환한다.

▷ 예제 1 ) 주요 메서드와 프로퍼티 기본 사용법

let mineSet = new Set([0]);

// use add Chaining
mineSet.add(1).add(5); // Set { 1, 5 }
mineSet.add(5); // Set { 1, 5 }
mineSet.add('some text'); // Set { 1, 5, 'some text' }
let o = {a: 1, b: 2};
mineSet.add(o);
mineSet.add({a: 1, b: 2}); // o와 다른 객체를 참조하므로 괜찮음
console.log(mineSet);
// Set(6) {0, 1, 5, 'some text', {a: 1, b: 2}, {a: 1, b: 2}}

mineSet.has(1); // true
mineSet.has(o); // true
mineSet.has(3); // false

mineSet.size; // 6

mineSet.delete(5); // true
mineSet.delete(3); // false

console.log(mineSet);
// Set(5) {0, 1, 'some text', {a: 1, b: 2}, {a: 1, b: 2}}

mineSet.clear();
// Set(0) {size: 0}

 

2-2. 셋(Set)의 요소 반복하기

셋 역시도 맵과 마찬가지로 세가지의 반복 가능한 요소롤 뽑아내는 메서드와 forEach 메서드가 존재한다.

  • keys() : 셋 내의 모든 값을 포함하는 반복 가능한 iterable(이터러블) 객체를 반환한다.
  • values() : keys()와 동일한 작업을 한다. (맵과의 호환성을 위해 만들어진 메서드다.)
  • entries() : 셋 내의 각 값을 이용해 만든 [value, value] 배열을 포함하는 이터러블 객체를 반환한다. (맵과의 호환성을 위해 만들어진 메서드다.)
  • forEach(callFuc(value, valueAgain, set)) : 내장 메서드인 forEach 반복문이다. (맵과의 호환성을 위해 콜백 변수가 3개이며, 두번째 valueAgain 역시 첫번째 value 와 동일한 값을 반환하다.)

▷ 예제 2 ) 반복을 위한 메서드의 기본 사용법

let mineSet = new Set([0, 1, 2, 3]);

for (let key of mineSet.keys()) {
    console.log(key); // 0, 1, 2, 3
}

for (let value of mineSet.values()) {
    console.log(value); // 0, 1, 2, 3
}

for (let data of mineSet) {// mineSet mineSet.entries()와 동일
    console.log(data); // 0, 1, 2, 3
}

mineSet.forEach((value, valueAgain, set) => {
    console.log(`${value} : ${valueAgain}`);
    // 0 : 0, 1 : 1, 2 : 2, 3 : 3
});

 

2-3. 배열을 셋으로, 셋을 배열로 변환하기

셋은 배열과 유사하다고 말했었는데, 서로 상호작용하는것에 대해 배워보자.

 

▷ 예제 3 ) 배열을 셋으로 변환하기

const array = [0, 1, 2, 3];

let mineSet = new Set(array);
console.log(mineSet); // Set(4) {0, 1, 2, 3}

 

▷ 예제 4 ) 셋을 배열로 변환하기

let mineSet = new Set([0, 1, 2, 3]);

// 1. 전개 연산자 사용
const arr1 = [...mineSet];
console.log(arr1); // [0, 1, 2, 3]

// 2. Array.from 사용
const arr2 = Array.from(mineSet);
console.log(arr2); // [0, 1, 2, 3]

참고 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Keyed_collections

 

키기반의 컬렉션 - JavaScript | MDN

이번 장에서는 입력된 키값을 기준으로 정렬되는 데이터의 집합(자료 구조)에 대해 소개 할 것이다. Map과 Set은 입력된 순서대로 반복적으로 접근 가능한 요소들을 포함하고 있다. 

developer.mozilla.org

참고 : https://ko.javascript.info/map-set

 

맵과 셋

 

ko.javascript.info

반응형

비동기 처리를 도와주는 async와 await에 대해 알아보자.

 

기존의 비동기 처리 방식인 콜백 함수와  프로미스(Promise)의 단점을 보완하고 개발자가 일기 좋은 코드를 작성할 수 있게 도와주는 문법인데, 제목에서 말한것처럼 Promise의 syntactic sugar라고 보면 된다.

 

기존에 없던게 아니고 그냥 좀더 Promise를 사용하기 쉽게 도와주는 문법이라 생각하면 될 것 같다.

(object와 class의 관계랄까...?)

 

코드를 통해 하나씩 알아가보자.


1. async 사용법

// 1.함수로 만들어진 기본 promise 문법 (async x)
function blog() {
    return new Promise((resolve, reject) => {
        resolve('mine-it-record');
    });
}

const bloger = blog();
bloger.then(result => console.log(result));

// 2. async를 사용하여 위 코드를 변경하기 (async o)
async function blog() {
    return 'mine-it-record';
}

const bloger = blog();
bloger.then(result => console.log(result));

 

위 예제를 보면 알겠지만 일반 Promise 문법을 사용했을때와 마찬가지로 아래 async로 이루어진 메서드 역시 동일한 방식으로 then을 통해 제어하는 것을 볼 수 있는데,

이는 본문 시작부분에서 얘기했던것처럼 async는 Promise의 syntactic sugar 이기 때문에 Promise 문법을 좀 더 사용하기 쉽게 도와주는 역할을 해주기 때문이다.

 

그래서 위 예제를 직접 실행해보면 2번 예제 역시 Promise를 반환해 주는것을 볼 수 있다.

 

2. await 사용법

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function getMine(){
    await delay(1000);
    return 'mine';
}

async function getIt(){
    await delay(1000);
    return 'it';
}

async function createBlogName(){
    const mine = await getMine();
    const it = await getIt();

    return `${mine}-${it}-record`;
}

createBlogName().then(result => {
    console.log(result);
});

await는 async와 꼭 같이 사용해야한다. (async 설정이 안되어있는 일반 함수에서는 사용이 불가능하다.)

 

await는 말 그대로 "기다려" 라는 의미인데,

await가 사용된 비동기 통신이 완료될때까지 기다린 다음에야 다음으로 넘어간다.

 

위 예제로 설명을 해보자면 각각 delay나 getMine, getIt에 await가 사용되었는데,

그 메서드들이 완전히 실행이 완료되어야 다음 줄이 실행된다는 의미이다.

 

await를 쓰는 가장 큰 이유는 콜백지옥 때문인데, then 역시도 남발하면 콜백지옥과 별다를게 없기 때문에 await를 사용해주면 가독성이 더 좋은 코드가 완성된다.

 

3. async & await 예외 처리

...

async function createBlogName(){
    try {
        const mine = await getMine();
        const it = await getIt();
    } catch (error) {
        console.log(erro);
    }

    return `${mine}-${it}-record`;
}

...

이런식으로 가볍게 try catch문을 사용해서 잡아주면 된다.


참고 : https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous/Async_await

 

async와 await를 사용하여 비동기 프로그래밍을 쉽게 만들기 - Web 개발 학습하기 | MDN

Javascript에 대한 최신 추가 사항은 ECMAScript 2017 JavaScript 에디션의 일부인 async functions 그리고 await 키워드는 ECMAScript2017에 추가되었습니다. 이 기능들은 기본적으로 비동기 코드를 쓰고 Promise를

developer.mozilla.org

 

반응형