目录
抽象类
当定义一个类时,常常需要定义一些成员方法描述类的行为特征,但有时这些方法的实现方式是无法确定的。例如,前面在定义Animal类时,shout()方法用于描述动物的叫声,但是针对不同的动物,叫声也是不同的,因此在shout()方法中无法准确描述动物的叫声。
针对上面描述的情况,Java提供了抽象方法来满足这种需求。抽象方法是使用abstract关键字修饰的成员方法,抽象方法在定义时不需要实现方法体。
抽象方法的定义格式:
修饰符 abstract void 方法名(参数列表);
在抽象类或接口中可以包含抽象方法,用于定义一种行为的规范,具体的实现由子类来完成。抽象方法没有方法体,只有方法的声明,使用分号(;)结束。
注意
使用抽象方法时需要注意以下几点:
- 抽象方法只能存在于抽象类或接口中,不能在普通类中定义。
- 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
- 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
- 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
- 使用abstract关键字修饰的抽象方法不能使用private 、 static、 final 以及 native 修饰,因为抽象方法必须被子类实现,如果使用了它们进行声明,则子类无法实现该方法。
- 在子类中重写抽象方法时,需要使用 @Override 注解来标识方法的重写。
- 如果子类没有完全实现所有的抽象方法,那么该子类仍然需要声明为抽象类。
下面是一个简单的示例,展示了抽象方法的使用:
// 定义一个抽象类 Shape
abstract class Shape {
// 定义一个抽象方法 calculateArea
public abstract double calculateArea();
}
// Circle 类继承自 Shape 抽象类
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
// 实现抽象方法 calculateArea,计算圆的面积
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
// Rectangle 类继承自 Shape 抽象类
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
// 实现抽象方法 calculateArea,计算矩形的面积
@Override
public double calculateArea() {
return width * height;
}
}
public class myclass {
public static void main(String[] args) {
// 创建一个 Circle 对象,并计算其面积
Circle circle = new Circle(5.0);
double circleArea = circle.calculateArea();
System.out.println("圆的面积:" + circleArea);
// 创建一个 Rectangle 对象,并计算其面积
Rectangle rectangle = new Rectangle(4.0, 6.0);
double rectangleArea = rectangle.calculateArea();
System.out.println("矩形的面积:" + rectangleArea);
}
}
在这个示例中,定义了一个抽象类 Shape,其中包含了一个抽象方法 calculateArea。抽象类不能被实例化,因此需要创建具体的子类来实现抽象方法。Circle 类和 Rectangle 类分别继承自抽象类 Shape,并重写了 calculateArea 方法以计算圆和矩形的面积。
在主函数中,我们创建了一个 Circle 对象和一个 Rectangle 对象,并分别调用它们的 calculateArea 方法来计算面积并输出结果。
运行结果:
圆的面积:78.53981633974483
矩形的面积:24.0
接口
如果一个抽象类的所有方法都是抽象的,则可以将这个类定义接口。接口是Java中最重要的概念之一,接口是一种特殊的类,由全局常量和公共的抽象方法组成,不能包含普通方法。
- 在JDK8之前,接口是由全局常量和抽象方法组成的,且接口中的抽象方法不允许有方法体。
- JDK 8对接口进行了重新定义,接口中除了抽象方法外,还可以有默认方法和静态方法(也叫类方法),默认方法使用default修饰,静态方法使用static修饰,且这两种方法都允许有方法体。更多内容可参考 Java 8 默认方法。
- JDK 9 以后,允许将方法定义为private,使得某些复用的代码不会把方法暴露出去。更多内容可参考 Java 9 私有接口方法。
接口的声明
接口使用interface关键字声明,语法格式如下:
import java.lang.*;
//引入包
public interface 接口名
{
//任何类型 final, static 字段
//抽象方法
}
接口有以下特性:
- 接口是隐式抽象的:在声明一个接口时,默认情况下它就被认为是抽象的,不需要使用关键字abstract来修饰。
- 接口中的方法是隐式抽象的:接口中定义的方法也默认被认为是抽象的,不需要使用abstract关键字来修饰。接口中的方法只有方法签名,没有方法体,具体的实现是由实现该接口的类提供。
- 接口中的方法是公有的:在接口中定义的方法都默认为public(公有)访问权限,可以被实现该接口的类公开访问和调用。
实例
interface Animal {
public void eat();
public void travel();
}
接口的使用
与抽象类一样,接口的使用必须通过子类,子类通过implements关键字实现接口,并且子类必须实现接口中的所有抽象方法。需要注意的是,一个类可以同时实现多个接口,多个接口之间需要使用英文逗号(,)分隔。
定义接口的实现类,语法格式如下:
修饰符 class 类名 implements 接口1,接口2,...{
...
}
实例
// 定义一个接口 Animal
interface Animal {
// 定义抽象方法
void eat();
void sleep();
}
// 实现 Animal 接口的 Cat 类
class Cat implements Animal {
// 实现接口中的抽象方法
@Override
public void eat() {
System.out.println("猫在吃东西");
}
@Override
public void sleep() {
System.out.println("猫在睡觉");
}
}
// 实现 Animal 接口的 Dog 类
class Dog implements Animal {
// 实现接口中的抽象方法
@Override
public void eat() {
System.out.println("狗在吃东西");
}
@Override
public void sleep() {
System.out.println("狗在睡觉");
}
}
public class Main {
public static void main(String[] args) {
Animal cat = new Cat();
cat.eat();
cat.sleep();
Animal dog = new Dog();
dog.eat();
dog.sleep();
}
}
以上实例编译运行结果如下:
猫在吃东西
猫在睡觉
狗在吃东西
狗在睡觉
重写接口中声明的方法时,需要注意以下规则:
- 类在实现接口的方法时,不能抛出比接口方法更宽泛的异常类型(强制性异常),只能在接口中声明抛出该异常或在继承接口的抽象类中声明抛出该异常。
- 类在重写接口方法时必须保持相同的方法签名,包括方法名、参数列表和返回值类型。
- 如果一个类是抽象类并且已经实现了某个接口的部分方法,那么它并不需要再次实现该接口的方法。
在实现接口的时候,也要注意一些规则:
- 一个类可以同时实现多个接口,使用逗号分隔。
- 一个类只能继承一个类(单继承),但可以实现多个接口。
- 接口之间也可以存在继承关系,一个接口可以继承自另一个接口,使用关键字 extends。这样子接口会继承父接口的方法声明。
接口的继承
一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。
public interface 子接口名 extends 父接口名 {
// 子接口的其他方法和常量声明
}
通过接口的继承,子接口可以拥有父接口的所有方法声明,并且在子接口中可以定义自己的方法和常量。需要注意的是,一个接口可以继承多个接口,使用逗号分隔。例如:
public interface 子接口名 extends 父接口1, 父接口2, ... {
// 子接口的其他方法和常量声明
}
这样子接口就可以同时继承多个父接口的方法声明。接口的继承可以帮助组织和扩展代码结构,提高代码的可复用性和可维护性。
以下是一个示例,演示了接口的继承:
//定义父接口A
interface InterfaceA {
void methodA(); // 父接口A的方法声明
}
//定义子接口B,继承自父接口A
interface InterfaceB extends InterfaceA {
void methodB(); // 子接口B的方法声明
}
//实现子接口B的类
class MyClass implements InterfaceB {
@Override
public void methodA() {
System.out.println("实现了父接口A的方法");
}
@Override
public void methodB() {
System.out.println("实现了子接口B的方法");
}
}
//测试代码
public class My {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.methodA(); // 调用父接口A的方法 ,输出结果:实现了父接口A的方法
obj.methodB(); // 调用子接口B的方法 ,输出结果:实现了子接口B的方法
}
}
如果在开发中一个子类既要实现接口又要继承抽象类,则可以按照以下格式定义子类。
修饰符 class 子类名 extends 父类名 implements 接口1, 接口2, ... {
// 类的成员和方法定义
}
在上述格式中,子类使用 extends 关键字继承指定的父类,使用 implements 关键字实现指定的接口。可以同时继承一个抽象类(父类)并实现多个接口。
注意:Java中只支持单继承,即一个类只能直接继承自一个父类。但可以通过实现多个接口来实现多重继承的效果,从而在一个类中具备多个接口的功能。
以下是一个示例代码,展示了一个子类继承抽象类并实现两个接口的情况:
abstract class AbstractClass {
abstract void methodA();
}
interface InterfaceA {
void methodB();
}
interface InterfaceB {
void methodC();
}
class Subclass extends AbstractClass implements InterfaceA, InterfaceB {
@Override
void methodA() {
System.out.println("实现了抽象类中的方法");
}
@Override
public void methodB() {
System.out.println("实现了接口A中的方法");
}
@Override
public void methodC() {
System.out.println("实现了接口B中的方法");
}
}
public class Main {
public static void main(String[] args) {
Subclass obj = new Subclass();
obj.methodA(); // 调用抽象类中的方法 ,输出结果:实现了抽象类中的方法
obj.methodB(); // 调用接口A中的方法 ,输出结果:实现了接口A中的方法
obj.methodC(); // 调用接口B中的方法 ,输出结果:实现了接口B中的方法
}
}
标记接口
最常用的继承接口是没有包含任何方法的接口。
标记接口是一种没有任何方法和属性的接口,它仅仅用于标记一个类属于特定的类型或具有某些特权。标记接口通过将类转变为接口类型来向类添加数据类型。
标记接口的作用主要可以归纳为以下两个方面:
- 建立一个公共的父接口:标记接口可以作为一个公共的父接口,被其他接口继承。例如,Java API中的
java.util.EventListener
接口就是一个标记接口,许多其他接口都间接继承自该接口。通过继承标记接口,可以实现一组接口的统一约束和规范。 - 向一个类添加数据类型:通过实现标记接口,类可以获得某个特定类型的标识,从而在程序中进行判断和处理。通过多态性,将实现了标记接口的类转换为接口类型,可以进行特定类型的操作或分配特定的权限。这样可以方便地对类进行分类、组织和处理。
常见的例子:
- java.io.Serializable:没有实现该接口的类无法进行对象的序列化和反序列化操作。为了保证在不同的Java编译器实现下一致性,序列化类必须声明一个明确的serialVersionUID值。
import java.io.Serializable; public class MyClass implements Serializable { // 类的实现... }
- java.lang.Cloneable:表明Object.clone()方法可以合法地对该类的实例进行按字段复制。实现了该接口的类应该使用公共方法重写Object.clone()(它是受保护的)。如果在未实现Cloneable接口的实例上调用Object的clone()方法,则会抛出CloneNotSupportedException异常。
public class MyClass implements Cloneable { @Override public Object clone() throws CloneNotSupportedException { // 对象的复制逻辑... } }
- java.util.RandomAccess:用于标识其支持快速(通常是固定时间)随机访问。通过实现该接口,一般算法能够改变其行为,从而在应用到随机或连续访问列表时提供更好的性能。
import java.util.ArrayList; import java.util.List; import java.util.RandomAccess; public class MyClass { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); if (list instanceof RandomAccess) { // 列表支持快速随机访问的处理逻辑... } } }
- java.rmi.Remote:Remote接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口,并且只有在扩展java.rmi.Remote的接口中指定的方法才可被远程使用。
import java.rmi.Remote; import java.rmi.RemoteException; public interface MyRemoteInterface extends Remote { void performRemoteAction() throws RemoteException; }
这些标识接口主要是为了在代码中进行类型查询和处理,以及在特定的运行时环境或容器中进行识别和处理。
需要注意的是,标记接口虽然没有定义任何方法,但通过实现该接口,类表明自己具有特定的特征或性质。标记接口常见的应用场景包括事件监听、序列化标识、权限标识等。
总之,标记接口是一种轻量级的行为标记机制,通过简单地实现接口来给类打上特定的标记,从而在程序中进行分类和处理。
什么时候使用抽象类和接口
-
使用抽象类:
- 当你有一组相关的类,它们之间存在共同的属性和方法,并且你希望这些方法中的一些具有默认实现时,可以使用抽象类。抽象类可以定义具体实现的方法和抽象方法,子类继承抽象类后可以直接使用具体实现的方法,并根据需要重写抽象方法。
- 抽象类适合用于基本功能相对稳定的情况,如果基本功能会频繁变化,则需要修改所有实现了该抽象类的子类。
-
使用接口:
- 当你想要实现多重继承或多个类之间需要遵守统一规范时,可以使用接口。接口定义了一组需要实现的方法,类可以通过实现接口来达到遵守规范的目的。
- 接口适合用于不同类之间需要共享行为但没有继承关系的情况,一个类可以实现多个接口,从而实现多重继承的效果。
- 如果基本功能需要频繁变化,使用接口会更加灵活,因为只需修改实现接口的类即可,无需修改其他类。
综上所述,选择抽象类还是接口要根据具体情况进行判断。如果有默认实现、基本功能相对稳定且不需要多重继承,可以选择抽象类。如果需要多重继承或多个类之间需要遵守统一规范,或者基本功能需要频繁变化,那么选择接口会更合适。