笔记:接口、lambda表达式与内部类
接口
基本概念
- 在Java程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
为了让类实现一个接口,通常需要两个步骤:
1、将类声明为实现给定的接口,声明需要使用implements关键字;
2、对接口中的所有方法进行定义。 - 在接口声明中,没有将方法声明为public,这是因为在接口中的所有方法都自动为Public,但是在实现接口的时候,必须对方法进行声明,否则编译器将认为这个方法的访问属性为包可见性,即类的默认访问属性。
- 以Comparable接口为例:Arrays类的sort方法可以对对象数组进行排序,但是对象所属的类必须实现Comparable接口(为向sort方法提供比较方式)。
Comparable接口的代码为:
public interface Comparable{
int compareTo(Object other);
}
//改进后:
public interface Comparable<T>{
int compareTo(T other);
}
import java.util.Arrays;
public class EmployeeSortTest {
public static void main(String[]args) {
Employee[] staff=new Employee[3];
staff[0]=new Employee("a1",70000);
staff[1]=new Employee("a2",78000);
staff[2]=new Employee("a3",69000);
Arrays.sort(staff);
for(Employee e:staff) {
System.out.println("name="+e.getName()+",salary="+e.getsalary());
}
}
}
public class Employee implements Comparable<Employee>{
private double salary;
private String name;
public Employee(String n,double s) {
name=n;
salary=s;
}
public String getName() {
return name;
}
public double getsalary() {
return salary;
}
public void raiseSalary(double byPercent) {
double raise=salary*byPercent/100;
salary+=raise;
}
//实现Comparable接口中的compareTo方法
@Override
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
/*
public int compareTo(Object otherObject){
Employee other=(Employee)otherObject;
return Double.compare(salary,other.salary);
*/
}
接口的特性
- 接口不是类,不能用new运算符实例化一个接口:
x=new Comparable(…);//error - 可以声明接口变量:
Comparable x; - 可以使用instance检查一个对象是否实现了某个接口:
if(object instanceof Comparable){…} - 接口可以被扩展:
public interface Moveable extends Powered{…} - 接口不能包含实例域或静态方法,但是可以定义常量:
public interface Powered{
double milesPerGallon();
double SPEED_LIMIT=95;
//a public static final constant,接口中的域自动设置为public static final
}
- 类只能有一个超类但是可以实现多个接口,使用逗号将各个接口分隔开:
class Employee implements Cloneable,Comparable;
其它特性
- Java不支持多重继承,因为多继承会让语言变得复杂,降低效率,使用抽象类会存在无法多次扩展的问题,接口的引入可以实现多重继承同时还能避免多重继承的复杂性和低效性;
- 接口允许有静态方法,通常将静态方法放在伴随类中;
- 默认方法:
1、可以为接口方法提供一个默认实现,必须用default修饰符标记这个方法:
public interface Comparable<T>{
default int compareTo(T other){return 0;
}
2、大多情况下,若只关心接口之中的某几个事件类型,可以将接口中的方法设置为默认方法,实现这个接口的程序员只需要为他们关心的事件覆盖监听器.
//默认方法可以调用其他方法
public interface Collection{
int size();
default boolean isEmpty(){
return size()==0;
}
}
3、默认方法的一个重要的用法是“接口演化”,如果为一个已有的接口增加了一个新方法,要将该方法设为默认,否则无法保证“源代码兼容”,如果使用该接口的类创建的实例来调用新增加的方法,可能会出现AbstractMethodError。
4、解决默认方法冲突,如果在一个接口中将一个方法定义为默认方法,然后在超类或是在其他接口中定义了同样的方法,规则如下:
超类优先:如果超类提供了一个具体方法,同名并且有相同的参数类型的默认方法会被忽略。
class Student extends Person implements Named{...}
//此时只会考虑超类方法,接口的所有默认方法将会被忽略,这也是所谓的类优先
接口冲突:如果一个超接口提供了一个默认方法,另一个接口提供了一个同名并且参数类型相同的方法,必须覆盖这个方法来解决冲突。
interface Named{
default String getName(){return getClass().getName()+"_"+hashCode();}
}
class Student implements Person,Named{
public String getName(){return Person.super.getName();}
//通常Java编译器会报告错误有程序员来解决这个二义性
}
接口实际应用
接口与回调
回调是一种常见的程序设计模式,在这种模式中,可以指出某个特定事件发生的时候应该采取的行动。
例如下例给出了定时器和操作器的行为,在定时器启动后,程序会弹出一个消息框,并等待用户点击OK来终止程序的执行,并且在程序等待用户操作的同时每隔10秒会显示一次当前的时间:
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
import javax.swing.JOptionPane;
import javax.swing.Timer;
public class TimerTest {
public static void main(String[] args) {
//构造一个ActionListener类的对象并将它传递给Timer构造器
ActionListener listener=new TimerPrinter();
//java.swing包中的一个Timer类,可以在到达给定时间间隔的时候发出通告
//第一个参数是时间间隔,第二个参数是监听对象
Timer t=new Timer(10000,listener);
//启动定时器
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
//定时器需要知道实现了哪个方法,并要求传递的对象所属的类实现了ActionListener接口
//定义一个实现了ActionListener接口的类,并将需要执行的语句放在actionPerformed方法中
class TimerPrinter implements ActionListener{
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is "+new Date());
Toolkit.getDefaultToolkit().beep();
}
}
Comparator接口
如果要对一个对象数组进行排序,前提是这些对象是实现了Comparable接口的类的实例,例如可以对一个字符串数组排序,因为String类实现了 Comparable接口,String.compareTo方法可以按字典顺序比较字符串。
假设我们希望按照长度递增的顺序比较字符串,就无法改写compareTo方法,为了处理这种情况,Arrays.sort方法还有另一个版本,例如:
public interface Comparator<T>{
int compare(T first,T second);
}
//定义一个实现Comparable<String>接口的类
class LengthComparator implements Comparable<String>{
public int compare(String first,String second){
return first.length()-second.length();
}
}
//具体实现:
Comparable<String> comp=new LengthComparator();
if(comp.compare(words[i],words[j])>0)...
//相比words[i].compareTo(words[j]),这个compare在比较器对象上调用,而不在字符串本身上调用
//要对数组排序,需要传入一个LengthComparator对象
String[] friends={"Peter","Paul","Mary"};
Arrays.sort(friends,new LengthComparator());
对象克隆
- 要想copy一个新的对象,它的初始状态与Original相同,但是之后拥有自己不同的状态,可以使用clone方法;
- 默认的克隆操作是“浅克隆”,并没有克隆对象中引用的其他对象,如果原对象和浅克隆对象是不可变的,这种共享就是安全的,如String,但是通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆所有子对象;
对于一个类如果默认的clone无法满足要求,需要在可变的子对象上调用clone来修补默认的clone方法,则需要:1、实现Cloneable接口;2、重新定义clone方法,并指定public访问修饰符; - Cloneable接口是Java提供的一组标记接口之一,它没有指定clone方法,这个方法是从Object类继承的,如果一个对象请求克隆,但是没有实现这个接口,就会生成一个受查异常;
即使clone的浅拷贝可以满足要求,还是需要实现Cloneable接口,例如:
class Employee implements Cloneable{
public Employee clone() throws CloneNotSuportedException{
return (Employee)super.clone();
}
}
深拷贝例子:
public class CloneTest {
public static void main(String[] args) {
try {
Employee original =new Employee("John Q. Public",50000);
original.setHireDay(2000, 1, 1);
Employee copy=original.clone();
copy.raiseSalary(10);
copy.setHireDay(2002, 12, 31);
System.out.println("original="+original.tostring());
System.out.println("copy="+copy.tostring());
}
//最好保留throws说明符,这样就允许子类在不支持克隆时选择抛出一个 CloneNotSupportedException异常
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
import java.util.Date;
import java.util.GregorianCalendar;
public class Employee implements Cloneable{
private String name;
private double salary;
private Date hireDay;
public Employee(String n,double s) {
name=n;
salary=s;
hireDay=new Date();
}
public Employee clone() throws CloneNotSupportedException{
Employee cloned=(Employee)super.clone();
cloned.hireDay=(Date)hireDay.clone();
return cloned;
}
public void setHireDay(int year,int month,int day) {
Date newHireDay=new GregorianCalendar(year,month-1,day).getTime();
hireDay.setTime(newHireDay.getTime());
}
public void raiseSalary(double byPercent) {
double raise=salary*byPercent/100;
salary=salary+raise;
}
public String tostring() {
return getClass().getName()+"[name="+name+",salary="+salary+",hireDay"+hireDay+"]";
}
}
lambda表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
lambda语法
- lambda表达式就是一个代码块,以及必须传入代码的变量规范,带参数变量的表达式称为lambda表达式。
- lambda表达式形式:
//1、参数,箭头以及一个表达式:
(String first,String second)->{
if(first.length()<second.length()) return -1;
else if(first.length()>second.length()) return 1;
else return 0;
}
//2、假如lambda表达式没有参数,仍要保留空括号:
()->{System.out.println("aaa");}
//如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型:
//下例将lambda表达式赋给了一个字符串比较器
Comparator<String> comp
=(first,second)->first.length()-second.length();
//如果方法只有一个参数,而且这个参数的类型可以推导出,甚至可以省略小括号:
ActionListener listener
=event->System.out.println("At the tone, the time is "+new Date());
//无需指定lambda表达式返回类型,lambda表达式的类型会由上下文推到而出
具体使用举例:
import java.util.Arrays;
import java.util.Date;
import javax.swing.JOptionPane;
import javax.swing.Timer;
public class LambdaTest {
public static void main(String[] args) {
String[] planets=new String[] {"Alice","Bob","Willen","Earth","Vans"};
System.out.println(Arrays.toString(planets));
System.out.println("Sorted by dictionary order:");
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
System.out.println("Sorted by length order:");
Arrays.sort(planets,(first,second)->first.length()-second.length());
System.out.println(Arrays.toString(planets));
Timer t=new Timer(10000,event->
System.out.println("The time is "+new Date()));
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
拓展内容
-
函数式接口:
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式,这种接口称为函数式接口。
例如:
Arrays.sort(planets,
(first,second)->first.length()-second.length());
//第二个参数需要一个Comparator对象,Comparator就是只有一个方法的接口。
Java API在java.util.function包中定义了很多非常通用的函数式接口。 -
方法引用:
1、有时已经有现成的方法可以完成你想要传递到其他代码的动作。例如:
Timer t=new Timer(10000,event->System.out.println(event);
但是如果可以将println方法直接传递给Timer构造器就更好了:
Timer t=new Timer(10000,System.out::println);
//System.out::println是一个方法引用,等价于x->System.out.println(x);
2、要使用::操作符分隔方法名与对象或类名,有以下三种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
前两种等价于提供方法参数的lambda表达式,后一种,第一个参数会成为方法目标。
3、可以在方法引用中使用this参数,例如:this::equals等同于x->this.equals(x);
使用super也是合法的,例如super::instanceMethod会调用指定方法的超类。 -
构造器引用:
构造器引用于方法引用类似,只是方法名为new,例如:
int[]::new等价于x->new int[x]; -
变量作用域:
1、lambda表达式有三个部分:一个代码块、参数和自由变量的值(非参数且不在代码中定义的值)
public static void repeatMessage(String text,int delay){
ActionListener listener=event->
{
System.out.println(text);
//text为自由变量。
//表示lambda表达式的数据结构必须存储自由变量的值
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}
//调用:
//这里可以说“hello”被lambda表达式捕获
repeatMessage("hello",1000);
lambda表达式可以捕获外围作用域的中变量的值。
2、在Java中,为了确保所捕获的值是明确定义的,要保证在lambda表达式中只能引用值不会改变的变量(如果在lambda表达式中改变变量,并发执行多个动作时就会不安全)。
public static void countDown(int start,int delay){
ActionListener listener=event->
{
start--;//error
System.out.println(start);
};
new Timer(delay,listener).start();
}
3、如果在lambda表达式中引用变量,而这个变量在外部改变,这也是不合法的;
lambda表达式捕获的变量必须是最终变量,指初始化之后就不会再赋值的变量。
public static void countDown(String text,int count){
for(int i=1;i<=count;i++){
ActionListener listener=event->
{
System.out.println(i+";"+text);//eror
};
new Timer(delay,listener).start();
}
}
4、在lambda表达式中声明一个与局部变量同名的参数或局部变量是不合法的;
5、在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
处理lambda表达式
使用lambda表达式的重点是延迟执行,之所以想以后再运行,原因:
在一个单独的线程中运行代码;
多次运行代码;
在算法合适的位置运行代码;
发生某种情况时执行代码;
只在必要时才运行代码。
内部类
内部类指定义在一个类中的类,使用内部类的原因:
- 内部类可以访问该类定义在作用域中的数据,包括私有的数据;
- 内部类可以对同一个包中的其他类隐藏起来;
- 当想定义一个回调函数且不想编写大量代码的时候。
内部类的使用
构建一个语音时钟为例:
import javax.swing.JOptionPane;
public class InnerClassTest {
public static void main(String[] args) {
TalkingClock clock=new TalkingClock(1000,true);
clock.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
import javax.swing.Timer;
public class TalkingClock {
private int interval;
private boolean beep;
public TalkingClock(int interval,boolean beep ) {
this.interval=interval;
this.beep=beep;
}
public void start() {
ActionListener listener =new TimePrinter();
Timer t=new Timer(interval,listener);
t.start();
}
//内部类,TimePrinter内并没有名为beep的变量,而是引用于TalkingClock类
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();
}
}
}
- 内部类既可访问自身的数据域,也可以访问创建它的外围类对象的数据类,内部类有一个隐式引用,它指向创建它的外围类对象,这个引用在内部类的定义中是不可见的,例如:
actionPerdormed方法等价于
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone,the time is "+new Date());
//将外围类对象引用称为outer
if(outer.beep) Toolkit.getDefaultToolkit().beep();
}
- 外围类的引用在构造器中设置,编译器修改了所有的内部类的构造器,添加了一个外围类引用的参数, outer 不是JAVA关键字。对于本例,TimePrinter未定义构造器,编译生成了一个默认的构造器:
public actionPerformed(TalkingClock clock)
outer=clock;
}
- 只有内部类可以是私有类,常规类只可以具有包可见性或共有可见性。
特殊语法规则
- 表达式 OuterClass.this 表示外围类引用,例如:
public void actionPerformed(ActionEvent event) {
...
if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
-
表达式 OuterObject.new InnerClass(construction parameters) 来编写内部类对象的构造器,例如:
ActionListener listener =this.new TimePrinter();
this限定词一般是多余的,但是可以通过显示地命名将外围类引用设置为其他对象,例如:如果TimePrinter是一个共有内部类,对于任意语音时钟都可以构造一个TimePrinter
TalkingClock clock=new TalkingClock(1000,true);
TalkingClock.TimePrinter listener=clock.new TimePrinter(); -
表达式 OuterClass.InnerClass 在外围类的作用域之外,可以引用内部类
-
内部类中声明的所有静态域都必须是final
-
内部类是一种编译器现象,与虚拟器无关,编译器会把内部类翻译成用$ (美元符)分隔外部类名与内部类名的常规类文件。例如:在TalkingClock类中的TimePrinter类会被翻译为类文件TalkingClock$TimePrinter.class
局部内部类
- 局部类不能用public或private访问说明符进行声明,它的作用域被限制在声明这个局部类的块里面,局部类可以对外部世界类完全隐藏起来。例如:
可以将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();
}
- 局部类不仅能够访问他们的外部类,还可以访问局部变量,局部变量必须事实上为final,例如:
public void start(int internal,boolean beep) {
//TimePrinter类在beep域释放之前将beep域用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();
}
编译器必须检测对局部变量的访问,为每一个变量建立相应的数据域,并将局部变量拷贝到构造器中,以便这些数据域初始化为局部变量的副本。注意局部类的方法只能引用定义为final的局部变量,因此使局部变量在局部类內建立的拷贝一致。