栏目分类:
子分类:
返回
终身学习网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
终身学习网 > IT > 软件开发 > 后端开发 > Java

JavaSE--抽象类和接口(下)

Java 更新时间:发布时间: 百科书网 趣学号

抽象类和接口(下)
  • 前言
  • 正文开始
  • 一丶关于Object类
  • 二丶 println()默认调用toString方法
  • 三丶关于hashcode()
    • 1.关于hash
    • 2.关于hashcode
    • 3.hashcode存在的意义
  • 重 写 s o r t ( ) 方 法 color{purple}{重写sort()方法} 重写sort()方法
  • 四丶关于对象的比较
    • 1>关于compareTo
    • 2>关于equals()和 ==
      • 对 于 = = : color{red}{对于==:} 对于==:
      • 对 于 e q u a l s ( ) : color{red}{对于equals():} 对于equals():
    • 3>关于hashcode和equals
      • <1>两者关系
      • <2> 关于二者的重写问题
    • 4>关于浅拷贝和深拷贝(拓展)
      • 关于Clonable 接口
  • 五丶总结

前言

对于剩下的内容,我会用一个题从上而下进行总的概述,因为单独拉出来我感觉串联不起来,用一个题来进行串的话我思路能清晰很多,然后讲到对应知识点的话,我会写在两条等号线中间,像下面这样

=========================================================

PS:这是写的是讲解内容

=========================================================

正文开始

我们在之前学过,在包java.util.Arrays下,有一个sort方法,能够对整形数组进行排序。具体操作如下:

import java.util.Arrays;

public class Student {
    public static void main(String[] args) {
        int[] a = {1,3,5,2,4,6,7,8};
        Arrays.sort(a);
        for(int i: a){
            System.out.print(i + " ");
        }
    }
}

如果这个时候,我们创建对应的学生对象并且用一个数组进行接收,然后对其进行排序,那么此时的sort方法还能继续进行使用嘛?
那我们就来试试看。

import java.util.Arrays;

public class Student {
    String name;
    int score;

    public Student(String name,int score) {
        this.name = name;
        this.score = score;
    }

    public static void main(String[] args) {
        Student[] arr = {//创建Student数组
            new Student("张三",18),
            new Student("李四",17),
            new Student("王五",19),
            new Student("赵六",20),
        };
        //接下来使用sort方法对其进行排序
        Arrays.sort(arr);
        for(Student i:arr){
            System.out.print(i + " ");
        }
    }
}

具体的运行结果如下:

这里抛出了一个类型转换异常,具体是怎么样说的呢?
就是说:这个学生类型不能被转换为Comparable这个类型,既然不能转换,那么我们想办法让这个学生类型可以转换成为Comparable类型,这个时候我们可以想到什么呢?对,转型,那么转型的前提是什么呢?是继承
那么我们就让Student这个类实现Comparable这个接口,然后对应的对象进行转型不就好了?
这里由接口转到对应的子类叫做接口回调
(注意:接口这里不是向上转型和向下转型而是向上转型和接口回调,这里属于后者
那么我们试试:

在这里我们实现Comparable这个接口的时候,我们发现这里抛出了如下异常:Student类必须被声明为抽象类或者实现在Comparable中的抽象方法compareTo(T)
那么我们按住ctrl然后点击Comparable,看一下这里面的具体情况


里面的东西不多,就一个抽象方法,那么我们接下来对其进行重写。

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

那么这里可以看到,我们的形参类型我设置为了Object,那为什么要这样设置?这样设置又有什么好处呢?

( 以 下 是 关 于 O b j e c t 类 的 讲 解 ) color{red}{(以下是关于Object类的讲解)} (以下是关于Object类的讲解)

=========================================================

一丶关于Object类

Object类是JAVA默认提供的一个类,它是所有类的父类。什么意思?

就是说在JAVA里面,除了Object,所有类的都是有继承关系的,再细一点

public class Student{}
public class Student extends Object{}

这两个类是一模一样的,所有的类都默认继承Object

那么从这里,根据多态实现性质:

父类引用指向子类对象

所有类的对象实例都可以通过Object利用自动向上转型机制来进行接收,也就是说,Object实现了真正意义上的参数统一
那么这里我们对此进行实验。

public class Animal {
    public void call(){//定义一个类方法
        System.out.println("某只大猩猩在仰天咆哮");
    }

    public static void main(String[] args) {
        Object obj = new Animal();//利用父类Object引用指向子类对象
        if(obj instanceof Animal){
            Animal animal = (Animal) obj;
            animal.call();
        }
    }
}

结果如下:

Object类接收一切引用数据类型,这意味着:Object才是真正在实际开发过程之中最常见的公共的统一参数类型。

然后对上述的引用数据类型进行讨论,这里的引用数据类型除了类之外,还有我们以前学过的什么呢?String和数组,没错,Object也可以用来接收数组和String。具体演示如下:

import java.util.Arrays;

public class TestObject {
    public static void print(Object o){//利用Object来接收一切参数
        if(o instanceof String){//验证是否是String类型
            String s = (String)o;
            System.out.println(s.toUpperCase());//对字母全部大写输出
        }
        if(o instanceof int[]){//验证是否是int[]数组类型
            int[] arr2 = (int[])o;
            Arrays.sort(arr2);//进行排序
            for(int i: arr2){
                System.out.print(i + " ");
            }
        }
    }

    public static void main(String[] args) {
        String arr1 = "abcdefg";
        int[] arr2 = {2,3,1,6,8,4,5};
        print(arr1);
        print(arr2);
    }
}

程序的运行结果如下:

而关于Object类呢,其中定义了许多方法(IDEA中按Alt + 7):

这些类中的方法我们需要全部掌握,但是目前我们只需要掌握其中的几种方法就好了,具体掌握哪些下面我会详细讲解。

=========================================================
( 此 处 O b j e c t 类 讲 解 完 毕 , 继 续 看 题 ) color{red}{(此处Object类讲解完毕,继续看题)} (此处Object类讲解完毕,继续看题)

重写之后的样子是这样:

然 后 这 里 的 两 个 s c o r e 具 体 比 较 是 怎 么 比 较 的 呢 ? 这 个 t h i s . s c o r e color{blue}{然后这里的两个score具体比较是怎么比较的呢?这个this.score} 然后这里的两个score具体比较是怎么比较的呢?这个this.score
是 从 哪 里 来 的 呢 ? s 和 t h i s 分 别 代 表 谁 ? color{blue}{是从哪里来的呢?s和this分别代表谁?} 是从哪里来的呢?s和this分别代表谁?
对于此处的解答,在下面的sort方法重写会进行详细讲解。
我们继续向下:
重写完毕之后,我们再让这个程序跑一下。

我们发现,这个Student类运行起来之后,排序完毕之后打印出来的结果我们有点看不懂,那么我们怎么办?
这里我们要知道一个知识点:
p r i n t l n 打 印 时 候 , 默 认 调 用 父 类 O b j e c t 的 t o S t r i n g 方 法 color{red}{println打印时候,默认调用父类Object的toString方法} println打印时候,默认调用父类Object的toString方法

( 以 下 是 关 于 t o S t r i n g ( ) 方 法 ) color{red}{(以下是关于toString()方法)} (以下是关于toString()方法)

=======================================================

二丶 println()默认调用toString方法

我们在上文中提到了,所有类的父类都是Object类,所以Object类中所有的方法都可以被子类所继承,就意味着只要是子类都可以使用Object类里面所提供的方法进行操作,这些方法的主要意义也就是为了子类对象实例化所保留的。

所以在这里我们对println默认调用toString()方法进行验证:

public class TestObject {
    public static void main(String[] args) {
        TestObject o = new TestObject();
        System.out.println(o);
        System.out.println(o.toString());
    }
}

运行结果如下:

所以可以证明:不管是否调用了toString()方法,最终在执行的时候也都会自动的调用此方法将对象转为字符串后进行输出
事实上,任何类的对象在进行实例化输出的时候其实都是一个编码数据,就像上面我们输出的那样,而会这样输出原因就是因为Object在其中的定义

然后我们点开Object类中的toString方法

可以看出,这里toString方法实际输出的是:

类的路径+类名称+@+对应对象转化的十六进制地址值

一般Object类中的toString()不太适用于我们的需求,因为这个方法现在所给出的适用于所有的类对象,所有的类对象只有编码是公共的诉求。
所以我们根据继承关系,支持对Object类中的toString()方法进行覆写

======================================================
( t o S t r i n g ( ) 讲 解 完 毕 ) color{red}{(toString()讲解完毕)} (toString()讲解完毕)

那么接下里我们对这个方法进行重写。

public String toString(){
        String s = "[";
        s += this.name + ",";
        s += this.score + "]";
        return s;
    }

那 么 这 里 的 t h i s 从 哪 里 来 的 呢 ? color{blue}{那么这里的this从哪里来的呢?} 那么这里的this从哪里来的呢?
当然就是当前调用对象的this了。
继续看题:
重写完毕之后,我们再次运行这个程序,可以发现,程序运行结果和我们预期的相同。

然后这个题目到了这里初步讲解就算完成了,接下来深入剖析一下,从toString()入手,我们刚才的图中已经看到了toString()方法具体是怎么实现的。

然后在上面的讲解中,对于这个返回值我是这样解释的:

类的路径+类名称+@+对应对象转化的十六进制地址值

那么这里需要剖析的就是这里的hashcode()

( 以 下 是 关 于 h a s h c o d e ( ) ) color{red}{(以下是关于hashcode())} (以下是关于hashcode())

=========================================================

三丶关于hashcode()

我思考很多问题都会习惯从字面入手,那么这里也一样,在搞清楚什么是hashcode之前,我们需要知道,hash—哈希这到底是个什么东西。

1.关于hash

Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。
换种说法:

hash其实就是一个函数,这个函数的具体实现就是一种算法,我们通过这些算法可以得到一个个的hash值,也就是函数值。而这些函数值我们存储在一个叫做hash表的地方,而hash表由很多函数构成,也就是有很多通过各种算法得到的hash值。

2.关于hashcode

在上面的基础上,我们可以很容易理解

hashcode就是hash把得到的数字通过各种算法得到对应的值,对吗?对也不对,关键点就在这里了!!

h a s h c o d e 值 并 不 是 对 象 在 内 存 当 中 的 地 址 值 ! 并 不 是 ! 这 里 的 color{red}{hashcode值并不是对象在内存当中的地址值!并不是!这里的} hashcode值并不是对象在内存当中的地址值!并不是!这里的
h a s h c o d e 值 具 体 由 来 : 对 象 在 内 存 当 中 的 地 址 值 先 转 换 为 一 个 color{red}{hashcode值具体由来:对象在内存当中的地址值先转换为一个} hashcode值具体由来:对象在内存当中的地址值先转换为一个
整 数 , 然 后 该 整 数 被 h a s h 函 数 利 用 散 列 算 法 换 算 成 h a s h 值 并 储 color{red}{整数,然后该整数被hash函数利用散列算法换算成hash值并储} 整数,然后该整数被hash函数利用散列算法换算成hash值并储
存 在 h a s h 表 中 , 所 以 这 里 的 h a s h c o d e 值 其 实 就 是 在 h a s h 表 中 对 color{red}{存在hash表中,所以这里的hashcode值其实就是在hash表中对} 存在hash表中,所以这里的hashcode值其实就是在hash表中对
应 的 位 置 。 ( 所 谓 的 内 存 当 中 的 地 址 值 也 称 为 成 为 物 理 地 址 ) color{red}{应的位置。(所谓的内存当中的地址值也称为成为物理地址)} 应的位置。(所谓的内存当中的地址值也称为成为物理地址)

但是具体有哪些算法目前不做过多解释。

3.hashcode存在的意义

存在的意义可以一句话概括:增加查找效率。
具体为什么使用hashcode()能提高查找效率目前不做过多解释。

=========================================================
( h a s h c o d e 讲 解 完 毕 ) color{red}{(hashcode讲解完毕)} (hashcode讲解完毕)

到了这里,那么就可以继续深入讲解了。
为了进一步加深理解,接下里需要对sort这个排序进行重写,以此来讲解上面的各种参数具体是怎么回事。

重 写 s o r t ( ) 方 法 color{purple}{重写sort()方法} 重写sort()方法

在这里,我们重写sort()方法,并且对其中的关键点进行讲解:

public static void sort(Comparable[] arr){
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length - i - 1; j++) {
                if(arr[i].compareTo(arr[i+1]) > 0){
                    Comparable tmp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = tmp;
                }
            }
        }
    }

这里为什么要使用的是Comparable[]这种类型的数组?
因为我们原来的类实现了Comparable这个接口,所以此时的Comparable接口是我们所对应的父类。

然后到了这里,就需要进行剖析啦!

下 面 是 关 于 对 象 比 较 的 讲 解 color{red}{下面是关于对象比较的讲解} 下面是关于对象比较的讲解

=========================================================

四丶关于对象的比较 1>关于compareTo

这里compareTo()这个比较方法在上述比较就出现了一次:

这里对于咋们重写的compareTo方法进行详解:

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

首先,这里有一个this引用,这个引用是谁呢?

就是前面的arr[i],也就是调用这个方法的对象自身的this参数

而这个s又是谁呢?

就是传入的对象,因为Object类是所有对象的父类,那么这里用object类进行接收,接收完毕之后,进行向下转型(强转),转为对应的对象原来自身的类型。

然后只有当该数组的某一个槽位中对象的score值小于其下一个score值的时候,就会互换两个槽位对象的地址值。
这里如果想要降序排列,那么交换对应的返回值即可,即就是:

2>关于equals()和 ==

首先,数据类型分为基本数据类型和引用数据类型。
基本数据类型:就是那四类八种
引用数据类型:除了基础数据类型基本都是

对 于 = = : color{red}{对于==:} 对于==:

若 是 基 本 数 据 类 型 : color{blue}{若是基本数据类型:} 若是基本数据类型:

这里直接比较的就是两个值的大小,和数据类型没有关系


若 是 引 用 数 据 类 型 : color{blue}{若是引用数据类型:} 若是引用数据类型:

比较的是两个对象的地址值

这里注意是关于String字符串,这玩意储存在元空间的字符串常量池当中(JDK8及其以后),常量池中只要内容相同,那么就不会再创建新的同样的字符串了。

对 于 e q u a l s ( ) : color{red}{对于equals():} 对于equals():

对于equals的话其实我以前的认知就是equals()比较的地址值的内容是否相同,可是呢,在这里其实不是这样的。
这里其实比较坑,需要特别注意一下,咋们还是用代码来对比着说:

public static void main(String[] args) {
        String r1 = new String("111");
        String r2 = new String("111");
        System.out.println(r1.equals(r2));
        String[] s1 = new String[]{"111"};
        String[] s2 = new String[]{"111"};
        System.out.println(s1.equals(s2));
    }

看上面的代码,都是引用数据,都是地址值,那按照我以前的的认知来说的话,应该两个都是true,但是实际上是这样的嘛?

那么咋回事呢?

其实呀,这里两码事,两个equals()方法是不一样的,它们分别是Object类中的equals方法String类型的equals()方法

那么首先分别打开对应的源码(按住ctrl + 点击对应的equals())



Object类作为所有对象的父类,如果说这里的String类和Object类的equals()方法不一样,那么只有一种情况:

String类对Object类中的equals()方法进行了重写

具体写了啥,我看了半天反正没看懂,那么接下来对Object类当中的equals()方法进行查看,可以发现:

Object类的equals()方法底层也还是比较的是地址值。

那么这个时候。为了比较引用类型所含的内容是否相同,那么我们只能也重写Object类当中的equals()方法。具体如下:

public class TestDataType {
   String name;
   int age;

    public TestDataType(String name, int data) {
        this.name = name;
        this.age = data;
    }
    public String toString(){
        return "[" + this.name + "," + this.age + "]";
    }

    public boolean equals(Object obj){
        if(this == obj) {//如果两个对象地址相同,那就没有比较的意义了
            return true;
        }
        //确保传入的类是TestDataType,如果不是,就会出现类型转换异常。
        if(!(obj instanceof TestDataType)){
            return false;
        }

        TestDataType test = (TestDataType) obj;//向下转型为当前类
        return this.name.equals(test.name) && this.age == test.age;
    }
    public static void main(String[] args) {
        TestDataType test1 = new TestDataType("光头强",25);
        TestDataType test2 = new TestDataType("光头强",25);
        System.out.println(test1.equals(test2));
        System.out.println(test1.equals("巴拉啦能量"));//比较对象值
        System.out.println(test1.equals(null));//比较空地址
    }
}

这里有一个点我说一下,就是我重写的equals()方法中,我没有增加对null指针的判断。为什么我没写呢?

因为如果传入的是null的话,那么instanceof会判断其不属于任何对象的实例,就是直接报错!

那么用运行结果来证明我的做法没有问题:

所以此刻给出结论:
比 较 引 用 类 型 是 否 相 同 的 时 候 , 一 定 要 重 写 e q u a l s 方 法 。 color{red}{比较引用类型是否相同的时候,一定要重写equals方法。} 比较引用类型是否相同的时候,一定要重写equals方法。

3>关于hashcode和equals

那么这里其实我懂得也不多,但是这里的话我觉得有几个点需要特别注意一下,所以就有这一部分内容

<1>两者关系

首先:hashcode相同,equals相同嘛?
再者:equals相同,hashcode相同嘛?

先对第一个问题解答:hashcode相同,意味着什么?

hashcode相同,意味着在hash函数的散列结构中的存放位置相同,但是这个位置存放具体由哪种散列算法得来,这个并不清楚,所以你不能说明hashcode相同,equals就相同

再接着对第二个问题解答:equals相同,hashcode相同嘛?

如果两个对象equals相同,意味什么?地址相同。那么地址相同,转换的数字肯定也相同,因为不论用哪种散列算法,得到的值一样,散列表中的位置肯定相同。

总结:
h a s h c o d e 相 同 , e q u a l s 不 一 定 相 等 。 e q u a l s 相 等 , h a s h c o d e 必 定 color{red}{hashcode相同,equals不一定相等。equals相等,hashcode必定} hashcode相同,equals不一定相等。equals相等,hashcode必定
相 同 color{red}{相同} 相同

<2> 关于二者的重写问题

我们建议,如果重写了equals方法,那么建议hashcode也要进行重写。为什么呢?我们用一个代码来进行说明。

首 先 就 是 没 有 写 e q u a l s 方 法 之 前 的 , 因 为 O b j e c t 类 中 e q u a l s 方 法 color{blue}{首先就是没有写equals方法之前的,因为Object类中equals方法} 首先就是没有写equals方法之前的,因为Object类中equals方法
比 较 的 是 地 址 , 所 以 当 然 是 f a l s e color{blue}{比较的是地址,所以当然是false} 比较的是地址,所以当然是false

接 着 是 重 写 e q u a l s 方 法 之 后 的 color{blue}{接着是重写equals方法之后的} 接着是重写equals方法之后的


在重写完equals方法之后呢,这两个对象就相等了,因为他们对象蕴含的内容完全一致,那么按照我们之前说的,equals相等,hashcode也一样一定相同,因为你不论怎样变化,他们一定是一样的。可是当我们重写了equals方法之后,hashcode还一样嘛?我们来试试。


其实这里为什么会不一样呢?也很好理解。

这里equals比较的已经不是地址了,他比较的是两个对象的内容,所以为了equals相同,那么我们也要对hashcode进行重写。

重写之前,我们点开hashcode源码进行查看:


可以看到,这里啥都没有,只有一个,为什么呢?

因为hashcode是native方法,底层是由c/c++编写的,所以我们看不到。

但是这不妨碍我们进行重写,那么继续,重写之后如下:

到了这里的话,hashcode的值也就相等了。

4>关于浅拷贝和深拷贝(拓展)

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。(不会开辟新空间)

深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。(会开辟新空间)

具体演示如下:

首 先 是 浅 拷 贝 color{blue}{首先是浅拷贝} 首先是浅拷贝

可以看到就是把t1的地址值赋值给了t2。

关于Clonable 接口

然后是关于深拷贝(深拷贝需要使用Clonable 接口)

具体实施如下:

public class TestCopy implements Cloneable{
    String name;

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

    @Override
    public TestCopy clone() {
        TestCopy o = null;
        try {
            o = (TestCopy) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return o;
    }
    @Override
    public String toString(){
        String s ="[" + this.name + "]";
        return s;
    }
    public static void main(String[] args) {
        TestCopy t1 = new TestCopy("测试一");
        TestCopy t2 = t1.clone();
        System.out.println(t1);
        System.out.println(t2);
    }
}

具体运行结果如下:

五丶总结

总结的话最后还是回到这一章的主题吧。
关于抽象类和接口的区别:

那么。
对 于 接 口 : color{red}{对于接口:} 对于接口:

接口是一个自上而下的抽象过程,它对某些行为进行了规范,是行为的抽象。我需
要某个行为,我就去实现某个接口,但是具体怎么实现,我自己说了算。

对 于 抽 象 类 : color{red}{对于抽象类:} 对于抽象类:

抽象类是一个自下而上的抽象过程,其提供了通用实现,是事物的抽象。我们在实
现类的过程中,可以发现有些类有几乎相同的实现,我们把这些几乎相同的实现抽
取出来成为抽象类。然后如果有一些差异,那么我们支持抽象方法自定义实现。

一起加油吧!!!!

转载请注明:文章转载自 www.051e.com
本文地址:http://www.051e.com/it/273099.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 ©2023-2025 051e.com

ICP备案号:京ICP备12030808号