一.概述
上一篇讲了反射技术,它可以在运行时分析类信息,也能分析对象信息,还能在运行时调用任意方法,功能确实很强大,同时也存在着很多的风险,所以在使用的时候一定要多加小心,能直接创建对象来调用方法,就不要使用反射技术。链接:Java反射技术
接下来讲解Java三大特性中多态使用场景最多,体现真正价值的接口技术。它主要用来描述类的能力,而并不给出每个功能的具体实现。
还需要讲解继承层次中抽象类的概念,它属于继承里的一部分以及和接口之间的关系。
二.抽象类
1.概念
抽象类从字面意思就是抽象的类,比如男人更抽象化就是人类。它更加变的通用,抽象类的目的就是要把更通用的方法和实例域抽离出来进行封装,这样抽象类能更加的提高代码的复用性,通过继承抽象类可以实现各自特有的方法实现。
2.使用和说明
像人类,动物类,这种抽象类,实例化一个对象对我们来说是没有任何意义的,所以抽象类规定是不可以进行实例化的。
/**
* 子类雇员类
*/
public class AbstractEmployee extends AbstractPerson {
private Double salary;
private LocalDate hireDay;
/**
* 必须要继承父类的构造器
*/
public AbstractEmployee(String name, Double salary, int year, int month, int day) {
super(name);
this.salary = salary;
hireDay = LocalDate.of(year,month,day);
}
/**
* 实现抽象方法
* @return
*/
@Override
public String getDescription() {
return String.format("an employee with a salary of $%.2f",salary);
}
}
/**
* 子类学生
*/
public class AbstractStudent extends AbstractPerson{
private String majoy;
public AbstractStudent(String name, String majoy) {
super(name);
this.majoy = majoy;
}
/**
* 实现抽象方法
* @return
*/
@Override
public String getDescription() {
return "a student majoring in " + majoy;
}
}
/**
* 抽象测试
*/
public class AbstractDemo {
public static void main(String[] args) {
// 通过继承我们知道可以存放子类对象
AbstractPerson[] people = new AbstractPerson[2];
people[0] = new AbstractEmployee("Harry",5000.0,1998,11,1);
people[1] = new AbstractStudent("Maria","computer");
for (AbstractPerson person : people) {
// 调用各自类实现的抽象方法(多态)
System.out.println(person.getName() + "," + person.getDescription());
}
}
}
总结:
- 抽象类可以没有抽象方法,只是说当前类只能由别的类来继承,如果不覆盖抽象类的方法,则默认使用抽象类的方法,否则自行在子类中进行覆盖
- 存在抽象方法必须是抽象类,因为类中不允许存在没有实现的方法,所以必须要将类声明成抽象类
- 继承抽象类,可以不实现抽象方法,那么子类也要成为抽象类。 子类必须实现继承的抽象类中的抽象方法。
抽象类和继承并没有很大的区别,只是变的更加抽象,让代码更加具有复用性,但是能将类封装成一个好的抽象类还是需要技术功底的。
三.接口
1.接口概念
接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
实例:生活中常见的接口就是usb接口,或者是手机充电接口。表明如果我们想要使用某种充电接口,就要使用对应接口的充电器,否则不能使用。
接口中所有的方法默认是public,所以在声明接口方法的时候不需要使用权限修饰符。在接口中也可以声明常量,并且只能声明常量,所以接口中声明实例域的时候默认是public static final的修饰符,可以省略。可以理解为没有实例域的抽象类。
/**
* 接口声明
*/
public interface InterfaceDemo {
/**
* 接口中声明的实例域默认全部是public static final,所以在定义的时候可以省略
*/
public static final int NUM = 1;
/**
* 推荐使用
*/
int NUM1 = 2;
/**
* 声明接口方法,默认是public的修饰符,可以省略
* @return
*/
public String getString();
/**
* 推荐使用
* @return
*/
String getString1();
}
2.实现一个接口
/**
* 测试实现接口
* implements 实现接口关键字
* <>表示范型 后面会讲到 暂时理解为固定类型
*/
public class Employee implements Comparable<Employee> {
private String name;
private Double salary;
public Employee(String name, Double salary) {
this.name = name;
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
'}';
}
/**
*
* @param o
* @return
*/
@Override
public int compareTo(Employee o) {
// 调用Double的静态方法进行比较
return Double.compare(salary,o.salary);
}
public static void main(String[] args) {
Employee[] employee = new Employee[3];
employee[0] = new Employee("Carl",1997.0);
employee[1] = new Employee("Harry",2222.0);
employee[2] = new Employee("Tony",3333.0);
Arrays.sort(employee);
for (Employee employee1 : employee) {
System.out.println(employee1);
}
}
}
这里会有一个疑问?如果不实现接口在类中直接声明方法可以么?这个当然可以,但是例如本例中的compareTo方法,如果不写,我们可以自己进行比较,不能使用Arrays.sort()方法,因为该方法使用了Comparable接口的compareTo方法,所以虚拟机在运行时会检查该方法是否存在,不存在将会抛出异常。所以接口就好比一种约定,如果你想使用我的方法,那么就必须实现特定的接口方法,否则将会抛出异常。并且如果不使用接口,那就要在每个类中实现一个比较方法,在创建对象的时候调用来进行比较,没有达到代码的复用性。
3.接口特性
- 接口不是类,不能使用new操作符创建对象。
- 可以声明接口变量,并且引用实现该接口的对象。
- 可以使用instanceof检查一个对象是否实现了某个特定的接口。
- 接口可以进行扩展并且可以实现多扩展,也就是多继承。
- 实现多接口时使用逗号将接口隔开
四.接口与抽象类
区别:
- 接口不是类,抽象类是类
- 类只能单继承,继承一个抽象类,但是可以实现多个接口。所以接口更加灵活。
- 接口不存在实例,抽象类可以存在实例。
- 接口仅仅表示一个能力,抽象类是使具体事物更加抽象化。
五.Java8接口新特性
1.静态方法和默认方法
在Java8出来之前在定义一个接口后,需要给出接口的默认实现,会为声明的接口提供一个实现类,并将接口中想要实现的默认方法以静态方法的形式实现,提供调用,例如:Collection/Collections。现在就可以不用提供这样的实现类了,Java8支持接口方法的默认实现。
/**
* 接口的默认实现
*/
public interface InterfaceDefaultDemo {
/**
* 声明方法 并提供默认实现
* default 提供默认实现的关键字
* @return
*/
default String getString(){
return "a";
}
}
接口默认实现的好处:
- 我们可以不关心默认实现的方法,只关心我们需要覆盖的方法,因为我们不用在实现接口的默认方法。
- 接口演化:当需要为老接口增加新的方法时,如果不提供新增加方法的默认实现,那么实现老接口的类就会报错,因为必须要实现接口的方法。及时不重新编译原来的类,如果对实现该接口的类使用新增加的方法就会抛出异常。
2.解决默认方法的冲突
情况:如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法。
规则:
(1)超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
(2)接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)形同的方法,必须覆盖这个方法来解决冲突。
解决:demo实例
/**
* 接口A
*/
public interface InterfaceA {
default String getString(){
return "a";
}
}
/**
* 接口B
*/
public interface InterfaceB {
default String getString(){
return "b";
}
}
/**
* 接口c
*/
public interface InterfaceC {
/**
* 不提供默认实现但但是方法相同
* @return
*/
String getString();
}
/**
* 作为父类
*/
public class ClassD {
public String getString(){
return "d";
}
}
/**
* 定一个抽象类 类E
*/
public abstract class ClassE {
public abstract String getString();
}
四种情况:
/**
* 测试实现两个有同样默认方法的接口A,B
*/
public class ClassTestAB implements InterfaceA,InterfaceB{
/**
* 必须实现方法,解决二义性
* @return
*/
@Override
public String getString() {
// 使用接口A的默认实现来解决
return InterfaceA.super.getString();
}
}
/**
* 测试实现两个同名接口但是一个是默认方法,一个不是默认方法
*/
public class ClassTestAC implements InterfaceA,InterfaceC{
/**
* 编译器不知道是否采用默认方法还是要必须实现该方法,所以需要解决二义性
* @return
*/
@Override
public String getString() {
return null;
}
}
/**
* 测试继承一个父类和实现一个接口
*/
public class ClassTestAD extends ClassD implements InterfaceA{
public static void main(String[] args) {
ClassTestAD a = new ClassTestAD();
System.out.println(a.getString());
}
}
/**
* 测试继承一个抽象类和实现一个接口
*/
public class ClassTestAE extends ClassE implements InterfaceA{
/**
* 类优先原则
* @return
*/
@Override
public String getString() {
return "AE";
}
public static void main(String[] args) {
ClassTestAE s = new ClassTestAE();
System.out.println(s.getString());
}
}
类优先的规则可以确保Java7的兼容性。如果为一个接口增加默认方法,这对于有这个默认方法之前能正常工作的代码不会有任何影响。
六.接口实例
1.接口与回调
回调是一种常见的程序设计模式,可以指出某个特定事件发生时应该采取的动作。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
/**
* 接口与回调
*/
public class TimerDemo {
public static void main(String[] args) {
ActionListener listener = new TimerPrinter();
// 每隔10秒中调用一次接口方法然后响一声。
Timer t = new Timer(10000,listener);
// 启动定时服务
t.start();
// 对话框点击确定后退出程序
JOptionPane.showMessageDialog(null,"Quit program?");
System.exit(0);
}
}
/**
* 定时器类
*/
class TimerPrinter implements ActionListener{
/**
* 当定时器到时间后所调用的方法
* @param e
*/
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + new Date());
// 发出系统铃响
Toolkit.getDefaultToolkit().beep();
}
}
2.对象克隆
默认的克隆操作是浅拷贝,并没有克隆对象中引用的其他对象。如果愿对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。但是通常类中的子对象引用都是我们自定义的可变对象,所以需要实现深拷贝。
import java.util.Date;
import java.util.GregorianCalendar;
/**
* 克隆接口
* 这个接口是个标记接口,目的只是告诉此类可以进行克隆,如果没有这个标记则会抛出异常
* 它唯一的作用是允许在类型查询中使用instanceof
*/
public class Employee1 implements Cloneable{
private String name;
private Double salary;
private Date hireDay;
public static void main(String[] args) {
// 处理异常,后面会讲
try {
Employee1 original = new Employee1("John",400.0);
original.setHireDay(2000,1,1);
// 进行深拷贝
Employee1 copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2001,1,2);
System.out.println("original = " + original);
System.out.println("copy = " + copy);
}catch (CloneNotSupportedException e) {
e.getStackTrace();
}
}
public Employee1(String name, Double salary) {
this.name = name;
this.salary = salary;
}
/**
* 覆盖了Object中的clone()方法
* @return
* @throws CloneNotSupportedException
*/
public Employee1 clone() throws CloneNotSupportedException {
// 调用父类的克隆方法
Employee1 cloned = (Employee1)super.clone();
// 使用date类的克隆方法
cloned.hireDay = (Date)hireDay.clone();
return cloned;
}
public void setHireDay(int year, int month, int day) {
Date newHirDay = new GregorianCalendar(year,month - 1,day).getTime();
hireDay.setTime(newHirDay.getTime());
}
public void raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
}
注意:
- 默认的clone方法是否满足需求。方法(1)
- 是否可以在可变的自对象上调用clone来修补默认的clone方法。方法(2)
- 是否不该使用clone。
解决方法:
(1)实现Cloneable接口。
(2)重新定义clone方法,并指定public访问修饰符。
建议在自己的程序中不要使用标记接口,即使clone是默认实现能够满足, 还是需要实现Cloneable接口,将clone接口定义为public。
public class Employee1 implements Cloneable{
public Employee1 clone() throws CloneNotSupportedException {
// 调用父类的克隆方法
return (Employee1)super.clone();
}
七.总结:
接口技术也是后面使用最多的技术,封装,继承,多态。这三个特性的基本使用方法,特性,区别暂时就先讲这么多,当然还有更深层次的地方,以后在高级应用,或者对某些点有更深的理解时在做补充。前面这些部分基础很重要。希望大家多理解理解,消化一下。多敲代码。
下一篇讲lambda表达式的基本使用,这是Java8特性中改变最大的地方后面还有一个流操作,这两个技术让我们编写的效率大幅度提高。
有些可能我理解的不够深刻,大家如果觉得我说的不够详细可以参考我的推荐书,详细的看一下。欢迎大家评论。第一时间我会回复大家。谢谢!