1.接口概念
在Java中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
示例:Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了Comparable接口。下面是Comparable接口的代码。
public interface Comparable{ int compareTo(Object other); }
这就是说,任何实现Comparable接口的类都需要包含comparaTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值。
接口中还有一个没有明确说明的附加要求:在调用x.compareTo(y)的时候,当x小于y时,返回一个负数;当x等于y时,返回0;否则返回一个正数。
接口中的所有方法自动地属于public。因此,在接口中声明方法时,不必提供关键字public。
在接口中还可以定义常量。
接口绝不能含有实例域,在Java SE8之前也不能在接口中实现方法,现在已经可以在接口中提供简单的方法了。当然,这些方法不能引用实例域---接口没有实例。
注:1)在Java SE5.0中,Comparable接口已经改进为泛型类型。
2)在实现接口时,必须把方法声明为public;否则,编译器将认为这个方法的访问属性是包可见性,之后编译器就会给出试图提供更严格的访问权限的警告信息。
2.接口的特性
尽管不能构造接口的对象,却能声明接口的变量。接口变量必须引用实现了接口的类对象。
也可以使用instanceof检查一个对象是否实现了某个特点的接口:
if(anObject instanceof Comparable){...}
与可以建立类的继承关系一样,接口也可以被扩展。这里允许存在多条从具有较高通用性的接口到较高专用性的接口的链。
虽然在接口中不能包含实例域或静态方法(Java SE8中,允许在接口中增加静态方法),但却可以包含常量。
与接口的方法都自动地被设置为public一样,接口中的域将被自动设为public static final。
尽管每个类只能够拥有一个超类,但却可以实现多个接口。使用逗号将实现的各个接口分隔开。
3.接口与抽象类
使用抽象类表示通用属性存在这样一个问题:每个类只能扩展于一个类。但每个类可以是实现多个接口。
4.静态方法
在Java SE8中,允许在接口中增加静态方法。只是这有违于将接口作为抽象规范的初衷。
目前为止,通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如Collection/Collections或Path/Paths。
5.默认方法
可以为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。
public interface Comparable<T>{ default int compareTo(T other){return 0;} //By default, all elements are the same }
默认方法可以调用任何其他方法。例如,Collection接口可以定义一个便利方法:
public interface Collection{ int size();//An abstract method default boolean isEmpty(){ return size()==0; } ... }
默认方法的一个重要用法是接口演化。以Collection接口为例,这个接口作为Java的一部分已经有很多年了。假设很久以前你提供了这样一个类:public class Bag implements Collection,后来又为这个接口增加了一个stream方法。
将方法实现为一个默认方法能解决问题,Bag类能正常编译。
6.解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,会发生什么情况?Java的规则如下:
1)超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
2)接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
下面来看第二个规则。考虑另一个getName方法的接口:
interface Named{ default String getName(){ return getClass().getName()+"_"+hashCode(); } }
如果有一个类同时实现这两个接口会怎么样?
class Student implements Person,Named{...}
类会继承Person和Named接口提供的两个不一致的getName方法。并不是从中选择一个,Java编译器会报告一个错误,让程序员解决这个二义性。只需要在Student类中提供一个getName方法。可以选择两个冲突方法中的一个,如下
class Student implements Person,Named{ public String getName(){ return Person.super.getName(); } }
现在假设Named接口没有为getName提供默认实现:
interface Named{ String getName();}
Student类会从Person接口继承默认方法吗?不过,Java设计者更强调一致性。如果至少有一个接口提供了一个实现,编译器就会报告错误,而程序员就必须解决这个二义性。
另一种冲突情况,一个类扩展了一个超类,同时实现了一个接口,并从超类和继承了相同的方法。
在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。这正是“类优先”规则。如果为一个接口增加默认方法,这对于有这个默认方法之前能正常工作的代码不会有任何影响。
7.接口与回调
回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。
例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。
8.Comparator接口
我们已经了解如何对一个对象数组进行排序,前提是这些对象是实现了Comparable接口的实例。例如,可以对一个字符串数组排序,因为String类实现了Comparable<String>,而且String.compareTo方法可以按字典顺序比较字符串。
现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按照字典顺序进行排序。肯定不能让String类用两种不同的方式实现compareTo方法--更何况,String类也不轮到我们修改。
要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。
public interface Comparator<T>{ int compareTo(T first,T second); }
9.对象克隆
10.lambda表达式语法
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
考虑之前讨论的排序的例子。我们传入代码来检查一个字符串是否比另一个字符串短。这里要计算:
first.length()-second.length()
first和second是什么?它们都是字符串。Java是一种强类型语言,所以我们还要指定它们的类型:
(String first,String second)
-> first.length()-second.length()
这就是你看到的第一个lambda表达式。lambda表达式就是一个代码块,以及必须传入代码的变量规范。
Java中的一种lambda表达式形式:参数,箭头(->)以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}中,并包含显式的return语句。
(String first,String second) -> { if(first.length()<second.length()) return -1; else if(first.length()>second.length()) return 1; else return 0; }
即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样。
() ->{for(int i=100;i>=0;i--) System.out.print(i);}
如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。
Comparator<String> comp
=(first,second) ->first.length()-second.length();
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
ActionListener listener=event -> System.out.println("The time is " + new Date());
无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。
(String first,String second)
->first.length()-second.length()
可以在需要int类型结果的上下文中使用。
注:如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。
(int x) -> {if(x>=0) return 1;}//wrong
11.函数式接口
Java中已经有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口。
最好把lambda表达式看作是一个函数,而不是一个对象,另外接受lambda表达式可以传递到函数式接口。
Java API在java.util.function包中定义了许多非常通用的函数式接口。
想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。
12.方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如:
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)
从例子可以看出,要用::操作符分隔方法名与对象或类名。主要有三种方法:
objedct::instance Method
class::static Method
class::instance Method
在前两种情况中,方法引用等价于提供方法参数的lambda表达式。
对于第三种情况,第一个参数会成为方法的目标。例如:
String::compareToIgnoreCase//(x,y) ->x.compareToIgnoreCase(y)
可以在方法引用中使用this参数。例如
this::equals//x -> this.equals(x)
使用super也是合法的。
super::instance Method
使用了this作为目标,会调用给定方法的超类版本。
13.构造器引用
构造器引用与方法引用类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。哪一个构造器呢?这取决于上下文。假设你有一个字符串列表,可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器。
ArrayList<String> names=...; Stream<Person> stream=names.stream().map(Person::new); List<Person> people=stream.collect(Collectors.tolist());
重点是map方法会为各个列表元素调用Person(String)构造器。
可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x->new int[x]。
Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]。
例如,假设我们需要一个Person对象数组。stream接口有一个toArray方法可以返回Object数组:
Object[] people=stream.toArray();
不过这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]::new传入toArray方法:
Person[] people=stream.toArray(Person[]::new);
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
14.变量作用域
通常,你可能希望能够在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表达式有三个部分:
1)一个代码块;
2)参数;
3)自由变量的值,这是指非参数而且不在代码中定义的变量。
在我们的例子中,这个lambda表达式有一个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"。我们说它被lambda表达式捕获(captured)。(下面来看具体的实现细节。例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中)。
在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在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表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如:
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表达式中捕获的变量必须实际上是最终变量。实际上的最终变量是指,这个变量初始化之后就不会在为它赋新值。在这里,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表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如:
public class Application(){ public void init(){ ActionListener listener=event ->{ System.out.println(this.toString()); ... } ... } }
表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。
15.处理lambda表达式
下面来看如何编写方法处理lambda表达式。
使用lambda表达式的重点是延迟执行(deferred execution)。毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。之所以希望以后再执行代码,这里有很多原因,如:
在一个单独的线程中运行代码;
多次运行代码;
在算法的适当位置运行代码;
发生某种情况时执行代码;
只在必要时才运行代码。
来看一个简单的例子。假设你想要重复一个动作N次。将这个动作和重复次数传递到一个repeat方法:
repeat(10,()->System.out.println("Hello,World!"));
要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口。
Java API中提供的最重要函数式接口如下:
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
Runnable | 无 | void | run | 作为无参数或返回值的动作运行 | |
Supplier<T> | 无 | T | get | 提供一个T类型的值 | |
Consumer<T> | T | void | accept | 处理一个T类型的值 | andThen |
BiConsumer<T> | T,U | void | accept | 处理T和U类型的值 | andThen |
Function<T,R> | T | R | apply | 有一个T类型参数的函数 | compose,andThen,identity |
BiFunction<T,U,R> | T,U | R | apply | 有T和U类型参数的函数 | andThen |
UnaryOperator<T> | T | T | apply | 类型T上的一元操作符 | compose,andThen,identity |
BinaryOperator<T> | T,T | T | apply | 类型T上的二元操作符 | andThen,maxBy,minBy |
Predicate<T> | T | boolean | test | 布尔值函数 | and,or,negate,isEqual |
BiPredicate<T,U> | T,U | boolean | test | 有两个参数的布尔值函数 | and,or,negate |
在这里我们可以使用Runnable接口:
public static void repeat(int n,Runnable action){ for(int i=0;i<n;i++) action.run(); }
需要说明,调用action.run()时会执行这个lambda表达式的主体。
现在让这个例子更复杂一些。我们希望告诉这个动作出现在哪一个迭代中。为此,需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类型为void。处理int值的标准接口如下:
public interface IntConsumer { void accept(int value); }
下面给出repeat方法的改进版本:
public static void repeat(int n,IntConsumer action) { for(int i=0;i<N;i++) action.accept(i); }
可以如下调用它:
repeat(10,i->System.out.println("Countdown: "+(9-i)));
16.内部类
为什么需要使用内部类呢?其主要原因有以下三个点:
1)内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
2)内部类可以对同一个包中的其他类隐藏起来。
3)当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
下面将进一步分析TimerTest示例,并抽象出一个TalkingClock类。构造一个语音时钟时需要提供两个参数:发布通告的间隔和开关铃声的标志。
public class TalkingClock{ private int interval; private boolean beep; public TalkingClock(int interval,boolean beep){...} public void start(){...} public class TimePrinter implements ActionListener {...} }
需要注意,这里的TimePrinter类位于TalkingClock类内部。这并不意味着每个TalkingClock都有一个TimePrinter实例域。
下面是TimePrinter类的详细内容。需要注意的一点,actionPerforemed方法在发出铃声之前检查了beep标志。
public class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { System.out.println("At the tone,the time is "+new Date()); if(beep) Toolkit.getDefaultToolkit().beep(); } }
TimePrinter类没有实例域或者名为beep的变量,取而代之的是beep引用了创建TimerPrinter的TalkingClock对象的域。
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
为了能够运行这个程序,内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。
这个引用在内部类的定义是不可见的。然而,为了说明这个概念,我们将外围类对象的引用称为outer。于是actionPerformed方法将等价于下列形式:
public void actionPerformed(ActionEvent event){ System.out.println("At the tone,the time is "+new Date()); if(outer.beep) Toolkit.getDefaultToolkit().beep(); }
外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器,添加一个外围类引用的参数。因为TimePrinter类没有定义构造器,所以编译器为这个类生成了一个默认的构造器,其代码如下所示:
public TimePrinter(TalkingClock clock) { outer=clock; }
请在注意一下,outer不是Java的关键字。我们只是用它说明内部类的机制。
当在start方法中创建了TimePrinter对象后,编译器就会将this引用传递给当前的语音时钟的构造器:
ActionListener listener=new TimePrinter(this);
注:TimePrinter类声明为私有的。这样一来,只有TalkingClock的方法才能够构造TimePrinter对象。只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
事实上,使用外围类引用的正规语法还要复杂一些。
OuterClass.this
此表达式表示外围类。例如,可以像下面这样编写TimePrinter内部类的actionPerformed方法:
public void actionPerformed(ActionEvent event){ ... if(TalkingClock.this.beep){ Toolkit.getDefaultToolkit().beep(); } }
反过来,可以采用下列语法格式更加明确地编写内部对象的构造器:
outerObject.new InnerClass(construction parameters)
例如,
ActionListener listener=this.new TimePrinter();
在这里,最新构造的TimePrinter对象的外围类引用被设置为创建内部类对象的方法中的this引用。这是一种最常见的情况。通常,this限定词是多余的。不过,可以通过显式地命名将外围类引用设置为其他的对象。例如,如果TimePrinter是一个公有内部类,对于任意的语音时钟可以构造一个TimePrinter:
TalkingClock jabberer=new TalkingClock(1000,true); TalkingClock.TimePrinter listener=jabberer.new TimePrinter();
需要注意,在外围类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
注:1)内部类中声明的所有静态域都必须是final。原因很简单。我们希望一个静态域只有一个实例,不过对于每个外部对象,会分别有一个单独的内部类实例。如果这个域不是final,它可能就不是唯一的。
2)内部类不能有static方法,但只能访问外围类的静态域和方法。
内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用$(美元符号)分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。
例如,在TalkingClock类内部的TimePrinter类将被翻译成类文件TalkingClock$TimePrinter.class。
如果仔细地阅读一下TalkingClock示例的代码就会发现,TimePrinter这个类名字之在start方法中创建这个类型的对象时使用了一次。
当遇到这类情况,可以在一个方法中定义局部类:
public void start(){ class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event){ System.out.println("At the tone,the time is "+new Date()); if(beep) Toolkit.getDefaultToolkit().beep(); } } ActionListener listener=new TimePrinter(); Timer t=new Timer(interval,listener); t.start(); }
局部类不能用public或private访问说明符进行说明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。不过,那些局部变量必须事实上为final。这说明,它们一旦赋值就绝不会改变。
下面是一个典型的示例。这里,将TalkingClock构造器的参数interval和beep移至start方法中。
public void start(int interval,boolean beep){ class TimePrinter implements ActionListener{ public void actionPerformed(ActionEvent event){ System.out.println("At the tone,the time is "+new Date()); if(beep) Toolkit.getDefaultToolkit().beep(); } } ActionListener listener=new TimePrinter(); Timer t=new Timer(interval,listener); t.start(); }
请注意,TalkingClock类不再需要存储实例变量beep了,它只是引用start方法中的beep参数数量。
这看起来好像没什么值得大惊小怪的。程序行
if(beep) ...
毕竟在start方法内部,为什么不能访问beep变量的值呢?
为了能够清楚地看到内部的问题,让我们仔细地考查一下控制流程。
1)调用start方法。
2)调用内部类TimePrinter的构造器,以便初始化变量listener。
3)将listener引用传递给Timer构造器,定时器开始计时,start方法结束,此时,start方法的beep参数变量不复存在。
4)然后,actionPerformed方法执行if(beep)...。
为了能够让actionPerformed方法工作,TimerPrinter类在beep域释放之前将beep域用start方法的局部变量进行备份。实际也是这样做的。
匿名内部类
将局部内部类的使用再深入一步。假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类。
public void start(int interval,boolean beep){ ActionListener listener=new ActionListener(){ public void actionPerformed(ActionEvent event){ System.out.println("At the tone,the time is "+new Date()); if(beep) Toolkit.getDefaultToolkit().beep(); } }; Timer t=new Timer(interval,listener); t.start(); }
它的含义是:创建一个实现ActionListener接口的类的新对象,需要实现的方法actionPerformed定义在括号{}内。
通常的语法格式为:
new SuperType(construction parameters) { inner class methods and data }
其中,SuperType可以是ActionListener这样的接口,于是内部类就要实现这个接口。SuperType也可以是一个类,于是内部类就要扩展它。
由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给超类(superclass)构造器。尤其是在内部类实现接口的时候,不能有任何构造参数。不仅如此,还要像下面这样提供一组括号:
new InterfaceType() { methods and data }
请仔细研究一下,看看构造一个类的新对象与构造一个扩展了那个类的匿名内部类的对象之间有什么差别。
Person queen=new Person("Marry"); // a Person object Person count=new Person("Dracula"){...}; // an object of an inner class extending Person
如果构造参数的闭小括号后面跟一个开大括号,正在定义的就是匿名内部类。
多年来,Java程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。如今最好还是使用lambda表达式。例如,这一节前面给出的start方法用lambda表达式来写会简洁得多,如下所示:
public void start(int interval,boolean beep){ Timer t=new Timer(interval,event->{ System.out.println("At the tone,the time is "+new Date()); if(beep) Toolkit.getDefaultToolkit().beep(); }; t.start(); }
注:下面的技巧称为“双括号初始化”,这里利用了内部类的语法,假设你想构造一个数组列表,并将它传递到一个方法:
Array<String> friends=new ArrayList<>(); friends.add("Harry"); friends.add("Tony"); invite(friends);
如果不再需要这个数组列表,最好让它作为一个匿名列表。不过作为一个匿名列表,该如何为它添加元素呢?方法如下:
invite(new ArrayList<String>(){{ add("Harry);add("Tony");}});
注意这里的双括号。外层括号建立了ArrayList的一个匿名子类。内层括号则是一个对象的构造块。