Java8特性
目录
- FunctionalInterface注解的作用
- java.util.function包下的常用接口
- 方法引用
- Stream的常用方法
- LocalDate和LocalDateTime以及LocalTime的使用
1.1 @FunctionalInterface注解
用于添加在接口上的注解。它的作用是:在编译时抛出异常,除非满足下面两种情况:
- 被注解标注的类型是一个interface类型,并且不是一个annotation类型、enum类型、或class类型
- 被注解的对象满足方法接口的要求
那么问题来了,方法接口的要求是什么呢?
注释的文档是这样写的
Conceptually, a functional interface has exactly one abstract method.
从概念上来讲,一个方法结构只有一个抽象的方法。 欸?只有一个 抽象的 方法?
注意,有两个词被加粗了分别是 一个 和 抽象的
一个 这个量词容易理解,之前也使用过,无非是接口里面就只留下一个方法呗。
但是 抽象的 方法 为什么还要单独拿出来说呢?
是因为在java8之后,接口里面不再只有抽象的方法,还可以添加两种非抽象的方法,他们分别是:default method 和 static method, 默认方法和静态方法。
1.2 默认方法和静态方法
- 默认方法:提供默认的实现,不能够重写Object的方法,但是却可以重载, 默认方法允许我们在接口里添加新的方法,而不会破坏实现这个接口的已有类的兼容性,也就是说不会强迫实现接口的类实现默认方法。 default方法可以被重写。
- 静态方法:就像类中的静态方法一样,可以给出方法的实现,并且这个方法属于方法接口。
class Ca implements Ia {
@Override
public void fun3() {
}
}
@FunctionalInterface
interface Ia {
default void fun1() {
System.out.println("fun1 invoked");
}
static void fun2() {
System.out.println("fun2 invoked");
}
abstract void fun3();
}
就像上面的例子,接口Ia里面有三个方法,同时它使用了@FunctionalInterface注解,此时编译器不会抛异常。因为它满足方法接口的条件,只有一个抽象方法。
class Ca实现方法接口Ia,此时编译器只强制要求我们实现抽象方法fun3(),此例可以证明我们上面说的关于@FunctionalInterface, 默认方法和静态方法的部分特征。
值得一提的是:@FunctionalInterface的作用只体现在编译期,通俗的讲:只要任何一个接口满足了 只有一个抽象接口 这个条件,我们就可以说他是方法接口,与是否标注注解无关,注解的作用只是帮助我们检查。
-
java.util.function包下的常用接口
只会介绍典型的几种方法接口, 其他的都是这几种的变形,所以不着重介绍,以下是重点的几个接口:
- Function
- Consumer
- Predicate
- Supplier
2.1 Function接口的使用
既然是方法接口,那么它肯定满足只有一个抽象方法,那么它的抽象方法是什么呢?打开文档,可以看到它的抽象方法是这样的:
R apply(T t);
其中T和R是方法接口声明的泛型类型
public interface Function
那么apply方法的作用是什么呢,通过参数和返回值类型不难看出,这个方法要求调用者传入一个T类型的参数,然后返回一个R类型的值。T和R在实现接口的时候就已经明确了。
那么,我们通常怎么使用这个方法接口呢?举一个简单的实例,如果我现在有一个String类型的对象,想要统计这个对象包含‘a’这个字符的个数,应该怎么做呢?
我们之前的做法通常会写一个方法,把String类型的参数传进来,然后返回一个Integer类型的值,像是这样:
public static void main(String[] args) { String str = "aaaaabcdefg"; int cnt = countFun(str); System.out.println("a 的个数是" + cnt); } public static int countFun(String str) { int res = 0; int len = str.length(); for (int i = 0;i < len;i++) { if (str.charAt(i) == 'a') { ++res; } } return res; }
仔细一观察,发现这个方法似乎可以用方法接口来实现,就像这样:
public static void main(String[] args) { String str = "aaaaabcdefg"; Function
counter = new Function () { @Override public Integer apply(String str) { int res = 0; int len = str.length(); for (int i = 0;i < len;i++) { if (str.charAt(i) == 'a') { ++res; } } return res; } }; // int cnt = countFun(str); int cnt = counter.apply(str); System.out.println("a 的个数是" + cnt); } 这两种方法会得到的结果无疑是一样的,都是5个‘a’,使用方法接口的方式相较于之前的那种方式,counter仿佛变成了一种行为,这个行为描述了“有一个String类型的对象,想要统计这个对象包含‘a’这个字符的个数”这件事情,似乎增加了代码的可阅读性,但是下面的方式略显臃肿。别着急,其实java给提供了一种语法糖,可以让我们简化这种写法,我们可以这样做:
public static void main(String[] args) { String str = "aaaaabcdefg"; Function
counter = str1 -> { int res = 0; int len = str1.length(); for (int i = 0;i < len;i++) { if (str1.charAt(i) == 'a') { ++res; } } return res; }; // int cnt = countFun(str); int cnt = counter.apply(str); System.out.println("a 的个数是" + cnt); } 这种方式是java8的新特性之一:使用lambda表达式的形式!
2.1.1 Java的lambda表达式(此处是参考的菜鸟教程的描述)
(parameters) -> expression
或
(parameters) ->{ statements; }
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
实例:我们实例化一个Thread的时候通常要实现一个Runnable的子类的对象,而Runnable其实也是一个方法接口,也就是说只有一个抽象的方法run()待实现。其实没必要用匿名内部类的方式,使用lambda的形式会显得更简洁,更能容易被阅读。
new Thread(() -> { System.out.println("这是用lambda形式实现的Runnable方法哦~"); }).start();
2.1.2 Function接口的其他方法
/** * Returns a composed function that first applies the {@code before} * function to its input, and then applies this function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param
the type of input to the {@code before} function, and to the * composed function * @param before the function to apply before this function is applied * @return a composed function that first applies the {@code before} * function and then applies this function * @throws NullPointerException if before is null * * @see #andThen(Function) */ default Function compose(Function super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } /** * Returns a composed function that first applies this function to * its input, and then applies the {@code after} function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param the type of output of the {@code after} function, and of the * composed function * @param after the function to apply after this function is applied * @return a composed function that first applies this function and then * applies the {@code after} function * @throws NullPointerException if after is null * * @see #compose(Function) */ default Function andThen(Function super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } /** * Returns a function that always returns its input argument. * * @param the type of the input and output objects to the function * @return a function that always returns its input argument */ static Function identity() { return t -> t; } 有三个方法,分别是:
- default
Function compose(Function super V, ? extends T> before) - default
Function andThen(Function super R, ? extends V> after) - static
Function identity()
这三个方法有一个共同点,那就是它们的返回值都是Function类型的,用函数式的思想来看,可以把它们理解成返回的某种行为。
compose
要求传入一个Function参数before,返回一个在调用当前Function的apply之前先调用参数的apply之前的Function。可以看做是这样的一个过程:本来当前的Function会通过调用apply传入一个T类型的参数得到一个R类型的返回值,而compose返回的复合函数的会通过调用apply传入一个V 类型的参数,得到一个R类型的返回值,其中的过程是这样的 T ------》 V -------》 R。假设有以下场景:有一个class Person有一个name属性,现在我们通过Person获取name的长度,可以这样写:class Person { String name; public Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public class FunctionTest { public static void main(String[] args) { Function
function = String::length; Function nameLengthGetter = function.compose(Person::getName); Person person = new Person("张三"); System.out.println(nameLengthGetter.apply(person)); // 将会输出person名字的个数 } } andThen
和compose
类似,区别是顺序变了andThen的顺序将会是T ------》 R -------》 V,复合函数最终得到的值将会是在本身的apply的结果之上又进行操作的值。不再详细说明。// TODO 其他的方法接口与Function大同小异,暂时不详细说明,进行完下面的部分后再继续这部分
-
方法引用
简单的来说,方法引用,就是借助目前已有的方法,将其作为某个方法接口的实现。这些方法需要具备这样的特征,参数类型或者本身的类型符合方法引用的类型,返回值类型也需要符合方法接口的 R 类型。
引用方法有下面几种方式
- 对象引用::实例方法名
- 类名::静态方法名
- 类名::实例方法名
还可以引用构造器,像是这样:ClassName::new, int[]::new
-
Stream的常用方法
关于流和其它集合具体的区别:
- 不存储数据。流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
- 函数式编程。流的操作不会修改数据源,例如
filter
不会将数据源中的数据删除。 - 延迟操作。流的很多操作如filter,map等中间操作是延迟执行的,只有到终止操作才会将操作执行。
- 可以解绑。对于无限数量的流,有些操作是可以在有限的时间完成的,比如
limit(n)
或findFirst()
,这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。 - 纯消费。流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。
流的操作是以管道的方式串起来的。流管道包含一个数据源,接着包含零到N个中间操作,最后以一个终点操作结束。
创建流的方式
可以通过多种方式创建流:
1、通过集合的
stream()
方法或者parallelStream()
,比如Arrays.asList(1,2,3).stream()
。
2、通过Arrays.stream(Object[])
方法, 比如Arrays.stream(new int[]{1,2,3})
。
3、使用流的静态方法,比如Stream.of(Object[])
,IntStream.range(int, int)
或者Stream.iterate(Object, UnaryOperator)
,如Stream.iterate(0, n -> n * 2)
,或者generate(Supplier s)
如Stream.generate(Math::random)
。
4、BufferedReader.lines()
从文件中获得行的流。
5、Files
类的操作路径的方法,如list
、find
、walk
等。
6、随机数流Random.ints()
。中间操作
Stream提供的方法中有许多方法会返回一个流,并且是延迟执行的,只有遇到终止操作的时候才会执行。
这些方法有:
-
distinct
distinct
保证输出的流中包含唯一的元素,它是通过
Object.equals(Object)来检查是否包含相同的元素。
-
filter
filter
返回的流中只包含满足断言(predicate)的数据。 -
map
map
方法将流中的元素映射成另外的值,新的值类型可以和原来的元素的类型不同。 -
flatmap
flatmap
方法混合了map
+flattern
的功能,它将映射后的流的元素全部放入到一个新的流中。它的方法定义如下:Stream flatMap(Function super T,? extends Stream extends R>> mapper) 可以看到
mapper
函数会将每一个元素转换成一个流对象,而flatMap
方法返回的流包含的元素为mapper
生成的所有流中的元素。 -
peek
peek
方法主要用来做调试用,比如可以这么做Stream.of("one", "two", "three", "four") .filter(e -> e.length() > 3) .peek(e -> System.out.println("Filtered value: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("Mapped value: " + e)) .collect(Collectors.toList());
-
sorted
sorted
方法将流中的元素排升序,如果元素没有实现Comparable接口,会抛出java.lang.ClassCastException
异常。sorted(Comparator comparator)
可以指定排序的方式。 -
skip
skip
放弃了流中前n个元素,如果流中元素个数小于或等于n则会返回一个空的流。
终止操作
-
Math相关
这一组方法用来检查流中的元素是否满足断言。
allMatch
只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回trueanyMatch
只有在任意一个元素满足断言时就返回true,否则flase,noneMatch
只有在所有的元素都不满足断言时才返回true,否则flase -
count
count
方法返回此时流中的元素的个数 -
collect
R collect(Collector super T,A,R> collector) R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) 辅助类
Collectors
提供了很多的collector,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。我们也可以使用第二种重载方法,第一个参数是一个用来构造容器的Supplier,第二个参数是用来把元素整合在一起的累加器,第三个参数是用来合并两个容器的。
-
find
findAny()
返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst()
,但是有可能多次执行的时候返回的结果不一样。
findFirst()
返回第一个元素,如果流为空,返回空的Optional。 -
forEach
forEach
遍历流的每一个元素, 跟peek
用法相似,区别是peek是中间操作,forEach是终止操作 -
max/min
max
返回流中的最大值,
min
返回流中的最小值。 -
reduce
reduce
是常用的一个方法,常用来做累加操作,事实上很多操作都是基于它实现的。
它有几个重载方法第一个方法使用流中的第一个值作为初始值,后面两个方法则使用一个提供的初始值。
-
toArray
toArray
将流中的元素放入一个数组。
-
LocalDate和LocalDateTime以及LocalTime的使用
java1.8之前的Date类一直被各种诟病,在1.8版本中对这点进行了增强(此处以下文字来源)
处理日期的 LocalDate
不同于 Calendar 既能处理日期又能处理时间,java.time 的新式 API 分离开日期和时间,用单独的类进行处理。LocalDate 专注于处理日期相关信息。
LocalDate 依然是一个不可变类,它关注时间中年月日部分,我们可以通过以下的方法构建和初始化一个 LocalDate 实例:
- public static LocalDate now():截断当前系统时间的年月日信息并初始化一个实例对象
- public static LocalDate of(int year, int month, int dayOfMonth):显式指定年月日信息
- public static LocalDate ofYearDay(int year, int dayOfYear):根据 dayOfYear 可以推出 month 和 dayOfMonth
- public static LocalDate ofEpochDay(long epochDay):相对于格林零时区时间的日偏移量
处理时间的 LocalTime
类似于 LocalDate,LocalTime 专注于时间的处理,它提供小时,分钟,秒,毫微秒的各种处理,我们依然可以通过类似的方式创建一个 LocalTime 实例。
- public static LocalTime now():根据系统当前时刻获取其中的时间部分内容
- public static LocalTime of(int hour, int minute):显式传入小时和分钟来构建一个实例对象
- public static LocalTime of(int hour, int minute, int second):通过传入时分秒构造实例
- public static LocalTime of(int hour, int minute, int second, int nanoOfSecond):传入时分秒和毫微秒构建一个实例
- public static LocalTime ofSecondOfDay(long secondOfDay):传入一个长整型数值代表当前日已经过去的秒数
- public static LocalTime ofNanoOfDay(long nanoOfDay):传入一个长整型代表当前日已经过去的毫微秒数