Java 8에 추가된 것은?

de_sj_awa·2021년 5월 5일
0

Java 8에서 추가되거나 변경된 것은 매우 많지만 꼭 알아야 하는 것은 다음과 같다.

  • Lambda(람다) 표현식
  • Functional(함수형) 인터페이스
  • Stream(스트림)
  • Optional(옵셔널)
  • 인터페이스의 기본 메소드(Default method)
  • 날짜 관련 클래스들 추가
  • 병렬 배열 정렬
  • StringJoiner 추가

1. Optional

Optional은 Functional 언어인 Hashkell과 Scala에서 제공하는 기능을 따온 것이다. 즉, 객체를 편리하게 처리하기 위해서 만든 클래스이다.

Optional 클래스는 java.util 패키지에 속해있다. 이 클래스의 선언 부분을 보자.

public final class Optional<T>
  extends Object

Object 클래스를 확장했고 final 클래스로 선언되어 있으며, Generic한 클래스이다. final로 선언되어 있다는 것은 무엇을 의미하가?

final 변수는 변경 불가능하지만, final 클래스로 선언했다고 해서 내용 변경이 불가능한 것은 아니다. 대신 추가적인 확장이 불가능하다. 즉, 자식 클래스를 만들 수 없다는 의미다.

Optional 클래스에 대해서 이해하려면 Optional 클래스는 하나의 깡통이라고 생각하면 된다. 이 깡통에 물건을 넣을 수도 있고, 아무 물건이 없을 수도 있다. 그래서 기본적인 깡통을 만들기 위해서 Optional 클래스는,

new Optional();

와 같이 객체를 생성하지 않는다. API 문서를 잘 살펴보면, Optional 클래스를 리턴하는 empty(), of(), ofNullable() 메소드들이 존재한다. 이 메소드들을 사용하여 객체를 생성하는 방법을 보자.

private void creteOptionalObjects() {
    Optional<String> emptyString=Optional.empty();  //1.
    String common=null;
    Optional<String> nullableString=Optional.ofNullable(common); //2.
    commont="common";
    Optional<String> commonString=Optional.of(common);  //3.
  1. 데이터가 없는 Optional 객체를 생성하려면 이와 같이 empty() 메소드를 사용한다.
  2. 만약 null이 추가될 수 있는 상황이라면 ofNullable() 메소드를 사용한다.
  3. 반드시 데이터가 추가될 수 있는 상황에는 of() 메소드를 사용한다.

이와 같이 Optional 클래스의 객체를 생성하는 방법은 세 가지로 나뉜다.

Optional 클래스가 비어 있는지 확인하는 메소드는 isEmpty()가 아닌 isPresent() 메소드다. 다음의 예제 코드를 보면 쉽게 이해할 것이다.

private void checkOptionalData(){
    System.out.println(Optional.of("Present").isPresent());
    System.out.println(Optional.ofNullable(null).isPresent());
}

결과는 첫 번째 코드는 true, 두 번째 코드는 false를 리턴한다.

그러면 값을 꺼내는 방법은 어떻게 될까? API 문서에 "T"를 리턴하는 메소드들을 살펴보면 된다. 다음의 예제를 보자.

private void getOptionalData(Optional<String> data) throws Exception {
    String defaultValue = "default";
    String result = data.get();  //1.
    String result2 = data.orElse(defaultValue);  //2.
    Supplier<String> stringSupplier = new Supplier<String>() {
        @Override
        public String get(){
            return "GodOfJava";
        }
    }
    String result3 = data.orElseGet(stringSupplier);  //3.
    Supplier<Exception> exceptionSupplier = new Supplier<Exception>() {
        @Override
        public Exception get() {
            return new Exception();
        }
    };
    String result4 = data.orElseThrow(exceptionSupplier);  //4.
}

이와 같이 4가지의 데이터를 꺼내는 방법이 존재한다.

  1. 가장 많이 사용되는 get() 메소드다. 만약 데이터가 없을 경우에는 null이 리턴된다.
  2. 만약 값이 없을 경우에는 orElse() 메소드를 사용하여 기본값을 지정할 수 있다.
  3. Supplier<T>라는 인터페이스를 활용하는 방법으로 orElseGet() 메소드를 사용할 수 있다.
  4. 만약 데이터가 없을 경우에 예외를 발생시키고 싶다면, orElseThrow() 메소드를 사용한다. 여기서 Exception도 3과 마찬가지로 Supplier<T> 인터페이스를 사용한다.

Supplier<T>는 람다 표현식에서 사용하려는 용도로 만들어졌으며, get() 메소드가 선언되어 있다.

지금까지 간단하게 Optional 클래스에 대해서 살펴봤다. 그런데 언제 Optional 클래스가 필요할까?

Optional 클래스는 null 처리를 보다 간편하게 하기 위해서 만들어졌다. 자칫 잘못하면 NullPointerException이 발생할 수도 있는데, 이 문제를 보다 간편하고 명확하게 처리하려면 Optional을 사용하면 된다. 단, Optional 클래스에 값을 잘못 넣으면 NoSuchElementException이 발생할 수도 있으니 유의해서 사용해야 한다.

2. Default method

Java 8부터는 default 메소드라는 것이 추가되었다. 예제를 먼저 보자. 다음과 같이 PreviousInterface라는 인터페이스가 있다.

package f.defaultmethod;

public interface PreviousInterface (
    static final String name="GodOfJavaBook";
    static final int since=2013;
    String getName();
    int getSince();

다음 인터페이스를 보자.

package f.defaultmethod;

public interface DefaultStaticInterface (
    static final String name="GodOfJavaBook";
    static final int since=2013;
    String getName();
    int getSince();
    default String getEmail() {
        return name+"@godofJava.com";
    }
}

인터페이스는 구현된 메소드가 있으면 안된다. 그런데, 이 인터페이스는 default라는 키워드와 함께 메소드가 구현되어 있다.
Java 8에서 컴파일이 잘 될까?

일반적으로 생각했던 것과 다르게 컴파일이 이상 없이 잘 된다. 이러한 메소드를 default 메소드라고 한다. 이 인터페이스를 구현해보자.

package f.defaultmethod;

public class DefaultImplementChild implements DefaultStaticInterface{
    @Override
    public String getName() {
        return name;
    }
    
    @Override
    public String getSince() {
        return since;
    }
}

abstract 클래스의 경우 extends를 사용해야 하지만, default를 사용한 인터페이스는 implements 키워드를 사용해서 구현하면 된다.

그런데 왜 이렇게 혼동되는 default 메소드를 만들었을까?

바로 "하위 호환성" 때문이다. "하위 호환성"이라는 말은 예를 들어 설명하자면, 만약 오픈 소스 코드를 만들었다고 가정하자. 그 오픈 소스가 엄청나게 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 자칫 잘못하면 내가 만든 오픈 소스를 사용한 사람들은 전부 오류가 발생하고 수정을 해야 하는 일이 발생할 수도 있다. 이럴 때 사용하는 것이 바로 default 메소드다.

이렇게 간단하게 라이브러리를 공유하지 않더라도, 비행기가 자율 주행차에 포함되는 프로그램이 수정되어야 한다면 더 심각한 문제를 야기할 수도 있기 때문에 default 메소드가 나왔다고 기억하면 더 이해가 빠를 것이다.

인터페이스와 abstract 클래스의 가장 큰 차이점은 다음과 같다. 인터페이스는 변수 X(상태값 X), 상수와 행위만 가질 수 있다.
abstract 클래스는 변수 O(상태값 O)이다. default 메소드가 만들어진 가장 중요한 이유는 하위 호환성이나 다중 상속과도 연결지어 생각해 볼 수 있다.

3. 날짜 관련 클래스들

Java 8에서 새롭게 추가된 클래스들 중에서 날짜 관련 클래스들이 있다. 이전에는 Data나 SimpleDataFormatter라는 클래스를 사용하여 날짜를 처리해 왔다. 하지만 이들 클래스들은 쓰레드에 안전하지도 않다. 그래서 하나의 클래스에 생성해 놓은 이들 클래스는 여러 쓰레드에서 접근할 때 예기치 못한 값들을 리턴할 수도 있었다. 그리고 불변(immutable) 객체도 아니어서 지속적으로 값이 변경 가능했다.

게다가 API 구성도 복잡하게 되어 있어서 연도는 1900년부터 시작하도록 되어 있고, 달은 1부터, 일은 0부터 시작한다. 그래서 1900년 1월 1일은 1900, 1, 0을 매개변수로 넘겨줘야만 했다. 이러한 여러 가지 이슈들 때문에 Java 8에서부터는 java.time이라는 패키지를 만들었다.

기존 클래스와 신규 클래스들의 차이를 표를 통해 알아보자.

내용 버전 패키지 설명
값 유지 예전 버전 java.util.Date
java.util.Calendar
Date 클래스는 날짜 계산을 할 수 없다. Calendar 클래스는 불변 객체가 아니므로 연산시 객체 자체가 변경되었다.
값 유지 Java 8 java.time.ZonedDateTime
java.time.LocalDate
ZonedDateTime과 LocalDate 등은 불변 객체이다. 모든 클래스가 연산용의 새로운 메소드를 가지고 있으며, 연산시 새로운 불변 객체를 돌려준다. 그리고 쓰레드에 안전하다.
변경 예전 버전 java.text.SimpleDateFormat SimpleDateFormat는 쓰레드 안전하지도 않고 느리다.
변경 Java 8 java.time.format.DateTimeFormatter DateTimeFormatter는 쓰레드 안전하며 빠르다.
시간대 예전 버전 java.util.Timezone "Asia/Seoul"이나 "+09 : 00" 같은 정보를 가진다.
시간대 Java 8 java.util.ZoneId
java.util.ZoneOffset
ZoneId는 "Asia/Seoul"라는 정보를 갖고 있고, ZoneOffset는 "+09 : 00"라는 정보를 가지고 있다.
속성 관련 예전 버전 java.util.Calendar Calendar.YEAR
Calendar.MONTH
Calendar.DATE(또는 Calendar.DAY_OF_MONTH)
등 이들은 정수(int)이다.
속성 관련 Java 8 java.time.temporal.ChronoField
(java.time.temporal.TemporalField)
ChronoField.YEAR
ChronoField.MONTH_OF_YEAR
ChronoField.DAY_OF_MONTH
등이 enum 타입이다.
속성 관련 Java 8 java.time.temporal.ChronoUnit
(java.time.temporal.TemporalUnit)
ChronoUnit.YEARS(연수)
ChronoUnit.MONTHS(개월)
ChronoUnit.DAYS(일)
등이 enum 타입이다.

위의 이유 뿐만 아니라 시간을 처리하는 편의를 위한 클래스와 enum들이 추가되었다.

추가로 시간을 나타내는 클래스는 Local, Offset, Zoned로 3가지 종류가 존재한다.

  • Local : 시간대가 없는 시간. 예를 들어 "1시"는 어느 지역의 1시인지 구분되지 않는다.
  • Offset : UTC(그리니치 시간대)와의 오프셋(차이)를 가지는 시간. 한국은 "+09 : 00"
  • Zoned : 시간대("한국 시간"과 같은 정보)를 갖는 시간. 한국의 경우 "Asia/Seoul"

여기서 말하는 Local은 Locale는 다른 클래스다. Locale은 지역을 의미하는 클래스이며, Local은 시간을 이야기하는 것이다.

지금은 국제화 시대이다. 그래서 시스템을 개발할 때에도 사용자의 시간이 매우 중요하다. 누군가가 SNS에 글을 올리면, 그 글이 저장되는 시간은 언제로 표시되어야 할까?

그 글이 저장되는 시간은 글쓴이의 Locale(지역) 정보와 함께 저장되어야 한다. 그렇지 않으면 24시간이 다른 전 세계의 시간이 모두 꼬이게 될 것이다. 그래서 보통은, 글을 쓴 사람은 자신의 시간대로 글이 보이게 되며, 글을 읽는 사람도 자신의 시간대에 맞게 글이 올라간 시간이 보인다. 그래서 ZonedDateTime이라는 클래스와 LocalDate가 추가된 것이다.

위의 표에는 없지만 Java 8에 추가된 클래스 중에서 DayOfWeek라는 클래스가 있다. 지금까지는 요일을 표현하기 위해 한글을 배열에 넣는 등의 작업이 필요했었지만, 이제는 DayOfWeek 클래스를 사용하면 된다. 정확하게는 DayOfWeek는 enum이다.

 private void printDayOfWeek() {
        DayOfWeek[] dayOfWeeks = DayOfWeek.values();
        Locale locale = Locale.getDefault();
        for(DayOfWeek day:dayOfWeeks) {
            System.out.print(day.getDisplayName(TextStyle.FULL, locale)+" ");
            System.out.print(day.getDisplayName(TextStyle.SHORT, locale)+" ");
            System.out.println(day.getDisplayName(TextStyle.NARROW, locale)+" ");
        }
    }

DayOfWeek 클래스는 MONDAY부터 SUNDAY까지의 상수가 enum에 선언되어 있다. 그래서 각 요일을 가져다 쓸 때에는 DayOfWeek.MONDAY처럼 사용하면 된다. 여기서는 values()라는 메소드를 사용해서 모든 요일을 가져왔다.

DayOfWeek 클래스에는 getDisplayName()이라는 메소드를 사용해서 해당 요일을 출력할 수 있다. 그런데, 이 메소드에는 TextStyle과 앞서 이야기한 지역 정보인 Locale을 전달해 줘야만 한다. 그리고 TextStyle에는 FULL, SHORT, NARROW라는 이미 정의되어 있는 스타일들이 존재한다. 이제 이 메소드를 실행한 결과를 보자.

월요일 월 월 
화요일 화 화 
수요일 수 수 
목요일 목 목 
금요일 금 금 
토요일 토 토 
일요일 일 일 

FULL로 하면 "월요일"과 같이 "요일"도 같이 붙는 것을 볼 수 있다. 그리고 한글에서는 SHORT와 NARROW의 차이가 없다. 그렇다면 다른 나라의 요일을 확인하는 메소드를 다음과 같이 만들자.

private void printDayOfWeekOfLocales(){
        DayOfWeek day = DayOfWeek.SUNDAY;
        Locale[] locales = Locale.getAvailableLocales();
        for(Locale locale:locales){
            System.out.print(locale.getCountry()+ " ");
            System.out.print(day.getDisplayName(TextStyle.FULL, locale)+" ");
            System.out.print(day.getDisplayName(TextStyle.SHORT, locale)+" ");
            System.out.println(day.getDisplayName(TextStyle.NARROW, locale)+" ");
        }
    }

실행 결과는 다음과 같다.

US Sunday Sun S
'''
KR 일요일 일 일
'''
GB Sunday Sun S

4. 병렬 배열 정렬(Parallel array Sorting)

자바를 사용하면서, 배열을 정렬하는 가장 간편한 방법은 java.util 패키지의 Arrays 클래스를 사용하는 것이다. 이 Arrays 클래스에는 다음과 같은 static 메소드들이 존재한다.

  • binarySearch() : 배열 내에서의 검색
  • copyOf() : 배열의 복제
  • equals() : 배열의 비교
  • fill() : 배열 채우기
  • hashCode() : 배열의 해시코드 제공
  • sort() : 정렬
  • toString() : 배열 내용을 출력

Java 8에서는 parallelSort()라는 정렬 메소드가 제공되며, Java 7에서 소개된 Fork-Join 프레임웍이 내부적으로 사용된다.

사용법은 간단하다.

int[] intValues=new int[10];
//배열 값 지정
Arrays.parallelSort(intValue);

이렇게 해당 배열을 매개 변수로 집어 넣으면 정렬이 된다.
그렇다면, sort() 메소드를 사용해야 할까? 아니면 parallelSort() 메소드를 사용해야 할까?

sort()의 경우 단일 쓰레드로 수행되며, parallelSort()는 필요에 따라 여러 개의 쓰레드로 나뉘어 작업이 수행된다. 따라서 parallelSort()가 CPU를 더 많이 사용하게 되겠지만, 처리 속도는 더 빠르다.

실제 두 개의 성능을 비교 테스트한 결과를 보면 5,000개 정도부터 parallelSort()의 성능이 더 빨라지는 것을 볼 수 있다. 따라서, 개수가 많지 않은 배열에서는 굳이 parallelSort()를 사용할 필요는 없다고 봐도 무방하다.

5. StringJoiner

문자열을 처리하는 방법에 String, StringBuilder, StringBuffer 등이 있다. 그리고, 문자열을 예쁘게 처리하기 위한 java.util 패키지의 Formatter라는 클래스도 있다. 이번에 Java 8에서는 StringJoiner라는 클래스가 새롭게 추가되었다. 이 클래스는 java.util에 포함되어 있으며, 순차적으로 나열되는 문자열을 처리할 때 사용한다.

String[] stringArray = new String[]{"StudyHard", "GodOfJava", "Book"}

이와 같은 배열을 다음과 같이 변환하려고 하면 어떻게 해야 할까?

(StudyHard, GodOfJava, Book)

만약 String, StringBuilder, StringBuffer를 사용한다면 Book 뒤에 있는 콤마를 처리하기 위해서 if문을 넣거나, subString으로 콤마를 잘라 주어야 한다. 이러한 단점을 보완하기 위해서 StringJoiner가 만들어졌다.

먼저 배열의 구성요소 사이에 콤마만 넣자고 한다면 다음과 같이 사용하면 된다.

public void joinStringOnlyComma(String[] stringArray){
        StringJoiner joiner = new StringJoiner(",");
        for(String string:stringArray){
            joiner.add(string);
        }
        System.out.println(joiner);
    }

생성자의 중간에 들어갈 콤마를 지정하고, 해당 객체에 add() 메소드를 사용하여 내용을 추가하면 된다. 이 메소드의 결과는 다음과 같다.

StudyHard,GodOfJava,Book

아주 간단하게 배열의 구성 요소 사이 사이마다 콤마가 들어간 것을 볼 수 있다. 이번에는 앞뒤에 소괄호를 넣는 예를 보자.

public void joinString(String[] stringArray){
        StringJoiner joiner = new StringJoiner(",", "(", ")");
        for(String string:stringArray){
            joiner.add(string);
        }
        System.out.println(joiner);
    }

방금 살펴본 예제와 다른 점은 생성자뿐이다. 생성자에 맨 앞에 들어갈 prefix와 뒤에 들어갈 suffix 값을 지정해주면 된다. 결과는 예상한 대로 다음과 같이 출력된다.

(StudyHard,GodOfJava,Book)

이렇게 StringJoiner를 사용하는 방법도 있지만 스트림과 람다 표현식을 사용하면 다음과 같이 코드를 작성할 수도 있다.

public void withCollector(String[] stringArray){
        List<String> stringList = Arrays.asList(stringArray);
        String result = stringList.stream().collect(Collectors.joining(","));
        System.out.println(result);
    }

Collectors라는 클래스의 joining 메소드를 사용하면 이와 같이 간단한 코드로 정리할 수도 있다.

참고

  • 자바의 신
profile
이것저것 관심많은 개발자.

0개의 댓글