zoukankan      html  css  js  c++  java
  • 【读书笔记】《写给大忙人看的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/

  • 相关阅读:
    【leetcode】N叉树的前序遍历
    【leetcode】第 N 个泰波那契数
    【leetcode】下一个更大元素 I
    【leetcode】Nim 游戏
    【leetcode】非递减数列
    053-208
    053-211
    053-56
    053-53
    053-236
  • 原文地址:https://www.cnblogs.com/justcooooode/p/7701260.html
Copyright © 2011-2022 走看看