티스토리 뷰

[출처]



  람다식과 함수 스트림과 같은 새 기능의 변화가 Java 8에서 두드러졌기에 이에 관련한 수많은 소개 글과 학습가이드가 제공된다. 그러나 이 외에도 JDK 8 API에는 기존하던 클래스들에도 상당히 유용한 기능 향상이 있었다.


  본 글에서는 Java 8 API에서 나타난 이러한 작은 변화들에 대해서 소개하고자 한다. 각각을 쉬운 예제와 함께 살펴보자. 고로 Strings, Numbers, Math 그리고 Files에 대해 깊이 알아봅세.


Slicing Strings


  join()과 chars라는 새 메서드가 추가되었다. 첫번째 메서드는 여러 문자열을 단 하나의 문자열로 치환시킨다. 이때 인자로 두어진 구분자(delimiter)를 이용한다.


String.join(":", "foobar", "foo", "bar");
// => foobar:foo:bar


  두번째 메서드인 chars는 한 문자열의 스트림을 생성한다. 그렇게 때문에 이러한 문자열셋에 대해 스트림 연산을 수행할 수 있다.


"foobar:foo:bar"
    .chars()
    .distinct()
    .mapToObj(c -> String.valueOf((char)c))
    .sorted()
    .collect(Collectors.joining());
// => :abfor


  비단 일반 문자열 뿐만 아니라 정규패턴 또한 이제 스트림의 기능의 해택을 볼 수 있다. 각각의 문자(charactor)에 대한 스트림에 대해 문자열을 쪼개(split)는 대신에 특정 패턴에 해당하는 문자열만 쪼갤 수도 있다. 그리고 이에 대한 추가적인 스트림을 사용할 수도 있다.


Pattern.compile(":")
    .splitAsStream("foobar:foo:bar")
    .filter(s -> s.contains("bar"))
    .sorted()
    .collect(Collectors.joining(":"));
// => bar:foobar


  추가적으로 predicate로 정규 패턴을 넘길 수 있다. 한 예로 이 predicate는 문자열의 스트림에서 필터로 사용될 수 있다.


Pattern pattern = Pattern.compile(".*@gmail\\.com");
Stream.of("bob@gmail.com", "alice@hotmail.com")
    .filter(pattern.asPredicate())
    .count();
// => 1


  위에서 @gmail.com으로 끝나는 모든 문자열만을 받아들이는 패턴을 생성하고 이를 Java 8의 Predicate로 사용하였다. 목적은 응당 이메일을 필터링하기 위해서일터.


Crunching Numbers


  Java 8에서 unsigned 숫자를 다루기 위한 기능이 추가되었다. Java에서 숫자는 항상 부호가 있는 자료형이었는데 기쁜 소식이다. Integer를 예로 살펴보자.


  int 자료형은 최대 2³²를 이진수로 표현할 수 있다. 그러나 Java에서는 기본적으로 부호가 있는 자료형이므로(signed) 최상단 비트는 부호 비트가 된다(0이면 양수, 1이면 음수).  그렇기에 표현 가능한 최대 양수값은 2³¹ -1이었다.


  이 값은 손쉽게 Integer.MAX_VALUE 상수값을 통해 얻을 수 있다.


System.out.println(Integer.MAX_VALUE);      // 2147483647
System.out.println(Integer.MAX_VALUE + 1);  // -2147483648


  자바 8에서는 unsigned int를 변환하기 위한 기능을 제공한다. 아래와 같다.


long maxUnsignedInt = (1l << 32) - 1;
String string = String.valueOf(maxUnsignedInt);
int unsignedInt = Integer.parseUnsignedInt(string, 10);
String string2 = Integer.toUnsignedString(unsignedInt, 10);


  예제 코드에서 알 수 있듯이, 이제 가능한 unsigned int의 최대값을 얻는 것이 가능해졌다. 그리고 이를 다시 Integer에서 그 값을 갖는 문자열(string)로 변환이 가능하다.


  이는 기존의 parseInt로는 불가능했다. 아래의 코드가 이를 증명한다.


try {
    Integer.parseInt(string, 10);
}
catch (NumberFormatException e) {
    System.err.println("could not parse signed int of " + maxUnsignedInt);
}10);


  그 최대값을 넘어가면 signed로 표현이 불가했기 때문에 예외가 발생했었다.


Do the Math


  유틸리티 클래스인 Math 또한 숫자의 오버플로우를 다루는 서너개의 새로운 메서드들을 탑재하며 강화되었다. 무슨 소린고 하니, 지금까진 모든 숫자 자료형에는 표현 가능한 최댓값이 있었는데, 수학 연산의 결과가 자료형의 크기에 맞지 않았다면 무슨 일이 있었는지를 기억하는가?


System.out.println(Integer.MAX_VALUE);      // 2147483647
System.out.println(Integer.MAX_VALUE + 1);  // -2147483648


  코드서 보이듯, Integer Overflow라 부르는 이 현상은 정상적이지 않으며 의도한 동작이 아니다.


  Java 8 에서 이러한 문제를 다루기 위해 엄격한 지원 기능을 추가했다. Math는 exact로 끝나는 메서드들을 추가했다. 가령 addExact와 같은 식이다.

  이러한 메서드들은 overflow를 만났을 때 그 결과값이 주어진 숫자 자료형 크기에 알맞지 아니하면 ArithmeticException 예외를 던진다.


try {
    Math.addExact(Integer.MAX_VALUE, 1);
}
catch (ArithmeticException e) {
    System.err.println(e.getMessage());
    // => integer overflow
}


  같은 예외는 long 자료형을 int로 toIntExact 메서드를 통해 변환을 시도할 때도 던져진다.


try {
    Math.toIntExact(Long.MAX_VALUE);
}
catch (ArithmeticException e) {
    System.err.println(e.getMessage());
    // => integer overflow
}


Working with Files


  Files 유틸리티는 Java 7에서 NIO의 부속으로 처음 도입되었다. 이후 JDK 8 API 에서 파일에 대해 함수형 스트림을 사용할 수 있는 메서드가 추가되었다. 이것들 살펴봐야 하지 않겠는가.


 Listing files


  Files.list 메서드는 주어진 디렉터리의 모든 경로(path)를 스트림한다. 따라서 filter나 sorted와 같은 스트림 연산을 사용할 수 있다. 그 대상은 파일 시스템의 내용들이다.


try (Stream<Path> stream = Files.list(Paths.get(""))) {
    String joined = stream
        .map(String::valueOf)
        .filter(path -> !path.startsWith("."))
        .sorted()
        .collect(Collectors.joining("; "));
    System.out.println("List: " + joined);
}


  위 예제에선 현재 작업 디렉터리 내의 모든 파일을 훑는다. 그리고 각각의 경로를 각자의 문자 표현식(valueOf 메서드)으로 맵핑한다. 그 결과는 필터되고, 정렬되고 마지막으로 합쳐진(joined) 하나의 문자열이다. 만약 함수형 스트림에 익숙하지 않다면 Java 8 Stream Tutorial 을 읽어보길 권한다.


  눈치가 빠른 자라면 스트림의 생성이 try/with 문장으로 감싸져 있다는 것을 깨달았을 것이다. Stream들은 AutoCloseable를 구현하고 있으며 이 경우 반드시 명시적으로 스트림을 닫아 주어야만 한다. 이는 뒷쪽에서 IO 연산이 수행되기 때문이다.


The returned stream encapsulates a DirectoryStream. If timely disposal of file system resources is required, the try-with-resources construct should be used to ensure that the stream's close method is invoked after the stream operations are completed.

 이는 관련 설명이다.


Finding files


  다음은 한 디렉터리 혹은 그 하위 디렉터리에서 어떻게 파일들을 찾는지를 보여준다.


Path start = Paths.get("");
int maxDepth = 5;
try (Stream<Path> stream = Files.find(start, maxDepth, (path, attr) ->
        String.valueOf(path).endsWith(".js"))) {
    String joined = stream
        .sorted()
        .map(String::valueOf)
        .collect(Collectors.joining("; "));
    System.out.println("Found: " + joined);
}


  find 메서드는 인자 3개를 받는다. 첫째는 start는 시작 경로이며 둘째는 maxDepth로 말 그대로 탐색할 최대 폴더 깊이를 의미한다. 마지막 세 번째는 탐색 로직을 결정하는 람다식이 들어가며 타입은 predicate이다. 위 예에서 모든 .js로 끝나는 javascript파일을 찾았다.


  Files.walk 메서드를 통해 동일한 작업을 할 수도 있다. 탐색 로직 predicate를 인자로 전달하지 않으며 모든 파일들을 순회한다.


Path start = Paths.get("");
int maxDepth = 5;
try (Stream<Path> stream = Files.walk(start, maxDepth)) {
    String joined = stream
        .map(String::valueOf)
        .filter(path -> path.endsWith(".js"))
        .sorted()
        .collect(Collectors.joining("; "));
    System.out.println("walk(): " + joined);
}


  이 예에서는 스트림 연산 중 filter를 사용했으며 이 로직 덕분에 상위의 예제와 동일한 결과를 발한다.


 Reading and writing files


  메모리에 텍스트 파일을 읽어들이고 텍스트 파일에 문자열을 쓰는 작업은 Java 8에서 최종적으로 매우 단순한 작업이 됐다. Reader와 Writer 외에 다른 코드는 필요 없다. Files.readAllLines 메서드는 주어진 파일의 모든 줄을 읽어서 문자열의 리스트를 만든다. 이 리스트를 변경한 뒤 Files.write를 통해 다른 파일에 쓰면(write) 손쉽게 파일을 변경할 수 있다.


List<String> lines = Files.readAllLines(Paths.get("res/nashorn1.js"));
lines.add("print('foobar');");
Files.write(Paths.get("res/nashorn1-modified.js"), lines);


  단, 이 방법은 메모리 면에서 효율적이지 않다는 것을 명심하길 바란다. 모든 파일이 메모리에 적재될 것이기 때문이다. 더 큰 파일을 대상으로 하면 할 수록 힙 메모리 사이즈는 더욱 더 점유된다.


  따라서 이를 대신해 효율적인 메모리 사용을 위한 방법으로 Files.lines 메서드를 이용할 수 있다. 한번에 메모리에 모든 줄(line)을 메모리에 적재하는 대신 함수형 스트림 방식으로 한줄 한줄씩 읽어들인다.


try (Stream<String> stream = Files.lines(Paths.get("res/nashorn1.js"))) {
    stream
        .filter(line -> line.contains("print"))
        .map(String::trim)
        .forEach(System.out::println);
}


  더욱 정교한 작업이 필요하다면야 새로운 완충기(?;Buffred reader)를 사용할 수 있다.


Path path = Paths.get("res/nashorn1.js");
try (BufferedReader reader = Files.newBufferedReader(path)) {
    System.out.println(reader.readLine());
}


  또는 파일에 쓸 경우를 대비하여 Buffered Wrtier를 대신 생성해 사용할 수도 있다.


Path path = Paths.get("res/output.js");
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
    writer.write("print('Hello World');");
}


  Buffered reader들 역시 함수형 스트림에 접근이 가능하다. lines 메서드로 이 buffered reader에 의해 점유된 파일의 모든 줄 각각에 대해 함수형 스트림을 생성할 수 있다.


Path path = Paths.get("res/nashorn1.js");
try (BufferedReader reader = Files.newBufferedReader(path)) {
    long countPrints = reader
        .lines()
        .filter(line -> line.contains("print"))
        .count();
    System.out.println(countPrints);
}


  이렇게 Java 8이 제공하는 3 개의 간단한 텍스트 파일의 줄 단위 읽기-쓰기 방법을 살펴봤다. 텍스트 파일을 다루는 것은 꽤나 편리하다.


  하지만 반드시 명시적으로 try/with 구문을 통해 파일 스트림을 닫아주어야만 한다. 위는 try 때문에 코드가 다소 어수선한 편이다만 필요하다. 본인은 count나 collect와 같은 종료 연산을 수행했을 때 자동적으로 파일 스트림을 닫아주는 기능이 나오기를 기대한다. 동일한 스트림에 대해 종료 연산을 두번 호출할 수는 없으니 가능하다 생각하기 때문이다.


(본 글은 마무리 글로 끝난다. 링크가 걸려 있으니 이를 명시하겠다)

코드는 [여기]서 볼 수 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함