Java中的匿名内部类及Lambda表达式

匿名内部类适合那些只需要使用一次的类,比如在对按钮等进行事件监听的时候会用到

Lambda表达式(lambda expression)是一个匿名函数,即没有函数名的函数。

lambda表达式大量替代匿名内部类的使用,简化代码的同时,更突出了原来匿名内部类中最重要的那部分包含真正逻辑的代码。

一、匿名内部类

定义

匿名内部类没有名字,其定义格式如下:

1
2
3
4
new 父类构造器(参数列表)|实现接口()  
{
//匿名内部类的类体部分
}

如下所示为一个典型的常用内部类:

1
2
3
4
5
6
new View.OnClickListener(){
@Override
public void onClick(View v) {

}
};

匿名内部类适合那些只需要使用一次的类,比如在对按钮等进行事件监听的时候会用到,但是上面例子中的OnClickListener是一个接口,对接口进行new显然是错误的。
下面来还原一个完整的匿名内部类应该就很清楚了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Person {
public abstract void eat();
}

class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}

public class Demo {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}

Person是一个抽象类,Child继承这个类成为一个子类,main函数里实例化这个类,其中我们需要在匿名内部类中隐藏Child这个类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Person {
public abstract void eat();
}

public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}

我们可以看到,这样相当于在实例化的时候直接重写了Child这个类,这就是匿名内部类。


必须继承一个父类或者实现一个接口,没有class关键字,直接使用new生成一个对象的引用。如下所示:

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
public abstract class Bird {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract int fly();
}

public class Test {
public void test(Bird bird){
System.out.println(bird.getName() + "能够飞 " + bird.fly() + "米");
}
public static void main(String[] args) {
Test test = new Test();
test.test(new Bird() {
public int fly() {
return 10000;
}
public String getName() {
return "大雁";
}
});
}
}
------------------
Output:
大雁能够飞 10000

test方法接收一个Bird类型参数,但是我们都知道一个抽象类是不能进行实例化的,也就是直接new,所以我们必须要有一个实现类才可以进行new操作,但是由于它是一个抽象类,所以通过匿名内部类创建一个Bird实例。又因为匿名内部类不能是抽象类,所以我们必须要实现抽象父类或者接口里的所有抽象方法才行。

注意事项

  1. 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。
  2. 匿名内部类中是不能定义构造函数的。
  3. 匿名内部类中不能存在任何的静态成员变量和静态方法。
  4. 匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
  5. 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

匿名内部类的初始化

它没有构造函数,name怎么进行相应初始化呢?
使用构造代码块进行初始化,可以达到一个构造器的效果。

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
public class OutClass {
public InnerClass getInnerClass(final int age,final String name){
return new InnerClass() {
int age_ ;
String name_;
//构造代码块完成初始化工作
{
if(0 < age && age < 200){
age_ = age;
name_ = name;
}
}
public String getName() {
return name_;
}

public int getAge() {
return age_;
}
};
}

public static void main(String[] args) {
OutClass out = new OutClass();

InnerClass inner_1 = out.getInnerClass(201, "chenssy");
System.out.println(inner_1.getName());

InnerClass inner_2 = out.getInnerClass(23, "chenssy");
System.out.println(inner_2.getName());
}
}

补充:Java中的代码块

代码块就是用{}包起来的代码,进行封装,形成一个独立的数据体,用于实现特定的算法。代码块不能单独运行,必须要有一个运行主体。
Java的代码块主要分为四种、

  1. 普通代码块
    普通代码块是不能够单独存在的,它必须要紧跟在方法名后面。同时也必须要使用方法名调用它。
1
2
3
4
5
public class Test {
public void test(){
System.out.println("普通代码块");
}
}
  1. 静态代码块
    想到静态我们就会想到static,静态代码块就是用static修饰的用{}括起来的代码段,它的主要目的就是对静态属性进行初始化。
1
2
3
4
5
public class Test {
static{
System.out.println("静态代码块");
}
}
  1. 同步代码块
    使用 synchronized 关键字修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
  2. 构造代码块
    在类中直接定义没有任何修饰符、前缀、后缀的代码块即为构造代码块。我们明白一个类必须至少有一个构造函数,构造函数在生成对象时被调用。构造代码块和构造函数一样同样是在生成一个对象时被调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
/**
* 构造代码
*/
{
System.out.println("执行构造代码块...");
}

/**
* 无参构造函数
*/
public Test(){
System.out.println("执行无参构造函数...");
}

/**
* 有参构造函数
* @param id id
*/
public Test(String id){
System.out.println("执行有参构造函数...");
}
}

上面定义了一个非常简单的类,该类包含无参构造函数、有参构造函数以及构造代码块,同时在上面也提过代码块是没有独立运行的能力,他必须要有一个可以承载的载体,那么编译器会如何来处理构造代码块呢?编译器会将代码块按照他们的顺序(假如有多个代码块)插入到所有的构造函数的最前端,这样就能保证不管调用哪个构造函数都会执行所有的构造代码块。上面代码等同于如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
/**
* 无参构造函数
*/
public Test(){
System.out.println("执行构造代码块...");
System.out.println("执行无参构造函数...");
}

/**
* 有参构造函数
* @param id id
*/
public Test(String id){
System.out.println("执行构造代码块...");
System.out.println("执行有参构造函数...");
}

}

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
new Test();
System.out.println("----------------");
new Test("1");
}
------------
Output:
执行构造代码块...
执行无参构造函数...
----------------
执行构造代码块...
执行有参构造函数...

从上面的运行结果可以看出在new一个对象的时候总是先执行构造代码,再执行构造函数,但是有一点需要注意构造代码不是在构造函数之前运行的,它是依托构造函数执行的。
各个代码块执行顺序为:静态代码块 > 构造代码块 > 构造函数。

lambda表达式

“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。
Java8发布已经有一段时间了,这次发布的改动比较大,很多人将这次改动与Java5的升级相提并论。Java8其中一个很重要的新特性就是lambda表达式,允许我们将行为传到函数中。想想看,在Java8之前我们想要将行为传入函数,仅有的选择就是匿名内部类。Java8发布以后,lambda表达式将大量替代匿名内部类的使用,简化代码的同时,更突出了原来匿名内部类中最重要的那部分包含真正逻辑的代码。
下面看看一些常用写法:

替代匿名内部类

毫无疑问,lambda表达式用得最多的场合就是替代匿名内部类,而实现Runnable接口是匿名内部类的经典例子。lambda表达式的功能相当强大,用()->就可以代替整个匿名内部类!
这是我们上面刚说的匿名内部类

1
2
3
4
5
6
7
8
public void oldRunable() {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("The old runable now is using!");
}
}).start();
}

下面我们看看怎么用表达式写出来

1
2
3
4
5
6
7
public void runable() {
new Thread(() -> System.out.println("It's a lambda function!")).start();
}

输出结果
The old runable now is using!
It's a lambda function!

使用lambda表达式对集合进行迭代

Java的集合类是日常开发中经常用到的,甚至说没有哪个java代码中没有使用到集合类…而对集合类最常见的操作就是进行迭代遍历了。
虽然Java里没用过foreach,但是我在c#里提过。

1
2
3
4
5
6
7
8
9
10
public void iterTest() {
List<String> languages = Arrays.asList("java","scala","python");
//before java8
for(String each:languages) {
System.out.println(each);
}
//after java8
languages.forEach(x -> System.out.println(x));
languages.forEach(System.out::println);
}

用lambda表达式实现map

至于什么是map可以百度以下。
一提到函数式编程,一提到lambda表达式,怎么能不提map…没错,java8肯定也是支持的。

1
2
3
4
5
6
7
8
9
public void mapTest() {
List<Double> cost = Arrays.asList(10.0, 20.0,30.0);
cost.stream().map(x -> x + x*0.05).forEach(x -> System.out.println(x));
}

输出结果
10.5
21.0
31.5

map函数可以说是函数式编程里最重要的一个方法了。map的作用是将一个对象变换为另外一个。在我们的例子中,就是通过map方法将cost增加了0.05倍的大小然后输出。

用lambda表达式实现map与reduce

既然提到了map,又怎能不提到reduce。reduce与map一样,也是函数式编程里最重要的几个方法之一…map的作用是将一个对象变为另外一个,而reduce实现的则是将所有值合并为一个。

1
2
3
4
5
6
7
8
public void mapReduceTest() {
List<Double> cost = Arrays.asList(10.0, 20.0,30.0);
double allCost = cost.stream().map(x -> x+x*0.05).reduce((sum,x) -> sum + x).get();
System.out.println(allCost);
}

输出结果
63.0

filter操作

filter也是我们经常使用的一个操作。在操作集合的时候,经常需要从原始的集合中过滤掉一部分元素。

1
2
3
4
5
6
7
8
9
10
public void filterTest() {
List<Double> cost = Arrays.asList(10.0, 20.0,30.0,40.0);
List<Double> filteredCost = cost.stream().filter(x -> x > 25.0).collect(Collectors.toList());
filteredCost.forEach(x -> System.out.println(x));

}

输出结果
30.0
40.0

与函数式接口Predicate配合

除了在语言层面支持函数式编程风格,Java 8也添加了一个包,叫做 java.util.function。它包含了很多类,用来支持Java的函数式编程。其中一个便是Predicate,使用 java.util.function.Predicate 函数式接口以及lambda表达式,可以向API方法添加逻辑,用更少的代码支持更多的动态行为。Predicate接口非常适用于做过滤。

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
public static void filterTest(List<String> languages, Predicate<String> condition) {
languages.stream().filter(x -> condition.test(x)).forEach(x -> System.out.println(x + " "));
}

public static void main(String[] args) {
List<String> languages = Arrays.asList("Java","Python","scala","Shell","R");
System.out.println("Language starts with J: ");
filterTest(languages,x -> x.startsWith("J"));
System.out.println("\nLanguage ends with a: ");
filterTest(languages,x -> x.endsWith("a"));
System.out.println("\nAll languages: ");
filterTest(languages,x -> true);
System.out.println("\nNo languages: ");
filterTest(languages,x -> false);
System.out.println("\nLanguage length bigger three: ");
filterTest(languages,x -> x.length() > 4);
}

运行结果
Language starts with J:
Java

Language ends with a:
Java
scala

All languages:
Java
Python
scala
Shell
R

No languages:

Language length bigger three:
Python
scala
Shell

可以看到,Stream API的过滤方法也接受一个Predicate,这意味着可以将我们定制的 filter() 方法替换成写在里面的内联代码,这也是lambda表达式的魔力。

感谢您的支持!