Java 8 was a huge step forward for the programming language and now, with the release of Android Studio 3.0, Android developers finally have access to built-in support for some of Java 8’s most important features.
In this three-part series, we’ve been exploring the Java 8 features you can start using in your Android projects today. In Cleaner Code With Lambda Expressions, we set up our development to use the Java 8 support provided by Android’s default toolchain, before taking an in-depth look at lambda expressions.
In this post, we’ll look at two different ways that you can declare non-abstract methods in your interfaces (something that wasn’t possible in earlier versions of Java). We’ll also answer the question of, now that interfaces can implement methods, what exactly is the difference between abstract classes and interfaces?
We’ll also be covering a Java 8 feature that gives you the freedom to use the same annotation as many times as you want in the same location, while remaining backwards compatible with earlier versions of Android.
But first, let’s take a look at a Java 8 feature that’s designed to be used in combination with the lambda expressions we saw in the previous post.
Write Cleaner Lambda Expressions, With Method References
In the last post, you saw how you can use lambda expressions to remove lots of boilerplate code from your Android applications. However, when a lambda expression is simply calling a single method that already has a name, you can cut even more code from your project by using a method reference.
For example, this lambda expression is really just redirecting work to an existing handleViewClick
method:
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(view -> handleViewClick(view)); } private void handleViewClick(View view) { }
In this scenario, we can refer to this method by name, using the ::
method reference operator. You create this kind of method reference, using the following format:
Object/Class/Type::methodName
In our Floating Action Button example, we can use a method reference as the body of our lambda expression:
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(this::handleViewClick); }
Note that the referenced method must take the same parameters as the interface—in this instance, that’s View
.
You can use the method reference operator (::
) to reference any of the following:
A Static Method
If you have a lambda expression that calls a static method:
(args) -> Class.staticMethod(args)
Then you can turn it into a method reference:
Class::staticMethodName
For example, if you had a static method PrintMessage
in a MyClass
class, then your method reference would look something like this:
public class myClass { public static void PrintMessage(){ System.out.println("This is a static method"); } } public static void main(String[] args) { Thread thread = new Thread(myClass::PrintMessage); thread.start(); } }
An Instance Method of a Specific Object
This is an instance method of an object that’s known ahead of time, allowing you to replace:
(arguments) -> containingObject.instanceMethodName(arguments)
With:
containingObject::instanceMethodName
So, if you had the following lambda expression:
MyClass.printNames(names, x -> System.out.println(x));
Then introducing a method reference would give you the following:
MyClass.printNames(names,System.out::println);
An Instance Method of an Arbitrary Object of a Particular Type
This is an instance method of an arbitrary object that will be supplied later, and written in the following format:
ContainingType::methodName
Constructor References
Constructor references are similar to method references, except that you use the keyword new
to invoke the constructor. For example, Button::new
is a constructor reference for the class Button
, although the exact constructor that’s invoked depends on the context.
Using constructor references, you can turn:
(arguments) -> new ClassName(arguments)
Into:
ClassName::new
For example, if you had the following MyInterface
interface:
public Interface myInterface{ public abstract Student getStudent(String name, Integer age); }
Then you could use constructor references to create new Student
instances:
myInterface stu1 = Student::new; Student stu = stu1.getStudent("John Doe", 27);
It’s also possible to create constructor references for array types. For example, a constructor reference for an array of int
s is int[]::new
.
Add Default Methods to Your Interfaces
Prior to Java 8, you could only include abstract methods in your interfaces (i.e. methods without a body), which made it difficult to evolve interfaces, post-publication.
Every time you added a method to an interface definition, any classes that implemented this interface would suddenly be missing an implementation. For example, if you had an interface (MyInterface
) that was used by MyClass
, then adding a method to MyInterface
would break compatibility with MyClass
.
In the best case scenario where you were responsible for the small number of classes that used MyInterface
, this behaviour would be annoying but manageable—you’d just have to set aside some time to update your classes with the new implementation. However, things could become much more complicated if a large number of classes implemented MyInterface
, or if the interface was used in classes that you weren’t responsible for.
While there were a number of workarounds for this problem, none of them were ideal. For example, you could include new methods in an abstract class, but this would still require everyone to update their code to extend this abstract class; and, while you could extend the original interface with a new interface, anyone who wanted to use these new methods would then need to rewrite all their existing interface references.
With the introduction of default methods in Java 8, it’s now possible to declare non-abstract methods (i.e. methods with a body) inside your interfaces, so you can finally create default implementations for your methods.
When you add a method to your interface as a default method, any class that implements this interface doesn’t necessarily need to provide its own implementation, which gives you a way of updating your interfaces without breaking compatibility. If you add a new method to an interface as a default method, then every class that uses this interface but doesn’t provide its own implementation will simply inherit the method’s default implementation. Since the class isn’t missing an implementation, it’ll continue to function as normal.
In fact, the introduction of default methods was the reason that Oracle was able to make such a large number of additions to the Collections API in Java 8.
Collection
is a generic interface that’s used in many different classes, so adding new methods to this interface had the potential to break countless lines of code. Rather than adding new methods to the Collection
interface and breaking every class that was derived from this interface, Oracle created the default method feature, and then added these new methods as default methods. If you take a look at the new Collection.Stream() method (which we’ll be exploring in detail in part three), you’ll see that it was added as a default method:
default Streamstream() { return StreamSupport.stream(spliterator(), false); }
Creating a default method is simple—just add the default
modifier to your method signature:
public interface MyInterface { void interfaceMethod(); default void defaultMethod(){ Log.i(TAG,"This is a default method”); } }
Now, if MyClass
uses MyInterface
but doesn’t provide its own implementation of defaultMethod
, it’ll just inherit the default implementation provided by MyInterface
. For example, the following class will still compile:
public class MyClass extends AppCompatActivity implements MyInterface { }
An implementing class can override the default implementation provided by the interface, so classes are still in complete control of their implementations.
While default methods are a welcome addition for API designers, they can occasionally cause a problem for developers who are trying to use multiple interfaces in the same class.
Imagine that in addition to MyInterface
, you have the following:
public interface SecondInterface { void interfaceMethod(); default void defaultMethod(){ Log.i(TAG,"This is also a default method”); } }
Both MyInterface
and SecondInterface
contain a default method with exactly the same signature (defaultMethod
). Now imagine you try to use both of these interfaces in the same class:
public class MyClass extends AppCompatActivity implements MyInterface, SecondInterface { }
At this point you have two conflicting implementations of defaultMethod
, and the compiler has no idea which method it should use, so you’re going to encounter a compiler error.
One way to resolve this problem is to override the conflicting method with your own implementation:
public class MyClass extends AppCompatActivity implements MyInterface, SecondInterface { public void defaultMethod(){ } }
The other solution is to specify which version of defaultMethod
you want to implement, using the following format:
.super. ();
So if you wanted to call the MyInterface#defaultMethod()
implementation, then you’d use the following:
public class MyClass extends AppCompatActivity implements MyInterface, SecondInterface { public void defaultMethod(){ MyInterface.super.defaultMethod(); } }
Using Static Methods in Your Java 8 Interfaces
Similar to default methods, static interface methods give you a way of defining methods inside an interface. However, unlike default methods, an implementing class cannot override an interface’s static methods.
If you have static methods that are specific to an interface, then Java 8’s static interface methods give you a way of placing these methods inside the corresponding interface, rather than having to store them in a separate class.
You create a static method by placing the keyword static
at the beginning of the method signature, for example:
public interface MyInterface { static void staticMethod(){ System.out.println("This is a static method"); } }
When you implement an interface that contains a static interface method, that static method is still part of the interface and isn’t inherited by the class implementing it, so you’ll need to prefix the method with the interface name, for example:
public class MyClass extends AppCompatActivity implements MyInterface { public static void main(String[] args) { MyInterface.staticMethod(); ... ... ...
This also means that a class and an interface can have a static method with the same signature. For example, using MyClass.staticMethod
and MyInterface.staticMethod
in the same class won’t cause a compile-time error.
So, Are Interfaces Essentially Just Abstract Classes?
The addition of static interface methods and default methods has led some developers to question whether Java interfaces are becoming more like abstract classes. However, even with the addition of default and static interface methods, there are still some notable differences between interfaces and abstract classes:
- Abstract classes can have final, non-final, static and non-static variables, whereas an interface can only have static and final variables.
- Abstract classes allow you to declare fields that are not static and final, whereas an interface’s fields are inherently static and final.
- In interfaces, all methods that you declare or define as default methods are inherently public, whereas in abstract classes you can define public, protected, and private concrete methods.
- Abstract classes are classes, and therefore can have state; interfaces cannot have state associated with them.
- You can define constructors inside an abstract class, something that’s not possible inside Java interfaces.
- Java only allows you to extend one class (regardless of whether it’s abstract), but you’re free to implement as many interfaces as you require. This means that interfaces typically have the edge when you require multiple inheritance, although you do need to beware the deadly diamond of death!
Apply the Same Annotation as Many Times as You Want
Traditionally, one of the limitations of Java annotations has been that you cannot apply the same annotation more than once in the same location. Try to use the same annotation multiple times, and you’re going to encounter a compile-time error.
However, with the introduction of Java 8’s repeating annotations, you’re now free to use the same annotation as many times as you want in the same location.
In order to ensure your code remains compatible with earlier versions of Java, you’ll need to store your repeating annotations in a container annotation.
You can tell the compiler to generate this container, by completing the following steps:
- Mark the annotation in question with the
@Repeatable
meta-annotation (an annotation that’s used to annotate an annotation). For example, if you wanted to make the@ToDo
annotation repeatable, you’d use:@Repeatable(ToDos.class)
. The value in parentheses is the type of container annotation that the compiler will eventually generate. - Declare the containing annotation type. This must have an attribute that’s an array of the repeating annotation type, for example:
public @interface ToDos { ToDo[] value(); }
Attempting to apply the same annotation multiple times without first declaring that it’s repeatable will result in an error at compile-time. However, once you’ve specified that this is a repeatable annotation, you can use this annotation multiple times in any location where you’d use a standard annotation.
Conclusion
In this second part of our series on Java 8, we saw how you can cut even more boilerplate code from your Android projects by combining lambda expressions with method references, and how to enhance your interfaces with default and static methods.
In the third and final installment, we’ll be looking at a new Java 8 API that lets you process huge amounts of data in a more efficient, declarative manner, without having to worry about concurrency and thread management. We’ll also be tying together a few of the different features we’ve discussed throughout this series, by exploring the role that Functional Interfaces have to play in lambda expressions, static interface methods, default methods, and more.
And finally, even though we’re still waiting for Java 8’s new Date and Time API to officially arrive on Android, I’ll show how you can start using this new API in your Android projects today, with the help of some third-party libraries.
In the meantime, check out some of our other posts on Java and Android app development!
-
Android SDKJava vs. Kotlin: Should You Be Using Kotlin for Android Development?
-
KotlinKotlin From Scratch: Variables, Basic Types, and Arrays
-
Android SDKIntroduction to Android Architecture Components
Powered by WPeMatico