이 글은 원래 제 블로그에 게시된 글입니다.
또한 내 Java 11 튜토리얼(Java 9, 10 및 11의 새로운 언어 및 API 기능 포함)을 읽어야 합니다.
Java 8 소개에 오신 것을 환영합니다. 이 튜토리얼에서는 모든 새로운 언어 기능을 단계별로 안내합니다. 짧고 간단한 코드 샘플을 통해 기본 인터페이스 메소드, 람다 표현식, 메소드 참조 및 반복 가능한 주석을 사용하는 방법을 배우게 됩니다. 기사가 끝나면 스트림, 기능적 인터페이스, 지도 확장 및 새로운 날짜 API와 같은 최신 API 변경 사항에 익숙해질 것입니다. 텍스트로 가득한 벽은 없고 주석이 달린 코드 조각만 잔뜩 있습니다. 즐기다!
★★★ 이 프로젝트가 마음에 드시나요? 별표를 남기거나 Twitter에서 팔로우하거나 기부하여 내 작업을 지원하세요. 감사해요! ★★★
Java 8에서는 default
키워드를 활용하여 인터페이스에 비추상 메소드 구현을 추가할 수 있습니다. 이 기능은 가상 확장 방법이라고도 합니다.
첫 번째 예는 다음과 같습니다.
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
추상 메서드 calculate
외에도 Formula
인터페이스는 기본 메서드 sqrt
도 정의합니다. 구상 클래스는 추상 메서드인 calculate
구현하기만 하면 됩니다. 기본 방법인 sqrt
즉시 사용할 수 있습니다.
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
수식은 익명 개체로 구현됩니다. 코드는 매우 장황합니다. sqrt(a * 100)
의 간단한 계산을 위한 코드는 6줄입니다. 다음 섹션에서 살펴보겠지만 Java 8에는 단일 메소드 객체를 구현하는 훨씬 더 좋은 방법이 있습니다.
이전 버전의 Java에서 문자열 목록을 정렬하는 방법에 대한 간단한 예부터 시작해 보겠습니다.
List < String > names = Arrays . asList ( "peter" , "anna" , "mike" , "xenia" );
Collections . sort ( names , new Comparator < String >() {
@ Override
public int compare ( String a , String b ) {
return b . compareTo ( a );
}
});
정적 유틸리티 메소드 Collections.sort
주어진 목록의 요소를 정렬하기 위해 목록과 비교기를 허용합니다. 익명 비교자를 생성하여 정렬 메서드에 전달하는 경우가 많습니다.
하루 종일 익명 객체를 생성하는 대신 Java 8에는 훨씬 더 짧은 구문인 람다 표현식이 제공됩니다.
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
보시다시피 코드가 훨씬 짧고 읽기 쉽습니다. 하지만 더 짧아집니다.
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
한 줄 메서드 본문의 경우 중괄호 {}
와 return
키워드를 모두 건너뛸 수 있습니다. 하지만 더 짧아집니다.
names . sort (( a , b ) -> b . compareTo ( a ));
이제 목록에 sort
방법이 있습니다. 또한 Java 컴파일러는 매개변수 유형을 인식하므로 건너뛸 수도 있습니다. 람다 표현식이 실제로 어떻게 사용될 수 있는지 더 자세히 살펴보겠습니다.
람다 표현식은 Java의 유형 시스템에 어떻게 적합합니까? 각 람다는 인터페이스에 의해 지정된 특정 유형에 해당합니다. 소위 기능적 인터페이스에는 정확히 하나의 추상 메소드 선언이 포함되어야 합니다. 해당 형식의 각 람다 식은 이 추상 메서드와 일치됩니다. 기본 메소드는 추상적이지 않으므로 기능적 인터페이스에 기본 메소드를 자유롭게 추가할 수 있습니다.
인터페이스에 하나의 추상 메서드만 포함되어 있는 한 임의의 인터페이스를 람다 식으로 사용할 수 있습니다. 인터페이스가 요구 사항을 충족하는지 확인하려면 @FunctionalInterface
주석을 추가해야 합니다. 컴파일러는 이 주석을 인식하고 인터페이스에 두 번째 추상 메서드 선언을 추가하려고 하면 즉시 컴파일러 오류를 발생시킵니다.
예:
@ FunctionalInterface
interface Converter < F , T > {
T convert ( F from );
}
Converter < String , Integer > converter = ( from ) -> Integer . valueOf ( from );
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
@FunctionalInterface
주석이 생략되는 경우에도 코드가 유효하다는 점을 명심하세요.
위의 예제 코드는 정적 메서드 참조를 활용하여 더욱 단순화할 수 있습니다.
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Java 8에서는 ::
키워드를 통해 메소드 또는 생성자의 참조를 전달할 수 있습니다. 위의 예에서는 정적 메서드를 참조하는 방법을 보여줍니다. 하지만 객체 메소드를 참조할 수도 있습니다:
class Something {
String startsWith ( String s ) {
return String . valueOf ( s . charAt ( 0 ));
}
}
Something something = new Something ();
Converter < String , String > converter = something :: startsWith ;
String converted = converter . convert ( "Java" );
System . out . println ( converted ); // "J"
::
키워드가 생성자에 대해 어떻게 작동하는지 살펴보겠습니다. 먼저 다양한 생성자를 사용하여 예제 클래스를 정의합니다.
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
다음으로 새로운 사람을 생성하는 데 사용할 사람 팩토리 인터페이스를 지정합니다.
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
팩토리를 수동으로 구현하는 대신 생성자 참조를 통해 모든 것을 하나로 묶습니다.
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
Person::new
통해 Person 생성자에 대한 참조를 만듭니다. Java 컴파일러는 PersonFactory.create
의 서명과 일치하여 올바른 생성자를 자동으로 선택합니다.
람다 식에서 외부 범위 변수에 액세스하는 것은 익명 개체와 매우 유사합니다. 로컬 외부 범위뿐만 아니라 인스턴스 필드 및 정적 변수에서도 최종 변수에 액세스할 수 있습니다.
람다 표현식의 외부 범위에서 최종 지역 변수를 읽을 수 있습니다.
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
그러나 익명 객체와는 다르게 num
변수는 final로 선언될 필요가 없습니다. 다음 코드도 유효합니다.
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
그러나 코드를 컴파일하려면 num
암시적으로 final이어야 합니다. 다음 코드는 컴파일되지 않습니다 .
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
람다 식 내에서 num
에 쓰는 것도 금지됩니다.
지역 변수와 달리 람다 식 내에서 인스턴스 필드와 정적 변수에 대한 읽기 및 쓰기 액세스 권한이 모두 있습니다. 이 동작은 익명 개체에서 잘 알려져 있습니다.
class Lambda4 {
static int outerStaticNum ;
int outerNum ;
void testScopes () {
Converter < Integer , String > stringConverter1 = ( from ) -> {
outerNum = 23 ;
return String . valueOf ( from );
};
Converter < Integer , String > stringConverter2 = ( from ) -> {
outerStaticNum = 72 ;
return String . valueOf ( from );
};
}
}
첫 번째 섹션의 공식 예를 기억하시나요? 인터페이스 Formula
익명 개체를 포함한 각 공식 인스턴스에서 액세스할 수 있는 기본 메서드 sqrt
정의합니다. 람다 식에서는 작동하지 않습니다.
기본 메서드는 람다 식 내에서 액세스 할 수 없습니다 . 다음 코드는 컴파일되지 않습니다.
Formula formula = ( a ) -> sqrt ( a * 100 );
JDK 1.8 API에는 많은 내장 기능 인터페이스가 포함되어 있습니다. 그 중 일부는 Comparator
또는 Runnable
과 같은 이전 버전의 Java에서 잘 알려져 있습니다. 이러한 기존 인터페이스는 @FunctionalInterface
주석을 통해 Lambda 지원을 활성화하도록 확장되었습니다.
그러나 Java 8 API에는 여러분의 삶을 더 쉽게 만들어주는 새로운 기능적 인터페이스도 가득합니다. 이러한 새로운 인터페이스 중 일부는 Google Guava 라이브러리에서 잘 알려져 있습니다. 이 라이브러리에 익숙하더라도 일부 유용한 메소드 확장을 통해 해당 인터페이스가 어떻게 확장되는지 주의 깊게 관찰해야 합니다.
조건자는 하나의 인수에 대한 부울 값 함수입니다. 인터페이스에는 조건자를 복잡한 논리 용어(및, 또는 부정)로 구성하기 위한 다양한 기본 방법이 포함되어 있습니다.
Predicate < String > predicate = ( s ) -> s . length () > 0 ;
predicate . test ( "foo" ); // true
predicate . negate (). test ( "foo" ); // false
Predicate < Boolean > nonNull = Objects :: nonNull ;
Predicate < Boolean > isNull = Objects :: isNull ;
Predicate < String > isEmpty = String :: isEmpty ;
Predicate < String > isNotEmpty = isEmpty . negate ();
함수는 하나의 인수를 받아들이고 결과를 생성합니다. 기본 메소드를 사용하여 여러 함수를 함께 연결할 수 있습니다(compose, andThen).
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
공급자는 주어진 일반 유형의 결과를 생성합니다. 함수와 달리 공급자는 인수를 허용하지 않습니다.
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
소비자는 단일 입력 인수에 대해 수행되는 작업을 나타냅니다.
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
비교기는 이전 버전의 Java에서 잘 알려져 있습니다. Java 8은 인터페이스에 다양한 기본 메소드를 추가합니다.
Comparator < Person > comparator = ( p1 , p2 ) -> p1 . firstName . compareTo ( p2 . firstName );
Person p1 = new Person ( "John" , "Doe" );
Person p2 = new Person ( "Alice" , "Wonderland" );
comparator . compare ( p1 , p2 ); // > 0
comparator . reversed (). compare ( p1 , p2 ); // < 0
Optional은 기능적 인터페이스가 아니지만 NullPointerException
방지하기 위한 멋진 유틸리티입니다. 이는 다음 섹션에서 중요한 개념이므로 Optional이 어떻게 작동하는지 간단히 살펴보겠습니다.
Optional은 null이거나 null이 아닌 값에 대한 간단한 컨테이너입니다. null이 아닌 결과를 반환할 수 있지만 때로는 아무것도 반환하지 않는 메서드를 생각해 보세요. null
반환하는 대신 Java 8에서는 Optional
을 반환합니다.
Optional < String > optional = Optional . of ( "bam" );
optional . isPresent (); // true
optional . get (); // "bam"
optional . orElse ( "fallback" ); // "bam"
optional . ifPresent (( s ) -> System . out . println ( s . charAt ( 0 ))); // "b"
java.util.Stream
은 하나 이상의 작업을 수행할 수 있는 일련의 요소를 나타냅니다. 스트림 작업은 중간 또는 터미널 입니다. 터미널 작업은 특정 유형의 결과를 반환하지만 중간 작업은 스트림 자체를 반환하므로 여러 메서드 호출을 연속으로 연결할 수 있습니다. 스트림은 목록이나 세트와 같은 java.util.Collection
과 같은 소스에서 생성됩니다(맵은 지원되지 않음). 스트림 작업은 순차적으로 또는 병렬로 실행될 수 있습니다.
스트림은 매우 강력하므로 별도의 Java 8 스트림 튜토리얼을 작성했습니다. 웹용 유사한 라이브러리인 Sequency도 확인해 보세요.
먼저 순차 스트림이 어떻게 작동하는지 살펴보겠습니다. 먼저 문자열 목록 형식으로 샘플 소스를 만듭니다.
List < String > stringCollection = new ArrayList <>();
stringCollection . add ( "ddd2" );
stringCollection . add ( "aaa2" );
stringCollection . add ( "bbb1" );
stringCollection . add ( "aaa1" );
stringCollection . add ( "bbb3" );
stringCollection . add ( "ccc" );
stringCollection . add ( "bbb2" );
stringCollection . add ( "ddd1" );
Java 8의 컬렉션은 확장되었으므로 Collection.stream()
또는 Collection.parallelStream()
호출하여 간단히 스트림을 생성할 수 있습니다. 다음 섹션에서는 가장 일반적인 스트림 작업을 설명합니다.
필터는 스트림의 모든 요소를 필터링하는 조건자를 허용합니다. 이 작업은 결과에 대해 다른 스트림 작업( forEach
)을 호출할 수 있는 중간 작업입니다. ForEach는 필터링된 스트림의 각 요소에 대해 실행될 소비자를 허용합니다. ForEach는 터미널 작업입니다. void
이므로 다른 스트림 작업을 호출할 수 없습니다.
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Sorted는 스트림의 정렬된 보기를 반환하는 중간 작업입니다. 사용자 정의 Comparator
전달하지 않는 한 요소는 자연 순서로 정렬됩니다.
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
sorted
지원되는 컬렉션의 순서를 조작하지 않고 스트림의 정렬된 보기만 생성한다는 점을 명심하세요. stringCollection
의 순서는 그대로 유지됩니다.
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
중간 작업 map
주어진 함수를 통해 각 요소를 다른 객체로 변환합니다. 다음 예에서는 각 문자열을 대문자 문자열로 변환합니다. 그러나 map
사용하여 각 객체를 다른 유형으로 변환할 수도 있습니다. 결과 스트림의 일반 유형은 map
에 전달하는 함수의 일반 유형에 따라 달라집니다.
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
특정 조건자가 스트림과 일치하는지 확인하기 위해 다양한 일치 작업을 사용할 수 있습니다. 이러한 모든 작업은 터미널 작업이며 부울 결과를 반환합니다.
boolean anyStartsWithA =
stringCollection
. stream ()
. anyMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( anyStartsWithA ); // true
boolean allStartsWithA =
stringCollection
. stream ()
. allMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( allStartsWithA ); // false
boolean noneStartsWithZ =
stringCollection
. stream ()
. noneMatch (( s ) -> s . startsWith ( "z" ));
System . out . println ( noneStartsWithZ ); // true
Count는 스트림의 요소 수를 long
으로 반환하는 터미널 작업입니다.
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
이 터미널 작업은 지정된 기능을 사용하여 스트림 요소를 축소합니다. 결과는 감소된 값을 보유하는 Optional
입니다.
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
위에서 언급한 것처럼 스트림은 순차적이거나 병렬적일 수 있습니다. 순차 스트림에 대한 작업은 단일 스레드에서 수행되는 반면 병렬 스트림에 대한 작업은 여러 스레드에서 동시에 수행됩니다.
다음 예에서는 병렬 스트림을 사용하여 성능을 향상시키는 것이 얼마나 쉬운지 보여줍니다.
먼저 고유한 요소의 큰 목록을 만듭니다.
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
이제 이 컬렉션의 스트림을 정렬하는 데 걸리는 시간을 측정합니다.
long t0 = System . nanoTime ();
long count = values . stream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "sequential sort took: %d ms" , millis ));
// sequential sort took: 899 ms
long t0 = System . nanoTime ();
long count = values . parallelStream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "parallel sort took: %d ms" , millis ));
// parallel sort took: 472 ms
보시다시피 두 코드 조각은 거의 동일하지만 병렬 정렬이 대략 50% 더 빠릅니다. 당신이 해야 할 일은 stream()
parallelStream()
으로 변경하는 것뿐입니다.
이미 언급했듯이 맵은 스트림을 직접 지원하지 않습니다. Map
인터페이스 자체에는 stream()
메소드를 사용할 수 없습니다. 그러나 map.keySet().stream()
, map.values().stream()
및 map.entrySet().stream()
.
또한 지도는 일반적인 작업을 수행하기 위한 다양하고 새롭고 유용한 방법을 지원합니다.
Map < Integer , String > map = new HashMap <>();
for ( int i = 0 ; i < 10 ; i ++) {
map . putIfAbsent ( i , "val" + i );
}
map . forEach (( id , val ) -> System . out . println ( val ));
위의 코드는 자체적으로 설명되어야 합니다. putIfAbsent
null 검사를 추가로 작성하는 것을 방지합니다. forEach
맵의 각 값에 대한 작업을 수행하는 소비자를 허용합니다.
이 예에서는 함수를 활용하여 지도에서 코드를 계산하는 방법을 보여줍니다.
map . computeIfPresent ( 3 , ( num , val ) -> val + num );
map . get ( 3 ); // val33
map . computeIfPresent ( 9 , ( num , val ) -> null );
map . containsKey ( 9 ); // false
map . computeIfAbsent ( 23 , num -> "val" + num );
map . containsKey ( 23 ); // true
map . computeIfAbsent ( 3 , num -> "bam" );
map . get ( 3 ); // val33
다음으로, 현재 주어진 값에 매핑된 경우에만 주어진 키에 대한 항목을 제거하는 방법을 알아봅니다.
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
또 다른 유용한 방법:
map . getOrDefault ( 42 , "not found" ); // not found
지도 항목을 병합하는 것은 매우 쉽습니다.
map . merge ( 9 , "val9" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9
map . merge ( 9 , "concat" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9concat
병합은 키에 대한 항목이 없는 경우 키/값을 맵에 넣거나 병합 함수가 호출되어 기존 값을 변경합니다.
Java 8에는 java.time
패키지 아래에 새로운 날짜 및 시간 API가 포함되어 있습니다. 새로운 Date API는 Joda-Time 라이브러리와 비슷하지만 동일하지는 않습니다. 다음 예에서는 이 새로운 API의 가장 중요한 부분을 다룹니다.
시계는 현재 날짜와 시간에 대한 액세스를 제공합니다. 시계는 시간대를 인식하며 System.currentTimeMillis()
대신 Unix EPOCH 이후의 현재 시간을 밀리초 단위로 검색하는 데 사용될 수 있습니다. 타임라인의 이러한 순간 지점은 Instant
클래스로도 표시됩니다. Instants는 레거시 java.util.Date
객체를 생성하는 데 사용될 수 있습니다.
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
시간대는 ZoneId
로 표시됩니다. 정적 팩토리 메소드를 통해 쉽게 액세스할 수 있습니다. 시간대는 순간과 현지 날짜 및 시간 간의 변환에 중요한 오프셋을 정의합니다.
System . out . println ( ZoneId . getAvailableZoneIds ());
// prints all available timezone ids
ZoneId zone1 = ZoneId . of ( "Europe/Berlin" );
ZoneId zone2 = ZoneId . of ( "Brazil/East" );
System . out . println ( zone1 . getRules ());
System . out . println ( zone2 . getRules ());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime은 시간대가 없는 시간(예: 오후 10시 또는 17:30:15)을 나타냅니다. 다음 예에서는 위에 정의된 시간대에 대해 두 개의 현지 시간을 만듭니다. 그런 다음 두 시간을 비교하고 두 시간 사이의 시간과 분 차이를 계산합니다.
LocalTime now1 = LocalTime . now ( zone1 );
LocalTime now2 = LocalTime . now ( zone2 );
System . out . println ( now1 . isBefore ( now2 )); // false
long hoursBetween = ChronoUnit . HOURS . between ( now1 , now2 );
long minutesBetween = ChronoUnit . MINUTES . between ( now1 , now2 );
System . out . println ( hoursBetween ); // -3
System . out . println ( minutesBetween ); // -239
LocalTime에는 시간 문자열 구문 분석을 포함하여 새 인스턴스 생성을 단순화하는 다양한 팩토리 메서드가 함께 제공됩니다.
LocalTime late = LocalTime . of ( 23 , 59 , 59 );
System . out . println ( late ); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedTime ( FormatStyle . SHORT )
. withLocale ( Locale . GERMAN );
LocalTime leetTime = LocalTime . parse ( "13:37" , germanFormatter );
System . out . println ( leetTime ); // 13:37
LocalDate는 2014-03-11과 같은 고유한 날짜를 나타냅니다. 이는 변경할 수 없으며 LocalTime과 정확히 유사하게 작동합니다. 이 샘플은 일, 월 또는 연도를 더하거나 빼서 새 날짜를 계산하는 방법을 보여줍니다. 조작할 때마다 새 인스턴스가 반환된다는 점을 명심하세요.
LocalDate today = LocalDate . now ();
LocalDate tomorrow = today . plus ( 1 , ChronoUnit . DAYS );
LocalDate yesterday = tomorrow . minusDays ( 2 );
LocalDate independenceDay = LocalDate . of ( 2014 , Month . JULY , 4 );
DayOfWeek dayOfWeek = independenceDay . getDayOfWeek ();
System . out . println ( dayOfWeek ); // FRIDAY
문자열에서 LocalDate를 구문 분석하는 것은 LocalTime을 구문 분석하는 것만큼 간단합니다.
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedDate ( FormatStyle . MEDIUM )
. withLocale ( Locale . GERMAN );
LocalDate xmas = LocalDate . parse ( "24.12.2014" , germanFormatter );
System . out . println ( xmas ); // 2014-12-24
LocalDateTime은 날짜-시간을 나타냅니다. 위 섹션에서 볼 수 있듯이 날짜와 시간을 하나의 인스턴스로 결합합니다. LocalDateTime
변경할 수 없으며 LocalTime 및 LocalDate와 유사하게 작동합니다. 날짜-시간에서 특정 필드를 검색하는 방법을 활용할 수 있습니다.
LocalDateTime sylvester = LocalDateTime . of ( 2014 , Month . DECEMBER , 31 , 23 , 59 , 59 );
DayOfWeek dayOfWeek = sylvester . getDayOfWeek ();
System . out . println ( dayOfWeek ); // WEDNESDAY
Month month = sylvester . getMonth ();
System . out . println ( month ); // DECEMBER
long minuteOfDay = sylvester . getLong ( ChronoField . MINUTE_OF_DAY );
System . out . println ( minuteOfDay ); // 1439
시간대에 대한 추가 정보를 사용하면 인스턴트로 변환할 수 있습니다. 인스턴트는 java.util.Date
유형의 레거시 날짜로 쉽게 변환될 수 있습니다.
Instant instant = sylvester
. atZone ( ZoneId . systemDefault ())
. toInstant ();
Date legacyDate = Date . from ( instant );
System . out . println ( legacyDate ); // Wed Dec 31 23:59:59 CET 2014
날짜-시간 형식 지정은 날짜 또는 시간 형식 지정과 동일하게 작동합니다. 미리 정의된 형식을 사용하는 대신 사용자 정의 패턴에서 포맷터를 만들 수 있습니다.
DateTimeFormatter formatter =
DateTimeFormatter
. ofPattern ( "MMM dd, yyyy - HH:mm" );
LocalDateTime parsed = LocalDateTime . parse ( "Nov 03, 2014 - 07:13" , formatter );
String string = formatter . format ( parsed );
System . out . println ( string ); // Nov 03, 2014 - 07:13
java.text.NumberFormat
과 달리 새로운 DateTimeFormatter
변경할 수 없으며 스레드로부터 안전합니다 .
패턴 구문에 대한 자세한 내용은 여기를 참조하세요.
Java 8의 주석은 반복 가능합니다. 이를 이해하기 위해 예제를 직접 살펴보겠습니다.
먼저 실제 주석의 배열을 보유하는 래퍼 주석을 정의합니다.
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
Java 8에서는 @Repeatable
주석을 선언하여 동일한 유형의 여러 주석을 사용할 수 있습니다.
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
변형 2를 사용하면 Java 컴파일러는 내부적으로 @Hints
주석을 암시적으로 설정합니다. 이는 리플렉션을 통해 주석 정보를 읽는 데 중요합니다.
Hint hint = Person . class . getAnnotation ( Hint . class );
System . out . println ( hint ); // null
Hints hints1 = Person . class . getAnnotation ( Hints . class );
System . out . println ( hints1 . value (). length ); // 2
Hint [] hints2 = Person . class . getAnnotationsByType ( Hint . class );
System . out . println ( hints2 . length ); // 2
Person
클래스에 @Hints
주석을 선언한 적은 없지만 getAnnotation(Hints.class)
통해 여전히 읽을 수 있습니다. 그러나 더 편리한 방법은 주석이 달린 모든 @Hint
주석에 대한 직접 액세스를 부여하는 getAnnotationsByType
입니다.
또한 Java 8의 주석 사용은 두 가지 새로운 대상으로 확장되었습니다.
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
Java 8에 대한 나의 프로그래밍 가이드는 여기서 끝납니다. JDK 8 API의 모든 새로운 클래스와 기능에 대해 자세히 알아보려면 JDK8 API Explorer를 확인하세요. 이는 Arrays.parallelSort
, StampedLock
및 CompletableFuture
와 같은 JDK 8의 모든 새로운 클래스와 숨겨진 보석을 파악하는 데 도움이 됩니다.
또한 귀하가 관심을 가질 만한 후속 기사를 내 블로그에 여러 개 게시했습니다.
트위터에서 나를 팔로우해야 합니다. 읽어주셔서 감사합니다!