在这个由三部分组成的系列文章中,我们一直在探索您可以在今天的Android项目中开始使用的所有主要Java 8功能。
在使用Lambda表达式的Cleaner Code中 ,我们专注于使用lambda表达式从您的项目中删除样板,然后在Default和Static Methods中 ,我们看到了如何通过将它们与方法引用结合来使这些Lambda表达式更简洁。 我们还介绍了重复注释,以及如何使用默认和静态接口方法在您的接口中声明非抽象方法。
在最后一篇文章中,我们将研究类型注释,功能接口,以及如何使用Java 8的新Stream API采取更具功能性的方法进行数据处理。
我还将向您展示如何使用Joda-Time和ThreeTenABP库访问Android平台当前不支持的其他Java 8功能。
类型注释
通过将诸如Lint之类的代码检查工具告知他们应该寻找的错误,注释可以帮助您编写更健壮且更不易出错的代码。 然后,如果一段代码不符合这些注释所列出的规则,这些检查工具将向您发出警告。
注释并不是一个新功能(实际上,它们可以追溯到Java 5.0),但是在Java的早期版本中,只能将注释应用于声明。
在Java 8发行版中,您现在可以在使用类型的任何地方使用注释,包括方法接收器; 类实例创建表达式; 接口的实现; 泛型和数组; throws
和implements
子句的规范; 并进行类型转换。
令人沮丧的是,尽管Java 8确实使在比以前更多的位置中使用注释成为可能,但是它没有提供任何特定于类型的注释。
Android的注释支持库提供对一些其他注释的访问,例如@Nullable
, @NonNull
以及用于验证资源类型的注释,例如@Nullable
, @NonNull
@DrawableRes
, @DimenRes
@ColorRes
和@StringRes
。 但是,您可能还需要使用第三方静态分析工具,例如Checker Framework ,该工具是与JSR 308规范(Java类型规范上的注释)共同开发的。 该框架提供了自己的一组注释,这些注释可以应用于类型,外加大量“检查器”(注释处理器),这些“钩子”可以插入到编译过程中,并对Checker框架中包含的每种类型注释执行特定的“检查”。
由于类型注释不影响运行时操作,因此可以在项目中使用Java 8的类型注释,同时保持与Java早期版本的向后兼容性。
流API
Stream API提供了另一种“管道和过滤器”方法来处理集合。
在Java 8之前,您通常通过遍历集合并依次对每个元素进行操作来手动操作集合。 这种显式的循环需要很多样板,此外,在到达循环主体之前,很难掌握for循环的结构。
通过对数据执行一次运行,Stream API为您提供了一种更有效地处理数据的方式,无论您要处理的数据量是多少,或者是否执行多次计算。
在Java 8中,每个实现java.util.Collection
类都有一个stream
方法,该方法可以将其实例转换为Stream
对象。 例如,如果您有Array
:
String[] myArray = new String[]{"A", "B", "C", "D"};
然后,您可以使用以下命令将其转换为Stream:
Stream<String> myStream = Arrays.stream(myArray);
通过一系列计算步骤(称为流管道) ,Stream API通过携带来自源的值来处理数据。 流管道由以下部分组成:
- 源,例如
Collection
,数组或生成器函数。 - 零个或多个中间“惰性”操作。 在调用终端操作之前,中间操作不会开始处理元素,这就是为什么它们被认为是惰性的。 例如,在数据源上调用
Stream.filter()
只是建立流管道。 在调用终端操作之前,实际上不会进行任何过滤。 这样就可以将多个操作串在一起,然后在一次数据传递中执行所有这些计算。 中间操作将产生一个新的流(例如,filter
将产生一个包含已过滤元素的流), 而无需修改数据源,因此您可以*使用项目中其他地方的原始数据,也可以从同一源创建多个流。 - 终端操作,例如
Stream.forEach
。 当您调用终端操作时,所有中间操作将运行并产生一个新的流。 流无法存储元素,因此,一旦您调用终端操作,该流即被视为“已消耗”且不再可用。 如果您确实想重新访问流的元素,则需要从原始数据源生成一个新的流。
创建流
有多种方法可从数据源获取流,包括:
Stream.of()
根据单个值创建一个流:
Stream<String> stream = Stream.of("A", "B", "C");
IntStream.range()
从一系列数字创建流:
IntStream i = IntStream.range(0, 20);
Stream.iterate()
通过对每个元素重复应用运算符来创建流。 例如,在这里我们创建一个流,其中每个元素的值增加一:
Stream<Integer> s = Stream.iterate(0, n -> n + 1);
通过操作转换流
您可以使用大量操作对流执行功能样式的计算。 在本节中,我将仅介绍一些最常用的流操作。
地图
map()
操作将lambda表达式作为唯一参数,并使用此表达式转换流中每个元素的值或类型。 例如,以下代码为我们提供了一个新的流,其中每个String
都已转换为大写:
Stream<String> myNewStream =
myStream.map(s -> s.toUpperCase());
限制
此操作设置流大小的限制。 例如,如果您要创建一个最多包含五个值的新流,则可以使用以下内容:
List<String> number_string = numbers.stream()
.limit(5)
过滤
通过filter(Predicate<T>)
操作,您可以使用lambda表达式定义过滤条件。 此lambda表达式必须返回一个布尔值,该布尔值确定是否应在结果流中包含每个元素。 例如,如果您有一个字符串数组,并且想要过滤掉所有少于三个字符的字符串,则可以使用以下命令:
Arrays.stream(myArray)
.filter(s -> s.length() > 3)
.forEach(System.out::println);
}
已排序
此操作对流的元素进行排序。 例如,以下代码返回以升序排列的数字流:
List<Integer> list = Arrays.asList(10, 11, 8, 9, 22);
list.stream()
.sorted()
.forEach(System.out::println);
并行处理
除非您另外明确指定,否则所有流操作都可以串行或并行执行,尽管流是顺序的。 例如,以下将逐个处理每个元素:
Stream.of("a","b","c","d","e")
.forEach(System.out::print);
要并行执行一个流,您需要使用parallel()
方法将该流显式标记为并行:
Stream.of("a","b","c","d","e")
.parallel()
.forEach(System.out::print);
在后台,并行流使用Fork / Join Framework,因此可用线程数始终等于CPU中的可用核数。
并行流的缺点是每次执行代码时都可能涉及不同的内核,因此通常每次执行都会获得不同的输出。 因此,仅在处理顺序不重要时才应使用并行流,而在执行基于顺序的操作(如findFirst()
时应避免使用并行流。
终端机操作
您可以使用终端操作从流中收集结果,该操作始终是流方法链中的最后一个元素,并且始终返回除流之外的内容。
有几种不同类型的终端操作可以返回各种类型的数据,但是在本节中,我们将介绍两种最常用的终端操作。
收集
Collect
操作将所有已处理的元素收集到一个容器中,例如List
或Set
。 Java 8提供了一个Collectors
实用程序类,因此您不必担心实现Collectors
接口,以及许多常见的Collectors
工厂,包括toList()
, toSet()
和toCollection()
。
以下代码将生成仅包含红色形状的List
:
shapes.stream()
.filter(s -> s.getColor().equals("red"))
.collect(Collectors.toList());
另外,您可以将这些过滤后的元素收集到Set
:
.collect(Collectors.toSet());
每次
forEach()
操作对流的每个元素执行一些操作,使其成为流API的for-each语句的等效项。
如果您有items
列表,则可以使用forEach
打印此List
中包含的所有项目:
items.forEach(item->System.out.println(item));
在上面的示例中,我们使用了lambda表达式,因此可以使用方法引用以更少的代码执行相同的工作:
items.forEach(System.out::println);
功能介面
功能接口是仅包含一种抽象方法(称为功能方法)的接口。
单方法接口的概念并不新- Runnable
, Comparator
, Callable
,并OnClickListener
是这种接口的所有例子,虽然在Java中的早期版本中,他们被称为一个抽象方法接口(SAM接口)。
这不仅仅是简单的名称更改,因为与早期版本相比,在Java 8中使用功能(或SAM)接口的方式有一些显着差异。
在Java 8之前,您通常使用庞大的匿名类实现实例化功能接口。 例如,在这里我们使用匿名类创建Runnable
的实例:
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
正如我们在第1部分中回顾的那样,当您具有单方法接口时,可以使用lambda表达式而不是匿名类实例化该接口。 现在,我们可以更新此规则:您可以使用lambda表达式实例化功能接口 。 例如:
Runnable r = () -> System.out.println("My Runnable");
Java 8还引入了@FunctionalInterface
批注,使您可以将接口标记为功能接口:
@FunctionalInterface
public interface MyFuncInterface {
public void doSomething();
}
为了确保与Java的早期版本向后兼容, @FunctionalInterface
批注是可选的。 但是,建议您帮助确保正确实现功能接口。
如果您尝试在标记为@FunctionalInterface
的接口中实现两个或多个方法,则编译器将抱怨发现了多个非覆盖的抽象方法。 例如,以下内容不会编译:
@FunctionalInterface
public interface MyFuncInterface {
void doSomething();
//Define a second abstract method//
void doSomethingElse();
}
而且,如果您尝试编译包含零个方法的@FunctionInterface
接口,则将遇到“ 找不到目标方法”错误。
功能接口必须只包含一个抽象方法,但是由于默认方法和静态方法没有主体,因此它们被认为是非抽象的。 这意味着您可以在接口中包括多个默认方法和静态方法,将其标记为@FunctionalInterface
,它仍然可以编译。
Java 8还添加了一个java.util.function程序包 ,其中包含许多功能接口。 花点时间熟悉所有这些新功能接口是非常值得的,只是为了让您确切地知道开箱即用的功能。
JSR-310:Java的新日期和时间API
在Java中使用日期和时间从未如此简单,因为许多API省略了重要功能,例如时区信息。
Java 8引入了一个新的Date and Time API(JSR-310),旨在解决这些问题,但是不幸的是,在撰写本文时,Android平台不支持此API。 但是,您现在可以使用第三方库在Android项目中使用一些新的日期和时间功能。
在最后一部分中,我将向您展示如何设置和使用两个流行的第三方库,这些库可以在Android上使用Java 8的Date and Time API。
ThreeTen Android Backport
ThreeTen Android Backport (也称为ThreeTenABP)是流行的ThreeTen backport项目的改编,该项目提供了Java 6.0和Java 7.0的JSR-310的实现。 ThreeTenABP旨在提供对所有Date and Time API类(尽管具有不同的包名称)的访问,而无需向您的Android项目添加大量方法。
要开始使用此库,请打开模块级别的build.gradle文件,并将ThreeTenABP添加为项目依赖项:
dependencies {
//Add the following line//
compile 'com.jakewharton.threetenabp:threetenabp:1.0.5'
然后,您需要添加ThreeTenABP导入语句:
import com.jakewharton.threetenabp.AndroidThreeTen;
并在Application.onCreate()
方法中初始化时区信息:
@Override public void onCreate() {
super.onCreate();
AndroidThreeTen.init(this);
}
ThreeTenABP包含两个类,用于显示时间和日期信息的两种“类型”:
-
LocalDateTime
,它以2017-10-16T13:17:57.138的格式存储时间和日期 -
ZonedDateTime
,它是时区感知的,并以以下格式存储日期和时间信息: 2011-12-03T10:15:30 + 01:00 [欧洲/巴黎]
为了让您了解如何使用此库来检索日期和时间信息,让我们使用LocalDateTime
类显示当前日期和时间:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.jakewharton.threetenabp.AndroidThreeTen;
import android.widget.TextView;
import org.threeten.bp.LocalDateTime;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidThreeTen.init(getApplication());
setContentView(R.layout.activity_main);
TextView textView = new TextView(this);
textView.setText("Time: " + LocalDateTime.now());
setContentView(textView);
}
}
这不是显示日期和时间的最人性化的方式! 要将原始数据解析为更易于理解的内容,可以使用DateTimeFormatter
类并将其设置为以下值之一:
-
BASIC_ISO_DATE
。 将日期格式设置为2017-1016 + 01.00 -
ISO_LOCAL_DATE
。 将日期格式设置为2017-10-16 -
ISO_LOCAL_TIME
。 将时间格式化为14:58:43.242 -
ISO_LOCAL_DATE_TIME
。 将日期和时间格式设置为2017-10-16T14:58:09.616 -
ISO_OFFSET_DATE
。 将日期格式设置为2017-10-16 + 01.00 -
ISO_OFFSET_TIME
。 将时间格式化为14:58:56.218 + 01:00 -
ISO_OFFSET_DATE_TIME
。 将日期和时间格式设置为2017-10-16T14:5836.758 + 01:00 -
ISO_ZONED_DATE_TIME
。 将日期和时间格式设置为2017-10-16T14:58:51.324 + 01:00(欧洲/伦敦) -
ISO_INSTANT
。 将日期和时间格式设置为2017-10-16T13:52:45.246Z -
ISO_DATE
。 将日期格式设置为2017-10-16 + 01:00 -
ISO_TIME
。 将时间格式化为14:58:40.945 + 01:00 -
ISO_DATE_TIME
。 将日期和时间格式设置为2017-10-16T14:55:32.263 + 01:00(欧洲/伦敦) -
ISO_ORDINAL_DATE
。 将日期格式设置为2017-289 + 01:00 -
ISO_WEEK_DATE
。 将日期格式设置为2017-W42-1 + 01:00 -
RFC_1123_DATE_TIME
。 将日期和时间格式设置为2017年10月16日星期一14:58:43 + 01:00
在这里,我们正在更新我们的应用,以使用DateTimeFormatter.ISO_DATE
格式显示日期和时间:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.jakewharton.threetenabp.AndroidThreeTen;
import android.widget.TextView;
//Add the DateTimeFormatter import statement//
import org.threeten.bp.format.DateTimeFormatter;
import org.threeten.bp.ZonedDateTime;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidThreeTen.init(getApplication());
setContentView(R.layout.activity_main);
TextView textView = new TextView(this);
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
String formattedZonedDate = formatter.format(ZonedDateTime.now());
textView.setText("Time: " + formattedZonedDate);
setContentView(textView);
}
}
要以其他格式显示此信息,只需将DateTimeFormatter.ISO_DATE
替换为另一个值。 例如:
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
乔达时代
在Java 8之前,Joda-Time库被认为是处理Java中日期和时间的标准库,以至于Java 8的新Date and Time API实际上吸收了“ 从Joda-Time项目获得的经验 。”
尽管Joda-Time网站建议用户尽快迁移到Java 8 Date and Time,但由于Android当前不支持此API,因此Joda-Time仍然是Android开发的可行选择。 但是,请注意,Joda-Time确实具有较大的API并使用JAR资源加载时区信息,这两者都可能影响应用程序的性能。
要开始使用Joda-Time库,请打开模块级别的build.gradle文件并添加以下内容:
dependencies {
compile 'joda-time:joda-time:2.9.9'
...
...
...
Joda-Time库具有六个主要的日期和时间类:
-
Instant
:表示时间轴中的一个点; 例如,您可以通过调用Instant.now()
获得当前日期和时间。 -
DateTime
:JDK的Calendar
类的通用替代品。 -
LocalDate
:没有时间的日期,或对时区的任何引用。 -
LocalTime
:没有日期或任何时区参考的时间,例如14:00:00。 -
LocalDateTime
:本地日期和时间,仍然没有任何时区信息。 -
ZonedDateTime
:带有时区的日期和时间。
让我们看看如何使用Joda-Time打印日期和时间。 在下面的示例中,我重用了ThreeTenABP示例中的代码,因此,为了使事情变得更加有趣,我还使用了withZone
将日期和时间转换为ZonedDateTime
值。
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DateTime today = new DateTime();
//Return a new formatter (using withZone) and specify the time zone, using ZoneId//
DateTime todayNy = today.withZone(DateTimeZone.forID("America/New_York"));
TextView textView = new TextView(this);
textView.setText("Time: " + todayNy );
setContentView(textView);
}
}
您可以在Joda-Time官方文档中找到受支持的时区的完整列表。
结论
在本文中,我们研究了如何使用类型注释创建更健壮的代码,并探索了使用“管道和过滤器”的方法来使用Java 8的新Stream API进行数据处理。
我们还研究了接口在Java 8中的演变以及如何将其与我们在本系列中一直在探索的其他功能(包括lambda表达式和静态接口方法)结合使用。
总结一下,我向您展示了如何使用Joda-Time和ThreeTenABP项目访问默认情况下Android当前不支持的一些其他Java 8功能。
您可以在Oracle网站上了解有关Java 8发行版的更多信息。