写点什么

JavaSE 抽象类和接口

作者:whispar
  • 2022-10-15
    陕西
  • 本文字数:9642 字

    阅读完需:约 1 分钟

一、抽象类

在 Java 中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。


public class TestDemo {    public static void main(String[] args){        Circle c = new Circle();        c.setR(5);        c.getArea();        Squre s = new Squre();        s.setLength(10);        s.getArea();    }}//抽象类abstract class Shape{    private int size;    //抽象方法    abstract public void getArea();}class Circle extends Shape{    private int r;    public int getR() {        return r;    }    public void setR(int r) {        this.r = r;    }    //重写抽象方法    @Override    public void getArea() {        double  area = r*r*r*4.0/3;        System.out.println("此圆形的面积是: "+area);    }}class Squre extends Shape{    private int length;    //重写抽象方法    @Override    public void getArea() {        double area = length*length;        System.out.println("此正方形的面积是: "+area);    }    public int getLength() {        return length;    }    public void setLength(int length) {        this.length = length;    }}
复制代码


  • 抽象类的特性

  • 抽象类中使用 abstract 修饰类和抽象方法,这个方法没有具体的实现,抽象类中可以包含普通类所能包含的成员,抽象类所存在的最大意义就是被继承。

  • 抽象类方法不能是私有的,如果一个普通类继承了抽象类,那么必须重写抽象类中的抽象方法,不能被 static 和 final 修饰,因为抽象方法要被子类继承。

  • 抽象类中不一定包含抽象方法,但是包含抽象方法的一定是抽象类,抽象类之间的相互继承不需要重写抽象方法。

二、接口

  • 接口的定义


接口的定义格式与定义类的格式基本相同,将 class 关键字换成 interface 关键字,就定义了一个接口。


  • 接口的使用


//接口的定义interface USB {    void openDevice();    void closeDevice();}//实现接口class Mouse implements USB {    @Override    public void openDevice() {        System.out.println("打开鼠标");    }    @Override    public void closeDevice() {        System.out.println("关闭鼠标");    }    public void click(){        System.out.println("鼠标点击");    }}//实现接口class KeyBoard implements USB{//实现接口中的抽象类    @Override    public void openDevice() {        System.out.println("打开键盘");    }    @Override    public void closeDevice() {        System.out.println("关闭键盘");    }    public void inPut(){        System.out.println("键盘输入");    }}
复制代码


  • 注意事项

  • ❗ 接口不能够直接使用,必须有一个类来实现接口,并实现接口中的所有抽象方法


  • ❗ 子类和父类之间是 extends 继承关系,类与接口之间是 implements 实现关系

  • 接口中每一个方法都是 public 的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错。


  • ❗ 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现 ,JDK1.8 开始允许有可以实现的方法,但这个方法只能是 default 修饰的,类在实现该接口时,不需要重写该默认方法。

  • 具体作用: 当我们进行业务扩展时,需要在接口中新增方法。如果新增的这个方法写成普通方法的话,那么需要在该接口所有的实现类中都重写这个方法。如果新增的方法定义为 default 类型,就不需要在所有的实现类中全部重写该 default 方法,哪个实现类需要新增该方法,就在哪个类中进行实现


  • 重写接口中方法时,不能使用 default 访问权限修饰

  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量

  • 接口中不能有静态代码块和构造方法

  • 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class


  • 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类


  • 实现多个接口

  • 一个类实现多个接口

  • 一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类

  • 一个类继承一个父类,同时实现多个接口

  • 接口中的多态

  • 有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力.

  • 接口之间的继承


一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字


interface IRing {    void run();} interface ISing {    void swim();} interface IAmphibious extends IRunning, ISwimming {}class Frog implements IAmphibious {    @Override    public void run() {        System.out.println("跑啊跑");    }    @Override    public void swim() {        System.out.println("游啊游");    }}
复制代码


接口间的继承相当于把多个接口合并在一起.


✅抽象类和接口的区别??


三、Object 类

Object 是 Java 默认提供的一个类。Java 里面除了 Object 类,所有的类都是存在继承关系的。默认会继承 Object 父类。即所有类的对象都可以使用 Object 的引用进行接收。


public class TestDemo5 {    public static void main(String[] args) {        function(new Person());        function(new Student());    }    public static void function(Object obj){        System.out.println(obj);    }}class Person{    private int age;    private String name;}class Student{    private int grade;    private String sno;}
复制代码


Object 类中提供的一些默认方法

3.1 toString()方法
//Object类中的toString()方法实现:public String toString() {     return getClass().getName() + "@" + Integer.toHexString(hashCode());}
复制代码


toString()方法一般需要通过重写之后进行使用。

3.2 hashcode()方法
  • 返回对象的 hash 代码值


源码:


public native int hashCode();
复制代码



重写 hashCode() 方法


class Per{    public String name;    public int age;    public Per(String name, int age) {        this.name = name;        this.age = age;    }    @Override    public int hashCode() {        return Objects.hash(name, age);    }}public class TestDemo6 {    public static void main(String[] args) {        Per per1 = new Per("gaobo",20);        Per per2 = new Per("gaobo", 20);        System.out.println(per1.hashCode());       /*         注意事项:哈希值一样。        结论:        1、hashcode方法用来确定对象在内存中存储的位置是否相同        2、事实上hashCode() 在散列表中才有用,在其它情况下没用。        在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。        */        System.out.println(per2.hashCode());    }}
复制代码


3.3 equals()方法
  • 比较的是地址


// Object类中的equals方法
public boolean equals(Object obj){
return (this == obj); // 使用引用中的地址直接来进行比较 }
复制代码



如果要比较对象中内容,必须重写 Object 中的 equals 方法,因为 equals 方法默认也是按照地址比较的


重写 equals()方法


@Override public boolean equals(Object obj) {     //判断是否为空        if (obj == null) {            return false ;        } if(this == obj) {            return true ;        }        // 不是Person类对象        if (!(obj instanceof Per)) {            return false ;        }        Per per = (Per) obj ; // 向下转型,比较属性值        return this.name.equals(per.name) && this.age==per.age ;    }    /*  @Override    public boolean equals(Object obj) {        Per per = (Per)obj;        //String类调用的是自身的equals,        // s1跟s2两者比较的规则则是按照String类重写后的equals方法来比较,        //很显然,String类的比较规则是按照值来比较的,因此结果会输出true。        if(this.name.equals(per.name)&&this.age == per.age){                return true;        }        return false;
}}*/
复制代码



编译器自动生成重写的 hashcode()和 equals()方法


@Overridepublic boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        Per per = (Per) o;        return age == per.age &&                Objects.equals(name, per.name);    }    @Override    public int hashCode() {        return Objects.hash(name, age);   }
复制代码


在 object 类中,hashcode()方法是本地方法,返回的是对象的地址值,而 object 类中的 equals()方法比较的也是两个对象的地址值,**如果 equals()相等,说明两个对象地址值也相等,当然 hashcode()也就相等了.**但是 hashcode() 相同时,equals()不一定相同


✅✅重写 equals 方法时,也必须重写 hashcode()方法吗?

答:必须,hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度,当重写equals方法后有必要将hashCode方法也重写,这样做才能保证不违背hashCode方法中“相同对象必须有相同哈希值”的约定。


✅✅ == 和 equals 的区别是什么?

答:

对于基本类型和引用类型 == 的作用效果是不同的,如下所示:

  • 基本类型:比较的是值是否相同;

  • 引用类型:比较的是引用是否相同

String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true

对于 equals() 方法,根据源码可以得知 : equals() 的本质上就是 true

public boolean equals(Object obj) {
        return (this == obj);
}

所以 equals()方法 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。

四、常用接口

4.1 Comparable 接口(比较)

在学习数组时,Arrays 类中的 sort 方法可以对对象数组进行排序 , 那下面的对象数组能不能用 Arrays.sort 排序呢?


class Student {      String name;    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 test4 {    public static void main(String[] args) {        Student[] students = new Student[] {               new Student("zhangsan", 13),              new Student("lisi", 23),              new Student("able", 17),        };        Arrays.sort(students);        System.out.println(Arrays.toString(students));       }}
复制代码



此时编译器并不知道到底是按姓名还是年龄进行排序,当 sort 方法对对象所属的类进行排序时,对象所属的类必须实现 Comparable 接口,通过参考文档可知,Comparable 接口中仅有一个抽象方法。




那么我们就可以实现 Comparable 接口,并实现和重写 compareTo 方法


class Student implements Comparable<Student>{    public int age;    public String name;        public Student(int age, String name) {        this.age = age;        this.name = name;    }    @Override    public String toString() {        return "Student{" +                "age=" + age +                ", name='" + name + '\'' +                '}';    }    //重写compareTo方法    @Override    public int compareTo(Student o) {        if (this.age - o.age > 0)            return 1;        else        if (this.age - o.age < 0)            return -1;        else            return 0;    }     public static void main1(String[] args) {        Student student = new Student(16, "liba");        Student student1 = new Student(13, "zhangsan");        System.out.println(student.toString());        System.out.println(student1.toString());        if (student.compareTo(student1) > 0) {            System.out.println("student > student1");        } else {            System.out.println("student < student1");        }    }
}
复制代码


此时可以得到按年龄进行排序的结果:



我们知道在 Arrays.sort(students); 中是传了一个学生对象数组,在调用 Arrays 对对象数组排序的时候,其实就调用了我们的 Comparable 接口中的 compareTo 方法对数组的每个元素进行了排序和比较,在 Java 中对于引用数据类型的比较或者排序,一般都要用到使用 Comparable 接口中的 compareTo() 方法


按姓名排序时,重写的 compareTo 方法


@Override  public int compareTo(Student o) {   // this.代表对当前对象的引用,o.代表对参数对的引用        if (this.name.compareTo(o.name) > 0)//String类中重写了compareTo方法,可直接使用             return 1;          else if (this.name.compareTo(o.name) < 0)              return -1;        else              return 0;    }    //如果当前对象应排在参数对象之前, 返回小于 0 的数字;    //如果当前对象应排在参数对象之后, 返回大于 0 的数字;    //如果当前对象和参数对象不分先后, 返回 0;
复制代码


🎈 缺点:一旦重写了 comparable()方法,那么就只能对一种参数类型进行比较,把方法写死了,此时就需要使用 Comparator 接口 ❗

4.2 Comparator 接口(比较)

这里是 Arrays.sort 中只有一个参数的方法



当实现 Comparator 接口时,可以使用两个参数重载的方法实现排序,包含一个比较器类型的参数



首先通过参考文档了解 Comparator 接口,我们需要重写的是 compare()方法



所以就像 Comparable 接口一样,我们只要实现了 Comparator 接口,并重写 Comparator 里的 compare 方法就可以实现对学生对象数组的排序


比如我们上面的年龄比较就可以写成这样


class Student {     String name;    int age;    public Student(String name, int age) {        this.name = name;        this.age = age;    }     @Override      public String toString() {        return "[" + this.name + ":" + this.age + "]";    }} // 实现Comparator接口中的compare方法class AgeComparator implements Comparator<Student> { // 年龄比较器    @Override     public int compare(Student o1, Student o2) {        return o1.age - o2.age;    // 反正返回的也是数字,当o1.age>o2.age时返回大于零的数,即o1对象排在o2对象的后面,升序排列,我们之前用Comparable接口时也可以这样简写    }}public class test4 {    public static void main(String[] args) {        Student[] students = new Student[]{                 new Student("zhangsan", 13),                new Student("lisi", 23),                new Student("able", 17),        };        AgeComparator ageComparator = new AgeComparator();         Arrays.sort(students, ageComparator);         // 用类Arrays.sort对students数组进行排序,这里传了两个参数(学生对象和所对应的年龄比较器)        System.out.println(Arrays.toString(students));    }}
复制代码


同样,当我们按照姓名进行排序时,也可以使用此接口


class Student {    String name;    int age;     public Student(String name, int age) {        this.name = name;        this.age = age;    }    @Override      public String toString() {        return "[" + this.name + ":" + this.age + "]";    }}class NameComparator implements Comparator<Student> { // 姓名比较器    // 实现Comparator接口中的compare方法    @Override      public int compare(Student o1, Student o2) {        return o1.name.compareTo(o2.name); // 因为name是String类型,也是一个引用类型,也要用到compareTo方法,此时的compareTo方法是String类里重写的方法    }}public class test4 {    public static void main(String[] args) {        Student[] students = new Student[]{                 new Student("zhangsan", 13),                new Student("lisi", 23),                new Student("able", 17),        };        NameComparator nameComparator = new NameComparator();        Arrays.sort(students, nameComparator);           System.out.println(Arrays.toString(students));      }}
复制代码


Comparable 接口和 Comparator 接口都是 Java 中用来比较和排序引用类型数据的接口,要实现比较,就需要实现他们所各种对应的 compareTo 方法或者 compare 方法。


Comparator 使用起来更加灵活,所以我更倾向于使用比较器:Comparator

4.3 Cloneable 接口(拷贝)
  • 对象在内存当中的存储


class Student {    public int age = 15;    @Override     public String toString() {        return "Student{" +                "id=" + id +                '}';    }}public class test3 {    public static void main(String[] args) {        Student student1 = new Student();        System.out.println(student1);    }}
复制代码


此时如果在堆内存中对 student1 对象拷贝一份,如果使用


Student student2 = student1;
复制代码


这只是我们在栈上重新定义了一个引用变量 student2,并指向了堆上的 student1 对象,并没有对我们的 student1 实现拷贝,改变 student2.age 会影响 student.age 的值。



所以我们需要重写 Object 类中的 clone 方法进行克隆,在使用 clone 方法之前,需要实现 Cloneable 接口




由源码和参考文档可知,Cloneable 是一个空接口即标记接口,如果有其他类继承该接口,说明该类的对象是可以被克隆的。


  • 要克隆的这个对象的类必须实现 Cloneable 接口

  • 类中重写 Objectclone() 方法

  • 处理重写 clone 方法时的异常情况

  • clone 方法需要进行强转(比较特殊,先记住就好)


class Student implements Cloneable{    public int age = 10;    @Override    protected Object clone() throws CloneNotSupportedException {        return super.clone();    }    @Override    public String toString() {        return "Person{" +                "age=" + age +                '}';    }}public class Demo2 {    public static void main(String[] args) throws CloneNotSupportedException{        Student student = new Student();        Student student2 = (Student)student.clone();   //返回值为Object需要进行强制类型转换        System.out.println(student.age);        System.out.println(student2.age);         student2.age = 18;        System.out.println(student.age);        System.out.println(student2.age);    }}
复制代码


此时在内存当中就是这样,student1 和 student2 中的两个 age 是相互独立的,student2 的 age 发生改变不会影响 student1 的内容。此时我们就成功实现了对象的拷贝


4.4 浅拷贝与深拷贝
  • 浅拷贝


根据上边 Cloneable 接口使用介绍我们已经详细了解了,此时我们提出了一个问题,如果在 Student 类当中再定义一个引用类型,那么又该如何拷贝呢?



class Teacher{    int number = 20;}class Student implements Cloneable{    public int age = 10;    Teacher teacher = new Teacher();    @Override    protected Object clone() throws CloneNotSupportedException {        return super.clone();    }
@Override public String toString() { return "Person{" + "age=" + age + '}'; }}public class Demo2 { public static void main(String[] args) throws CloneNotSupportedException{ Student student = new Student(); Student student2 = (Student)student.clone(); //返回值为Object需要进行强制类型转换
System.out.println(student.teacher.number); System.out.println(student2.teacher.number);
student.teacher.number = 100; System.out.println(student.teacher.number); System.out.println(student2.teacher.number);
}}
复制代码


此时,student 中 teacher 的改变也引起了 student2 中地址的改变,此种拷贝就好像只拷贝了 student.teacher.number 的地址,并未重新复制一块内存出来,此种拷贝就叫做浅拷贝



浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。



  • 深拷贝


刚刚我们通过实现 Cloneable 接口、重写 clone 方法对 Student 类实现了拷贝,那么同理我们也可以用这样的办法对 Teacher 类对象进行拷贝.


class Teacher implements Cloneable{    int number = 20;    @Override    protected Object clone() throws CloneNotSupportedException {        return super.clone();    }}class Student implements Cloneable{    public int age = 10;    public Teacher teacher = new Teacher();    @Override    protected Object clone() throws CloneNotSupportedException {        //  此时我们在进行 “(Student) student.clone();” 操作,        //  我们在堆上对student克隆拷贝出来一个新对象,并让引用变量tmp指向新对象        Student tmp = (Student) super.clone();        // 用this.teacher.clone()对引用变量teacher所指向的Teacher类对象进行克隆        tmp.teacher = (Teacher) this.teacher.clone();        return tmp;    }    @Override    public String toString() {        return "Person{" +                "age=" + age +                '}';    }}public class Demo2 {    public static void main(String[] args) throws CloneNotSupportedException{        Student student = new Student();        // 此时的student.clone返回Student类对象的引用tmp,student2 就指向了原来tmp所指向的对象        Student student2 = (Student)student.clone();  
System.out.println(student.teacher.number); System.out.println(student2.teacher.number); student.teacher.number = 100; System.out.println(student.teacher.number); System.out.println(student2.teacher.number);
}}
复制代码



此时的内存结构图为:



上面的拷贝就把引用变量 teacher 所指向的 Teacher 类的对象也在堆中拷贝了一份,这就是深拷贝, 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。


深拷贝:创建一个新对象,然后将当前对象的各种成员属性复制到该新对象,无论该成员属性是值类型的还是引用类型,都复制独立的一份,引用类型也会复制该引用类型所指向的对象。


发布于: 刚刚阅读数: 3
用户头像

whispar

关注

笔下生花~ 2022-09-13 加入

还未添加个人简介

评论

发布
暂无评论
JavaSE 抽象类和接口_接口_whispar_InfoQ写作社区