文章目录
- 《C++转Java快速入手系列》抽象类和接口篇
-
- 1、抽象类
- 2、接口
-
- 2.1、接口的概念
- 2.2、语法规则
- 2.3、接口的使用
- 2.4、接口特性
- 2.5、实现多个接口
- 2.6、接口间的继承
- [2.7、接口的使用-> 比较器](#2.7、接口的使用-> 比较器)
- [2.8、Clonable 接口和深拷贝](#2.8、Clonable 接口和深拷贝)
《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、接口特性
- 接口类型是一种引用类型,但是不能直接new接口接口对象
- 接口中的方法默认也只能为public abstract,可以省略不写,但如果写成其它种类的修饰会报错
- 接口中的方法是不能在接口中实现 的,只能由实现接口的类来实现
- 重写接口中的方法时不能使用默认的访问权限
- 接口中可以含有变量 ,但是接口中的变量会被隐式的指定为 public static final 变量
- 接口中不能有静态代码块和构造方法(因为本身不需要)
- 接口编译后的文件后缀也是.class
- 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
- 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、外部比较器
这种方法的耦合度较低且更灵活适配多种情况,但是需要自定义类,额外传递比较器参数比较麻烦
当我们对引用类型的比较存在以下几种场景时,内部比较器就满足不了我们的需求,这时我们就要用到外部比较器了。
- 多标准排序:同一类对象按不同属性排序(如先按年龄再按姓名)。
- 第三方类排序:无法修改源码的类(如String、Integer)需自定义排序规则时。
- 临时排序需求:运行时动态指定排序逻辑(如升序/降序切换)。
- 复杂比较逻辑:需要组合多个属性或非自然顺序的比较(如字符串长度优先于字母顺序)。
外部比较器的使用:创建一个类专门用来重写独立的比较规则,具体表现为实现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方法是浅拷贝
使用方法:
- 将需要进行拷贝对象的类实现Clonable方法,并且重写Object中的clone方法
- 注意处理重写clone方法中的异常抛出问题
- 调用克隆方法并且接收时要注意强制类型转换,因为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);
}
}
- Money类的克隆
Money类实现了Cloneable接口,并重写了clone()方法: - 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() 可以一键生成副本,免去逐字段赋值的烦琐。但这种场景拷贝构造同样胜任,并没有压倒性的优势。
-