In this three-part series, we’ve been exploring all the major Java 8 features that you can start using in your Android projects today.
In Cleaner Code With Lambda Expressions, we focused on cutting boilerplate from your projects using lambda expressions, and then in Default and Static Methods, we saw how to make these lambda expressions more concise by combining them with method references. We also covered Repeating Annotations and how to declare non-abstract methods in your interfaces using default and static interface methods.
In this final post, we’re going to look at type annotations, functional interfaces, and how to take a more functional approach to data processing with Java 8’s new Stream API.
I’ll also show you how to access some additional Java 8 features that aren’t currently supported by the Android platform, using the Joda-Time and ThreeTenABP libraries.
Type Annotations
Annotations help you write code that’s more robust and less error-prone, by informing code inspection tools such as Lint about the errors they should be looking out for. These inspection tools will then warn you if a piece of code doesn’t conform to the rules laid out by these annotations.
Annotations aren’t a new feature (in fact, they date back to Java 5.0), but in previous versions of Java it was only possible to apply annotations to declarations.
With the release of Java 8, you can now use annotations anywhere you’ve used a type, including method receivers; class instance creation expressions; the implementation of interfaces; generics and arrays; the specification of throws
and implements
clauses; and type casting.
Frustratingly, although Java 8 does make it possible to use annotations in more locations than ever before, it doesn’t provide any annotations that are specific to types.
Android’s Annotations Support Library provides access to some additional annotations, such as @Nullable
, @NonNull
, and annotations for validating resource types such as @DrawableRes
, @DimenRes
, @ColorRes
, and @StringRes
. However, you may also want to use a third-party static analysis tool, such as the Checker Framework, which was co-developed with the JSR 308 specification (the Annotations on Java Types specification). This framework provides its own set of annotations that can be applied to types, plus a number of “checkers” (annotation processors) that hook into the compilation process and perform specific “checks” for each type annotation that’s included in the Checker Framework.
Since Type Annotations don’t affect runtime operation, you can use Java 8’s Type Annotations in your projects while remaining backwards compatible with earlier versions of Java.
Stream API
The Stream API offers an alternative, “pipes-and-filters” approach to collections processing.
Prior to Java 8, you manipulated collections manually, typically by iterating over the collection and operating on each element in turn. This explicit looping required a lot of boilerplate, plus it’s difficult to grasp the for-loop structure until you reach the body of the loop.
The Stream API gives you a way of processing data more efficiently, by performing a single run over that data—regardless of the amount of data you’re processing, or whether you’re performing multiple computations.
In Java 8, every class that implements java.util.Collection
has a stream
method that can convert its instances into Stream
objects. For example, if you have an Array
:
String[] myArray = new String[]{"A", "B", "C", "D"};
Then you can convert it into a Stream with the following:
StreammyStream = Arrays.stream(myArray);
The Stream API processes data by carrying values from a source, through a series of computational steps, known as a stream pipeline. A stream pipeline is composed of the following:
- A source, such as a
Collection
, array, or generator function. - Zero or more intermediate “lazy” operations. Intermediate operations don’t start processing elements until you invoke a terminal operation—which is why they’re considered lazy. For example, calling
Stream.filter()
on a data source merely sets up the stream pipeline; no filtering actually occurs until you call the terminal operation. This makes it possible to string multiple operations together, and then perform all of these computations in a single pass of the data. Intermediate operations produce a new stream (for example,filter
will produce a stream containing the filtered elements) without modifying the data source, so you’re free to use the original data elsewhere in your project, or create multiple streams from the same source. - A terminal operation, such as
Stream.forEach
. When you invoke the terminal operation, all of your intermediate operations will run and produce a new stream. A stream isn’t capable of storing elements, so as soon as you invoke a terminal operation, that stream is considered “consumed” and is no longer usable. If you do want to revisit the elements of a stream, then you’ll need to generate a new stream from the original data source.
Creating a Stream
There are various ways of obtaining a stream from a data source, including:
-
Stream.of()
Creates a stream from individual values:
Streamstream = Stream.of("A", "B", "C");
-
IntStream.range()
Creates a stream from a range of numbers:
IntStream i = IntStream.range(0, 20);
-
Stream.iterate()
Creates a stream by repeatedly applying an operator to each element. For example, here we’re creating a stream where each element increases in value by one:
Streams = Stream.iterate(0, n -> n + 1);
Transforming a Stream With Operations
There are a ton of operations that you can use to perform functional-style computations on your streams. In this section, I’m going to cover just a few of the most commonly used stream operations.
Map
The map()
operation takes a lambda expression as its only argument, and uses this expression to transform the value or the type of every element in the stream. For example, the following gives us a new stream, where every String
has been converted to uppercase:
StreammyNewStream = myStream.map(s -> s.toUpperCase());
Limit
This operation sets a limit on the size of a stream. For example, if you wanted to create a new stream containing a maximum of five values, then you’d use the following:
Listnumber_string = numbers.stream() .limit(5)
Filter
The filter(Predicate
operation lets you define filtering criteria using a lambda expression. This lambda expression must return a boolean value that determines whether each element should be included in the resulting stream. For example, if you had an array of strings and wanted to filter out any strings that contained less than three characters, you’d use the following:
Arrays.stream(myArray) .filter(s -> s.length() > 3) .forEach(System.out::println); }
Sorted
This operation sorts the elements of a stream. For example, the following returns a stream of numbers arranged in ascending order:
Listlist = Arrays.asList(10, 11, 8, 9, 22); list.stream() .sorted() .forEach(System.out::println);
Parallel Processing
All stream operations can execute in serial or in parallel, although streams are sequential unless you explicitly specify otherwise. For example, the following will process each element one by one:
Stream.of("a","b","c","d","e") .forEach(System.out::print);
To execute a stream in parallel, you need to explicitly mark that stream as parallel, using the parallel()
method:
Stream.of("a","b","c","d","e") .parallel() .forEach(System.out::print);
Under the hood, parallel streams use the Fork/Join Framework, so the number of available threads always equals the number of available cores in the CPU.
The drawback to parallel streams is that different cores may be involved each time the code is executed, so you’ll typically get a different output with each execution. Therefore, you should only use parallel streams when the processing order is unimportant, and avoid parallel streams when performing order-based operations, such as findFirst()
.
Terminal Operations
You collect the results from a stream using a terminal operation, which is always the last element in a chain of stream methods, and always returns something other than a stream.
There are a few different types of terminal operations that return various types of data, but in this section we’re going to look at two of the most commonly used terminal operations.
Collect
The Collect
operation gathers all the processed elements into a container, such as a List
or Set
. Java 8 provides a Collectors
utility class, so you don’t need to worry about implementing the Collectors
interface, plus factories for many common collectors, including toList()
, toSet()
, and toCollection()
.
The following code will produce a List
containing red shapes only:
shapes.stream() .filter(s -> s.getColor().equals("red")) .collect(Collectors.toList());
Alternatively, you could collect these filtered elements into a Set
:
.collect(Collectors.toSet());
forEach
The forEach()
operation performs some action on each element of the stream, making it the Stream API’s equivalent of a for-each statement.
If you had an items
list, then you could use forEach
to print all the items that are included in this List
:
items.forEach(item->System.out.println(item));
In the above example we’re using a lambda expression, so it’s possible to perform the same work in less code, using a method reference:
items.forEach(System.out::println);
Functional Interfaces
A functional interface is an interface that contains exactly one abstract method, known as the functional method.
The concept of single-method interfaces isn’t new—Runnable
, Comparator
, Callable
, and OnClickListener
are all examples of this kind of interface, although in previous versions of Java they were known as Single Abstract Method Interfaces (SAM interfaces).
This is more than a simple name change, as there are some notable differences in how you work with functional (or SAM) interfaces in Java 8, compared with earlier versions.
Prior to Java 8, you typically instantiated a functional interface using a bulky anonymous class implementation. For example, here we’re creating an instance of Runnable
using an anonymous class:
Runnable r = new Runnable(){ @Override public void run() { System.out.println("My Runnable"); }};
As we saw back in part one, when you have a single-method interface, you can instantiate that interface using a lambda expression, rather than an anonymous class. Now, we can update this rule: you can instantiate functional interfaces, using a lambda expression. For example:
Runnable r = () -> System.out.println("My Runnable");
Java 8 also introduces a @FunctionalInterface
annotation that lets you mark an interface as a functional interface:
@FunctionalInterface public interface MyFuncInterface { public void doSomething(); }
To ensure backwards compatibility with earlier versions of Java, the @FunctionalInterface
annotation is optional; however, it’s recommended to help ensure you’re implementing your functional interfaces correctly.
If you try to implement two or more methods in an interface that’s marked as @FunctionalInterface
, then the compiler will complain that it’s discovered multiple non-overriding abstract methods. For example, the following won’t compile:
@FunctionalInterface public interface MyFuncInterface { void doSomething(); //Define a second abstract method// void doSomethingElse(); }
And, if you try to compile a @FunctionInterface
interface that contains zero methods, then you’re going to encounter a No target method found error.
Functional interfaces must contain exactly one abstract method, but since default and static methods don’t have a body, they’re considered non-abstract. This means that you can include multiple default and static methods in an interface, mark it as @FunctionalInterface
, and it’ll still compile.
Java 8 also added a java.util.function package that contains lots of functional interfaces. It’s well worth taking the time to familiarize yourself with all of these new functional interfaces, just so you know exactly what’s available out of the box.
JSR-310: Java’s New Date and Time API
Working with date and time in Java has never been particularly straightforward, with many APIs omitting important functionality, such as time-zone information.
Java 8 introduced a new Date and Time API (JSR-310) that aims to resolve these issues, but unfortunately at the time of writing this API isn’t supported on the Android platform. However, you can use some of the new Date and Time features in your Android projects today, using a third-party library.
In this final section, I’m going to show you how to set up and use two popular third-party libraries that make it possible to use Java 8’s Date and Time API on Android.
ThreeTen Android Backport
ThreeTen Android Backport (also known as ThreeTenABP) is an adaption of the popular ThreeTen backport project, which provides an implementation of JSR-310 for Java 6.0 and Java 7.0. ThreeTenABP is designed to provide access to all the Date and Time API classes (albeit with a different package name) without adding a large number of methods to your Android projects.
To start using this library, open your module-level build.gradle file and add ThreeTenABP as a project dependency:
dependencies { //Add the following line// compile 'com.jakewharton.threetenabp:threetenabp:1.0.5'
You then need to add the ThreeTenABP import statement:
import com.jakewharton.threetenabp.AndroidThreeTen;
And initialize the time-zone information in your Application.onCreate()
method:
@Override public void onCreate() { super.onCreate(); AndroidThreeTen.init(this); }
ThreeTenABP contains two classes that display two “types” of time and date information:
-
LocalDateTime
, which stores a time and a date in the format 2017-10-16T13:17:57.138 -
ZonedDateTime
, which is time-zone aware and stores date and time information in the following format: 2011-12-03T10:15:30+01:00[Europe/Paris]
To give you an idea of how you’d use this library to retrieve date and time information, let’s use the LocalDateTime
class to display the current date and time:
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); } }
This isn’t the most user-friendly way of displaying the date and time! To parse this raw data into something more human-readable, you can use the DateTimeFormatter
class and set it to one of the following values:
-
BASIC_ISO_DATE
. Formats the date as 2017-1016+01.00 -
ISO_LOCAL_DATE
. Formats the date as 2017-10-16 -
ISO_LOCAL_TIME
. Formats the time as 14:58:43.242 -
ISO_LOCAL_DATE_TIME
. Formats the date and the time as 2017-10-16T14:58:09.616 -
ISO_OFFSET_DATE
. Formats the date as 2017-10-16+01.00 -
ISO_OFFSET_TIME
. Formats the time as 14:58:56.218+01:00 -
ISO_OFFSET_DATE_TIME
. Formats the date and time as 2017-10-16T14:5836.758+01:00 -
ISO_ZONED_DATE_TIME
. Formats the date and time as 2017-10-16T14:58:51.324+01:00(Europe/London) -
ISO_INSTANT
. Formats the date and time as 2017-10-16T13:52:45.246Z -
ISO_DATE
. Formats the date as 2017-10-16+01:00 -
ISO_TIME
. Formats the time as 14:58:40.945+01:00 -
ISO_DATE_TIME
. Formats the date and time as 2017-10-16T14:55:32.263+01:00(Europe/London) -
ISO_ORDINAL_DATE
. Formats the date as 2017-289+01:00 -
ISO_WEEK_DATE
. Formats the date as 2017-W42-1+01:00 -
RFC_1123_DATE_TIME
. Formats the date and time as Mon, 16 OCT 2017 14:58:43+01:00
Here, we’re updating our app to display the date and time with DateTimeFormatter.ISO_DATE
formatting:
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); } }
To display this information in a different format, simply substitute DateTimeFormatter.ISO_DATE
for another value. For example:
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
Joda-Time
Prior to Java 8, the Joda-Time library was considered the standard library for handling date and time in Java, to the point where Java 8’s new Date and Time API actually draws “heavily on experience gained from the Joda-Time project.”
While the Joda-Time website recommends that users migrate to Java 8 Date and Time as soon as possible, since Android doesn’t currently support this API, Joda-Time is still a viable option for Android development. However, note that Joda-Time does have a large API and loads time-zone information using a JAR resource, both of which can affect your app’s performance.
To start working with the Joda-Time library, open your module-level build.gradle file and add the following:
dependencies { compile 'joda-time:joda-time:2.9.9' ... ... ...
The Joda-Time library has six major date and time classes:
-
Instant
: Represents a point in the timeline; for example, you can obtain the current date and time by callingInstant.now()
. -
DateTime
: A general-purpose replacement for JDK’sCalendar
class. -
LocalDate
: A date without a time, or any reference to a time zone. -
LocalTime
: A time without a date or any reference to a time zone, for example 14:00:00. -
LocalDateTime
: A local date and time, still without any time-zone information. -
ZonedDateTime
: A date and time with a time zone.
Let’s take a look at how you’d print date and time using Joda-Time. In the following example I’m reusing code from our ThreeTenABP example, so to make things more interesting I’m also using withZone
to convert the date and time into a ZonedDateTime
value.
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); } }
You’ll find a full list of supported time zones in the official Joda-Time docs.
Conclusion
In this post, we looked at how to create more robust code using type annotations, and explored the “pipes-and-filters” approach to data processing with Java 8’s new Stream API.
We also looked at how interfaces have evolved in Java 8 and how to use them in combination with other features we’ve been exploring throughout this series, including lambda expressions and static interface methods.
To wrap things up, I showed you how to access some additional Java 8 features that Android currently doesn’t support by default, using the Joda-Time and ThreeTenABP projects.
You can learn more about the Java 8 release at Oracle’s website.
And while you’re here, check out some of our other posts about Java 8 and Android development!
-
Android SDKJava vs. Kotlin: Should You Be Using Kotlin for Android Development?
-
KotlinKotlin From Scratch: Variables, Basic Types, and Arrays
-
KotlinKotlin From Scratch: More Fun With Functions
-
Android SDKIntroduction to Android Architecture Components
-
Android SDKQuick Tip: Write Cleaner Code With Kotlin SAM Conversions
Powered by WPeMatico