文章目录
说明
因为是个人复习java的总结,所以结构稍显杂乱,有些语句过于口语化.
接下来的学习会尽量增加代码的思考,光看理论终究是虚的.
对于这一篇文章的思考
这一篇文章篇幅比较长,主要是因为我在进入函数式接口时对于接口函数以及stream流到底有什么用过于深究,但是从结果来看过程这个过程是有意义的.
刚开始看函数式接口时,我觉得这种方式有什么用,为了使用还要去定义一个接口,还要重写方法.还不如采用其他方式来的效率高.更不要说stream流这种很可能影响使用效率的方式…
但是随着深入的理解,发现其实这里讲的是一种使用接口的编程方式,而不是说这些东西到底有什么用.函数式接口描述的实质是简化你使用接口的编程,而不是说让你为了使用而去使用接口编程.
接口的编程方式具体有什么好处这里就不提,但是总结来说庆幸自己坚持啃完这部分,不是说这部分多有用,而是说至少没有忽视他可能有用的方面.
文中大家很容易看出我情绪的变化,可能情绪导致语言有些影响阅读,请大家见谅.
如果时间比较紧,建议这部分主要理解一下lambda和方法引用,至于接口这些可以简单地过一下.
函数式接口
其实就是符合函数式编程的接口,也就是lambda使用的只有一个抽象方法的接口
格式如下:
修饰符 interface 接口名称{
public abstract 返回值 方法名称(参数列表); //权限和抽象标识可以省略
//其他非抽象方法,
}
注解
其实就相当于一个便利贴,在程序中增加一个告诉虚拟机的信息,比如告诉编译器这里可能有错误(@Override,@FunctionalInterface),又或者软件工具利用注解来生成代码(@Param),亦或是在程序运行中接收代码的提取.
因为注解涉及到部分反射内容,在之后的内容中会再次整理
这里主要是说明一下@FunctionalInterface,这个注解可以用来检测接口是否为函数式接口
@Override可以用来检测方法是否为重写方法.
语法糖
其实就是更好吃的语法,比如说for-each语法,底层的原理其实不变,但是语法更为简洁.
而lambda其实严格意义上不算语法糖,因为lambda和匿名内部类的底层实现不太一样.
函数式接口的使用
一般是作为方法的参数和返回值使用.
作为方法的参数,也就是将实现这个接口的实现类的对象作为参数传入,其实这个方式之前用到过,比如说线程的构造方法Thread()中就有一个对Runnable接口的参数,又或者是之前文件提到的文件过滤器listFiles(FilenameFilter filter)也是对FilenameFilter也是一个接口.
对于这种方式,采用匿名内部类就可以使用lambda表达式简化,比如说
//原形式
new Thread(new Runnable(){
@Override
public void run(){
System.out.println(“使用匿名内部类实现接口”);
}
}).start(); //很容易忽略线程的启动
//简化后的形式
new Thread(() -> System.out.println(“使用lambda表达式实现接口”)).start();
作为方法的返回值其实也就是相当于接口的实现类对象作为方法返回,之前说过Arrays.sort(int a[], Comparator<> comparator)方法,其实后半部分的参数就可以通过定义一个Comparator<>返回类型的方法,然后return的是对函数式接口中compare()的重写,也就是lambda写的返回.
lambda和匿名内部类的区别
匿名内部类的原理是生成一个匿名的内部类,会生成.class文件在相应文件夹中,但是lambda和匿名内部类不同.lambda不是通过匿名内部类实现的底层,而是使用了一种类似匿名内部类底层实现的方式.lambda不会生成.class文件,而是在原本的类中生成一个方法,再通过invokedynamic,通过一个创建的lambda地内部类来调用这个方法.也就是说实际上方法的具体实现不是在内部类中,而是在使用lambda的类中.
那么就出现了一个问题,既然lambda也是通过内部类调用,为什么就不是匿名内部类的实现方式.这个具体涉及到底层很多复杂的内容,无法详解.如果有兴趣可以查找其他博客了解.
但是可以简单地先做一些区分,匿名内部类创建一个,就会有一个匿名内部类文件,但是lambda如果使用多次,只会在调用地类中创建多个方法,而lambda类不会出现新的.
性能浪费的日志案例
private static void showLog(int level, String message) {
if(level == 1) {
System.out.println(message);
}
}
public static void main(String[] args) {
String msg1 = "Hello";
String msg2 = "world";
String msg3 = "Java";
showLog(2, msg1+msg2+msg3);
}
其实就是说对于这个代码来说,如果前面你的level和判断中的内容不匹配,后面传入的String信息就白拼接了,前面也提到过,这种字符串的拼接是很浪费的一种方式,前面如果还不匹配就更加浪费.
那么接下来使用lambda进行一些优化
//测试类
public class Logger {
private static void showLog(int level, MessageBuilder mb) {
if(level == 1) {
System.out.println(mb.buldMessage());
}
}
public static void main(String[] args) {
String msg1 = "Hello";
String msg2 = "world";
String msg3 = "Java";
showLog(2, ()->{
return msg1+msg2+msg3;
});
}
}
//接口
public interface MessageBuilder {
public abstract String buldMessage() ;
}
这里是通过对传入的接口设置一个接口,然后再通过lambda表达式传递参数.可能乍看觉得这么做纯属浪费时间,还额外定义了一个接口.但是实际上这么做之后,如果判断不成立就不会对传入的字符串进行拼接. 这个案例可以增加对lambda的了解,lambda实际上是在类中生成一个方法,但是不会直接去调用,而是先将lambda作为参数传入,先在判断的方法中判断,如果成立才会调用到了lambda中重写的方法才会调用这个方法,对里面的字符串进行拼接再输出. 其实使用内部类也可以达到一样的效果,就是延迟一些浪费资源的操作. 当然这个案例实际上是模拟案例,如果真的对这个案例进行性能分析,会发现没优化之前的性能比优化后高很多.但是需要思考,对于实际的应用中,肯定不是简单的一个字符串的拼接问题,如果大量的字符串在常量池出现,这会对内存造成很大的影响.那时对比于使用lambda,性能的影响就显得微不足道.
lambda作为参数和返回值的案例
这里就是对前面提到的lambda的使用方式的案例时间,下面是Thread中传入Runnable参数,也就是lambda作为参数传递的方式.
public static void startThread(Runnable run) {
new Thread(run).start();
}
public static void main(String[] args) {
startThread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
startThread(() -> Thread.currentThread().getName());
}
下面的案例是lambda表达式作为返回值传递的方式,也就是之前体到的Array.sort()中可以提到的Comparator接口中compare()方法使用lambda作为return.
public static Comparator<String> getComparator(){
/*
return new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
};
*/
return (o1,o2) -> o1.length()-o2.length();
}
常用的函数式接口
放在java.util.function包中的一些常用的函数接口.
Supplier接口
其实就是一个用来获取某种类型的数据的函数式接口,其中只有一个get()方法.比如说调用某个函数需要String类型的数据,那么就可以调用一个String返回类型的方法,而这个方法中以Supplier作为参数,方法体中return get()方法获取的值.然后就可以在调用需要String类型的函数的时候使用lambda将get()重写,然后得到相应类型的数据.这个也可以用来创建对象.
但是其实这个接口单独这么看没有任何用处,Supplier需要和其他的接口函数相配合才会显示出效果,所以接下来还会分析.
并且对于使用Supplier获取对象其实还有简写的方式
Supplier<TestSupplier> sup= TestSupplier::new;
但是其中涉及到方法引用,所以也在之后的内容中再补充完整.
默认方法(补充一些概念)
默认方法就是接口中已经实现,不需要实现类实现的方法,使用default标记.这个默认方法也是为了解决不能多继承的问题,也就是为了能修改一个接口使得所有实现类调用的都改变.如果实现的多个接口中的默认方法发生了冲突,首先可以自身重写这个默认方法,或者就是使用
接口名.super.默认方法名
的方式来实现对指定默认方法的调用.
Consumer接口
这个接口其实和Supplier相反,Supplier是获取一个类型的数据,而Consumer则是消费指定类型的数据,也就是使用指定类型的数据,其中只有一个accept()抽象方法,也是需要重写的具体使用数据的方法.
使用上的话其实也和Supplier的方式一样,只是具体的用处是不同的,一个是获取并返回,一个是使用数据.
Consumer的andThen()默认方法
返回值default Consumer 方法andThen(Consumer<? super T> after)
这个方法实际上就是将两个Consumer接口组合在一起,然后再对数据进行消费.
因为实际使用中这个泛型一般为对象,这里可以看作当有两个对象需要对同一个数据进行操作的时候可以使用andThen().而且从其泛型可以看出,后面这个对象一定要是前面对象的父类.
语句就是第一个对象.andThen(第二个对象).accept()
下面是一个案例,但是实际上没什么用,只是用来理解一下
就是对于一个字符串可能实际上有两个信息需要输出,就可以通过Consumer+lambda的方式使得同一个数据被不同的方式消耗.
public static void main(String[] args) {
String[] array = { "小明,男", "小王,男", "小花,女" };
printInfo(s -> {
System.out.print("姓名" + s.split(",")[0]);
}, s -> {
System.out.println("性别" + s.split(",")[1]);
}, array);
}
public static void printInfo(Consumer<String> con1, Consumer<String> con2, String[] array) {
//遍历每个字符串进行操作
for (String info : array) {
con1.andThen(con2).accept(info);
}
}
真的用的时候,可能传入一个对象进行两种操作.虽然我看到现在也还没发现具体在什么途径可以有效地使用这接口.
Predicate接口
用于某种数据类型的判断的接口,使用和上面差不多.还需要继续往下学才能了解其作用
示例
public static void main(String[] args) {
String str = "abc";
System.out.println(checkString(str, s -> s.length()>5));
}
public static boolean checkString(String s, Predicate<String> pre) {
return pre.test(s);
}
Predicate默认方法
and()方法
default Predicate<T> and(Predicate<? super T> other)`
其底层就是用&&连接两个test()方法.
所以很容易联想到他还有or()方法
这里示例就不写了,和上面andThen()没有什么区别,甚至直接写运算符还方便点
Function接口
是根据一个类型的数据得到另外一个类型的数据,主要实现其中的apply()方法.
使用也是跟前面的几个接口一样,接口加lambda的方式,具体转换的apply()自己重写.
Function接口andThen(),其实就是可以转换两次类型,将输入的类型转换操作之后再转换.
default <V> Function<T,V> andThen(Function<? super R,? extends V> after)
使用案例如下
public static void change(String str, Function<String, Integer> fun1, Function<Integer, String> fun2) {
String s = fun1.andThen(fun2).apply(str);
System.out.println(s);
}
public static void main(String[] args) {
String s = "111";
change(s, a -> Integer.parseInt(a)+100, i -> i + "");
}
看到这里我顿然醒悟,上面这些接口不就类似平时编程的时候对于一些比较判断的操作进行封装然后使用吗.平时可能使用String类之类的有写好的比较方法,但是对于自定义类,这不就是一个现成的接口标准吗,套这个接口就能使用这个方法.
而且上面案例都是将接口作为参数,如果直接用lambda把接口给实现了,那不就是非常精简吗.之前一直在纠结这些接口有什么用,回过头才发现,重要的不是接口,而是这些接口是给lambda使用的,其延迟生效的特性可以节约性能,代码又精简.
流式思想
其实就是说对于一个数据流,按照步骤不断进行过滤和操作,从而达到想要的数据流的过程.但是说实话,下面使用的Stream流主要是对于代码的简化,在一些场合效率可能没有for循环来的好用.其中还有一个并行流,效率会稍有提高,但是有人提到并行流使用中可能存在一些问题.
对于Stream流的使用,还是需要看对集合是否有必要进行多次循环操作,如果有必要,使用这种方式进行简化代码是非常好的,但是如果根本不用循环多次,可以使用类似&&或者是一个for中进行多个操作大概目的的,就没必要使用Stream流了.使用Stream流还得循环多次,效率反而更低.
Stream流
只能说这个Stream流是一个很花里胡哨的东西,将集合或者是数组转换成流的思想进行操作,像流一样一步一步地对集合或者数组进行操作,然后得到想要的数据流.
实际上是一种算法.类似一个循环套一个循环.其实还分为并行流,但是并行流涉及到底层的调度,十分复杂,不好使用.
接下来看一下案例以及使用
假设你对于一个ArrayList集合存了一些名字,你要对这个名字做一个筛选,假设你要选姓张的,名字长度三,再打印输出.
那么你先不要考虑自己的想法,因为是介绍stream流的使用,你先从流的思想考虑,也就是你要先循环找出姓张的,再循环找出名字长度三的,最后循环输出.emmmm…反正就先这么看,不要深究,那么实现上面的编程就需要写三个循环,特别麻烦.这时候你使用stream流,豁然开朗,就一句.
list.stream()
.filter(name->name.startsWith("张"))
.filter(name->name.length()==3)
.forEach(name->System.out.println(name));
这样显得代码简洁了很多,但是深究一下,哪个傻子会写三个循环,用&&一个循环就搞定,效率比你高了不知道多少.
所以需要强调,需要看场合使用这个stream流,不要因为简化代码而损失效率.
两种获取stream流的方式
第一种就是Collection的子类都有stream()方法可以直接获取
第二种Stream接口的静态方法of()可以获取数据对应的流
stream流常用的方法
分为两类,延迟方法和终结方法,也就是一个返回的还是stream,一个返回的就不是stream.一个能继续连接方法操作,一个不行.
void forEach(Consumer<? super T> action)
可以看出是一个终结方法并且带的是Consumer接口的消耗接口
Long Count()
就是返回流中的元素个数
Stream<T> filter(Predicate<? super T> predicate)
可以看出这是一个过滤方法,其中的接口是判定接口
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
其实就是可以将流中的数据转换成另一个类型
Stream<T> limit(Long maxSize)
其实就是可以对流中数据进行一个截取,截取前几个元素
Stream<T> skip(Long n)
就是截取后几个元素,逃过前面几个
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
就是将两个流合并成一个流,按输入顺序组合
方法引用
其实是对于lambda表达式的优化,但是需要注意使用情况,这里的使用情况其实是如果lambda中冲写的抽象方法其实已经通过某个方法实现了,那么就没必要再次重写这个方法,直接引用这个方法作为其重写方法的实质就可以了.
比如说现在定义了一个接口
public interface Printable {
void print(String s);
}
然后在测试类中用lambda实现这个接口
public static void main(String[] args) {
printString(s -> System.out.println(s));
}
private static void printString(Printable data) {
data.print("Hello, World!");
}
这时候就会发现,其实lambda中所要实现的其实就是System.out.println();那么就完全不需要重写这个方法,直接使用System.out::println作为方法的重写传入.也就是说将println()方法作为实际对于接口print()方法的重写.这样的好处也很容易明白,如果多次调用这个实现接口的方法,而其中的实现实际上相同的时候,就可以复用方法,而不需要每次使用时都通过lambda重写这个方法.
使用方法引用之后的效果也就是这样的
public static void main(String[] args) {
printString(System.out::println);
}
private static void printString(Printable data) {
data.print("Hello, World!");
}
这时可能有人疑惑为什么是System.out::println,其实这个语句表达的就是System.out对象可以调用的println()方法.out是System类下打印输出流的一个静态对象. 上面是方法引用中特定对象的引用,其实共有四种引用方式:
- 构造器引用:它的语法是Class::new,或者更一般的Class< T >::new
- 静态方法引用:它的语法是Class::static_method
- 特定类的任意对象的方法引用:它的语法是Class::method
- 特定对象的方法引用:它的语法是instance::method
其中3说的其实是类似 String::toString,对应的Lambda:(s) -> s.toString()
也就是说是说如果具体方法的参数比接口定义的参数少一个,那么将第一个参数作为对象去调用具体的方法.
或者说其实lambda的实现方法中传入的第一个是对象,而后面的参数刚好匹配前面对象能调用的方法的参数列表,且就是我想实现的方法.那么久可以直接使用这个对象的类去调用这个成员方法.
比较难以理解,实际中应该不常使用.
super和this和方法引用
之前提到过lambda中的this和super其实是外部类的,区别于匿名内部类中是自身类.super和this的引用可以在下面的例子中详细了解:
//接口
public interface Greetable {
void greet();
}
//父类
public class Human {
public void sayHello() {
System.out.println("Human");
}
}
//子类
public class Man extends Human{
@Override
public void sayHello() {
System.out.println("Man");
}
public void mthod(Greetable g) {
g.greet();
}
public void show() {
/*
mthod(()->{
Human h = new Human();
h.sayHello();
});
*/
mthod(super::sayHello);
}
public static void main(String[] args) {
new Man().show();
}
}
其实上面的案例可以看出来这里就是前面提到的第三类方法引用的实现.super作为类,实际创造一个对象去引用成员变量.
那么this关键字的使用也是类似的,这里就不继续说了.
如有错误欢迎读者批评指正!!