根据 Android 官网的说明,在开发面向 Android N 的应用时,可以使用 Java8 语言功能。目前 Android 只支持一部分 Java8 的特性:
Lambda 表达式
方法引用
默认和静态接口方法
重复注解
其中,只有前两者可以兼容 API 23 以下的版本。
Lambda 表达式
从一个实际例子来引入 lamdba 的使用。
有一组 Person 对象(具体实现不复杂,参考这里),需要通过年龄大小来过滤出满足要求的对象,然后对其进行输出操作,实现很简单,如下:
publicstaticvoidprintPersonsOlderThan(List roster,intage){for(Person p : roster) {if(p.getAge() >= age) { p.printPerson(); } }}
如果我的过滤条件变更了,就必须修改这个方法的代码,比如我现在根据年龄上下限进行过滤:
publicstaticvoidprintPersonsWithinAgeRange(List roster,intlow,inthigh){for(Person p : roster) {if(low <= p.getAge() && p.getAge() <= high) { p.printPerson(); } }}
这样一来,过滤条件经常变更的话,需要频繁修改这个方法。根据面向对象的思想,封装变化,把经常改变的逻辑封装起来,有外部来决定。这里我把过滤条件封装到CheckPerson接口里,根据不同的过滤条件去实现这个接口即可。
@FunctionalInterfacepublicinterfaceCheckPerson{booleantest(Person p);}publicstaticvoidprintPersons(List roster, CheckPerson tester){for(Person p : roster) {if(tester.test(p)) { p.printPerson(); } }}// 实际使用List roster = Person.createRoster();// 制造一些数据printPersons(roster,newCheckPerson() {@Overridepublicbooleantest(Person p){returnp.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来替代,它相当于 RxJava 中的Func1,接收一个对象,返回布尔值。
publicstaticvoidprintPersonsWithPredicate(List roster, Predicate 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,它相当于 RxJava 中的Action1,接收一个对象,返回 void。
publicstaticvoidprocessPersons(List roster, Predicate tester, Consumer 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将其封装起来,这个接口相当于 RxJava 中的Func1,接收一个类型的对象,返回另外个类型的对象,达到数据转换的目的。比如例子中,把 Person 转换成 String 对象。
publicstaticvoidprocessPersonsWithFunction( List roster, Predicate tester,Function mapper, Consumer block) {for(Person person : roster) {if(tester.test(person)) {Stringdata = 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对象。
publicstatic void processElements( Iterable source, Predicate tester,Functionmapper,Consumerblock){for(X x : source) {if(tester.test(x)) { Y data = mapper.apply(x); block.accept(data); } }}// 实际使用Iterable source = roster;Predicate tester = p -> p.getGender() == Person.Sex.MALE && p.getAge() >=18&& p.getAge() <=25;Functionmapper=p->p.getEmailAddress();Consumer 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 -> {returnp.getGender() ==Person.Sex.MALE&& p.getAge() >=18&& p.getAge() <=25;}
方法引用
当你使用一个 lambda 表达式的时候,如果它仅仅是调用了一下已有的方法,并没有做其他任何操作,就可以把它转换成方法引用。方法引用有四种写法,下面一一介绍。
// 先制造一些数据, 供后面的例子使用List roster = Person.createRoster();Person[] rosterAsArray = roster.toArray(newPerson[roster.size()]);
引用静态方法
首先在 Person 类中有一个静态方法,通过年龄比较大小:
// Person.javapublicstaticintcompareByAge(Person a, Person b){returna.birthday.compareTo(b.birthday);}
//MethodReferencesTest.java// 原来的写法,传入匿名类Arrays.sort(rosterAsArray,newComparator() {@Overridepublicintcompare(Person o1, Person o2){returnPerson.compareByAge(o1, o2); }});// 写成 lambda 形式Arrays.sort(rosterAsArray, (a, b) -> Person.compareByAge(a, b));// 转换成方法引用 =>Arrays.sort(rosterAsArray, Person::compareByAge);
引用具体实例的方法
classComparisonProvider{publicintcompareByName(Person a, Person b){returna.getName().compareTo(b.getName()); }publicintcompareByAge(Person a, Person b){returna.getBirthday().compareTo(b.getBirthday()); }}ComparisonProvider comparisonProvider =newComparisonProvider();// lambda 形式Arrays.sort(rosterAsArray, (p1, p2) -> comparisonProvider.compareByAge(p1, p2));// 转换成方法引用 =>Arrays.sort(rosterAsArray, comparisonProvider::compareByName);
引用特定类型的对象的实例方法
Person 实现一下Comparable接口,会有一个compareTo(Person)方法。
publicclassPersonimplementsComparable{@OverridepublicintcompareTo(Person o){returnPerson.compareByAge(this, o);// 复用之前静态方法的逻辑}// 其他忽略}
// lambda 形式Arrays.sort(rosterAsArray, (p1, p2) -> p1.compareTo(p2));// 转换成方法引用 =>Arrays.sort(rosterAsArray,Person::compareTo);
引用构造方法
有一个transferElements方法,将SOURCE类型的集合转换成DEST类型的集合。
publicstatic, DEST extends Collection>DESTtransferElements(SOURCE sourceCollection,
Supplier collectionFactory){ DEST result = collectionFactory.get();for(T t : sourceCollection) { result.add(t); }returnresult;}
其中java.util.function.Supplier也是标准的函数式接口,它有一个get()方法来获取所提供的对象。
// 匿名类形式Set rosterSet = transferElements(roster,newSupplier>() { @Override publicSet get() {returnnewHashSet(); }});// lambda 形式Set rosterSet = transferElements(roster, () ->newHashSet<>());// 转换成方法引用 =>Set rosterSet = transferElements(roster, HashSet::new);
lambda 表达式中直接 new 了一个 HashSet,相当于调用了 HashSet 的构造方法,故可以写成HashSet::new方法引用的形式。
静态和默认接口方法
在 Java8 之前,接口不允许有默认实现,如果接口的两个实现类有同样的实现逻辑,就得写重复代码了。现在接口可以通过关键字default实现默认方法,另外接口还可以实现静态方法。
publicinterfaceSampleInterface{defaultinttest(){ System.out.println("SampleInterface default impl");returnstaticTest() +666; }staticintstaticTest(){return100; }}
publicclassSampleTest{publicstaticvoidmain(String[] args){inttest =newSampleInterfaceImpl1().test(); System.out.println(test);inttest2 =newSampleInterfaceImpl2().test(); System.out.println(test2); }staticclassSampleInterfaceImpl1implementsSampleInterface{@Overridepublicinttest(){ System.out.println("SampleInterfaceImpl1 override");returnSampleInterface.staticTest() +233; } }staticclassSampleInterfaceImpl2implementsSampleInterface{// 不需要实现 test 方法}}
最后输出结果:
SampleInterfaceImpl1override333SampleInterfacedefaultimpl766
使用接口的默认方法可以减少代码重复,静态方法也可以方便地封装一些通用逻辑。
重复注解
重复注解就是允许在同一申明类型(类,属性,或方法)多次使用同一个注解。
@Repeatable(Schedules.class)// 指定存储 Schedule 的注解@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceSchedule {StringdayOfWeek()default"Mon";StringdayOfMonth()default"first";inthour()default12;}@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceSchedules { Schedule[] value();// 存储 Schedule}
使用时可以通过AnnotatedElement.getAnnotationsByType()方法来获取到注解,然后进行相应的处理。
publicclassAnnotationTest{@Schedule(dayOfMonth ="last")@Schedule(dayOfWeek ="Fri", hour =9)publicvoiddoSomethingWork(){ 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(); } }publicstaticvoidmain(String[] args){newAnnotationTest().doSomethingWork(); }}
输出如下:
doSomethingWorkSchedule: Mon,last,12Schedule: Fri, first,9
使用就是这么简单~
在 Android 中使用这些特性
在主 module (app) 的build.gradle里配置,开启 jack 编译器,使用 Java8 进行编译。 如果要体验接口的默认方法等特性,minSdkVersion 需要指定为 24 (Android N)。
android { compileSdkVersion24buildToolsVersion"24.0.0"defaultConfig { applicationId"me.brucezz.sharedelementdemo"minSdkVersion14targetSdkVersion24versionCode1versionName"1.0"// 开启 jack 编译jackOptions { enabledtrue} } compileOptions {// 指定用 Java8 编译sourceCompatibilityJavaVersion.VERSION_1_8targetCompatibilityJavaVersion.VERSION_1_8}}
Reference
文中代码大部分来自于 Oracle 官方文档教程