抽象类和接口
抽象类
抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是并不是所有的类都是用来描述对象的,如果一个类中没有足够的信息来描绘一个具体的对象,这样的类就是抽象类。
例如我们 上篇文章 中的代码例子中的 Animal 类中并没有实际意义的方法,在之后的子类都进行了重写,而且该类并不能直接表示某种动物,例如狗叫是汪汪汪,猫叫是喵喵喵,并不能表示直接具体的动物,所以该类可以设计为抽象类。另一个例子 shape 类画图也是同样可以设计为抽象类。
像draw这种没有实际工作的方法,我们可以将其设置为抽象方法,包含抽象方法的类我们称为抽象类。
抽象类语法
在java中,一个类如果被abstract修饰为抽象类,抽象类中被abstract修饰的方法称为抽象方法,抽象方法不用具体给出具体的实现体。
//抽象类:被 abstract 修饰的类
public abstract class Shape {
//抽象方法:被abstract修饰的方法,没有方法体
public abstract void draw();
public abstract void calcArea();
//抽象类也是类,可以增加普通方法和属性
protected double Area; //面积
public double getArea(){
return Area;
}
}
注意:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法,抽象类和普通类的区别在于抽象类中可以包含抽象方法,如果这个类中有抽象方法,那这个类一定是抽象类。
抽象类特性
- 抽象类不能直接实例化对象。
public class Test {
public static void main(String[] args) {
Shape shape = new Shape(); //编译报错
}
}
- 抽象方法不能是 private 修饰
public abstract class Shape {
private abstract void draw(); //编译报错
}
- 抽象方法不能被final 和 static修饰,因为抽象方法要在子类中重写
public abstract class Shape {
public final abstract void draw(); //编译报错
public static abstract void calcArea(); //编译报错
}
- 抽象类必须被继承,并且继承后子类必须要重写父类中的抽象方法,否则子类也应该是抽象类,必须使用abtract修饰
public class Rect extends Shape{
private double length;
private double width;
public Rect(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public void draw() {
System.out.println("矩形:length = " + length + "width = " + width);
}
@Override
public void calcArea() {
Area = length * width;
}
}
- 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
- 抽象类可以有构造方法,供子类创建对象时,初始化父类成员变量
抽象类的作用
抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类来重写抽象类中的方法。
但是普通的类也可以被继承,普通的方法也可以被重写,为什么非得用抽象类和抽象方法呢?
使用抽象类相当于多了一重编译器的校验。
使用抽象类场景的代码如上面所示,实际工作不应该由父类来完成,而应由子类来完成。此时如果不小心使用成父类,使用普通类编译器是不会报错的,但是如果父类是抽象类的话,就会在实例化的时候提示错误,让我们尽早发现问题。
接口
接口的概念
现实生活中接口的例子有很多,如笔记本上的USB接口,电源插座等等。
笔记本上的USB接口可以插:U盘,鼠标,键盘···所有符合USB协议的的设备
电源插座可以插:电脑,电视机,电饭煲···所有符合规范的设备
通过上述例子可以看出:接口就是公共的行为规范标准,大家在实现时只要符合规范标准就可以通用
在java中,接口可以看成:多个类的公共规范,是一种引用数据类型。
语法规则
接口定义的格式与定义类的格式基本相同,将class关键字换成interface关键字就定义了一个接口。
public interface 接口名称{
//抽象方法
public abstract void method1(); //public abstract是固定搭配,可以不写
public void method2();
abstract void method3();
void method4();
}
//注意:在接口上述写法都是抽象方法,更推荐写法4,代码更简洁。
提示:
- 创建接口时,接口的命名一般以大写字母 I 开头。
- 接口的名字一般使用形容词性的单词
- 阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号,保持代码的简洁性。
接口特性
- 接口是一种引用数据类型,但是不能直接new接口的对象。
public class Test {
public static void main(String[] args) {
IShape iShape = new IShape(); //编译报错,IShape是抽象的; 无法实例化
}
}
- 接口当中的成员变量默认是public static final修饰的
public interface IShape {
public static final int a = 1;
int b = 2;
}
- 接口当中的成员方法默认是public static修饰的
public interface IShape {
public abstract void draw();
void draw1();
}
- 因为接口中的方法默认是public abstract修饰的,所以该方法的具体实现只能由实现接口的类来实现,在接口内不可以实现。
- 如果接口当中的方法被default修饰,该方法是可以具体实现的。
public interface IShape {
default void test(){
System.out.println("接口中的方法被default修饰是可以具体实现的");
}
}
- 如果接口当中的方法被static修饰,该方法是可以具体实现的。
public interface IShape {
static void test(){
System.out.println("接口中的方法被static修饰是可以具体实现的");
}
}
- 类和接口之间可以使用关键字 implements 来实现接口
public interface IShape {
void draw();
}
public class Cycle implements IShape{
@Override
public void draw() {
System.out.println("画一个⚪");
}
}
- 重写接口当中的方法时,不能使用默认的访问权限
public interface IShape {
void draw(); //默认是public static修饰的
}
public class Cycle implements IShape{
@Override
void draw() {
System.out.println("画一个⚪");
}
//编译报错
//java: Cycle中的draw()无法实现IShape中的draw()
//正在尝试分配更低的访问权限; 以前为public,default默认权限比public权限高,所以报错,
//要高于等于public权限,所以应该使用public修饰
}
- 一个接口对应一个字节码文件
- 如果一个类不想实现接口当中的方法,那么此时这个类可以被定义为抽象类,但是这个类如果被继承,那么就得实现所有没被实现的方法。
接口的使用
接口不能直接使用,必须要有一个实现类来实现该接口,实现接口中的所有抽象方法。
子类和父类之间是 extends 继承关系,类与接口之间是 implements 实现关系。
请实现笔记本电脑使用USB鼠标、USB键盘的例子:
- USB接口:包含打开设备、关闭设备功能
- 笔记本类:包含开机功能、关机功能、使用USB设备功能
- 鼠标类:实现USB接口,并具备点击功能
- 键盘类:实现USB接口,并具备输入功能
public interface USB {
void openDevice();
void closeDevice();
}
public class Mouse implements USB{
@Override
public void openDevice() {
System.out.println("打开鼠标设备");
}
@Override
public void closeDevice() {
System.out.println("关闭鼠标设备");
}
public void click(){
System.out.println("点击鼠标");
}
}
public class KeyBoard implements USB{
@Override
public void openDevice() {
System.out.println("打开键盘设备");
}
@Override
public void closeDevice() {
System.out.println("关闭键盘设备");
}
public void input(){
System.out.println("键盘输入");
}
}
public class Computer implements USB{
@Override
public void openDevice() {
System.out.println("开机");
}
@Override
public void closeDevice() {
System.out.println("关机");
}
public void useUSBDevice(USB usb){
usb.openDevice();
if(usb instanceof Mouse){
Mouse mouse = (Mouse)usb;
mouse.click();
}else if(usb instanceof KeyBoard){
KeyBoard keyBoard = new KeyBoard();
keyBoard.input();
}
usb.closeDevice();
}
}
public class Test {
public static void main(String[] args) {
Computer computer = new Computer();
computer.openDevice();
computer.useUSBDevice(new Mouse());
computer.useUSBDevice(new KeyBoard());
computer.closeDevice();
}
}
//开机
//打开鼠标设备
//点击鼠标
//关闭鼠标设备
//打开键盘设备
//键盘输入
//关闭键盘设备
//关机
实现多个接口
在java中,类和类之间是单继承的,一个类只能有一个父类,即java中不支持多继承,但是一个类可以实现多个接口。
下面通过动物的例子来理解
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
public interface IRunning {
void run();
}
public interface IFlying {
void fly();
}
public interface ISwimming {
void swim();
}
public class Cat extends Animal implements IRunning{
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在跑");
}
}
public class Fish extends Animal implements ISwimming{
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在游泳");
}
}
public class Frog extends Animal implements ISwimming,IRunning{
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在跑步");
}
@Override
public void swim() {
System.out.println(this.name + "正在游泳");
}
}
public class Duck extends Animal implements IRunning,ISwimming,IFlying{
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在飞");
}
@Override
public void run() {
System.out.println(this.name + "正在跑步");
}
@Override
public void swim() {
System.out.println(this.name + "正在游泳");
}
}
上面的代码展示了java面向对象编程中常用的用法:一个类继承一个父类,同时实现多种接口。
继承表达的含义是is-a的含义,而接口表达的含义是具有xxx属性
猫是一种动物,具有会跑的属性
鱼是一种动物,具有会游的属性
青蛙是一种动物,具有会跑、游的属性
鸭子是一种动物,具有会跑、游、飞的属性
这样设计的好处是让程序员忘记类型,有了接口之后,类的使用者不必关注具体类型,而只关注某个类是否具备某种能力
例如:我们实现一个散步的方法
public class Test {
public static void walk(IRunning iRunning){
iRunning.run();
}
public static void main(String[] args) {
Cat cat = new Cat("猫");
walk(cat);
Frog frog = new Frog("青蛙");
walk(frog);
}
}
//结果为:
//猫正在跑
//青蛙正在跑步
而且参数可以不是动物,只要是具有跑的属性,实现了实现跑步接口的类都可以
public class Robot implements IRunning{
protected String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在跑");
}
}
public class Test {
public static void walk(IRunning iRunning){
iRunning.run();
}
public static void main(String[] args) {
Cat cat = new Cat("猫");
walk(cat);
Frog frog = new Frog("青蛙");
walk(frog);
Robot robot = new Robot("罗伯特");
walk(robot);
}
}
//结果为:
//猫正在跑
//青蛙正在跑步
//罗伯特正在跑
接口间的继承
在java中,类和类之间是单继承的,一个类可以实现多个接口,接口和接口之间可以多继承,即用接口可以实现多继承的目的。
接口可以继承一个接口,达到复用,使用extends关键字
public interface IRunning {
void run();
}
public interface ISwimming {
void swim();
}
//两栖动物既能跑,也能游
public interface IAmphibious extends IRunning,ISwimming{
}
public class Frog extends Animal implements IAmphibious{
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在跑步");
}
@Override
public void swim() {
System.out.println(this.name + "正在游泳");
}
}
通过接口继承创建一个新的接口IAmphibious表示两栖的,此时实现接口创建的Frog类,就要继承实现run方法和swim方法,接口间的继承相当于把多个接口合并在一起。
Object类
Object类是java默认提供的一个类,java里面除了Object类,所以的类都是存在继承关系的,都是默认继承Object父类的,即所有类的对象都可以使用Object的引用进行接收。
例子:使用Object接收所有类的对象
public class Person {
}
public class Student {
}
public class Test {
public static void main(String[] args) {
function(new Person());
function(new Student());
}
public static void function(Object object){
System.out.println(object);
}
}
//结果为
//Person@3b07d329
//Student@404b9385
所以在开发中,Object类是参数的最高统一类型,而且Object类也存在一些定义好的方法
我们先熟悉红色方框中三种方法:toString()方法,hashCode()方法,equals()方法
获取对象信息
如果要打印对象中的信息,可以直接重写Object类中的toString方法,首先我们先看Object类中toString方法实现,当我们使用 System.out.println()方法就会调用Object类中toString方法,如果我们要打印自己需要的信息则需要重写该方法。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("张三",18);
System.out.println(student1);
}
}
//结果为:
//Student@3b07d329
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("张三",18);
System.out.println(student1);
}
}
//结果为:
//Student{name='张三', age=18}
对象比较equals方法
在java中,== 进行比较时:
- 如果 == 左右两侧是基本类型变量,比较的是变量中的值是否相同;
- 如果 == 左右两侧是引用类型变量,比较的是引用变量地址是否相同;
- 如果要比较对象中的内容,必须重写Object中的equals方法,因为equals方法 ,因为equals方法默认也是按照地址比较的:
//Object类中的equals方法
public boolean equals(Object obj) {
return (this == obj); //使用引用中的地址直接来进行比较
}
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("张三",18);
Student student2 = new Student("张三",18);
int a = 10;
int b = 10;
System.out.println(a == b);
System.out.println(student1 == student2);
System.out.println(student1.equals(student2));
}
}
//结果为:
//true
//false
//false
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if(o == null){
return false;
}
if(o == this){
return true;
}
//不是Student类对象
if(!(o instanceof Student)){
return false;
}
Student student = (Student)o;
return this.name.equals(student.name) && this.age == student.age;
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("张三",18);
Student student2 = new Student("张三",18);
int a = 10;
int b = 10;
System.out.println(a == b);
System.out.println(student1 == student2);
System.out.println(student1.equals(student2));
}
}
//结果为:
//true
//false
//true
hashCode()方法
如我们刚才看到的toString方法的源码中
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
在源码中我们看到了hashCode()这个方法,帮我们找到了对象的具体位置,这里面涉及到数据结构,我们先简单的理解为给了我们内存地址,然后调用Integer.toHexString()方法,将这个地址以16进制输出。
hashCode方法源码
public native int hashCode();
该方法是一个native方法,底层是由C/C++代码写的。
我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,如果不重写hashCode()方法,他们的位置是不同的。
public class Test {
public static void main(String[] args) {
Student student1 = new Student("张三",18);
Student student2 = new Student("张三",18);
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
}
}
//结果为:
//990368553
//1096979270
两个对象的hash值不一样
我们对hashCode()方法进行重写
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("张三",18);
Student student2 = new Student("张三",18);
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
}
}
//结果为:
//24022538
//24022538
哈希值一样。
结论:
- hashCode()方法用来确定对象在内存中存储的位置是否相同
- 事实上hashCode()方法在散列表中才有用,在其他情况下没用,在散列表中hashCode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
匿名内部类
匿名内部类就是没有名字且在其他内部类中的一个类
语法格式:
new 类名或接口名{
重写方法;
}
我们知道接口是没办法实例化的,如果使用匿名内部类就可以很好的解决这个问题
public interface Animal {
void run();
}
public class Out {
public void test(){
System.out.println("out()方法");
}
}
public class Test {
public static void main(String[] args) {
new Out(){
}.test();
new Out(){
public void test(){
System.out.println("haha");
}
}.test();
new Animal(){
public void run(){
System.out.println("狗在跑");
}
}.run();
}
}
//结果为:
//out()方法
//haha
//狗在跑
如果我们没有使用匿名内部类,我们要输出狗在跑,我们需要以下代码:
public interface Animal {
void run();
}
public class Dog implements Animal {
@Override
public void run() {
System.out.println("狗在跑");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.run();
}
}
接口的使用实例
给对象数组排序
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
//我们想将三名学生按照年龄进行排序,是否可以如之前数组排序一样调用sort方法呢
Student[] students = new Student[3];
students[0] = new Student("张三",18);
students[1] = new Student("李四",16);
students[2] = new Student("王五",20);
Arrays.sort(students);
}
}
//运行出错,抛出异常
//Exception in thread "main" java.lang.ClassCastException:...
我们进一步思考,我们可以发现和以前的普通整数不一样,整数是可以直接比较的,大小关系明确,而两个学生对象的大小关系怎么确定,需要我们额外指定。
在sort方法中要求比较的对象要实现Comparable接口,并实现其中的compareTo方法
public class Student implements Comparable<Student>{
public String name;
public 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 -1;
}else{
return 0;
}
}
}
public class Test {
public static void main(String[] args) {
//我们想将三名学生按照年龄进行排序,是否可以如之前数组排序一样调用sort方法呢
Student[] students = new Student[3];
students[0] = new Student("张三",18);
students[1] = new Student("李四",16);
students[2] = new Student("王五",20);
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
//结果为:
//[Student{name='李四', age=16}, Student{name='张三', age=18}, Student{name='王五', age=20}]
在sort方法中会自动调用compareTo方法,compareTo方法的参数是Object,其实传入的是Student类型的对象。然后比较当前对象和参数对象的大小关系(按照年龄大小)
如果当前对象应排在参数对象之前的话,则返回小于0的数字;
如果当前对象应排在参数对象之后的话,则返回大于0的数字;
如果当前对象和参数对象不分先后,则返回0.
为了进一步加深对接口的理解,我们可以尝试自己实现一个sort方法来完成和刚才的排序过程(使用冒泡排序)
public class Test {
public static void mySort(Comparable[] comparable){
for (int i = 0; i < comparable.length - 1; i++) {
for (int j = 0; j < comparable.length - 1 - i; j++) {
if (comparable[j].compareTo(comparable[j+1]) > 0){
Comparable tmp = comparable[j];
comparable[j] = comparable[j+1];
comparable[j+1] = tmp;
}
}
}
}
public static void main(String[] args) {
//我们想将三名学生按照年龄进行排序,是否可以如之前数组排序一样调用sort方法呢
Student[] students = new Student[3];
students[0] = new Student("张三",18);
students[1] = new Student("李四",16);
students[2] = new Student("王五",20);
/*Arrays.sort(students);*/
mySort(students);
System.out.println(Arrays.toString(students));
}
}
//结果为:
//[Student{name='李四', age=16}, Student{name='张三', age=18}, Student{name='王五', age=20}]
如果我们现在希望通过名字来排序的话我们就应该再次修改compareTo方法
public class Student implements Comparable<Student>{
public String name;
public 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) {
return this.name.compareTo(o.name);
}
}
public class Test {
public static void main(String[] args) {
//将三名学生按照名字进行排序
Student[] students = new Student[3];
students[0] = new Student("张三",18);
students[1] = new Student("李四",16);
students[2] = new Student("王五",20);
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
//结果为:
//[Student{name='张三', age=18}, Student{name='李四', age=16}, Student{name='王五', age=20}]
这种比较的方式一般用于固定的比较,不适合非常灵活的比较,每次我们想换一种比较方式则需要修改compareTo方法,比较麻烦,解决方法就是使用另一种接口。
首先只要是自定义类型,涉及到了大小的比较,目前的结论是一定要实现Comparable接口
我们根据不同的属性进行比较,但是不想每次修改已经写好的方法,我们可以换一个接口Comparator这个接口来解决。
import java.util.Comparator;
public class NameComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
import java.util.Comparator;
public class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
public class Test {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("张三",18);
students[1] = new Student("李四",16);
students[2] = new Student("王五",20);
NameComparator nameComparator = new NameComparator();
Arrays.sort(students,nameComparator);
System.out.println(Arrays.toString(students));
}
}
//结果为:
//[Student{name='张三', age=18}, Student{name='李四', age=16}, Student{name='王五', age=20}]
public class Test {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("张三",18);
students[1] = new Student("李四",16);
students[2] = new Student("王五",20);
AgeComparator ageComparator = new AgeComparator();
Arrays.sort(students,ageComparator);
System.out.println(Arrays.toString(students));
}
}
//结果为:
//[Student{name='李四', age=16}, Student{name='张三', age=18}, Student{name='王五', age=20}]
如何通过方法交换实参的值
public class Test {
public static void swap(int a, int b){
int tmp = a;
a = b;
b = tmp;
}
public static void main(String[] args) {
int value1 = 10;
int value2 = 20;
System.out.println("交换前: value1 = " + value1 + " value2 = " + value2 );
swap(value1 ,value2 );
System.out.println("交换后: value1 = " + value1 + " value2 = " + value2 );
}
}
//结果为:
//交换前: value1 = 10 value2 = 20
//交换后: value1 = 10 value2 = 20
只是交换了形参的值,而对实参的值并没有任何改变
public class Value {
public Value(int value) {
this.value = value;
}
public int value;
}
public class Test {
public static void swap(Value v1, Value v2){
int tmp = v1.value;
v1.value = v2.value;
v2.value = tmp;
}
public static void main(String[] args) {
Value value1 = new Value(10);
Value value2 = new Value(20);
System.out.println("交换前: value1 = " + value1.value + " value2 = " + value2.value );
swap(value1 ,value2 );
System.out.println("交换后: value1 = " + value1.value + " value2 = " + value2.value );
}
}
//结果为:
//交换前: value1 = 10 value2 = 20
//交换后: value1 = 20 value2 = 10
Clonable接口和深拷贝
java中内置了一些很有用的接口,Clonable就是其中之一。
Object类中存在一个clone方法,调用这个方法可以创建一个对象的“拷贝”,但是要想合法调用clone方法,必须要实现Clonable接口,否则就会抛出CloneNotSupportedException异常。
clone方法的源代码如下:
protected native Object clone() throws CloneNotSupportedException;
由protected修饰的,是Object类中的方法,限定再同一个包或不同包的子类中调用,我们想要调用需要我们在其子类进行重写该方法。
返回值是Object方法,所以要对进行强转后赋值给克隆对象。
该方法会抛出异常,我们有两种方式,依然在我们的方法后抛出异常,提醒调用者该方法会抛出异常,我们也可以处理该异常,具体细节我们将在后续学习异常进行理解。
我们再来看看Clonable接口代码
public interface Cloneable {
}
它是一个空接口,也叫做标记接口,表示当前类是可以被克隆的
public class Person implements Cloneable{
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
protected Object clone() throws CloneNotSupportedException{
return super.clone();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException{
Person person1 = new Person("张三",18);
Person person2 = (Person) person1.clone();
System.out.println(person1.toString());
System.out.println(person2.toString());
}
}
//结果为:
//Person{name='张三', age=18}
//Person{name='张三', age=18}
我们也可以对异常进行处理
public class Person implements Cloneable{
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
protected Object clone(){
Object o = null;
try{
o = super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args){
Person person1 = new Person("张三",18);
Person person2 = (Person) person1.clone();
System.out.println(person1.toString());
System.out.println(person2.toString());
}
}
//结果为:
//Person{name='张三', age=18}
//Person{name='张三', age=18}
以上Cloneable拷贝出的对象是一份浅拷贝
浅拷贝和深拷贝
上述代码就是将person1所指对象克隆一份出来
那我们再来看一段代码
public class Money {
public double m = 9.9;
}
public class Person implements Cloneable{
public String name;
public int age;
Money money = new Money();
public Person(String name, int age) {
this.name = name;
this.age = age;
}
protected Object clone(){
Object o = null;
try{
o = super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args){
Person person1 = new Person("张三",18);
Person person2 = (Person) person1.clone();
System.out.println("修改前person1:" + person1.money.m);
System.out.println("修改前person2:" + person2.money.m);
person2.money.m = 99.9;
System.out.println("修改后person1:" + person1.money.m);
System.out.println("修改后person2:" + person2.money.m);
}
}
//结果为:
//修改前person1:9.9
//修改前person2:9.9
//修改后person1:99.9
//修改后person2:99.9
上述代码克隆了Person对象,没有克隆Money对象。所以当我们对person2的money中的m值进行修改,person1中money的m值也发生了改变,这就是浅拷贝。
如果对Money对象也进行拷贝,那就是深拷贝
我们要进行深拷贝,对Money对象进行拷贝,就需要我们去修改clone方法,并对Money类实现Cloneable接口,并重写clone方法。
public class Money implements Cloneable{
public double m = 9.9;
protected Object clone() {
Object o = null;
try{
o = super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
public class Person implements Cloneable{
public String name;
public int age;
Money money = new Money();
public Person(String name, int age) {
this.name = name;
this.age = age;
}
protected Object clone(){
Person tmp = null;
try{
tmp = (Person) super.clone();
tmp.money = (Money) this.money.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return tmp;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args){
Person person1 = new Person("张三",18);
Person person2 = (Person) person1.clone();
System.out.println("修改前person1:" + person1.money.m);
System.out.println("修改前person2:" + person2.money.m);
person2.money.m = 99.9;
System.out.println("修改后person1:" + person1.money.m);
System.out.println("修改后person2:" + person2.money.m);
}
}
//结果为:
//修改前person1:9.9
//修改前person2:9.9
//修改后person1:9.9
//修改后person2:99.9
此时完成的就是深拷贝。
抽象类和接口的区别
核心区别:抽象类中可以包含普通方法和普通字段,这样的普通方法和字段可以被子类使用(不必重写),而接口中不能表示普通方法,子类必须重写所有的抽象方法。
如我们之前写的Animal类,此处的Animal中包含一个name属性,这个属性在任何子类中都是存在的,因此此处的Animal只能作为一个抽象类,而不应该成为一个接口。
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
抽象类存在的意义就是为了让编译器更好的校验,像Animal这样的类我们并不会直接使用,而是使用它的子类,万一不小心创建了Animal的实例,编译器会及时提醒我们的。
No | 区别 | 抽象类(abstract) | 接口(interface) |
---|---|---|---|
1 | 结构组成 | 普通类+抽象方法 | 抽象方法+全局常量 |
2 | 权限 | 各种权限 | public |
3 | 子类使用 | 使用extends关键字继承抽象类 | 使用implements关键字实现接口 |
4 | 关系 | 一个抽象类可以实现多个接口 | 接口不能继承抽象类,但是接口可以使用extends继承多个父接口 |
5 | 子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
我们对于抽象类和接口先学习到这,后续我们将继续一起学习和了解更多有趣的内容~~