在 Android 中使用 Java8 的特性
根據 Android 官網 的說明,在開發面向 Android N 的應用時,可以使用 Java8 語言功能。目前 Android 只支持一部分 Java8 的特性:
其中,只有前兩者可以兼容 API 23 以下的版本。
Lambda 表達式
從一個實際例子來引入 lamdba 的使用。
有一組 Person 對象(具體實現不復雜,參考 這里 ),需要通過年齡大小來過濾出滿足要求的對象,然后對其進行輸出操作,實現很簡單,如下:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
如果我的過濾條件變更了,就必須修改這個方法的代碼,比如我現在根據年齡上下限進行過濾:
public static void printPersonsWithinAgeRange(List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() <= high) {
p.printPerson();
}
}
}
這樣一來,過濾條件經常變更的話,需要頻繁修改這個方法。根據面向對象的思想,封裝變化,把經常改變的邏輯封裝起來,有外部來決定。這里我把過濾條件封裝到 CheckPerson 接口里,根據不同的過濾條件去實現這個接口即可。
@FunctionalInterface
public interface CheckPerson {
boolean test(Person p);
}
public static void printPersons(List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
// 實際使用
List<Person> roster = Person.createRoster(); // 制造一些數據
printPersons(roster, new CheckPerson() {
@Override
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
});
CheckPerson 接口是一個 函數式接口 (functional interface),即 僅有一個抽象方法 的接口。 因此實現這個接口的時候可以忽略掉方法名稱,使用 Lambda 表達式來替代匿名類。
printPersons(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
其實在 Java8 的包中,已經內置了一些標準的函數式接口。比如 CheckPerson 接收一個對象,然后輸出一個 boolean 值。可以使用 java.util.function.Predicate<T> 來替代,它相當于 RxJava 中的 Func1<T, Boolean> ,接收一個對象,返回布爾值。
public static void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
// 使用起來并沒有什么差別
printPersonsWithPredicate(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
這是,我并不滿足于僅僅把過濾條件封裝起來,還想把過濾之后對 Person 對象的操作也封裝起來,便于修改。可以用另外一個標準的函數式接口 java.util.function.Consumer<T> ,它相當于 RxJava 中的 Action1<T> ,接收一個對象,返回 void。
public static void processPersons(List<Person> roster, Predicate<Person> tester, Consumer<Person> block) {
for (Person person : roster) {
if (tester.test(person)) {
block.accept(person);
}
}
}
// 具體使用
processPersons(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
如果我的處理過程中有數據轉換的過程,可以用 java.util.function.Function<T, F> 將其封裝起來,這個接口相當于 RxJava 中的 Func1<T, F> ,接收一個類型的對象,返回另外個類型的對象,達到數據轉換的目的。比如例子中,把 Person 轉換成 String 對象。
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person person : roster) {
if (tester.test(person)) {
String data = mapper.apply(person);
block.accept(data);
}
}
}
// 實際使用
processPersonsWithFunction(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(), // 獲取 Person 對象的 email 字符串
email -> System.out.println(email)
);
最后可以把數據源也封裝成一個 java.lang.Iterable<T> 對象。
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function<X, Y> mapper,
Consumer<Y> block) {
for (X x : source) {
if (tester.test(x)) {
Y data = mapper.apply(x);
block.accept(data);
}
}
}
// 實際使用
Iterable<Person> source = roster;
Predicate<Person> tester = p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
Function<Person, String> mapper = p -> p.getEmailAddress();
Consumer<String> block = email -> System.out.println(email);
processElements(roster, tester, mapper, block);
在 Java8 中也可以把 Collections 對象快速轉換成 Stream 來使用方便的操作符。
roster.stream() // 獲取數據流
.filter( // 根據 Predicate 過濾數據
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress()) // 根據 Function 轉換數據
.forEach(email -> System.out.println(email)); // 對數據執行操作(消費數據)
Lambda 寫法
基本寫法:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
// 沒有參數
() -> System.out.println("Hello lambda")
// 參數多于 1 個
(x, y) -> x + y
參數列表,如果只有一個參數,可以省略掉括號,其他情況需要寫上一對括號。
需要注意的是, 箭頭 -> 后面必須是一個單獨的表達式(expression)或者是一個語句塊(statement block)。
// 表達式
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
// 代碼塊
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
方法引用
當你使用一個 lambda 表達式的時候,如果它僅僅是調用了一下已有的方法,并沒有做其他任何操作,就可以把它轉換成 方法引用 。方法引用有四種寫法,下面一一介紹。
// 先制造一些數據, 供后面的例子使用
List<Person> roster = Person.createRoster();
Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);
引用靜態方法
首先在 Person 類中有一個靜態方法,通過年齡比較大小:
// Person.java
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
//MethodReferencesTest.java
// 原來的寫法,傳入匿名類
Arrays.sort(rosterAsArray, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return Person.compareByAge(o1, o2);
}
});
// 寫成 lambda 形式
Arrays.sort(rosterAsArray, (a, b) -> Person.compareByAge(a, b));
// 轉換成方法引用 =>
Arrays.sort(rosterAsArray, Person::compareByAge);
引用具體實例的方法
class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider comparisonProvider = new ComparisonProvider();
// lambda 形式
Arrays.sort(rosterAsArray, (p1, p2) -> comparisonProvider.compareByAge(p1, p2));
// 轉換成方法引用 =>
Arrays.sort(rosterAsArray, comparisonProvider::compareByName);
引用特定類型的對象的實例方法
Person 實現一下 Comparable<T> 接口,會有一個 compareTo(Person) 方法。
public class Person implements Comparable<Person> {
@Override
public int compareTo(Person o) {
return Person.compareByAge(this, o); // 復用之前靜態方法的邏輯
}
// 其他忽略
}
// lambda 形式
Arrays.sort(rosterAsArray, (p1, p2) -> p1.compareTo(p2));
// 轉換成方法引用 =>
Arrays.sort(rosterAsArray, Person::compareTo);
引用構造方法
有一個 transferElements 方法,將 SOURCE 類型的集合轉換成 DEST 類型的集合。
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
DEST transferElements(
SOURCE sourceCollection,
Supplier<DEST> collectionFactory) {
DEST result = collectionFactory.get();
for (T t : sourceCollection) {
result.add(t);
}
return result;
}
其中 java.util.function.Supplier<T> 也是標準的函數式接口,它有一個 get() 方法來獲取所提供的對象。
// 匿名類形式
Set<Person> rosterSet = transferElements(roster, new Supplier<Set<Person>>() {
@Override
public Set<Person> get() {
return new HashSet<Person>();
}
});
// lambda 形式
Set<Person> rosterSet = transferElements(roster, () -> new HashSet<>());
// 轉換成方法引用 =>
Set<Person> rosterSet = transferElements(roster, HashSet::new);
lambda 表達式中直接 new 了一個 HashSet,相當于調用了 HashSet 的構造方法,故可以寫成 HashSet::new 方法引用的形式。
靜態和默認接口方法
在 Java8 之前,接口不允許有默認實現,如果接口的兩個實現類有同樣的實現邏輯,就得寫重復代碼了。現在接口可以通過關鍵字 default 實現默認方法,另外接口還可以實現靜態方法。
public interface SampleInterface {
default int test() {
System.out.println("SampleInterface default impl");
return staticTest() + 666;
}
static int staticTest() {
return 100;
}
}
public class SampleTest {
public static void main(String[] args) {
int test = new SampleInterfaceImpl1().test();
System.out.println(test);
int test2 = new SampleInterfaceImpl2().test();
System.out.println(test2);
}
static class SampleInterfaceImpl1 implements SampleInterface {
@Override
public int test() {
System.out.println("SampleInterfaceImpl1 override");
return SampleInterface.staticTest() + 233;
}
}
static class SampleInterfaceImpl2 implements SampleInterface {
// 不需要實現 test 方法
}
}
最后輸出結果:
SampleInterfaceImpl1 override
333
SampleInterface default impl
766
使用接口的默認方法可以減少代碼重復,靜態方法也可以方便地封裝一些通用邏輯。
重復注解
重復注解就是允許在同一申明類型(類,屬性,或方法)多次使用同一個注解。
@Repeatable(Schedules.class) // 指定存儲 Schedule 的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
String dayOfWeek() default "Mon";
String dayOfMonth() default "first";
int hour() default 12;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
Schedule[] value(); // 存儲 Schedule
}
使用時可以通過 AnnotatedElement.getAnnotationsByType() 方法來獲取到注解,然后進行相應的處理。
public class AnnotationTest {
@Schedule(dayOfMonth = "last")
@Schedule(dayOfWeek = "Fri", hour = 9)
public void doSomethingWork() {
System.out.println("doSomethingWork");
try {
Method method = AnnotationTest.class.getMethod("doSomethingWork");
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);
for (Schedule schedule : schedules) {
System.out.println("Schedule: " + schedule.dayOfWeek() + ", " + schedule.dayOfMonth() + ", " + schedule.hour());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new AnnotationTest().doSomethingWork();
}
}
輸出如下:
doSomethingWork
Schedule: Mon, last, 12
Schedule: Fri, first, 9
使用就是這么簡單~
在 Android 中使用這些特性
在主 module (app) 的 build.gradle 里配置,開啟 jack 編譯器,使用 Java8 進行編譯。 如果要體驗接口的默認方法等特性,minSdkVersion 需要指定為 24 (Android N)。
android {
compileSdkVersion 24
buildToolsVersion "24.0.0"
defaultConfig {
applicationId "me.brucezz.sharedelementdemo"
minSdkVersion 14
targetSdkVersion 24
versionCode 1
versionName "1.0"
// 開啟 jack 編譯
jackOptions {
enabled true
}
}
compileOptions {
// 指定用 Java8 編譯
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
Reference
- 文中代碼大部分來自于 Oracle 官方文檔教程
- 在 Android N 預覽版中使用 Java 8 的新特性
來自:http://brucezz.itscoder.com/articles/2016/10/05/use_java8_in_android/