《C++转Java快速入手系列》抽象类和接口篇

文章目录

《C++转Java快速入手系列》抽象类和接口篇

在这一章节的内容与C++的关系就不是太大了,可以体会到这两种语言的设计哲学的不同之处,但是我们还是可以从解决问题的角度来观测这两种语言的处理方式。

1、抽象类

顾名思义,在我们设计类时,有一些 它本身存在的意义不是为了去实例化出具体的对象 ,就像我们生活中一些抽象概念并不是客观存在的,而是为了便于理解赋予了这个概念。比如图形类分为三角形、圆形、正方形等,我们就会发现它的子类都可以具体的实例化出一个对象,但是图形类本身却很难去有一些标准来实例化出合理的对象,或者倒不如说它的实例化没有什么意义 ,所以这种类我们不如不让它进行实例化 ,进而抽象类 这个设计就是为这种情况而生。与此同时,抽象类中的方法也并没有什么实际的具体实现,但是可以被用来继承,所以我们可以把它设计成一个抽象方法

1.1、抽象类语法

在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体

  • 虽然抽象类不进行直接构造,但是也需要构造方法,因为要考虑子类的构造

1.2、抽象类特性

  • 抽象类不能直接实例化对象
  • 抽象方法不能是 private 的
  • 抽象方法不能被final和static修饰,因为抽象方法要被子类重写
  • 抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
  • 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
  • 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量

1.3、抽象类的作用

抽象类本身不能被实例化,所以它存在的意义就是为了让其子类重写抽象类中的抽象方法,虽然普通类也能做到,但是抽象类多了一层检验,如果错用父类来进行操作,就会进行报错。

2、接口

2.1、接口的概念

java中的接口也和生活中接口的理解很相近,都是提供了一种公共规范,在java中,接口就是为多种类提供了公共规范 ,其接口也属于引用类型

2.2、语法规则

接口的定义格式与类的定义格式相同,将class 关键字替换成interface即可

java 复制代码
public interface 接口名{
	public abstract void func(){ //此处 public abstract是默认设置,可以省略不写
	}
}
  • 创建接口时, 接口的命名一般以大写字母 I 开头.
  • 接口的命名一般使用 "形容词" 词性的单词.
  • 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性

2.3、接口的使用

  • 接口的使用必须依托于关键字 implements作用于类 去实现对应接口,进而实现接口中的所有抽象方法
java 复制代码
public class 类名 implements 接口名{
}
  • 注意区分:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系。

以下是电脑类,USB接口,鼠标类,键盘类 之间的实现关系

java 复制代码
// 1. 定义USB接口
public interface UsbInterface {
    void connect(); // 抽象方法,无实现
}

// 2. 鼠标类实现接口
public class Mouse implements UsbInterface {
    @Override
    public void connect() {
        System.out.println("鼠标通过USB接口连接:开始移动光标");
    }
}

// 3. 键盘类实现接口
public class Keyboard implements UsbInterface {
    @Override
    public void connect() {
        System.out.println("键盘通过USB接口连接:等待输入指令");
    }
}

// 4. 笔记本电脑类(使用接口类型作为参数)
public class Laptop {
    public void plugInDevice(UsbInterface device) {
        System.out.println("笔记本检测到USB设备...");
        device.connect(); // 多态调用具体实现
    }
}

// 5. 主类演示关系
public class Main {
    public static void main(String[] args) {
        Laptop myLaptop = new Laptop();
        UsbInterface mouse = new Mouse(); // 接口引用指向鼠标对象
        UsbInterface keyboard = new Keyboard(); // 接口引用指向键盘对象

        myLaptop.plugInDevice(mouse);     // 连接鼠标
        myLaptop.plugInDevice(keyboard);  // 连接键盘
    }
}

2.4、接口特性

  1. 接口类型是一种引用类型,但是不能直接new接口接口对象
  2. 接口中的方法默认也只能为public abstract,可以省略不写,但如果写成其它种类的修饰会报错
  3. 接口中的方法是不能在接口中实现 的,只能由实现接口的来实现
  4. 重写接口中的方法时不能使用默认的访问权限
  5. 接口中可以含有变量 ,但是接口中的变量会被隐式的指定为 public static final 变量
  6. 接口中不能有静态代码块和构造方法(因为本身不需要)
  7. 接口编译后的文件后缀也是.class
  8. 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
  9. jdk8中:接口中还可以包含default方法。

2.5、实现多个接口

众所周知,在java中的继承只能进行单继承,但是接口就很好的弥补了这一点,一个类可以实现多个接口,实现的接口名之间用逗号隔开

java 复制代码
// 定义第一个接口
interface Printable {
    void printContent();
}

// 定义第二个接口
interface Calculable {
    int calculate(int a, int b);
}

// 实现类同时实现两个接口
class MathPrinter implements Printable, Calculable {
    // 实现Printable接口的方法
    @Override
    public void printContent() {
        System.out.println("计算结果:");
    }
    
    // 实现Calculable接口的方法
    @Override
    public int calculate(int a, int b) {
        return a * b; // 乘法运算
    }
}

public class MultiInterfaceDemo {
    public static void main(String[] args) {
        MathPrinter printer = new MathPrinter();
        printer.printContent();
        
        int result = printer.calculate(5, 8);
        System.out.println("5 * 8 = " + result);
    }
}
  • 注意一个类实现多个接口时,必须要将每个接口的抽象方法都要实现,否则该类必须设定为抽象类

2.6、接口间的继承

接口之间也是可以进行继承的,虽然在java中类之间不支持多继承啊,但是在接口之间是支持多继承的

java 复制代码
interface IRunning {
    void run();
}
 
interface ISwimming {
    void swim();
}
 
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
 
}
 
class Frog implements IAmphibious {
 ...
}

2.7、接口的使用-> 比较器

在Java中,对于基本类型的比较,我们可以直接使用比较运算符(如>、<、==等)来实现。

对于引用类型的比较,我们通常用以下两种比较器来进行处理,这两种比较器也与接口密切相关

2.7.1、内部比较器

这种方法比较方便调用,但是也会使代码的耦合度变高

  • 使用方法:需要比较的类实现Comparable接口并且重写其compareTo
    eg:
java 复制代码
public class Student implements Comparable<Student>{
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" + "name=" + name + ",age=" + age + "}";
    }

    @Override
    public int compareTo(Student o) {
//        if(this.age > o.age){
//            return 1;
//        }else if (this.age == o.age){
//            return 0;
//        }else{
//            return -1;
//        }
        return this.name.compareTo(o.name);
    }

    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",10);
        students[1] = new Student("lisi",19);
        students[2] = new Student("wangwu",4);
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students);
        System.out.println("排序后" + Arrays.toString(students));
    }
}

这里接口 的用法其实一部分隐藏在了sort方法中,其实这里sort内部的实现运用了接口,因为Student类实现了Comparable类,所以sort可以将传入的Student数组类型 强转为Comparable的数组类型 再调用重写过的compareTo

注意:在compareTo中如果逻辑是this的属性大于传入参数的属性 返回正数的话,即为升序排序

2.7.2、外部比较器

这种方法的耦合度较低且更灵活适配多种情况,但是需要自定义类,额外传递比较器参数比较麻烦

当我们对引用类型的比较存在以下几种场景时,内部比较器就满足不了我们的需求,这时我们就要用到外部比较器了。

  1. 多标准排序:同一类对象按不同属性排序(如先按年龄再按姓名)。
  2. 第三方类排序:无法修改源码的类(如String、Integer)需自定义排序规则时。
  3. 临时排序需求:运行时动态指定排序逻辑(如升序/降序切换)。
  4. 复杂比较逻辑:需要组合多个属性或非自然顺序的比较(如字符串长度优先于字母顺序)。

外部比较器的使用:创建一个类专门用来重写独立的比较规则,具体表现为实现Comparator接口然后重写其compare抽象方法即可
注意:类实现Comparator后面要跟<要比较的类名>

java 复制代码
public class NameComparator implements Comparator<Student> {

    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }

}
public class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}
public class Student implements Comparable<Student>{
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" + "name=" + name + ",age=" + age + "}";
    }

    @Override
    public int compareTo(Student o) {
//        if(this.age > o.age){
//            return 1;
//        }else if (this.age == o.age){
//            return 0;
//        }else{
//            return -1;
//        }
        return this.name.compareTo(o.name);
    }

    public static void main(String[] args) {
        NameComparator nameComparator = new NameComparator();
        AgeComparator ageComparator = new AgeComparator();
        Student[] students = new Student[3];
        students[0] = new Student("zhangsan",10);
        students[1] = new Student("lisi",19);
        students[2] = new Student("wangwu",4);
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students,nameComparator);
        System.out.println("排序后" + Arrays.toString(students));
        System.out.println("排序前:" + Arrays.toString(students));
        Arrays.sort(students,ageComparator);
        System.out.println("排序后" + Arrays.toString(students));
        System.out.println(nameComparator.compare(students[0], students[1]));
    }
}

2.8、Clonable 接口和深拷贝

Java中内置了一些很常用的接口,Clonable 就是其中之一,Object类 中存在一个clone 方法,调用这个方法可以创建一个对象 的"拷贝 ",调用该方法的前提是该类必须要先实现Clonable接口 ,否则会抛出CloneNotSupportedException异常

对于拷贝和这个接口,有几点需要强调:

  • Clonable 是一个标记接口 ,该接口内部并没有任何方法 ,它的作用是告诉 JVM:这个类的实例可以安全地调用 Object.clone() 方法
  • 在C++当中,我们一般要对某个类拷贝,需要的是拷贝构造,在Java中也并不例外,虽然有clone方法,但是也并不好用,java虽然没有 像C++一样有默认生成的拷贝构造 ,但是可以手动去编写拷贝构造,在Java中更推荐使用 拷贝构造方法 或者 静态工厂方法 进行拷贝,对于clone方法的弊端稍后会提到
2.8.1、Clonable接口与clone方法

注意:Object默认提供的clone方法是浅拷贝

使用方法:

  1. 将需要进行拷贝对象的类实现Clonable方法,并且重写Object中的clone方法
  2. 注意处理重写clone方法中的异常抛出问题
  3. 调用克隆方法并且接收时要注意强制类型转换,因为clone方法默认返回类型为Object

eg:

java 复制代码
public class Person implements Cloneable{
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("zhangsan",22);
        Person person2 = (Person) person1.clone();
    }
}

为什么说clone方法默认是浅拷贝呢,接下来对此情况进行分析。

当类中有引用类型的成员变量进行clone方法的拷贝就会出现一个问题:

java 复制代码
class Money{
    public double money = 9.9;
}
public class Person implements Cloneable{
    public String name;
    public int age;

    public Money money = new Money();

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("zhangsan",22);
        Person person2 = (Person) person1.clone();
    }
}

当拷贝的对象中有引用类型成员变量 时,并不会自动创建一个新的对象进行指向,而是将该对象的引用类型对象的地址进行复制放到拷贝对象中,所以就出现了上面这一幕,person1和person2对象中的money对象都指向了同一个。如果一个person1中对money进行修改,那么person2中的money也跟着修改,这显然是我们不想要看到的情况。

那么我们再思考一个问题,String也是引用类型 ,那么他的浅拷贝也会同时指向一个对象,但这就是我们愿意看到的,因为String对象是不可修改的,所以是不是指向同一个对象就变得不重要了。

所以我们可以得出以下结论:

  • clone方法默认为浅拷贝

  • 浅拷贝对引用类型:只复制地址,共享原对象。

  • String 不可变,共享无妨。

  • 真正危险的:是那些可变的引用类型(包括自定义类、集合、数组等),无论它是不是你写的

这种情况解决办法和C++中一样,本质都是:深拷贝,即连引用指向的对象也复制一份。

2.8.2、深拷贝

将类中的那个引用类型的变量也独立进行一次clone复制一份

java 复制代码
class Money implements Cloneable{
    public double money = 9.9;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Person implements Cloneable{
    public String name;
    public int age;

    public Money m = new Money();

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person tmp = (Person)super.clone();
        tmp.m = (Money)this.m.clone();
        return tmp;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person("zhangsan",22);
        Person person2 = (Person) person1.clone();
        person1.m.money = 3.3;
        System.out.println(person2.m.money);
    }
}
  1. Money类的克隆
    Money类实现了Cloneable接口,并重写了clone()方法:
  2. Person类的深拷贝实现
    Person类也实现了Cloneable接口,并重写了clone()方法
    步骤1: 浅拷贝基础
    通过super.clone()创建一个浅拷贝对象tmp。这复制了name和age等基本类型字段,但m字段会共享原始对象的引用(浅拷贝)
    步骤2: 手动深拷贝引用字段
    关键代码是tmp.m = (Money) this.m.clone();。这行代码显式调用m.clone()来克隆Money对象,并将新副本赋值给tmp.m:
2.8.3、Clonable接口clone方法的优势劣势与适用情况
  • 优势:
    • 多态克隆的核心能力

      通过基类引用,无需知道具体子类,就能复制当前对象。这是拷贝构造做不到的。

    • 针对特定类的一键浅拷贝

      Object.clone() 的默认实现会直接复制字段,在字段多且所需的就是浅拷贝时,代码极简。

    • 与某些框架的自然契合

      部分框架(如早期原型工厂)或 JDK 集合类(如 ArrayList)内建支持 clone()。


  • 劣势(远多于优势):

    • 设计拙劣,门槛较高

      必须实现 Cloneable,否则 clone() 会直接抛出 CloneNotSupportedException。这个接口是空的,只是标记作用,编译器完全无法检查你是否正确实现了克隆逻辑。

    • 必须手动处理受检异常

      clone() 签名抛出了 CloneNotSupportedException,每次调用都需 try-catch 或向上抛出,污染代码。

    • 返回值是 Object,每次调用都需强转

      不安全,容易埋下类型转换隐患。

    • 默认浅拷贝,深拷贝需手工递归

      对于可变引用字段,浅拷贝会造成原对象和副本绑定同一内部对象,埋下难以排查的 bug。实现深拷贝要在重写 clone() 时逐个字段递归克隆,过程繁琐且容易遗漏。

    • 与 final 字段的使用不兼容

      由于 clone() 是逐个字段赋值,无法为带 final 修饰的引用字段重新设置指向,导致无法实现深拷贝。

    • 违反清晰编码的原则

      复制逻辑隐藏在 clone() 方法内,调用者需查看文档才知道是浅拷贝还是深拷贝,而拷贝构造函数的语义更明确。

  • 适用情况:

    • 原型模式 / 多态克隆

      当你持有基类引用,而不知道具体子类类型,却想得到一个与当前对象类型完全一致的副本时,clone() 的多态特性就很关键。拷贝构造无法做到这一点,因为它需要知道具体的类名。

    • 遗留代码/框架兼容

      很多老旧框架或 JDK 内部类(如 ArrayList、HashMap)提供了 public clone() 方法,必须通过 clone() 来复制集合或对象。

    • 快速获得字段完全相同的"浅拷贝"

      如果一个对象字段多且全部是基本类型或不可变类(如 String、Integer),调用 super.clone() 可以一键生成副本,免去逐字段赋值的烦琐。但这种场景拷贝构造同样胜任,并没有压倒性的优势。

相关推荐
MuYiLuck1 小时前
01-spring-boot-autoconfig-principle
java·spring·maven·自动配置
河阿里1 小时前
Lambda表达式(Java):从语法本质到工程实践
java·开发语言
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【47】状态图定义:StateGraph 源码解析
java·人工智能·spring
6190083361 小时前
spring中 HTTP 请求常见格式
java·spring·http
MATLAB代码顾问1 小时前
MATLAB实现粒子群算法优化PID参数
开发语言·算法·matlab
雪度娃娃1 小时前
结构型设计模式——桥接模式
c++·设计模式·桥接模式
Veggie261 小时前
cuda 13.2 install on ubuntu26
java
陈天伟教授1 小时前
图解人工智能(1)居里点
大数据·开发语言·人工智能·gpt
翎沣1 小时前
C++11异常处理机制
java·c++·算法