【读书笔记】《写给大忙人看的Java SE 8》——Java8新特性总结

接口中的默认方法和静态方法

先考虑一个问题,如何向Java中的集合库中增加方法?例如在Java 8中向Collection接口中添加了一个forEach方法。

如果在Java 8之前,对于接口来说,其中的方法必须都为抽象方法,也就是说接口中不允许有接口的实现,那么就需要对每个实现Collection接口的类都需要实现一个forEach方法。

但这就会造成在给接口添加新方法的同时影响了已有的实现,所以Java设计人员引入了接口默认方法,其目的是为了解决接口的修改与已有的实现不兼容的问题,接口默认方法可以作为库、框架向前兼容的一种手段。

默认方法就像一个普通Java方法,只是方法用default关键字修饰。

下面来举一个简单的例子

public interface Person {
    //默认方法
    default String getName(String name) {
        return name;
    }
}
///////////////////////////////////////////////////////////////////////
public class Student implements Person {

}
//////////////////////////////////////////////////////////////////////
public class Test {
    public static void main(String[] args) {
        Person p = new Student();
        String name = p.getName("小李");
        System.out.println(name);
    }
}

我们定义了一个Person接口,其中getName是一个默认方法。接着编写一个实现类,可以从结果中看到,虽然Student是空的,但是仍然可以实现getName方法。

显然默认接口的出现打破了之前的一些基本规则,使用时要注意几个问题。

考虑如果接口中定义了一个默认方法,而另外一个父类或者接口中又定义了一个同名的方法,该选择哪个?

1. 选择父类中的接口。如果一个父类提供了具体的实现方法,那么接口中具有相同名称和参数的默认方法会被忽略。

2. 接口冲突。如果一个父接口提供了一个默认方法,而另一个接口也提供了具有相同名称和参数类型的方法(不管该方法是否是默认方法),那么必须通过覆盖方法来解决。

记住一个原则,就是“类优先”,即当类和接口都有一个同名方法时,只有父类中的方法会起作用。

“类优先”原则可以保证与Java 7的兼容性。如果你再接口中添加了一个默认方法,它对Java 8以前编写的代码不会产生任何影响。

下面来说说静态方法

静态方法就像一个普通Java静态方法,但方法的权限修饰只能是public或者不写。

默认方法和静态方法使Java的功能更加丰富。

在Java 8中Collection接口中就添加了四个默认方法,stream()、parallelStream()、forEach()和removeIf()。Comparator接口也增加了许多默认方法和静态方法。

函数式接口和Lambda表达式

函数式接口(Functional Interface)是只包含一个方法的抽象接口。

比如Java标准库中的java.lang.Runnable,java.util.concurrent.Callable就是典型的函数式接口。

在Java 8中通过@FunctionalInterface注解,将一个接口标注为函数式接口,该接口只能包含一个抽象方法。

@FunctionalInterface注解不是必须的,只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。

一般建议在接口上使用@FunctionalInterface注解进行声明,以免他人错误地往接口中添加新方法,如果在你的接口中定义了第二个抽象方法的话,编译器会报错。

函数式接口是为Java 8中的lambda而设计的,lambda表达式的方法体其实就是函数接口的实现。

为什么要使用lambda表达式?

“lambda表达式”是一段可以传递的代码,因为他可以被执行一次或多次。我们先回顾一下之前在Java中一直使用的相似的代码块。

当我们在一个线程中执行一些逻辑时,通常会将代码放在一个实现Runnable接口的类的run方法中,如下所示:

new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++)
                    System.out.println("Without Lambda Expression");
            }}).start();

然后通过创建实例来启动一个新的线程。run方法内包含了一个新线程中需要执行的代码。

再来看另一个例子,如果想利用字符串长度排序而不是默认的字典顺序排序,就需要自定义一个实现Comparator接口的类,然后将对象传递给sort方法。

class LengthComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
}
Arrays.sort(strings, new LengthComparator());

按钮回调是另一个例子。将回调操作放在了一个实现了监听器接口的类的一个方法中。

JButton button = new JButton("click");

button.addActionListener(new ActionListener() {    
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Without Lambda Expression");
    }
});

这三个例子中,出现了相同方式,一段代码被传递给其他调用者——一个新线程、是一个排序方法或者是一个按钮。这段代码会在稍后被调用。

在Java中传递代码并不是很容易,不可能将代码块到处传递。你不得不构建一个类的对象,由他的某个方法来包含所需的代码。

而lambda表达式实际上就是代码块的传递的实现。其语法结构如下:

(parameters) -> expression 或者 (parameters) -> {statements;}

括号里的参数可以省略其类型,编译器会根据上下文来推导参数的类型,你也可以显式地指定参数类型,如果没有参数,括号内可以为空。

方法体,如果有多行功能语句用大括号括起来,如果只有一行功能语句则可以省略大括号。

new Thread(() -> {
            for (int i = 0; i < 100; i++)
                System.out.println("Lambda Expression");
        }).start();
Comparator<String> c = (s1, s2) -> Integer.compare(s1.length(), s2.length());
button.addActionListener(e -> System.out.println("Lambda Expression"));

可以看到lambda表达式使代码变得简单,代替了匿名内部类。

下面来说一下方法引用,方法引用是lambda表达式的一种简写形式。 如果lambda表达式只是调用一个特定的已经存在的方法,则可以使用方法引用。

使用“::”操作符将方法名和对象或类的名字分隔开来。以下是四种使用情况:

  • 对象::实例方法
  • 类::静态方法
  • 类::实例方法
  • 类::new
Arrays.sort(strings, String::compareToIgnoreCase);
// 等价于
Arrays.sort(strings, (s1, s2) -> s1.compareToIgnoreCase(s2));

上面的代码就是第三种情况,对lambda表达式又一次进行了简化。

Stream API

当处理集合时,通常会迭代所有元素并对其中的每一个进行处理。例如,我们希望统计一个字符串类型数组中,所有长度大于3的元素。

String[] strArr = { "Java8", "new", "feature", "Stream", "API" };
        int count = 0;
        for (String s : strArr) {
            if (s.length() > 3)
                count++;
        }

通常我们都会使用这段代码来统计,并没有什么错误,只是它很难被并行计算。这也是Java8引入大量操作符的原因,在Java8中,实现相同功能的操作符如下所示:

long count = Stream.of(strArr).filter(w -> w.length() > 3).count();

stream方法会为字符串列表生成一个Streamfilter方法会返回只包含字符串长度大于3的一个Stream,然后通过count方法计数。

一个Stream表面上与一个集合很类似,允许你改变和获取数据,但实际上却有很大区别:

  1. Stream自己不会存储元素。元素可能被存储在底层的集合中,或者根据需要产生出来。
  2. Stream操作符不会改变源对象。相反,他们返回一个持有新结果的Stream。
  3. Stream操作符可能是延迟执行的。意思是它们会等到需要结果的时候才执行。

Stream相对于循环操作有更好的可读性。并且可以并行计算:

long count = Arrays.asList(strArr).parallelStream().filter(w -> w.length() > 3).count();

只需要把stream方法改成parallelStream,就可以让Stream去并行执行过滤和统计操作。

Stream遵循“做什么,而不是怎么去做”的原则。只需要描述需要做什么,而不用考虑程序是怎样实现的。

Stream很像Iterator,单向,只能遍历一遍。但是Stream可以只通过一行代码就实现多线程的并行计算。

当使用Stream时,会有三个阶段:

  1. 创建一个Stream。
  2. 在一个或多个步骤中,将初始Stream转化到另一个Stream的中间操作
  3. 使用一个终止操作来产生一个结果。该操作会强制他之前的延迟操作立即执行。在这之后,该Stream就不会在被使用了。

从着三个阶段来看,对应着三种类型的方法,首先是Stream的创建方法。

// 1. Individual values
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();

中间操作包括:map (mapToInt, flatMap 等)、 filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。

终止操作包括:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。

关于Stream的每个方法如何使用就不展开了,更详尽的介绍看这篇文章:https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

新的日期和时间 API

Java8 引入了一个新的日期和时间API,位于java.time包下。

新的日期和时间API借鉴了Joda Time库,其作者也为同一人,但它们并不是完全一样的,做了很多改进。

下面来说一下几个常用的类。首先是Instant,一个Instant对象表示时间轴上的一个点。

Instant.now()会返回当前的瞬时点(格林威治时间)。Instant.MIN和Instant.MAX分别为十亿年前和十亿年后。

如下代码可以计算某算法的运行时间:

Instant start = Instant.now();
runAlgorithm();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();

Duration对象表示两个瞬时点间的时间量。可以通过不同的方法,换算成各种时间单位。

上面所说的绝对时间并不能应用到生活中去,所以新的Java API中提供了两种人类时间,本地日期/时间带时区的时间

LocalDate是一个带有年份、月份和天数的日期。创建他可以使用静态方法now或者of。

LocalDate today = LocalDate.now();
LocalDate myBirthday = LocalDate.of(1994, 03, 15);
// use Enum
myBirthday = LocalDate.of(1994, Month.MARCH, 15);

System.out.println(today); // 2017-10-23
System.out.println(myBirthday); // 1994-03-15

下面是LocalDate中的一些常用方法:

LocalTime表示一天中的某个时间,同样可以使用now或者of来创建实例。

LocalTime rightNow = LocalTime.now();
LocalTime bedTime = LocalTime.of(2, 0);
System.out.println(rightNow); // 01:26:17.139
System.out.println(bedTime); // 02:00

LocalDateTime表示一个日期和时间,用法和上面类似。

上面几种日期时间类都属于本地时间,下面来说一下带时区的时间。

ZonedDateTime通过设置时区的id来创建一个带时区的时间。

ZonedDateTime beijingOlympicOpenning = ZonedDateTime.of(2008, 8, 8, 20, 0, 0, 0, ZoneId.of("Asia/Shanghai"));
System.out.println(beijingOlympicOpenning); // 2008-08-08T20:00+08:00[Asia/Shanghai]

更新后的API同样加入了新的格式化类DateTimeFormatter。DateTimeFormatter提供了三种格式化方法来打印日期/时间:

  • 预定义的标准格式
String formattered = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(beijingOlympicOpenning);
System.out.println(formattered); // 2008-08-08T20:00:00

DateTimeFormatter类提供了多种预定义的标准格式可供使用。

  • 语言环境相关的格式

标准格式主要用于机器可读的时间戳。为了让人能够读懂日期和时间,你需要使用语言环境相关的格式。

Java8提供了4种风格,SHORT、MEDIUM、LONG、FULL。

String formattered = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).format(beijingOlympicOpenning);
System.out.println(formattered);    //2008年8月8日 星期五 下午08时00分00秒 CST
  • 自定义的格式

或者你也可以自定义日期和时间的格式。

String formattered = DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm").format(beijingOlympicOpenning);
System.out.println(formattered); // 星期五 2008-08-08 20:00

  下图中为一些常用的模式元素。

新的API提供了从字符串解析出日期/时间的parse静态方法和与遗留类(java.util.Date、java.sql.Time和java.txt.DateFormat等)互相转换的方法。

杂项改进

 Java8在String类中只添加了一个新方法,就是join,该方法实现了字符串的拼接,可以把它看作split方法的逆操作。

String joined = String.join(".", "www", "cnblogs", "com");
System.out.println(joined); // www.cnblogs.com

数字包装类提供了BYTES静态方法,以byte为单位返回长度。

所有八种包装类都提供了静态的hashCode方法。

Short、Integer、Long、Float和Double这5种类型分别提供了了sum、max和min,用来在流操作中作为聚合函数使用。

集合类和接口中添加的方法:

Java8为使用流读取文件行及访问目录项提供了一些简便的方法(Files.linesFiles.list)。同时也提供了进行Base64编码/解码的方法。

Java8对GUI编程(JavaFX)、并发等方面也做了改进,本文没有一一列出。


转载请注明原文链接:http://www.cnblogs.com/justcooooode/p/7701260.html

参考资料

《写给大忙人看的Java SE 8》

http://study.163.com/course/introduction/1003856028.htm

https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

posted @ 2017-10-23 16:37  没课割绿地  阅读(14941)  评论(6编辑  收藏  举报