写给Android开发者的Java 8简单入门教程

转载自:http://tangpj.com/2017/04/24/java8-inAndroid/


原创声明: 该文章为原创文章,未经博主同意严禁转载。
简介:Java 8是在2014年3月发布的,Android工程师为什么要关心Java 8呢?理由是Java 8所做的改变比Java历史上任何一次改变都要深远。Java 8对于程序员的主要好处在于它提供了更多的编程工具和概念,能以更快,更重要的是能以更为简洁、更易于维护的方式解决新的或现有的编程问题。我希望通过这篇文章,能让读者对Java 8产生兴趣,从而使用Java 8进行开发。

如何在Android Studio上应用Java 8?

这里首先需要说明下在Android Studio(下文中使用AS代指)上使用Java 8会遇到的坑和问题。
一般我们在AS上应用Java 8的方式是通过使用Jack来进行编译,使用方法如下:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
     
     
android {
...
defaultConfig {
...
jackOptions {
enabled true
}
}
compileOptions {
sourceCompatibility JavaVersion .VERSION_1_8
targetCompatibility JavaVersion .VERSION_1_8
}
}

当我们使用Jack的时候会导致我们无法使用AS的Instant Run和DataBinding功能,而且不支持接口的默认方法。这不能成为我们放弃使用Java 8的理由。当我们正准备愉快地使用Java 8时,Google突然在17年3月的某一天宣布放弃Jack,对的,我们的Google又弃坑了。
不过放心,Google在AS 2.4版本中提供了对Java 8的官方支持,在AS 2.4中使用Java 8不会产生任何负面的影响。AS 2.4提供了测试版供开发者使用,如果有兴趣的话可以提前体验AS 2.4的新功能,到目前为止笔者已经愉快地使用AS 2.4开发一周多了。AS 2.4的下载地址为:https://developer.android.com/studio/preview/index.html
在AS 2.4中使用Java 8的方法:

     
     
1
2
3
4
5
6
7
     
     
android {
...
compileOptions {
sourceCompatibility JavaVersion .VERSION_1_8
targetCompatibility JavaVersion .VERSION_1_8
}
}

我们为什么要使用Java 8?

在回答这个问题前,我们现在看一段简单的Java代码。代码的功能是把一堆苹果按照重量进行排序。
我们先来看看苹果类代码:

     
     
1
2
3
4
     
     
public class Apple {
private int weight;
private String color;
}

对苹果按照重量进行排序:

     
     
1
2
3
4
5
6
     
     
Collections .sort(apples, new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1 .getWeight() .compareTo(o2.getWeight());
}
});

在Java 8里,你可以编写更为简洁的代码,这些代码读起来更接近问题的描述:

     
     
1
     
     
apples .sort( Comparator .comparing( Apple ::getWeight));

上面这段代码非常简单,它念起来就是“对苹果排序,排序的条件是苹果的重量”。如果你研究过Lambda表达式的话,上面这段代码你就能够快速读懂,如果暂时理解不了的话,也没关系,笔者会慢慢带你深入了解的。

Java 8对硬件的影响:平常我们用的CPU都是多核的,在知乎上我看到过一句话就是:大部分Android App都没有充分利用多核的性能,具体是哪个问题的哪个回答我忘了,这句话十分有道理。因为绝大多数现有Java 程序都只使用其中一个内核,其它的都闲着,或者一小部分的处理能力来运行操作系统和一些系统相关的Service。
在Java 8之前,我们必须使用多线程才能使用多个内核。问题是,线程用起来很难,也很容易出错,当线程竞争同一资源时,性能会大打折扣。Java 5添加了工业级的构建模块,如线程池和并发集合。Java 7添加了分支/合并集合框架,使得并行变得更加实用,但使用起来依然很难,并且很容易产生无法预期的错误。而Java 8对并行有了一个更简单的思路,不过在使用的使用仍然需要遵守一些规则,下面笔者会谈到。

Java 8的新特性

Java从函数式编程中引入的两个核心思想:将方法和Lambda作为一等值,以及在没有可变共享时,函数或方法可以有效、安全地并发执行。
下面我们来简单介绍下Java 8的一些重要概念:

  1. Stream API
  2. 行为参数化
  3. 接口中的默认方法

流(Stream):

Stream是Java 8提供的新API,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来看,你可以把把他们看成遍历数据集合的高级迭代器。此外Stream还可以透明地并行处理(parallelStream方法)。
Stream API和Java 现有的Collection(集合)API的行为差不多:它们都能够访问数据项目的序列。它们的区别是:Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的运算。这里的关键点在于,Stream允许并提倡并行处理一个Stream中的元素。 如果需要处理的数据量十分庞大的话,推荐使用并行流来进行处理。
流的简短定义是:从支持数据处理操作的源生成的元素序列 。

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素。但流的目的在于表达计算,集合讲的是数据,流讲的是计算。
  • 源——流会使用一个提供数据的源,如集合、数组、输入流/输出资源。从有序集合生成流时会保留原有的顺序。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操纵,以函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可以并行。
  • 流水线——很多流操作本身会返回一个流,许多个操作就可以链接起来,形成一个大的流水线。流水线可以看作对数据源进行数据库式查询。
  • 内部迭代——与使用显式迭代的集合不同,流的迭代操作是在背后进行的。流只能遍历一次,遍历完之后,这个流就已经被消费掉了。可以从数据源中重新获取一个流。

流操作:

  • 中间操作:可以连接起来的流操作(返回结果是流)。
  • 终端操作:关闭流的操作称为终端操作(其返回结果不是流)。

行为参数化:

行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力
行为参数化主要有以下这些特点:

  • 行为参数化可以让代码更好地适应不断变化的要求,减轻未来的工作量。
  • 传递代码,是将新的行为作为参数传递给方法。但在Java8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成啰嗦的代码,在Java8之前可以用匿名类来减少。
  • Java API包含很多可以用不同行为参数化的方法,包括排序、线程和GUI处理等。

在Java 8中行为参数化主要是通过Lambda表达式和函数式接口来实现的。

Lambda概念
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有可以抛出异常的列表。

  • 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写的少而想得多。
  • 函数——Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
  • 传递——Lambda表达式可以作为参数传递给方法或存储在变量中。
  • 简洁——无需像匿名类那样写很多模版代码。

注:函数式接口是指只带有一个抽象方法的接口(这里说成抽象方法主要是为了区分默认方法)。Java 8中提供了一系列通用的函数式接口供我们使用,通过使用这些系统提供的函数式接口可以避免重复定义类似接口。如:在Android中的OnClickListener接口可以使用Java 8中的Supplier\接口代替。

接口中的默认方法

在Java 8之前,接口中的方法是不能带有方法实现对的,这就意味着,一旦当我们的接口发布出去后,就不能轻易更改接口。因为更改接口会导致所有实现了该接口的类都需要更改,这会带来无法估量的问题。Java 8提供了默认方法这一特性,我们可以对方法添加默认的实现,这样一来,实现类就不必实现/覆盖这一方法。我们可以通过前面对苹果排序的代码来加深理解。

     
     
1
2
3
4
5
6
     
     
Collections .sort(apples, new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1 .getWeight() .compareTo(o2.getWeight());
}
});

在Java 8里

     
     
1
     
     
apples .sort( Comparator .comparing( Apple ::getWeight));

我们通过对比得知,在Java 8之前,我们需要通过Collections类sort方法来对列表进行排序。这不符合我们的理解,因为按照人类的理解,对列表进行排序的话,应该是属于列表的方法,而不是通过引入第二个类来实现。但是在Java 8之前,由于在接口中增加方法会导致所有的实现类都报错,所以为了保证兼容性,引入了Collections来实现对列表的操作。
而在Java 8中,List接口中提供了默认方法sort,这个方法的实际效果和Collections.sort是一样的。我们可以来看看List接口中的sort方法的具体实现:

     
     
1
2
3
     
     
default void sort(Comparator<? super E> c) {
Collections. sort( this, c);
}

这个方法最终也是通过Collections.sort方法来实现排序的。通过这个方法,我们加深对默认方法的理解。
这里笔者要提醒大家的是:默认方法并不是我们的灵丹妙药,不能滥用默认方法。当我们发布一个公开的接口时,正确的做法是,做好充分的设计与验证才能进行发布。

Java 8的简单使用

我们通过一个简单的例子来介绍Java 8在Android上的应用。
定义一个菜肴列表,我们通过Stream对这个列表进行一些处理。
菜肴类:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
     
     
public class Dish {
public static final String MEAT = "MEAT";
public static final String OTHER = "OTHER";
public static final String FISH = "FISH";
@StringDef({MEAT,OTHER,FISH})
@Retention(RetentionPolicy.SOURCE)
public @interface Type{}
private final String name;
private final boolean vegetarian;
private final int calories;
private final @Type String type;
public Dish(String name,boolean vegetarian,int calories,@Type String type){
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName(){
return name;
}
public boolean isVegetarian(){
return vegetarian;
}
public int getCalories(){
return calories;
}
public @Type String getType(){
return type;
}
@Override
public String toString() {
return name;
}
}

菜肴菜单:

     
     
1
2
3
4
5
6
7
8
9
10
     
     
List<Dish> menu = Arrays.asList(
new Dish( "pork", false, 800,Dish.MEAT),
new Dish( "beef", false, 700,Dish.MEAT),
new Dish( "chicken", false, 400,Dish.MEAT),
new Dish( "french fries", true, 530,Dish.OTHER),
new Dish( "rice", true, 350,Dish.OTHER),
new Dish( "season fruit", true, 120,Dish.OTHER),
new Dish( "pizza", true, 550,Dish.OTHER),
new Dish( "prawns", false, 300,Dish.FISH),
new Dish( "salmon", false, 450,Dish.FISH));

现在我们需要实现一个功能:把卡路里大于300的菜肴筛选出来,并打印出菜肴名字。我们来看看普通实现和用Java 8实现的区别:
普通实现:

     
     
1
2
3
4
5
6
7
     
     
private void greaterThan300(){
for (Dish dish : menu){
if (dish.getCalories() > 300){
Log .d(TAG, "greaterThan300: " + dish.getName());
}
}
}

Java 8实现:

     
     
1
2
3
4
5
     
     
private void greaterThan300Java8(){
menu .stream()
.filter(dish -> dish.getCalories() > 300)
.forEach(name ->Log.d(TAG, "greaterThan300: " + name));
}

从上面的对比可以看到,使用Java 8编写的代码更直观,更符合逻辑思维,并且把循环和if去掉了。一旦习惯了Java 8的写法后,下面的代码我们一眼就能看出它的具体功能是什么,而上面的还需要花一点时间来理解。可能读者会觉得区别也不大,代码并没有少多少。那么我们再增加一个条件呢?例如把卡路里大于300的鱼类筛选出来,并打印出名字。

我们看看Java 8实现这个功能的代码

     
     
1
2
3
4
5
6
     
     
private void greaterThan300Java8(){
menu.stream()
. filter(dish -> dish.getCalories() > 300)
. filter(dish -> dish.getType() == Dish.FISH)
. forEach(name ->Log.d(TAG, "greaterThan300Java8: " + name));
}

我们运行这段代码会打印出:greaterThan300Java8: salmon

如果我们需要获得卡路里小于300的菜肴的名字列表,利用Java 8我们要如何做呢?

     
     
1
2
3
4
     
     
List< String> menuNames = menu.stream()
.filter(dish -> dish.getCalories() > 300)
. map(Dish ::getName)
.collect(Collectors.toList());

是不是非常简单?如果我们不用Java 8实现的话,就只能够使用嵌套if else的形式来实现了,这种实现方式会大大降低我们的代码可读性,并且比较容易出错。而使用Java 8由于我们没有保存任何中间变量,所以出错的可能性会更低。

在上面的代码中,filter和map操作符属于中间操作符,中间操作符是指对流进行操作后返回的对象是一个流;而forEach和collect是终端操作符,终端操作符是指返回对象不是流的操作。它们的共同点是接受的对象都是一个Lambda表达式(行为参数化)。Java 8提供了多个流操作符来供我们使用。

Java 8常用的函数式接口

函数式接口就是指只含有一个抽象方法的接口,而抽象方法的签名我们一般称为函数描述符。一般被设计为函数式接口的接口会用@FunctionalInterface标注标记,当然这不是强制实现的,但是通常使用这一标注是比较好的做法。
较为常用的函数式接口:

Predicate
Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean,在需要表示一个设计类型T的布尔表达式时,就可以使用这个接口。

Consumer
Consumer定义了一个名叫accept的抽象方法,它接受泛型T对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。

Function
Function接口定义了一个叫作apply的方法,它接受一个泛型T对象,并返回一个泛型R对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。

题外话:熟悉RxJava 2.0的同学应该不难看出,这些函数式接口的定义和RxJava 2.0中的十分类似,笔者推断RxJava 2.0中的一些标准应该是遵循Java 8标准的。

笔者整理了一个Java 8中的函数式接口表格供大家参考:

这里需要注意的一点是,原始类型特化。我们知道,普通类型如:int,double等是无法使用泛型的。所以我们需要用它们的装箱类来使用泛型,如Integer,Double。装箱使用泛型会带来一个问题就是运行效率的问题,装箱类的自动装箱和拆箱会大大影响程序的运行效率。所以在Java 8中提供了一系列特化类型的类和接口等,以消除自动装箱盒拆箱带来的的影响。

Java 8中常用的流操作符

上面的例子中,我们简单的介绍过流的操作符。在这里笔者整理了一个关于流的操作符的表格,方便读者使用的时候用来参考。

小结

从这篇文章可以得出一个结论:Java 8除了使我们编程变得更加简单外,还大大加强了代码的可读性。而对作为开发者的我们来说,使用Java 8能够简化代码的同时让我们专注于逻辑而不必写一堆模版代码。
这篇文章只是Java 8的简单的介绍文章,实际上Java 8提供的功能更加强大,有兴趣的读者可以继续深入了解下。大家也可以关注我的博客,我会不时发表一些关于Java 8文章的。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值