java 8 方法引用笔记

  

   在看lambda的时候,经常看到 :: 这个双冒号符号,合着也不是c那一系列的语言,这玩意什么意思?百度了很久,原来是java8的新特性——方法引用。网上的看来看去就几篇,有几篇貌似还有冲突,现在总结一下自己的理解。

正文

   首先,很重要的一点,方法引用是一个(精简的)lambda表达式。这就使得本被lambda精简过的代码更加精简了,让人摸不着头脑。(-> :: 尝尝给我一种我在看c++代码的错觉)所以,方法引用是一个函数式接口的实现(简化),由此当看的很懵逼的时候,把他还原有助于理解
  ok,现在说说4种方法引用,这个网上有很多很多版本(叫法),看得我有点…这边我按照我的理解分一下。

名称 形式
1 静态方法的引用 类名::静态方法名
2 构造方法的引用 类名::new
3 实例对象的成员方法的引用 实例对象::成员方法名
4 类的成员方法的引用 类名::成员方法名

   为什么要用方法引用?lambda意在精简,那么有时候我们仅仅是需要调用一个已存在的方法,如果使用方法引用就可以进一步精简(到丧心病狂的地步) 在详细解释之前,先来创造一个试验对象,然后详细的解释

试验对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 实验对象
public class Person {
String name;
Integer age;

public Person() {
}

public Person(Integer age) {
this("default", age);
}

public Person(String name, Integer age) {
super();
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public void playWith(Person another) {
System.out.println(this.name+ " play with " + another.getName());
}

@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}

}

//测试类
class magicTest{

List<Person> pList = new ArrayList<Person>();

@Before
public void init(){
pList.add(new Person("a", 1));
pList.add(new Person("b", 2));
pList.add(new Person("c", 3));
pList.add(new Person("d", 23));
pList.add(new Person("e", 33));
}

......
}




静态方法的引用

   首先是最容易理解的,静态方法的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class magicTest{
......

//一个静态方法
public static void staticMethod(Person person){
System.out.println("this is "+person.getName() + ",now is "+person.getAge()+" years old");
}


@Test
public void t1(){

//type 1
//最原始的方法 直接创建一个实现类
Consumer<Person> consumer = new Consumer<Person>() {
@Override
public void accept(Person t) {
magicTest.staticMethod(t);
}
};
//然后在需要使用的地方使用自己创建的类
pList.stream().forEach(consumer);


//type 2
//简化一下 用了内部匿名类
pList.stream().forEach(new Consumer<Person>() {
@Override
public void accept(Person t) {
magicTest.staticMethod(t);
}
});
//用lambda 简化
pList.stream().forEach(p->magicTest.staticMethod(p));


//方法引用 再次简化
pList.stream().forEach(magicTest::staticMethod);

}

}

   我在刚刚看方法应用的时候,经常会报错,各种各样的错。踩了很多坑之后,发现了一个要点:使用方法引用,必须注意入参与返回参,正文第一句话,强调过他其实是一个lambda表达式,所以该表达式实现的接口 ,引用方法,以及 接口调用处(使用 lambda表达式/方法引用表达式 的地方)的入参 与 返回参 的类型必须是要一致的。
   用上面的代码来解释看看,首先我用的方法是Stream下的forEach方法,这个方法接收的是一个 Consumer的实现类 ,forEach内Consumer 默认的入参是 Stream转化前Collection的定义时传入的泛型类,在上面的代码中就是 Person。
   1.接口调用处 入参 person ,同时实现的接口 入参 person 返回 void
   2.再看看那个静态方法,入参 person 返回 void
   3.ojbk

构造方法的引用

   构造方法的引用看起来应该是最简单的,但是我在摸索的时候也进了很多坑,不过记住上面说的 入参 出参 保持一致,那应该就不会出什么问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//首先是java8系统提供的一个函数式接口Supplier,没有入参,获得一个泛型指定类型的对象,这对应的一般是默认的构造方法:无入参,返回对象
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}


class magicTest{
......

@Test
public void t2(){

//默认的构造方法
Supplier<Person> s1 = new Supplier<Person>() {
@Override
public Person get() {
return new Person();
}
};

//方法引用简化
Supplier<Person> s1plus = Person::new;

//怎么用呢?stream中确实有几个需要Supplier的方法,不过不在这边展开,和方法引用关系不大,是stream的事情,简单的使用就像这样
Person p = s1plus.get();
}

}

  说道这,就有问题了,构造函数有入参呢?我也不能再new后面加个括号写参数啊!注意啊,方法引用是只写方法名的,不要(能)写括号和参数。回忆一下,方法引用是啥?是lambda表达式,是函数式接口的实现!所以这个这个问题可以自己定义函数式接口来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//先自己定义一个函数式接口:只能有一个方法,用@FunctionalInterface标注
@FunctionalInterface
public interface MagicInterface {
Person create(String name, Integer age);
}

class magicTest{
.....
@Test
public void t3(){
//用自定义的函数式接口来使用
MagicInterface mi = Person::new;
Person p2 = mi.create("test", 1);

//好了,这仅仅是一个演示,不具备实际使用价值,一般会在Stream中用方法引用来精简
}
}

实例对象的成员方法的引用

  实例对象的引用,这个顾名思义,就是引用一个实例的方法,不多废话上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class magicTest{

@Test
public void t4(){

//创建了一个实例对象
Person boy = new Person("magic", 26);

//依然使用consumer
Consumer<Person> c1 = new Consumer<Person>() {
@Override
public void accept(Person t) {
boy.playWith(t);
}
};

//精简一下就是
Consumer<Person> c1plus = boy::playWith;

//在stream中的使用
pList.stream().forEach(boy::playWith);

}

}

类的成员方法的引用

  好了,终于到这里了。仔细看看这个标题,有没有违和感?类的成员方法的引用。一般来说的话,除了静态方法,有见过直接用类调用方法的么???没有吧!这tm就很蛋疼了,网上百度方法引用,全部直接给你个例子:

1
2
3
4
//假设 O是类
list.stream().forEach(o->o.method());
//可以简化成
list.forEach(O::method);

  这样的文章看了很多篇,经常还是第一个讲这种引用方式,给人一种很简单的感觉。先思考一下,下面的代码有没有问题:

1
2
3
4
5
6
7
class magicTest{

@Test
public void t5(){
pList.stream().forEach(Person::playWith);
}
}

  好像也没什么问题嘛…playWith的方法接受一个person作为入参,而forEach正好提供了一个person,是不是?不是!把他还原出来看看问题就出来了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class magicTest{
@Test
public void t6(){
Consumer<Person> c2 = Person::playWith; //这是不行的

Consumer<Person> c2plus = new Consumer<Person>() {
@Override
public void accept(Person t) {
//关键在这里
t.playWith("????");
xxx.playWith(t);
}
};

}
}


  真的是坑死我了,为什么?为什么刚刚的实例调用没问题,现在用类调用就有问题?对比一下accept的实现方法看看:

1
2
3
4
5
6
7
8
9
10
//对象方法引用
public void accept(Person t) {
boy.playWith(t);
}

//类方法引用
public void accept(Person t) {
t.playWith("????");
xxx.playWith(t);
}

  一目了然,在对象方法引用的时候,接受的参数person很明显的作为对象boy的成员方法palyWith的入参,这实在是太正常了,让人不假思索就这样认为了。而类方法引用时就出了一点问题,貌似…一个参数不够啊?forEach提供了一个person,这一个person到底作为方法的调用者呢?还是方法的参数呢? 不可确定…
  好,现在回过头去执行一下上一节的T4方法得输出到结果:

1
2
3
4
5
magic play with a
magic play with b
magic play with c
magic play with d
magic play with e

  这输出结果和我们还原的实现方法一样!实例对象的引用没问题了,现在接着刚刚的话题,类方法所需对象少了一个。如果执行一个不需要的入参的方法会怎么样?在person类中新增一个成员方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Person {
...
public void play() {
System.out.println(this.name+ " play with himself" );
}
...
}

class magicTest{
@Test
public void t7(){
//类方法引用
Consumer<Person> c3 = Person::play;
//扩产缩写
Consumer<Person> c3plus = new Consumer<Person>() {
@Override
public void accept(Person t) {
t.play();
}
};
//使用测试
pList.stream().forEach(Person::play);
}
}

输出结果:
a play with himself
b play with himself
c play with himself
d play with himself
e play with himself

  由此可见,forEach中提供的参数person确实是被当做了类方法的调用者,由于Consumer在stream中使用的很频繁,那么做一个不负责的总结:实现Consumer的 类方法引用,那个类必须是Consumer的泛型类,并且方法不可接受参数,换句刷说就是 Consumer c = T::method ,method不接受入参。
  继续思考这个问题,假如我就是要在用playWith方法的时候(有入参的方法)用类方法引用呢?刚刚已经想到了他缺了一个参数,那试验的方向就很明显了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//首先自建一个函数接口
@FunctionalInterface
public interface MyInterface {
void method(Person p1 ,Person p2);
}

class magicTest{
@Test
public void t8(){
Person p1 = new Person("p1", 1);
Person p2 = new Person("p2", 1);

MyInterface m = Person::playWith;

//由于是自己创建的函数式接口,就简单的手动调用一下吧
m.method(p1, p2);

}
}

输出结果:
p1 play with p2

  到了这里就几乎已经有一个答案呼之欲出了,首先再次验证了在静态方法引用那一节归纳的,需要满足入参和出参对应一致。然后在做一个猜测:在stream.forEach中Consumer用类成员方法引用实现的时候,java可能已经隐式的替我们传入了一个对象,那一个对象作为consumer实现方法的执行者。 上面的几个试验也确实验证了这个猜测,无参方法(play)使用forEach传入的对象作为执行者。有参方法(playWith)仅有执行者无执行对象所以会报错,而使用了自定义的双参函数接口后,可以正常使用。
  说道这里,回到本节开头,类的方法引用没有没违和感?有!确实,类不能直接使用成员方法(static除外),但是 类的成员方法引用 其实用了第一个接受的参数(该类的实例对象)作为方法的执行者,类并没有直接调用方法。所以在使用类方法引用的时候一定要注意,要提供该类的实例(验证就请各位自己验证吧,如果说错了请记得联系我打我脸)。

后记

  这篇文章把自己在看方法引用的时候遇到的问题,思考的过程,个人的总结都记录了下来,以防自己忘记。
  由于我没有给网站配置搜索和留言,这文在搜索引擎应该是搜不到的。如果有人能看到这篇文章,并且有疑问的话,可以联系我的qq 418379149.