抽象类
1.什么是抽象类?
抽象类(Abstract Class),它是一种无法直接创建实例的类,主要用于定义一组子类必须遵循的规范,同时允许子类根据具体需求实现差异化的功能。
想象你手里有一个 “图形模板” 叫 Shape(图形类)。这个模板上写着:“所有照着我产生的图形(比如圆形、三角形),都得会‘画’自己。” 但问题是,Shape 本身不是一个具体的图形(比如它既不是圆,也不是三角形),它没办法告诉你 “怎么画”,只能规定这个 “必须会画” 的任务。这种 自身不具体、只给子类定规则 的类,就是抽象类。就好比你拿到一个 “家具模板”,模板上写着 “所有家具都得有‘使用方法’”,但模板本身不是沙发、不是桌子,没法告诉你具体怎么用,只能让沙发、桌子自己去定。
在这个 “图形模板”(Shape)里,有个 “画” 的任务 叫 draw () 方法。但 Shape 不是具体图形啊,它不知道怎么画一个具体的图形,所以这个 draw () 方法 只有名字,没有具体步骤(就像只告诉你 “要吃饭”,但不告诉你 “怎么吃、吃什么”)。这种 只定义任务名称、不写具体实现 的方法,就是抽象方法。
圆形类、三角形类这些子类,就像拿到模板后 “加工具体图形” 的工厂。它们必须完成 Shape 模板里的 draw () 任务,每个子类都按自己的样子实现 draw (),但这个任务最初是抽象类 Shape 规定的。这就好比不同工厂按 “家具模板” 生产沙发、桌子,沙发会实现 “坐的方法”,桌子会实现 “放东西的方法”,但任务源头是模板定的。
抽象类其实和普通类很像,但他不能直接new一个对象。对于抽象方法,它只能定义而没有实现,它的实现只能由子类提供,一个包含抽象方法的类必须声明成抽象类。
简单说,抽象类就是一个 “强制子类遵守规则的模板”,它自己不完整(没法直接用),但能让子类既统一又灵活 —— 统一在必须实现某些功能,灵活在每个子类可以用不同的方式实现这些功能。
2.抽象类有哪些特性?
- (1)抽象类不可以被实例化。
- (2)抽象类可以包含抽象方法,可以没有具体的实现,这些方法将在具体的 子类中实现。
- (3)抽象方法不能被private和static修饰。
- (4)抽象方法不能被final和static修饰,因为抽象方法要被子类重写。
- (5)抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用abstract 修饰。
- (6)抽象类中不一定包含抽象方法,但有抽象方法的类一定是抽象类。
- (7)抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量。
- (8)当一个抽象类A不想被一个普通类B继承,此时可以把B这个类变成抽象类,那么再当一个普通类C继承这个抽象类B后,C要重写B和A里面的所有抽象方法。
3.抽象类的作用是什么?
抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。有些同学可能会说了,普通的类也可以被继承呀,普通的方法也可以被重写呀,为啥非得用抽象类和抽象方法呢?
确实如此。但是使用抽象类相当于多了一重编译器的校验。举个具体例子:假设我们设计一个 “图形绘制” 功能,用普通类实现时:
class CommonGraph {
public void draw() {
// 若父类本不该实现具体绘制,却写了不完整逻辑
System.out.println("普通图形绘制");
}
}
class Circle extends CommonGraph {
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
若不小心直接用父类:
CommonGraph graph = new CommonGraph();
graph.draw(); // 编译器不报错,但违背“绘制应由子类实现”的逻辑。
而用抽象类时,正常情况下应该是:
abstract class AbstractGraph {
public abstract void draw(); // 抽象方法,无实现
}
class Rectangle extends AbstractGraph {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
假如我们不小心创建了对象,则会报错:
AbstractGraph ag = new AbstractGraph(); // 编译器直接报错,禁止实例化抽象类,及时发现问题。
使用抽象类的场景,本就该由子类完成具体工作。若用普通类,编译器不会检查这种逻辑错误;但抽象类会在实例化时提示问题。
很多语法的意义在于 “预防出错”,就像 final
。变量若不修改,本可当常量,但加 final
后,若不小心误修改,编译器会提醒。抽象类也如此,它利用编译器校验,让我们在编码阶段发现潜在逻辑错误,而非等到运行时。这种强制校验确保子类遵循规则、实现特定方法,保障继承体系的功能一致性和正确性,大大提高代码的可靠性与可维护性。
4.抽象方法和普通方法的区别?
抽象方法:
只要方法被 final
、private
、static
、native
修饰,或者方法包含方法体(有具体实现),或者所在的类不是抽象类 / 接口,就 不能定义成抽象方法。
普通方法:
只要方法有方法体,且所在类符合语法规则(普通类、抽象类、支持默认方法的接口),就是普通方法,它不强制子类重写(除非在抽象类中被重写,但抽象类中的普通方法本身有实现,子类可选择是否覆盖)。
特征 | 普通方法 | 抽象方法 |
---|---|---|
方法体 | 必须有 {} 和具体实现 | 不能有 {} ,仅有声明(; 结尾) |
所在类 | 普通类、抽象类、接口(Java 8+ 支持接口默认 / 静态方法) | 只能在抽象类或接口中 |
修饰符限制 | 允许 final 、private 、static 、native 等 | 禁止 final 、private 、static 、native ,仅允许 public /protected (或默认,仅抽象类) |
子类要求 | 子类可继承、重写(除非被 final 修饰) | 子类(非抽象类)必须重写实现 |
用途 | 提供具体功能实现 | 定义 “必须由子类实现” 的契约,自身无实现 |
接口
1.什么是接口?
在 Java 中,接口(Interface)是一种抽象类型,属于引用数据类型的范畴。它定义了一组方法的声明,却不包含方法的具体实现内容,就如同一份具有强制效力的 “契约”。这份 “契约” 明确规定了实现该接口的类必须具备的功能,也就是类要为接口中声明的所有方法提供具体的实现逻辑。
从这个角度来说,接口是一种公共的行为规范标准。不同的类在实现接口时,只要严格遵循这个规范标准,实现相应的方法,那么这些类的对象就可以在使用接口的场景中相互通用。例如,多个不同的类实现了同一个 “可打印” 接口,那么这些类的对象都能被用于打印相关的操作流程中,调用它们实现的打印方法,而调用者不需要关心具体是哪个类的对象,只需要知道这些对象都遵循了 “可打印” 接口的规范。所以,接口可以看成是多个类的公共规范,它极大地增强了代码的可扩展性、可维护性和可复用性,使得程序结构更加灵活、清晰。
2.接口怎么使用?
接口不能直接使用,必须要有一个“实现类”来“实现”该接口,实现接口中的所有抽象方法。
public interface 接口名称{
// 抽象方法
public abstract void method1(); // public abstract 是固定搭配,可以不写
public void method2();
abstract void method3();
void method4();
// 注意:在接口中上述写法都是抽象方法,跟推荐方式4,代码更简洁
}
- 创建接口时, 接口的命名一般以大写字母 I 开头.
- 接口的命名一般使用 "形容词" 词性的单词.
- 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
3.接口的特性有哪些?
- 接口类型是一种引用类型,但是不能直接new接口的对象。
- 接口当中,不可以有普通的方法。
- 接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是public abstract,其他修饰符都会报错。
- 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现。
- 重写接口中方法时,不能使用默认的访问权限。
- 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量。
- 接口中不能有静态代码块和构造方法。
- 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class。
- 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类。
- jdk8中:接口中还可以包含default方法。
- 在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到 多继承的目的。
- 接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字。
- 接口当中的方法如果是static修饰的方法,那么是可以有具体实现的。
- 类和接口之间,可以用关键字implements来实现接口。
- 接口也可以发生向上转型和动态绑定。
- 当一个类实现接口当中的方法的时候,当前类当中的方法不能不加public。
- 接口当中不能有构造方法和代码块。
- 一个接口也会产生独立的字节码文件
4.如何实现多个接口?
// 定义第一个接口
interface Interface1 {
void method1();
}
// 定义第二个接口
interface Interface2 {
void method2();
}
// 实现多个接口的类
class MyClass implements Interface1, Interface2 {
@Override
public void method1() {
System.out.println("实现 Interface1 的 method1");
}
@Override
public void method2() {
System.out.println("实现 Interface2 的 method2");
}
}
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.method1();
myClass.method2();
}
}
// 定义一个父类
class ParentClass {
public void parentMethod() {
System.out.println("这是父类的方法");
}
}
// 定义第一个接口
interface InterfaceOne {
void methodOne();
}
// 定义第二个接口
interface InterfaceTwo {
void methodTwo();
}
// 子类继承父类并实现两个接口
class ChildClass extends ParentClass implements InterfaceOne, InterfaceTwo {
@Override
public void methodOne() {
System.out.println("实现 InterfaceOne 的 methodOne 方法");
}
@Override
public void methodTwo() {
System.out.println("实现 InterfaceTwo 的 methodTwo 方法");
}
}
public class Main {
public static void main(String[] args) {
ChildClass child = new ChildClass();
// 调用父类的方法
child.parentMethod();
// 调用接口一的方法
child.methodOne();
// 调用接口二的方法
child.methodTwo();
}
}
这种方式让类既能从父类继承特性,又能获得多个接口所定义的行为,增加了代码的灵活性和可扩展性。
5.接口有什么作用?
想象举办一场大型的科技竞技活动,接口就如同活动主办方制定的比赛规则手册。 这个规则手册规定了不同比赛项目的标准和要求。比如有机器人竞赛,手册里明确指出参赛机器人要具备行走、抓取物品和识别目标这三项技能,这就相当于定义了一个“参赛机器人接口”,包含“行走”“抓取物品”“识别目标”三个方法。 各个参赛团队(相当于不同的类)要想参加这个机器人竞赛,就必须按照规则手册(接口)来打造自己的机器人,也就是实现这些技能(方法)。每个团队打造机器人的具体方式可能不同,有的用履带实现行走,有的用轮子;抓取物品的机械臂设计也各有特色。但只要满足规则手册的要求,就能参赛。 活动的评委(相当于调用者)在评判时,只需要依据规则手册(接口)来检查机器人是否具备相应技能,而不用关心每个团队具体是如何实现这些技能的。如果后续活动规则有变化,比如新增了“语音交互”技能,团队只需要按照新规则改进自己的机器人,评委也能依据新规则进行评判,而不会影响到活动的整体流程和其他部分,这体现了接口在编程中实现多态、解耦以及便于扩展和维护的作用。
6.什么是深拷贝和浅拷贝?
浅拷贝
浅拷贝的核心特点是:对对象中基本数据类型的属性进行值复制,对引用数据类型的属性仅复制引用(即新、旧对象的引用类型属性指向同一对象)。
class Animal implements Cloneable {
private String name; // 私有属性,存储动物的名字
@Override
public Animal clone() { // 重写 clone 方法,实现对象克隆功能
Animal o = null;
try {
o = (Animal) super.clone(); // 调用父类的 clone 方法创建新对象,并强制转换为 Animal 类型
} catch (CloneNotSupportedException e) { // 捕获克隆不支持的异常(虽然实现了 Cloneable 接口,理论上不会抛出,但需处理)
e.printStackTrace(); // 打印异常堆栈信息,便于调试
}
return o; // 返回克隆后的 Animal 对象
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal(); // 创建一个 Animal 实例
Animal animal2 = animal.clone(); // 克隆 animal 实例,得到新对象 animal2
System.out.println(animal == animal2); // 检查两个对象是否为同一个引用(克隆后应为不同对象,输出 false)
}
}
Animal
类的clone
方法仅调用super.clone()
,未对类中属性(如private String name
,从拷贝机制角度看)做额外深层复制操作。它仅仅复制了对象的引用关系,未为引用类型属性(即使是String
这种特殊的不可变引用类型,从拷贝逻辑上看)创建独立新对象来存储副本。- 若
Animal
类中有更复杂的引用类型属性(如自定义的类对象),此代码也不会对该属性进行递归克隆,新、旧对象的该引用属性仍指向同一对象。
因此,这段代码仅复制了对象的表层引用关系,未对引用类型属性做深层独立复制,属于浅拷贝。
深拷贝
深拷贝像是重新写一份和原文件内容一样的文件,文件里引用的资料也会重新复制一份。在 Java 中,深拷贝创建一个新对象,新对象的属性值和原对象相同。对于基本数据类型,复制其值;对于引用类型,会递归地复制对象本身,新对象和原对象的引用类型属性指向不同的对象。
假如我们想将上面的浅拷贝变成深拷贝,如果想改为深拷贝,我们需要确保对象中的引用类型属性也被递归地复制,这样新对象和原对象的所有属性都指向不同的对象实例。在原代码中,Animal
类有一个 String
类型的属性 name
,String
是不可变类型,在 Java 中,对 String
进行浅拷贝和深拷贝的效果是一样的,因为一旦创建,其值不能被修改,所以我们可以给 Animal
类添加一个引用类型的属性,比如 Address
类,然后实现深拷贝。代码如下:
// 定义 Address 类,用于表示地址信息
class Address implements Cloneable {
private String street;
public Address(String street) {
this.street = street;
}
// 重写 clone 方法,实现 Address 类的克隆
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
// 定义 Animal 类,实现 Cloneable 接口以支持克隆
class Animal implements Cloneable {
private String name;
private Address address;
public Animal(String name, Address address) {
this.name = name;
this.address = address;
}
// 重写 clone 方法,实现 Animal 类的深拷贝
@Override
public Animal clone() {
Animal o = null;
try {
// 调用父类的 clone 方法创建新的 Animal 对象
o = (Animal) super.clone();
// 对引用类型的属性 address 进行克隆
o.address = (Address) address.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
public class DeepCopyExample {
public static void main(String[] args) {
// 创建一个 Address 对象
Address address = new Address("123 Main St");
// 创建一个 Animal 对象
Animal animal = new Animal("Tom", address);
// 对 Animal 对象进行深拷贝
Animal animal2 = animal.clone();
// 修改克隆对象的 address 属性
animal2.getAddress().setStreet("456 Elm St");
// 输出原对象的地址信息
System.out.println("Original animal address: " + animal.getAddress().getStreet());
// 输出克隆对象的地址信息
System.out.println("Cloned animal address: " + animal2.getAddress().getStreet());
}
}
深拷贝要求对象的所有引用类型属性都创建独立新实例,新、原对象的引用属性互不影响。在修改后的代码中
Animal
类的clone
方法:先通过super.clone()
复制Animal
自身,然后对引用类型属性address
调用clone()
。Address
类的支持:Address
类实现Cloneable
接口并重写clone
方法,确保address
在复制时生成新实例。- 效果验证:修改克隆对象(如
animal2
)的address
属性,原对象(如animal
)的address
不受影响。这表明animal
与animal2
的address
是独立的不同实例,所有引用类型属性都被深层复制,符合深拷贝的定义。
浅拷贝小课堂:
浅拷贝的引用类型就像多人共享一份在线文档,所有人的链接都指向同一个文档,修改会实时同步给所有人。
假设你和同学一起编辑一份在线文档(比如腾讯文档、Google Docs):
- 基本类型:比如文档里的 “标题”“简单文字”,你复制到自己的笔记里,是独立的内容。你改自己的标题,不影响原文档。
- 引用类型(共享文档):你没有把文档内容复制一份,而是直接保存了一个 “文档链接”。此时:
- 你和同学的 “链接” 都指向同一篇在线文档。
- 只要有人修改文档内容(比如删除一段文字),所有人通过链接打开的文档都会看到变化,因为你们共享的是同一个 “底层对象”。
深拷贝小课堂:
如果你想避免共享带来的影响,就需要深拷贝:
- 基本类型:同样独立复制,和浅拷贝一样。
- 引用类型(共享文档):你不仅复制了链接,还把文档内容下载并保存为一份新文件。此时:
- 你有自己的 “本地文档”,同学有他的 “原始文档”。
- 你修改自己的本地文档,不会影响同学的原始文档,因为你们的引用指向不同的对象。
抽象类和接口有什么区别?
No | 区别 | 抽象类 (abstract) | 接口 (interface) |
---|---|---|---|
1 | 结构组成 | 包含普通方法与抽象方法,也可有成员变量 | 仅包含抽象方法(默认 public abstract )和全局常量(默认 public static final ) |
2 | 权限 | 方法和成员变量可拥有多种访问权限(如 private 、protected 、public 等) | 方法默认 public abstract ,常量默认 public static final ,均为 public 权限 |
3 | 子类使用 | 子类通过 extends 关键字继承抽象类,继承后需实现抽象类中的抽象方法 | 子类通过 implements 关键字实现接口,需实现接口中的所有抽象方法 |
4 | 关系 | 一个抽象类可以实现若干接口,体现对接口的使用 | 接口不能继承抽象类,但接口可以使用 extends 关键字继承多个父接口,形成接口的扩展体系 |
5 | 子类限制 | 一个子类只能继承一个抽象类,受单继承限制 | 一个子类可以实现多个接口,突破单继承限制,灵活组合多种行为能力 |