扣丁学堂Java培训之Java 8新特性详解

2018-05-22 14:25:27 576浏览

今天扣丁学堂给大家介绍一下关于Java8新特性,并将使用简单的代码示例来指导你如何使用默认接口方法,lambda表达式,方法引用以及多重Annotation,之后你将会学到最新的API上的改进,比如流,函数式接口,Map以及全新的日期API等内容,下面我们一起来看一下吧。

一、接口的默认方法

Java8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可,这个特征又叫做扩展方法,示例如下:

interfaceFormula{
doublecalculate(inta);
defaultdoublesqrt(inta){
returnMath.sqrt(a);
}
}

Formula接口在拥有calculate方法之外同时还定义了sqrt方法,实现了Formula接口的子类只需要实现一个calculate方法,默认方法sqrt将在子类上可以直接使用。

代码如下:

Formulaformula=newFormula(){
@Override
publicdoublecalculate(inta){
returnsqrt(a*100);
}
};
formula.calculate(100);//100.0
formula.sqrt(16);//4.0

文中的formula被实现为一个匿名类的实例,该代码非常容易理解,6行代码实现了计算sqrt(a*100)。在下一节中,我们将会看到实现单方法接口的更简单的做法。

译者注:在Java中只有单继承,如果要让一个类赋予新的特性,通常是使用接口来实现,在C++中支持多继承,允许一个子类同时具有多个父类的接口与功能,在其他语言中,让一个类同时具有其他的可复用代码的方法叫做mixin。新的Java8的这个特新在编译器实现的角度上来说更加接近Scala的trait。在C#中也有名为扩展方法的概念,允许给已存在的类型扩展方法,和Java8的这个在语义上有差别。

二、Lambda表达式

首先看看在老版本的Java中是如何排列字符串的:

代码如下:

List<String>names=Arrays.asList("peter","anna","mike","xenia");
Collections.sort(names,newComparator<String>(){
@Override
publicintcompare(Stringa,Stringb){
returnb.compareTo(a);
}
});

只需要给静态方法Collections.sort传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。

在Java8中你就没必要使用这种传统的匿名对象的方式了,Java8提供了更简洁的语法,lambda表达式:

代码如下:

Collections.sort(names,(Stringa,Stringb)->{
returnb.compareTo(a);
});

看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短:

代码如下:

Collections.sort(names,(Stringa,Stringb)->b.compareTo(a));

对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字,但是你还可以写得更短点:

代码如下:
Collections.sort(names,(a,b)->b.compareTo(a));

Java编译器可以自动推导出参数类型,所以你可以不用再写一次类型。接下来我们看看lambda表达式还能作出什么更方便的东西来:

三、函数式接口

Lambda表达式是如何在java的类型系统中表示的呢?每一个lambda表达式都对应一个类型,通常是接口类型。而“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。因为默认方法不算抽象方法,所以你也可以给你的函数式接口添加默认方法。

我们可以将lambda表达式当作任意只包含一个抽象方法的接口类型,确保你的接口一定达到这个要求,你只需要给你的接口添加@FunctionalInterface注解,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的。

示例如下:

@FunctionalInterface
interfaceConverter<F,T>{
Tconvert(Ffrom);
}
Converter<String,Integer>converter=(from)->Integer.valueOf(from);
Integerconverted=converter.convert("123");
System.out.println(converted);//123

需要注意如果@FunctionalInterface如果没有指定,上面的代码也是对的。

译者注将lambda表达式映射到一个单方法的接口上,这种做法在Java8之前就有别的语言实现,比如RhinoJavaScript解释器,如果一个函数参数接收一个单方法的接口而你传递的是一个function,Rhino解释器会自动做一个单接口的实例到function的适配器,典型的应用场景有org.w3c.dom.events.EventTarget的addEventListener第二个参数EventListener。

四、方法与构造函数引用

前一节中的代码还可以通过静态方法引用来表示:

代码如下:

Converter<String,Integer>converter=Integer::valueOf;
Integerconverted=converter.convert("123");
System.out.println(converted);//123

Java8允许你使用::关键字来传递方法或者构造函数引用,上面的代码展示了如何引用一个静态方法,我们也可以引用一个对象的方法:

代码如下:
converter=something::startsWith
Stringconverted=converter.convert("Java");
System.out.println(converted);//"J"

接下来看看构造函数是如何使用::关键字来引用的,首先我们定义一个包含多个构造函数的简单类:

代码如下:

classPerson{
StringfirstName;
StringlastName;
Person(){}
Person(StringfirstName,StringlastName){
this.firstName=firstName;
this.lastName=lastName;
}
}

接下来我们指定一个用来创建Person对象的对象工厂接口:

代码如下:

interfacePersonFactory<PextendsPerson>{
Pcreate(StringfirstName,StringlastName);
}

这里我们使用构造函数引用来将他们关联起来,而不是实现一个完整的工厂:

代码如下:
PersonFactory<Person>personFactory=Person::new;
Personperson=personFactory.create("Peter","Parker");

我们只需要使用Person::new来获取Person类构造函数的引用,Java编译器会自动根据PersonFactory.create方法的签名来选择合适的构造函数。

五、Lambda作用域

在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。

六、访问局部变量

我们可以直接在lambda表达式中访问外层的局部变量:

代码如下:

finalintnum=1;
Converter<Integer,String>stringConverter=
(from)->String.valueOf(from+num);
stringConverter.convert(2);//3

但是和匿名对象不同的是,这里的变量num可以不用声明为final,该代码同样正确:

代码如下:

intnum=1;
Converter<Integer,String>stringConverter=
(from)->String.valueOf(from+num);
stringConverter.convert(2);//3

不过这里的num必须不可被后面的代码修改(即隐性的具有final的语义),例如下面的就无法编译:

复制代码代码如下:

intnum=1;
Converter<Integer,String>stringConverter=
(from)->String.valueOf(from+num);
num=3;

在lambda表达式中试图修改num同样是不允许的。

七、访问对象字段与静态变量

和本地变量不同的是,lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的:

代码如下:

  classLambda4{
  staticintouterStaticNum;
  intouterNum;
  voidtestScopes(){
  Converter<Integer,String>stringConverter1=(from)->{
  outerNum=23;
  returnString.valueOf(from);
  };
  Converter<Integer,String>stringConverter2=(from)->{
  outerStaticNum=72;
  returnString.valueOf(from);
  };
  }
  }

八、访问接口的默认方法

还记得第一节中的formula例子么,接口Formula定义了一个默认方法sqrt可以直接被formula的实例包括匿名对象访问到,但是在lambda表达式中这个是不行的。

Lambda表达式中是无法访问到默认方法的,以下代码将无法编译:

代码如下:

Formulaformula=(a)->sqrt(a*100);
Built-inFunctionalInterfaces

JDK1.8API包含了很多内建的函数式接口,在老Java中常用到的比如Comparator或者Runnable接口,这些接口都增加了@FunctionalInterface注解以便能用在lambda上。

Java8API同样还提供了很多全新的函数式接口来让工作更加方便,有一些接口是来自GoogleGuava库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。

Predicate接口

Predicate接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非):

代码如下:

  Predicate<String>predicate=(s)->s.length()>0;
  predicate.test("foo");//true
  predicate.negate().test("foo");//false
  Predicate<Boolean>nonNull=Objects::nonNull;
  Predicate<Boolean>isNull=Objects::isNull;
  Predicate<String>isEmpty=String::isEmpty;
  Predicate<String>isNotEmpty=isEmpty.negate();

Function接口

Function接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose,andThen):

代码如下:

Function<String,Integer>toInteger=Integer::valueOf;
Function<String,String>backToString=toInteger.andThen(String::valueOf);
backToString.apply("123");//"123"

Supplier接口

Supplier接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数

复制代码代码如下:

Supplier<Person>personSupplier=Person::new;
personSupplier.get();//newPerson

Consumer接口

Consumer接口表示执行在单个参数上的操作。

复制代码代码如下:

Consumer<Person>greeter=(p)->System.out.println("Hello,"+p.firstName);
greeter.accept(newPerson("Luke","Skywalker"));

Comparator接口

Comparator是老Java中的经典接口,Java8在此之上添加了多种默认方法:

复制代码代码如下:

Comparator<Person>comparator=(p1,p2)->p1.firstName.compareTo(p2.firstName);
Personp1=newPerson("John","Doe");
Personp2=newPerson("Alice","Wonderland");
comparator.compare(p1,p2);//>0
comparator.reversed().compare(p1,p2);//<0

Optional接口

Optional不是函数是接口,这是个用来防止NullPointerException异常的辅助类型,这是下一届中将要用到的重要概念,现在先简单的看看这个接口能干什么:

Optional被定义为一个简单的容器,其值可能是null或者不是null。在Java8之前一般某个函数应该返回非空对象但是偶尔却可能返回了null,而在Java8中,不推荐你返回null而是返回Optional。

复制代码代码如下:

Optional<String>optional=Optional.of("bam");
optional.isPresent();//true
optional.get();//"bam"
optional.orElse("fallback");//"bam"
optional.ifPresent((s)->System.out.println(s.charAt(0)));//"b"

Stream接口

java.util.Stream表示能应用在一组元素上一次执行的操作序列。Stream操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream的创建需要指定一个数据源,比如java.util.Collection的子类,List或者Set,Map不支持。Stream的操作可以串行执行或者并行执行。

首先看看Stream是怎么用,首先创建实例代码的用到的数据List:

复制代码代码如下:

List<String>stringCollection=newArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

Java8扩展了集合类,可以通过Collection.stream()或者Collection.parallelStream()来创建一个Stream。下面几节将详细解释常用的Stream操作:

Filter过滤

过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。

复制代码代码如下:

stringCollection
.stream()
.filter((s)->s.startsWith("a"))
.forEach(System.out::println);
//"aaa2","aaa1"

Sort排序

排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。

复制代码代码如下:
stringCollection
.stream()
.sorted()
.filter((s)->s.startsWith("a"))
.forEach(System.out::println);
//"aaa1","aaa2"

需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的:

复制代码代码如下:

System.out.println(stringCollection);
//ddd2,aaa2,bbb1,aaa1,bbb3,ccc,bbb2,ddd1

Map映射

中间操作map会将元素根据指定的Function接口来依次将元素转成另外的对象,下面的示例展示了将字符串转换为大写字符串。你也可以通过map来讲对象转换成其他类型,map返回的Stream类型是根据你map传递进去的函数的返回值决定的。

复制代码代码如下:

stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a,b)->b.compareTo(a))
.forEach(System.out::println);
//"DDD2","DDD1","CCC","BBB3","BBB2","AAA2","AAA1"

Match匹配

Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。

复制代码代码如下:

  booleananyStartsWithA=
  stringCollection
  .stream()
  .anyMatch((s)->s.startsWith("a"));
  System.out.println(anyStartsWithA);//true
  booleanallStartsWithA=
  stringCollection
  .stream()
  .allMatch((s)->s.startsWith("a"));
  System.out.println(allStartsWithA);//false
  booleannoneStartsWithZ=
  stringCollection
  .stream()
  .noneMatch((s)->s.startsWith("z"));
  System.out.println(noneStartsWithZ);//true

Count计数

计数是一个最终操作,返回Stream中元素的个数,返回值类型是long。

复制代码代码如下:

longstartsWithB=
stringCollection
.stream()
.filter((s)->s.startsWith("b"))
.count();
System.out.println(startsWithB);//3
Reduce规约

这是一个最终操作,允许通过指定的函数来讲stream中的多个元素规约为一个元素,规越后的结果是通过Optional接口表示的:

复制代码代码如下:

Optional<String>reduced=
stringCollection
.stream()
.sorted()
.reduce((s1,s2)->s1+"#"+s2);
reduced.ifPresent(System.out::println);
//"aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

并行Streams

前面提到过Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。

下面的例子展示了是如何通过并行Stream来提升性能:

首先我们创建一个没有重复元素的大表:

复制代码代码如下:

intmax=1000000;
List<String>values=newArrayList<>(max);
for(inti=0;i<max;i++){
UUIDuuid=UUID.randomUUID();
values.add(uuid.toString());
}

然后我们计算一下排序这个Stream要耗时多久,

串行排序:

复制代码代码如下:

longt0=System.nanoTime();
longcount=values.stream().sorted().count();
System.out.println(count);
longt1=System.nanoTime();
longmillis=TimeUnit.NANOSECONDS.toMillis(t1-t0);
System.out.println(String.format("sequentialsorttook:%dms",millis));
//串行耗时:899ms

并行排序:

复制代码代码如下:

  longt0=System.nanoTime();
  longcount=values.parallelStream().sorted().count();
  System.out.println(count);
  longt1=System.nanoTime();
  longmillis=TimeUnit.NANOSECONDS.toMillis(t1-t0);
  System.out.println(String.format("parallelsorttook:%dms",millis));
  //并行排序耗时:472ms

上面两个代码几乎是一样的,但是并行版的快了50%之多,唯一需要做的改动就是将stream()改为parallelStream()。

Map

前面提到过,Map类型不支持stream,不过Map提供了一些新的有用的方法来处理一些日常任务。

复制代码代码如下:

Map<Integer,String>map=newHashMap<>();
for(inti=0;i<10;i++){
map.putIfAbsent(i,"val"+i);
}
map.forEach((id,val)->System.out.println(val));


以上代码很容易理解,putIfAbsent不需要我们做额外的存在性检查,而forEach则接收一个Consumer接口来对map里的每一个键值对进行操作。

下面的例子展示了map上的其他有用的函数:

复制代码代码如下:

map.computeIfPresent(3,(num,val)->val+num);
map.get(3);//val33
map.computeIfPresent(9,(num,val)->null);
map.containsKey(9);//false
map.computeIfAbsent(23,num->"val"+num);
map.containsKey(23);//true
map.computeIfAbsent(3,num->"bam");
map.get(3);//val33

接下来展示如何在Map里删除一个键值全都匹配的项:

复制代码代码如下:

map.remove(3,"val3");
map.get(3);//val33
map.remove(3,"val33");
map.get(3);//null

以上就是关于Java8的新特性的详细介绍,希望能给大家一个参考。想要了解更多内容的小伙伴可以登录扣丁学堂官网了解更多内容。扣丁学堂是专业的Java培训机构,不仅有专业的老师和与时俱进的课程体系,还有大量的Java视频教程供学员观看学习,想要学习Java的小伙伴快快行动吧。Java技术交流群:670348138。


扣丁学堂微信公众号


【关注微信公众号获取更多学习资料】



查看更多关于“Java开发资讯的相关文章>>

标签: Java培训 Java开发程序员 Java视频教程

热门专区

暂无热门资讯

课程推荐

微信
微博
15311698296

全国免费咨询热线

邮箱:codingke@1000phone.com

官方群:148715490

北京千锋互联科技有限公司版权所有   北京市海淀区宝盛北里西区28号中关村智诚科创大厦4层
京ICP备12003911号-6   Copyright © 2013 - 2019

京公网安备 11010802030908号