时间:2017/08/09 21:34:36
其实JDK1.8 发布已经有相当长的时间了,网上也有很多对于JAVA8的解释新特性的文章,但是在实际开发过程中,个人还没有正式使用JAVA8进行开发。所以对此关心的较少,今天在开发任务较少的情况下,发现了JAVA8的新特性文章,还是惊讶于JAVA8的巨大升级。
1.简介
目前来看,JAVA8(发布于2014年3月19日)是自JAVA5(发布于2004年)以来,最为重大的一次版本升级。这个版本包含语言、编译器、库、工具和JVM等方面的十多个新特性。但同时虽然这些新特性领Java开发人员十分期待,但同时也需要花不少精力去学习。
- 接口的默认方法
- Lambda 表达式
- 函数式接口
- 方法与构造函数引用
- Lambda 作用域
- 访问局部变量
- 访问对象字段与静态变量
- 访问接口的默认方法
- Date API
- Annotation 注解
以上就是JAVA8的十大新特性。在此,我只对实际使用中对我们影响较大的几个特性做一些介绍。如果对详情感兴趣可以借鉴
JAVA8 十大新特性详解
2.Lambda表达式的使用
2.1介绍
其实在网上的一些代码之中已经有了很多lambda表达式使用的例子,非常的酷炫,但是具体是什么样的?
先来一段简单的代码:
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
这是一段很简单的排序代码,但是涉及到一个Java匿名内部类的书写之痛,排序方法真正核心的是compare方法内的实现,但是为了实现b.compareTo(a),我们写了很多不必要的代码,lambda表达式正是这种类型代码的救星。
下面我们再来看看同样的代码,使用lambda表达式的写法:
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
代码具有可读性,并且更短,但其实这段代码lambda表达式还可以写的更短:
Collections.sort(names, (String a, String b) -> {b.compareTo(a);});
甚至方法体中,只有一句代码的,我们可以省去大括号:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
由于JAVA8中可以根据上下文直接推导出参数类型,去掉参数类型也可以:
Collections.sort(names, (a,b) -> b.compareTo(a));
最终:我们的代码由原来的七行代码变为一行代码,虽然可读性降低了,但是逼格瞬间就提升了很多。
但是Lambda表达式的好处显而易见,弊端或者说是缺点也是有的。
2.2 使用限制
我们来分析一下Lambda实际使用的所需条件和使用规定:
- 匿名内部类;
- 类中所要重写的方法只能有一个;
- Lambda的作用域:在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量(在访问外层局部变量时虽然没有添加final关键字,但是我们可以理解为该变量为隐形final),或者实例的字段以及静态变量。即局部变量在lambda表达式中只可读不可写,而成员变量和静态变量既可读又可写;
2.3 使用场景
我们再实际使用过程中经常可以用到的场景:
- runnable接口
- 单实现的接口
- 条目点击事件
- 封装网络请求的回调
- 与RxJava的链式调用
//dosomething
}, 2000);
2.view.setOnClickListener(v -> {
//dosomething
});
3. listview.setOnItemClickListener((parent, view, position, id) -> {
//dosomething
});
4.例如通过封装OkHttp或者Retrofit的回调方法,将其转变成单实现的回调接口进行调用
5.Integer[] list = {1,2,3,4,5};
Observable
.from(list)
.filter(integer->integer%2==0)//挑选出偶数
.map(integer -> "number is"+integer)//转换成String
.subscribe(s->System.out.println(s));//相当于forEach(s->System.out.println(s));
//forEach是同步的 subscribe是异步的
2.4 Android Studio中使用Lambda的配置
- 首先确定你的AndroidStudio中使用的是jdk1.8的版本
- 在项目的build.gradle文件中添加
classpath 'me.tatarka:gradle-retrolambda:3.2.5'
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'me.tatarka:gradle-retrolambda:3.2.5'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3.在你的module目录下的build.gradle下添加
apply plugin: 'me.tatarka.retrolambda'
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
如下:
apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "ex.hxx.com.daggertest"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs 'libs'
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'}
4.使用成功
可以看到编译完成之后再可使用lambda的位置出现灰色提示
直接使用ALT+ENTER快捷键出现提示
回车确认
Android Studio智能的帮我们完成了一次lambda表达式的修改 继续修改匿名Runnable接口
顿时感觉代码清爽了很多。
3. JAVA8
3.1 Sream接口
Java 8 通过增加大量新类,扩展已有类的功能的方式来改善对并发编程、函数式编程、日期/时间相关操作以及其他更多方面的支持。而Stream API极大简化了集合框架的处理。是个人认为在JAVA8中最为重大的一个补充。
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。 Stream 的另外一大特点是,数据源本身可以是无限的。
3.1.1 Stream的构造
当前的stream对象的构造可以有以下几种方式:
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
//4.parallelStream;
List<String> list = Arrays.asList(strArray);
stream = list.parallelStream();
需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:
IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long>、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。
Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
3.2.2 Stream的操作符
Stream的操作符可以分为中间操作类和终止类,从字面意思来说,中间操作符代表操作完成仍然会返回该Stream对象并可以继续操作,终止符则代表完成该操作后关闭流,无法继续操作。
常见的中间操作符有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered
终止符有:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator 、groupingBy、partitioningBy
其实从操作符的字面意思大概就能看出所代表的操作功能。过滤、去重、排序、跳过、遍历、集合、最大、最小、总数等等,都是我们集合和数组操作常见的功能。
下面我们来看一下一个例子:
private final String title;
private final String author;
private final List<String> tags;
private Article(String title, String author, List<String> tags) {
this.title = title;
this.author = author;
this.tags = tags;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public List<String> getTags() {
return tags;
}
}
这是一个书籍的实体类,里面包含的字段有title:标题、author:作者、tags:标签这三个,然后对其进行一系列的操作:
例子1. 我们要在集合中查找包含“Java”标签的第一篇文章。
首先来看我们的一般写法:
for (Article article : articles) {
if (article.getTags().contains("Java")) {
return article;
}
}
return null;
}
然后是我们通过Stream的写法:
.filter(article -> article.getTags().contains("Java"))
.findFirst();
一句代码就已经完成。
例子2. 获取所有匹配的元素
还是先使用for循环进行:
for (Article article : articles) {
if (article.getTags().contains("Java")) {
result.add(article);
}
}
然后是Stream操作:
.filter(article -> article.getTags().contains("Java"))
.collect(Collectors.toList());
例子3.根据作者来把所有的文章分组
for循环:
for (Article article : articles) {
if (result.containsKey(article.getAuthor())) {
result.get(article.getAuthor()).add(article);
} else {
ArrayList<Article> articles = new ArrayList<>();
articles.add(article);
result.put(article.getAuthor(), articles);
}
}
Stream操作:
.collect(Collectors.groupingBy(Article::getAuthor));
Stream的操作还有很多可以开发的潜力,对于代码的简洁性有着巨大的提升,然而有优点,必然也有缺点,当前Stream的操作在网上的讨论中,对比for循环,Stream操作的性能较低,在一些核心代码中不建议使用。但这仍然不应该阻碍Stream操作对于我们平常代码的提升。
3.2 接口的默认方法与静态方法
3.2.1 接口的默认方法
Java 8用默认方法与静态方法这两个新概念来扩展接口的声明。默认方法使接口有点像Traits(Scala中特征(trait)类似于Java中的Interface,但它可以包含实现代码,也就是目前Java8新增的功能),但与传统的接口又有些不一样,它允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。
默认方法其实就是在接口的中添加一个且只能有一个以default标注的方法。
默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。让我们看看下面的例子:
// Interfaces now allow default methods, the implementer may or
// may not implement (override) them.
default String notRequired() {
return "Default implementation";
}
}
private static class DefaultableImpl implements Defaulable {
}
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}
Defaulable接口用关键字default声明了一个默认方法notRequired(),Defaulable接口的实现者之一DefaultableImpl实现了这个接口,并且让默认方法保持原样。Defaulable接口的另一个实现者OverridableImpl用自己的方法覆盖了默认方法。
Java 8带来的另一个有趣的特性是接口可以声明(并且可以提供实现)静态方法。例如:
// Interfaces now allow static methods
static Defaulable create( Supplier< Defaulable > supplier ) {
return supplier.get();
}
}
下面的一小段代码片段把上面的默认方法与静态方法黏合到一起。
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
System.out.println( defaulable.notRequired() );
defaulable = DefaulableFactory.create( OverridableImpl::new );
System.out.println( defaulable.notRequired() );
}
最终控制台的结果如下:代码的运行逻辑还是清晰的。
Overridden implementation
3.2.2 方法引用
public static Car create( final Supplier< Car > supplier ) {
return supplier.get();
}
public static void collide( final Car car ) {
System.out.println( "Collided " + car.toString() );
}
public void follow( final Car another ) {
System.out.println( "Following the " + another.toString() );
}
public void repair() {
System.out.println( "Repaired " + this.toString() );
}
}
构造方法引用
它的语法是Class::new,或者更一般的Class< T >::new。请注意构造方法没有参数。
Car car = Car::new ;
静态方法引用
第二种方法引用是静态方法引用,它的语法是Class::static_method。请注意这个方法接受一个Car类型的参数。
cars.forEach( Car::collide );
这么看有点难以理解,那么我们把它展开:其实也比较容易理解,forEach中为匿名内部类中提供了一个car的参数,而Car.collide刚好需要这个car对象作为参数,所以,双方同时省去 car这个对象,就形成了上文中的代码语义。
@Override
public void accept(Car car) {
Car.collide(car);
}
});
这种静态方法的引用,其方法所需要的参数数量及参数类型,需要和上下文相一致。即不一定非要规定是一个参数。如:
public enum Sex{
MALE,FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public String getEmailAddress() {
return emailAddress;
}
public Sex getGender() {
return gender;
}
public LocalDate getBirthday() {
return birthday;
}
public String getName() {
return name;
}
public static int compareByAge(Person a,Person b){
return a.birthday.compareTo(b.birthday);
}
}
该静态方法中所需要的参数为两个,上下文中提供了两个,所以语义正确
Arrays.sort(persons, Person::compareByAge);
我们把该方法展开:sort方法提供了person o1,和person o2两个参数,而这两个参数则刚好是Person.compareByAge这个方法所需要的两个参数,所以双方同时省去,形成上文中的代码,语义相同。
Arrays.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return Person.compareByAge(o1, o2);
}
});
特定类的任意对象的方法引用
它的语法是Class::method。请注意,这个方法没有参数。
cars.forEach( Car::repair );
如果不是静态方法,这种调用方式,method必须为无参的方法。
特定对象的方法引用
它的语法是instance::method。请注意,这个方法接受一个Car类型的参数
cars.forEach( car::follow );
对象的方法引用和静态类的方法引用规则是一样的,则提供的参数类型和数量必须和方法的参数类型和方法相吻合。参数数量不定,但提供方和消费方必须等同起来。
当前这几种方法引用的使用次数不是太多,也不是很常见,但如果遇到,也要知道是什么意思。