在 Android 中使用 Java8 的特性

Brucezz發表於2016-10-11

根據 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

相關文章