【JavaSE】对象的比较


一、基本数据类型及Integer包装类的比较

几乎任何一种编程语言都支持基本数据类型比较。

但是对于基本数据来讲,背后还有功能更加强大的包装类,也就是引用类型。

引用类型的数据为什么不能比较呢?首先画个图浅浅的理解一下。

Integer valA = new Integer(10);

为什叫引用类型呢,因为访问数据是通过Java栈区上的引用变量存储的地址来达到访问Java 堆区上对象的数据,可以理解为头在栈区,身体在堆区,只有通过头才能找到身体,引用类型的本质是地址.

那么一块地址值又怎么能够比较?即使可以比较,那么又怎么能达到我们想要的效果呢。

理论上引用类型是不可以比较的,但是有一个特例,就是Integer 包装类。

1.1 包装类的装箱/装包操作

装箱/装包 :把基本数据类型转换成 引用类型(对应的包类型)

在进行自动“装箱”的时候,IDEA会自动调用一个 Integer.valueOf() 方法,来帮我们转换,由这个方法我们可以看到当数值的范围在 [-128, 127] 的范围内存储的是基本数据类型,所以在这个范围内的 Integer 类型的数据是可以直接使用比较运算符比较的。

当数值处在[-128, 127] 的范围内,我们Integer 的底层实际上就是调用数组来存储数据。

如果我们超过这个范围就不行了,原因是变成“引用类型了"。地址是无法比较的。

如果我们直接使用关键字 "new" 来创建一个对象值。

其实也不难想象,这个时候 valA 和 valB 是妥妥的引用类型,即指向对象的地址,他们虽然拥有相同的数值,但是他们指向了不同的两个对象,所以两个地址之间的比较是不可能相同的。我们本质是 期望比较数值 127 的关系,很明显达不到我们的要求。


二、对象的比较

对象的比较有三种比较方式:

2.1 equals 方法

Java 中所有的类都继承于 Object 类,Object 类中提供了一个 equals 方法,但是 equals 方法实际上比较的是引用变量的存储的地址而不是比较的是引用类型引用的对象的内容。

这里是 Object 实现的 equals 方法的实现。

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

this 表示对当前类引用变量的引用,由此可见,实际上还是比较两个地址是否相同。

所以当我们想要判断两个对象是都相同的时候需要重写我们的 equals 方法,按照我们想要的方式来比较对象的内容判断是否相同。

定义一个学生类 Student, 成员变量: Id 学号, name 姓名, age 年龄, sex 性别,并提供一个构造方法

public class Student {
    public int id;      //学号
    public String name; //姓名
    public int age;     //年龄
    public String sex;  //性别

    //构造方法
    public Student(int id, String name, int age, String sex) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() { //重写toString 方法方便打印展示效果。
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}

来到测试类这边 new 两个对象直接使用 equals 方法来比较。

很明显是不相同的,原因是他是直接比较的两个引用类型存储的地址,这两个对象虽然数值相同,但是他们是两个独立的对象,所以比较的返回值就是不相同的。


重写 equals 方法,我们设计当对象之间的成员变量的数值相同,则认为这两个对象相同。

首先我们需要在 Student 类中重写,因为是基于 Student 类比较嘛,子类重写父类方法,子类对象在调用该方法时调用重写后的方法,没有重写的话默认直接调用父类的方法。

快捷键 :Alt + insert

会弹出这个界面,选择 equals() and hashCode, 一路 next 即可。

在每个类中,在重写equals方法的时侯,一定要重写hashcode方法。这是Java官方的语法规定

这里还有一个 hashCode() 方法没有介绍,hashCode 是通过哈希函数计算映射出这个对象的地址,

equals 是基于对象的比较。

面试问题:

equals 比较一样,hashCode 一定一样吗? 一定一样

hachCode 比较一样,equals 一定一样吗? 不一定

为什么呢? hashCode值是依据地址通过某种公式计算来的, 当遇到不同的地址时不能保证计算的结果一定不相同。如果二者是相同的地址,那么通过计算后,得到的hashCode 值是必然相同的。

当只重写equals方法,不重写hashCode时,违反规定:equals相等的对象必须具有相等的哈希码值


总结: 重写equals 方法

如果两个引用指向的是同一个对象,直接返回 true
如果传参的引用指向的为 null ,或者传入的对象所属的类与比较的类不一致返回 false
按照类的这个属性进行比较,例如 Id, name , sex 等相同,那么认为是同一个学生。
String 类型在比较的时候也是调用了 equals 方法,是比较字符串是否相同,String 也是引用类型嘛

涉及到的 Java语法知识:

1.当发生向上转型之后, 此时通过父类的引用只能访问父类自己的成员,不能访问子类特有的成员
2.向上转型后 子类重写的后的方法, 父类调用该方法,此时是调用子类重写后的方法,发生动态绑定
3.动态绑定: 通过父类引用 引用子类对象重写后的方法
4.发生向上转型之后 如果父类引用想使用子类特有的 成员 ,需要进行向下转型(强制类型转换)
5. 向下转型 不安全只能发生在 向上转型 的类型之间, 且右边的范围(父类)大于 左边(子类)
6. 可以用 instanceof 关键字 判断 向下转型是否安全

注意:equals 只能按照相等进行比较,不能按照大于,小于的方式进行比较。

重写 equals 方法后就可以判断 两个对象是否相同了。


2.2 Comparable 接口类的比较

Comparable接口是Java JDK 提供给用户用来实现对象比较的一个泛型接口,然后覆写该接口的一个compareTo 方法,比较的规则就是在该方法定义的,下面我们来看看这个接口的简介:

public interface Comparable<T> {
    public int compareTo(T o);
}

以上代码可以看到, compareTo 方法的返回值是整型int 数据

返回值 < 0; 表示this 指向的对象小于 o 指向的对象。
返回值 == 0; 表示this 指向的对象等于 o 指向的对象。
返回值 > 0; 表示this 指向的对象大于 o 指向的对象。

我们在面对对自定义类型的时候,如果要想按照大小进行比较时:在定义类时实现Comparable接口即可,然后在类中重写compareTo方法。

如果只是Integer , double 等等基本数据类型之间的比较就可以直接实现该接口然后调用 compareTo() 方法。

以Student 类为例:

public class Student implements Comparable<Student>{
    public int id;      //学号
    public String name; //姓名
    public int age;     //年龄
    public String sex;  //性别

    //构造方法
    public Student(int id, String name, int age, String sex) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    /**
     * 重写 compareTo 方法,博主设计 按照 id 来比较数据从而判断对象的大小
     * @param o
     * @return
     */
    @Override
    public int compareTo(Student o) {
        return this.id - o.id;
    }

    @Override
    public String toString() { //重写toString 方法方便打印展示效果。
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}

可以看到,对象之间就可以根据我们的需求来比较对象之间的大小,博主首先设计的是 采用 id 作为 关键字来比较,但是如果我们想要采用姓名 或者是 年龄 等等来作为关键字来比较对象的大小,我们就需要修改compareTo 方法的比较方式。

如果我们采用name 来比较数据:

/**
     * 重写 compareTo 方法,博主设计 按照 name 来比较数据从而判断对象的大小
     * @param o
     * @return
     */
    @Override
    public int compareTo(Student o) {
        if(this.name.compareTo(o.name) == 0) { //字符串的比较 调用 String 的 compareTo 方法
            return 0;
        } else if (this.name.compareTo(o.name) < 0) {
            return -1;
        } else {
            return 1;
        }
         //为了方便理解所以上面写的复杂,按照下面的方式更好
        /*return o1.name.compareTo(o2.name);*/
    }

关于String 类型的对象的比较,我们也可以使用 String 类下的 compareTo 方法我们来看看 JDK 是如何实现这个方法的:

 public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

首先计算两个字符串的长度,转换成字符数组,拿最小的lim字符长度来遍历字符串,在lim 范围内比较,如果字符不相同就返回 两个字符 ASCll 的差值,如果在 lim 的范围内字符都相同,则返回两个字符串长度的差值。


总结:

1.我们想要按照某个关键字来比较对象之间大小的关系,我们可以让实现 Comparable 接口,然后根据实际环境的需要重写compareTo 方法。
2. 使用Comparable 接口来比较对象之间的大小的关系的时候,我们一般是 写死compareTo 方法,无法灵活的更改关键字来达到更改对象比较的方式。比如说我们的通讯录,可以按照我们的姓氏首字母来排序,也可以根据修改日期,可以根据号码排序,我们使用 compareTo 方法比较岂不是每次都要修改源代码?这是不现实的,那怎么办呢,接下来会讲。
3. Comparable 是 Java.lang 中的接口类,可以直接使用。

2.3 Comparator 接口(基于比较器比较)

使用这个接口需要我们自己定义一个比较器类然后实现 Comparator泛型接口,再重写 compare() 方法。

class idComparator implements Comparator<Student>{ //通过学号来比较大小
    @Override
    public int compare(Student o1, Student o2) {
        return o1.id - o2.id;
    }
}

兄弟们注意:Comparator 跟 Comparable 的区别。

class idComparator implements Comparator<Student> { //通过学号来比较大小
    @Override
    public int compare(Student o1, Student o2) {
        return o1.id - o2.id;
    }
}

class nameComparator implements Comparator<Student> { //通过姓名来比较大小
    @Override
    public int compare(Student o1, Student o2) {
        if(o1.name.compareTo(o2.name) == 0) { //字符串的比较 调用 String 的 compareTo 方法
            return 0;
        } else if (o1.name.compareTo(o2.name) < 0) {
            return -1;
        } else {
            return 1;
        }
    }
}

public class Test3 {
    public static void main(String[] args) {
        Student student1 = new Student(202300,"李三",19,"男");
        Student student2 = new Student(202301,"张四",19,"男");

        idComparator idComparator = new idComparator(); //按照 id 来比较
        System.out.println(idComparator.compare(student1, student2));

        nameComparator nameComparator = new nameComparator();
        // l 在 z 的前面嘛,所以返回的是 1;
        System.out.println(nameComparator.compare(student1, student2)); 
    }
}

采用这种方式我们才可能实现关键字随需变换的机制,但是还有一点问题,就是我们我们虽然写了很多比较类,但是我们怎么做到在Student类外传入比较器,从而改变Student类内部的比较方式呢?

如果我们在 Student 类的内部需要比较的形式,要求是通过类外影响Student 类的比较机制。

首先我们在 Student 类的内部既然需要比较那肯定涉及到方法,我们就可以对比较的方法入手,如果将比较器以传参的形式输入,那么类方法就在内部拿到比较器,从而通过传参的比较器从而达到计较的目的。

现在摆在我们眼前的是如何接收比较器对象,因为我们的需求是可以接收多个比较器对象。

以该比较器为例:

class idComparator implements Comparator<Student>{ //通过学号来比较大小
    @Override
    public int compare(Student o1, Student o2) {
        return o1.id - o2.id;
    }
}

我们使得 idComparator 类继承了Comparator<Student> 的泛型接口,然后我们重写了接口的 compare() 方法,两个Student对象参数根据我们设计的某种机制比较大小,返回 int 类型的数据。

所以我们可以采用 Comparator 接口来接收(Student)泛型对象。

接口作为方法的参数,可以接收该接口的所有实现类的对象

接口作为方法的返回值,可以返回该接口的所有实现类的对象

JDK 关于这方面的实现是使用 Comparator<? super T> 来接收比较器对象。

Comparator<? super T> 代表任意T的父类或祖先,Comparator<? super Student>可以表示接受任意一个泛型类型是Student父类的Comparator,比如一个Comparator<Person>可以给所有Person(人)比较,那么自然也可以给Student比较。

? 是通配符表示可传入的类型不确定,通常需要限制一下范围,super T就是限定条件,表示 ?只能接收T类型及其父类型。 T 类型此时是接收了 Student 类型。

? extends T:可以接收T类型或者T类型的子类,泛型的上限

? super T:可以接收E类型或者E的父类型, 泛型的下限

话不多说直接上代码:

 /**
     * 冒泡排序 从小到大排序
     * @param stu
     * @param c
     * @return
     */
    public static <T> T[] bubbleSort(T[] stu, Comparator<? super T> c) {
        if(stu == null || c == null) {
            return stu;
        }
        for (int i = 0; i < stu.length - 1; i++) {
            for (int j = 1; j < stu.length - i; j++) {
                if(c.compare(stu[j - 1],stu[j]) > 0) {
                    T tmp = stu[j - 1];
                    stu[j - 1] = stu[j];
                    stu[j] = tmp;
                }
            }
        }
        return stu;
    }

为了方便理解博主简单的写了一个冒泡排序,然后该方法是静态方法,可以直接使用类名调用,静态的泛型方法需要声明一下类型 <T>。

可以看到非常的成功,如果我们想要根据姓名来排序,直接new 一个我们设计的name类来作为参数即可。

总结:

1. equals 适用于比较两个对象之间的相等于否。
2. Comparable 泛型接口 适用于类的内部比较,如果想要对自定义对象按照某种方式来进行比较,则需要该类需要实现 Comparable 接口,并重写 compareTo() 方法,这 种情况下比较方式基本上是写死的
3. Comparable 泛型接口 适用于自定义类型的灵活比较,需要自己选择比较器对象,提供比较器类,可以在类的内部,也可以在类的外部传参比较器,这个需要根据自己的设计和需求来,使用该接口需要重写 compare() 方法。

至此,Java 的对象的比较博主已经分享完了,希望对大家有所帮助,如有不妥之处欢迎批评指正。

本期收录于博主的专栏——JavaSE,适用于编程初学者,感兴趣的朋友们可以订阅,查看其它“JavaSE基础知识”。

感谢每一个观看本篇文章的朋友,更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★*

do

cc即撒后看来大家了类

猜你喜欢

转载自blog.csdn.net/weixin_67603503/article/details/129245934