1.default关键字

在java里面,我们通常都是认为接口里面是只能有抽象方法,不能有任何方法的实现的,那么在jdk1.8里面打破了这个规定,引入了新的关键字default,通过使用default修饰方法,可以让我们在接口里面定义具体的方法实现,如下。

  1. public interface NewCharacter {
  2. public void test1();
  3. public default void test2(){
  4. System.out.println("我是新特性1");
  5. }
  6. }

那这么定义一个方法的作用是什么呢?为什么不在接口的实现类里面再去实现方法呢?

其实这么定义一个方法的主要意义是定义一个默认方法,也就是说这个接口的实现类实现了这个接口之后,不用管这个default修饰的方法,也可以直接调用,如下。

  1. public class NewCharacterImpl implements NewCharacter{
  2. @Override
  3. public void test1() {
  4. }
  5. public static void main(String[] args) {
  6. NewCharacter nca = new NewCharacterImpl();
  7. nca.test2();
  8. }
  9. }

所以说这个default方法是所有的实现类都不需要去实现的就可以直接调用,那么比如说jdk的集合List里面增加了一个sort方法,那么如果定义为一个抽象方法,其所有的实现类如arrayList,LinkedList等都需要对其添加实现,那么现在用default定义一个默认的方法之后,其实现类可以直接使用这个方法了,这样不管是开发还是维护项目,都会大大简化代码量。

2.Lambda 表达式

Lambda表达式是jdk1.8里面的一个重要的更新,这意味着java也开始承认了函数式编程,并且尝试引入其中。

首先,什么是函数式编程,引用廖雪峰先生的教程里面的解释就是说:函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

简单的来说就是,函数也是一等公民了,在java里面一等公民有变量,对象,那么函数式编程语言里面函数也可以跟变量,对象一样使用了,也就是说函数既可以作为参数,也可以作为返回值了,看一下下面这个例子。

  1. //这是常规的Collections的排序的写法,需要对接口方法重写
  2. public void test1(){
  3. List<String> list =Arrays.asList("aaa","fsa","ser","eere");
  4. Collections.sort(list, new Comparator<String>() {
  5. @Override
  6. public int compare(String o1, String o2) {
  7. return o2.compareTo(o1);
  8. }
  9. });
  10. for (String string : list) {
  11. System.out.println(string);
  12. }
  13. }
  14. //这是带参数类型的Lambda的写法
  15. public void testLamda1(){
  16. List<String> list =Arrays.asList("aaa","fsa","ser","eere");
  17. Collections.sort(list, (Comparator<? super String>) (String a,String b)->{
  18. return b.compareTo(a);
  19. }
  20. );
  21. for (String string : list) {
  22. System.out.println(string);
  23. }
  24. }
  25. //这是不带参数的lambda的写法
  26. public void testLamda2(){
  27. List<String> list =Arrays.asList("aaa","fsa","ser","eere");
  28. Collections.sort(list, (a,b)->b.compareTo(a)
  29. );
  30. for (String string : list) {
  31. System.out.println(string);
  32. }

可以看到不带参数的写法一句话就搞定了排序的问题,所以引入lambda表达式的一个最直观的作用就是大大的简化了代码的开发,像其他一些编程语言Scala,Python等都是支持函数式的写法的。当然,不是所有的接口都可以通过这种方法来调用,只有函数式接口才行,jdk1.8里面定义了好多个函数式接口,我们也可以自己定义一个来调用,下面说一下什么是函数式接口。

3、函数式接口

定义:“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。jdk1.8提供了一个@FunctionalInterface注解来定义函数式接口,如果我们定义的接口不符合函数式的规范便会报错。

  1. @FunctionalInterface
  2. public interface MyLamda {
  3. public void test1(String y);
  4. //这里如果继续加一个抽象方法便会报错
  5. // public void test1();
  6. //default方法可以任意定义
  7. default String test2(){
  8. return "123";
  9. }
  10. default String test3(){
  11. return "123";
  12. }
  13. //static方法也可以定义
  14. static void test4(){
  15. System.out.println("234");
  16. }
  17. }

看一下这个接口的调用,符合lambda表达式的调用方法。

  1. MyLamda m = y -> System.out.println("ss"+y);

4.方法与构造函数引用

jdk1.8提供了另外一种调用方式::,当 你 需 要使用 方 法 引用时 , 目 标引用 放 在 分隔符::前,方法的名称放在 后 面 ,即ClassName :: methodName 。例如 ,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法,如下示例。

  1. //先定义一个函数式接口
  2. @FunctionalInterface
  3. public interface TestConverT<T, F> {
  4. F convert(T t);
  5. }

测试如下,可以以::形式调用。

  1. public void test(){
  2. TestConverT<String, Integer> t = Integer::valueOf;
  3. Integer i = t.convert("111");
  4. System.out.println(i);
  5. }

此外,对于构造方法也可以这么调用。

  1. //实体类User和它的构造方法
  2. public class User {
  3. private String name;
  4. private String sex;
  5. public User(String name, String sex) {
  6. super();
  7. this.name = name;
  8. this.sex = sex;
  9. }
  10. }
  11. //User工厂
  12. public interface UserFactory {
  13. User get(String name, String sex);
  14. }
  15. //测试类
  16. UserFactory uf = User::new;
  17. User u = uf.get("ww", "man");

这里的User::new就是调用了User的构造方法,Java编译器会自动根据UserFactory.get方法的签名来选择合适的构造函数。

5、局部变量限制

Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。 Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。
  为什么局部变量有这些限制?
  (1)实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此, Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  (2)这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

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

6、Java8全新的日期和时间API

1.LocalDate

LocalDate是日期处理类,具体API如下:

  1. // 获取当前日期
  2. LocalDate now = LocalDate.now();
  3. // 设置日期
  4. LocalDate localDate = LocalDate.of(2019, 9, 10);
  5. // 获取年
  6. int year = localDate.getYear(); //结果:2019
  7. int year1 = localDate.get(ChronoField.YEAR); //结果:2019
  8. // 获取月
  9. Month month = localDate.getMonth(); // 结果:SEPTEMBER
  10. int month1 = localDate.get(ChronoField.MONTH_OF_YEAR); //结果:9
  11. // 获取日
  12. int day = localDate.getDayOfMonth(); //结果:10
  13. int day1 = localDate.get(ChronoField.DAY_OF_MONTH); // 结果:10
  14. // 获取星期
  15. DayOfWeek dayOfWeek = localDate.getDayOfWeek(); //结果:TUESDAY
  16. int dayOfWeek1 = localDate.get(ChronoField.DAY_OF_WEEK); //结果:2

2.LocalTime

LocalTime是时间处理类,具体API如下:

  1. // 获取当前时间
  2. LocalTime now = LocalTime.now();
  3. // 设置时间
  4. LocalTime localTime = LocalTime.of(13, 51, 10);
  5. //获取小时
  6. int hour = localTime.getHour(); // 结果:13
  7. int hour1 = localTime.get(ChronoField.HOUR_OF_DAY); // 结果:13
  8. //获取分
  9. int minute = localTime.getMinute(); // 结果:51
  10. int minute1 = localTime.get(ChronoField.MINUTE_OF_HOUR); // 结果:51
  11. //获取秒
  12. int second = localTime.getSecond(); // 结果:10
  13. int second1 = localTime.get(ChronoField.SECOND_OF_MINUTE); // 结果:10

3.LocalDateTime

LocalDateTime可以设置年月日时分秒,相当于LocalDate + LocalTime

  1. // 获取当前日期时间
  2. LocalDateTime localDateTime = LocalDateTime.now();
  3. // 设置日期
  4. LocalDateTime localDateTime1 = LocalDateTime.of(2019, Month.SEPTEMBER, 10, 14, 46, 56);
  5. LocalDateTime localDateTime2 = LocalDateTime.of(localDate, localTime);
  6. LocalDateTime localDateTime3 = localDate.atTime(localTime);
  7. LocalDateTime localDateTime4 = localTime.atDate(localDate);
  8. // 获取LocalDate
  9. LocalDate localDate2 = localDateTime.toLocalDate();
  10. // 获取LocalTime
  11. LocalTime localTime2 = localDateTime.toLocalTime();

4.修改LocalDate、LocalTime、LocalDateTime

LocalDate、LocalTime、LocalDateTime、Instant为不可变对象,修改这些对象对象会返回一个副本。增加、减少年数、月数、天数等 以LocalDateTime为例。

  1. // 创建日期:2019-09-10 14:46:56
  2. LocalDateTime localDateTime = LocalDateTime.of(2019, Month.SEPTEMBER, 10, 14, 46, 56);
  3. //增加一年
  4. localDateTime = localDateTime.plusYears(1); //结果: 2020-09-10 14:46:56
  5. localDateTime = localDateTime.plus(1, ChronoUnit.YEARS); //结果: 2021-09-10 14:46:56
  6. //减少一个月
  7. localDateTime = localDateTime.minusMonths(1); //结果: 2021-08-10 14:46:56
  8. localDateTime = localDateTime.minus(1, ChronoUnit.MONTHS); //结果: 2021-07-10 14:46:56

通过with修改某些值,年月日时分秒都可以通过with方法设置。

  1. //修改年为2020
  2. localDateTime = localDateTime.withYear(2020);
  3. //修改为2022
  4. localDateTime = localDateTime.with(ChronoField.YEAR, 2022);

日期计算。比如有些时候想知道这个月的最后一天是几号、下个周末是几号,通过提供的时间和日期API可以很快得到答案 。TemporalAdjusters提供的各种日期时间格式化的静态类,比如firstDayOfYear是当前日期所属年的第一天

  1. LocalDate localDate = LocalDate.now();
  2. LocalDate localDate1 = localDate.with(TemporalAdjusters.firstDayOfYear());

格式化时间。DateTimeFormatter默认提供了多种格式化方式,如果默认提供的不能满足要求,可以通过DateTimeFormatter的ofPattern方法创建自定义格式化方式

  1. LocalDate localDate = LocalDate.of(2019, 9, 10);
  2. String s1 = localDate.format(DateTimeFormatter.BASIC_ISO_DATE);
  3. String s2 = localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
  4. //自定义格式化
  5. DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
  6. String s3 = localDate.format(dateTimeFormatter);

解析时间。和SimpleDateFormat相比,DateTimeFormatter是线程安全的

  1. LocalDate localDate1 = LocalDate.parse("20190910", DateTimeFormatter.BASIC_ISO_DATE);
  2. LocalDate localDate2 = LocalDate.parse("2019-09-10", DateTimeFormatter.ISO_LOCAL_DATE);

7.流

java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换(由中间操作完成)→执行操作获取想要的结果(由最终操作完成),每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),所以活动流Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set, Map不支持 但是数组是支持的。

Stream 流的中间操作和最终操作:
  • 中间操作(Intermediate):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。其中最主要的中间操作有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  • 最终操作(Terminal):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。常用的最终操作有:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

Stream 的特性
  • 不是数据结构
  • 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
  • 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
  • 所有 Stream 的操作必须以 lambda 表达式为参数
  • 不支持索引访问
  • 你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。
  • 很容易生成数组或者 List
  • 惰性化
  • 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
  • Intermediate 操作永远是惰性化的。
  • 并行能力
  • 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的
  • 可以是无限的
  • 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

构造流的几种常见方法
  1. // 1. Individual values
  2. Stream stream = Stream.of("a", "b", "c");
  3. // 2. Arrays
  4. String [] strArray = new String[] {"a", "b", "c"};
  5. stream = Stream.of(strArray);
  6. stream = Arrays.stream(strArray);
  7. // 3. Collections
  8. List<String> list = Arrays.asList(strArray);
  9. stream = list.stream();

流转换为其它数据结构
  1. // 1. Array
  2. String[] strArray1 = stream.toArray(String[]::new);
  3. // 2. Collection
  4. List<String> list1 = stream.collect(Collectors.toList());
  5. List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
  6. Set set1 = stream.collect(Collectors.toSet());
  7. Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
  8. // 3. String
  9. String str = stream.collect(Collectors.joining()).toString();

流的操作

filter 顾名思义是过滤的意思,根据某个规则,过滤生成新的stream

保留偶数

  1. Integer[] sixNums = {1, 2, 3, 4, 5, 6};
  2. Integer[] evens =
  3. Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

打印姓名(forEach 和 pre-java8 的对比)

  1. // Java 8
  2. roster.stream()
  3. .filter(p -> p.getGender() == Person.Sex.MALE)
  4. .forEach(p -> System.out.println(p.getName()));
  5. // Pre-Java 8
  6. for (Person p : roster) {
  7. if (p.getGender() == Person.Sex.MALE) {
  8. System.out.println(p.getName());
  9. }

limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素(它是由一个叫 subStream 的方法改名而来)。

limit 和 skip 对运行次数的影响

  1. public void testLimitAndSkip() {
  2. List<Person> persons = new ArrayList();
  3. for (int i = 1; i <= 10000; i++) {
  4. Person person = new Person(i, "name" + i);
  5. persons.add(person);
  6. }
  7. List<String> personList2 = persons.stream().
  8. map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
  9. System.out.println(personList2);
  10. }
  11. private class Person {
  12. public int no;
  13. private String name;
  14. public Person (int no, String name) {
  15. this.no = no;
  16. this.name = name;
  17. }
  18. public String getName() {
  19. System.out.println(name);
  20. return name;
  21. }
  22. }

输出结果为:

  1. name1
  2. name2
  3. name3
  4. name4
  5. name5
  6. name6
  7. name7
  8. name8
  9. name9
  10. name10
  11. [name4, name5, name6, name7, name8, name9, name10]