2020-11-03 多态、final、Object
多态
多态一般分为两种:重写式多态和重载式多态。
-
重载式多态,也叫编译时多态。也就是说这种多态再编译时已经确定好了。重载大家都知道,方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。
但是这里是有歧义的,有的人觉得不应该把重载也算作多态。因为很多人对多态的理解是:程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,这种情况叫做多态。 这个定义中描述的就是我们的第二种多态—重写式多态。并且,重载式多态并不是面向对象编程特有的,而多态却是面向对象三大特性之一(如果我说的不对,记得告诉我。。)。
我觉得大家也没有必要在定义上去深究这些,我的理解是:同一个行为具有多个不同表现形式或形态的能力就是多态,所以我认为重载也是一种多态,如果你不同意这种观点,我也接受。
-
重写式多态,也叫运行时多态。这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。 这种多态通过函数的重写以及向上转型来实现,我们上面代码中的例子就是一个完整的重写式多态。我们接下来讲的所有多态都是重写式多态,因为它才是面向对象编程中真正的多态。
多态存在的三个必要条件
-
继承
-
重写
-
父类引用指向子类对象
比如:
Parent p = new Child();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
-
成员变量,静态方法看左边;
-
非静态方法:编译看左边,运行看右边。
老毕在讲到多态执行问题时,结合下面的例子,给我们总结了一套口诀:“成员变量,静态方法看左边;非静态方法:编译看左边,运行看右边。意思是:当父类变量引用子类对象时,在这个引用变量f指向的对象中,他的成员变量和静态方法与父类是一致的,他的非静态方法,在编译时是与父类一致的,运行时却与子类一致(发生了复写)。
例:
class Fu {
intnum = 5;
static void method4() {
System.out.println("fu method_4");
}
void method3() {
System.out.println("fu method_3");
}
}
class Zi extends Fu {
intnum = 8;
static void method4() {
System.out.println("zi method_4");
}
void method3() {
System.out.println("zi method_3");
}
}
class DuoTaiDemo4 {
public static void main(String[] args) {
Fu f = new Zi();
System.out.println(f.num);//与父类一致
f.method4();//与父类一致
f.method3();//编译时与父类一致,运行时与子类一致
Ziz = new Zi();
System.out.println(z.num);
z.method4();
z.method3();
}
}
输出结果:
5
fu method_4
zi method_3
8
zi method_4
zi method_3
当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
public class Test { public static void main(String[] args) { A a1 = new A(); A a2 = new B(); B b = new B(); C c = new C(); D d = new D(); System.out.println("1---"+a1.Show(b)); System.out.println("2---"+a1.Show(c)); System.out.println("3---"+a1.Show(d)); System.out.println("4----"+a2.Show(b)); System.out.println("5----"+a2.Show(c)); System.out.println("6----"+a2.Show(d)); System.out.println("7---"+b.Show(b)); System.out.println("8---"+b.Show(c)); System.out.println("9---"+b.Show(d)); } } System.out.println("1---"+a.show(b));//a.show(b) -->A没有父类 跳过--> a.show(A obj) 类A有这个方法、执行 String show(A obj) { return("A-A"); } System.out.println("2---"+a.show(c));//a.show(c) -->A没有父类 跳过--> a.show(B obj) -->a.show(b) -->A没有父类 跳过--> a.show(A obj) 类A有这个方法、执行 String show(A obj) { return("A-A"); } System.out.println("3---"+a.show(d));//传入d 直接加载String show(D obj){ return ("A-D"); } System.out.println("--------------"); System.out.println("4----"+a2.show(b));//a2.show(b) -->A没有父类 跳过--> a2.show(A obj) 类A有这个方法、但是被重写、执行String show(A obj) { return("B-A"); } System.out.println("5----"+a2.show(c));//a2.show(c) -->A没有父类 跳过--> a2.show(B obj)--> a2.show(b) -->A没有父类 跳过-->a2.show(A obj) 类A有这个方法、但是被重写、执行String show(A obj) { return("B-A"); } System.out.println("6----"+a2.show(d));//a2.show(d) this.show(d)直接执行 String show(D obj){ return ("A-D"); } System.out.println("--------------"); System.out.println("7---"+b.show(b));//B.show(b)没有-->A.show(b)-->B.show(A) 执行String show(A obj) { return("B-A"); } System.out.println("8---"+b.show(c));//B.show(c)没有-->A.show(c)-->B.show(B)-->A.show(B)-->B.show(A)执行String show(A obj) { return("B-A"); } //假如在这里把String show(A obj) { return("B-A"); }注释掉,找不到B.show(A)就会去更高级的super.show(super(super)(O) A.show(A)中寻找 System.out.println("9---"+b.show(d));//B.show(d)没有这个方法、跳过-->去super也就是A中找A.show(d)搞定
final
final关键字可以用来修饰引用、方法和类。
1.用来修饰一个引用
-
如果引用为基本数据类型,则该引用为常量,该值无法修改;
final固定的是一个地址。
-
如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
-
如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
例子如下所示:
final class Person { String name ="zs"; //3. 此处不赋值会报错 //final int age; final int age = 10; } public class Demo01 { public static void main(String[] args) { //1. 基本数组类型为常量,无法修改 final int i = 9; //i = 10; //2. 地址不能修改,但是对象本身的属性可以修改 Person p = new Person(); p.name = "lisi"; final int[] arr = {1,2,3,45}; arr[3] = 999; //arr = new int[]{1,4,56,78}; } }
2.用来修饰一个方法
当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
class Person { public final void say() { System.out.println("说...."); } public void eat() { System.out.println("吃..."); } } class Teacher extends Person { //1. final修饰的方法不能被重写,但此方法仍然被继承 /*@Override public void say() { System.out.println("老师在一本正经的说..."); }*/ public void eat() { System.out.println("老师在大口大口的吃..."); } } public class Demo02 { public static void main(String[] args) { Teacher t = new Teacher(); t.say(); } }
3.用来修饰类
当用final修改类时,该类成为最终类,无法被继承。简称为“断子绝孙类”。
/*** * final用法3:修饰类,则该类成为最终类,无法被继承 * @author Administrator * */ final class Person02 { } //class Teacher02 extends Person02 { } //class MyString extends String{} public class Demo03 { }
比如常用的String类就是最终类。
Object
一、为什么重写equals()方法一定要重写hashCode()方法
1.首先解释equals方法和hashcode方法分别是用来干什么的?
equals()方法:
在Object类源码(如下所示)中,其底层是使用了“==”来实现,也就是说通过比较两个对象的内存地址是否相同判断是否是同一个对象。
public boolean equals(Object obj) { return (this == obj); }
但是在实际应用中,该方法不能满足的我们的需求。因为我们认为两个对象即使不是指向的同一块内存,只要这两个对象的各个字段属性值都相同,那么就认为这两个对象是同一个对象。所以就需要重写equals()方法,即如果两个对象指向内存地址相同或者两个对象各个字段值相同,那么就是同一个对象。
hashCode()方法:
一提到hashcode,很自然就想到哈希表。将某一key值映射到表中的一个位置,从而达到以O(1)的时间复杂度来查询该key值。Object类源码中,hashCode()是一个native方法,哈希值的计算利用的是内存地址。
public native int hashCode();
可以认为利用哈希表也能起到一定的判重的作用,但是现实是可能存在哈希冲突,即使是两个不同的对象,他们的哈希值也可能相同,如何解决哈希冲突。总之,我们记住哈希表具有优越的查询性能,并且存在哈希冲突。
hash冲突:
public static void main(String[] args) { String astring = "Ok"; String bString = new String("Ok"); System.out.println(astring.hashCode()); System.out.println(bString.hashCode()); }
输出:
2556
2556
2.equals()方法和hashCode()方法两者有什么关系?
-
如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相 同!!!!;
-
如果两个对象不同(即用equals比较返回false),那么它们的hashCode值可能相同也可能不同;
\3. 如果两个对象的hashCode相同(存在哈希冲突),那么它们可能相同也可能不同(即equals比 较可能是false也可能是true)
4.如果两个对象的hashCode不同,那么他们肯定不同(即用equals比较返回false)
3.最后来看为什么重写equals()就一定要重写hashCode()方法?
对于对象集合的判重,如果一个集合含有10000个对象实例,仅仅使用equals()方法的话,那么对于一个对象判重就需要比较10000次,随着集合规模的增大,时间开销是很大的。但是同时使用哈希表的话,就能快速定位到对象的大概存储位置,并且在定位到大概存储位置后,后续比较过程中,如果两个对象的hashCode不相同,也不再需要调用equals()方法,从而大大减少了equals()比较次数。
所以从程序实现原理上来讲的话,既需要equals()方法,也需要hashCode()方法。那么既然重写了equals(),那么也要重写hashCode()方法,以保证两者之间的配合关系。
基于以上分析,我们可以在Java集合框架中得到验证。由于HashSet是基于HashMap来实现的,所以这里只看HashMap的put方法即可。源码如下
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); //这里通过哈希值定位到对象的大概存储位置 int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //if语句中,先比较hashcode,再调用equals()比较 //由于“&&”具有短路的功能,只要hashcode不同,也无需再调用equals方法 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
我们在实际应用过程中,如果仅仅重写了equals(),而没有重写hashCode()方法,会出现什么情况?
字段属性值完全相同的两个对象因为hashCode不同,所以在hashmap中的table数组的下标不同,从而这两个对象就会同时存在于集合中,所以重写equals()就一定要重写hashCode()方法。
对于“为什么重写equals()就一定要重写hashCode()方法?”这个问题应该是有个前提,就是你需要用到HashMap,HashSet等Java集合。用不到哈希表的话,其实仅仅重写equals()方法也可以吧。而工作中的场景是常常用到Java集合,所以Java官方建议 重写equals()就一定要重写hashCode()方法。
二、java修饰符的访问范围
14、● 请你解释Object若不重写hashCode()的话,hashCode()如何计算出来的? 考点:基础
参考回答: Object 的 hashcode 方法是本地方法,也就是用 c 或 c++ 实现的,该方法直接返回对象的内存地址。 如果没有重写hashCode(),则任何对象的hashCode()值都不相等(而hashmap想让部分值的hashCode值一样,所以就要重写)
“15、● 请你解释为什么重写equals还要重写hashcode?” 考点:java基础
参考回答: 由题目14知道没有重写hashCode(),则任何对象的hashCode()值都不相等。 HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等。 如果只重写equals没有重写hashCode(),就会导致相同的key值也被hashcode认为是不同的key值(因为没有重写hashCode(),则任何对象的hashCode()值都不相等),就会在hashmap中存储相同的key值(map中key值不能相同),这就不符合条件了。
equals和hashcode的关系: 1、如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同; 2、如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)
不重写public native int hashCode();因为没有重写hashCode(),则任何对象的hashCode()值都不相等,如果没有重写hashCode(),则任何对象的hashCode()值都不相等。如果只重写equals没有重写hashCode(),就会导致相同的key值也被hashcode认为是不同的key值(因为没有重写hashCode(),则任何对象的hashCode()值都不相等)。
128陷阱
在Integet的valueOf()方当中,如果我们的数值在-128-127之间的数值都存储在有一个catch数组当中,该数组相当于一个缓存,当我们在-128-127之间进行自动装箱的时候,我们就直接返回该值在内存当中的地址,所以在-128-127之间的数值用==进行比较是相等的。而不在这个区间的数,需要新划分出一个内存空间,所以不相等。
包装类
一、为什么要有包装类
1.在面向对象中,“一切皆对象”,但基本数据类型的数据不太符合这一理念,基本数据类型不是对象
2.涉及到进制之间的转化,数据类型之间的基本操作;如果都有我们自己去实现,那么工作量过大。
所以java针对每一个基本数据类型都设计了一个包装类
二、自动拆装箱
1.装箱:把基本类型数据转成对应的包装类对象。
方式一:Integer i = Integer.value(13);
方式二:Integer i = new Integer(13);
2.拆箱:把包装类对象转成对应的基本数据类型数据。
int value = i.intValue();
自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)
在Java 5之前的版本中,基本数据类型和包装类之间的转换是需要手动进行的,但Sun公司从Java5开始提供了的自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)操作 ;
自动装箱:可以把一个基本类型变量直接赋给对应的包装类型变量。
比如:Integer i = 13;
自动拆箱:允许把包装类对象直接赋给对应的基本数据类型变量。
比如:Integer i = new Integer(13); Int j = i;
java抽象
什么是抽象:
抽象类是为了把相同的但不确定的东西的提取出来,为了以后的重用。定义成抽象类的目的,就是为了在子类中实现抽象类。
抽象类的特点
//1.由abstract修饰的类叫做抽象类,也可以修饰抽象方法 //2.abstract修饰的抽象方法可以不在抽象类当中实现,但一定要在子类当中重写,并实现 //3.只有抽象类当中才能有抽象方法,普通类当中不能有抽象方法 //4.抽象类当中不一定全是抽象方法,也可以使用普通方法,不同方法不同重写 //5.抽象类不能被实例化,但是可以使用多态,不能有构造方法 //6.final 不能和abstract同时使用,fianl修饰的方法禁止重写, abstract修饰的方法要求重写 ,冲突 //7.private修饰的方法时子列不可见的, abstract修饰的方法要求重写,冲突 //8.抽象方法不能使用static. public abstract class Animal { int a ; private void getRun() { System.out.println("我是一个普通的跑方法。。。。。。"); } abstract void run(); }
package com.kyz.learn; public abstract class Abstract { abstract void Abstract(int i); public void commonmethod(){ System.out.println("抽象类的普通方法"); } } Student student = new Student(); student.commonmethod(); student.Abstract(23);