面向对象特征之二:继承性
为描述和处理个人
信息,定义类Person
import java.util.Date;
class Person {
public String name;
public int age;
public Date birthDate;
public String getInfo() {
// ...
}
}
为描述和处理学生
信息,定义类 Student
import java.util.Date;
class Student {
public String name;
public int age;
public Date birthDate;
public String school;
public String getInfo() {
// ...
}
}
通过继承
,简化Student类的定义:
class Student extends Person {
public String school;
}
Student 类继承了父类Person的所有属性和方法,并增加了一个属性 school。Person中的属性和方法,Student都可以使用。
为什么要有继承?
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可
继承性的格式
class A extends B {}
A:子类,派生类,subclass
B:父类,超类,基类,superclass
继承的作用
- 继承的出现减少了代码冗余,提高了代码的复用性。
- 继承的出现,更有利于功能的扩展。
- 继承的出现让类与类之间产生了关系,提供了多态的前提。
注意:不要仅为了获取其他类中某个功能而去继承
继承的体现
- 子类继承了父类,就继承了父类的方法和属性。
- 在子类中,可以使用父类中定义的方法和属性,也可以创建新的数据和方法。
- 在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展” 。
继承的规则
- 子类不能直接访问父类中
私有的(private)
的成员变量和方法。 - Java只支持单继承和多层继承,不允许多重继承
方法重写
重写定义
在子类中可以根据需要对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法
重写应用
重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法
重写的规定
方法的声明
权限修饰符 返回值类型 方法名(形参列表) {
//方法体
}
约定俗成:子类中的叫重写的方法,父类中的叫被重写的方法
重写的的规定
- 子类重写的方法
必须
和父类被重写的方法具有相同的方法名称、参数列表
- 子类重写的方法的返回值类型
不能大于
父类被重写的方法的返回值类型 - 子类重写的方法使用的访问权限
不能小于
父类被重写的方法的访问权限(子类不能重写父类中声明为private权限
的方法) - 返回值类型
- 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void
- 父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类
- 父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型(必须也是double)
- 子类方法抛出的异常不能大于父类被重写方法的异常
子类和父类中的同名同参数的方法要么都声明为非static的(考虑重写),要么都声明为static的(不是重写)。
重写举例
public class Person {
public String name;
public int age;
public String getInfo() {
return "Name: " + name + "
" + "age: " + age;
}
}
public class Student extends Person {
public String school;
public String getInfo() { //重写方法
return "Name: " + name + "
age: " + age
+ "
school: " + school;
}
public static void main(String args[]) {
Student s1 = new Student();
s1.name = "Bob";
s1.age = 20;
s1.school = "school2";
System.out.println(s1.getInfo());//Name:Bob age:20 school:school2
}
}
super关键字的使用
super理解为:父类的
super可以用来调用:属性、方法、构造器
super的使用
- 我们可以在子类的方法或构造器中,通过使用
super.属性
或super.方法
的方式,显式的调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略super.
- 特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的使用
super.属性
的方式,表明调用的是父类中声明的属性 - 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用
super.方法
的方式,表明调用的是父类中被重写的方法
注意
- 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
super的追溯不仅限于直接父类
- super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
super 调用构造器
- 我们可以在子类的构造器中显式的使用
super(形参列表)
的方式,调用父类中声明的指定的构造器 super(形参列表)
的使用,必须声明在子类构造器的首行。- 我们在类的构造器中,针对于
this(形参列表)
或super(形参列表)
只能二选一,不能同时出现 - 在构造器的首行,没有显式的声明
this(形参列表)
或super(形参列表)
,则默认调用的是父类中空参的构造器:super()
- 在类的多个构造器中,至少有一个类的构造器中使用了
super(形参列表)
,调用父类中的构造器
this 和 super 的区别
No. | 区别点 | this | super |
---|---|---|---|
1 | 访问属性 | 访问本类中的属性,如果本类没有此属性则从父类中继续查找 | 直接访问父类中的属性 |
2 | 调用方法 | 访问本类中的方法,如果本类没有此方法则从父类中继续查找 | 直接访问父类中的方法 |
3 | 调用构造器 | 调用本类构造器,必须放在构造器的首行 | 调用父类构造器,必须放在子类构造器的首行 |
子类对象的实例化过程
从结果上来看:(继承性)
- 子类继承父类以后,就获取了父类中声明的属性或方法
- 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性
从过程上来看:
当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object
类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。
明确:
虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建了一个对象
多态性
理解多态性
可以理解为一个事物的多种形态
何为多态性
即对象的多态性,父类的引用指向子类的对象(或子类的对象赋给父类的引用)
多态的使用
编译时类型和 运行时类型。编译时类型由声明
该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边
- 若编译时类型和运行时类型不一致 , 就出现了对象的多态性 (Polymorphism)
- 多态情况下 , “ 看左边 ” : 看的是父类的引用(父类中不具备子类特有的方法)
“ 看右边 ” : 看的是子类的对象(实际运行的是子类重写父类的方法)
多态性的使用前提
- 类的继承关系
- 方法的重写
对象的多态性,只适用于方法,不适用于属性
(编译和运行都看左边)
虚拟方法调用
正常的方法调用
Person e = new Person();
e.getInfo();
Student e = new Student();
e.getInfo();
虚拟方法调用( 多态情况下 )
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法
确定的。
Person e = new Student();
e.getInfo(); // 调用Student 类的getInfo()
编译时类型和运行时类型
编译时e 为Person 类型,而方法的调用是在运行时确定的,所以调用的是Student类的getInfo() 方法。—— 动态绑定
面试题
1.多态是编译时行为还是运行时行为
?
运行时行为
2.方法的重载与重写
二者的定义细节:略
从编译和运行的角度看:
重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。
所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;
而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
Object 类的使用
向下转型的使用
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子特有的属性和方法不能调用
如果要调用子类特有的属性和方法,可以使用向下转型,即使用强制类型转换符
Person p1 = new Person();
P1.eat();
Man man = new Man();
man.eat();
man.age = 25;
man.earnMoney;
// 对象的多态性,父类的引用指向子类的对象
Persion p2 = new Man();
// 多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法
P2.eat();
p2.walk();
//但是调用子类特有的方法时,是不能调用的
p2.earnMoney(); // 比如earnMoney是子类特有的方法,则这里不能调用
// 可以进行强制的类型转换
Man m1 = (Man)p2;
m1.earnMoney();
instanceof 的使用
a instanceof A
:判断对象a是否是类A的实例,如果是,返回 true,如果不是,返回false
// 可以转换
Man m1 = (Man)p2;
m1.earnMoney();
// 不可以转换
Woman w1 = (Woman)p2;
m1.goShopping();
为了避免在向下转型时出现ClassCastException
的异常,我们在向下转型之前,先进行 instanceof的判断
if(p2 instanceof Woman){
Woman w1 = (Woman)p2;
m1.goShopping();
}
== 运算符和 equals() 的区别
== 运算符:
- 可以使用在基本数据类型变量和引用数据类型变量中
- 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等(不一定类型要相同)
- 如果比较的是引用数据类型变量:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体
equals() 方法的使用:
-
是一个方法,而非运算符
-
只能适用于引用数据类型
-
Object 类中 equals() 的定义:
public boolean equals(Object obj) { return (this == obj); } 说明:Object 类中定义的 equals() 和 == 的作用是相同的:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体
-
像String、Date、File、包装类等都重写了Object类中的equals() 方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的 "实体内容"是否相同
-
通常情况下,我们自定义的类如果使用 equals() 的话,也通常是比较两个对象的 "实体内容" 是否相同。那么我们就需要对Object类中的 equals() 进行重写
@Override public boolean equals(Object obj){ if (this == obj){ return true; } if(obj instanceof Customer){ Customer cust = (Customer)obj; // 比较两个对象的每个属性是否相同 if(this.age == cust.age && this.name.equals(cust.name)){ return true; }else{ return false; } } }
重写 equals() 方法的原则
对称性:
如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。自反性:
x.equals(x)必须返回是“true”。传递性:
如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。一致性:
如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。- 任何情况下,x.equals(null),永远返回是“false”; x.equals(和x不同类型的对象)永远返回是“false”。
面试回答==和equals的区别
1 == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
2 equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。
3 具体要看自定义类里有没有重写Object的equals方法来判断。
4 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。
toString()方法的使用
- 当我们输出一个对象的引用时,实际上就是调用当前对象的toString()
Object类中 toString() 的定义:
public String toString(){
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
- 像String、Date、File、包装类等都重写了Object类中的toString()方法。使得在调用对象的toString()时,返回“实体内容”信息
- 自定义类也可以重写toString()方法,当调用此方法时,返回对象的“实体内容”
包装类的使用
-
针对八种基本数据类型定义相应的引用类型—包装类(封装类)
-
有了类的特点,就可以调用类中的方法,Java才是真正的面向对象
基本数据类型转换为包装类
java 提供了8种基本数据类型对应的包装类,使得基本数据类型的变量具有类的特征
import org.junit.Test;
public class WrapperTest {
//基本数据类型 ----> 包装类:调用包装类的构造器
@Test
public void test1(){
int num1 = 10;
// System.out.println(num1.toString()); //会报错,num1基本数据类型,没有toString方法
Integer in1 = new Integer(num1);
System.out.println(in1.toString());
Integer in2 = new Integer("123");
System.out.println(in2.toString());
// 会报异常,得是纯粹的数字
// Integer in3 = new Integer("123abc");
// System.out.println(in3.toString());
Float f1 = new Float(12.3f);
Float f2 = new Float("12.3");
System.out.println(f1);
System.out.println(f2);
Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean("true");
System.out.println(b2);
Boolean b3 = new Boolean("true12");
System.out.println(b2); //false,只有传进去的是大小写true的字符串才会变成Boolean类的true
Order order = new Order();
System.out.println(order.isMale); //false 由于是基本数据类型,所以是false
System.out.println(order.isFemale); //null 由于是类,所以是null
}
}
class Order{
boolean isMale;
Boolean isFemale;
}
包装类转换为基本数据类型
import org.junit.Test;
public class WrapperTest {
//包装类 ----> 基本数据类型:调用包装类 Xxx的 xxxValue()
@Test
public void test2(){
Integer in1 = new Integer(12);
int i1 = in1.intValue();
System.out.println(i1+1);
Float f1 = new Float(12.3);
float f2 = f1.floatValue();
System.out.println((f2+1));
}
}
自动装箱 和 自动拆箱
import org.junit.Test;
public class WrapperTest {
//自动装箱:基本数据类型 ----> 包装类
int num2 = 10;
Integer in1 = num2; //自动装箱
boolean b1 = true;
Boolean b2 = b1; //自动装箱
//自动拆箱:包装类 ---->基本数据类型
System.out.println(in1.toString());
int num3 = in1; //自动拆箱
}
基本数据类型包装类与String的相互转换
import org.junit.Test;
public class WrapperTest {
//String 类型 ----> 基本数据类型、包装类:调用包装类的 parseXxx()
@Test
public void test5() {
String str1 = "123";
//错误的情况
// int num1 = (int)str1;
// Integer in1 = (Integer)str1;
int num2 = Integer.parseInt(str1);
System.out.println(num2 + 1);
String str2 = "true";
boolean b1 = Boolean.parseBoolean(str2);
System.out.println(b1);
}
//基本数据类型、包装类 ---->String类型,调用String重载的valueOf(Xxx xxx)
@Test
public void test4() {
int num1 = 10;
//方式1:连接运算
String str1 = num1 + "";
//方式2:调用String的valueOf(Xxx xxx)
float f1 = 12.3f;
String str2 = String.valueOf(f1); //"12.3"
Double d1 = new Double(12.4);
String str3 = String.valueOf(d1);
System.out.println(str2);
System.out.println(str3); //"12.4"
}
}