> 文章列表 > Java核心技术 卷1-总结-7

Java核心技术 卷1-总结-7

Java核心技术 卷1-总结-7

Java核心技术 卷1-总结-7

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 中内置的异常类不能够满足需求,用户可以创建自己的异常类。
Java核心技术 卷1-总结-7
所有的异常都是由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