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

目录
  • 一.抽象类
    • 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 中多态的常见使用方式

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

到此这篇关于Java入门基础之抽象类与接口的文章就介绍到这了,更多相关Java抽象类与接口内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java 抽象类与接口的区别总结

    java 抽象类与接口的区别总结 abstract class和interface是Java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力. abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义时对于 abstract class和interface 选择显得比较随意. 其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的 理解

  • 详细解析Java中抽象类和接口的区别

    在Java语言中, abstract class 和interface 是支持抽象类定义的两种机制.正是由于这两种机制的存在,才赋予了Java强大的 面向对象能力.abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进 行抽象类定义时对于abstract class和interface的选择显得比较随意.其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对 于问题领域本质的理解.对于设计意图的理解是否正确.合理

  • Java接口和抽象类用法实例总结

    本文实例讲述了Java接口和抽象类用法.分享给大家供大家参考,具体如下: 接口 1 因为java不支持多重继承,所以有了接口,一个类只能继承一个父类,但可以实现多个接口,接口本身也可以继承多个接口. 2 接口里面的成员变量默认都是public static final类型的.必须被显示的初始化. 3 接口里面的方法默认都是public abstract类型的.隐式声明. 4 接口没有构造方法,不能被实例化. 5 接口不能实现另一个接口,但可以继承多个接口. 6 类如果实现了一个接口,那么必须实现

  • 浅谈Java抽象类和接口的个人理解

    今天来说一波自己对Java中抽象类和接口的理解,含参考内容: 一.抽象类 1.定义: public abstract class 类名{} Java语言中所有的对象都是用类来进行描述,但是并不是所有的类都是用来描述对象的.我所理解的抽象类其实就是对同一类事物公共部分的高度提取,这个公共部分包括属性和行为.比如牛.羊.猪它们的公共属性是都有毛,公共行为是都哺乳,所以我们可以把公共部分抽象成一个哺乳类,含有属性毛和行为哺乳,当牛.羊.猪继承了哺乳类后也就有了哺乳的功能,至于怎么完成这个功能就需要自己

  • JAVA 继承基本类、抽象类、接口介绍

    封装:就是把一些属性和方法封装到一个类里. 继承:就如子类继承父类的一些属性和方法. 多态:就如一个父类有多个不同特色的子类. 这里我就不多讲解,下面我主要说明一个继承.继承是OOP(面向对象)的一个特色,java只支持单继承(如果继承两个有同样方法的父类,那么就不知道继承到那个父类的,所以java只支持单继承).继承是java的一个特色,我们用的所以类都继承Objict类,所以就要Object类的方法,如toString().getClass().wait()--所以我们建立的类都有父类. J

  • Java基础篇_有关接口和抽象类的几道练习题(分享)

    呃,一定要理解之后自己敲!!!这几道题,使我进一步了解了接口和抽象类. 1.设计一个商品类 字段: 商品名称,重量,价格,配件数量,配件制造厂商(是数组,因为可能有多个制造厂商) 要求: 有构造函数 重写 toString 方法 重写 equals方法,进行两件商品的比较 package TT; import java.util.Arrays; public class G { private int 重量; private String 商品名称; private int 价格; privat

  • Java中的接口和抽象类用法实例详解

    本文实例讲述了Java中的接口和抽象类用法.分享给大家供大家参考,具体如下: 在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类. 抽象类往往用来表征我们在对问题领域进行分析. 设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象,我们不能把它们实例化(拿不出一个具体的东西)所以称之为抽象. 比如:我们要描述"水果",它就是一个抽象,它有质量.体积等

  • Java接口和抽象类实例分析

    本文实例讲述了Java的接口和抽象类.分享给大家供大家参考.具体分析如下: 对于面向对象编程来说,抽象是它的一大特征之一.在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类.这两者有太多相似的地方,又有太多不同的地方.很多人在初学的时候会以为它们可以随意互换使用,但是实际则不然.今天我们就一起来学习一下Java中的接口和抽象类. 若有不正之处,请多多谅解并欢迎批评指正,不甚感激. 一.抽象类 在了解抽象类之前,先来了解一下抽象方法.抽象方法是一种特殊的方法:它只有声明,而没有具体的实

  • Java中接口和抽象类的区别与相同之处

    1. 抽象类: (1).概念:抽象类是对一种事物的抽象,即对类抽..抽象类是对整个类整体进行抽象,包括属性.行为.Java抽象类和Java接口一样,都用来声明一个新的类型.并且作为一个类型的等级结构的起点. (2).格式: 复制代码 代码如下: public abstract class abstractDemo{             /**属性*/             private String name;             /**方法*/             public

  • java中抽象类、抽象方法、接口与实现接口实例详解

    前言 对于java中的抽象类,抽象方法,接口,实现接口等具体的概念就不在这里详细的说明了,网上书本都有很多解释,主要是我懒,下面通过一个例子来说明其中的精髓要点,能不能练成绝世武功,踏上封王之路,就看自己的的啦(不要误会,我指的只是我自己啦啦) 用接口实现一个简单的计算器 1.利用接口做参数,写个计算器,能完成+-*/运算 (1)定义一个接口Compute含有一个方法int computer(int n,int m); (2)设计四个类分别实现此接口,完成+-*/运算 (3)设计一个类UseCo

随机推荐