教学目标
- 理解封装的概念
- 掌握权限修饰符的使用
- 掌握成员变量的私有化
- 能够写出类的继承格式
- 能够说出继承的特点
- 能够说出方法重写的概念以及和重载的区别
- 能够应用多态解决问题
- 理解向上转型与向下转型
- 能够使用
instanceof关键字判断对象类型 - 掌握构造器的声明与使用
- 能够使用this关键字解决问题
- 能够使用super关键字解决问题
- 了解非静态代码块的格式和执行特点
- 了解实例初始化过程
- 掌握final关键字的使用
- 会重写Object的常用方法:
toString()、equals()
1. 关键字:this
1.1 this是什么?
-
在Java中,this关键字比较难理解,它的作用和其词义很接近。
- 它在方法(准确的说是实例方法或非static的方法)内部使用,表示调用该方法的对象
- 它在构造器内部使用,表示该构造器正在初始化的对象。
-
this可以调用的结构:成员变量、方法和构造器
1.2 什么时候使用this
1.2.1 实例方法或构造器中使用当前对象的成员
在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的阅读性。不过,通常我们都习惯省略this。
但是,当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量。即:我们可以用this来区分成员变量和局部变量。比如:
this.name = name;
另外,使用this访问属性和方法时,如果在本类中未找到,会从父类中查找。这个在继承中会讲到。
举例1:
class Person{ // 定义Person类
private String name ;
private int age ;
public Person(String name,int age){
this.name = name ;
this.age = age ;
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public void getInfo(){
System.out.println("姓名:" + name) ;
this.speak();
}
public void speak(){
System.out.println(“年龄:” + this.age);
}
}
举例2:
public class Rectangle {
int length;
int width;
public int area() {
return this.length * this.width;
}
public int perimeter(){
return 2 * (this.length + this.width);
}
public void print(char sign) {
for (int i = 1; i <= this.width; i++) {
for (int j = 1; j <= this.length; j++) {
System.out.print(sign);
}
System.out.println();
}
}
public String getInfo(){
return "长:" + this.length + ",宽:" + this.width +",面积:" + this.area() +",周长:" + this.perimeter();
}
}
测试类:
public class TestRectangle {
public static void main(String[] args) {
Rectangle r1 = new Rectangle();
Rectangle r2 = new Rectangle();
System.out.println("r1对象:" + r1.getInfo());
System.out.println("r2对象:" + r2.getInfo());
r1.length = 10;
r1.width = 2;
System.out.println("r1对象:" + r1.getInfo());
System.out.println("r2对象:" + r2.getInfo());
r1.print('#');
System.out.println("---------------------");
r1.print('&');
System.out.println("---------------------");
r2.print('#');
System.out.println("---------------------");
r2.print('%');
}
}
//运行结果
r1对象:长:0,宽:0,面积:0,周长:0
r2对象:长:0,宽:0,面积:0,周长:0
r1对象:长:10,宽:2,面积:20,周长:24
r2对象:长:0,宽:0,面积:0,周长:0
##########
##########
---------------------
&&&&&&&&&&
&&&&&&&&&&
---------------------
---------------------
1.2.2 同一个类中构造器互相调用
this可以作为一个类中构造器相互调用的特殊格式。
- this():调用本类的无参构造器
- this(实参列表):调用本类的有参构造器
public class Student {
private String name;
private int age;
// 无参构造
public Student() {
// this("",18);//调用本类有参构造器
}
// 有参构造
public Student(String name) {
this();//调用本类无参构造器
this.name = name;
}
// 有参构造
public Student(String name,int age){
this(name);//调用本类中有一个String参数的构造器
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
注意:
- 不能出现递归调用。比如,调用自身构造器。
- 推论:如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了"this(形参列表)"
- this()和this(实参列表)只能声明在构造器首行。
- 推论:在类的一个构造器中,最多只能声明一个"this(形参列表)"
1.3 练习
**练习1:**添加必要的构造器,综合应用构造器的重载,this关键字。
class Boy{
private String name;
private int age;
public Boy(){}
public Boy(String name,String age){
this.name = name;
this.age = age;
}
public void setName(String i){
this.name = i;
}
public String getName(){
return name;
}
public void setAge(int i){
this.age = i;
}
public int getAge(){
return age;
}
public void marray(Girl girl){
System.out.println("我要和"+girl.getName+"结婚");
}
public void shout(){
System.out.println("hhhhh");
}
}
class Girl{
private String name;
private int age;
public Girl(){}
public Girl(String name,int age){
this.name = name;
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public void marray(Boy boy){
System.out.println("我要和"+boy.getName+"结婚");
}
public int compare(Girl girl){
return this.age - girl.age;
}
}
练习2:
(1)按照如下的UML类图,创建相应的类,提供必要的结构:
在提款方法withdraw()中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。deposit()方法表示存款。
(2)按照如下的UML类图,创建相应的类,提供必要的结构
(3)按照如下的UML类图,创建相应的类,提供必要的结构
-
addCustomer 方法必须依照参数(姓,名)构造一个新的 Customer对象,然后把它放到 customer 数组中。还必须把 numberOfCustomer 属性的值加 1。
-
getNumOfCustomers 方法返回 numberofCustomers 属性值。
-
getCustomer方法返回与给出的index参数相关的客户。
(4)创建BankTest类,进行测试。
内存解析图:
public class Account {//账号
private double balance;//余额
public Account(double init_balance){
this.balance = init_balance;
}
public double getBalance(){
return balance;
}
//存钱的方法
public void deposit(double amt){
if(amt > 0){
balance += amt;
}
}
//取钱的方法
public void withdraw(double amt){
if(balance >= amt){
balance -= amt;
}else{
System.out.println("余额不足,取款失败");
}
}
}
public class Customer {//客户
//属性
private String firstName;
private String lastName;
private Account account; //声明的自定义类的属性
public Customer(String f,String l){
this.firstName = f;
this.lastName = l;
}
public String getFirstName(){
return firstName;
}
public String getLastName(){
return lastName;
}
public void setAccount(Account acct){
this.account = acct;
}
public Account getAccount(){
return account;
}
}
public class Bank { //银行类
private Customer[] customers ;// 表示:一个银行中可以有多个客户,我们使用一个数组来表示多个客户
private int numberOfCustomer;// 表示:记录实际上客户的个数
public Bank(){
customers = new Customer[5];
}
/**
* 将指定命名的客户信息保存在数组中
*
* addCustomer 方法必须依照参数(姓,名)构造一个新的 Customer对象,
* 然后把它放到 customer 数组中。还必须把 numberOfCustomer 属性的值加 1。
* @param f
* @param l
*/
public void addCustomer(String f,String l){
if(numberOfCustomer < customers.length){
//1.将客户信息封装到一个Customer对象中
Customer cust = new Customer(f,l);
customers[numberOfCustomer++] = cust;
}else{
System.out.println("不好意思,银行客户信息已满。添加失败");
}
}
/**
* 返回当前银行中客户的数量
*
* @return
*/
public int getNumOfCustomers(){
//正确的:
return numberOfCustomer;
//错误的:
// return customers.length;
}
/**
* 获取指定索引位置上的客户信息
* @param index
* @return
*/
public Customer getCustomer(int index){
if(index < 0 || index >= numberOfCustomer){
return null;
}else{
return customers[index];
}
}
}
public class BankTest {
public static void main(String[] args) {
//1. 创建Bank对象
Bank bank1 = new Bank();
//2. 向银行的客户列表中添加一个客户
bank1.addCustomer("smith","joe");
bank1.addCustomer("tom","li");
System.out.println(bank1.getNumOfCustomers());
Customer customer = bank1.getCustomer(0);
customer.setAccount(new Account(1000));
customer.getAccount().withdraw(300);
System.out.println("剩余的钱数:" + customer.getAccount().getBalance());
}
}
2. 面向对象特征二:继承
2.1 继承的概述
2.1.1 生活中的继承
继承有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思。
2.1.2 Java中的继承
角度一:从上而下
为描述和处理个人信息,定义类Person:
为描述和处理学生信息,定义类Student:
通过继承,简化Student类的定义:
说明:Student类继承了父类Person的所有属性和方法,并增加了一个属性school。Person中的属性和方法,Student都可以使用。
角度二:从下而上
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系。如图所示:
再举例:
2.1.3 继承的好处
-
继承的出现减少了代码冗余,提高了代码的复用性。
-
继承的出现,更有利于功能的扩展。
-
继承的出现让类与类之间产生了
is-a的关系,提供了多态的前提。- 继承描述的是事物之间的所属关系,这种关系是:
is-a的关系。可见,父类更通用或更一般,子类更具体。
- 继承描述的是事物之间的所属关系,这种关系是:
注意:不要仅为了获取其他类中某个功能而去继承
2.2 继承的语法
2.2.1 继承中的语法格式
通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}
2.2.2 继承中的基本概念
类B,称为子类、派生类、SubClass
类A,称为父类、超类、基类、SuperClass
2.3 代码举例
1、父类
package com.atguigu.inherited.grammar;
/*
* 定义动物类Animal,做为父类
*/
public class Animal {
// 定义name属性
String name;
// 定义age属性
int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的"
+ name + "在吃东西");
}
}
2、子类
package com.atguigu.inherited.grammar;
/*
* 定义猫类Cat 继承 动物类Animal
*/
public class Cat extends Animal {
int count;//记录每只猫抓的老鼠数量
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
count++;
System.out.println("抓老鼠,已经抓了"
+ count + "只老鼠");
}
}
3、测试类
package com.atguigu.inherited.grammar;
public class TestCat {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值
cat.name = "Tom";
// 为该猫类对象的age属性进行赋值
cat.age = 2;
// 调用该猫继承来的eat()方法
cat.eat();
// 调用该猫的catchMouse()方法
cat.catchMouse();
cat.catchMouse();
cat.catchMouse();
}
}
2.4 继承性的细节说明
1、子类会继承父类所有的实例变量和实例方法
从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
- 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
- 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循
从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。
所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。
2、子类不能直接访问父类中私有的(private)的成员变量和方法
子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。如图所示:
3、在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”
子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展。
4、Java只支持单继承,不支持多重继承
public class A{}
class B extends A{}
//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} //ok
class C extends A,B... //error
5、Java支持多层继承(继承体系)
class A{}
class B extends A{}
class C extends B{}
说明:
子类和父类是一种相对的概念
顶层父类是Object类。所有的类默认继承Object,作为父类。
6、一个父类可以同时拥有多个子类
class A{}
class B extends A{}
class D extends A{}
class E extends A{}
2.5 练习
**练习1:**定义一个学生类Student,它继承自Person类
class Person{
private String name;
private char sex;
private int age;
public Person(String name,char sex,int age){
this.name = name;
this.sex = sex;
this.age = age;
}
public String toString(){
return "Person{name='"+name+"',sex="+sex+",age="+age+"}"
}
}
class Student extends Person{
private long number;
private int math;
private int english;
private int computer;
public Student(String n,char s,int a,long k,int m,int e,int c){
super(n,s,a);
number=k;
math = m;
english = e;
computer = c;
}
public double aver(){
return (math + english + computer) * 1.0 / number;
}
public int max(){
int max = math > english ? math : english;
max = max > computer ? max : computer;
reutrn max;
}
public int min(){
int min = math < english ? math : english;
min = min < computer ? min : computer;
return min;
}
}
练习2:
(1)定义一个ManKind类,包括
-
成员变量int sex和int salary;
-
方法void manOrWoman():根据sex的值显示“man”(sex==1)或者“woman”(sex==0);
-
方法void employeed():根据salary的值显示“no job”(salary==0)或者“ job”(salary!=0)。
(2)定义类Kids继承ManKind,并包括
- 成员变量int yearsOld;
- 方法printAge()打印yearsOld的值。
(3)定义类KidsTest,在类的main方法中实例化Kids的对象someKid,用该对象访问其父类的成员变量及方法。
public class ManKind {
private int sex;
private int salary;
public ManKind(int sex, int salary) {
this.sex = sex;
this.salary = salary;
}
public ManKind() {
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public void manOrWoman(){
if(sex == 1){
System.out.println("Man");
}else if(sex == 0){
System.out.println("Woman");
}
}
public void employeed(){
if(salary == 0){
System.out.println("no job");
}else{
System.out.println("job");
}
}
}
public class Kids extends ManKind {
private int yearsOld;
public int getYearsOld() {
return yearsOld;
}
public void setYearsOld(int yearsOld) {
this.yearsOld = yearsOld;
}
public Kids() {
}
public Kids(int yearsOld) {
this.yearsOld = yearsOld;
}
public void printAge(){
System.out.println("I am " + yearsOld + " years old!");
}
}
public class KidsTest {
public static void main(String[] args) {
Kids someKid = new Kids();
someKid.setSex(1);
someKid.setSalary(1000);
someKid.setYearsOld(12);
System.out.println(someKid.getSex());
System.out.println(someKid.getSalary());
System.out.println(someKid.getYearsOld());
someKid.printAge();
someKid.manOrWoman();
someKid.employeed();
}
}
**练习3:**根据下图实现类。在CylinderTest类中创建Cylinder类的对象,设置圆柱的底面半径和高,并输出圆柱的体积。
public class Circle {
private double radius;//半径
public Circle(){
System.out.println("Circle()...");
radius = 1.0;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
public double findArea(){
return Math.PI * radius * radius;
}
}
public class Cylinder extends Circle{
private double length;//高
public Cylinder(){
super();
System.out.println("Cylinder()....");
length = 1.0;
}
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
//求圆柱的体积
public double findVolume(){
return super.findArea() * length;
}
}
public class CylinderTest {
public static void main(String[] args) {
Cylinder cy1 = new Cylinder();
cy1.setRadius(2.0);
cy1.setLength(3.4);
System.out.println("圆柱的体积:" + cy1.findVolume());
}
}
3. 方法的重写(override/overwrite)
父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)。也称为方法的重置、覆盖。
在程序执行时,子类的方法将覆盖父类的方法。
3.1 方法重写举例
比如新的手机增加来电显示头像的功能,代码如下:
package com.atguigu.inherited.method;
public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
package com.atguigu.inherited.method;
//SmartPhone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
@Override
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能的方法
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
}
package com.atguigu.inherited.method;
public class TestOverride {
public static void main(String[] args) {
// 创建子类对象
SmartPhone sp = new SmartPhone();
// 调用父类继承而来的方法
sp.call();
// 调用子类重写的方法
sp.showNum();
}
}
@Override使用说明:
写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。
3.2 方法重写的要求
-
子类重写的方法
必须和父类被重写的方法具有相同的方法名称、参数列表。 -
子类重写的方法的返回值类型
不能大于父类被重写的方法的返回值类型。(例如:Student < Person)。
注意:如果返回值类型是基本数据类型和void,那么必须是相同
- 子类重写的方法使用的访问权限
不能小于父类被重写的方法的访问权限。(public > protected > 缺省 > private)
注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写
- 子类方法抛出的异常不能大于父类被重写方法的异常
此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。
3.3 小结:方法的重载与重写
方法的重载:方法名相同,形参列表不同。不看返回值类型。
方法的重写:见上面。
(1)同一个类中
package com.atguigu.inherited.method;
public class TestOverload {
public int max(int a, int b){
return a > b ? a : b;
}
public double max(double a, double b){
return a > b ? a : b;
}
public int max(int a, int b,int c){
return max(max(a,b),c);
}
}
(2)父子类中
package com.atguigu.inherited.method;
public class TestOverloadOverride {
public static void main(String[] args) {
Son s = new Son();
s.method(1);//只有一个形式的method方法
Daughter d = new Daughter();
d.method(1);
d.method(1,2);//有两个形式的method方法
}
}
class Father{
public void method(int i){
System.out.println("Father.method");
}
}
class Son extends Father{
public void method(int i){//重写
System.out.println("Son.method");
}
}
class Daughter extends Father{
public void method(int i,int j){//重载
System.out.println("Daughter.method");
}
}
3.4 练习
**练习1:**如果现在父类的一个方法定义成private访问权限,在子类中将此方法声明为default访问权限,那么这样还叫重写吗? (NO)
**练习2:**修改继承内容的练习2中定义的类Kids,在Kids中重新定义employeed()方法,覆盖父类ManKind中定义的employeed()方法,输出“Kids should study and no job.”
public class ManKind {
private int sex;
private int salary;
public ManKind(int sex, int salary) {
this.sex = sex;
this.salary = salary;
}
public ManKind() {
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public void manOrWoman(){
if(sex == 1){
System.out.println("Man");
}else if(sex == 0){
System.out.println("Woman");
}
}
public void employeed(){
if(salary == 0){
System.out.println("no job");
}else{
System.out.println("job");
}
}
}
public class Kids extends ManKind {
private int yearsOld;
public int getYearsOld() {
return yearsOld;
}
public void setYearsOld(int yearsOld) {
this.yearsOld = yearsOld;
}
public Kids() {
}
public Kids(int yearsOld) {
this.yearsOld = yearsOld;
}
public void printAge(){
System.out.println("I am " + yearsOld + " years old!");
}
public void employeed(){
System.out.println("Kids should study and no job.");
}
}
public class KidsTest {
public static void main(String[] args) {
Kids someKid = new Kids();
someKid.setSex(1);
someKid.setSalary(1000);
someKid.setYearsOld(12);
System.out.println(someKid.getSex());
System.out.println(someKid.getSalary());
System.out.println(someKid.getYearsOld());
someKid.printAge();
someKid.manOrWoman();
someKid.employeed();
}
}
4. 再谈封装性中的4种权限修饰
权限修饰符:public,protected,缺省,private
| 修饰符 | 本类 | 本包 | 其他包子类 | 其他包非子类 |
|---|---|---|---|---|
| private | √ | × | × | × |
| 缺省 | √ | √(本包子类非子类都可见) | × | × |
| protected | √ | √(本包子类非子类都可见) | √(其他包仅限于子类中可见) | × |
| public | √ | √ | √ | √ |
外部类:public和缺省
成员变量、成员方法等:public,protected,缺省,private
1、外部类要跨包使用必须是public,否则仅限于本包使用
(1)外部类的权限修饰符如果缺省,本包使用没问题
(2)外部类的权限修饰符如果缺省,跨包使用有问题
2、成员的权限修饰符问题
(1)本包下使用:成员的权限修饰符可以是public、protected、缺省
(2)跨包下使用:要求严格
(3)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义
5. 关键字:super
5.1 super的理解
在Java类中使用super来调用父类中的指定操作:
- super可用于访问父类中定义的属性
- super可用于调用父类中定义的成员方法
- super可用于在子类构造器中调用父类的构造器
注意:
- 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
- super的追溯不仅限于直接父类
- super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
5.2 super的使用场景
5.2.1 子类中调用父类被重写的方法
- 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
- 如果子类重写了父类的方法,在子类中需要通过
super.才能调用父类被重写的方法,否则默认调用的子类重写的方法
举例:
package com.atguigu.inherited.method;
//smartphone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}
总结:
-
方法前面没有super.和this.
- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
-
方法前面有this.
- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
-
方法前面有super.
- 从当前子类的直接父类找,如果没有,继续往上追溯
5.2.2 子类中调用父类中同名的成员变量
- 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
- 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
- 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问
举例:
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;
public void test(){
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}
总结:起点不同(就近原则)
-
变量前面没有super.和this.
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
局部变量, - 如果不是局部变量,先从当前执行代码的
本类去找成员变量 - 如果从当前执行代码的本类中没有找到,会往上找
父类声明的成员变量(权限修饰符允许在子类中访问的)
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
-
变量前面有this.
- 通过this找成员变量时,先从当前执行代码的==本类去找成员变量==
- 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量(==权限修饰符允许在子类中访问的)
-
变量前面super.
- 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
- 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)
再举例:
public class Test{
public static void main(String[] args){
Son s = new Son();
s.test();
Daughter d = new Daughter();
d.test();
}
}
class Father{
protected int num = 10;
public int getNum(){
return num;
}
}
class Son extends Father{
private int num = 20;
public void test(){
System.out.println(getNum());//10 本类没有找父类,执行父类中的getNum()
System.out.println(this.getNum());//10 本类没有找父类,执行父类中的getNum()
System.out.println(super.getNum());//10 本类没有找父类,执行父类中的getNum()
}
}
class Daughter extends Father{
private int num = 20;
@Override
public int getNum(){
return num;
}
public void test(){
System.out.println(getNum());//20 先找本类,执行本类的getNum()
System.out.println(this.getNum());//20 先找本类,执行本类的getNum()
System.out.println(super.getNum());//10 直接找父类,执行父类中的getNum()
}
}
特别说明:应该避免子类声明和父类重名的成员变量
因为,子类会继承父类所有的成员变量,所以:
-
如果重名的成员变量表示相同的意义,就无需重复声明
-
如果重名的成员变量表示不同的意义,会引起歧义
在阿里的开发规范等文档中都做出明确说明:
5.2.3 子类构造器中调用父类构造器
- 子类继承父类时,不会继承父类的构造器。只能通过super()或super(实参列表)的方式调用父类的构造器。
- super();:子类构造器中一定会调用父类的构造器,默认调用父类的无参构造,super();可以省略。
- 当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器。同时,只能”二选一”
- super()和super(实参列表)都只能出现在子类构造器的首行
- 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错
public class Person {
private String name;
private int age;
private Date birthDate;
public Person(String name, int age, Date d) {
this.name = name;
this.age = age;
this.birthDate = d;
}
public Person(String name, int age) {
this(name, age, null);
}
public Person(String name, Date d) {
this(name, 30, d);
}
public Person(String name) {
this(name, 30);
}
}
public class Student extends Person {
private String school;
public Student(String name, int age, String s) {
super(name, age);
school = s;
}
public Student(String name, String s) {
super(name);
school = s;
}
// 编译出错: no super(),系统将调用父类无参数的构造器。
public Student(String s) {
school = s;
}
}
package com.atguigu.constructor;
public class TestEmployee {
public static void main(String[] args) {
Manager m1 = new Manager();
System.out.println(m1.getInfo());
Manager m2 = new Manager("张三",23,20000,0.1);
System.out.println(m2.getInfo());
}
}
形式一:
class A{
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类和B类都是默认有一个无参构造,B类的默认无参构造中还会默认调用A类的默认无参构造
//但是因为都是默认的,没有打印语句,看不出来
}
}
形式二:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类默认有一个无参构造,
//B类的默认无参构造中会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"
}
}
形式三:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}
形式四:
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中明确写了super(),表示调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}
形式五:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test05{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造没有写super(...),表示默认调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}
形式六:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test06{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造明确写super(),表示调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}
形式七:
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(int a){
super(a);
System.out.println("B类有参构造器");
}
}
class Test07{
public static void main(String[] args){
B b = new B(10);
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个有参构造,
//B类的有参构造明确写super(a),表示调用A类的有参构造
//会打印“A类有参构造器"和"B类有参构造器"
}
}
形式八:
class A{
A(){
System.out.println("A类无参构造器");
}
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();//可以省略,调用父类的无参构造
System.out.println("B类无参构造器");
}
B(int a){
super(a);//调用父类有参构造
System.out.println("B类有参构造器");
}
}
class Test8{
public static void main(String[] args){
B b1 = new B();
B b2 = new B(10);
}
}
5.3 小结:this与super
1、this和super的意义
this:当前对象
- 在构造器和非静态代码块中,表示正在new的对象
- 在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
2、this和super的使用格式
- this
- this.成员变量:表示当前对象的某个成员变量,而不是局部变量
- this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
- this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
- super
- super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
- super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
- super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
5.4 练习
**练习1:**修改方法重写的练习2中定义的类Kids中employeed()方法,在该方法中调用父类ManKind的employeed()方法,然后再输出“but Kids should study and no job.”
public class ManKind {
private int sex;
private int salary;
public ManKind(int sex, int salary) {
this.sex = sex;
this.salary = salary;
}
public ManKind() {
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public void manOrWoman(){
if(sex == 1){
System.out.println("Man");
}else if(sex == 0){
System.out.println("Woman");
}
}
public void employeed(){
if(salary == 0){
System.out.println("no job");
}else{
System.out.println("job");
}
}
}
public class Kids extends ManKind {
private int yearsOld;
public int getYearsOld() {
return yearsOld;
}
public void setYearsOld(int yearsOld) {
this.yearsOld = yearsOld;
}
public Kids() {
}
public Kids(int yearsOld) {
this.yearsOld = yearsOld;
}
public void printAge(){
System.out.println("I am " + yearsOld + " years old!");
}
public void employeed(){
super.employeed();
System.out.println("Kids should study and no job.");
}
}
public class KidsTest {
public static void main(String[] args) {
Kids someKid = new Kids();
someKid.setSex(1);
someKid.setSalary(1000);
someKid.setYearsOld(12);
System.out.println(someKid.getSex());
System.out.println(someKid.getSalary());
System.out.println(someKid.getYearsOld());
someKid.printAge();
someKid.manOrWoman();
someKid.employeed();
}
}
**练习2:**修改继承中的练习3中定义的Cylinder类,在Cylinder类中覆盖findArea()方法,计算圆柱的表面积。考虑:findVolume方法怎样做相应的修改?
在CylinderTest类中创建Cylinder类的对象,设置圆柱的底面半径和高,并输出圆柱的表面积和体积。
附加题:在CylinderTest类中创建一个Circle类的对象,设置圆的半径,计算输出圆的面积。体会父类和子类成员的分别调用。
public class Circle {
private double radius;//半径
public Circle(){
System.out.println("Circle()...");
radius = 1.0;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
public double findArea(){
return Math.PI * radius * radius;
}
}
public class Cylinder extends Circle{
private double length;//高
public Cylinder(){
super();
System.out.println("Cylinder()....");
length = 1.0;
}
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
//求圆柱的体积
public double findVolume(){
return super.findArea() * length;
}
//求圆柱的表面积
@Override
public double findArea() {
return super.findArea() * 2 + Math.PI * 2 * getRadius() * getLength();
}
}
public class CylinderTest {
public static void main(String[] args) {
Cylinder cy1 = new Cylinder();
// cy1.setRadius(2.0);
// cy1.setLength(3.4);
System.out.println("圆柱的体积:" + cy1.findVolume());
System.out.println("圆柱的表面积:" + cy1.findArea());
Circle c = new Circle();
c.setRadius(2.0);
System.out.println("圆的面积:"+c.findArea());
}
}
练习3:
1、写一个名为Account的类模拟账户。该类的属性和方法如下图所示。该类包括的属性:账号id,余额balance,年利率annualInterestRate;包含的方法:访问器方法(getter和setter方法),返回月利率的方法getMonthlyInterest(),取款方法withdraw(),存款方法deposit()。
写一个用户程序测试Account类。在用户程序中,创建一个账号为1122、余额为20000、年利率4.5%的Account对象。使用withdraw方法提款30000元,并打印余额。 再使用withdraw方法提款2500元,使用deposit方法存款3000元,然后打印余额和月利率。
提示:在提款方法withdraw中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。 运行结果如图所示:
2、创建Account类的一个子类CheckAccount代表可透支的账户,该账户中定义一个属性overdraft代表可透支限额。在CheckAccount类中重写withdraw方法,其算法如下:
如果(取款金额<账户余额),
可直接取款
如果(取款金额>账户余额),
计算需要透支的额度
判断可透支额overdraft是否足够支付本次透支需要,如果可以
将账户余额修改为0,冲减可透支金额
如果不可以
提示用户超过可透支额的限额
要求:写一个用户程序测试CheckAccount类。在用户程序中,创建一个账号为1122、余额为20000、年利率4.5%,可透支限额为5000元的CheckAccount对象。
使用withdraw方法提款5000元,并打印账户余额和可透支额。
再使用withdraw方法提款18000元,并打印账户余额和可透支额。
再使用withdraw方法提款3000元,并打印账户余额和可透支额。
提示:
(1)子类CheckAccount的构造方法需要将从父类继承的3个属性和子类自己的属性全部初始化。
(2)父类Account的属性balance被设置为private,但在子类CheckAccount的withdraw方法中需要修改它的值,因此应修改父类的balance属性,定义其为protected。
运行结果如下图所示:
public class Account {
private int id;
private double balance;
private double annualInterestRate; //年利率
// public Account(){}
public Account(int id, double balance, double annualInterestRate) {
this.id = id;
this.balance = balance;
this.annualInterestRate = annualInterestRate;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getBalance() {
return balance;
}
// public void setBalance(double balance) {
// this.balance = balance;
// }
public double getAnnualInterestRate() {
return annualInterestRate;
}
public void setAnnualInterestRate(double annualInterestRate) {
this.annualInterestRate = annualInterestRate;
}
//返回月利率
public double getMonthlyInterest(){
return annualInterestRate / 12;
}
//取钱方法
public void withdraw(double amount){
if(balance >= amount){
balance -= amount;
}else{
System.out.println("余额不足");
}
}
//存钱方法
public void deposit(double amount){
if(amount > 0){
balance += amount;
}
}
}
public class CheckAccount extends Account{
private double overdraft;
public double getOverdraft() {
return overdraft;
}
// public void setOverdraft(double overdraft) {
// this.overdraft = overdraft;
// }
public CheckAccount(int id, double balance, double annualInterestRate, double overdraft){
super(id,balance,annualInterestRate);
this.overdraft = overdraft;
}
@Override
public void withdraw(double amount) {
if(getBalance() >= amount){ //余额够
super.withdraw(amount);
}else if(getBalance() + overdraft >= amount){ //加上透支额度以后够
//从透支额度中补齐剩下的钱
overdraft -= (amount - getBalance());
//将账户余额设置为0
super.withdraw(getBalance());
}else{
System.out.println("余额不足");
}
}
}
public class CheckAccountTest {
public static void main(String[] args) {
CheckAccount ck = new CheckAccount(1122,20000,0.045,5000);
ck.withdraw(5000);
System.out.println("账户余额:" + ck.getBalance());
System.out.println("可透支额度:" +ck.getOverdraft());
ck.withdraw(18000);
System.out.println("账户余额:" + ck.getBalance());
System.out.println("可透支额度:" +ck.getOverdraft());
ck.withdraw(3000);
System.out.println("账户余额:" + ck.getBalance());
System.out.println("可透支额度:" +ck.getOverdraft());
}
}
6. 子类对象实例化全过程
Dog dog = new Dog("小花","小红");
举例:
class Creature {
public Creature() {
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature {
public Animal(String name) {
System.out.println("Animal带一个参数的构造器,该动物的name为" + name);
}
public Animal(String name, int age) {
this(name);
System.out.println("Animal带两个参数的构造器,其age为" + age);
}
}
public class Wolf extends Animal {
public Wolf() {
super("灰太狼", 3);
System.out.println("Wolf无参数的构造器");
}
public static void main(String[] args) {
new Wolf();
}
}
7. 面向对象特征三:多态性
7.1 为什么需要多态性?
开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
案例:
(1)声明一个Dog类,包含public void eat()方法,输出“狗啃骨头”
(2)声明一个Cat类,包含public void eat()方法,输出“猫吃鱼仔”
(3)声明一个Person类,功能如下:
- 包含宠物属性
- 包含领养宠物方法 public void adopt(宠物类型Pet)
- 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
package com.atguigu.polymorphism.problem;
public class Dog {
public void eat(){
System.out.println("狗啃骨头");
}
}
package com.atguigu.polymorphism.problem;
public class Cat {
public void eat(){
System.out.println("猫吃鱼仔");
}
}
package com.atguigu.polymorphism.problem;
public class Person {
private Dog dog;
//adopt:领养
public void adopt(Dog dog){
this.dog = dog;
}
//feed:喂食
public void feed(){
if(dog != null){
dog.eat();
}
}
/*
问题:
1、从养狗切换到养猫怎么办?
修改代码把Dog修改为养猫?
2、或者有的人养狗,有的人养猫怎么办?
3、要是同时养多个狗,或猫怎么办?
4、要是还有更多其他宠物类型怎么办?
如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
*/
}
7.2 多态的形式和体现
7.2.1 对象的多态性
多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象
格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象;
举例:
Person p = new Student();
Object o = new Person();//Object类型的变量o,指向Person类型的对象
o = new Student(); //Object类型的变量o,指向Student类型的对象
对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象
7.2.2 多态的理解
Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边。
- 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
- 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法) “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
多态的使用前提:① 类的继承关系 ② 方法的重写
7.2.3 举例
package com.atguigu.polymorphism.grammar;
public class Pet {
private String nickname; //昵称
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void eat(){
System.out.println(nickname + "吃东西");
}
}
package com.atguigu.polymorphism.grammar;
public class Cat extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("猫咪" + getNickname() + "吃鱼仔");
}
//子类扩展的方法
public void catchMouse() {
System.out.println("抓老鼠");
}
}
package com.atguigu.polymorphism.grammar;
public class Dog extends Pet {
//子类重写父类的方法
@Override
public void eat() {
System.out.println("狗子" + getNickname() + "啃骨头");
}
//子类扩展的方法
public void watchHouse() {
System.out.println("看家");
}
}
1、方法内局部变量的赋值体现多态
package com.atguigu.polymorphism.grammar;
public class TestPet {
public static void main(String[] args) {
//多态引用
Pet pet = new Dog();
pet.setNickname("小白");
//多态的表现形式
/*
编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
*/
pet.eat();//运行时执行子类Dog重写的方法
// pet.watchHouse();//不能调用Dog子类扩展的方法
pet = new Cat();
pet.setNickname("雪球");
pet.eat();//运行时执行子类Cat重写的方法
}
}
2、方法的形参声明体现多态
package com.atguigu.polymorphism.grammar;
public class Person{
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
package com.atguigu.polymorphism.grammar;
public class TestPerson {
public static void main(String[] args) {
Person person = new Person();
Dog dog = new Dog();
dog.setNickname("小白");
person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
person.feed();
Cat cat = new Cat();
cat.setNickname("雪球");
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
}
}
3、方法返回值类型体现多态
package com.atguigu.polymorphism.grammar;
public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
package com.atguigu.polymorphism.grammar;
public class TestPetShop {
public static void main(String[] args) {
PetShop shop = new PetShop();
Pet dog = shop.sale("Dog");
dog.setNickname("小白");
dog.eat();
Pet cat = shop.sale("Cat");
cat.setNickname("雪球");
cat.eat();
}
}
7.2.4 多态的好处和弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
Student m = new Student();
m.school = "pku"; //合法,Student类有school成员变量
Person e = new Student();
e.school = "pku"; //非法,Person类没有school成员变量
// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。
7.3 虚方法调用(Virtual Method Invocation)
在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
举例:
前提:Person类中定义了welcome()方法,各个子类重写了welcome()。
执行:多态的情况下,调用对象的welcome()方法,实际执行的是子类重写的方法。
编译看左边,运行看右边
7.4 成员变量没有多态性
-
若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
-
对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量
package com.atguigu.polymorphism.grammar;
public class TestVariable {
public static void main(String[] args) {
Base b = new Sub();
System.out.println(b.a);
System.out.println(((Sub)b).a);
Sub s = new Sub();
System.out.println(s.a);
System.out.println(((Base)s).a);
}
}
class Base{
int a = 1;
}
class Sub extends Base{
int a = 2;
}
7.5 向上转型与向下转型
首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
7.4.1 为什么要类型转换呢?
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过。
-
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是自动完成的
-
向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断
7.4.2 如何向上转型与向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量
package com.atguigu.polymorphism.grammar;
public class ClassCastTest {
public static void main(String[] args) {
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
//向上转型
Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
pet.setNickname("小白");
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse
Dog d = (Dog) pet;
System.out.println("d.nickname = " + d.getNickname());
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse
Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
}
}
7.4.3 instanceof关键字
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
- 说明:
- 只要用
instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。 - 如果对象a属于类A的子类B,
a instanceof A值也为true。 - 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
- 只要用
示例代码:
package com.atguigu.polymorphism.grammar;
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat) pets[i];
cat.catchMouse();
}
}
}
}
7.4.4 复习:类型转换
- 基本数据类型的Casting:
- 自动类型转换:小的数据类型可以自动转换成大的数据类型 如long g=20; double d=12.0f
- 强制类型转换:可以把大的数据类型强制转换(casting)成小的数据类型 如 float f=(float)12.0; int a=(int)1200L
- 对Java对象的强制类型转换称为造型
- 从子类到父类的类型转换可以自动进行
- 从父类到子类的类型转换必须通过造型(强制类型转换)实现
- 无继承关系的引用类型间的转换是非法的
- 在造型前可以使用instanceof操作符测试一个对象的类型
- 举例
public class ConversionTest {
public static void main(String[] args) {
double d = 13.4;
long l = (long) d;
System.out.println(l);
int in = 5;
// boolean b = (boolean)in;
Object obj = "Hello";
String objStr = (String) obj;
System.out.println(objStr);
Object objPri = new Integer(5);
// 所以下面代码运行时引发ClassCastException异常
String str = (String) objPri;
}
}
public class Test {
public void method(Person e) { // 设Person类中没有getschool() 方法
// System.out.pritnln(e.getschool()); //非法,编译时错误
if (e instanceof Student) {
Student me = (Student) e; // 将e强制转换为Student类型
System.out.pritnln(me.getschool());
}
}
public static void main(String[] args){
Test t = new Test();
Student m = new Student();
t.method(m);
}
}
7.6 练习
练习1:继承成员变量和继承方法的区别
class Base {
int count = 10;
public void display() {
System.out.println(this.count);
}
}
class Sub extends Base {
int count = 20;
public void display() {
System.out.println(this.count);
}
}
public class FieldMethodTest {
public static void main(String[] args){
Sub s = new Sub();
System.out.println(s.count); //20
s.display(); //20
Base b = s;
System.out.println(b == s); //true
System.out.println(b.count); //10
b.display(); //20
}
}
练习2:
class Person {
protected String name="person";
protected int age=50;
public String getInfo() {
return "Name: "+ name + "\n" +"age: "+ age;
}
}
class Student extends Person {
protected String school="pku";
public String getInfo() {
return "Name: "+ name + "\nage: "+ age
+ "\nschool: "+ school;
}
}
class Graduate extends Student{
public String major="IT";
public String getInfo()
{
return "Name: "+ name + "\nage: "+ age
+ "\nschool: "+ school+"\nmajor:"+major;
}
}
建立InstanceTest 类,在类中定义方法method(Person e); 在method中: (1)根据e的类型调用相应类的getInfo()方法。 (2)根据e的类型执行: 如果e为Person类的对象,输出: “a person”; 如果e为Student类的对象,输出: “a student” “a person ” 如果e为Graduate类的对象,输出: “a graduated student” “a student” “a person”
public class InstanceTest {
public static void main(String[] args) {
InstanceTest instanceTest = new InstanceTest();
Person p = new Person();
Student s = new Student();
Graduate g = new Graduate();
instanceTest.method(p); //a person
instanceTest.method(s); //a student a person
instanceTest.method(g); //a graduate student a student a person
Person s1 = new Student();
instanceTest.method(s1); //a student a person
Person g1 = new Graduate();
instanceTest.method(g1); //a graduate student a student a person
}
public void method(Person e){
if(e instanceof Graduate){
System.out.println("a graduate student");
System.out.println("a student");
System.out.println("a person");
} else if(e instanceof Student){
System.out.println("a student");
System.out.println("a person");
} else if(e instanceof Person){
System.out.println("a person");
}
}
}
练习3:定义三个类,父类GeometricObject代表几何形状,子类Circle代表圆形,MyRectangle代表矩形。定义一个测试类GeometricTest,编写equalsArea方法测试两个对象的面积是否相等(注意方法的参数类型,利用动态绑定技术),编写displayGeometricObject方法显示对象的面积(注意方法的参数类型,利用动态绑定技术)。
public class GeometricTest {
public static void main(String[] args) {
GeometricObject circle = new Circle(1,"red",1);
GeometricObject myRectangle = new MyRectangle(Math.PI,1,"blue",1);
GeometricTest t = new GeometricTest();
System.out.println(t.equalsArea(circle, myRectangle)); //true
t.displayGeometircObject(circle); //圆形的面积:3.141592653589793
t.displayGeometircObject(myRectangle); //长方形的面积:3.141592653589793
}
public boolean equalsArea(GeometricObject obj1,GeometricObject obj2){
if(obj1.findArea() == obj2.findArea()){
return true;
}
return false;
}
public void displayGeometircObject(GeometricObject obj){
if(obj instanceof Circle){
System.out.print("圆形的面积:");
}else if(obj instanceof MyRectangle){
System.out.print("长方形的面积:");
}
System.out.println(obj.findArea());
}
}
class GeometricObject{
String color;
double weight;
public GeometricObject(String color, double weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
public double findArea(){
return 0;
}
}
class Circle extends GeometricObject{
double radius;
public Circle(double radius,String color,double weight){
super(color,weight);
this.radius = radius;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
@Override
public double findArea() {
return Math.PI * radius * radius;
}
}
class MyRectangle extends GeometricObject{
double width;
double height;
public MyRectangle(double width,double height,String color,double weight){
super(color,weight);
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
@Override
public double findArea() {
return width * height;
}
}
练习4:多态是编译时行为还是运行时行为?如何证明?
运行时行为,
animal.eat()的结果不确定
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
public class InterviewTest {
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}
public static void main(String[] args) {
int key = new Random().nextInt(3);
System.out.println(key);
Animal animal = getInstance(key);
animal.eat();
}
}
练习5:判断输出结果
public class InterviewTest1 {
public static void main(String[] args) {
Base base = new Sub();
base.add(1, 2, 3); //sub_1
// Sub s = (Sub)base;
// s.add(1,2,3); //sub_2 子类特有的方法
}
}
class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}
class Sub extends Base {
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
// public void add(int a, int b, int c) {
// System.out.println("sub_2");
// }
}
练习6:判断输出结果
//getXxx()和setXxx()声明在哪个类中,内部操作的属性就是哪个类里的。
public class InterviewTest2 {
public static void main(String[] args) {
Father f = new Father();
Son s = new Son();
System.out.println(f.getInfo());//atguigu
System.out.println(s.getInfo());//尚硅谷
s.test();//尚硅谷 atguigu
System.out.println("-----------------");
s.setInfo("大硅谷");
System.out.println(f.getInfo());//atguigu
System.out.println(s.getInfo());//大硅谷
s.test();//大硅谷 atguigu
}
}
class Father {
private String info = "atguigu";
public void setInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
}
class Son extends Father {
private String info = "尚硅谷";
public void setInfo(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void test() {
System.out.println(this.getInfo());
System.out.println(super.getInfo());
}
}
8. Object 类的使用
8.1 如何理解根父类
类 java.lang.Object是类层次结构的根类,即所有类的父类。每个类都使用 Object 作为超类。
-
Object类型的变量与除Object以外的任意引用数据类型的对象都多态引用
method(Object obj){…} //可以接收任何类作为其参数 Person o=new Person(); method(o); -
所有对象(包括数组)都实现这个类的方法。
-
如果一个类没有特别指定父类,那么默认则继承自Object类。例如:
public class Person { ... } //等价于: public class Person extends Object { ... }
8.2 Object类的方法
根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。今天我们主要学习其中的5个:
1、equals()
= =:
-
基本类型比较值:只要两个变量的值相等,即为true。
int a=5; if(a==6){…} -
引用类型比较引用(是否指向同一个对象):只有指向同一个对象时,==才返回true。
Person p1=new Person(); Person p2=new Person(); if (p1==p2){…}- 用“==”进行比较时,符号两边的
数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错
- 用“==”进行比较时,符号两边的
**equals():**所有类都继承了Object,也就获得了equals()方法。还可以重写。
-
只能比较引用类型,其作用与“==”相同,比较是否指向同一个对象。
-
格式:obj1.equals(obj2)
-
特例:当用equals()方法进行比较时,对类**File、String、Date及包装类(Wrapper Class)**来说,是比较类型及内容而不考虑引用的是否是同一个对象;
- 原因:在这些类中重写了Object类的equals()方法。
-
当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等
-
重写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”。
-
-
重写举例:
class User{
private String host;
private String username;
private String password;
public User(String host, String username, String password) {
super();
this.host = host;
this.username = username;
this.password = password;
}
public User() {
super();
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [host=" + host + ", username=" + username + ", password=" + password + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (host == null) {
if (other.host != null)
return false;
} else if (!host.equals(other.host))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
}
面试题:==和equals的区别
-
== 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
-
equals的话,它是属于
java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。 -
具体要看自定义类里有没有重写Object的equals方法来判断。
-
通常情况下,重写equals方法,会比较类中的相应属性是否都相等。
练习1:
int it = 65;
float fl = 65.0f;
System.out.println(“65和65.0f是否相等?” + (it == fl)); // true
char ch1 = 'A'; char ch2 = 12;
System.out.println("65和'A'是否相等?" + (it == ch1));//true
System.out.println("12和ch2是否相等?" + (12 == ch2));// true
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println("str1和str2是否相等?"+ (str1 == str2));// false :==比较的时String的地址值
System.out.println("str1是否equals str2?"+(str1.equals(str2)));// true
System.out.println(“hello” == new java.util.Date()); // 编译报错 Operator '==' cannot be applied to 'java.lang.String', 'java.util.Date'
练习2:
编写Order类,有int型的orderId,String型的orderName,相应的getter()和setter()方法,两个参数的构造器,重写父类的equals()方法:public boolean equals(Object obj),并判断测试类中创建的两个对象是否相等。
public class OrderTest {
public static void main(String[] args) {
Order o1 = new Order(1,"tom");
Order o2 = new Order(1,"tom");
System.out.println(o1.equals(o2));
}
}
class Order{
int orderId;
String orderName;
public Order(int orderId, String orderName) {
this.orderId = orderId;
this.orderName = orderName;
}
public int getOrderId() {
return orderId;
}
public void setOrderId(int orderId) {
this.orderId = orderId;
}
public String getOrderName() {
return orderName;
}
public void setOrderName(String orderName) {
this.orderName = orderName;
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;
}
if(o instanceof Order){
Order order =(Order)obj;
return this.orderName.equals(order.orderName) && this.orderId == order.orderId;
}
}
// 自动生成
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// Order order = (Order) o;
// return orderId == order.orderId && Objects.equals(orderName, order.orderName);
// }
}
练习3:
请根据以下代码自行定义能满足需要的MyDate类,在MyDate类中覆盖equals方法,使其判断当两个MyDate类型对象的年月日都相同时,结果为true,否则为false。 public boolean equals(Object o)
public class EqualsTest {
public static void main(String[] args) {
MyDate m1 = new MyDate(14, 3, 1976);
MyDate m2 = new MyDate(14, 3, 1976);
if (m1 == m2) {
System.out.println("m1==m2");
} else {
System.out.println("m1!=m2"); // m1 != m2
}
if (m1.equals(m2)) {
System.out.println("m1 is equal to m2");// m1 is equal to m2
} else {
System.out.println("m1 is not equal to m2");
}
}
}
class MyDate{
private int year;
private int month;
private int day;
public MyDate(int day,int month,int year){
this.day = day;
this.month = month;
this.year = year;
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
MyDate myDate = (MyDate)obj;
return this.year == myDate.year && this.month == myDate.month && this.day == myDate.day;
}
}
2、toString()
方法签名:public String toString()
① 默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"
② 在进行String与其它类型数据的连接操作时,自动调用toString()方法
Date now=new Date();
System.out.println(“now=”+now); //相当于
System.out.println(“now=”+now.toString());
③ 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()
因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。
④ 可以根据需要在用户自定义类型中重写toString()方法 如String 类重写了toString()方法,返回字符串的值。
s1="hello";
System.out.println(s1);//相当于System.out.println(s1.toString());
例如自定义的Person类:
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
练习:定义两个类,父类GeometricObject代表几何形状,子类Circle代表圆形。
public class GeometricTest2 {
public static void main(String[] args) {
GeometricObject object1 = new Circle("red",1,1);
System.out.println(object1.toString());
}
}
class GeometricObject{
protected String color;
protected double weight;
public GeometricObject() {
}
public GeometricObject(String color, double weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
}
class Circle extends GeometricObject {
private double radius;
public Circle() {
}
public Circle(double radius) {
this.radius = radius;
}
public Circle(String color, double weight, double radius) {
super(color, weight);
this.radius = radius;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
public double findArea(){
return Math.PI * radius * radius;
}
public String toString(){
return "Circle1{radius=" + radius
+",color="+color +
",weight="+weight+"}";
}
}
3、getClass()
public final Class<?> getClass():获取对象的运行时类型
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}
4、hashCode()
public int hashCode():返回每个对象的hash值。
如果重写equals,那么通常会一起重写hashCode()方法,hashCode()方法主要是为了当对象存储到哈希表(后面集合章节学习)等容器中时提高存储和查询性能用的,这是因为关于hashCode有两个常规协定:
- ①如果两个对象的hash值是不同的,那么这两个对象一定不相等;
- ②如果两个对象的hash值是相同的,那么这两个对象不一定相等。
重写equals和hashCode方法时,要保证满足如下要求:
-
①如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;
-
②如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;
-
③如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false
public static void main(String[] args) {
System.out.println("Aa".hashCode());//2112
System.out.println("BB".hashCode());//2112
}
5、clone()
//Object类的clone()的使用
public class CloneTest {
public static void main(String[] args) {
Animal a1 = new Animal("花花");
try {
Animal a2 = (Animal) a1.clone();
System.out.println("原始对象:" + a1);
a2.setName("毛毛");
System.out.println("clone之后的对象:" + a2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Animal implements Cloneable{
private String name;
public Animal() {
super();
}
public Animal(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal [name=" + name + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}
6、finalize()
public class FinalizeTest {
public static void main(String[] args) {
Person p = new Person("Peter", 12);
System.out.println(p);
p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
System.gc();//强制性释放空间
}
}
class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//子类重写此方法,可在释放对象前进行某些操作
@Override
protected void finalize() throws Throwable {
System.out.println("对象被释放--->" + this);
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}