JDK1.8 —— lambda 表达式(以一种更优雅的方式编写Java代码)

1. 函数式接口

Java 在 JDK1.8 之后引入了 lambda 表达式,在了解 lambda 表达式之前,我们首先需要了解下什么是函数式接口。所谓的函数式接口,就是指只有一个抽象方法的接口。例如 ActionListener 接口就是一个函数式接口:

public interface ActionListener extends EventListener {

    /**
     * Invoked when an action occurs.
     */
    public void actionPerformed(ActionEvent e);

}

对于函数式接口,需要强调的是“只有一个抽象方法”,既不能没有抽象方法,也不能多于一个。为什么要强调是抽象方法呢?Java 接口的方法不都是抽象的吗?那是不是改成只有一个方法更好呢?

实际上,接口完全有可能重新申明 Object 类的方法,如 toString 或 clone,这些声明有可能会让方法不再是抽象的。更重要的是,JDK1.8 之后接口中已经可以声明非抽象默认方法了。如:

public interface Lambda {
	
	default int method() {
		return 0;
	}
	
}

如果在设计自己的接口时,接口中只有一个抽象方法,可以用 @FunctionalInterface 注 解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外javadoc 页里会指出你的接口是一个函数式接口。

2. 从匿名内部类到 lambda 表达式

假如我们在编程中需要用到某个方法,这个方法需要接收一个实现了某个函数式接口的对象参数,例如:

	addActionListener(ActionListener l);

这时候我们该怎么实现此方法呢?这里比较容易想到的方法大概有四种,分别是:

  1. 编写一个普通的类,让这个类实现 ActionListener 接口,然后实例化一个对象传入方法中。
  2. 编写一个一般内部类或局部内部类(根据方法所在位置决定),让这个类实现 ActionListener 接口,然后实例化一个对象传入方法中。
  3. 使用匿名内部类传入一个实现了 ActionListener 接口的对象。
  4. 使用 lambda 表达式。

在 JDK1.8 之前,较为方便的方法是使用匿名内部类,但随着 lambda 表达式的出现,在函数式接口上,我们显然有了一个更好的选择。接下来将分别用上述四种方法进行具体的实现。

2.1 通过编写一个普通的类实现

// 编写一个实现了 ActionListener 接口的类
public class MyActionListener implements ActionListener {

	@Override
	public void actionPerformed(ActionEvent e) {
		// TODO Auto-generated method stub

	}

}
	addActionListener(new MyActionListener());

使用此方法实现时,我们需要编写一个实现了 ActionListener 的类,这样做显然比较繁琐,同时我们还需要为此再增加一个.java文件。

当然这么做也有其自己的优点——当在其它类中需要一个实现了 ActionListener 接口的对象,且接口中所需实现的方法的功能相同时(这种情况一般较少),就可以不必重新编写代码,直接实例化一个此类的对象即可。

2.2 通过内部类实现

public class MyClass {
	// ...

	// 内部类
	public class MyActionListener implements ActionListener {
		@Override
		public void actionPerformed(ActionEvent e) {
			// TODO Auto-generated method stub
		}
	}

	addActionListener(new MyActionListener());
}

通过内部类,可以不用新增一个 .java 文件,但显然我们还有更好的方法——匿名内部类。

2.3 通过匿名内部类实现

	addActionListener(new ActionListener() {	
		@Override
		public void actionPerformed(ActionEvent e) {
			// TODO Auto-generated method stub		
		}
	});

在 JDK1.8 之前,使用匿名内部类是一种惯用的做法,就算是相比于 lambda 表达式,匿名内部类也有它的优点——不局限于函数式接口,可以有多个抽象方法,例如:

	JButton button = new JButton();
	button.addMouseListener(new MouseListener() {
			
		@Override
		public void mouseReleased(MouseEvent e) {
			// TODO Auto-generated method stub
				
		}
			
		@Override
		public void mousePressed(MouseEvent e) {
			// TODO Auto-generated method stub
				
		}
			
		@Override
		public void mouseExited(MouseEvent e) {
			// TODO Auto-generated method stub
				
		}
			
		@Override
		public void mouseEntered(MouseEvent e) {
			// TODO Auto-generated method stub
				
		}
			
		@Override
		public void mouseClicked(MouseEvent e) {
			// TODO Auto-generated method stub
				
		}
	});

但对于函数式接口,在 JDK1.8 之后,利用 lambda 表达式实现无疑会更加方便。

2.4 通过lambda 表达式实现

	addActionListener(e -> {
		// TODO Auto-generated method stub
	});

3. lambda 表达式的语法

前面的例子已经很好的展示了 lambda 表达式的优点——简洁、优雅。接下来我们将介绍如何使用 lambda表达式。继续拿 ActionListener 接口为例:

public interface ActionListener {

    void actionPerformed(ActionEvent e);

}
	// e 对应 ActionListener 接口里抽象方法中的参数
	addActionListener(e -> {
		// 抽象方法的具体实现
	});

lambda 的基本结构是 (…)-> {…}。

(…)中的是接口中的抽象方法的参数,当没有参数时用,仍然要提供一个空括号();只有一个参数时可以不加括号,只给出参数,如上例所示;当有多个参数时用逗号分隔,如(int a, int b, …)。如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型,如(a, b)。

{…} 中是抽象方法的主体,可以在大括号内部具体实现此方法。对于 lambda 表达式而言,它与匿名内部类相似,是可以访问其所在作用域范围内的外围数据的。具体访问细节与匿名内部类相似,即只能引用值不会改变的(final)变量。例如下面的做法就是不合法的:

	// 在lambda表达式中被使用的变量,即使未被显示的定义为final,也会被视为是final的。
	int start = 0;
	ActionListener listener = e -> {
		start++; // Error: Can't mutate captured variable 
		System.out.println(start);
	};

另外如果在 lambda 表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如:

	for (int i = 1; i <= count; i++) {
		ActionListener listener = e -> {
			System.out.println(i);
			// Error: Local variable i defined in an enclosing scope must be final or effectively final
		};
	}

4. 方法引用和构造器引用

1.方法引用

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,希望只要出现一个定时器事件就打印这个事件,当然利用 lambda 表达式我们可以轻松的完成此功能:

	// Timer(int delay, ActionListener listener)
	Timer t = new Timer(1000, e -> System.out.println(event));

但是,如果直接把 println 方法传递到 Timer 构造器就更好了。利用方法引用可以直接传递一个已有的方法,例如:

	Timer t = new Timer(1000, Systei.out::println);

表达式 System.out::println 是一个方法引用(method reference )。
它等价于 lambda 表达式 x -> System.out.println(x)。
其中,:: 操作符分隔方法名与对象或类名,操作符前是对象或类名,操作符后是方法名。

2.构造器引用

构造器引用与方法引用很类似,只不过方法名为 new,例如,String::new 是 String 构造器的一个引用,具体引用哪一个构造器将取决于上下文。

可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数—— 即数组的长度。这等价于 lambda 表达式 x -> new int[x]。

Java 有一个限制,无法构造泛型类型 T 的数组(表达式 new T[n] 会产生错误,因为这会被虚拟机擦除替换为 new Object[n]),数组构造器引用对于克服这个限制很有用。

Stream 接口有一个 toArray方法可以返回 Object 数组:

	Object[] stringList = stream.toArray();

不过,这并不让人满意。这里我们希望得到一个 String 引用数组,而不是 Object 引用数组。流库利用构造器引用解决了这个问题。可以把 String[]::new 传入 toArray方法:

	String[] stringList = stream.toArray(String[]::new);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值