欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

适用于Android开发的Java 8:流API和日期和时间库

程序员文章站 2022-05-30 14:33:22
...

在这个由三部分组成的系列文章中,我们一直在探索您可以在今天的Android项目中开始使用的所有主要Java 8功能。

使用Lambda表达式的Cleaner Code中 ,我们专注于使用lambda表达式从您的项目中删除样板,然后在Default和Static Methods中 ,我们看到了如何通过将它们与方法引用结合来使这些Lambda表达式更简洁。 我们还介绍了重复注释,以及如何使用默认和静态接口方法在您的接口中声明非抽象方法。

在最后一篇文章中,我们将研究类型注释,功能接口,以及如何使用Java 8的新Stream API采取更具功能性的方法进行数据处理。

我还将向您展示如何使用Joda-TimeThreeTenABP库访问Android平台当前不支持的其他Java 8功能。

类型注释

通过将诸如Lint之类的代码检查工具告知他们应该寻找的错误,注释可以帮助您编写更健壮且更不易出错的代码。 然后,如果一段代码不符合这些注释所列出的规则,这些检查工具将向您发出警告。

注释并不是一个新功能(实际上,它们可以追溯到Java 5.0),但是在Java的早期版本中,只能将注释应用于声明。

在Java 8发行版中,您现在可以在使用类型的任何地方使用注释,包括方法接收器; 类实例创建表达式; 接口的实现; 泛型和数组; throwsimplements子句的规范; 并进行类型转换。

令人沮丧的是,尽管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操作将所有已处理的元素收集到一个容器中,例如ListSet 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);

功能介面

功能接口是仅包含一种抽象方法(称为功能方法)的接口。

单方法接口的概念并不新- RunnableComparatorCallable ,并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);

    }
  }
适用于Android开发的Java 8:流API和日期和时间库

这不是显示日期和时间的最人性化的方式! 要将原始数据解析为更易于理解的内容,可以使用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);

   }
}
适用于Android开发的Java 8:流API和日期和时间库

您可以在Joda-Time官方文档中找到受支持的时区的完整列表

结论

在本文中,我们研究了如何使用类型注释创建更健壮的代码,并探索了使用“管道和过滤器”的方法来使用Java 8的新Stream API进行数据处理。

我们还研究了接口在Java 8中的演变以及如何将其与我们在本系列中一直在探索的其他功能(包括lambda表达式和静态接口方法)结合使用。

总结一下,我向您展示了如何使用Joda-Time和ThreeTenABP项目访问默认情况下Android当前不支持的一些其他Java 8功能。

您可以在Oracle网站上了解有关Java 8发行版的更多信息。

翻译自: https://code.tutsplus.com/tutorials/java-8-for-android-development-stream-api-and-date-time-libraries--cms-29904