예제와 함께 Java 8 기능

자바 8는 2014년 3월 18일에 출시되었습니다. 그것은 오랜 시간이 지났지만 여전히 많은 프로젝트가 자바 8에서 실행 중입니다. 그것은 많은 새로운 기능이 있는 주요 릴리스였기 때문입니다. 자바 8의 모든 흥미로운 주요 기능을 예제 코드와 함께 살펴보겠습니다.

자바 8 기능의 간단한 개요

중요한 자바 8 기능 중 일부는 다음과 같습니다;

  1. Iterable 인터페이스의 forEach() 메서드
  2. 인터페이스의 default 및 static 메서드
  3. 함수형 인터페이스 및 람다 표현식
  4. 컬렉션에 대한 대량 데이터 작업을 위한 자바 스트림 API
  5. 자바 시간 API
  6. 컬렉션 API 개선
  7. 동시성 API 개선
  8. 자바 IO 개선

이러한 자바 8 기능을 간단하게 살펴보겠습니다. 이러한 기능을 더 잘 이해하기 위해 몇 가지 코드 조각을 제공하겠습니다.

1. Iterable 인터페이스의 forEach() 메서드

필요할 때마다 Collection을 통과해야 할 때는 Iterator를 생성해야 합니다. Iterator의 전체 목적은 Collection의 각 요소를 반복하는 것이며, 그런 다음 Collection의 각 요소에 대한 루프 내에서 비즈니스 로직이 있습니다. Iterator를 제대로 사용하지 않으면 ConcurrentModificationException이 발생할 수 있습니다.

Java 8에서는 코드 작성 시 비즈니스 로직에 집중할 수 있도록 java.lang.Iterable 인터페이스에 forEach 메서드가 도입되었습니다. forEach 메서드는 java.util.function.Consumer 객체를 인수로 취하므로 비즈니스 로직을 재사용할 수 있는 별도의 위치에 있습니다. 간단한 예제로 forEach 사용법을 살펴보겠습니다.

package com.journaldev.java8.foreach;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;

public class Java8ForEachExample {

	public static void main(String[] args) {
		
		// 샘플 Collection 생성
		List<Integer> myList = new ArrayList<Integer>();
		for(int i=0; i<10; i++) myList.add(i);
		
		// Iterator를 사용하여 통과
		Iterator<Integer> it = myList.iterator();
		while(it.hasNext()){
			Integer i = it.next();
			System.out.println("Iterator Value::"+i);
		}
		
		// 익명 클래스의 Iterable forEach 메서드를 사용하여 통과
		myList.forEach(new Consumer<Integer>() {

			public void accept(Integer t) {
				System.out.println("forEach anonymous class Value::"+t);
			}

		});
		
		// Consumer 인터페이스 구현으로 통과
		MyConsumer action = new MyConsumer();
		myList.forEach(action);
		
	}

}

// 재사용할 수 있는 Consumer 구현
class MyConsumer implements Consumer<Integer>{

	public void accept(Integer t) {
		System.out.println("Consumer impl Value::"+t);
	}
}

행 수가 늘어나더라도 forEach 메서드는 반복 및 비즈니스 로직을 별도의 위치에 두어 관심 분리가 더 높아지고 코드가 더 깨끗해집니다.

인터페이스의 기본 및 정적 메서드

forEach 메서드 세부 정보를 주의 깊게 읽으면 Iterable 인터페이스에 정의되어 있지만 우리는 인터페이스가 메서드 본문을 가질 수 없다는 것을 알고 있습니다. Java 8부터는 인터페이스가 구현된 메서드를 가질 수 있도록 확장되었습니다. 우리는 default 및 static 키워드를 사용하여 메서드 구현이 포함된 인터페이스를 만들 수 있습니다. Iterable 인터페이스의 forEach 메서드 구현은 다음과 같습니다:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

우리는 Java가 클래스에서 다중 상속을 제공하지 않는다는 것을 알고 있습니다. 왜냐하면 이것은 Diamond Problem을 유발하기 때문입니다. 그래서 이제 인터페이스가 추상 클래스와 유사해졌는데 인터페이스는 어떻게 처리될까요?

이 문제의 해결책은 컴파일러가 이러한 시나리오에서 예외를 throw하고 인터페이스를 구현하는 클래스에서 구현 로직을 제공해야 한다는 것입니다.

package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface1 {

	void method1(String str);
	
	default void log(String str){
		System.out.println("I1 logging::"+str);
	}
	
	static void print(String str){
		System.out.println("Printing "+str);
	}
	
	// Object 메서드를 재정의하려고 하면 컴파일 시간 오류가 발생합니다
	// "A default method cannot override a method from java.lang.Object"
	
//	default String toString(){
//		return "i1";
//	}
	
}
package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface2 {

	void method2();
	
	default void log(String str){
		System.out.println("I2 logging::"+str);
	}

}

주의하세요, 두 인터페이스 모두 구현 논리를 가진 log() 공통 메서드를 가지고 있습니다.

package com.journaldev.java8.defaultmethod;

public class MyClass implements Interface1, Interface2 {

	@Override
	public void method2() {
	}

	@Override
	public void method1(String str) {
	}

	// MyClass에는 자체 log() 구현이 없으면 컴파일되지 않습니다.
	@Override
	public void log(String str){
		System.out.println("MyClass logging::"+str);
		Interface1.print("abc");
	}
	
}

보시다시피 Interface1에는 MyClass.log() 메서드 구현에 사용되는 정적 메서드 구현이 있습니다. Java 8은 defaultstatic 메서드를 컬렉션 API에서 많이 사용하며, 기본 메서드는 코드가 하위 호환성을 유지하도록 추가됩니다.

계층 구조의 클래스 중에는 동일한 시그니처를 가진 메서드가 있는 경우 기본 메서드는 관련이 없어집니다. Object는 기본 클래스이므로 인터페이스에 equals(), hashCode() 기본 메서드가 있는 경우 관련이 없어집니다. 이것이 왜 더 나은 명확성을 위해 인터페이스가 Object 기본 메서드를 가질 수 없도록 허용되지 않는 이유입니다.

Java 8에서 인터페이스 변경의 완전한 세부 정보는 Java 8 인터페이스 변경을 읽어주십시오.

3. 기능적 인터페이스와 람다 표현식

위의 인터페이스 코드를 주목하면 @FunctionalInterface 주석을 발견할 수 있습니다. 함수형 인터페이스는 Java 8에서 소개된 새로운 개념입니다. 정확히 하나의 추상 메서드를 갖는 인터페이스는 함수형 인터페이스가 됩니다. 우리는 인터페이스를 함수형 인터페이스로 표시하기 위해 @FunctionalInterface 주석을 사용할 필요가 없습니다.

@FunctionalInterface 주석은 함수형 인터페이스에 추상 메서드를 실수로 추가하는 것을 방지하기 위한 도구입니다. 이를 @Override 주석과 같은 것으로 생각할 수 있으며 사용하는 것이 최선의 실천입니다. 하나의 추상 메서드 run()을 갖는 java.lang.Runnable은 함수형 인터페이스의 훌륭한 예입니다.

함수형 인터페이스의 주요 이점 중 하나는 람다 표현식을 사용하여 인스턴스화할 수 있는 가능성입니다. 인터페이스를 익명 클래스로 인스턴스화할 수 있지만 코드가 복잡해 보입니다.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

함수형 인터페이스는 하나의 메서드만 갖기 때문에 람다 표현식이 메서드 구현을 쉽게 제공할 수 있습니다. 메서드 인수와 비즈니스 로직을 제공하기만 하면 됩니다. 예를 들어, 위의 구현을 람다 표현식을 사용하여 작성할 수 있습니다:

Runnable r1 = () -> {
			System.out.println("My Runnable");
		};

메서드 구현에 단일 문이 있는 경우 중괄호도 필요하지 않습니다. 예를 들어, 위의 Interface1 익명 클래스는 다음과 같이 람다를 사용하여 인스턴스화될 수 있습니다:

Interface1 i1 = (s) -> System.out.println(s);
		
i1.method1("abc");

람다 표현식은 함수형 인터페이스의 익명 클래스를 쉽게 생성하는 수단입니다. 람다 표현식을 사용하는 것에는 런타임 이점이 없으므로 몇 줄의 추가 코드를 작성하는 것을 꺼리지 않는다면 주의해서 사용해야 합니다.

A new package java.util.function has been added with bunch of functional interfaces to provide target types for lambda expressions and method references. Lambda expressions are a huge topic, I will write a separate article on that in the future.

전체 튜토리얼은 다음에서 읽을 수 있습니다: Java 8 Lambda Expressions Tutorial.

4. 컬렉션에 대한 대량 데이터 작업을 위한 Java Stream API

A new java.util.stream has been added in Java 8 to perform filter/map/reduce like operations with the collection. Stream API will allow sequential as well as parallel execution. This is one of the best features for me because I work a lot with Collections and usually with Big Data, we need to filter out them based on some conditions.

Collection 인터페이스는 순차적 및 병렬 실행을 위한 스트림을 얻기 위한 stream() 및 parallelStream() 기본 메서드로 확장되었습니다. 간단한 예제로 사용법을 살펴보겠습니다.

package com.journaldev.java8.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {

	public static void main(String[] args) {
		
		List<Integer> myList = new ArrayList<>();
		for(int i=0; i<100; i++) myList.add(i);
		
		// 순차 스트림
		Stream<Integer> sequentialStream = myList.stream();
		
		// 병렬 스트림
		Stream<Integer> parallelStream = myList.parallelStream();
		
		// Stream API에서 람다 사용, 필터 예제
		Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
		// forEach에서 람다 사용
		highNums.forEach(p -> System.out.println("High Nums parallel="+p));
		
		Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
		highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));

	}

}

위의 예제 코드를 실행하면 다음과 같은 출력이 나옵니다:

High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99

병렬 처리 값은 순서대로 나오지 않으므로 매우 큰 컬렉션과 작업할 때 병렬 처리가 매우 유용할 것입니다.

이 게시물에서는 Stream API에 관한 모든 것을 다루는 것이 불가능합니다. Stream API에 관한 모든 내용은 Java 8 Stream API Example Tutorial에서 읽을 수 있습니다.

5. Java Time API

Java에서 날짜, 시간 및 시간대와 작업하는 것은 항상 어렵습니다. Java에서는 날짜 및 시간을 처리하기 위한 표준 접근 방식이나 API가 없었습니다. Java 8의 좋은 추가 기능 중 하나는 java.time 패키지로 Java에서 시간을 다루는 과정을 간소화합니다.

Java Time API 패키지를 살펴보면 매우 쉽게 사용할 수 있을 것으로 느껴집니다. java.time.format 하위 패키지에는 날짜와 시간을 출력하고 구문 분석하는 클래스를 제공하고, java.time.zone 패키지에는 시간대와 관련된 지원 및 규칙을 제공합니다.

새로운 Time API는 월 및 요일에 대한 정수 상수 대신 enum을 선호합니다. 유용한 클래스 중 하나는 DateTime 개체를 문자열로 변환하는 DateTimeFormatter입니다. 자세한 자습서는 Java Date Time API Example Tutorial를 참조하십시오.

6. Collection API 개선 사항

이미 forEach() 메서드와 Stream API를 컬렉션에서 보았습니다. Collection API에 추가된 일부 새로운 메서드는 다음과 같습니다:

  • Iterator 기본 메서드 forEachRemaining(Consumer action) 을 사용하여 모든 요소가 처리되었거나 작업이 예외를 throw 할 때까지 남은 각 요소에 대해 주어진 작업을 수행합니다.
  • Collection 기본 메서드 removeIf(Predicate filter) 를 사용하여이 컬렉션의 모든 요소를 만족시키는 요소를 제거합니다.
  • Collection spliterator() 메서드는 요소를 순차적으로 또는 병렬로 트래버스 할 수있는 Spliterator 인스턴스를 반환합니다.
  • Map replaceAll()compute()merge() 메서드.
  • Key Collisions를 갖는 HashMap 클래스의 성능 향상

7. 동시성 API 개선

일부 중요한 동시 API 개선 사항은 다음과 같습니다:

  • ConcurrentHashMap compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), reduce() 및 search() 메서드.
  • CompletableFuture (값과 상태를 설정하여) 명시적으로 완료 될 수있는 CompletableFuture입니다.
  • Executors newWorkStealingPool() 메서드를 사용하여 사용 가능한 모든 프로세서를 대상 병렬 처리 수준으로 사용하여 작업 스틸링 스레드 풀을 만듭니다.

8. Java IO 개선 사항

내가 아는 IO 개선 사항은 다음과 같다:

  • Files.list(Path dir)는 디렉토리의 항목들이 포함된 Stream을 게으르게 채우고, 그 요소는 디렉토리의 항목들이다.
  • Files.lines(Path path)는 파일에서 모든 줄을 Stream으로 읽는다.
  • Files.find()는 주어진 시작 파일을 루트로 하는 파일 트리에서 파일을 검색하여 게으르게 채워진 Path로 이루어진 Stream을 반환한다.
  • BufferedReader.lines()는 이 BufferedReader에서 읽은 줄로 이루어진 Stream을 반환한다.

잡다한 Java 8 핵심 API 개선 사항

도움이 될 수 있는 몇 가지 잡다한 API 개선 사항은 다음과 같다:

  1. ThreadLocal 정적 메서드 withInitial(Supplier supplier)를 사용하여 인스턴스를 쉽게 생성할 수 있다.
  2. Comparator 인터페이스는 자연 순서, 역순 등에 대한 많은 기본 및 정적 메서드로 확장되었다.
  3. Integer, Long 및 Double 래퍼 클래스에 min(), max() 및 sum() 메서드가 추가되었다.
  4. Boolean 클래스의 logicalAnd(), logicalOr() 및 logicalXor() 메서드.
  5. ZipFile.stream() 메서드를 사용하여 ZIP 파일 항목에 대한 정렬된 Stream을 가져옵니다. 항목은 ZIP 파일의 중앙 디렉토리에 나타나는 순서대로 Stream에 나타납니다.
  6. Math 클래스에 여러 유틸리티 메서드가 있습니다.
  7. jjs 명령이 Nashorn 엔진을 호출하기 위해 추가되었습니다.
  8. jdeps 명령이 클래스 파일을 분석하기 위해 추가되었습니다.
  9. JDBC-ODBC 브리지가 제거되었습니다.
  10. PermGen 메모리 공간이 제거되었습니다

Java 8의 기능과 예제 프로그램은 여기까지입니다. Java 8의 중요한 기능을 놓친 경우에는 주석을 통해 알려주세요.

Source:
https://www.digitalocean.com/community/tutorials/java-8-features-with-examples