IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java:抽象类和接口 -> 正文阅读

[Java知识库]Java:抽象类和接口

抽象类

什么是抽象类?

在Java中,并不是所有的对象都是通过类来进行描述的,在有些时候,一个类中并没有包含足够的信息来描绘一个具体的对象,类似这样的类就是抽象类。

常见的,如果我们在类中写了一个方法,但是这个方法并没有具体的实现细节(不给出具体的实现体),那么我们就可以将这个方法设计成一个抽象方法,那么包含抽象方法的类就可以称为抽象类

abstract class A{
    //被abstract修饰的方法没有方法体
    abstract public void eat();

    //抽象类也是类,可以添加普通变量和方法
    private String name;

    public void sleep(){
        System.out.println("666");
    }

    //抽象类也是可以构造方法
    public A(String name) {
        this.name = name;
    }
}

注意:

  • 使用abstract修饰方法的就是抽象方法,使用abstract修饰的类就是抽象类。
  • 抽象类也是类,内部既可以包含抽象方法,也可以包含普通方法和属性,甚至是构造方法等。

抽象类的特性

  • 抽象类不能直接实例化对象,需要子类继承这个抽象类,再实例化这个子类
  • 在之前我们讲过,如果不加访问限定符的话,会默认是包访问权限;但是如果是抽象方法在没有加访问限定符,默认则是public
  • 抽象类一般来说,是必须被继承的,并且继承后子类要重写父类中的所有抽象方法,否则子类也必须是抽象类(就是必须被abstract修饰的);若两样都不具备,编译器则会直接报错
abstract class A{
    //被abstract修饰的方法没有方法体
    abstract public void eat();

    //抽象类也是类,可以添加普通变量和方法
    private String name;

    public void sleep(){
        System.out.println("666");
    }

    //抽象类也是可以构造方法
    public A(String name) {
        this.name = name;
    }
}

abstract class C extends A{
    public int age;

    public C(String name) {
        super(name);
    }
}

//或者

class E extends A{
    public int age;

    public E(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println("888");
    }
}
  • 抽象方法不能被final和private修饰,因为抽象方法要被子类进行重写(但其实就算子类是抽象类没有对父类的抽象方法进行重写,父类中的抽象方法也是不能被final和private修饰的)
  • 抽象方法不能被static修饰,原因也是抽象方法要被子类进行重写,虽然被static修饰后事不依赖对象的,但是既然是抽象类,那么其中的抽象方法一定会被子类所重写,所以抽象方法是不能被static修饰的
  • 抽象类中不一定包含有抽象方法,但是有抽象方法的类一定是抽象类
  • 抽象类中可以有构造方法,供子类创建对象的时候,初始化父类的成员变量
  • 抽象类存在的最大意义就是为了被继承
  • 抽象类也可以发生向上转型,进一步发生多态

以上抽象类的这几条特性,只要读透、把握好,最后其实会发现抽象类也并不是很难理解。可以仔细理解下面这段示例代码:

abstract class Shape{
    public abstract 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 Triangle extends Shape{
    @Override
    public void draw() {
        System.out.println("△");
    }
}

public class Main {
    public static void drawMap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        drawMap(new Cycle());
        drawMap(new Rect());
        drawMap(new Triangle());
    }
}

运行结果:
在这里插入图片描述

为什么会有抽象类这种东西?

从上面抽象类的那么多特性可以知道一点也是较为重要的一点:抽象类本身是不能被实例化的,如果想要使用它,就只能创建这个抽象类的子类,让这个子类重写抽象类中的抽象方法。


那么,这时候有些人就会有一个疑问:在之前学习中的普通类也是能够实现本文抽象类中这些功能(可以别继承,也可以被重写),那么为何还要这么麻烦再来学习抽象类这样的东西,还要考虑比普通类多这么些的特性呢?

对于这样的疑问,我的回答是:其实抽象类相当于多了一重编译器的校验。例如有些程序本来是不可以调用父类中的内容的,但是如果不小心调用了父类的内容,对于普通类时不会报错的,但是对于抽象类会直接报错,这样有助于快速定位到错误的地方。

接口

什么是接口?

在Java中,接口其实就是一种行为的规范和标准,可以看成:多个类的公共规范,是一种引用数据类型。

interface I{
    public abstract void func1();   //默认就是public abstract,可以省略不写
    void func2();
}

接口的定义格式与类的定义格式基本是相同的,只是将class关键字换成interface关键字而已。
注意:

  • 在接口中的抽象方法中public abstract是固定搭配,在定义抽象方法的时候,可以省略不写
  • 创建接口的时候,接口命名一般都是以大写字母 I 开头

接口的特性

  • 子类和父类之间是extend继承关系,类与接口之间是implement实现关系
  • 接口中每一个成员方法都是抽象方法,如果不写修饰符,则接口中的方法会被隐式指定为public abstract;如果写也只能是写public abstract,写成其他的修饰符都会直接报错
  • 接口中的每一个成员变量都会被隐式制定为public static final变量
  • 接口类型是一种引用类型,但是不能直接new接口对象,是不能够进行实例化的
  • 接口中的方法是不能在接口中实现的,因为接口中的方法默认都是抽象方法,只能由实现接口的类来进行实现
  • 接口中的方法,如果要实现,需要使用default来进行修饰
interface I{
    //可以不在实现接口类中进行重写
    default void func() {
        System.out.println("666");
    }
}
  • 接口中的静态方法可以有具体的实现
interface I{
    //和前面一样,是不依赖对象的
    public static void func2(){
        System.out.println("这是一个静态方法");
    }
}
  • 一个普通的类可以通过implements来实现这个接口
  • 接口也可以发生向上转型,进一步发生多态
  • 接口虽然不是类,但是接口编译完成后字节码文件后缀格式也是.class的文件夹

以上接口的这几条特性,跟抽象类一样,只要读透、把握好,最后其实会发现接口和前面的那些只是点并无两别。可以仔细理解下面这段示例代码:

interface IShape{
    public abstract void draw();
}

class Cycle implements IShape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}

class Rect implements IShape {
    @Override
    public void draw() {
        System.out.println("◇");
    }
}

class Triangle implements IShape {
    @Override
    public void draw() {
        System.out.println("△");
    }
}

public class Main {
    public static void drawMap(IShape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        drawMap(new Cycle());
        drawMap(new Rect());
        drawMap(new Triangle());
    }
}

在这里插入图片描述

实现多个接口

在Java中,类和类之间是进行单继承的,一个类只能有一个父类,也就是说,在Java中是不支持多继承的。那么如何跟C++一样可以实现多继承呢?

因为在Java中是不支持多继承,所以引出了接口这个概念。虽然不能实现多继承,但是可以一个类实现多个接口。
示例代码:

class Animal{
    public String name;
    public int age;

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

interface IRun{
    void run();
}

interface ISwim{
    void swim();
}

interface IFly{
    void fly();
}

class Duck extends Animal implements IRun,ISwim,IFly{

    public Duck(String name) {
        super(name);
    }

    @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 Main {
    public static void main(String[] args) {
        Duck duck=new Duck("小灰");
        duck.run();
        duck.swim();
        duck.fly();
    }
}

运行结果:
在这里插入图片描述

接口间的继承

在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间是可以多继承的。

类和接口之间的关系是implement,接口和接口之间的关系是extends。
示例代码:

interface IRun{
    void run();
}

interface ISwim{
    void swim();
}

interface IFly{
    void fly();
}

interface IDuck extends IRun,ISwim,IFly{

}

class Duck implements IDuck{

    @Override
    public void run() {

    }

    @Override
    public void swim() {

    }

    @Override
    public void fly() {

    }
}

总结:其实接口间的继承就相当于把多个接口合并在一起,拓展了之前的功能。但是最后在实现这个接口的时候,还是要对之前接口中的抽象方法进行重写。

三个重要的接口

Comparable接口

此接口可以对对象数组进行排序。在此之前,我们排序基本上都是对数组元素进行排序,但是针对对象进行排序,又应该如何实现呢?

就比如说,我有如下的对象数组,想要对这些对象按照年龄进行排序:

Student[] students=new Student[3];
students[0]=new Student("张三",18);
students[1]=new Student("李四",23);
students[2]=new Student("王五",9);

这时候,如果只是简单地对对象进行>或者<进行判断,又或者使用equals来进行判断都会是不行的,因为在代码中其实就并未指定到底是按何种方式进行排序的(姓名还是年龄)。
所以就引出了Comparable接口,使用Comparable接口来调用Student类,然后来对这个接口中排序的抽象方法进行重写(也就是重写Comparable接口中的compareTo方法),就可以达到按姓名或者年龄排序的效果了,示例代码:

//按年龄进行排序
class Student implements Comparable<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 +
                '}';
    }

   @Override
    public int compareTo(Student o) {
        return this.age-o.age;
    }
}

public class Main {
    public static void main(String[] args) {
        Student[] students=new Student[3];
        students[0]=new Student("张三",18);
        students[1]=new Student("李四",23);
        students[2]=new Student("王五",9);
        System.out.println("排序前:"+ Arrays.toString(students));
        Arrays.sort(students);
        System.out.println("排序后:"+ Arrays.toString(students));
    }
}

Comparator接口

在上面的Comparable接口,我们会发现一个与实际开发不符的地方:把实现排序的方法写在Student类中,这导致这个排序方法直接就写死了。就比如,我已经实现了按年龄排序,但是我有想再按姓名进行排序,那么这时候可能就要先将原本的按年龄排序方法先注释掉,再重写一个按姓名排序的方法,长此以往,效率实在是太低了。那么,又该如何解决这样的问题呢?

在Java中,这时候就引出了另外一个接口——Comparator接口,重写Comparator接口接口中的compareTo方法,使用这个接口来跟Comparable接口打配合,即可完成多种排序共同存在,想要什么排序就直接在sort方法中传入哪个类即可。
示例代码:

class Student implements Comparable<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 +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return 0;
    }
}

//根据年龄比较
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 TestDemo1 {
    public static void main(String[] args) {
        Student[] students=new Student[3];
        students[0]=new Student("张三",18);
        students[1]=new Student("李四",23);
        students[2]=new Student("王五",9);
        AgeComparator ageComparator=new AgeComparator();
        System.out.println("排序前:"+ Arrays.toString(students));
        Arrays.sort(students,ageComparator);
        System.out.println("排序后:"+ Arrays.toString(students));
    }
}

**注意:**sort方法之前我们都是只传了一个参数,其实它是可以传入第二个参数的,也就是可以传入一个比较器。

Clonable接口

我们在前面对数组进行拷贝的时候,用的是clone方法进行拷贝。但是如果我们是想对一个对象进行拷贝的话,又该如何操作呢?

很显然,直接调用clone方法肯定是不行的,这时候我们就要使用一个接口:Clonable接口,这样之后就可以合法调用Object类中的clone方法。
示例代码:

class A implements Cloneable{
    public int a=10;
    public int b=20;

    @Override
    public String toString() {
        return "A{" +
                "a=" + a +
                ", b=" + b +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        A a=new A();
        A b= (A) a.clone();
        System.out.println(b);
    }
}

注意:需要重写Object类中的clone方法;此处使用的throws是抛出异常(必须写上,否则会报错)只需了解即可,在后面文章中介绍到异常会详细讲到。

浅拷贝和深拷贝

浅拷贝

在上面例子中,我们实现的拷贝是对类中的基本数据类型进行拷贝。那么这时候就会有一个疑问,对于类中的非基本数据类型(比如String等引用类型变量),我们又应该如何进行拷贝呢?

如果我们还是像上面代码一样,就会出现一些问题,先上代码:

class B {
    public int c=100;
}

class A implements Cloneable{
    public int a=10;
    public B b=new B();

    @Override
    public String toString() {
        return "A{" +
                "a=" + a +
                ", b=" + b +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        A a=new A();
        A b= (A) a.clone();
        System.out.println(a.b.c);
        System.out.println(b.b.c);
        System.out.println("==============");
        a.b.c=50;
        System.out.println(a.b.c);
        System.out.println(b.b.c);
    }
}

运行结果:
在这里插入图片描述

从上面代码以及运行结果可以看出,当对对象a中调用的其他对象中的成员变量进行修改之后,对象b也会是相同的结果,也就是说这个代码其实没有实现真正的拷贝,因为b对象会随着a对象的改变而改变,其实b对象只是对a对象的一个拷贝,而b对象中b引用指向的还是原来a对象中b引用的对象,这句话可能会比较难理解,这里通过画图来进行展示:
在这里插入图片描述

对于这种只拷贝了一半,有一部分并没有进行拷贝,就叫做浅拷贝
深拷贝
对于深拷贝来说,相较于浅拷贝来说,是对整个对象全部都进行拷贝的。就是说,会先将一个对象1进行拷贝得到对象2,再将对象1引用的对象3进行拷贝得到对象4,接着让对象2引用对象4,这样的话,如果再对对象1引用的对象3进行修改,也不会影响到对象2和对象4了。下面通过画图展示:
在这里插入图片描述

示例代码:

class B implements Cloneable{
    public int c=100;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class A implements Cloneable{
    public int a=10;
    public B b=new B();

    @Override
    public String toString() {
        return "A{" +
                "a=" + a +
                ", b=" + b +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        A tmp=(A)super.clone();
        tmp.b=(B)this.b.clone();
        return tmp;
    }
}

public class TestDemo1 {
    public static void main(String[] args) throws CloneNotSupportedException {
        A a=new A();
        A b= (A) a.clone();
        System.out.println(a.b.c);
        System.out.println(b.b.c);
        System.out.println("==============");
        a.b.c=50;
        System.out.println(a.b.c);
        System.out.println(b.b.c);
    }
}

运行结果:
在这里插入图片描述

抽象类和接口的区别

在前面的学习中,我们已经知道了抽象类和接口的使用方法以及注意事项等,接下来就对两者的一个区分。
在Java中,抽象类和接口都是实现多态的常用方式,其核心区别是:抽象类可以包含普通方法和普通字段,这些普通方法和普通字段在子类中是可以直接进行使用的,不需要对其进行重写操作;而接口中不能包含普通方法,子类必须重写接口中的所有抽象方法。

区别抽象类接口
结构组成普通类+抽象方法抽象方法+全局常量
权限各种权限public
子类使用使用extends关键字继承抽象类使用implements关键字实现接口
关系一个抽象类可以实现若干接口接口不能继承抽象类,但是接口可以使用extends关键字继承多个父接口
子类限制一个子类只能继承一个抽象类一个子类可以实现多个接口

Object类

在Java中,默认提供了一个类:Object类。Object类默认是所有类的父类,也就是说所有类的对象都可以使用Object的引用进行接收(就比如上面克隆对象讲到的)。

本文只是对Object类中的一部分方法进行简单的介绍。完整内容后续掌握。

对象打印toString方法

前面文章介绍过,详细见之前文章。

对象比较equals方法

在Java中,对==进行比较的时候:

  • 如果==两边都是基本数据类型,比较的是变量之间的值是否相同
  • 如果==两边是引用类型变量,比较的是引用变量之间的地址是否相同
  • 如果要比较对象中的内容,必须重写Object类中的equals方法,因为equals方法默认是按照地址进行比较的
class A{
    public int a=10;
    public String b="abc";
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        int a=1;
        int b=1;
        System.out.println(a==b);   //true
        System.out.println(a1==a2);   //false 按地址进行比较
        System.out.println(a1.equals(a2));   //false 按地址进行比较
    }
}

重写equals方法之后:

class A{
    public int a=10;
    public String b="abc";

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;   //两个引用 引用同一个对象
        if (o == null || getClass() != o.getClass()) return false;
        A a1 = (A) o;
        return a == a1.a && Objects.equals(b, a1.b);
    }
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        int a=1;
        int b=1;
        System.out.println(a==b);   //true
        System.out.println(a1==a2);   //false
        System.out.println(a1.equals(a2));   //true
    }
}

总结:在比较对象是否相同的时候,一定要重写equals方法。

hashcode方法

hashcode()方法是一个能够帮我们算具体对象位置存在的方法。这个方法是一个native方法,底层是由C/C++代码写的。

如果不对hashcode进行重写的话,因为a1和a2都是new出来的对象,那么即使这两个对象里面存储的内容一样,也会判定这两个对象是在不同位置的(地址是不相等的)。

class A{
    public int a=10;
    public String b="abc";
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        System.out.println(a1.hashCode());
        System.out.println(a2.hashCode());
    }
}

运行结果:
在这里插入图片描述

对hashcode进行重写后,因为new出来的这两个对象存储的内容是一样的,是由重写之后会判定这两个对象的地址是一样的。

class A{
    public int a=10;
    public String b="abc";

    @Override
    public int hashCode() {
        return Objects.hash(a, b);
    }
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        System.out.println(a1.hashCode());
        System.out.println(a2.hashCode());
    }
}

运行结果:
在这里插入图片描述

总结:

  • hashcode方法用来确定对象在内存中存储的位置是否相同
  • 事实上hashcode()在散列表中才有用,在其他情况下没用,在散列表中hashcode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置

接收引用数据类型

在前面我们知道了Object可以接收任意对象,因为Object是所有类的父类,但是Object并不局限于此,它可以接收所有数据类型,包括:类、数组、接口。

以接收接口为例:

interface I{
    void func();
}

class A implements I{
    @Override
    public void func() {
        System.out.println("666");
    }
}

public class Main {
    public static void main(String[] args) {
        Object obj=new A();   //向上转型
        A a=(A)obj;   //向下转型
    }
}

Object真正达到了参数统一,如果一个类希望接收所有的数据类型,就是Object完成,在Java中,泛型就是底层就是通过Object来实现的。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-23 10:43:02  更:2022-04-23 10:43:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:05:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码