포스트

Java - 8 Version 의 특장점

JAVA8 버전의 특징에 대해서 알아보자😎 ( ☝표시 중요)

1. ☝람다 표현식(lambda expression) : 함수형 프로그래밍

  • 람다 표현식이란 메소드를 하나의 ‘식’으로 표현한 것인데 주목적은 명확하면서 간략한표현을 추구한다.

장점

  • 메소드의 구현을 간결하게 하여 가독성을 높인다
  • 람다로 구현하여 코드 줄 소비를 줄임
  • 지연 연산을 이용하여 퍼포먼스 향상

단점

  • 재활용이 불가능하기때문에 여러곳에서 반복적으로 사용된다면 람다를 사용하지않는것이 바람직하다.
  • 람다식이 남용된다면 유지보수 및 코드 이해에 어려움을 준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
//(예제1)
//기존의 익명 내부 함수
Runnable r1 = new Runnable(){
  @Override
  public void run(){
    System.out.println("Hello world!!");
  }
};
r1.run();

//람다를 이용한 구현
Runnable r2 = () -> System.out.println("Hello world!!");
r2.run();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//(예제2)
// 1번 표현식 한개
Comparator<String> c1 = (String lhs, String rhs) -> lhs.compareTo(rhs);
int result1 = c1.compare("Hello", "World");

// 2번 표현식 두개
Comparator<String> c2 = (String lhs, String rhs) -> {
	System.out.println("Comparing "+ lhs + " and " + rhs);
	return lhs.compareTo(rhs);
};
int result2 = c2.compare("Hello","World");

//3번 타입 추론
Comparator<String> c3 = (lhs, rhs) -> lhs.compareTo(rhs);
int result3 = c3.compare("Hello", "World");

-특징

  • 1.람다식은 함수형 인터페이스를 간편히 구현하는 역할을 한다.
  • 2.코드를 간결하게 만들어 줘서 코드 양을 줄여주고 가독성을 높임
  • 3.람다식은 매개변수 집합 + 화살표 + 바디 로 구성 되어 있다.
  • 4.(매개변수) -> {표현식};
  • 5.람다식 바디의 표현식이 한개일 경우(코드가 한줄일 경우) 중괄호 생략가능하다.
  • 6.매개변수에 타입을 따로 지정하지 않아도 된다
  • 7.함수형 인터페이스는 메소드를 하나만 가지고 있는 인터페이스이다.

2. ☝스트림 API(Stream API) : 데이터의 추상화

  • 데이터를 처리하는데 자주 사용되는 메서드를 포함한 함수형 스타일의 API를 제공한다
  • 컬렉션 데이터를 효과적으로 처리하고 병렬 처리를 쉽게 한다.
  • 중간 및 최종 연산을 통해 데이터 파이프라인을 생성하고 조작한다.
  • 간결하고 가독성 좋은코드 작성가능.
  • 병렬처리 지원 : 멀티코어 환경에서 데이터를 병렬로 처리하기 좋다

스트림을 사용하기 않고 작성한 코드 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Stream 사용 전
String[] nameArr = {"IronMan", "Captain", "Hulk", "Thor"}
List<String> nameList = Arrays.asList(nameArr);

// 원본의 데이터가 직접 정렬됨
Arrays.sort(nameArr);
Collections.sort(nameList);

for (String str: nameArr) {
  System.out.println(str);
}

for (String str : nameList) {
  System.out.println(str);
}

스트림을 사용하여 작성한 예

1
2
3
4
5
6
7
8
9
10
11
// Stream 사용 후
String[] nameArr = {"IronMan", "Captain", "Hulk", "Thor"}
List<String> nameList = Arrays.asList(nameArr);

// 원본의 데이터가 아닌 별도의 Stream을 생성함
Stream<String> nameStream = nameList.stream();
Stream<String> arrayStream = Arrays.stream(nameArr);

// 복사된 데이터를 정렬하여 출력함
nameStream.sorted().forEach(System.out::println);
arrayStream.sorted().forEach(System.out::println);

세부특징

  • 원본의 데이터를 변경하지 않음
1
List<String> sortedList = nameStream.sorted().collect(Collections.toList());
  • 데이터는 일회용이다.
1
2
3
4
userStream.sorted().forEach(System.out::print);

// 스트림이 이미 사용되어 닫혔으므로 에러 발생
int count = userStream.count();
  • 내부반복으로 작업을 처리함
1
2
 // 반복문이 forEach라는 함수 내부에 숨겨져 있다.
nameStream.forEach(System.out::println);

3. ☝Parallel Stream : 스트림 병렬처리

  • 병렬 스트림이란 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다.

  • 따라서 병렬 스트림을 이용하면 모든 멀티코어 프로세서가 각각의 청크를 처리하도록 할당할 수 있다.

예제

1
2
3
4
5
public long sequentialSum(long n) {
    return Stream.iterate(1L, i -> i + 1) // 무한 자연수 스티림 생성
        .limit(n) // n개 이하로 제한
        .reduce(0L, Long::sum); // 모든 숫자를 더하는 스트림 리듀싱 연산
 }

-> 만약 해당 코드의 n이 엄청나게 커진다면 병렬로 처리하는것이 좋다

순차 스트림을 병렬스트림으로 변환한 예제

1
2
3
4
5
6
public long parallelSum(long n) {
    return Stream.iterate(1L, i -> i + 1)
        .limit(n)
        .parallel() // 스트림을 병렬 스트림으로 변환
        .reduce(0L, Long::sum);
  }

-> 리듀싱 연산으로 스트림의 모든 숫자를 더함 -> 예제의 코드와 변환된코드의 차이는 여러 청크로 분할되어있다는것

병렬스트림을 사용해야하는 기준

  • 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려한다.
  • 소량의 데이터에서는 병렬 스트림이 도움 되지 않는다.
  • 스트림을 구성하는 자료구조가 적절한지 확인한다. 예를 들어 ArrayList를 LinkedList보다 효율적으로 분할할 수 있다.
  • 스트림의 특성과 파이프라인의 중간 연산이 스트림의 특성을 어떻게 바꾸는지에 따라 분해 과정의 성능이 달라질 수 있다.

4. java.time 패키지 : Joda-Time을 이용한 새로운 날짜와 시간 API(Calendar가 가지고 있는 단점을 보완)

- Java8 이전에 사용되는 날짜와 시간 API는 Date, Calendar, GregorianCalendar 등이 있었는데 여러모로 불편함을 유발했습니다.😥

문제점

  • 클래스 이름이 명확하지 않다 (날짜 클래스 중 Date는 Date인데도 불구하고 시간도 사용 가능하고, TimeStamp도 표현할 수 있습니다)
1
2
Date date = new Date();
long time = date.getTime();
  • Date 객체가 mutable(변하기쉽다) 하여 thread unsafe(쓰레드 안전하지못함) 하다 -> 멀티스레드 환경에서 안전하게 사용할 수 없다!
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws InterruptedException {
	Date date = new Date();
	long time = date.getTime();
	System.out.println("date = " + date);
	Thread.sleep(1000 * 3); // 3초 sleep
	Date after3Seconds = new Date();
	System.out.println("after3Seconds = " + after3Seconds);

	// setTime()를 사용하면 객체의 시간값을 변경할 수 있다.
	after3Seconds.setTime(time);
	System.out.println("after3Seconds = " + after3Seconds);
}
-> set으로 값을 변경시켜버릴수있는 이러한객체를 mutable 객체 , 상태를 변경할수 없는 객체를 immutable객체라고 한다.
  • Calendar 객체의 month가 0부터 시작하기때문에 헷갈림

JAVA 8버전에서 도입된 java.time 패키지가 제공하는 클래스

Instant : 타임스탬프 ( 기계용 시간, EPOCH )

LocalDate : 시간이 없는 날짜 ( 타임존에 대한 참조가 없음 )

LocalTime : 날짜가 없는 시간 ( 타임존에 대한 참조가 없음 )

LocalDateTime : 날짜와 시간을 결합한 클래스

ZonedDateTime : UTC 에서 시간대 및 타임존에 대한 참조를 가진 시간과 날짜를 다루는 클래스

유용한 메소드

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 시간, 분, 초 나노초 얻기
loalTime.getHour(); // 11
localTime.getMinute(); // 25
localTime.getSecond(); // 38
localTime.getNano(); // 879975840

// 시간 비교
LocalTime time1 = LocalTime.of( 12, 30, 0 );
LocalTime time2 = LocalTime.of( 13, 40, 0 );

time1.isAfter( time2 ); // false ( @param LocalTime )
time1.isBefore( time2 ); // true

// 특정 시간에서 특정 부분 ( 시간, 분, 초 ) 변경한 객체 얻기
LocalTime localTime = LocalTime.of( 12, 30, 0);

localTime.with( ChronoField.MINUTE_OF_HOUR, 40 );
localTime.withHour( 13 ); // 13:30
localTime.withMinute( 40 ); // 12:40
localTime.withSecond( 40 ); // 12:30:40
localTime.withNano( 40 ); // 12:30:00.000000040

// 시간 더하기
localTime.plus( 2, ChronoUnit.HOURS ); // 14:30
localTime.plusHours( 2 ); // 14:30
localTime.plusMinutes( 30 ); // 13:00
localTime.plusSecond( 30 ); // 12:30:30
localTime.plusNanos( 30 ); // 12:30:00.000000030

// 시간 빼기
localTime.minus( 2, ChronoUnit.HOURS );
localTime.minusHours( 2 ); // 10:30
localTime.minusMinutes( 30 ); // 12:00
localTime.minusSecond( 30 ); // 12:29:30
localTime.minusNanos( 30 ); // 12:29:59.999999970

// 두 시간 차이
Duration duration = Duration.between( time1, time2 );
duration.getNano(); // 0
duration.getSeconds(); // 4200

long diffHours = ChronoUnit.HOURS.between( time1, time2 ); // 1
long diffMinutes = ChronoUnit.MINUTES.between( time1, time2 ); // 70
long diffSeconds = ChronoUnit.SECONDS.between( time1, time2 ); // 4200

5. ☝ Default Method : 인터페이스의 구현체를 인터페이스 자체에서 기본으로 제공 가능

  • 지금까지 사용하던 Java의 인터페이스 기능은 추상 메서드와 달리 메서드 선언만 할 뿐 그 내부에 로직이 들어가는 일이 없었습니다. 하지만 Java 8부터 인터페이스에 메서드 선언뿐만 아니라 구현이 가능해졌는데 이 구현 방법으로는 default method(기본 메서드), static method(스태틱 메서드)가 있습니다.
  • (1) 기본 메서드(Default Method)

1
2
3
4
5
6
7
8
9
10
11
12
13
// Foo 인터페이스 생성
public interface Foo {
    // 추상 메서드(abstract method)
    void printName(); // void 앞에 abstract 생략 가능
}

// Foo 인터페이스를 상속받은 DefaultFoo 클래스 생성 - 구현체
public class DefaultFoo implements Foo {
    @Override
    public void printName() {
        System.out.println("DefaultFoo!!");
    }
}
  • 여기서 Foo를 구현한 클래스에 공통적으로 사용할 기능을 추가해야 하는 상황이 생겨 Foo 인터페이스가 추상 메서드 1개를 추가한다면 Foo를 구현한 모든 구현체에서 Error 메세지가 출력됩니다.

  • Why🙄? => 구현체(DefaultFoo 클래스)에 따로 구현을 해주지 않았기 때문입니다. 기본 메서드(default method)는 이러한 상황 때문에 생기게 되었고 Error가 발생하지 않고 공통적으로 사용할 기능을 추가할 수 있는 방법이  바로 default method를 활용하는 방법입니다.

  • 이처럼 default method를 사용하면 구현체(인터페이스를 상속받는 클래스)에서 따로 해당 메서드를 구현하지 않아도 사용 가능합니다. 하지만 default method는 구현체가 모르게 추가된 기능이기 때문에 구현체에서는 알 수 있는 방법이 없습니다. -> 일반적으로 선언된 메서드와 동일하게 오버라이드 해서 기능을 정의하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Foo {
    void printName();

    // 기본 메서드(Default Method)
    default void printNameUpperCase() {
        System.out.println("FOO");
    }
}

public class Main {
    public static void main(String[] args) {
        Foo foo = new DefaultFoo();
        foo.printName();
        foo.printNameUpperCase();
    }
}

class DefaultFoo implements Foo {
    @Override
    public void printName() {
        System.out.println("DefaultFoo");
    }
}
  • (2) 기본 메서드(Default Method)의 상속

  • 아래 코드를 보면  Foo 인터페이스에 있는 hello() 메서드를 default 메서드로 두고 싶지 않을 때 Foo 인터페이스를 상속받는 Hello 인터페이스에서 이 메서드를 다시 추상 메서드로 변경할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	public interface Foo {
    void printName();

    /**
     * @ImplSpec
     * 이 구현체는 "안녕하세요"를 출력한다.
     */
    default void hello() {
        System.out.println("안녕하세요!!");
    }
}

// ====================================================

	public interface Hello extends Foo {
    	// 추상 메서드 - abstract 생략 가능
    	@Override
    	default void hello() {
        	Foo.super.hello();
    }
}
  • (3) 💎다이아몬드 문제

  • 구현체가 상속 받은 두 인터페이스 모두 동일한 기본 메서드를 가진 경우 다이아몬드 문제가 발생한다. ( 다음과같은경우)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Bar{
	default void hello(){
    	System.out.println("Bar");
        }
 }
 //====================================================
 public interface Foo{
 	void printName();

    default void hello(){
    	System.out.println("Foo");
       }
    }
//======================================================
public class DefaultFoo implements Foo, Bar{
	private String name;
    //다이아몬드 문제 해결을 위해 직접 재정의 해준다
    @Override
    public void printName(){
    	System.out.println("DefaultFoo");
    }
}
  • (4) java8 에서 추가된 메서드

  • forEach()
  • spliterator()
  • stream() / parallelStream()
  • removeIf(Predicate)
  • reversed()
  • thenComparing()
  • static reverseOrder() / naturalOrder()
  • static nullsFirst() / nullsLast()
  • static comparing

6. Optional

  • JAVA8 부터 도입되었으며 NPE(NullPointerException)를 방지할수 있도록 도와주기위해 도입되었다.

JAVA 8 이전의 NPE를 막기위한 방법 예제

1
2
3
4
5
6
7
public static void main(String[] args) {
	OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
	Progress progress = spring_boot.getProgress();
    // 그냥 값이 null인지 아닌지 확인하고 null이 아니면 출력하는방법
	if (progress != null) {        System.out.println(progress.getStudyDuration());
    }
}
  • 다음과 같은 방식은 Error가 발생할 여지가 많다. 1.null체크와 같은 유효성 검사를 놓치는경우가 생길수있음 2.메서드가 null을 반환하는것 자체가 문제
  • null인경우 의도적으로 exception을 터트리는 방법도 존재하나 Error 발생시 Stack Trace를 찍게되며 성능하락의 원인이 될수있다.

🎇 이러한 문제를 해결하기위해 Optional이 등장했다.

1
2
3
public Optional<Progress> getProgress() {
	return Optional.ofNullable(progress);
   }
  • Optional은 클라이언트(Client)에 코드에게 명시적으로 빈 값일 수도 있다는 것을 알려주고 빈 값에 대한 처리를 강제합니다. 또한 Optional.ofNullable은 Optional로 감싸는 값이 null 일 수도 있고, 아닐 수도 있을 때 사용합니다.

  • JPA 사용시 Repositroy에서 리턴 타입을 Optional로 받을수 있도록 지원하고있다!

1
2
 userInfoRepository.findById(userId)
                .orElseThrow(() -> new LoginInfoMisMatchRuntimeException("UnAuthorized - UserInfo is not exist / userId = " + userId));
1
2
3
4
 userInfoRepository.findById(payload.getUserId())
                .ifPresent(userInfo -> {
                    throw new UserInfoDuplicationRuntimeException("Conflict - UserInfo is Duplicated / userId = " + payload.getUserId());
                });
  • ⚠️주의사항 - 문법적으로 Optional은 어디에 들어가도 문제되지 않으나 API 공식문서상 리턴값으로만 쓰기를 권장한다.

7. 나즈혼(Nashorn) : 자바스크립트의 새로운 엔진

  • 지금까지 자바스크립트의 기본 엔진으로는 모질라의 리노(Rhino)가 사용되어 왔다. 하지만 자바의 최신 개선 사항 등을 제대로 활용하지 못하는 등 노후화된 모습을 보여주게 된다. 따라서 이번 Java SE 8 버전부터는 자바스크립트의 새로운 엔진으로 오라클의 나즈혼(Nashorn)을 도입하게 되었다. 나즈혼(Nashorn)은 기존에 사용되어 온 리노에 비해 성능과 메모리 관리 면에서 크게 개선된 스크립트 엔진 이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.