重返Java之路——抽象类和接口

抽象类


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.抽象方法和普通方法的区别?

抽象方法:

只要方法被 finalprivatestaticnative 修饰,或者方法包含方法体(有具体实现),或者所在的类不是抽象类 / 接口,就 不能定义成抽象方法

普通方法:

只要方法有方法体,且所在类符合语法规则(普通类、抽象类、支持默认方法的接口),就是普通方法,它不强制子类重写(除非在抽象类中被重写,但抽象类中的普通方法本身有实现,子类可选择是否覆盖)。

特征普通方法抽象方法
方法体必须有 {} 和具体实现不能有 {},仅有声明(; 结尾)
所在类普通类、抽象类、接口(Java 8+ 支持接口默认 / 静态方法)只能在抽象类或接口中
修饰符限制允许 finalprivatestaticnative 等禁止 finalprivatestaticnative,仅允许 public/protected(或默认,仅抽象类)
子类要求子类可继承、重写(除非被 final 修饰)子类(非抽象类)必须重写实现
用途提供具体功能实现定义 “必须由子类实现” 的契约,自身无实现

接口


1.什么是接口?

在 Java 中,接口(Interface)是一种抽象类型,属于引用数据类型的范畴。它定义了一组方法的声明,却不包含方法的具体实现内容,就如同一份具有强制效力的 “契约”。这份 “契约” 明确规定了实现该接口的类必须具备的功能,也就是类要为接口中声明的所有方法提供具体的实现逻辑。

从这个角度来说,接口是一种公共的行为规范标准。不同的类在实现接口时,只要严格遵循这个规范标准,实现相应的方法,那么这些类的对象就可以在使用接口的场景中相互通用。例如,多个不同的类实现了同一个 “可打印” 接口,那么这些类的对象都能被用于打印相关的操作流程中,调用它们实现的打印方法,而调用者不需要关心具体是哪个类的对象,只需要知道这些对象都遵循了 “可打印” 接口的规范。所以,接口可以看成是多个类的公共规范,它极大地增强了代码的可扩展性、可维护性和可复用性,使得程序结构更加灵活、清晰。

2.接口怎么使用?

接口不能直接使用,必须要有一个“实现类”来“实现”该接口,实现接口中的所有抽象方法。

接口的定义格式与定义类的格式基本相同,将 class 关键字换成 interface 关键字,就定义了一个接口。
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.如何实现多个接口?

Java 中,类和类之间是单继承的,一个类只能有一个父类,即 Java 中不支持多继承 ,但是 一个类可以实现多个接
// 定义第一个接口
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();
    }
}
注意:一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类
提示, IDEA 中使用 ctrl + i 快速实现接口。
那么在Java中,当一个类继承一个父类 , 也可以 同时实现多种接口。
// 定义一个父类
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 类型的属性 nameString 是不可变类型,在 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());
    }
}    

深拷贝要求对象的所有引用类型属性都创建独立新实例,新、原对象的引用属性互不影响。在修改后的代码中

  1. Animal 类的 clone 方法:先通过 super.clone() 复制 Animal 自身,然后对引用类型属性 address 调用 clone()
  2. Address 类的支持Address 类实现 Cloneable 接口并重写 clone 方法,确保 address 在复制时生成新实例。
  3. 效果验证:修改克隆对象(如 animal2)的 address 属性,原对象(如 animal)的 address 不受影响。这表明 animal 与 animal2 的 address 是独立的不同实例,所有引用类型属性都被深层复制,符合深拷贝的定义。
浅拷贝小课堂:

浅拷贝的引用类型就像多人共享一份在线文档,所有人的链接都指向同一个文档,修改会实时同步给所有人。

假设你和同学一起编辑一份在线文档(比如腾讯文档、Google Docs):

 
  1. 基本类型:比如文档里的 “标题”“简单文字”,你复制到自己的笔记里,是独立的内容。你改自己的标题,不影响原文档。
  2. 引用类型(共享文档):你没有把文档内容复制一份,而是直接保存了一个 “文档链接”。此时:
    • 你和同学的 “链接” 都指向同一篇在线文档
    • 只要有人修改文档内容(比如删除一段文字),所有人通过链接打开的文档都会看到变化,因为你们共享的是同一个 “底层对象”。

深拷贝小课堂:

如果你想避免共享带来的影响,就需要深拷贝

 
  1. 基本类型:同样独立复制,和浅拷贝一样。
  2. 引用类型(共享文档):你不仅复制了链接,还把文档内容下载并保存为一份新文件。此时:
    • 你有自己的 “本地文档”,同学有他的 “原始文档”。
    • 你修改自己的本地文档,不会影响同学的原始文档,因为你们的引用指向不同的对象

抽象类和接口有什么区别?

核心区别 : 抽象类 中可以包含普通方法和普通字段 , 这样的普通方法和字段可以被子类直接使用 ( 不必重写 ), 接口 中不能包含普通方法, 子类必须重写所有的抽象方法。
No区别抽象类 (abstract)接口 (interface)
1结构组成包含普通方法与抽象方法,也可有成员变量仅包含抽象方法(默认 public abstract)和全局常量(默认 public static final
2权限方法和成员变量可拥有多种访问权限(如 privateprotectedpublic 等)方法默认 public abstract,常量默认 public static final,均为 public 权限
3子类使用子类通过 extends 关键字继承抽象类,继承后需实现抽象类中的抽象方法子类通过 implements 关键字实现接口,需实现接口中的所有抽象方法
4关系一个抽象类可以实现若干接口,体现对接口的使用接口不能继承抽象类,但接口可以使用 extends 关键字继承多个父接口,形成接口的扩展体系
5子类限制一个子类只能继承一个抽象类,受单继承限制一个子类可以实现多个接口,突破单继承限制,灵活组合多种行为能力
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值