信息发布→ 登录 注册 退出

Java入门基础之抽象类与接口

发布时间:2026-01-11

点击量:
目录
  • 一.抽象类
    • 1.什么是抽象类
    • 2.语法规则
    • 3.抽象类的作用
  • 二.接口
    • 1.什么是接口
    • 2.语法规则
    • 3.实现多个接口
    • 4.接口之间的继承
  • 三.接口的使用实例
    • 1. Comparable 接口
    • 2.Comparator接口
    • 3.Clonable接口
  • 四.总结

    一.抽象类

    1.什么是抽象类

    首先我们来回顾一下上一篇文章提到的一个例子:打印图形

    class Shape { 
     	public void draw() { 
     		// 啥都不用干
     	} 
    } 
    class Cycle extends Shape { 
     	@Override 
     	public void draw() { 
     		System.out.println("○"); 
     	} 
    } 
    class Rect extends Shape { 
     	@Override 
     	public void draw() { 
     		System.out.println("□"); 
     	} 
    } 
    class Flower extends Shape { 
     	@Override 
     	public void draw() { 
     		System.out.println("♣"); 
     	} 
    } 
    /我是分割线// 
    
    public class Test { 
     	public static void main(String[] args) { 
     		Shape shape1 = new Flower(); 
     		Shape shape2 = new Cycle(); 
     		Shape shape3 = new Rect(); 
     		drawMap(shape1); 
     		drawMap(shape2); 
     		drawMap(shape3); 
     	} 
     	// 打印单个图形
     	public static void drawShape(Shape shape) { 
     		shape.draw(); 
     	} 
    }
    
    

    我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作,主要的绘制图形都是由 Shape 的各种子类的 draw 方法来完成的。
    像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstractmethod),包含抽象方法的类我们称为 抽象类(abstract class)

    2.语法规则

    那么,抽象类到底怎么写呢?请看代码:

    abstract class Shape { 
     	abstract public void draw(); 
    }
    

    在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法。 同时抽象方法没有方法体(没有 { },不能执行具体代码)

    对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类

    注意事项:

    抽象类不能直接实例化:

    Shape shape = new Shape(); 
    // 编译出错
    Error:(30, 23) java: Shape是抽象的; 无法实例化
    

    抽象方法不能是 private 的:

    abstract class Shape { 
     	abstract private void draw(); 
    } 
    // 编译出错
    Error:(4, 27) java: 非法的修饰符组合: abstract和private
    

    抽象类中可以包含其他的非抽象方法,也可以包含字段。这个非抽象方法和普通方法的规则都是一样的,可以被重写,也可以被子类直接调用:

    abstract class Shape { 
     	abstract public void draw(); 
     	void func() { 
     		System.out.println("func"); 
     	} 
    } 
    class Rect extends Shape { 
     
    } 
    public class Test { 
     	public static void main(String[] args) { 
     		Shape shape = new Rect(); 
     		shape.func(); 
     	} 
    } 
    // 执行结果
    func
    

    3.抽象类的作用

    抽象类存在的最大意义就是为了被继承

    抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。

    那大家可能有一个疑问,普通的类也可以被继承, 普通的方法也可以被重写呀,为啥非得用抽象类和抽象方法呢?

    确实如此,但是使用抽象类相当于多了一重编译器的校验:

    使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成。

    那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。 但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。

    很多语法存在的意义都是为了 “预防出错”,例如我们曾经用过的 final 也是类似。 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候,让编译器及时提醒我们。
    充分利用编译器的校验, 在实际开发中是非常有意义的。

    二.接口

    1.什么是接口

    接口是抽象类的更进一步。抽象类中还可以包含 非抽象方法 和字段。而接口中包含的方法都是抽象方法, 字段只能包含静态常量

    2.语法规则

    在刚才的打印图形的示例中,我们的父类 Shape 并没有包含别的非抽象方法,也可以设计成一个接口:

    interface IShape { 
     	void draw(); 
    } 
    class Cycle implements IShape { 
     	@Override 
     	public void draw() { 
     		System.out.println("○"); 
     	} 
    } 
    public class Test { 
     	public static void main(String[] args) { 
     		IShape shape = new Rect(); 
     		shape.draw(); 
     	} 
    }
    
    • 使用 interface 定义一个接口
    • 接口中的方法一定是抽象方法,因此可以省略 abstract
    • 接口中的方法一定是 public ,因此可以省略 public
    • Cycle 使用 implements 继承接口。此时表达的含义不再是 “扩展”, 而是 “实现”
    • 在调用的时候同样可以创建一个接口的引用,对应到一个子类的实例
    • 接口不能单独被实例化
    • 从jdk1.8开始,接口中的普通方法可以有具体实现,但这个方法必须是default修饰的。

    扩展(extends) vs 实现(implements):

    • 扩展指的是当前已经有一定的功能了,进一步扩充功能
    • 实现指的是当前啥都没有,需要从头构造出来

    注意事项:

    接口中只能包含抽象方法。 对于字段来说, 接口中只能包含静态常量(final static):

    interface IShape { 
     	void draw(); 
     	public static final int num = 10; 
    }
    

    其中的 public, static, final 的关键字都可以省略.省略后的 num 仍然表示 public 的静态常量

    总结:

    • 我们创建接口的时候, 接口的命名一般以大写字母 I 开头
    • 接口的命名一般使用 “形容词” 词性的单词
    • 阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号,保持代码的简洁性

    一段易错的代码:

    interface IShape { 
     	abstract void draw() ; // 即便不写public,也是public 
    } 
    class Rect implements IShape { 
     	void draw() { 
     		System.out.println("□") ; //权限更加严格了,所以无法重写
     	} 
    }
    

    3.实现多个接口

    有的时候我们需要让一个类同时继承多个父类。这件事情在有些编程语言通过 多继承 的方式来实现的。

    然而 Java 中只支持单继承, 一个类只能 extends 一个父类。但是可以同时实现多个接口 ,也能达到多继承类似的效果。

    现在我们通过类来表示一组动物:

    class Animal { 
     	protected String name; 
     
     	public Animal(String name) { 
     		this.name = name; 
     	} 
    }
    

    另外我们再提供一组接口,分别表示 “会飞的” “会跑的” “会游泳的” :

    interface IFlying { 
     	void fly(); 
    } 
    interface IRunning { 
     	void run(); 
    } 
    interface ISwimming { 
     	void swim(); 
    }
    

    接下来我们创建几个具体的动物

    猫,是会跑的 :

    class Cat extends Animal implements IRunning { 
     	public Cat(String name) { 
     		super(name); 
     	} 
     	@Override 
     	public void run() { 
     		System.out.println(this.name + "正在用四条腿跑"); 
     	} 
    }
    

    鱼,是会游的 :

    class Fish extends Animal implements ISwimming { 
     	public Fish(String name) { 
     		super(name); 
     	} 
     	@Override 
     	public void swim() { 
     		System.out.println(this.name + "正在用尾巴游泳"); 
     	} 
    }
    

    青蛙,既能跑,又能游 :

    class Frog extends Animal implements IRunning, ISwimming { 
     	public Frog(String name) { 
     		super(name); 
     	} 
     	@Override 
     	public void run() { 
     		System.out.println(this.name + "正在往前跳"); 
     	} 
     	@Override 
     	public void swim() { 
     		System.out.println(this.name + "正在蹬腿游泳"); 
     	} 
    }
    

    PS : IDEA 中使用 ctrl + i 快速实现接口

    还有一种神奇的动物,水陆空三栖,叫做 “鸭子” :

    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 static void walk(IRunning running) { 
     	System.out.println("我带着伙伴去散步"); 
     	running.run(); 
    }
    

    在这个 walk 方法内部,我们并不关注到底是哪种动物,只要参数是会跑的, 就行:

    Cat cat = new Cat("小猫"); 
    walk(cat); 
    Frog frog = new Frog("小青蛙"); 
    walk(frog); 
    // 执行结果
    我带着伙伴去散步
    小猫正在用四条腿跑
    我带着伙伴去散步
    小青蛙正在往前跳
    

    甚至参数可以不是 “动物”,只要会跑!

    class Robot implements IRunning { 
     	private String name; 
     	public Robot(String name) { 
     		this.name = name; 
     	} 
     	@Override 
     	public void run() { 
     		System.out.println(this.name + "正在用轮子跑"); 
     	} 
    } 
    Robot robot = new Robot("机器人"); 
    walk(robot); 
    // 执行结果
    机器人正在用轮子跑
    

    4.接口之间的继承

    接口可以继承一个接口,达到复用的效果.使用 extends 关键字:

    interface IRunning { 
     	void run(); 
    } 
    interface ISwimming { 
     	void swim(); 
    } 
    // 两栖的动物, 既能跑, 也能游
    interface IAmphibious extends IRunning, ISwimming { 
    
    } 
    class Frog implements IAmphibious { 
     
    }
    

    通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”

    此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法,接口间的继承相当于把多个接口合并在一起

    三.接口的使用实例

    1. Comparable 接口

    刚才的例子比较抽象, 我们再来一个更能实际的例子,给对象数组排序 :

    给定一个学生类

    class Student { 
     	private String name; 
     	private int score; 
     	public Student(String name, int score) { 
     		this.name = name; 
     		this.score = score; 
     	} 
    
     	@Override 
     	public String toString() { 
     		return "[" + this.name + ":" + this.score + "]"; 
     	} 
    }
    

    再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序):

    Student[] students = new Student[] { 
     new Student("张三", 95), 
     new Student("李四", 96), 
     new Student("王五", 97), 
     new Student("赵六", 92), 
    };
    

    按照我们之前的理解, 数组我们有一个现成的 sort 方法,我们来试试能否直接用sort方法进行排序:

    仔细思考, 不难发现学生和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系怎么确定? 需要我们额外指定

    让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法:

    class Student implements Comparable { 
     	private String name; 
     	private int score; 
     	public Student(String name, int score) { 
     		this.name = name; 
     		this.score = score; 
     	} 
     	@Override 
     	public String toString() { 
     		return "[" + this.name + ":" + this.score + "]"; 
     	} 
     	@Override 
     	public int compareTo(Object o) { 
     		Student s = (Student)o; 
     		if (this.score > s.score) { 
     			return -1; 
     		} else if (this.score < s.score) { 
     			return 1; 
     		} else { 
     			return 0; 
     		} 
     	} 
    }
    

    在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象

    然后比较当前对象和参数对象的大小关系(按分数来算):

    • 如果当前对象应排在参数对象之前, 返回小于 0 的数字
    • 如果当前对象应排在参数对象之后, 返回大于 0 的数字
    • 如果当前对象和参数对象不分先后, 返回 0

    我们再次执行一下:


    这时候结果就符合我们预期了( ̄▽ ̄)*

    compareTo其实就是一个比较规则 , 如果我们想自定义比较类型的话 , 一定要实现可以比较的接口 . 但是 , Comparable接口有个很大的缺点 , 那就是对类的侵入性很强 , 所以我们一般不轻易改动

    2.Comparator接口

    刚才我们提到了Comparable接口对类的侵入性很强 , 那么有没有一个比较灵活的接口供我们使用呢? 答案是肯定的 , 那就是Comparator接口

    我们先来写一个用年龄进行比较的比较器:

    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);
        }
    }
    

    这时候,我们实例化这两个比较器,并且在sort方法中传入要排列的数组和我们写的比较器对象 :

    class Student implements Comparable<Student>{
        public int age;
        public String name;
    
        public Student(int age, String name, double score) {
            this.age = age;
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    public class Test {
    
        public static void main(String[] args) {
            Student[] students = new Student[3];
            students[0] = new Student(12,"af");
            students[1] = new Student(6,"be");
            students[2] = new Student(18,"zhangsan");
    
            System.out.println("按年龄排序:");
            AgeComparator ageComparator = new AgeComparator();
            Arrays.sort(students,ageComparator);
            System.out.println(Arrays.toString(students));
            System.out.println("---------------------------");
            System.out.println("按姓名排序:");
            NameComparator nameComparator = new NameComparator();
            Arrays.sort(students,nameComparator);
            System.out.println(Arrays.toString(students));
        }
    }
    

    运行结果:

    所以 Comparator接口 只需要根据自己的需求重新写比较器就 ok 了, 灵活很多, 而不是像Comparable接口直接就写死了

    3.Clonable接口

    Java 中内置了一些很有用的接口 , Clonable 就是其中之一

    Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常

    实现Clonable接口
    别忘了要抛出异常
    重写Object的clone方法

    我们来看一个例子 :

    class Person implements Cloneable{
        public int age;
    
        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    '}';
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    
    public class TestDemo {
        public static void main(String[] args) throws CloneNotSupportedException{
            Person person = new Person();
            person.age = 99;
            Person person2 = (Person) person.clone();
            System.out.println(person2);
        }
    }
    

    运行结果:

    此时内存如下:

    这时候,我们再来加一个Money类,并且在Person类中实例化它:

    class Money implements Cloneable{
        public double m = 12.5;
        }
    }
    class Person implements Cloneable{
        public int age;
        public Money money = new Money();
    
        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    '}';
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

    我们在person2中拷贝一份money的值,这时候修改person2中的money,那么person1的money是否改变呢?

    public class TestDemo {
        
        public static void main(String[] args) throws CloneNotSupportedException{
            Person person = new Person();
            Person person2 = (Person) person.clone();
            System.out.println(person.money.m);
            System.out.println(person2.money.m);
            System.out.println("-------------------------");
            person2.money.m = 13.5;
            System.out.println(person.money.m);
            System.out.println(person2.money.m);
        }
    }
    

    答案是不会改变!

    那么是否说明Clonable接口就是只能实现浅拷贝呢?

    答案也是否 , 决定深浅拷贝的并不是 方法的用途 , 而是代码的实现 !

    我们来看看此时的内存分布图:

    要想实现深拷贝,我们拷贝person的时候就要把person对象里的money也拷贝一份,让person2的money指向 新拷贝出来的money ,这时候咱们就实现了深拷贝

    具体的操作实现只需要将Money类重写clone方法(方便克隆),然后将Person中的clone方法进行修改 ,将money也进行拷贝即可

    具体代码如下 :

    class Money implements Cloneable{
        public double m = 12.5;
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    class Person implements Cloneable{
        public int age;
        public Money money = new Money();
    
        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    '}';
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            Person tmp = (Person) super.clone();
            tmp.money = (Money) this.money.clone();
            return tmp;
    //        return super.clone();
        }
    }
    

    我们来测试一下 :

    public class TestDemo {
    
        public static void main(String[] args) throws CloneNotSupportedException{
            Person person = new Person();
            Person person2 = (Person) person.clone();
            System.out.println(person.money.m);
            System.out.println(person2.money.m);
            System.out.println("-------------------------");
            person2.money.m = 13.5;
            System.out.println(person.money.m);
            System.out.println(person2.money.m);
        }
    

    这样就成功实现了深拷贝 !

    四.总结

    抽象类和接口都是 Java 中多态的常见使用方式

    抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法

    在线客服
    服务热线

    服务热线

    4008888355

    微信咨询
    二维码
    返回顶部
    ×二维码

    截屏,微信识别二维码

    打开微信

    微信号已复制,请打开微信添加咨询详情!