1. 多态基础
可以理解为一个事物的多种形态。其体现就是,用父类的类型声明一个对象变量,但是右边 new 的是其一个子类:
Person p1 = new Student() ; // 父类的引用指向子类的对象
多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类**重写**父类的方法 ==> 虚拟方法调用<br />Person类:
package pkg2;
public class Person {
// 属性
private String name;
private int age;
// 构造器
public Person(String name){
this.name = name;
}
// 方法
public void showMe(){
System.out.println("我叫"+name+",我是个人");
}
public String getName(){
return name ;
}
}
Man 类:
package pkg2;
public class Man extends Person{
public Man(String name){
super(name);
}
public void showMe(){
System.out.println("我叫"+getName()+",我是个男人");
}
public void showMan(){
System.out.println("I'm a man");
}
}
Woman 类:
package pkg2;
public class Woman extends Person{
public Woman(String name){
super(name);
}
public void showMe(){
System.out.println("我叫"+getName()+",我是个女人");
}
public void showWoman(){
System.out.println("I'm a woman");
}
}
test:
package pkg2;
public class test {
public static void main(String[] args) {
Person p1 = new Woman("张三");
Person p2 = new Man("李四");
p1.showMe();
System.out.println(p1.getName());
p2.showMe();
System.out.println(p2.getName());
}
}
上述代码中,p1、p2 均使用了多态,他们的 showMe 方法均是**重写后的** showMe 方法,而 getName 没有被重写,依然是父类 Person 的 getName 方法。所以 test 运行结果为:<br /><br />但是,p1、p2 只能调用 Person 类中已有的方法,无法调用 Man 或 Woman 中独有的属性方法!原因很简单,在编译时就确定 p1、p2 是一个Person类型,他拥有的所有属性和方法都是 Person 拥有的属性和方法。<br /><br />也就是说,p1、p2 所能调用的方法取决于其类型Person,而不是右边用来new的类。<br />总结:**编译看左边,运行看右边**
2. 使用前提
- 要有类的继承关系
- 子类要有方法重写
- 多态只适用于方法,不适用于属性
- 对属性而言,编译和运行都看左边的,即子父类若有同名属性,多态依然调用父类属性
3. 多态的意义
现给定一个方法func,它以某一个类A作为参数输入。B、C 都是A的子类,由于多态的存在,B、C均可作为func的参数,这样以来就可以用一个方法来表现出某一类的多个子类,这就是“多种形态”的体现。下面给个例子:
Animal父类:
package pkg3;
public class Animal {
public void eat(){
System.out.println("动物要吃东西");
}
public void shout(){
System.out.println("动物会叫");
}
}
Dog子类:
package pkg3;
public class Dog extends Animal{
public void eat(){
System.out.println("狗吃肉");
}
public void shout(){
System.out.println("汪!汪!汪!");
}
}
Cat子类:
package pkg3;
public class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
public void shout(){
System.out.println("喵!喵!喵!");
}
}
test:
package pkg3;
public class test {
public static void main(String[] args){
test t1 = new test() ; // 用这种方法可以调用test自身的方法
t1.AnimalTest(new Dog()); // Animal animal = new Dog()
t1.AnimalTest(new Cat()); // Animal animal = new Cat()
}
public void AnimalTest(Animal animal){
animal.eat();
animal.shout();
}
}
运行结果:<br /><br />可以看到,AnimalTest 虽然以 Animal 作为形参类型,但是可以传入 Dog 和 Cat 型的对象,从而执行 Dog 和 Cat 中重写的方法,这就是多态的意义。<br />如果没有多态,则需要以下两个函数来替代AnimalTest这一个函数。显然,当一个父类拥有很多子类的时候,多态就显得非常有必要。
public void DogTest(Dog dog){
dog.eat();
dog.shout();
}
public void CatTest(Cat cat){
cat.eat();
cat.shout();
}
4. 虚拟方法调用
也就是说 ,多态的使用是一个运行时行为,而不是一个编译时行为。
5. instanceof 关键字
如前文所述,用来多态后,只能调用父类的属性和方法,而不能调用子类的属性和方法。实际上,有了对象的多态性后,内存中实际上是加载了子类独有的属性与方法的,但是由于对象变量声明为父类类型,导致编译时只能调用父类中声明的属性和方法。
也就是说:内存中存在,但是用不了
那么如何才能调用子类特有的属性和方法呢? ==> 强制类型转换
Man m1 = (Man)p2 ;
虽然,子类的功能是大于父类的,但是我们认为 父类类型 > 子类类型,而强制类型转换是向下转换,因此可以讲父类类型的对象变量,转换为子类类型。
- 向上转型:多态 (子 -> 父)
- 向下转型:instanceof (父 -> 子)
a instanaceof A 就是判断对象a实际上(new)是否是类A的实例或者是A的子类的实例,如果是,返回true,反之返回false。
- 子类对象 instanceof 父类 == true
- 父类对象 instanceof 子类 == false ```java package pkg3;
public class test { public static void main(String[] args) { Animal a1 = new Cat(); Animal a2 = new Dog(); Animal a3 = new Animal(); if (a1 instanceof Cat){ // true System.out.println(“true1”); } if (a1 instanceof Animal){ // true System.out.println(“true2”); } if (a1 instanceof Object){ // true System.out.println(“true3”); } if (a1 instanceof Dog){ // true System.out.println(“true4”); } if (a3 instanceof Dog){ // true System.out.println(“true5”); } } }
<br />使用情境:为了避免向下转型时出现 ClassCastException 的异常,我们在向下转型之前,先进行 instanceof 的判断,一旦返回 true,就进行向下转型。
```java
package pkg3;
public class test {
public static void main(String[] args) {
Animal a1 = new Cat();
Animal a2 = new Dog();
Animal a3 = new Animal();
if (a1 instanceof Cat) {
((Cat) a1).showCat();
}
}
}
6. == 和 equals
- 如果比较的是基本数据类型变量,则比较两个变量保存的值是否一样,且不要求类型相同,这点和C一致
- 如果比较的是引用数据类型变量,则比较两个对象的地址值是否相同,即两个引用是否指向同一个实体 ```java package pkg2;
public class test { public static void main(String[] args) { Person p1 = new Person(“123”,12) ; Person p2 = new Person(“123”,12) ; System.out.println(p1 == p2); } }
上述代码中,虽然 p1 和 p2 的类型是一样的,但是 p1、p2 分别在堆中开辟了各自的空间,即指向了不同的实体,所以 p1 == p2 返回的false。<br /><br />但如果 p1 直接赋给 p2,那么 p1 == p2 显然是成立的。
```java
package pkg2;
public class test {
public static void main(String[] args) {
Person p1 = new Person("123",12) ;
Person p2 = p1 ;
System.out.println(p1 == p2);
}
}
对于一般的类均是这样,但是有个特殊的类需要另行分析,就是 String 类。想理解 String 类型的特殊性,就要先理解 Java 中的常量池
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和*符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 属性名称和描述符
- 方法名称和描述符
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池 。 运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中 。
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享 , 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
也就是说,如果两个 String 型的变量 s1、s2 指向了一个相同的字符串常量,由于编译期间每个字符串常量都被放进class文件的常量池中来实现复用,所以载入运行时常量池后,s1、s2 实际上指向了常量池中的同一片地址,故 s1 == s2。
package pkg2;
public class test {
public static void main(String[] args) {
String s1 = "123" ;
String s2 = "123" ;
System.out.println(s1 == s2); // true
}
}
但是,如果某一个 String 变量不是常量,而是从内存中 new 出来一片堆空间,那么它就不指向上述常量池了,故和其他的 String 型指向的地方就不同了。
package pkg2;
public class test {
public static void main(String[] args) {
String s1 = new String("123") ;
String s2 = "123" ;
String s3 = new String("123") ;
System.out.println(s1 == s2);
System.out.prSintln(s1 == s3);
}
}
6.2 equals()
- equals() 只适用用引用数据类型
如果类不重写 equals 方法的话,就调用 Object 中的 equals 方法,而 Object 中定义的 equals 实际上和 == 是一致的,即比较两个对象是否指向同一个实体。
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String aString = (String)anObject; if (!COMPACT_STRINGS || this.coder == aString.coder) { return StringLatin1.equals(value, aString.value); } } return false; }
但是,可以看到,当 anObject 是String类时,equals就不是 == 了,此时 equals 将比较两个 String 变量的内容是否一样,不再考虑地址了。
6.3 重写 euqals
由于对一般类而言 Object 中 euqal 和 == 是等价的,所以类基本都要重写 equals 方法,让它比较两个类的内容是否一样。
IDEA 会快捷重写 equals 方法,按下 Alt + Ins 即可选择,用它重写的 equals 即可比较两个类的内容是否一样。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}