前言
上期我们深入探讨了Java中的接口,其实Java中内置了很多非常有用的接口,为了能够进一步加深对接口的认识,也为能够灵活掌握这些常见接口的使用,我们就这期单独谈谈接口的实例。
一、初识Object类
在后面介绍接口的使用时,我们绕不开要使用Object类,为了后面能够更好的理解,我们这里就将Object类放到前面讲解。
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收;所有的类都可以重写Object的方法。Object类中方法都特别重要,由于这里我们还没学习多线程,所以就目前介绍一下如下四种:
🍑1、toString()
toString
方法的作用是将对象转换为字符串形式。通常为了方便输出对象的内容,需要重写toString方法。
class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } //重写toString @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class Test { public static void main(String[] args) { Student student = new Student("张三",18); System.out.println(student); }
🍑2、hashCode()
如果不重写Object类的toString方法,默认会调用Object类的toString方法,源码如下:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
假如不重写toString方法,这次我们再来运行:
这次我们并没有输出对象的内容,而是输出了一串数字,观察源码(我们先不理解@前的部分)显然这是Integer.toHexString(hashCode())
搞的鬼,那么hashCode究竟是什么呢?
加上这段代码System.out.println(student.hashCode());我们再来测试一下:
我们暂且将其理解为计算对象的位置,暂将这段地址看做对象的地址。其实hashcode这个方法对于我们目前来说是用不上的,这里先埋个伏笔,之后学了Hashmap再来详细介绍它的用法。
🍑3、equals()
equals
方法在Object类中的源码:
public boolean equals(Object obj) { return (this == obj); }
源码中的equals
比较的是对象的引用是否相同,显然这样默认的equals方法在对象的比较中是几乎用不到的,所以一般我们会对这个equals方法进行重写以满足实际的比较需求。
比如如果我们将两个名字一样的学生看作相同的对象,我们就可以这样重写equals方法:
public boolean equals(Object obj) { if ( obj == null ) {//空对象和任何非空对象不同 return false; } if ( this == obj ) {//引用相同对象必相同 return true; } if(! (obj instanceof Student)) {//如果obj不是Student类的实例 return false; //那么必然不可能等于this } Student student = (Student)obj; if(this.name.equals(student.name)) {//这里利用了String类中重写的equals方法 return true; } return false; }
测试:
//…… public class Test { public static void main(String[] args) { Student student1 = new Student("张三",18); Student student2 = new Student("张三",25); if(student1.equals(student2)) { System.out.println(student1+"和"+student2+"是同一个人!"); } } }
🍑4、clone()
clone()方法是用来复制一个对象,关于如何复制,这里涉及到了接口的知识,下面来详细介绍:👇
三、对象的深浅拷贝
1、Clonable接口
Clonable
接口源码:
public interface Cloneable { }
注:Clonable是一个空接口,也叫标记接口,只要一个类实现了这个接口就标志这个类是可以克隆的,否则不可以克隆。
2、重写clone方法
clone方法是Object类下protected修饰的一个native方法,如果希望提供从类(被克隆的类)外部复制其对象的功能,则可以覆盖Object.clone()作为公共对象,只需在内部调用super.clone()仍使用默认实现即可。(这里不太好理解,大家简单了解,主要是clone()的使用)
//浅拷贝实现方式 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }
//深拷贝实现方式不唯一,需要具体情况具体分析。
完成了上面两个步骤我们就可以开始进行对象的拷贝了,根据重写方法的实现方式,我们将拷贝分为浅拷贝和深拷贝,下面分别详细介绍:
🍑1、浅拷贝
浅拷贝: 按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址(引用) ,因此如果其中一个对象改变了这个引用下的数据,就会影响到另一个对象。(浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。)
理论太过枯燥,下面我们直接测试代码:
//--------------浅拷贝---------------- class Money implements Cloneable{ public double m = 15.3; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Money{" + "m=" + m + '}'; } } class Student implements Cloneable{ public String name; public int age; public Money money = new Money(); public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } //浅拷贝 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } //测试浅拷贝 public class Test { //throws CloneNotSupportedException先记忆固定格式,后期讲解 public static void main(String[] args) throws CloneNotSupportedException { Student student1 = new Student("张三",18); Student student2 = (Student) student1.clone(); System.out.println("修改前:"+student1); System.out.println("修改前:"+student2); System.out.print("\n"); student1.money.m=10.0; System.out.println("修改后:"+student1); System.out.println("修改后:"+student2); } }
🍑2、深拷贝
深拷贝: 在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。(深拷贝把要复制的对象所引用的对象都复制了一遍。)
实现深拷贝的核心就是对clone方法重写的实现,下面我们还以上面的Student-Money为例,重写一下深拷贝的方法:
@Override protected Object clone() throws CloneNotSupportedException { //1.先克隆student对象 Student stutclone = (Student) super.clone(); //2.克隆student对象中的money引用对象,并将克隆的新引用赋值给stuclone stutclone.money = (Money) this.money.clone(); //3.将得到的深拷贝对象引用返回 return stutclone; }
同样的测试用例,测试结果:
🍑3、深浅拷贝的特点
通过上面的例子,相信大家对深浅拷贝已经渐入佳境,下面就针对上面观察到的现象对深浅拷贝做一个简单的小结,加深一下印象。
浅拷贝特点 :
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
(2) 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。
深拷贝特点 :
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
(2) 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
(3) 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
(4) 深拷贝相比于浅拷贝速度较慢并且花销较大。
二、对象数组排序
在设计程序时我们会经常遇到给对象数组排序的问题,此时我们就可以用到Java为我们提供的两个比较接口对对象数组进行排序。
🍑1、通过Comparable接口排序
当我们使用Arrays.sort(数组名)
排序对象数组时,内部用到Comparable接口,所以我们进行排序时也要对排序类实现Comparable接口。
Comparable接口源码:
public interface Comparable<T> { public int compareTo(T o); }
注意事项:
- 源码中的代表泛型,这里我们写成比较对象的类名,先不理解会用即可。
- 对于一个对象可能会有多个成员属性,所以排序时我们要重写Comparable中的compareTo方法用来指定排序规则。
compareTo比较时返回值对于不同的情况可能会五花八门,这就给我们重写方法带来了障碍,所以这里有一种简单的技巧:
- 当我们比较的是整形家族的成员时,升序写成
this.成员名-o.成员名;
。 降序写成o.成员名-this.成员名;
。.- 当我们比较的是字符串类型的成员时,升序写成
this.成员名.compareTo(o.成员名);
。降序写成o.成员名.compareTo(this.成员名);
比如下面对一个学生对象的数组进行排序:
class Student implements Comparable<Student>{ private String name; private 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 Test { //按年龄排序 public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("zhangsan",18); students[1] = new Student("lisi",39); students[2] = new Student("wangwu",26); Arrays.sort(students); System.out.println(Arrays.toString(students)); } }
按姓名排序:(重写compareTO方法即可)
@Override public int compareTo(Student o) { return this.name.compareTo(o.name); }
🍑2、通过Comparator接口排序
我们通常将实现Comparator接口的类称为一个比较器,通过Arrays.sort(数组名,比较器类名)
实现对对象数组的排序。
下面仍以学生对象为例,这次通过实现Comparator接口实现对其排序:
import java.util.Arrays; import java.util.Comparator; class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } } //姓名比较器 class NameCompare implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.getName().compareTo(o2.getName()); } } //年龄比较器 class AgeCompare implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.getAge()- o2.getAge(); } } //测试用例 public class Test { public static void main(String[] args) { Student[] students = new Student[3]; NameCompare nameCompare = new NameCompare(); AgeCompare ageCompare = new AgeCompare(); students[0] = new Student("zhangsan",18); students[1] = new Student("lisi",39); students[2] = new Student("wangwu",26); Arrays.sort(students,nameCompare);//按姓名排序 System.out.println(Arrays.toString(students)); Arrays.sort(students,ageCompare);//按年龄排序 System.out.println(Arrays.toString(students)); } }
实现Comparable接口就可以实现对象的比较,那么为什么还要引出Comparator接口呢?
其实,实现Comparable接口的方式比实现Comparator接口的耦合性更强,也就是说,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改。从这个角度说,通过实现Comparator接口的比较,使用起来会更加灵活。
小结
本章浅浅介绍了Object类中的四种方法,另外本期重点:
重点一: 是能够理解并且灵活的使用Clonable接口搭配clone方法进行深浅拷贝。
重点二: 是能够灵活应用Comparator和Comparable给对象数组进行排序。
重点三: 通过这些例子再次加深对接口的理解。