Java 类和对象——抽象类、接口、Object类

本文章继续来介绍类和对象的知识。重点介绍抽象类和接口,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类来接收所有的数组类型,比如类,数组,接口等。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值