- 자바 스트림(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

반응형

- 변수 타입에 따른 기본값 (ft. 기본형 참조형) -


자바에서는 변수를 선언할 경우 초기화를 하지 않더라도 변수의 타입별로 기본값이 존재하는데,

이는 컴파일러(Compiler)에 의해 초기화가 되는 값이니

실제로 코드를 작성한다면 직접적으로 값을 할당하여 초기화 시켜주는게 좋다.

 

그리고 지역변수는 초기값이 들어가지 않기 때문에 여러모로 혼란스러울수 있으니 꼭 값을 할당해서 초기화 시켜주자.

그래도 일단 궁금하니 변수 타입에 따른 기본값은 아래 표로 정리해둔다.

 

▷ 변수 타입에 따른 기본값

자료형(변수 타입) 기본값
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char '\n0000'
boolean false
참조형 변수(String or any Object) null

 

▶ 코드 테스트

import java.util.Map;

public class Test {

	static byte byteT;
	static short shortT;
	static int intT;
	static long longT;
	static float floatT;
	static double doubleT;
	static char charT;
	static boolean booleanT;
	static String stringT;
	static Map mapT;
	
	public static void main(String[] args) {
		System.out.println(byteT); //0
		System.out.println(shortT); //0
		System.out.println(intT); //0
		System.out.println(longT); //0
		System.out.println(floatT); //0.0
		System.out.println(doubleT); //0.0
		System.out.println(charT); //
		System.out.println(booleanT); //false
		System.out.println(stringT); //null
		System.out.println(mapT); //null
	}

}

▶ 결과

 

반응형

- json-simple을 사용한 JSON 파일 READ / WRITE -


JAVA에서 JSON을 파일로 만들거나 기존에 있는 JSON 파일을 읽는 방법에 대해 알아보자.

우선 JSON을 다루기 위해 json-simple 라이브러리를 사용할 것인데

 

maven 설정을 해주어야 한다.

 

▷ json-simple 설정

<dependency>
	<groupId>com.googlecode.json-simple</groupId>
	<artifactId>json-simple</artifactId>
	<version>1.1</version>
</dependency>

https://mvnrepository.com/artifact/com.googlecode.json-simple/json-simple/1.1.1

 

maven을 사용하기 싫거나 maven 환경이 아니라면 .jar 파일을 직접 받아서 사용해주면 된다.

https://code.google.com/archive/p/json-simple/downloads

 

Google Code Archive - Long-term storage for Google Code Project Hosting.

 

code.google.com

 

이제 json-simple 환경이 잡혔다면 코드 예제를 통해 접근해보자.


▷ JSON 파일 쓰기(Write) / 파일로 저장하기

import java.io.FileWriter;
import java.io.IOException;

import org.json.simple.JSONObject;

public class JsonSimple {

	public static void main(String[] args) {
		
		JSONObject obj = new JSONObject();
		obj.put("name", "mine-it-record");
		obj.put("mine", "GN");
		obj.put("year", 2021);

		try {
			FileWriter file = new FileWriter("c:/mine_data/mine.json");
			file.write(obj.toJSONString());
			file.flush();
			file.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.print(obj);

	}

}

 

▶결과

JSONObject 형식의 파일을 FileWriter를 가지고 저장하는 원리이다.

JSONObject 안에 배열이나 이런걸 넣어도 잘 저장될 것이다.

 

▷ JSON 파일 읽기(Read)

package test.my.only;

import java.io.FileReader;
import java.io.IOException;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

public class JsonSimple {

	public static void main(String[] args) {
		
		JSONParser parser = new JSONParser();

		try {
			FileReader reader = new FileReader("c:/mine_data/mine.json");
			Object obj = parser.parse(reader);
			JSONObject jsonObject = (JSONObject) obj;
			
			reader.close();
			
			System.out.print(jsonObject);
			
		} catch (IOException | ParseException e) {
			e.printStackTrace();
		}

	}
	
}

 

▶결과

{"mine":"GN","year":2021,"name":"mine-it-record"}

 

FileReader 로 JSON 파일을 읽은 뒤 파일을 simle-json에서 제공하는 JSONParser를 사용하여 파싱 해준것이다.

알고보면 참 쉬운 JSON 파일의 읽고 쓰는 방법이다.

반응형

- 자바 특정 파일 이동 및 이름 변경하기 -


Java File 클래스에서 제공해주고 있는 renameTo() 함수 그리고 몇가지 방법을 통해

파일 이동 및 이름 변경에 대해 알아보자.

 

사실 파일 이름이라고 하면 경로를 포함하고 있기 때문에 rename과 move를 동일하게 보고있다.

 

1. File 클래스 renameTo() 사용

File oldfile = new File("c:/mine_data/mine(1).txt");
File newfile = new File("c:/mine_data/it/mine.txt");

if(oldfile.renameTo(newfile)){
	System.out.println("File rename success");
}else{
	System.out.println("File rename fail");
}

 

사용 방법은 간단하다.

변경전 파일(oldfile)새로운 경로 및 이름을 가진 파일(newfile)로 renameTo() 해준것이다.

 

renameTo() 함수는 boolean값을 return 해주므로 성공여부를 확인할 수 있으며,

구글링을 해볼 경우 renameTo()의 경우 실패하는 경우가 생길수 있어 성공여부에 따라

복사하고 삭제해주는 로직을 추가해주고는 한다.

 

보통 윈도우 환경에서 알수없는 이유로 실패하는 경우가 생길수도 있다하니 참고하자.

 

2. Path 클래스와 Files 사용 (Java 7 이상)

try {
	Path oldfile = Paths.get("c:/mine_data/mine(1).txt");
	Path newfile = Paths.get("c:/mine_data/it/mine.txt");
	Files.move(oldfile, newfile);
} catch (IOException e) {
	e.printStackTrace();
}

Java7 이상에서 제공하는 Files 와 Path 클래스를 이용하여 renameTo()와 동일하게 파일 이름 변경 또는 이동을 시킨다.

 

Files.move 또는 Files.copy에서는 추가적으로 옵션을 줄 수 있는데 다음과 같은 기본 옵션이 존재한다.

ATOMIC_MOVE : move 함수 전용 옵션, 원자적 이동 보장
COPY_ATTRIBUTES : 모든 파일 속성(File Attributes)을 복사
REPLACE_EXISTING : 파일이 이미 존재하면 파일을 덮어씀

 

▷옵션 사용법

try {
	Path oldfile = Paths.get("c:/mine_data/mine(1).txt");
	Path newfile = Paths.get("c:/mine_data/it/mine.txt");
	Files.move(oldfile, newfile, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
	e.printStackTrace();
}

 

3. Commons IO 사용하기

3번 방법은 사용하기에 앞서 설정이 필요한데, repository를 하나 받아야한다.

아래 링크로 들어가 jar를 받거나, maven 설정들을 해주자. (최신 버전이 부담스러우면 낮춰서 사용해도 된다.)

https://mvnrepository.com/artifact/commons-io/commons-io/2.11.0

 

설정이 완료되었다면 코드를 살펴보자.

 

▷파일 대상 함수

try {
	File oldfile = FileUtils.getFile("c:/mine_data/mine(1).txt");
	File newfile = FileUtils.getFile("c:/mine_data/it/mine.txt");
	FileUtils.moveFile(oldfile, newfile);
} catch (IOException e) {
	e.printStackTrace();
}

 

▷폴더 대상 함수

try {
	File file = FileUtils.getFile("c:/mine_data/it");
	File fileToMove = FileUtils.getFile("c:/mine_data/mine/its");
	FileUtils.moveDirectory(file, fileToMove);
} catch (IOException e) {
	e.printStackTrace();
}

 

FileUtils를 사용할때는 파일과 폴더를 대상으로 하는 함수가 다른데 파일을 대상으로 할때는 moveFile(), 폴더를 대상으로 할때moveDirectory()함수를 사용해야한다.

 

여기서 알아둬야할것은 위 두 함수는 경로 중간에 폴더가 없을경우 폴더를 만들어서 이동시켜주는데

 

만약 존재하지 않는 폴더는 생성하고 싶지 않다면 moveFileToDirectory(src, des, createDestDir) 함수를 사용하면된다.

 

try {
	File file = FileUtils.getFile("c:/mine_data/it");
	File fileToMove = FileUtils.getFile("c:/mine_data/mine/its");
	FileUtils.moveFileToDirectory(file, fileToMove, false);
} catch (IOException e) {
	e.printStackTrace();
}

 

createDestDir 옵션은 boolean 값 true, false 를 통해 경로상의 폴더를 생성할지 말지 설정이 가능하다.

물론 false일 경우에는 경로가 없으면 exception이 발생한다.

반응형

- 특정 디렉토리 파일 목록 가져오기 -


Java에서 File 클래스를 사용해 특정 경로의 파일 리스트를 가져오는 방법에 대해 알아보자.

File 클래스가 기본적으로 제공해주는 함수이다.

- list() : 디렉토리에 있는 파일들의 이름 목록을 반환

- list(FilenameFilter filter) : 디렉토리에 있는 파일들의 이름을 필터링한 이름 목록을 반환

- listFiles() : 디렉토리에 있는 파일 목록을 반환

- listFiles(FileFilter filter) : 디렉토리에 있는 파일들을 필터링한 파일 목록을 반환

- listFiles(FilenameFilter filter) : 디렉토리에 있는 파일들의 이름을 필터링한 파일 목록을 반환


코드로 살펴보기에 앞서 접근하기에 앞서 현재 파일구조에 대해 설명하자면

 

이런식으로 구성되어있다는 것을 기준으로 코드를 살펴보자.

 

1. 파일 이름 목록 반환 - list()

String DATA_DIRECTORY = "c:/mine_data/";
File dir = new File(DATA_DIRECTORY);

String[] filenames = dir.list();
for (String filename : filenames) {
	System.out.println("filename : " + filename);
}

▷결과

filename : it
filename : mine.txt
filename : mine_it.zip
filename : record

 

list() 함수는 해당 경로의 파일 목록을 가져오는데 이름만 가져오는것이다.

그래서 String[] 로 return 되는 것이다.

 

2. 필터링된 파일 이름 목록 반환 - list(FilenameFilter filter)

String DATA_DIRECTORY = "c:/mine_data/";
File dir = new File(DATA_DIRECTORY);

FilenameFilter filter = new FilenameFilter() {
  public boolean accept(File f, String name) {
  	//파일 이름에 "mine"가 붙은것들만 필터링
  	return name.contains("mine");
  }
};

String[] filenames = dir.list(filter);
for (String filename : filenames) {
	System.out.println("filename : " + filename);
}

▷결과

filename : mine.txt
filename : mine_it.zip

 

FilenameFilter를 통해 필터링을 하는데 파일을 탐색할 때마다 accept(File f, String name)가 호출된다.여기서 File f는 파일의 부모 디렉토리, 즉 위 예제 코드에서는 "c:/mine_data"를 의미한다.

 

그리고 name은 말그대로 파일 이름을 의미하기때문에 이 name을 가지고 조건을 걸어주어 필터링 시켜주면 된다.

 

3. 파일 목록 반환 - listFiles()

String DATA_DIRECTORY = "c:/mine_data/";
File dir = new File(DATA_DIRECTORY);

File files[] = dir.listFiles();
for (File file : files) {
	System.out.println("file : " + file);
}

▷결과

file : c:\mine_data\it
file : c:\mine_data\mine.txt
file : c:\mine_data\mine_it.zip
file : c:\mine_data\record

 

listFiles() 함수는 파일 목록을 가져오는거라 해당 디렉토리의 하위 파일 목록을 가져오는것이다.

해당 경로의 하위 파일들만 가져오기때문에 하위 폴더 안에있는 또다른 파일 목록들을 가져오고 싶다면 추가적인 코딩이 필요하다.

 

4. 필터링된 파일 목록 반환(1) - listFiles(FileFilter filter)

String DATA_DIRECTORY = "c:/mine_data/";
File dir = new File(DATA_DIRECTORY);

FileFilter filter = new FileFilter() {
	public boolean accept(File f) {
		return f.getName().endsWith("zip");
	}
};
        
File files[] = dir.listFiles(filter);
for (File file : files) {
	System.out.println("file : " + file);
}

▷결과

file : c:\mine_data\mine_it.zip

 

FilenameFilter 와 마찬가지로 파일마다 accept가 실행되는데 FilenameFilter 에서의 File이 의미하는건 부모 경로라고 한다면

FileFilter 에서의 File은 해당 File을 그대로 의미하기때문에 위 코드처럼 File이 가지는 속성을 가지고 필터링이 가능하다.

 

5. 필터링된 파일 목록 반환(2) - listFiles(FilenameFilter filter)

String DATA_DIRECTORY = "c:/mine_data/";
File dir = new File(DATA_DIRECTORY);

FilenameFilter filter= new FilenameFilter() {
  public boolean accept(File f, String name) {
  	return name.contains("mine");
  }
};
        
File files[] = dir.listFiles(filter);
for (File file : files) {
	System.out.println("file : " + file);
}

▷결과

file : c:\mine_data\mine.txt
file : c:\mine_data\mine_it.zip

 

list() 에서 사용한 FilenameFilter와 동일하게 작동한다.

 

반응형