Backend/JAVA

Java 8에 추가된 것들 1

keepbang 2021. 9. 9. 00:46

내가 쓰는 언어의 버전에서 어떤걸 사용할 수 있는지 알아보고자 정리하였다.

먼저 자바 8에서 추가된 주요 기능들을 나열하면 아래와 같다.

 

1. Optional
2. 인터페이스의 기본 메소드(Default method)
3. 날짜와 시간 관련 클래스들 추가
4. 병렬 배열 정렬
5. StringJoiner 추가
6. 람다(Lambda) 표현식
7. 함수형 인터페이스(Functional Interface)
8. 스트림(Stream)

Optional(옵셔널)

  • 선택적으로 뭔가를 처리 할 때 사용하는 것으로 객체를 편리하게 처리하기 위해서 만든 클래스다.
  • Optional클래스는 null 처리를 보다 편리하게 하기 위해서 만들어졌다.
  • NullPointerException이 발생할 수 있는 상황에서 문제를 보다 간편하고 명확하게 처리하기 위해 사용된다.
  • java.util 패키지에 속해있다.

Optionalfinal 클래스로 선언되어 있으며 Generic 한 클래스이다.

 

final클래스로 선언되어 있기 때문에 추가적인 확장(상속)이 불가능하다. 즉, 자식 클래스를 만들 수 없다는 의미다.

 

Optional 클래스는 객체를 생성할 때 Optional 클래스를 리턴하는 empty(), of(), ofNullable() 메소드들을 사용하여 생성한다.

String nullableStr = null;
String mandStr = "required";

Optional<String> empty = Optional.empty(); //1
Optional<String> nullable = Optional.ofNullable(nullableStr); //2
Optional<String> optionalStr = Optional.of(mandStr); //3
  1. 데이터가 없는 Optional 객체를 생성한다.
  2. Null이 추가될 수 있는 상황에 사용한다.(Null 허용)
  3. 반드시 데이터가 들어갈 수 있는 상황에서 사용한다.

Optional객체가 비어있는지 확인하려면 isEmpty()가 아닌 isPresent() 메소드를 사용한다.

Java 11부터는 isEmpty()를 사용하여 체크할 수 있다.
System.out.println(empty.isPresent());
System.out.println(optionalStr.isPresent());

첫번째 라인의 결과는 false로 나오고 두번째 라인의 결과는 true가 나온다.

 

 


 

Optional 객체를 print해보면 아래와 같이 Optional객체로 나오게 된다.

 

Optional객체가 아닌 값을 꺼내는 방법은 "T"를 리턴하는 메소드들을 살펴보면된다.

 

get(), orElse(), orElseGet(), orElseThrow() 메소드가 값을 리턴하는 메소드들이다.

 

String mandStr = "required";


Optional<String> empty = Optional.empty();
Optional<String> optionalStr = Optional.of(mandStr);
Supplier<String> stringSupplier = new Supplier<String>() {
	@Override
    	public String get() {
        	return "supplier get";
        }
    };

System.out.println(optionalStr.get()); //1
System.out.println(empty.orElse("empty")); //2
System.out.println(empty.orElseGet(stringSupplier)); //3
System.out.println(empty
	.orElseThrow(() -> new IllegalArgumentException("null exception"))
 ); //4

  1. get()을 통해 Optional객체에 있는 데이터를 가져온다. 만약 Optional 객체가 null이라면 NoSuchElementException이 발생한다.
  2. Optional 에 값이 없을 경우 orElse()를 사용하여 기본 값을 지정할 수 있다.
  3. Supplier<T>라는 인터페이스를 사용해서 값이 없는 경우 이를 대신해 반환할 값을 생성한다.
    • 익명 클래스를 사용하는 대신 람다 표현식으로 사용 할 수 있다.
    • empty.orElseGet(() -> "람다로 가능")
  4. 값이 없는 경우 예외를 발생시킬때는 orElseThrow()메소드를 사용한다. 아래와 같이 좀더 간결하게 표현 할 수 있다.
    • optionalStr.orElseThrow(NoSuchElementException::new)

 

 

ifPresent()를 사용하면 값이 있을 경우 해당 값을 사용하여 로직을 수행하고 값이 없을 때는 아무 동작도 하지 않는다.

optionalStr.ifPresent((str) -> System.out.println("str = " + str));

 


 

Default method

인터페이스에 default method라는 것이 추가되었다.

 

A라는 인터페이스가 있을때 해당 인터페이스를 구현한 B,C,D 클래스가 있다고 예를 들어보자. 여기서  A 인터페이스에 추가기능이 필요해서 추상 메서드를 추가하고 새로 E라는 클래스로 인터페이스를 구현했다. 여기서 문제는 E클래스를 위해 추가된 추상 메서드를 B,C,D 클래스에 추가로 구현해야한다. 이러한 이유때문에 default method가 추가되어 하위호환성유연성을 높여주고 있다.

 

사용 방법은 아래 예제와 같이 사용된다.

 

public interface DefaultInterface{
  String id = "bang";
  String getId();
  //추가된 부분
  default String getEmail(){
	  return id + "@gmail.com";
  }
}

 

default method는 재정의하여 사용할 수 있다.

 

default method외에도 static method도 추가되었는데 유틸 메서드를 사용하기 위해서 추가된거같다...

interface에서 생성된 static method는 재정의가 불가능하다.

 


 

날짜와 시간 관련 클래스

 

Java 8에서는 기존 Date나 Calendar의 문제를 해결 할 수 있는 새로운 날짜와 시간 관련 클래스들이 추가되었다.

 

아래는 Date클래스나 Calendar클래스등 이전에 사용하던 날짜와 시간관련 클래스들의 문제점들이다.

  • 불변 객체가 아니어서 지속적으로 값이 변경 가능하다
    • java.util.Date클래스는 날짜 계산을 할 수 없으며, java.util.Calendar클래스는 불변객체가 아니므로 연산시 객쳉 자체가 변경되었다.
  • SimpleDateFormat은 Thread safe하지 않고 느리다.
    • 멀티 쓰레드 상황에서 static으로 impleDateFormat을 사용하고 parse를 할 때 NumberFormatException이 발생하게 된다.
    • DateFormat을 javadoc에서 찾아보면 아래와 같은 내용이있다. 
    • 멀티 쓰레드에서는 동기화를 관리해야한다는 내용이다.
  • int 상수 필드의 남용
    • Calendaradd메서드를 사용할 때 정수를 사용한다. calendar.add(Calendar.YEAR, 1);
    • 여기서 Calendar.YEAR를 다른 정수로 변경해도 에러가 나지 않는다. calendar.add(Calendar.DECEMBER, 1);
  • 직관적이지 않은 월 지정
    • Calendar의 월은 1월이 0부터 시작한다.
  • 일관성없는 요일 상수
    • Calendarget으로 요일을 가져왔을때와 DategetDay()로 요일을 가져왔을 때 결과값이 다르다.
    • getDay()@Deprecated로 지정되어있다.
//일요일 기준
Calendar calendar = Calendar.getInstance();
calendar.get(Calendar.DAY_OF_WEEK); // 7

Date date = new Date();
date.getDay(); // 6
  • 오류에 둔감한 timezone id 지정
    • TimeZone.getTimeZone 메서드에 timezone id가 들어가는데 여기서 Asia/SeoulSeoul/Asia로 입력해도 오류가 나오지 않는다.
TimeZone zone = TimeZone.getTimeZone("Asia/Seoul");
zone.getID(); //Asia/Seoul
TimeZone testZone = TimeZone.getTimeZone("Seoul/Asia");
testZone.getID(); //GMT

 

 

이러한 여러가지 이슈들 때문에 Java 8에서는 java.time이라는 패키지가 추가되었다.

 

  • java.time 패키지에 LocalDate, LocalTime, LocalDateTime등 불변객체를 제공하고 있다.
    • 모든 클래스가 연산용 메소드를 갖고 있으며, 연산시 새로운 불변 객체를 return한다.
    • 쓰레드에 안전하다.
  • DateTimeFormatter를 사용하여 빠르고 Thread safe하게 코드를 작성할 수 있다.
  • ZoneId를 통해 timezone을 가져올 수 있는데 Asia/SeoulSeoul/Asia로 입력하면 ZoneRulesException이 발생한다.
  • java.time.temporal패키지에 ChronoFieldChronoUnit등의 enum타입을 제공한다. 연산을 하거나 날짜/시간의 데이터를 가져올때 사용되어 보다 직관적으로 사용할 수 있다.

 

 

 

Lesson: Standard Calendar (The Java™ Tutorials > Date Time)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 


 

병렬 배열 설정

Java 8에서는 parallelSort()라는 정렬 메서드가 제공되고 있다. 기존의 sort()의 경우 단일 쓰레드로 수행되고 있으며 parallelSort()는 필요에 따라 여러 개의 쓰레드로 나눠서 작업을 수행한다. 멀티 쓰레드로 작업을 수행하기 때문에 CPU를 더 많이 사용하지만 속도적인 측면에서는 더 빠르다.

 

parallelSortFork-Join이라는 프레임워크가 내부적으로 적용된 메서드다.

Fork-Join은 Java 7에서 새로 추가된 기능으로 CPU를 더 쉽고 효율적으로 사용하기 위해서 만들어진 기능이다. 작업을 여러개로 나누어서 처리한 후 합친다는 의미로 Fork-Join에는 Work Stealing이라는 개념이 포함되어 있다. 예를 들어 여러개의 작업을 나눠서 진행하고 있을 때 어떤 작업은 바쁜반면 다른 작업은 한가할 경우가 있다. 이 때 한가한 작업이 바쁜 작업의 대기하고 있는 일을 대신 가져가서 처리해 주는 것이다.

 

length를 증가시키면서 random한 정수를 가진 배열을 가지고 테스트 진행 결과 배열의 길이가 길어질 수록 parallelSort메서드가 더 빨라지는 모습을 볼 수 있었다.

 

sort() parallelSort()
length = 10
startTime = 17:59:58.343, endTime = 17:59:58.344
1

length = 100
startTime = 17:59:58.351, endTime = 17:59:58.352
1

length = 1000
startTime = 17:59:58.352, endTime = 17:59:58.352
0

length = 10000
startTime = 17:59:58.353, endTime = 17:59:58.356
3

length = 100000
startTime = 17:59:58.365, endTime = 17:59:58.377
12

length = 1000000
startTime = 17:59:58.401, endTime = 17:59:58.524
123

length = 10000000
startTime = 17:59:58.720, endTime = 17:59:59.358
638

length = 100000000
startTime = 18:00:01.602, endTime = 18:00:07.490
5888
length = 10
startTime = 18:00:35.692, endTime = 18:00:35.692
0

length = 100
startTime = 18:00:35.702, endTime = 18:00:35.702
0

length = 1000
startTime = 18:00:35.703, endTime = 18:00:35.704
1

length = 10000
startTime = 18:00:35.705, endTime = 18:00:35.718
13

length = 100000
startTime = 18:00:35.729, endTime = 18:00:35.755
26

length = 1000000
startTime = 18:00:35.790, endTime = 18:00:35.940
150

length = 10000000
startTime = 18:00:36.144, endTime = 18:00:36.386
242

length = 100000000
startTime = 18:00:38.708, endTime = 18:00:40.571
1863


배열의 길이가 엄청 많지 않는 이상 굳이 parallelSort()를 사용할 필요는 없어 보인다. 상황에 맞게 사용하는게 맞는거같다.

for (int i = 10; i < 1000000000; i*=10) {
    System.out.println("length = " + i);

    int[] randArr = new int[i];
    for (int j = 0; j < randArr.length; j++) {
    	randArr[j] = (int)(Math.random() * 10000);
    }

	LocalTime startTime1 = LocalTime.now();
	Arrays.parallelSort(randArr);
    //Arrays.sort(randArr);
	LocalTime endTime1 = LocalTime.now();

    System.out.println("startTime = " + startTime1 + ", endTime = " + endTime1);
	System.out.println(ChronoUnit.MILLIS.between(startTime1, endTime1));

	System.out.println();

}

StringJoiner

배열이나 리스트등 어떤 구분자로 구분하여 출력할 때 사용 할 수 있다.

 

List<String> list = Arrays.asList("A","B","C");

StringJoiner stringJoiner = new StringJoiner(",","(",")");

for (String s : list) {
	stringJoiner.add(s);
}

System.out.println("join string = " + stringJoiner);
// join string = (A,B,C)

 

StringJoiner는 두개의 생성자를 제공한다.

public StringJoiner(CharSequence delimiter) {...}
public StringJoiner(CharSequence delimiter,CharSequence prefix,CharSequence suffix){...}

 

위 코드에서 구분자만 사용하여 StringJoiner를 사용할 경우 괄호 없이 A,B,C가 나온다.

 

Collectors라는 클래스의 joining메서드를 사용하면 아래처럼 간단하게 사용할 수 있다.

list.stream().collect(Collectors.joining(",", "(", ")"));

 

[Reference]

 

Optional 제대로 활용하기 - Increment

Optional을 올바르게 사용하기 위해 공부한 내용을 정리합니다. 개요 getter에 Optional을 사용하는 것이 좋은지에 대해, Java 언어 아키텍트인 Brian Goetz가 Stackoverflow에 작성한 답변(링크)입니다. 당연히

www.latera.kr

 자바의 신 2nd - 32장