这篇文章最初发布在我的博客上。
您还应该阅读我的 Java 11 教程(包括 Java 9、10 和 11 中的新语言和 API 功能)。
欢迎阅读我对 Java 8 的介绍。本教程将逐步指导您了解所有新的语言功能。在简短的代码示例的支持下,您将学习如何使用默认接口方法、lambda 表达式、方法引用和可重复注释。在本文末尾,您将熟悉最新的 API 更改,例如流、功能接口、地图扩展和新的 Date 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
该公式作为匿名对象实现。代码相当冗长:6 行代码就完成了sqrt(a * 100)
的简单计算。正如我们将在下一节中看到的,在 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 没有整天创建匿名对象,而是提供了更短的语法,即 lambda 表达式:
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 编译器知道参数类型,因此您也可以跳过它们。让我们更深入地了解如何在野外使用 lambda 表达式。
lambda 表达式如何适应 Java 的类型系统?每个 lambda 对应于由接口指定的给定类型。所谓的函数式接口必须恰好包含一个抽象方法声明。该类型的每个 lambda 表达式都将与该抽象方法匹配。由于默认方法不是抽象的,因此您可以自由地将默认方法添加到功能接口中。
我们可以使用任意接口作为 lambda 表达式,只要该接口只包含一个抽象方法即可。为了确保您的接口满足要求,您应该添加@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
的签名自动选择正确的构造函数。
从 lambda 表达式访问外部作用域变量与匿名对象非常相似。您可以从本地外部作用域访问最终变量以及实例字段和静态变量。
我们可以从 lambda 表达式的外部作用域读取最终局部变量:
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
必须是隐式最终的才能编译代码。以下代码无法编译:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
也禁止从 lambda 表达式内写入num
。
与局部变量相比,我们可以从 lambda 表达式中读取和写入实例字段和静态变量。这种行为在匿名对象中是众所周知的。
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
,可以从每个公式实例(包括匿名对象)访问该方法。这不适用于 lambda 表达式。
无法从 lambda 表达式内访问默认方法。以下代码无法编译:
Formula formula = ( a ) -> sqrt ( a * 100 );
JDK 1.8 API 包含许多内置的函数接口。其中一些在旧版本的 Java 中众所周知,例如Comparator
或Runnable
。这些现有接口经过扩展,可通过@FunctionalInterface
注释启用 Lambda 支持。
但 Java 8 API 也充满了新的功能接口,让您的生活更轻松。其中一些新界面在 Google Guava 库中众所周知。即使您熟悉这个库,您也应该密切关注如何通过一些有用的方法扩展来扩展这些接口。
谓词是一个参数的布尔值函数。该接口包含各种默认方法,用于将谓词组成复杂的逻辑术语(and、or、negate)
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
可选不是函数式接口,而是防止NullPointerException
的漂亮实用程序。这是下一节的一个重要概念,所以让我们快速了解一下可选是如何工作的。
可选是一个简单的值容器,可以为空或非空。考虑一个可能返回非空结果但有时不返回任何内容的方法。在 Java 8 中,您返回的是一个Optional
而不是返回null
。
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 Streams 教程。您还应该查看 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
阻止我们编写额外的 if 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
表示。即时可用于创建旧版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)
读取它。然而,更方便的方法是getAnnotationsByType
,它允许直接访问所有带注释的@Hint
注释。
此外,Java 8 中注释的使用扩展到了两个新目标:
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
我的 Java 8 编程指南到此结束。如果您想了解有关 JDK 8 API 的所有新类和功能的更多信息,请查看我的 JDK8 API Explorer。它可以帮助您找出 JDK 8 的所有新类和隐藏的精华,例如Arrays.parallelSort
、 StampedLock
和CompletableFuture
- 仅举几例。
我还在我的博客上发表了一系列您可能感兴趣的后续文章:
你应该在 Twitter 上关注我。感谢您的阅读!