이미 Java 8로 업그레이드해야 하는 이유

게시 됨: 2022-03-11

Java 플랫폼의 최신 버전인 Java 8이 출시된 지 1년이 넘었습니다. 많은 회사와 개발자가 여전히 이전 버전으로 작업하고 있으며, 한 플랫폼 버전에서 다른 플랫폼 버전으로 마이그레이션하는 데 많은 문제가 있기 때문에 이해할 수 있습니다. 그럼에도 불구하고 많은 개발자들은 여전히 ​​이전 버전의 Java로 애플리케이션을 시작하고 있습니다. Java 8이 언어에 몇 가지 중요한 개선 사항을 가져왔기 때문에 이렇게 하는 데 합당한 이유는 거의 없습니다.

Java 8에는 많은 새로운 기능이 있습니다. 가장 유용하고 흥미로운 몇 가지 기능을 보여드리겠습니다.

  • 람다 표현식
  • 컬렉션 작업을 위한 스트림 API
  • CompletableFuture 를 사용한 비동기 작업 연결
  • 새로운 시간 API

람다 표현식

람다 는 향후 실행을 위해 한 번 이상 참조하고 다른 코드 조각으로 전달할 수 있는 코드 블록입니다. 예를 들어, 다른 언어의 익명 함수는 람다입니다. 함수와 마찬가지로 람다는 실행 시 인수를 전달받아 결과를 수정할 수 있습니다. Java 8에는 람다를 만들고 사용하기 위한 간단한 구문을 제공하는 람다 표현식 이 도입되었습니다.

이것이 어떻게 코드를 개선할 수 있는지에 대한 예를 살펴보겠습니다. 여기에 두 개의 Integer 값을 모듈로 2로 비교하는 간단한 비교기가 있습니다.

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

이 클래스의 인스턴스는 미래에 다음과 같이 이 비교기가 필요한 코드에서 호출될 수 있습니다.

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

새로운 람다 구문을 사용하면 이 작업을 더 간단하게 수행할 수 있습니다. 다음은 BinaryComparatorcompare 메서드와 동일한 작업을 수행하는 간단한 람다 표현식입니다.

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

구조는 기능과 많은 유사점이 있습니다. 괄호 안에 우리는 인수 목록을 설정합니다. 구문 -> 은 이것이 람다임을 보여줍니다. 그리고 이 표현식의 오른쪽 부분에서 람다의 동작을 설정합니다.

자바 8 람다 식

이제 이전 예제를 개선할 수 있습니다.

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

이 객체로 변수를 정의할 수 있습니다. 어떻게 보이는지 봅시다:

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

이제 다음과 같이 이 기능을 재사용할 수 있습니다.

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

이 예제에서 람다는 이전 예제에서 BinaryComparator 의 인스턴스가 전달된 것과 동일한 방식으로 sort() 메서드에 전달됩니다. JVM은 람다를 올바르게 해석하는 방법을 알고 있습니까?

함수가 람다를 인수로 사용할 수 있도록 Java 8은 기능적 인터페이스 라는 새로운 개념을 도입했습니다. 기능 인터페이스는 추상 메서드가 하나만 있는 인터페이스입니다. 사실, Java 8은 람다 표현식을 기능적 인터페이스의 특별한 구현으로 취급합니다. 즉, 람다를 메서드 인수로 수신하려면 해당 인수의 선언된 형식이 기능 인터페이스만 있으면 됩니다.

기능적 인터페이스를 선언할 때 @FunctionalInterface 표기법을 추가하여 개발자에게 이것이 무엇인지 보여줄 수 있습니다.

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

이제 다음과 같이 다른 동작을 수행하기 위해 다른 람다를 전달하는 sendDTO 메서드를 호출할 수 있습니다.

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

방법 참조

Lambda 인수를 사용하면 함수 또는 메서드의 동작을 수정할 수 있습니다. 마지막 예제에서 볼 수 있듯이 람다는 다른 메서드( sendToAndroid 또는 sendToIos )를 호출하는 역할만 하는 경우가 있습니다. 이 특별한 경우를 위해 Java 8은 편리한 약어인 메소드 참조 를 도입했습니다. 이 축약된 구문은 메서드를 호출하고 objectName::methodName 형식을 갖는 람다를 나타냅니다. 이를 통해 이전 예제를 훨씬 더 간결하고 읽기 쉽게 만들 수 있습니다.

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

이 경우 sendToAndroidsendToIos 메서드가 this 클래스에서 구현됩니다. 다른 객체나 클래스의 메서드를 참조할 수도 있습니다.

스트림 API

Java 8은 완전히 새로운 Stream API의 형태로 Collections 작업을 위한 새로운 기능을 제공합니다. 이 새로운 기능은 java.util.stream 패키지에 의해 제공되며 컬렉션을 사용한 프로그래밍에 대한 보다 기능적인 접근을 가능하게 하는 것을 목표로 합니다. 우리가 보게 되겠지만, 이것은 우리가 방금 논의한 새로운 람다 구문 덕분에 가능합니다.

Stream API는 컬렉션의 간편한 필터링, 계산 및 매핑을 제공할 뿐만 아니라 컬렉션에서 정보의 조각과 하위 집합을 가져오는 다양한 방법을 제공합니다. 기능적 스타일의 구문 덕분에 Stream API는 컬렉션 작업을 위한 더 짧고 세련된 코드를 허용합니다.

짧은 예부터 시작하겠습니다. 모든 예에서 이 데이터 모델을 사용합니다.

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

2005년 이후에 책을 쓴 books 컬렉션의 모든 저자를 인쇄해야 한다고 가정해 봅시다. Java 7에서는 이를 어떻게 수행할까요?

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

Java 8에서는 어떻게 할까요?

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

단 하나의 표현입니다! 모든 Collection 에서 stream() 메서드를 호출하면 해당 컬렉션의 모든 요소를 ​​캡슐화하는 Stream 객체가 반환됩니다. filter()map() ) 과 같은 Stream API의 다른 수정자로 조작할 수 있습니다. 각 수정자는 수정 결과와 함께 새 Stream 개체를 반환하며 추가로 조작할 수 있습니다. .forEach() 메서드를 사용하면 결과 스트림의 각 인스턴스에 대해 몇 가지 작업을 수행할 수 있습니다.

이 예제는 또한 함수형 프로그래밍과 람다 식 간의 밀접한 관계를 보여줍니다. 스트림의 각 메서드에 전달된 인수는 사용자 지정 람다이거나 메서드 참조입니다. 기술적으로 각 수정자는 이전 섹션에서 설명한 대로 모든 기능 인터페이스를 수신할 수 있습니다.

Stream API는 개발자가 Java 컬렉션을 새로운 각도에서 볼 수 있도록 도와줍니다. 이제 각 국가에서 사용 가능한 언어의 Map 를 가져와야 한다고 상상해 보십시오. 이것은 Java 7에서 어떻게 구현됩니까?

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

Java 8에서는 상황이 좀 더 깔끔해졌습니다.

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

collect() 메서드를 사용하면 스트림의 결과를 다양한 방식으로 수집할 수 있습니다. 여기에서 먼저 국가별로 그룹화한 다음 언어별로 각 그룹을 매핑하는 것을 볼 수 있습니다. ( groupingBy()toSet() 은 모두 Collectors 클래스의 정적 메서드입니다.)

자바 8 스트림 API

Stream API에는 다른 많은 기능이 있습니다. 전체 문서는 여기에서 찾을 수 있습니다. 이 패키지가 제공하는 모든 강력한 도구에 대해 더 깊이 이해하려면 더 읽어보기를 권장합니다.

CompletableFuture 를 사용한 비동기 작업 연결

Java 7의 java.util.concurrent 패키지에는 Future<T> 인터페이스가 있어 향후 비동기 작업의 상태나 결과를 얻을 수 있습니다. 이 기능을 사용하려면 다음을 수행해야 합니다.

  1. 비동기 작업의 실행을 관리하고 진행 상황을 추적하기 위해 Future 객체를 생성할 수 있는 ExecutorService 를 만듭니다.
  2. 비동기적으로 Runnable 가능한 작업을 만듭니다.
  3. 상태 또는 결과에 대한 액세스를 제공하는 Future 를 제공하는 ExecutorService 에서 작업을 실행합니다.

비동기 작업의 결과를 사용하려면 Future 인터페이스의 메서드를 사용하여 외부에서 진행 상황을 모니터링하고 준비가 되면 명시적으로 결과를 검색하고 추가 작업을 수행해야 합니다. 이는 특히 동시 작업이 많은 응용 프로그램에서 오류 없이 구현하기가 다소 복잡할 수 있습니다.

그러나 Java 8에서는 비동기 작업 체인의 생성 및 실행을 허용하는 CompletableFuture<T> 인터페이스를 사용하여 Future 개념을 한 단계 더 발전시켰습니다. 완료 시 각 작업의 결과를 자동으로 처리할 수 있기 때문에 Java 8에서 비동기식 응용 프로그램을 만드는 강력한 메커니즘입니다.

예를 들어 보겠습니다.

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

CompletableFuture.supplyAsync 메서드는 기본 Executor (일반적으로 ForkJoinPool )에 새로운 비동기 작업을 생성합니다. 작업이 완료되면 결과가 자동으로 this::getLinks 함수에 대한 인수로 제공되며 이는 새로운 비동기 작업에서도 실행됩니다. 마지막으로 이 두 번째 단계의 결과는 자동으로 System.out 에 인쇄됩니다. thenApply()thenAccept()Executors 를 수동으로 사용하지 않고 동시 작업을 빌드하는 데 도움이 되는 몇 가지 유용한 메서드 중 두 가지일 뿐입니다.

CompletableFuture 를 사용하면 복잡한 비동기 작업의 순서를 쉽게 관리할 수 있습니다. 세 가지 작업으로 다단계 수학 연산을 생성해야 한다고 가정해 보겠습니다. 작업 1작업 2 는 첫 번째 단계의 결과를 찾기 위해 서로 다른 알고리즘을 사용하며 그 중 하나만 작동하고 다른 하나는 실패한다는 것을 알고 있습니다. 그러나 어느 것이 작동하는지 미리 알 수 없는 입력 데이터에 따라 다릅니다. 이러한 작업의 결과는 작업 3 의 결과와 합산되어야 합니다. 따라서 작업 1 또는 작업 2 의 결과 작업 3 의 결과를 찾아야 합니다. 이를 달성하기 위해 다음과 같이 작성할 수 있습니다.

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

Java 8이 이를 처리하는 방법을 조사하면 세 가지 작업이 모두 동시에 비동기적으로 실행됨을 알 수 있습니다. 작업 2 가 예외적으로 실패하더라도 최종 결과는 성공적으로 계산되고 인쇄됩니다.

CompletableFuture를 사용한 JAVA 8 비동기식 프로그래밍

CompletableFuture 를 사용하면 여러 단계의 비동기 작업을 훨씬 쉽게 구축할 수 있으며 각 단계가 완료될 때 수행해야 하는 작업을 정확히 정의하기 위한 쉬운 인터페이스를 제공합니다.

자바 날짜 및 시간 API

Java의 자체 승인에 따르면 다음과 같습니다.

Java SE 8 릴리스 이전에는 Java 날짜 및 시간 메커니즘이 java.util.Date , java.util.Calendarjava.util.TimeZone 클래스와 해당 하위 클래스(예: java.util.GregorianCalendar . 이러한 클래스에는 다음과 같은 몇 가지 단점이 있습니다.

  • Calendar 클래스는 형식이 안전하지 않습니다.
  • 클래스는 변경 가능하므로 다중 스레드 응용 프로그램에서 사용할 수 없습니다.
  • 애플리케이션 코드의 버그는 비정상적인 월 번호와 형식 안전성 부족으로 인해 흔히 발생했습니다."

Java 8은 마침내 날짜 및 시간 작업을 위한 클래스가 포함된 새로운 java.time 패키지로 이러한 오랜 문제를 해결합니다. 그들 모두는 불변이며 거의 모든 Java 개발자가 기본 Date , CalendarTimeZone 대신 애플리케이션에서 사용하는 인기 있는 프레임워크 Joda-Time과 유사한 API를 가지고 있습니다.

다음은 이 패키지의 몇 가지 유용한 클래스입니다.

  • Clock - 현재 순간, 날짜 및 시간대가 있는 시간을 포함하여 현재 시간을 알려주는 시계입니다.
  • DurationPeriod - 시간입니다. Duration 은 "76.8초"와 같은 시간 기반 값을 사용하고 "4년 6개월 12일"과 같은 날짜 기반 Period 값을 사용합니다.
  • Instant - 여러 형식의 즉각적인 시점입니다.
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth - ISO-8601 달력 시스템에서 표준 시간대가 없는 날짜, 시간, 연도, 월 또는 이들의 조합.
  • OffsetDateTime , OffsetTime - ISO-8601 달력 시스템에서 UTC/그리니치로부터 오프셋이 있는 날짜-시간(예: "2015-08-29T14:15:30+01:00").
  • ZonedDateTime - "1986-08-29T10:15:30+01:00 Europe/Paris"와 같이 ISO-8601 달력 시스템의 관련 표준 시간대가 있는 날짜-시간입니다.

자바 8 시간 API

때로는 "첫 번째 화요일"과 같은 상대적 날짜를 찾아야 합니다. 이러한 경우 java.timeTemporalAdjuster 특수 클래스를 제공합니다. TemporalAdjuster 클래스에는 정적 메서드로 사용할 수 있는 표준 조정자 집합이 포함되어 있습니다. 이를 통해 다음을 수행할 수 있습니다.

  • 해당 월의 첫 번째 또는 마지막 날을 찾습니다.
  • 다음 또는 이전 달의 첫 번째 또는 마지막 날을 찾습니다.
  • 연도의 첫 번째 또는 마지막 날을 찾습니다.
  • 다음 연도 또는 이전 연도의 첫 번째 또는 마지막 날을 찾습니다.
  • "6월의 첫 번째 수요일"과 같이 한 달 내의 첫 번째 또는 마지막 요일을 찾습니다.
  • "다음 목요일"과 같이 다음 또는 이전 요일을 찾습니다.

다음은 매월 첫 번째 화요일을 얻는 방법에 대한 간단한 예입니다.

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
아직도 자바 7을 사용하고 계십니까? 프로그램과 함께하세요! #자바8
트위터

요약의 Java 8

보시다시피 Java 8은 Java 플랫폼의 획기적인 릴리스입니다. 특히 람다의 도입과 함께 많은 언어 변경 사항이 있습니다. 이는 Java에 더 많은 기능적 프로그래밍 기능을 제공하려는 움직임을 나타냅니다. Stream API는 우리가 이미 익숙한 표준 Java 도구로 작업하는 방식을 람다가 어떻게 바꿀 수 있는지에 대한 좋은 예입니다.

또한 Java 8은 비동기 프로그래밍 작업을 위한 몇 가지 새로운 기능과 매우 필요한 날짜 및 시간 도구 정밀 검사를 제공합니다.

이러한 변경 사항은 함께 Java 언어의 큰 발전을 나타내며 Java 개발을 보다 흥미롭고 효율적으로 만듭니다.