Java核心技术 卷1-总结-7
- lambda 表达式
- 方法引用
- 构造器引用
- 变量作用域
- 异常分类
- 声明受查异常
lambda 表达式
方法引用
有时, 可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如, 假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:
Timer t = new Timer(1000, event -> System.out.println(event));
但是,如果直接把 println 方法传递到Timer构造器就更好了。具体做法如下:
Timer t = new Timer(1000, System.out::println);
表达式 System.out::println
是一个方法引用(method reference),它等价于lambda 表达式x->System.out.println(x)
。
假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings, String::compareToIgnoreCase)
从这些例子可以看出,要用::
操作符分隔方法名与对象或类名。主要有3种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
在前2种情况中,方法引用等价于提供方法参数的lambda表达式。前面已经提到,System.out::printin
等价于x->System.out.println(x)
。类似地,Math::pow
等价于(x,y) -> Math.pow(x,y)
。
对于第3种情况、第1个参数会成为方法的目标。例如,
String::compareTolgnoreCase
等同于(x,y) -> x.compareTolgnoreCase(y)
如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。 例如,Math.max
方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于Math::max
转换为哪个函数式接口的方法参数。类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。
可以在方法引用中使用this参数。 例如,this::equals
等同于x->this.equals(x)
。使用super也是合法的。下面的方法表达式super::instanceMethod
使用this作为目标,会调用给定方法的超类版本。
构造器引用
构造器引用与方法引用很类似,只不过方法名为new。 例如,Person::new
是Person构造器的一个引用。构造器取决于上下文。
变量作用域
通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。考虑下面这个例子:
public static void repeatMessage(String text, int delay) {
ActionListener listener = event -> {
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay, listener).start();
}
调用:repeatMessage("Hello",1000);// Prints Hello every 1,000 milliseconds
。lambda表达式中的变量text并不是在这个lambda表达式中定义的。repeatMessage
方法的一个参数变量。lambda表达式的代码可能会在repeatMessage
调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text 变量呢?
要了解到底会发生什么,下面来巩固我们对lambda表达式的理解。lambda表达式有3 个部分:
- 一个代码块;
- 参数;
- 自由变量的值,这是指非参数而且不在代码中定义的变量。
在上述例子中,这个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"
。它被lambda表达式捕获(captured)。(具体的实现细节:例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)
lambda表达式可以捕获外围作用域中变量的值。 要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。 例如,下面的做法是不合法的:
public static void countDown(int start, int delay) {
ActionListener listener = event -> {
start--; // Error: Can't mutate captured variable
System.out.println(start);
};
new Timer(delay, listener).start();
}
之所以有这个限制是有原因的。如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。
另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。 例如,下面就是不合法的:
public static void repeat(String text,int count) {
for (int i= 1; i<= count; i++) {
ActionListener listener = event -> {
System.out.println(i + ":" + text);
// Error: Cannot refer to changing i
};
new Timer(1000, listener).start();
}
}
这里有一条规则:lambda表达式中捕获的变量必须实际上是最终变量(effectively final)。 实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text
总是指示同一个String对象,所以捕获这个变量是合法的。不过,i
的值会改变,因此不能捕获i
。lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
Path first = Paths.get("/usr/bin");
Comparator<String> comp =
(first, second)-> first.length() - second.length();
// Error: Variable first already defined
在方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。 例如,考虑下面的代码:
public class Application() {
public void init() {
ActionListener listener = event ->
{
System.out.println(this.toString ());
}
}
}
表达式this.toString()
会调用Application对象的toString
方法,而不是ActionListener实例的方法。在lambda表达式中,this
的使用并没有任何特殊之处。lambda表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this
的含义并没有变化。
异常分类
在 Java 程序设计语言中, 异常对象都是派生于 Throwable 类的一个实例。如果 Java 中内置的异常类不能够满足需求,用户可以创建自己的异常类。
所有的异常都是由Throwable 继承而来,但在下一层立即分解为两个分支:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象。
在设计Java程序时,需要关注Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。 划分两个分支的规则是:由程序错误导致的异常属于RuntimeException
;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
派生于RuntimeException
的异常包含下面几种情况:
- 错误的类型转换。
- 数组访问越界。
- 访问null指针。
不是派生于RuntimeException的异常包括:
- 试图在文件尾部后面读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException
异常;应该通过在使用变量之前检测是否为null
来杜绝NullPointerException
异常的发生。
Java语言规范将派生于Error
类或RuntimeException
类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。 编译器将核查是否为所有的受查异常提供了异常处理器。
声明受查异常
如果遇到了无法处理的情况,那么Java的方法可以抛出一个异常。一个方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。
方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类受查异常。
在自己编写方法时,不必将所有可能抛出的异常都进行声明。在遇到下面4种情况时应该抛出异常:
- 调用一个抛出受查异常的方法,例如,FileInputStream 构造器。
- 程序运行过程中发现错误,并且利用throw 语句抛出一个受查异常。
- 程序出现错误,例如,
a[-1] = 0
会抛出一个ArrayIndexOutOfBoundsException
这样的非受查异常。 - Java虚拟机和运行时库出现的内部错误。
如果出现前两种情况之一,则必须告诉调用这个方法的程序员有可能抛出异常。因为如果没有处理器捕获这个异常,当前执行的线程就会结束。
对于那些可能被他人使用的Java方法,应该根据异常规范(exception specification),在方法的首部声明这个方法可能抛出的异常。
class MyAnimation {
public Image loadImage(String s) throws IOException {
}
}
不应该声明从RuntimeException继承的那些非受查异常。
class MyAnimation {
void drawImage(int i) throws ArrayIndexOutOfBoundsException // bad style {
}
}
这些运行时错误完全在我们的控制之下。如果特别关注数组下标引发的错误,就应该将更多的时间花费在修正程序中的错误上,而不是说明这些错误发生的可能性上。
一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。 如果方法没有声明所有可能发生的受查异常,编译器就会发出一个错误消息。
除了声明异常之外,还可以捕获异常。这样会使异常不被抛到方法之外,也不需要throws规范。
如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常)。特别需要说明的是,如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。例如,FilelnputStream
构造器声明将有可能抛出一个IOExcetion
异常,然而并不知道具体是哪种IOException
异常。它既可能是IOException
异常,也可能是其子类的异常,例如,FileNotFoundException
。