JavaSE学习之--抽象类,接口,内部类(二)+https://developer.aliyun.com/article/1413499
3.hashCode方法
对象的hashCode值反应的是其在内存中的存储位置
hashCode的源码:
注:native代表此方法是由c/c++写的,无法查看真正的源码
public static void main(String[] args) { // 创建两个内容完全相同的person对象 Person person1 = new Person("lisi",18); Person person2 = new Person("lisi",18); // 验证他们在内存中是否位于同一地址 System.out.println(person1.hashCode()); System.out.println(person2.hashCode()); // 结果显示并不位于同一块地址 }
重写hashCode方法:
同样的,我们也可以重写hashCode方法,实现只要内容完全相同,对象就处于内存中的同一块地址(快捷方法和toString一样)
// 重写hashCode方法 @Override public int hashCode() { return Objects.hash(name, age); } public static void main(String[] args) { // 创建两个内容完全相同的person对象 Person person1 = new Person("lisi",18); Person person2 = new Person("lisi",18); // 验证他们在内存中是否位于同一地址 System.out.println(person1.hashCode()); System.out.println(person2.hashCode()); }
结论:
1、hashCode方法用来确定对象在内存中存储的位置是否相同
2、事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的 散列码,进而确定该对象在散列表中的位置。
总结:
如果是自定义类型,记得一定要重写equals和hashCode方法,因为你的逻辑不是根据地址来判断类型是否相同,而是根据类型的属性来判断,所以要重写这两个方法
四. 接口使用实例
1.Comparable接口和Comparator接口
先设定一个学生对象,并创建一个学生数组
class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } } public class Test2 { public static void main(String[] args) { Student[] students = new Student[] { new Student("张三", 10), new Student("李四", 20), new Student("王五", 30), new Student("赵六", 40), }; } }
假如我们现在想通过年龄进行排序,能否直接利用Arrays.sort呢?
Arrays.sort(students);// 可以直接这样排序吗?
发现产生类型转换异常,原因在于之前使用Arrays.sort排序的数组是整形,可以直接通过比较数字的大小来进行排序的,而student是一个引用类型,无法直接进行排序,必须指定排序的依据,比如我现在想通过年龄进行排序,该怎么实现呢?通过Comparable接口!!!
class Student implements Comparable<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 + '}'; } @Override // 重写compareTo public int compareTo(Object o) { Student s = (Student) o; /* if (this.age > s.age) { return 1; } else if (this.age == s.age) { return 0; }else { return -1; }*/ return this.age-s.age; } } // 输出打印 Arrays.sort(students); System.out.println(Arrays.toString(students));
sort方法会自动调用compareTo方法,compareTo方法的参数是Object类型,要进行强制类型转换。通过重写Comparable接口中的compareTo方法实现根据类的属性进行比较的目的
如果数据类型是数字可以直接调用sort方法,如果数组的数据类型是对象,则要使每个对象具有“可比较性”,就是要让对应的类实现Comparable接口,并重写compareTo方法 ,你需要告诉编译器是通过类的哪项属性进行比较的
但是我们发现这个方法也有一定的缺陷性,缺陷性在于我们重写compareTo方法时只能指定类的一个属性进行比较,比如在上述代码中的compareTo方法中,我们是通过age这个属性来比较的,但如果我们想通过名字比较呢?那不是要重写compareTo方法吗?可以看出,这样进行比较的方法可拓展性太差,对类的侵入性强,那有没有一种方法可以实现想通过什么比较就通过什么比较呢?答案是有的,即通过“比较器”来进行比较
使用方法:构造一个比较类,实现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 "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } } // <Student>表示比较的是Student对象(最好加上,在重写方法时会很方便) 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 Test2 { public static void main(String[] args) { Student student1 = new Student("zhangsan",10); Student student2 = new Student("lisi",15); AgeComparator ageComparator = new AgeComparator(); System.out.println(ageComparator.compare(student1, student2)); NameComparator nameComparator = new NameComparator(); System.out.println(nameComparator.compare(student1,student2)); }
解释一下:“return o1.name.compareTo(o2.name);”为什么通过name能直接调用compareTo方法?
因为name是一个字符串类型,属于String类,而String类中有compareTo方法!!!
注意:Comparator和Comparable是两个不同的接口,且用法也不同
Comparator是为了构造比较器
Comparable是使类具有可比较性
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序
// 能够排序的数组,要求数组元素必须具有可比较性,那把每个类型都设置为Comparable就能实现可比较性 public static void bubble_sort(Comparable[] comparables) { for (int i = 0; i < comparables.length-1; i++) { for (int j = 0; j < comparables.length-1-i ; j++) { if(comparables[j].compareTo(comparables[j+1]) > 0) { // 不符合顺序就交换位置 Comparable tmp = comparables[j]; comparables[j] = comparables[j+1]; comparables[j+1] = tmp; } } } }
2.Clonable 接口和深拷贝
Java中内置了很多有用的方法,clone就是其一,clone方法是Object类内置的一个方法,clone方法能够实现对象的拷贝,但要合法使用clone方法,需要先实现Clonable接口,否则会报错
原型:
代码实现:
class Money implements Cloneable { public double money = 19.9; } // 要想能clone,必须先实现Clonable接口 class Stu implements Cloneable{ int age; Money m; public Stu(int age) { this.age = age; m = new Money(); } // 重写Object类中的clone方法 // 访问权限是protected,只能跨包子类中使用 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } public class Testdemo1 { public static void main(String[] args) throws CloneNotSupportedException { Stu stu1 = new Stu(18); // 返回值是Object,所以必须进行强制类型转换 Stu stu2 = (Stu) stu1.clone(); System.out.println(stu1.m.money);// 19.9 System.out.println(stu2.m.money);// 19.9 System.out.println("================"); } }
注意:main后面必须添加:
throws CloneNotSupportedException
否则会报错
鼠标移动到clone,同时按下:alt+enter,点击第一行即可自动添加
现在我改变Stu2的m对象的money,再分别打印,看看会有什么结果
// 返回值是Object,所以必须进行强制类型转换 Stu stu2 = (Stu) stu1.clone(); System.out.println(stu1.m.money);// 19.9 System.out.println(stu2.m.money);// 19.9 System.out.println("================"); stu2.m.money = 20; System.out.println(stu1.m.money); System.out.println(stu2.m.money);
可以发现不仅Stu2的Money改变了,Stu1的Money也改变了,可我们不是只改变了Stu2的money吗?原因在于Cloneable的拷贝是一种浅拷贝,所谓浅拷贝就是只拷贝当前对象(Stu),并不拷贝对象里面的对象(m) ,让我画图解释一下
有浅拷贝就有深拷贝,深拷贝就是为了解决浅拷贝的缺陷存在的
深拷贝:解决对象里面嵌套对象的拷贝
拷贝方法:先克隆大对象,再克隆小对象(先暂存大对象)
修改后的Stu类
// 要想能clone,必须先实现Clonable接口 class Stu implements Cloneable{ int age; Money m; public Stu(int age) { this.age = age; m = new Money(); } // 重写Object类中的clone方法 // 访问权限是protected,只能跨包子类中使用 @Override protected Object clone() throws CloneNotSupportedException { // 深拷贝:先克隆大对象,再克隆小对象 Stu tmp = (Stu)super.clone(); // 克隆小对象 // 要克隆的是当前对象的里面的m对象,所以通过this表示当前对象调用 // 将当前对象的m克隆到tmp的m之内,所以两边要对齐 tmp.m = (Money)this.m.clone(); return tmp; } }
运行截图:
深拷贝 浅拷贝看的是代码的实现过程,浅拷贝并不实现对象内的对象的拷贝,深拷贝实现对象内的对象的拷贝
注意:能被拷贝的对象一定要实现clone接口!!!
五.补充:内部类,外部类
1.内部类:
定义在类里面或方法内部的类就叫做内部类
2.分类:
说内部类时一定要说清楚是什么内部类,对于内部类来说重点掌握静态内部类和匿名内部类
1.实例内部类
在类中定义的不被static修饰的类就叫做实例内部类
class OuterClass { int data1 = 1; int data2 = 2; // 实例内部类 class InnerClass { int data3 = 3; int data4 = 4; // 实例内部类中的成员变量不能被static修饰,但可以是static final修饰 // static int data5 = 5; static final int data10 = 10; public void method3() { System.out.println(data3); } public void method4() { // 访问外部类的成员变量,先实例化一个外部对象,通过外部对象访问成员变量 OuterClass outerClass = new OuterClass(); System.out.println(data4); System.out.println(data1);// 打印内部类的data1 // System.out.println(data5); System.out.println("================="); System.out.println(OuterClass.this.data1);// 打印外部类的data1 System.out.println(outerClass.data2); } } } public static void main(String[] args) { // 实例化一个实例内部类-->先实例化一个外部类对象,再实例化此外部类对象的内部类 // 实例内部类依赖于外部类对象 // 第一种写法(实例化出一个具体的外部类对象) OuterClass outerClass = new OuterClass(); OuterClass.InnerClass innerClass = outerClass.new InnerClass(); // 第二种写法(用了一个匿名的外部类对象) OuterClass.InnerClass innerClass2 = new OuterClass().new InnerClass(); // OuterClass.InnerClass innerClass = new OuterClass.InnerClass();// err innerClass.method3(); innerClass.method4(); }
总结:
1.实例内部类依赖于外部类对象,必须先有外部类对象才能实例化内部类对象
2.实例化一个内部类对象的方法有两种
- 先实例化出一个具体的外部类对象,再实例化此外部类对象的内部类对象(注意语法格式)
- 利用匿名外部类对象实例化内部类对象(很快速,但无具体的外部类对象)
3.实例内部类中的成员变量不能被static修饰,因为被static修饰的变量不依赖于对象,在类加载时就创建,而实例内部类的实例化又依赖于对象,两者矛盾。但如果被final修饰,则编译通过(此时已经是常量了)
4.当外部类成员变量和内部类成员变量相同时,会默认打印内部类的成员变量,Outerclass.this.访问外部类的成员变量
2.静态内部类
在类中定义的被static修饰的类就叫做静态内部类
我们都知道,类的成员变量如果被static修饰,那就说明这个成员变量是“类变量”,不依赖于对象。同样的,对于静态内部类的获取不依赖于外部类对象!
class OuterClass { int data1 = 1; int data2 = 2; // 静态内部类 static class InnerClass { int data3 = 3; int data4 = 4; static int data5 = 5; public void method3() { System.out.println(data3); } public void method4() { // 访问外部类的成员变量,先实例化一个外部对象,通过外部对象访问成员变量 OuterClass outerClass = new OuterClass(); System.out.println(data4); System.out.println(data5); System.out.println("================="); System.out.println(outerClass.data1); System.out.println(outerClass.data2); } } } public class Test1 { public static void main(String[] args) { // 如何实例化一个静态内部类对象呢? // 一定是先有外部类,再有内部类,也就是说一定要指明内部类的归属 OuterClass.InnerClass innerClass = new OuterClass.InnerClass(); innerClass.method3(); innerClass.method4(); } }
总结:
1.静态内部类是定义在类内部的,被static修饰的类
2.实例化一个静态内部类:外部类名.内部类名--》一定是先有外部类,再有内部类,也就是说一定要指明内部类的归属
3.在内部类中想要访问外部类的成员变量--》先实例化一个外部类对象,通过外部类对象访问成员变量
3.匿名内部类(通过接口实现)
// 匿名内部类 interface A { void methodA(); } public class Test1 { public static void main(String[] args) { int a = 10; a = 20;// err如果修改,在匿名内部类中就无法打印 final int b = 20; new A(){// 以下代码相当于一个类实现了A接口,并重写了A的方法,但是这个类没有名称,所以叫做匿名内部类()通过接口实现 @Override public void methodA() { // System.out.println(a); System.out.println(b); System.out.println("hehe!!!"); } }.methodA(); } }
总结:
1.在匿名内部类中只能打印被final修饰数据(常量) 或只被初始化未被修改过的数据(如果修改,就无法访问),所以建议在匿名内部类中访问的数据都设置为final修饰的
2.匿名内部类是通过接口实现的,调用方法有两种
- 直接在花括号外.方法名
- 创建一个对象,通过对象调用
4.局部内部类
在方法中定义的类就是局部内部类
public class OutClass { int a = 10; public void method(){ int b = 10; // 局部内部类:定义在方法体内部 // 不能被public、static等访问限定符修饰 class InnerClass{ public void methodInnerClass(){ System.out.println(a); System.out.println(b); } } // 只能在该方法体内部使用,其他位置都不能用 InnerClass innerClass = new InnerClass(); innerClass.methodInnerClass(); } public static void main(String[] args) { // OutClass.InnerClass innerClass = null; 编译失败 } }
总结:
1.局部内部类只能在定义的方法之中使用,子其他位置均无法使用
2.局部内部类非常少见,了解即可
补充:
一个类对应一个字节码文件
实例,静态内部类和匿名内部类的字节码文件名称不同,
实例,静态内部类的字节码文件名称:外部类名$内部类名
匿名内部类的字节码文件名称:外部类类名$数字
总结:
本文主要讲述了抽象类,接口的定义,使用,以及两者的区别。抽象类和接口都是面向对象编程的重要语法,要好好理解他们的作用和基础的语法,会大大提高代码的效率!最后还介绍了一种特殊的类--》内部类 ,重点掌握静态内部类和匿名内部类