Java核心技术 卷1-总结-6
- 接口示例
- 接口与回调
- Comparator接口
- 对象克隆
- lambda表达式
- 为什么引入lambda表达式
- lambda表达式的语法
接口示例
接口与回调
回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。 例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。
在java.swing包中有一个Timer类,可以使用它在到达给定的时间间隔时发出通告。例如,假如程序中有一个时钟,就可以请求每秒钟获得一个通告,以便更新时钟的表盘。
在构造定时器时,需要设置一个时间间隔,并告之定时器,当到达时间间隔时需要做些什么操作。 在Java标准类库中的类采用的是面向对象方法。它将某个类的对象传递给定时器,然后,定时器调用这个对象的方法。由于对象可以携带一些附加的信息,所以传递一个对象比传递一个函数要灵活得多。
当然,定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt. event包的ActionListener
接口。下面是这个接口:
public interface ActionListener {
void actionPerformed(ActionEvent event);
}
当到达指定的时间间隔时,定时器就调用actionPerformed
方法。假设希望每隔10秒钟打印一条信息"At the tone,the time is…",然后响一声,就应该定义一个实现 ActionListener
接口的类,然后将需要执行的语句放在actionPerformed
方法中。
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone,the time is"+ new Date());
Toolkit.getDefaultToolkit().beep();
}
}
actionPerformed
方法的ActionEvent
参数提供了事件的相关信息,例如,产生这个事件的源对象。
接下来,构造这个类的一个对象,并将它传递给Timer构造器。
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);
Timer构造器的第一个参数是发出通告的时间间隔,它的单位是毫秒。这里希望每隔10 秒钟通告一次。第二个参数是监听器对象。最后,启动定时器:
t.start();
在定时器启动以后,程序将弹出一个消息对话框,并等待用户点击Ok按钮来终止程序的执行。在程序等待用户操作的同时,每隔10秒显示一次当前的时间。
这个程序除了导入javax.swing.*
和java.util.*
外,还通过类名导入了javax. swing.Timer
。这就消除了javax.swing.Timer
与java.util.Timer
之间产生的二义性。
Comparator接口
实现了Comparable接口的类的实例可以对一个对象数组进行排序。因为String类实现了Comparable,而且String.compareTo方法可以按字典顺序比较字符串。
现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。肯定不能让String类用两种不同的方式实现compareTo方法,要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。
public interface Comparator<T> {
int compare(T first, T second);
}
要按长度比较字符串,可以如下定义一个实现Comparator<String>
的类:
class LengthComparator implements Comparator<String> {
public int compare(String first, String second) {
return first.length() - second.length();
}
}
具体完成比较时,需要建立一个实例:
Comparator<String> comp = new LengthComparator();
if(comp.compare(words[i],words[j])>0) { ... }
将这个调用与words[i].compareTo(words[j])
做比较。这个compare方法要在比较器对象上调用,而不是在字符串本身上调用。
注释:尽管LengthComparator对象没有状态,不过还是需要建立这个对象的一个实例。 我们需要这个实例来调用compare方法——它不是一个静态方法。
要对一个数组排序,需要为Arrays.sort
方法传入一个LengthComparator对象:
String [] friends = { "Peter", "Paul", "Mary" };
Arrays.sort(friends, new LengthComparator());
现在这个数组可能是["Paul","Mary","Peter"]
或["Mary","Paul","Peter"]
。
对象克隆
Cloneable接口,这个接口指示一个类提供了一个安全的clone
方法。
为一个包含对象引用的变量建立副本时原变量和副本都是同一个对象的引用。
这说明,任何一个变量改变都会影响另一个变量。
Employee original = new Employee("John Public",5000);
Employee copy = original;
copy.raiseSalary(10);// oops--also changed original
如果希望copy是一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用clone方法。
Employee copy = original.clone();
copy.raiseSalary(10);// OK--original unchanged
clone
方法是Object的一个protected
方法,只有Employee类可以克隆Employee对象。Object类对于Employee对象一无所知,所以只能逐个域地进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题。但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。
使用Object类的clone方法克隆一个Employee对象默认的克隆操作是"浅拷贝",并没有克隆对象中引用的其他对象。
浅拷贝会有什么影响吗?这要看具体情况。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String,或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。
不过,通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆所有子对象。在这个例子中,hireDay域是一个Date,这是可变的,所以它也需要克隆。
对于每一个类,需要确定:
- 默认的clone方法是否满足要求;
- 是否可以在可变的子对象上调用clone来修补默认的clone方法;
- 是否不该使用clone。
实际上第3个选项是默认选项。如果选择第1项或第2项,类必须:
- 实现Cloneable接口;
- 重新定义clone方法,并指定 public 访问修饰符。
必须当心子类的克隆。例如,一旦为Employee类定义了clone方法,任何人都可以用它来克隆Manager对象。Employee克隆方法能完成工作吗?这取决于Manager类的域。在这里是没有问题的,因为 bonus 域是基本类型。但是Manager 可能会有需要深拷贝或不可克隆的域。不能保证子类的实现者一定会修正clone方法让它正常工作。出于这个原因,在Object 类中clone方法声明为protected。
要不要在自己的类中实现clone呢?克隆没有你想象中那么常用。标准库中只有不到5%的类实现了clone。如果你的客户需要建立深拷贝,可能就需要实现这个方法。
lambda表达式
为什么引入lambda表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。在接口与回调中,已经了解了如何按指定时间间隔完成工作。将这个工作放在一个ActionListener的actionPerformed
方法中:
class worker implements ActionListener {
public void actionPerformed(ActionEvent event) {
// do some work
}
}
想要反复执行这个代码时,可以构造Worker类的一个实例。然后把这个实例提交到一个Timer对象。或者可以考虑如何用一个定制比较器完成排序。如果想按长度而不是默认的字典顺序对字符串排序,可以向sort
方法传入一个Comparator对象:
class LengthComparator implements Comparator<String> {
public int compare(String first, String second) {
return first.length() - second.length);
}
}
Arrays.sort(strings, new LengthComparator();
compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的顺序不正确就会重新排列元素。将比较元素所需的代码段放在sort方法中,这个代码将与其余的排序逻辑集成。
这两个例子有一些共同点,都是将一个代码块传递到某个对象(一个定时器,或者一个sort方法)。 这个代码块会在将来某个时间调用。
到目前为止,在Java中传递一个代码段并不容易,不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
lambda表达式的语法
再来考虑排序的例子。我们传入代码来检查一个字符串是否比另一个字符串短。这里要计算:
first.length() - second.length()
first和second是什么?它们都是字符串。Java是一种强类型语言,所以我们还要指定它们的类型:
(String first,String second)-> first.length()-second.length()
Java中的一种lambda表达式形式:参数,箭头(->)以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{ }中,并包含显式的return语句。例如:
(String first,String second)-> {
if (first.length()< second.lengthO) return -1;
else if(first.length()>second.length))return 1;
else return 0;
}
即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样:
()-> { for(int i=100;i >= 0;i--) System.out.println(i); }
如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。例如:
Comparator<String>comp = (first,second)// Same as(String first,String second)
-> first.length()- second.length();
在这里,编译器可以推导出first
和second
必然是字符串,因为这个lambda表达式将赋给一个字符串比较器。
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
ActionListener listener = event ->
System.out.println("The time is " + new Date()");
// Instead of (event)->....or(ActionEvent event)->...
无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。 例如,下面的表达式
(String first, String second)-> first.length() - second.length()
可以在需要int类型结果的上下文中使用。
注意∶如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,
这是不合法的。例如:
(int x)-> { if(x>=0)return 1; }
就不合法。