文章目录
抽象类抽象类概念抽象类语法抽象类要点总结 接口接口的概念接口实例接口间的多继承接口要点总结Comparable接口Comparator接口Clonable接口深拷贝的代码实现 抽象类和接口的区别
给个关注叭
个人主页
JavaSE专栏
前言:本篇文章主要整理了抽象类的概念及语法、抽象类要点总结.接口的概念,通过实例进一步理解接口、接口间的多继承、接口要点总结、Comparable接口、Comparator接口、Cloneable接口、浅拷贝和深拷贝代码实现及其堆栈图、抽象类和接口的区别。
抽象类
抽象类概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类就是抽象类。
比如:
在打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法, 包含抽象方法的类我们称为 抽象类
抽象类语法
抽象类需要用abstract
的关键字来修饰类,同时abstract
关键字也可以修饰方法,被abstract
修饰的方法称为抽象方法,抽象方法可以没有具体的实现。代码示例如下:
// 抽象类:被abstract修饰的类public abstract class Shape {// 抽象方法:被abstract修饰的方法,没有方法体 abstract public void draw(); abstract void calcArea();// 抽象类也是类,也可以增加普通方法和属性 public double getArea(){ return area; } protected double area; // 面积}
抽象类首先是一个类,和普通类一样,可以有普通方法和变量,甚至构造方法,可以被继承,唯一不同的是抽象类不能实例化对象
抽象类要点总结
被abstract
修饰的类称为抽象类,被abstract
修饰的方法称为抽象方法,抽象方法可以没有具体的实现抽象类和普通类一样,可以定义普通类中的成员变量和成员方法,构造方法抽象类不能实例化对象如果一个类中包含抽象方法,那么这个类一定是抽象类(被abstract
修饰)抽象类就是用来被继承的,继承后,子类一定要重写抽象类中的所有抽象方法,不然会有红线
在子类B中要重写抽象类A中的所有抽象方法,代码示例如下:
@Override public void func1() { }
如果不想重写抽象类的抽象方法,那么可以同样用abstract
修饰子类;但是“出来混迟早是要还的”,如果再有一个类继承了这个类(已经继承了抽象类的类),那么必须重写这两个类中所有的抽象方法。
这里虽然B类使用abstract
关键字修饰来避免了重写父类A中的抽象方法,但是C类继承了B类,C类还是要把A类和B类中的所有抽象方法全部重写
代码示例如下:
class C extends B { @Override public void func1() { } @Override public void func2() { }}
抽象方法既然要被重写,就要遵循重写的规则,抽象方法不能被final、private、static修饰 被private关键字修饰后,说明这个变量或方法只能在当前类中被访问。有关private
关键字的更多介绍,请移步 封装.访问限定符被static关键字修饰后,说明这个变量或方法是当前类的变量或方法,不属于某一个具体的对象,是所有对象共享的;随类的加载而创建,随类的销毁而卸载;而且只随类的创建加载一次,再次加载类或者new一个对象时,不会再次加载。有关static
关键字的更多介绍,请移步 封装.static 接口
接口的概念
接口类似一种功能,只要某个事物具备这样的功能,都可以实现这个接口。(公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。)
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
接口实例
实例一、
定义IShpe接口,Rect、Triangle、Cycle三个类实现IShape接口
public interface IShape { void draw();//默认被public abstract修饰,建议不加,代码更简洁}
public class Rect implements IShape{ @Override public void draw() { System.out.println("画一个矩形..."); }}
public class Triangle implements IShape{ @Override public void draw() { System.out.println("画一个三角形..."); }}
public class Cycle implements IShape{ @Override public void draw() { System.out.println("画一个圆..."); }}
public class Test { public static void drawShape(IShape shape) { shape.draw(); } public static void main(String[] args) { IShape rect = new Rect(); IShape triangle = new Triangle(); IShape cycle = new Cycle(); drawShape(rect); drawShape(triangle); drawShape(cycle); }}
运行结果:
画一个矩形…
画一个三角形…
画一个圆…
结果表明,向上转型也可以体现在类实现接口的关系上(同时也体现出了动态绑定和多态)
实例二、
实现笔记本电脑使用USB鼠标、USB键盘
public interface IUsb { void openDevice(); void closeDevice();}
鼠标类:实现USB接口,并具备点击功能 public class Mouse implements IUsb{ @Override public void openDevice() { System.out.println("连接鼠标..."); } @Override public void closeDevice() { System.out.println("关闭鼠标..."); } public void click() { System.out.println("点击鼠标..."); }}
键盘类:实现USB接口,并具备输入功能 public class KeyBoard implements IUsb{ @Override public void openDevice() { System.out.println("连接键盘..."); } @Override public void closeDevice() { System.out.println("关闭键盘..."); } public void input() { System.out.println("输入数据..."); }}
笔记本类:包含开机功能、关机功能、使用USB设备功能 public class Computer { public void openPower() { System.out.println("打开电脑..."); } public void closePower() { System.out.println("关闭电脑..."); } public void IUsbService(IUsb usb) { usb.openDevice(); if(usb instanceof Mouse) { Mouse mouse = (Mouse) usb; mouse.click(); }else if(usb instanceof KeyBoard) { KeyBoard keyBoard = (KeyBoard) usb; keyBoard.input(); } usb.closeDevice(); }}
测试类 public class Test { public static void main(String[] args) { Computer computer = new Computer(); computer.openPower(); //使用鼠标 computer.IUsbService(new Mouse()); //使用键盘 computer.IUsbService(new KeyBoard()); computer.closePower(); }}
运行结果:
打开电脑…
连接鼠标…
点击鼠标…
关闭鼠标…
连接键盘…
输入数据…
关闭键盘…
关闭电脑…
实例三、
一个类实现多个接口,定义Animal、Dog、Bird、Duck类,定义IRunable、ISwimable、IFlyable接口。让这些动物类实现合适的一个或多个接口
接口定义:
public interface IRunning { void run();}public interface ISwimming { void swim();}public interface IFlying { void fly();}
各种类的定义
public class Animal { String name; int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println("正在吃..."); }}
下面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
public class Dog extends Animal implements IRunning,ISwimming{ public Dog(String name,int age) { super(name,age); } @Override public void run() { System.out.println(this.name + " 正在跑..."); } @Override public void swim() { System.out.println(this.name + " 正在游..."); }}
public class Bird extends Animal implements IFlying{ public Bird(String name,int age) { super(name,age); } @Override public void fly() { System.out.println(this.name + " 正在飞..."); }}
public class Duck extends Animal implements IRunning,ISwimming,IFlying{ public Duck(String name,int age) { super(name,age); } @Override public void run() { System.out.println(this.name + " 正在跑..."); } @Override public void swim() { System.out.println(this.name + " 正在游..."); } @Override public void fly() { System.out.println(this.name + " 正在飞..."); }}
测试类
public class Test { public static void main(String[] args) { Dog dog = new Dog("旺财",3); Bird bird = new Bird("小飞飞",1); Duck duck = new Duck("唐老鸭",5); walk(dog); walk(duck); fly(bird); fly(duck); } public static void walk(IRunning iRunning) { iRunning.run(); } public static void fly(IFlying iFlying) { iFlying.fly(); }}
运行结果:
旺财 正在跑…
唐老鸭 正在跑…
小飞飞 正在飞…
唐老鸭 正在飞…
run方法和fly方法,使用接口这种引用类型作为参数,可以不用关注类的调用者的具体类型,只要这个类具备这个功能特性(实现了这个接口),就可以使用
接口间的多继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的.比如接口C 继承接口A和接口B,那C就具A和B的功能特性,当一个类实现这个接口C时,就要重写A、B、C接口中所有的方法
interface A { void testA();}interface B { void testB();}interface C extends A,B{ void testC();}public class Test implements C{ @Override public void testA() { } @Override public void testB() { } @Override public void testC() { }}
接口要点总结
接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口创建接口时, 接口的命名一般以大写字母 I 开头.接口的命名一般使用 “形容词” 词性的单词.阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性接口中不能定义构造方法,也不能定义有具体实现的方法(否则会有红线)接口中如果要定义有具体实现的方法,要用static 或 default 关键字修饰接口中可以定义变量,默认被public static final 修饰,为静态常量(如果用默认值以外的修饰符修饰,会有红线)接口中可以定义没有具体实现的方法,默认被public abstract修饰,为抽象方法(如果用默认值以外的修饰符修饰,会有红线)接口中变量 或 方法可以不加任何限定修饰符,它们都有自己的默认修饰符接口和抽象类一样,不能直接实例化对象,接口是用来被类实现的一个类实现接口,使用 implements关键字,代表类实现了接口实现接口的类,要在类中重写接口中所有的(抽象)方法如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类一个类可以实现多个接口,中间用逗号隔开接口与接口之间也可以存在继承关系,接口与接口之间可以多继承Comparable接口
在Comparable接口源码中,有一个compareTo方法
现有一个学生类,需要对两个学生对象 按照年龄 进行比较。不能使用直接使用 >、<或== 来比较两个学生引用。
应该让Student类实现Comparable接口,然后在Student中 按照比较需求 重写这个接口中的compareTo方法。代码示例如下:
public class Student implements Comparable<Student>{ String name; int age; public Student(String name,int age) { this.name = name; this.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.age-o.age}
测试类:
public class Test { public static void main(String[] args) { Student student1 = new Student("zhangSan",18); Student student2 = new Student("liSi",17); int ret = student1.compareTo(student2); if(ret > 0) { System.out.println("student1 > student2"); }else if(ret == 0) { System.out.println("student1 == student2"); }else { System.out.println("student1 < student2"); } }}
运行结果:
student1 > student2
使用Arrays.sort() 对学生类数组students进行排序,下图是Arrays.sort() 的源码,可以看出Arrays.sort() 的底层也是使用了comparable接口,所以Student类必须要实现comparable接口,并根据比较需求重写compareTo方法。
【students数组中的每个元素都是学生对象,比较时也是通过冒泡排序的方式,源码中要对学生对象强转为comparable接口,并且使用compareTo方法,所以Student类必须实现comparable接口】
Student 类实现comparable接口,重写compareTo方法,使用Arrays.sort(students)对学生数组按照年龄排序,代码示例如下:
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) { return this.age - o.age; }}
public class Test { public static void main(String[] args) { Student student1 = new Student("zhangSan",18); Student student2 = new Student("liSi",17); Student student3 = new Student("aBao",13); Student[] students = new Student[3]; students[0] = student1; students[1] = student2; students[2] = student3; Arrays.sort(students); System.out.println(Arrays.toString(students)); }}
运行结果:
[Student{name=‘aBao’, age=13}, Student{name=‘liSi’, age=17}, Student{name=‘zhangSan’, age=18}]
Student 类实现comparable接口,重写compareTo方法,使用Arrays.sort(students)对学生数组按照姓名排序,代码示例如下:
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) { return this.name.compareTo(o.name); }}
运行结果:
[Student{name=‘aBao’, age=13}, Student{name=‘liSi’, age=17}, Student{name=‘zhangSan’, age=18}]
只需改动compareTo方法的比较方式,测试类Test不变
【点进String类的源码,其实String类中也实现了Comparable接口,所以String类也重写了compareTo方法,因此对于String这种类的引用是可以调用compareTo方法,来比较两个引用类型的大小的】
使用冒号排序BubbleSort()实现 Arrays.sort(),按照姓名进行比较排序,代码示例如下:
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) { return this.name.compareTo(o.name); }}
public class Test { public static void main(String[] args) { Student student1 = new Student("zhangSan",18); Student student2 = new Student("liSi",17); Student student3 = new Student("aBao",13); Student[] students = new Student[3]; students[0] = student1; students[1] = student2; students[2] = student3; bubbleSort(students); System.out.println(Arrays.toString(students)); public static void bubbleSort(Student[] students) { for (int i = 0; i < students.length-1; i++) { for (int j = 0; j < students.length-1-i; j++) { if(students[j].compareTo(students[j+1]) > 0) { Student tmp = students[j]; students[j] = students[j+1]; students[j+1] = tmp; } } } }}
运行结果:
[Student{name=‘aBao’, age=13}, Student{name=‘liSi’, age=17}, Student{name=‘zhangSan’, age=18}]
students[j].compareTo(students[j+1]
能够使用compareTo()比较的原因是,数组的每个元素都是Student类对象,而学生类实现了comparable接口,同时重写了compareTo方法
使用冒号排序BubbleSort()实现 Arrays.sort(),按照年龄从小到大进行比较排序,只需更改compareTo方法里面的比较方式。代码示例如下:
@Override public int compareTo(Student o) { return this.age - o.age; }
如果想要按照年龄从大到小排序,只需将 return this.age - o.age
改为return o.age - this.age
Comparator接口
Comparable接口的缺点:如果一个类已经实现了Comparable接口,并且根据需要已经重写了compareTo抽象方法,那么一般这个已经改写的抽象方法就不会再变了,比如说之前的业务都是使用年龄age来比较,但是如果确实需要通过姓名比较,也不能把原来已经重写好的方法再次改写按照姓名比较,这样一来以前使用年龄age比较的所有业务都会出错。那么我们可以使用一个新的接口Comparator接口,可以设置两种不同比较方式的类,让两个类都实现Comparator接口,分别按照比较需求重写Comparator中的抽象方法。
以下是comparator接口的源码,可以看到有两个抽象方法
这个接口里面有两个抽象方法,但是一个类实现了这个接口后只重写compare()方法就可以了,也不会报错。这是为啥子呢?解释:我们知道Object类是所有类的父类,其实在Object类中就有equals()方法的具体实现,而作为子类的AgeComparator也实现了Comparator接口,可以认为子类已经从父类继承了equals方法,近似认为已经重写了equals()方法,所以不需要再次重写equals()方法。
使用AgeComparator类中重写的compare()方法按照年龄比较,代码示例如下:
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 student1 = new Student("zhangSan",18); Student student2 = new Student("liSi",17); AgeComparator ageComparator = new AgeComparator(); int ret = ageComparator.compare(student1,student2); if(ret > 0) { System.out.println("student1 > student2"); }else if(ret == 0) { System.out.println("student1 == student2"); }else { System.out.println("student1 < student2"); } }}
运行结果:
student1 > student2
使用NameComparator类中重写的compare()方法按照姓名比较,代码示例如下:
public class NameComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.name.compareTo(o2.name); }}
public class Test { public static void main(String[] args) { Student student1 = new Student("zhangSan",18); Student student2 = new Student("liSi",17); NameComparator nameComparator = new NameComparator(); int ret = nameComparator.compare(student1,student2); if(ret > 0) { System.out.println("student1 > student2"); }else if(ret == 0) { System.out.println("student1 == student2"); }else { System.out.println("student1 < student2"); } }
运行结果:
student1 > student2
Clonable接口
现通过一个实例总结使用clone()的注意事项;有一个Goods类,代码示例如下:
class Money { public double money = 9.9;}public class Goods { public String name; public Money money; public Goods(String name) { this.name = name; this.money = new Money(); }}
现需要创建一个Goods对象,并克隆这个对象。在Test类中,创建好了一个goods对象,当用这个对象的引用通过 点 访问clone()方法时,但实际 点 不出来clone()方法,此时可以在子类Goods中重写clone()方法,这时候就能 点出来了。这是因为Object类中已经实现了clone()方法,所以作为子类的Goods类,重写这个方法后一定可以被访问。
但是我们发现还是会有红线,此时阅读Cloneable的源代码可以发现:
1 . 返回值是Object,所以要把Object类型的赋值给左边Goods类型的,必须要对右边进行强转为(Goods)。
public static void main(String[] args) { Goods goods = new Goods("苹果"); Goods goods1 = (Goods)goods.clone(); }
强转之后发现还会有红线,这是因为throw CloneNotSuppurtedException
是一个不支持克隆的异常,需要在编译时解决异常,解决异常的方法是:main()方法后 加throw CloneNotSuppurtedException
。 public static void main(String[] args) throws CloneNotSupportedException { Goods goods = new Goods("苹果"); Goods goods1 = (Goods)goods.clone(); }
解决当前类是否能被克隆,编译通过但运行时报错,是因为没有实现Cloneable接口,实现Cloneable接口后才能真正实现拷贝, 阅读Cloneable接口的源码发现,其内部没有任何方法,
那么实现这个接口有什么用呢?它只是用来标记当前类是可以被拷贝的
完整代码示例如下:
class Money { public double money = 9.9;}public class Goods implements Cloneable{ public String name; public Money money; public Goods(String name) { this.name = name; this.money = new Money(); } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}
public class Test { public static void main(String[] args) throws CloneNotSupportedException { Goods goods = new Goods("苹果"); Goods goods1 = (Goods)goods.clone(); }}
public class Test { public static void main(String[] args) throws CloneNotSupportedException { Goods goods = new Goods("苹果"); Goods goods1 = (Goods)goods.clone(); System.out.println("改变前goods money = " +goods.money.money ); System.out.println("改变前goods1 money = " +goods1.money.money ); goods1.money.money = 66.6; System.out.println("改变后goods money = " +goods.money.money ); System.out.println("改变后goods1 money = " +goods1.money.money ); }}
运行结果:
改变前goods money = 9.9
改变前goods1 money = 9.9
改变后goods money = 66.6
改变后goods1 money = 66.6
通过以上测试类的结果说明,Money这个类的对象并没有被真正拷贝,新拷贝的goods1中的money这个引用还是指向了原来的地址,这是浅拷贝
使用堆栈图更能清晰的表示:
此时的goods1 和 goods 中的money引用所指向的对象是同一个对象(引用所存放的地址相同),所以在通过goods.money.money = 66.6
这条语句改变money的值时,goods中的money也会改变,money引用指向的地址都是同一个地址
深拷贝的代码实现
以下是实现深拷贝的具体代码实现:
class Money implements Cloneable{ public double money = 9.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class Goods implements Cloneable{ public String name; public Money money; public Goods(String name) { this.name = name; this.money = new Money(); } @Override protected Object clone() throws CloneNotSupportedException { Goods tmp = (Goods)super.clone(); tmp.money = (Money)this.money.clone(); return tmp; }}
public class Test { public static void main(String[] args) throws CloneNotSupportedException { Goods goods = new Goods("苹果"); Goods goods1 = (Goods)goods.clone(); System.out.println("改变前goods money = " +goods.money.money ); System.out.println("改变前goods1 money = " +goods1.money.money ); goods1.money.money = 66.6; System.out.println("改变后goods money = " +goods.money.money ); System.out.println("改变后goods1 money = " +goods1.money.money ); }}
运行结果:
改变前goods money = 9.9
改变前goods1 money = 9.9
改变后goods money = 9.9
改变后goods1 money = 66.6
操作的堆栈图:
先拷贝一份goods 给 tmp,然后再对money这个引用的对象进行拷贝,把拷贝好的money对象的地址 给 tmp.money。这里money引用也进行拷贝,所以Money也要实现Cloneable接口,重写clone()方法。
注意:从代码层次上,浅拷贝或深拷贝 是由具体的代码实现来决定的,不能说某个clone()方法是浅拷贝或深拷贝