常见面试题:java8有什么新特性?

  • 2021 年 8 月 29 日
  • 筆記

常见面试题:java8有什么新特性?

主要有以下这些新特性:

  • lambda 表达式,经常配合函数式接口使用,可以有效减少代码量

    • Runnable 是一个函数式接口,下面展示了创建线程三种写法,显然最后一种最简洁:

      class OldWay implements Runnable {
          @Override
          public void run() {
              System.out.println("最原始的方法");
          }
      }
      
      public class Test {
          public static void main(String[] args) {
      
              new Thread(new OldWay()).start();
      
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      System.out.println("匿名内部类");
                  }
              }).start();
      
              new Thread(() -> {
                  System.out.println("lambda表达式");
              }).start();
              
          }
      }
      
    • 在 new 一个 Thread 时需要传入一个 Runnable 接口的实现类

      • 第一种是最原始的做法,先创建一个 class 来实现 Runnable 接口,然后在创建线程时传入这个实现类,太麻烦了
      • 第二种是匿名内部类的写法,把实现类的名字给省略掉了,稍微方便点,但 run 这个方法名其实也有点冗余,因为 Runnable 里面就这么一个方法,不写出来应该也没关系啊
      • 第三种是 lambda 表达式的写法,把方法名也省略掉了,最简洁,但注意,如果接口里有多个方法,那么只能采用前两种方法了
    • 更直观的感受一下 lambda 表达式和函数式接口之间的关系:

      public class Test {
          public static void main(String[] args) { 
              Runnable runnable = () -> {
                  System.out.println("nb");
              };
          }
      }
      
    • 另一个常见应用就是集合类的 forEach 方法,需要一个 Consumer 参数,这也是一个函数式接口,里面的 accept 方法需要一个参数并且没有返回值(不用记,在 IDEA 里点进去看就行),一个例子如下,它遍历 list 中的每个元素,加一后输出:

      public class Test {
          public static void main(String[] args) {
              ArrayList<Integer> list = new ArrayList<>();
              list.add(1);
              list.add(2);
              list.add(3);
              //2 3 4
              list.forEach((Integer num) -> {
                  num = num + 1;
                  System.out.println(num);
              });
          }
      }
      
    • lambda 表达式还有些小细节,比如参数列表中参数的类型其实可以省略,如果代码块里只有一条语句那么花括号也可以省略,如果参数列表里只有一个参数那么圆括号也可以省略,但其实就算不省略也足够简洁了,我觉得没必要省略

  • 方法引用,感觉有点说不清,可以看个例子,就比如前面遍历 list,如果我就是想遍历一次 list 然后输出,可以用到方法引用:

    public class Test {
        public static void main(String[] args) {
            
            ArrayList<Integer> list = new ArrayList<>();
            list.add(1);
            list.add(2);
            list.add(3);
    
            list.forEach((Integer num) -> {
                System.out.println(num);
            });
    
            list.forEach(System.out::println);
            
        }
    }
    
    • 首先,forEach 是需要一个 Consumer 参数的,这个函数式接口的 accept 方法需要一个参数并且没有返回值,我们有两个方案,一个就是自己写一个 lambda 表达式,另一个就是使用方法引用,直接引用一个已经写好了的满足条件的方法,比如这里的 System.out.println 方法就是需要一个参数的 void 方法,满足条件,当然我们也可以定制一个满足条件的方法然后用方法引用的方式来使用,如下:

      class TestReference{
          public static void myPrint(Integer num){
              System.out.println(num);
          }
      }
      public class Test {
          public static void main(String[] args) {
              ArrayList<Integer> list = new ArrayList<>();
              list.add(1);
              list.add(2);
              list.add(3);
              list.forEach(TestReference::myPrint);
          }
      }
      
  • 函数式接口,前面其实已经提到过了,如果一个接口里面只有一个方法,那么这就是一个函数式接口,对于函数式接口,我们可以通过 lambda 表达式或者方法引用来进行快速的实现,而不必新建一个 class 去继承或者写一个匿名内部类

  • 默认方法,意思是说,我们在写一个接口时可以通过 default 关键字为其中的方法提供默认的实现方案,使得实现类就算不覆写这个方法也没有关系:

    interface TestInterface{
        default void test(){
            System.out.println("here");
        }
    }
    
    class TestDefault implements TestInterface{
        //没有覆写test方法也没有报错
    }
    
    public class Test {
        public static void main(String[] args) {
            //here
            new TestDefault().test();
        }
    }
    
  • Stream API,我们可以把一个集合转换为流,在这个流上做各种操作,比如查找、排序、过滤等等

    • 主要有以下这些操作:

      image

      • 中间操作:指操作完成后还是返回一个流对象,可以拿着这个对象继续操作下去
      • 结束操作:指操作完成后不再返回流对象,一切都结束了
      • 无状态:指元素的处理不受之前元素的影响,可以挨个处理
      • 有状态:指该操作只有拿到所有元素之后才能继续下去
      • 非短路操作:指必须处理完所有元素才能得到最终结果
      • 短路操作:指遇到某些符合条件的元素就可以得到最终结果
    • 来个案例,现在给定一个 list,想要先求出每个元素的平方,然后排序,然后找出 10 和 100 之间的那些元素,然后去除重复元素,最后输出:

      public class Test {
          public static void main(String[] args) {
              //准备我们的list
              ArrayList<Integer> list = new ArrayList<>();
              int[] ints = {4, 1, 6, 2, 8, 5, 15, 11, 9};
              for (int i : ints) {
                  list.add(i);
              }
              //转换为流
              Stream<Integer> stream = list.stream();
              //第一步,求出每个元素的平方
              stream.map((Integer origin) -> {
                  return origin * origin;
              })
                      //第二步,排序
                      .sorted()
                      //第三步,找出10和100之间的那些值
                      .filter((Integer num) -> {
                          return num >= 10 && num <= 100;
                      })
                      //第四步,去重
                      .distinct()
                      //第五步,输出
                      .forEach(System.out::println);
          }
      }
      
    • 从案例中可以发现,很多流操作是需要一个函数式接口作为参数的,因此一定要搭配前面的 lambda 表达式和方法引用来完成这些流操作,否则代码量是过大的

    • 至于到底需要写什么样的 lambda 表达式(几个参数,返回值是什么),一定要在 IDEA 里点进去看,直接背是不现实的

  • 新的 Date Time API,因为 java 中同时存在 java.util.Datejava.sql.Date 两个时间类,很容易让人迷惑,而且这两个包里的内容也存在诸多问题,因此 java8 中新增了 java.time 这个包来把所有时间类的 API 一网打尽

    • 直接看个案例吧,演示部分 API 的使用:

      public class Test {
          public static void main(String[] args) {
      
              //获取当前的日期时间(年月日+时分秒)
              LocalDateTime currentTime = LocalDateTime.now();
              System.out.println("当前的日期和时间: " + currentTime);
      
              //获取当前的日期(年月日)
              LocalDate date1 = currentTime.toLocalDate();
              System.out.println("date1: " + date1);
      
              //分别得到当前的月、日、秒
              Month month = currentTime.getMonth();
              int day = currentTime.getDayOfMonth();
              int seconds = currentTime.getSecond();
              System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);
      
              //把当前的日期时间中的年替换为2012,日替换为10
              LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
              System.out.println("date2: " + date2);
      
              //显式的构造出一个日期
              LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
              System.out.println("date3: " + date3);
      
              //显式的构造出一个时间
              LocalTime date4 = LocalTime.of(22, 15);
              System.out.println("date4: " + date4);
      
              //解析字符串来得到一个时间
              LocalTime date5 = LocalTime.parse("20:15:30");
              System.out.println("date5: " + date5);
          }
      }
      
    • 最后的输出如下:

      当前的日期和时间: 2021-08-24T22:03:43.468015700
      date1: 2021-08-24
      月: AUGUST, 日: 24, 秒: 43
      date2: 2012-08-10T22:03:43.468015700
      date3: 2014-12-12
      date4: 22:15
      date5: 20:15:30
      
  • Optional 类,很好的解决了 NullPointerException 的问题

学习过程中,部分内容有参考网上其它人的文章,如有侵权必删