本文章继续来介绍类和对象的知识。重点介绍抽象类和接口,Object类只做简单介绍。
现在,定义一个Shape类(形状类),当不同的对象去调用的时候,就会画出不同的图形,使用圆这个对象去调用,就会画出⚪来,使用三角形这个对象去调用,就会画出▲来。使用多态的思想来完成。
class Shape {
public void draw() {
System.out.println("画形状");
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画⚪;");
}
}
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("画▲;");
}
}
public class test_04_07_02 {//本人的主类
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Cycle cycle = new Cycle();
Triangle triangle = new Triangle();
drawMap(cycle);
drawMap(triangle);
}
}
我们发现,在Shape类中的draw()方法是不需要有具体的实现的。具体的实现可以在子类中实现。如果直接在父类中删去draw()方法的具体实现会报错。这,就引出了抽象类的概念。
一、抽象类
1、什么是抽象类
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。比入上面的Shape类。里面的draw()方法没有什么具体的工作,它的任务都是子类来完成的。
2、抽象类语法
//抽象类
abstract class Shape {
public void abstractCommonMethod() {
System.out.println("抽象类普通方法");
}
//抽象方法
public abstract void draw();
}
一个类,被abstract修饰,叫抽象类。一个方法,被abstract修饰,称为抽象方法。abstract不能修饰成员变量,同时,被abstract修饰的方法不能有具体的实现,否则会报错。
3、抽象类特点
(1)抽象类不能被实例化。
(2)一个抽象类除了加abstract关键字和类中的abstract方法不能有具体的实现外,其他的和普通类没有区别。
(3)一个普通的类继承了抽象类,需要在普通类中重写这个抽象类的所有抽象方法。否则,这个普通的类也是一个抽象类,需要用abstract来修饰。
(4)抽象类可以向上转型,进一步发生多态。
(5)一个抽象类B继承了抽象类A,可以不用重写抽象类A中的方法。
(6)当一个普通类继承了第(5)中的抽象类B,要重写所有的抽象方法。子类继承了抽象父类,抽象父类又继承了抽象父类(暂时称为爷爷类),那么这个子类要重写父类和爷爷类中的所有的抽象方法。
(7)final不能修饰abstract修饰的方法和类。final修饰类意味着不能被继承,而抽象类最大的作用就是为了被继承。
(8)抽象方法默认权限是public。不能在抽象方法中使用private权限。
(9)一个抽象类中不一定有抽象方法,但是一个方法中是抽象方法,这个类一定是抽象类。
(10)抽象类可以有构造方法,让子类创建父类的时候,初始化父类成员变量。
4、抽象类的作用
抽象类最大的作用是被继承,普通类也可以被继承。但是使用抽象类相当于多了一重的校验。Java的很多语法存在的意义是为了“预防出错”,这也是为什么Java是比较安全的原因之一。如果本来是子类完成的任务,不小心误用了父类完成,编译器是不会报错的。使用抽象的父类,编译器就会报错,让我们尽早发现问题。
二、接口
1、什么是接口
生活中的接口很常见,比如计算机上的接口、插座等等。这些接口是一种公共的行为规范标准。接口是公共的行为规范标准。大家在实现的时候,只要符合规范标准,就可以通用。Java中,接口可以看成多个类的公共规范,是一种引用数据类型。接口的出现是为了解决Java中一个类不能有多继承的情况。
2、语法
public interface 接口名称{//也可以不写public
// 抽象方法
public abstract void method1();
public void method2();
abstract void method3();
void method4();
}
将class换成interface关键字,就定义了一个接口。
现在将上面的Shape类换成接口。
//接口
interface IShape {
public abstract void draw();
}
3、接口特性
(1)接口中的方法只能是抽象方法。
(2)接口中的成员方法默认是public abstract,成员变量默认是public static final,而且需要初始化。
在编译器上,这里的public abstract 和 public static final是灰色的,说明它们是没有用的,可以省去。
(3)接口中的方法如果想要实现,就需要default来修饰。
(4)接口中的静态方法可以有具体的实现。
(5) 一个类想要实现接口,使用implements关键字,同时,要重写接口中的所有抽象方法。
(6)接口不能被实例化。
(7) 一个类可以实现多个接口,使用“,”来隔开。
(8)类和接口之间的关系是implements,接口和接口之间的关系是extends。
interface C extends A,B {}
表示C拓展了A和B的方法,对C实例化,要同时重写A、B和C的所有抽象方法。
(9)接口不是类,但是接口编译完成后的字节码文件后缀也是.class。
现在我们写一个Animal类,Animal类中有name属性和age属性,同时还有一个eat方法(假设给每个动物都起了名字和年龄。每一个动物都要进食)。在写一个Duck类(鸭子类),Duck会跑,会吃,会游泳,还会飞。使用封装、继承、接口来完成。
class Animal {
//动物有名字和年龄
protected String name;
protected int age;
//构造方法
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
//动物有进食的本能
public void eat() {
System.out.println(this.name + " 进食");
}
}
//飞
interface IFlying {
void fly();
}
//跑
interface IRunning {
void run();
}
//游泳
interface ISwimming {
void swimming();
}
class Duck extends Animal implements IRunning,ISwimming, IFlying {
public Duck(String name, int age) {
super(name, age);
}
@Override
public void fly() {
System.out.println(this.name + " 在飞");
}
@Override
public void run() {
System.out.println(this.name + " 在跑");
}
@Override
public void swimming() {
System.out.println(this.name + " 在游泳");
}
}
//------------以上是类的实现者写的代码------------
//------------以下是类的调用者写的代码------------
public class test_04_07_06 {//本人的主类
public static void fun(Duck duck) {
((IFlying) duck).fly();
((ISwimming) duck).swimming();
((IRunning) duck).run();
duck.eat();
}
public static void main(String[] args) {
Duck duck = new Duck("鸭子",1);
duck.run();
fun(duck);
}
}
4、接口的实例
(1)使用Comparable实现对象数组的比较
现在写一个学生类,包括姓名和年龄,给对象数组按照年龄从小到大排序。
class Student {
protected String name;
protected int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "[" + name + ',' + age +
']';
}
}
public class test_04_08_01 {//本人主类
public static void main(String[] args) {
Student[] stu = new Student[3];
stu[0] = new Student("zhangsan", 16);
stu[1] = new Student("lisi", 18);
stu[2] = new Student("wangwu", 14);
Arrays.sort(stu);
System.out.println(Arrays.toString(stu));
}
}
报错说学生类不能被强制转换为Comparable类。查看源码发现,它的底层实现是compareTo,所以要在学生类实现这个Compare接口,在类中重写这个方法。
这样,就能够实现对学生类进行排序。 但是这样写不够灵活。在代码中使用了年龄比较,就只能年龄比较,使用姓名比较,就只能姓名比较。能不能既可以按年龄比较,还能换成按姓名比较呢?我们采用比较器来比较。
(2)使用Comparator实现对象数组的比较
class Student {
protected String name;
protected int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "[" + name + ',' + age +
']';
}
}
//年龄比较器
class AgeComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
//姓名比较器
class NameComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
public class test_04_08_01 {
public static void main(String[] args) {
Student[] stu = new Student[3];
stu[0] = new Student("zhangsan", 16);
stu[1] = new Student("lisi", 18);
stu[2] = new Student("wangwu", 14);
AgeComparator ageComparator = new AgeComparator();
Arrays.sort(stu, ageComparator);
System.out.println(Arrays.toString(stu));
NameComparator nameComparator = new NameComparator();
Arrays.sort(stu, nameComparator);
System.out.println(Arrays.toString(stu));
}
}
(3)实现对象数组的拷贝
现在有这么一个想法,把其中一个学生对象给复制拷贝一下,能不能办到呢?数组是可以使用Arrays.clone()方法来实现数组的拷贝的,实例化的对象数组该如何拷贝?
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 ClassStudent {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("zhangsan", 18);
}
}
在克隆的时候,首先,这个对象要能够被拷贝,也就是说,这个Student类要实现一个Cloneable接口。这个接口是一个空的接口,也叫标记接口,表示当前的类能够被拷贝。
根据前面个两个接口的经验,在这里,我们还是要重写一个方法clone()方法。
重写后的方法返回类型是一个Object类,它是所有类的父类。那么这里的students[0]就相当于是students[1]的父类,把父类给子类了,所以要进行强制类型转换。
在打印studets[1],就可以看见它克隆了students[0]。这个拷贝的students[1]是students[0]的副本。现在我又想写一个Money类,表示一个学生一个月的零花钱(假设是100元),在学生类中实例化这个Money类,然后进行拷贝。
class Money {
public int money = 100;
}
class Student implements Cloneable {
public String name;
public int age;
public Money m = new Money();
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name=" + name + '\'' +
", age=" + age +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
对students[0]的m重新赋值,在打印students[0]和students[1]看看情况。
发现,改了students[0]的,拷贝过去却连students[1]的也改了。这种拷贝实际上是一种浅拷贝。
从这个大致的内存引用图可以看出来,要让students[1]的m不指向students[0]的m,也要对Money类实现cloneable接口,然后重写Money类中的clone()方法。
①中,这个super.clone()表示用Object类的Clone()方法克隆出来一个副本,然后用tmp来接收。这个时候,虽然克隆拷贝出来了students[0]的副本,但是这个副本的m还是指向students[0]的m。通过②就可以在students[1]中克隆一份新的m(this是students[0]),然后给tmp中的m,这个过程需要进行强制类型转换。最后,返回的tmp被students[1]接收,让students[1]引用指向tmp指向的对象。
这样,就实现了深拷贝。实现深拷贝,并没有使用什么特定的深拷贝方法,而是在代码逻辑上去思考的,出现引用的地方,这个对象的引用所指的对象也进行了重写clone方法。
5、抽象类和接口区别
三、Object类
前面在clone()方法中我们说过,Object是所有的类的父类,所有的类都继承了Object类。所以在使用一些方法的时候,需要子类里面重写方法,比如打印对象的toString方法。子类是继承了Object类里toString方法,但是Object类的toString方法是打印这个类实例化对象的引用的哈希值。想要用这个方法来打印引用的内容,就需要重写Object类中的toString方法。
可以使用Object类来接收所有的数组类型,比如类,数组,接口等。