一. 接口
1. 引入
接口:用于描述类具有的功能,而不关心具体地实现。接口不是类,一个类可以实现一个或多个接口。接口中只允许声明方法和常量(不能定义域和方法的具体实现)。
如果一个类implements某个接口,就必须实现该接口中的所有方法。(抽象类允许不实现接口中的方法)
如Arrays.sort()可以对对象数组进行排序,但是要求对象所属类必须实现了Comparable接口,定义其中的CompareTo方法:
class Employee implements Comparable<Employee>{
protected String name;
private int age;
public int getAge() {
return age;
}
public Employee(String name, int age){
this.name = name;
this.age = age;
}
@Override
public int compareTo(Employee o) {
return Integer.compare(age, o.age);
}
}
public class MyTest {
public static void main(String[] args) {
Employee[] e = new Employee[3];
e[0] = new Employee("lili", 23);
e[1] = new Employee("haha", 12);
e[2] = new Employee("jaja", 15);
Arrays.sort(e);
for(Employee m: e) {
System.out.println(m.name + " " + m.getAge()); // 12 15 23
}
}
}
2. 接口的特性
- 接口不是类,所以不能new一个接口的对象,但是可以声明一个接口的变量,该变量用于引用实现了该接口的类的对象。
- 接口可以被扩展,同样使用extends关键字。
- 接口中可以声明常量,接口中的域被自动定义为public static final,方法被定义为public。
- 一个类可以实现多个接口,通过逗号分隔各个实现的接口。
注意:为什么有了抽象类还需要使用接口呢?
接口与抽象类有很大的差别,首先,抽象类是一个类,可以定义域、方法、构造器等,接口不行;其次,Java只允许单继承,但可以实现多个接口。接口可以满足多继承的几乎所有的好处,同时避免了多继承带来的复杂性和低效性。如C++中的多继承,会出现二义性问题。实现多个接口却不会出现这样的问题,个人认为是因为接口中的方法都只是声明,而未实现,需要类自身进行实现,因此不存在引用哪个“父类”中的方法的问题;而且接口中的域都是public static final,也是属于类的不可变的。可以说,是接口本身的特性使其能够避免因多继承带来的问题。
3. clone()方法
当拷贝一个变量时,原始变量与拷贝变量将引用同一个对象,对拷贝变量对象的改变会影响原始变量对象。为了使两者不相互影响,就需要使用clone().
clone是Object类中的一个方法,所以所有的类对象都可以使用clone方法进行克隆。但是,由于clone()是protected的,只有在当前类中才能调用该方法克隆自身对象(如对Employee对象的克隆必须在Employee类中调用才行)。这样使得clone()太受限,一般我们会重写clone方法,将其声明为public。
Object类中clone默认是浅拷贝,它并没有克隆包含在对象中的内部对象。如果一个类中的各个域都是基本类型或不可变对象(如String),clone将不会出现问题(基本类型存储的是真正的值,clone将直接复制一份值;不可变对象变量虽然存储的是对象的引用,但由于引用的对象是不可变的,因此clone一份引用也不会出错)。但如果类中包含可变对象,就必须重新定义clone方法,实现深拷贝。
- 重写clone()需要:
- 实现Cloneable接口;
- 将protected改为public。
需要注意的是,Cloneable接口实际上不包含任何方法,仅仅作为一个标记,重写的是Object类中的clone方法,但不实现这个接口会报错。还有,将protected改为public不会影响方法的签名,是重写而不是重新定义了一个方法。
class Employee implements Cloneable{
private String name;
private int age;
private Date hireDay;
public Employee(String name, int age){
this.name = name;
this.age = age;
this.hireDay = new Date();
}
public int getAge() {
return age;
}
public void setHireDay(int year, int month, int day) {
hireDay = new GregorianCalendar(year, month-1, day).getTime();
}
public Date getHireDay() {
return hireDay;
}
@Override
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
public class MyTest{
public static void main(String[] args) throws CloneNotSupportedException {
Employee e = new Employee("lili", 12);
Employee cloned = e.clone();
cloned.setHireDay(2018, 9, 1);
System.out.println("origin: " + e.getHireDay());
System.out.println("cloned: " + cloned.getHireDay());
//origin: Fri Nov 08 11:03:17 CST 2019
//cloned: Sat Sep 01 00:00:00 CST 2018
}
}
上面的例子举得不太好,setHireDay是让hireDay重新指向了一个新的对象,即使没有cloned.hireDay = (Date) hireDay.clone(); 打印的也是上述内容。
在Employee中定义的clone方法也会被子类Manager继承,如果Manager中的域都是基本类型,可以直接使用Employee中的clone方法克隆Manager对象,否则还需自己重新实现。
4. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
函数回调就是将函数指针的地址当作参数传递给另一个函数。
函数回调的用途简单来说就是进行事件的响应或者事件触发。
在C++中,确实是通过将函数名传递给一个函数进行回调,但在Java中是通过传递对象来实现。下面通过一个定时打印时间的例子来进行说明:
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println(now);
}
}
public class MyTest {
public static void main(String[] args) {
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000, listener); //第一个参数是时间间隔,第二个参数是监听的对象
t.start();
}
}
ActionListener接口中包含一个actionPerformed方法,当到达指定的时间间隔时,定时器将调用这个方法。
二. 内部类
内部类是定义在一个类中的类,使用内部类的原因:
- 内部类可以访问其所在外部类的所有域,包括私有域;
- 内部类可以对外进行隐藏;
- 当想定义一个回调函数而不想编写大量代码时,使用匿名内部类会很方便。
1. 通过内部类访问所在类的私有域
为实现访问所在类的私有域,内部类的对象会存在一个隐式引用,它指向了创建它的外部类对象。这个引用会自动传入内部类的构造器中进行设置。
class TalkingClock{
private boolean beep;
public TalkingClock(boolean beep) {
this.beep = beep;
}
public void startPrint() {
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000, listener);
t.start();
}
//该类对外不可见
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println(now);
if(beep) //可以访问beep,相当于TalkingClock.this.beep
System.out.println("beep!");
}
}
}
public class MyTest {
public static void main(String[] args) {
TalkingClock tc = new TalkingClock(true);
tc.startPrint();
}
}
2. 局部内部类
像上述例子所示,TimePrinter类仅在startPrint()方法中用到了,所以可以将内部类直接放在该方法中,成为局部内部类。
局部内部类不能使用public或private进行声明,它的作用域仅被限定在该方法中。局部类的优势在于它完全对外隐藏(内部类还可以通过OuterClass.InnerClass进行调用),注意局部变量必须声明为final。
class TalkingClock{
public void startPrint(final boolean beep) {
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println(now);
if(beep) System.out.println("beep!"); //在beep释放前进行了拷贝,所以能够访问该局部变量
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000, listener);
t.start();
}
}
public class MyTest {
public static void main(String[] args) {
TalkingClock tc = new TalkingClock();
tc.startPrint(true);
}
}
3. 匿名内部类
匿名内部类顾名思义就是没有名字的内部类,如果只需要使用这样一个类的对象,我们就可以使用匿名内部类来节省代码。如下所示,创建了一个实现ActionListener接口的类的新对象listener,类需要实现接口中的actionPerformed方法,这部分就放在{…}中定义。
类的构造器与类同名,匿名类没有名字,因此也没有构造器。
class TalkingClock{
public void startPrint(final boolean beep) {
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println(now);
if(beep) System.out.println("beep!");
}
};
Timer t = new Timer(1000, listener);
t.start();
}
}
public class MyTest2 {
public static void main(String[] args) {
TalkingClock tc = new TalkingClock();
tc.startPrint(true);
}
}
4. 静态内部类
如果一个内部类不需要引用所在外部类的域时,可以将该内部类声明为static(注意,只有内部类才能被定义为static)。静态内部类除了不会创建一个隐式引用外,其他与普通内部类没有任何差别。
使用场景:当一个方法需要返回两个值时,可以考虑返回对象,此时就可以通过创建一个静态内部类,将返回值定义为其私有域,再在方法中返回该类的对象。