Java基础学习——第五章 面向对象编程(中)
一、面向对象的特征二:继承性
1. 继承性的优点
- 减少代码的冗余,提高了代码的复用性
- 便于功能的扩展
- 继承让类与类之间产生了关系,为多态性的使用提供了前提
2. 继承的格式
class A extends B{}
A:子类 / 派生类 / subclass
B:父类 / 超类 / 基类 / superclass
3. 继承性的规定
-
当子类继承了父类后,就继承了父类中声明的所有属性和方法。可以使用父类中定义的属性和方法,也可以声明新的属性和方法,实现功能的扩展
-
在Java 中,继承的关键字是“extends”,即子类不是父类的子集,而是对父类的“扩展”
-
特别的,子类能继承父类中私有的(private)的属性和方法,只是由于封装性的限制不能直接调用。但是可以通过其他操作(如setter和getter)来访问这些private的属性和方法
-
Java只支持==单继承和多层继承==,不允许多重继承:
- 一个子类只能有一个父类:单继承
- 一个父类可以派生出多个子类
- 子父类是相对的概念,多个类之间是可以多层继承的
- 子类继承父类后,就继承了直接父类以及所有间接父类的属性和方法
- 如果我们没有显式的声明一个类的父类,则此类继承于 java.lang.Object 类。即所有的java类都直接或间接地继承于Object类,即所有的java类都继承了Object类的功能。
4. 继承性例题
public class Day12_KidsTest {
public static void main(String[] args) {
Kids someKid = new Kids();
//父类的private属性不能直接调用,但可以通过get和set方法访问
someKid.setSex(1);
someKid.setSalary(0);
someKid.manOrWoman();
someKid.employed();
}
}
class ManKind {
private int sex;
private int salary;
public ManKind() {
}
public ManKind(int sex, int salary) {
this.sex = sex;
this.salary = salary;
}
public void manOrWoman() {
if (sex == 1) {
System.out.println("man");
} else if (sex == 0) {
System.out.println("woman");
}
}
public void employed() {
// if (salary == 0) {
// System.out.println("no job");
// } else {
// System.out.println("job");
// }
String info = (salary == 0)? "no job" : "job";
System.out.println(info);
}
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;
}
}
class Kids extends ManKind {
private int yearsOld;
public Kids() {
}
public Kids(int sex, int salary, int yearsOld) {
super(sex, salary);
this.yearsOld = yearsOld;
}
public void printAge() {
System.out.println(yearsOld);
}
}
二、方法的重写
1. 方法重写的概念
- 在子类中可以根据需要对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。
- 重写后,子类的方法将覆盖父类的方法。通过子类对象调用该同名同参数方法时,实际执行的是子类中重写的方法;而通过父类对象调用该同名同参数方法时,仍然执行父类中声明的方法(子类对父类没有影响)
3. 重写的规定
- 子类重写的方法必须和父类被重写的方法具有==相同的方法名、形参列表==
(因为在同一类中,允许存在多个同名但形参列表不同的重载方法,因此只有确定方法名+形参列表,才能确定一个唯一的方法) - 子类重写的方法使用的==权限修饰符不小于父类被重写的方法的访问权限(>=)
特殊情况:子类不能重写父类中声明为private权限的方法== - 子类重写的方法的==返回值类型不大于父类被重写的方法==的返回值类型(<=)
- 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型必须是void
- 父类被重写的方法的返回值类型是引用数据类型(A类),则子类重写的方法的返回值类型是A类或A类的子类
- 父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值必须是相同的基本数据类型
- 子类重写的方法==抛出的异常类型不大于父类被重写方法的异常类型==
- 子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为 static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。
3. 区分方法的重载与重写
3.1 从定义的角度来看
- 重载:在同一个类中,允许存在多个同名,但参数个数或者参数类型不同的方法。(两同一不同:同一类、同名的方法;不同的形参列表);此外,同一个类中的构造器也可以重载
- 重写:在子类中对从父类中继承来的方法进行改造,覆盖父类的方法。子类重写的方法必须和父类被重写的方法具有相同的方法名、形参列表
3.2 从编译和运行的角度来看
- 重载(不表现为多态性):指允许存在多个同名方法,而这些方法的参数不同。 编译器根据方法不同的形参列表, 对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法,它们的调用地址在编译期就绑定了。 Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。所以: 对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为==“早绑定”或“静态绑定”== ;
- 而对于多态,只有等到方法调用的那一刻, 解释运行器才会确定所要调用的具体方法,这称为==“晚绑定”或**“动态绑定” **。重写是多态性的体现==
三、测试4种权限修饰符
1、类内部(4种都可以用)
package test.order;
/**
* @ClassName Day12_Order
* @Description 测试4种权限修饰符在同一个类内的使用
*/
public class Day12_Order {
private int orderPrivate;
int orderDefault;
protected int orderProtected;
public int orderPublic;
private void methodPrivate() {
//四种权限的属性和方法在类内部都可以直接调用
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
void methodDefault() {
//四种权限的属性和方法在类内部都可以直接调用
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
protected void methodProtected() {
//四种权限的属性和方法在类内部都可以直接调用
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
public void methodPublic() {
//四种权限的属性和方法在类内部都可以直接调用
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
}
2、同一个包的非子类或子类(public、protected、default可以用)
package test.order;
/**
* @ClassName Day12_OrderTest
* @Description 测试4种权限修饰符在同一个包的非子类中的使用
*/
public class Day12_OrderTest {
public static void main(String[] args) {
Day12_Order order = new Day12_Order();
order.orderDefault = 1;
order.orderProtected = 2;
order.orderPublic = 3;
order.methodDefault();
order.methodProtected();
order.methodPublic();
//在同一个包的其他类(非子类)中,不能调用Order类的private属性和方法
//'orderPrivate' has private access in 'test.order.Day12_Order'
order.orderPrivate = 4;
//'methodPrivate()' has private access in 'test.order.Day12_Order'
order.methodPrivate();
}
}
- 注意:同一个包的子类能继承父类中的private属性和方法,但由于封装性的限制不能直接调用
3、不同包的子类(public、protected可以用)
package test.suborder;
import test.order.Day12_Order;
/**
* @ClassName Day12_SubOrder
* @Description 4种权限修饰符在不同包的子类中的使用
*/
public class Day12_SubOrder extends Day12_Order {
public void method() {
orderProtected = 1;
orderPublic = 2;
methodProtected();
methodPublic();
//在不同包的子类中,不能直接调用Order类的private、缺省(default)属性和方法
//'orderDefault' is not public in 'test.order.Day12_Order'. Cannot be accessed from outside package
orderDefault = 3;
//'orderPrivate' has private access in 'test.order.Day12_Order'
orderPrivate = 4;
//'methodDefault()' is not public in 'test.order.Day12_Order'. Cannot be accessed from outside package
methodDefault();
//'methodPrivate()' has private access in 'test.order.Day12_Order'
methodPrivate();
}
}
- 注意:不同包的子类能继承父类中的private、缺省(default)属性和方法,但由于封装性的限制不能直接调用
4、不同包的非子类(public可以用)
package test.suborder;
import test.order.Day12_Order;
/**
* @ClassName Day12_test
* @Description 4种权限修饰符在不同包的非子类中的使用
*/
public class Day12_test {
public static void main(String[] args) {
Day12_Order order = new Day12_Order();
order.orderPublic = 1;
order.methodPublic();
//在不同包的其他类(非子类)中,不能调用Order类的private、缺省(default)、protected属性和方法
//'orderPrivate' has private access in 'test.order.Day12_Order'
order.orderPrivate = 2;
//'orderDefault' is not public in 'test.order.Day12_Order'. Cannot be accessed from outside package
order.orderDefault = 3;
//'orderProtected' has protected access in 'test.order.Day12_Order'
order.orderProtected = 4;
//'methodPrivate()' has private access in 'test.order.Day12_Order'
order.methodPrivate();
//'methodDefault()' is not public in 'test.order.Day12_Order'. Cannot be accessed from outside package
order.methodDefault();
//'methodProtected()' has protected access in 'test.order.Day12_Order'
order.methodProtected();
}
}
四、关键字:super
1. 问题的由来
- 当子类重写了父类的方法后,子类重写的方法将覆盖父类被重写的方法,通过子类对象调用该同名同参数方法时,实际执行的是子类重写后的方法。如果想再调用父类中被重写的方法,就需要使用super关键字进行区分
- this关键字使用的场景:在类的方法中,如果形参和类的属性出现重名的情况,则使用this关键字进行区分。this表示当前类的对象或正在创建的对象
- super关键字使用的场景:当子类重写了父类的方法后,如果仍要调用父类中同名同参数的方法,就需要使用super关键字进行区分。super表示父类内存空间的标识
2. super关键字的使用
- 在Java类中使用super来调用父类中的指定成员
2.1 super调用父类的属性和方法
- 在子类的方法或构造器中,使用 “super.属性” 或 “super.方法”,显式的调用父类中声明的属性或方法。但通常情况下,我们都选择省略super
- 特殊情况:当子类和父类定义了同名的属性时,若想在子类中调用父类的同名属性,则必须显式的使用 “super.属性”,表明调用的是父类中声明的属性
- 特殊情况:当子类重写了父类的方法后,若想在子类中调用父类被重写的方法,则必须显式的使用 “super.方法”,表明调用的是父类中声明的方法
2.2 super调用父类的构造器
- 在子类的构造器中显式的使用 “super(形参列表)” 调用父类中指定的构造器
- 如果要在子类的构造器中调用父类的构造器,则必须要将 “super(形参列表)” 声明在首行
- 在类的构造器中,最多只能声明一个 “this(形参列表)” 或 “super(形参列表)” ,即最多只能调用一个本类中重载的构造器或父类中指定的构造器(二选一)!
- 在类的构造器中,如果没有显式的声明 “this(形参列表)” 或 “super(形参列表)”,则默认在首行声明了一个父类中空参的构造器 “super()”。
- 当父类中没有空参构造器时,子类的构造器必须通过 “this(形参列表)” 或者 "super(形参列表)"语句指定调用本类或者父类中相应的构造器
- 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参构造器,则编译出错
- 在类的多个构造器中,至少有一个构造器使用了 “super(形参列表)”
package test;
/**
* @ClassName Day12_SuperTest
* @Description super关键字的使用
*/
public class Day12_SuperTest {
public static void main(String[] args) {
SuperStudent stu = new SuperStudent();
stu.show();
}
}
class SuperPerson {
//属性
String name;
int age;
String id = "1002"; //身份证号
//构造器
public SuperPerson() {
}
public SuperPerson(String name) {
this.name = name;
}
public SuperPerson(String name, int age) {
//在构造器中使用 "this(形参列表)" 调用当前类中指定的其他构造器,放在首行
this(name);
this.age = age;
}
//方法
public void eat() {
System.out.println("人:吃饭");
}
}
class SuperStudent extends SuperPerson {
//属性
String major;
String id = "1001"; //学号,注意父类和子类中的同名属性不会覆盖
//构造器
public SuperStudent() {
}
public SuperStudent(String major) {
//默认有一个super();
this.major = major;
}
public SuperStudent(String name, int age, String major) {
//在子类的构造器中使用 "super(形参列表)" 调用父类中指定的构造器,放在首行
super(name, age);
this.major = major;
}
@Override
public void eat() {
System.out.println("学生:吃饭");
}
public void show() {
//super和this用来区分父类和子类中同名的属性:this表示当前类的对象或正在创建的对象;super表示父类内存空间的标识
System.out.println("身份证:" + super.id + ", 学号:" + this.id);
//子类中重写的方法
this.eat();
//父类中被重写的方法
super.eat();
}
}
3. this和super关键字的区别
五、子类对象实例化全过程
-
从结果上来看:子类继承了父类以后,就继承了直接父类以及所有间接父类的属性和方法。创建子类的对象后,在堆空间中就会加载所有父类中声明的属性
-
从过程上来看:
- 当使用子类的构造器创建对象时,需要保证先初始化父类,此时一定会直接或间接地调用父类的构造器,进而调用间接父类的构造器……直到调用了 java.lang.Object 类中空参的构造器
- 当子类继承父类后,“继承”父类中所有的属性和方法,因此子类有必要知道父类如何为对象进行初始化。正因为==调用过直接父类以及所有间接父类的构造器==,所以在子类对象的内存空间中才能看到父类的成员,子类对象才能调用这些属性和方法
- 虽然创建子类对象时,调用了父类的构造器,但自始至终就创建过一个对象,即new的子类对象
实验——继承性 & super
package test.account;
/**
* @ClassName Account
* @Description 实验——继承性 & super
*/
public class Account {
private int id; //账号
private double balance; //余额
private double annualInterestRate; //年利率
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;
return;
}
System.out.println("余额不足!");
}
//存钱
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
}
package test.account;
/**
* @ClassName CheckAccount
* @Description 创建 Account 类的一个子类 CheckAccount 代表可透支的账户,
* 该账户中定义一个属性 overdraft 代表可透支限额。
* 在 CheckAccount 类中重写 withdraw 方法
*/
public class CheckAccount extends Account {
private double overdraft;
public CheckAccount(int id, double balance, double annualInterestRate, double overdraft) {
super(id, balance, annualInterestRate);
this.overdraft = overdraft;
}
public double getOverdraft() {
return overdraft;
}
public void setOverdraft(double overdraft) {
this.overdraft = overdraft;
}
@Override
public void withdraw(double amount) {
if (amount <= getBalance()) {
//setBalance(getBalance() - amount);
super.withdraw(amount); //直接调用父类中的withdraw方法
} else {
if (amount - getBalance() <= overdraft) {
overdraft -= (amount - getBalance());
//setBalance(0);
super.withdraw(getBalance());
} else {
System.out.println("超出可透支额的限度!");
}
}
}
}
package test.account;
/**
* @ClassName AccountTest
* @Description
*
* 写一个用户程序测试 Account 类。
* 在用户程序中,创建一个账号为 1122、余额为 20000、年利率 4.5%的 Account 对象。
* 使用 withdraw 方法提款 30000 元,并打印余额。
* 再使用 withdraw 方法提款 2500 元,
* 使用 deposit 方法存款 3000 元,然后打印余额和月利率。
*
* 写一个用户程序测试 CheckAccount 类。
* 在用户程序中,创建一个账号为 1122、余额为 20000、年利率 4.5%,可透支限额为 5000 元的 CheckAccount 对象。
* 使用 withdraw 方法提款 5000 元,并打印账户余额和可透支额。
* 再使用 withdraw 方法提款 18000 元,并打印账户余额和可透支额。
* 再使用 withdraw 方法提款 3000 元,并打印账户余额和可透支额。
*/
public class AccountTest {
public static void main(String[] args) {
Account account = new Account(1122, 20000, 0.045);
account.withdraw(30000);
System.out.println("您的账户余额为:" + account.getBalance());
account.withdraw(2500);
account.deposit(3000);
System.out.println("您的账户余额为:" + account.getBalance());
System.out.println("月利率为:" + (account.getAnnualInterestRate() * 100) + "%");
CheckAccount check = new CheckAccount(1122, 20000, 0.045, 5000);
check.withdraw(5000);
System.out.println("您的账户余额为:" + check.getBalance());
System.out.println("您的可透支额度为:" + check.getOverdraft());
check.withdraw(18000);
System.out.println("您的账户余额为:" + check.getBalance());
System.out.println("您的可透支额度为:" + check.getOverdraft());
check.withdraw(3000);
System.out.println("您的账户余额为:" + check.getBalance());
System.out.println("您的可透支额度为:" + check.getOverdraft());
}
}
六、面向对象的特征三:多态性
1. 理解多态性
- 可以理解为一个事物的多种形态
2. 何为多态性:对象的多态性
- 对象的多态性:父类的引用指向子类的对象(将子类对象赋给父类的引用类型变量),即子类的对象可以替代父类的对象使用
- 一个变量只能有一种确定的数据类型,但一个引用类型变量可能指向(引用)多种不同类型的对象
- 子类可看做是特殊的父类, 所以父类类型的引用类型变量可以指向子类的对象:向上转型(upcasting)。
Person p = new Student(); //Person类型的引用类型变量p,指向Student类型的对象
3. 多态的使用:虚拟方法调用
- Java引用类型变量有两种类型: 编译时类型和运行时类型。 编译时类型由声明该变量时使用的类型决定, 运行时类型由实际赋给该变量的对象决定。 简称: 编译时, 看左边;运行时, 看右边
- 若编译时类型和运行时类型不一致, 就出现了对象的多态性。在多态情况下:
- “编译时, 看左边” : 看的是父类的引用(父类中不具备子类特有的方法,即如果一个引用类型变量声明为父类的类型,但实际指向的是子类的对象,那么在编译时,只能调用父类中声明的方法)
- “运行时, 看右边” : 看的是子类的对象(虽然在编译时调用的是父类中声明的方法,但在运行时实际执行的是子类重写父类的方法——虚拟方法调用)
- 虚拟方法调用:子类对父类的方法进行了重写,在多态情况下,将父类中被重写的方法称为虚拟方法。父类类型的引用变量根据赋给它的不同子类对象,动态地调用子类中重写的方法。这样的方法调用在编译期是无法确定的,因此==多态是一种运行时行为!==
- 对象的多态性,只适用于方法,不适用于属性。属性不具备多态性,只看引用类型变量声明的类型(即属性的编译和运行都看左边)。
- 如果父类和子类具有同名的属性,且具有不同的初始化值。在多态情况下,该属性的值应为父类中初始化的值
//正常情况下:
Student s = new Student();
s.school = “pku”; //合法,Student类有school属性
s.study(); //合法,Student类有study方法
s.eat(); //合法,Student类有eat方法
//多态情况下:虚拟方法调用
Person p = new Student();
p.school = “pku”; //非法,属性是在编译时确定的,编译时p为Person类型,无法调用子类Student中特有的属性
p.study(); //非法,编译时p为Person类型,无法调用子类Student中特有的方法
p.eat(); //合法,编译时p为Person类型,有eat方法;但实际运行的是子类Student中重写的eat方法
4. 多态性的使用前提
- 存在类的继承关系
- 子类对父类的方法进行了重写
5. 多态性的意义
- 没有多态性,抽象类和接口就没有存在的意义。因为抽象类和接口是不能创建对象的,因此在使用抽象类和接口时,我们一定会提供其子类或实现类的对象。
- 一旦方法的形参是引用数据类型的,如果没有多态性,意味着我们要声明大量重载的方法,方法的通用性会极大地缩小
6. 多态性的应用举例
package test;
/**
* @ClassName Day12_PolymorphismTest
* @Description 多态性的应用举例
*/
public class Day12_AnimalTest {
public static void main(String[] args) {
Day12_AnimalTest test = new Day12_AnimalTest();
test.func(new Dog()); //Animals animals = new Dog();
test.func(new Cat()); //Animals animals = new Cat();
}
public void func(Animals animals) {
animals.eat();
animals.shout();
}
}
class Animals {
public void eat() {
System.out.println("动物:进食");
}
public void shout() {
System.out.println("动物:叫");
}
}
class Dog extends Animals {
@Override
public void eat() {
System.out.println("狗:吃骨头");
}
@Override
public void shout() {
System.out.println("狗:汪汪汪");
}
}
class Cat extends Animals {
@Override
public void eat() {
System.out.println("猫:吃鱼");
}
@Override
public void shout() {
System.out.println("猫:喵喵喵");
}
}
7. 向下转型(upcasting)
7.1 向上转型和向下转型
- 在多态情况下,内存中实际加载了子类特有的属性和方法,但是由于引用类型变量声明为父类类型,导致编译时只能调用父类的属性和方法,而不能调用子类特有的属性和方法。
- 此时,如何才能调用子类特有的属性和方法?——向下转型:使用强制类型转换符 "()"
- 注意:强制类型转换 转换的是引用数据类型变量的类型,即左边编译时的类型
//父类的引用指向子类的对象(将子类对象赋给父类的引用变量)————向上转型(多态)
Person p = new Student();
p.school = “pku”; //非法,属性是在编译时确定的,编译时p为Person类型,无法调用子类Student中特有的属性
p.study(); //非法,编译时p为Person类型,无法调用子类Student中特有的方法
//将父类类型的引用变量 强制类型转换 为子类类型————向下转型
Student s = (Student)p;
s.school = “pku”; //合法,此时引用变量p被强制类型转换为子类Student类型,能调用子类中的属性
s.study(); //合法,此时引用变量p被强制类型转换为子类Student类型,能调用子类中的方法
7.2 instanceof关键字
- 强制类型转换的风险:在基本数据类型变量的强制转换中可能会有精度损失;而在多态情况下,父类类型的引用类型变量的强制转换需要预先知道子类对象的类型,即强转后的类型必须与子类对象的实际类型保持一致
- 为了避免向下转型时出现ClassCastException的异常,我们在向下转型之前,先进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。
x instanceof A //如:p instanceof Student
检验x是否为类A的对象,返回值为boolean型
3. 规定:若类B是类A的父类,且 a instanceof A 返回true,则 a instanceof B 也返回true
也就是说,假设引用类型变量a实际指向的是类A的对象,则他指向的对象不仅是类A的对象,也是类A的父类的对象——苹果是苹果的对象,苹果也是水果的对象
public class Day13_InstanceTest {
public static void main(String[] args) {
Day13_InstanceTest test = new Day13_InstanceTest();
test.method(new Person2());
test.method(new Student2());
test.method(new Graduate2());
}
public void method(Person2 e) {
System.out.println(e.getInfo()); //虚拟方法调用,根据e实际指向的对象类型调用相应类的方法
if (e instanceof Graduate2) {
System.out.println("a graduated student");
}
if (e instanceof Student2) {
System.out.println("a student");
}
if (e instanceof Person2) { //Condition 'e instanceof Person2' is always 'true'
System.out.println("a person");
}
}
}
class Person2 {
protected String name="person";
protected int age=50;
public String getInfo() {
return "Name: "+ name + "\n" +"age: "+ age;
}
}
class Student2 extends Person2 {
protected String school="pku";
public String getInfo() {
return "Name: "+ name + "\nage: "+ age + "\nschool: "+ school;
}
}
class Graduate2 extends Student2{
public String major="IT";
public String getInfo() {
return "Name: "+ name + "\nage: "+ age + "\nschool: "+ school+"\nmajor:"+major;
}
}
- 如果e指向的是Graduate类的对象,那他指向的对象是Graduate类的对象,也是父类Student和Person的对象,即 e instanceof Graduate, e instanceof Student 和 e instanceof Person都返回true
- 如果e指向的是Student类的对象,那他指向的对象是Student类的对象,也是父类Person的对象,但不是子类Graduate的对象,即 e instanceof Student 和 e instanceof Person返回true,e instanceof Graduate 返回false
- 如果e指向的是Person类的对象,那他指向的对象是Person类的对象,但不是子类Student和Graduate的对象,即 e instanceof Person 返回true,但 e instanceof Student 和 e instanceof Graduate 返回false
7.3 向下转型的几个常见问题
public class Day13_DownCastingTest {
public static void main(String[] args) {
//情况一:编译时通过,运行时不通过
//举例一:
People p1 = new Woman();
//ClassCastException: test.Woman cannot be cast to test.Man
Man m1 = (Man)p1;
//举例二:
People p2 = new People();
//ClassCastException: test.People cannot be cast to test.Man
Man m2 = (Man)p2;
//情况二:编译通过,运行也通过
Object obj = new Woman();
//obj可以强转成Woman类型,则一定可以强转成Woman的父类People类型
People p3 = (People)obj;
//情况三:编译不通过
Man m3 = new Woman(); //Woman不是Man的子类
}
}
class People {}
class Man extends People {}
class Woman extends People {}
8. 多态性练习
8.1 多态情况下调用方法和属性
- 若子类重写了父类的方法,就意味着子类里定义的方法彻底覆盖了父类的同名方法,系统将不可能把父类里的方法转移到子类中:编译看左边,运行看右边
- 属性不存在重写,即使子类中定义了与父类同名的属性,这一属性依然不可能覆盖父类中同名的属性。即在子类对象的内存空间中两个同名的属性都存在,具体调用哪一个就看引用类型变量的类型:编译和运行都看左边
public class Day13_FieldMethodTest {
public static void main(String[] args){
Sub s = new Sub();
System.out.println(s.count); //20
s.display(); //20——子类重写了父类的方法
Base b = s; //多态:父类的引用指向了子类的对象,内存空间中两个同名的count属性都存在
System.out.println(b == s); //true——两个引用数据类型比较的是地址值
System.out.println(b.count); //10——属性不存在多态性,属性编译运行都看左边
b.display(); //20——虚拟方法调用:虽然在编译时调用的是父类的方法,但在运行时实际执行的是子类重写父类的方法
}
}
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);
}
}
8.2 多态性的使用
/**
* @ClassName Day13_GeometricTest
* @Description 定义一个测试类GeometricTest,
* 编写equalsArea方法测试两个对象的面积是否相等(注意方法的参数类型,利用动态绑定技术),
* 编写displayGeometricObject方法显示对象的面积(注意方法的参数类型,利用动态绑定技术)
*/
public class Day13_GeometricTest {
public static void main(String[] args) {
Day13_GeometricTest test = new Day13_GeometricTest();
Circle2 c1 = new Circle2("red", 1.0, 2.3);
Circle2 c2 = new Circle2("white", 1.5, 3.3);
test.displayGeometricObject(c1); //对象的多态性'
System.out.println(test.equalsArea(c1, c2));
}
//动态绑定技术,虚拟方法调用,编译时调用的是父类的方法,运行时实际执行的是子类重写父类的方法
public boolean equalsArea(GeometricObject o1, GeometricObject o2) {
return o1.findArea() == o2.findArea();
}
public void displayGeometricObject(GeometricObject o) {
System.out.println(o.findArea());
}
}
class GeometricObject {
protected String color;
protected 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.0;
}
}
class Circle2 extends GeometricObject {
private double radius;
public Circle2(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;
}
@Override
public double findArea() {
return Math.PI * radius * radius;
}
}
class MyRectangle extends GeometricObject {
private double width;
private double height;
public MyRectangle(String color, double weight, double width, double height) {
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;
}
}
8.3 考查多态的笔试题目
public class Day13_InterviewTest {
public static void main(String[] args) {
//对象的多态性
Base1 base = new Sub1();
//编译时调用的是父类的方法,运行时实际执行的是子类重写父类的方法
base.add(1, 2, 3); //sub_1
//向下转型
Sub1 s = (Sub1)base;
//如果类中同时存在可变个数形参和确定参数的重载方法,
//且在调用时传的形参都符合二者的形参列表,则优先调用确定参数的方法
s.add(1,2,3); //sub_2
}
}
class Base1 {
public void add(int a, int ... arr) {
System.out.println("base");
}
}
class Sub1 extends Base1 {
//在java中,可变个数形参与相同类型的数组形参本质上是相同的
//即public void add(int a, int ... arr)和public void add(int a, int[] arr)的形参列表是一致的
//所以认为子类对父类的方法进行了重写。因此在多态情况下,运行时实际执行的是子类重写父类的方法
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");
}
}
9. 谈谈对多态性的理解
- 实现代码的通用性:父类的引用指向子类的对象。编译时调用的是父类的方法,运行时实际执行的子类重写父类的方法(虚拟方法调用)
- 举例:
- Object类中定义的public boolean equals(Object obj) {}
- JDBC:使用Java程序操作(获取数据库连接、CRUD) 数据库(MySQL、Oracle、DB2、SQL Server)
- 如果没有多态性的话,抽象类和接口就没有意义;抽象类和接口的使用体现了多态性,因为抽象类和借口不能实例化
七、Object类的使用
1. Object类是所有Java类的根父类
- 如果在类的声明中未使用extends关键字指明其父类, 则默认父类为java.lang.Object类:
public class Person {
...
}
等价于:
public class Person extends Object {
...
}
例如:
method(Object obj){…} //可以接收任何类作为其参数:对象的多态性
Person p = new Person();
method(p);
- 数组也可以看成一种特殊的类,继承与Object类,同样也能调用Object中声明的方法
2. Object类中的主要结构
- Object类中没有声明属性,且只声明了一个空参构造器
- Object类中的主要方法
3. equals()方法
3.1 比较运算符 ==
- 可以用来比较基本数据类型变量或引用数据类型变量:
- 基本数据类型变量比较数值:当两个变量的数值相等,返回true。不一定类型要相同,存在自动类型提升(如char型和int型进行比较,char型先自动类型提升为int型);但是两个变量的类型一定要能统一(如boolean型和int型就无法比较)
- 引用数据类型变量比较地址值:当两个引用类型变量保存的地址值相同,即指向同一个对象时,返回true
3.2 equals()方法
3.2.1 Object类中的equals()方法
- 所有类都继承了Object类, 也就获得了equals()方法。 可以对equals()方法进行重写
- Object类中equals()的定义:指示其他某个对象是否与此对象“相等”,返回值为boolean类型
public boolean equals(Object obj) {
return (this == obj);
}
- equals()方法只适用于比较两个引用数据类型变量,其作用和比较运算符 == 相同,比较两个引用数据类型变量的地址值是否相同,即是否指向同一个对象实体
- 格式:obj1.equals(obj2)
3.2.2 String/Date/File/包装类中的equals()方法
- String、Date、File、包装类等都重写了Object类中的equals()方法,重写后的方法不再比较两个引用类型变量的地址值,而是比较两个引用类型变量指向的两个对象的“实际内容”(一般是相应的属性)是否相同。
3.2.3 自定义类重写equals()方法
- 自定义类的对象调用equals()方法,通常希望==比较两个对象的“实际内容”(相应的属性值)是否相同==。因此,我们需要对Object类中的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”,null.equals(x)会报空指针异常 ;
x.equals(和x不同类型的对象)永远返回“false”
- 自定义类重写equals()方法示例:
class consumer {
String name;
int age;
@Override
public boolean equals(Object o) { //对象的多态性
//如果两个引用类型变量保存的地址值相同,即指向同一个对象,那么相应的属性值一定相同
if (this == o) {
return true;
}
//如果形参指向的对象类型不是consumer类型,则返回false,即x.equals(和x不同类型的对象)永远返回false
if (!(o instanceof consumer)) {
return false;
}
//在多态情况下,形参o(引用类型变量)的类型是Object类的,但指向的对象是consumer类型的,
//虽然对象内存空间中加载了子类consumer特有的属性和方法,但由于引用类型变量声明为父类Object类型,
//导致编译时只能调用Object类的属性和方法,而不能调用子类consumer特有的属性和方法
//向下转型:将引用形参的类型强转为consumer,使得形参o在编译时可以调用consumer类特有的属性和方法
consumer consumer = (consumer) o;
//比较int类型的属性age的值是否相同,比较String类型的属性name的实际内容是否相同
return age == consumer.age &&
name.equals(consumer.name); //这里调用的equals方法是String类中重写的equals方法
}
}
3.3 面试题:==和equals()的区别
- == 既可以比较基本数据类型变量也可以比较引用数据类型变量。对于基本数据类型变量就是比较值,对于引用数据类型变量就是比较地址值
- equals()是java.lang.Object类中声明的方法,如果该方法没有被重写过默认和==相同,用于比较引用数据类型变量的地址值;而在String等类中的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals方法是比较值的错误观点
- 具体要看自定义类里有没有重写Object的equals方法来判断
- 通常情况下,重写equals方法,会比较类中的相应属性是否都相等
3.4 equals()重写练习
编写Order类,有int型的orderId, String型的orderName,相应的getter()和setter()方法,两个参数的构造器,重写父类的equals()方法:public boolean equals(Object obj), 并判断测试类中创建的两个对象是否相等。
public class Day13_OrderTest {
public static void main(String[] args) {
Order o1 = new Order(1001, "AA");
Order o2 = new Order(1001, "BB");
Order o3 = new Order(1001, "AA");
System.out.println(o1.equals(o2));
System.out.println(o1.equals(o3));
}
}
class Order {
private int orderId;
private 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 o) {
if (this == o) {
return true;
}
if (!(o instanceof Order)) {
return false;
}
Order order = (Order) o;
return orderId == order.orderId &&
orderName.equals(order.orderName);
}
}
4. toString()方法
4.1 Object类中的toString()方法
- Object类中toString()方法的定义:返回==调用该方法的对象的类名和引用地址==(通过hashcode值计算出该对象在堆空间中的地址值),返回值是String类型
- 在多态情况下(父类的引用指向了子类的对象),父类类型的引用类型变量调用toString方法,返回的是实际指向的子类对象的类名和引用地址。也就是说,实际哪个对象调用toString方法,就返回该对象的类名和引用地址
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
- 当输出一个对象的引用数据类型变量时,实际上就是调用当前对象的toString()方法,System.out.println()自动调用了toString()方法
Date now=new Date();
//二者输出结果相同
System.out.println(now);
System.out.println(now.toString());
- 当进行String与其它引用数据类型变量的连接操作时, 自动调用该引用类型变量指向的对象的toString()方法
//二者输出结果相同
System.out.println(“now=”+now);
System.out.println(“now=”+now.toString());
4.2 String/Date/File/包装类中的toString()方法
- String、Date、File、包装类等都重写了Object类中的toString()方法,重写后的方法不再返回调用该方法的对象的类名和引用地址,而是返回对象的“实际内容”。
- 如String类重写了toString()方法, 返回字符串的值:
s1 = “hello”;
System.out.println(s1); //hello
System.out.println(s1.toString()); //相当于调用了String类重写的toString()方法
- 基本类型数据变量转换为String类型时,调用了对应包装类的toString()方法
int a=10;
System.out.println(“a=”+a); //a=10
4.3 自定义类重写toString()方法
- 自定义类的对象调用toString()方法时,通常希望==返回对象的“实际内容”(如对象的类型、属性值)==。因此,可以对Object类中的toString()方法进行重写
- 若当前类重写了toString()方法,当实例化一个类的对象并声明了其引用类型变量p时,System.out.println§; 也会自动调用toString()方法,即等价于System.out.println(p.toString());
@Override
public String toString() {
return "Order{" + "orderId=" + orderId + ", orderName='" + orderName + '\'' + '}';
}
八、JUnit单元测试
JUnit单元测试步骤:
- 创建Java类,进行单元测试
- 该Java类的要求是:① 权限是public;② 提供无参的公共构造器
- 在改类中声明单元测试方法
- 该单元测试方法的要求是:① 权限是public;② 没有返回值;③ 没有形参
- 在单元测试方法上声明注解 @Test ,并导入JUnit:import org.junit.Test;
- 在方法体内声明测试代码
- 此时右键可以直接运行该单元测试方法
import org.junit.Test;
public class Day13_JUnitTest {
@Test
public void testEquals() {
String s1 = "MM";
String s2 = "MM";
System.out.println(s1.equals(s2));
}
}
九、包装类的使用
1. 包装类(Wrapper)的概念
- 针对8种基本数据类型定义相应的引用类型——包装类(封装类)
- 目的:使基本数据类型变量也具有类的特征,从而可以调用类中的方法
2. 基本数据类型、包装类与String类之间的相互转换
2.1 基本数据类型与包装类之间的转换
- 基本数据类型 --> 包装类:装箱
- 调用以实际值为形参的包装类构造器:Integer t = new Integer(11);
- 调用以字符串为形参的包装类构造器:Float f = new Float(“32.1F”);
- 自动装箱(JDK5.0新特性)
//基本数据类型 --> 包装类:调用包装类的构造器 / 自动装箱
@Test
public void test1() {
//***********************调用包装类的构造器
int num1 = 10;
Integer in1 = new Integer(num1); //Integer包装类的构造器,形参为int类型的数值
System.out.println(in1.toString()); //10 包装类重写了toString方法,返回对象的"实际内容"
Integer in2 = new Integer("123"); //Integer包装类的另一个构造器,形参为字符串
System.out.println(in2.toString()); //123
Float f1 = new Float(12.3f);
System.out.println(f1); //相当于省略了toString
Float f2 = new Float("12.3");
System.out.println(f2);
Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean("true");
Boolean b3 = new Boolean("true123");
//false 在形参为字符串的情况下,只要传入的值的不是"true"(忽略大小写),就默认为false
System.out.println(b3);
//***********************自动装箱
//正常来说,num1是int类型的变量,不能作为method方法的形参。需要对其进行手动装箱才能传入method方法
method(new Integer(num1));
//但是当把num1直接作为形参传入method方法时,也能编译成功,这是因为Java对其进行了自动装箱
method(num1); //编译成功 相当于 Object obj = new Integer(num1)
int num2 = 10;
Integer in3 = num2; //自动装箱
boolean b4 = true;
Boolean b5 = b4;
}
public void method(Object obj) {
System.out.println(obj);
}
- 包装类 --> 基本数据类型:拆箱
- 调用包装类的方法:xxxValue()
- 自动拆箱(JDK5.0新特性)
//包装类 --> 基本数据类型:调用包装类的方法xxxValue() / 自动拆箱
@Test
public void test2() {
Integer in1 = new Integer(12);
int i1 = in1.intValue();
System.out.println(i1 + 1);
Float fl1 = new Float(12.3);
float f1 = fl1.floatValue();
System.out.println(f1 + 1);
//***********************自动拆箱
//直接将一个Integer对象的引用in1赋给int型变量num1,相当于int num1 = in1.intValue();
int num1 = in1;
}
2.2 (基本数据类型、包装类)与String类之间的转换
- 基本数据类型、包装类 --> String类型
- 字符串的连接运算:+ “”,基本数据类型可以直接和字符串做连接运算;包装类的对象通过自动拆箱也可以直接和字符串做连接运算
- 调用String类重载的valueOf()方法,形参列表可以是基本数据类型,也可以是包装类的对象
- 调用包装类重写的toString()方法
//基本数据类型、包装类 --> String类型:调用String类重载的方法valueOf(Xxx xxx)
@Test
public void test3() {
int num1 = 10;
//方式1:字符串的连接运算
String str1 = num1 + "";
String str2 = new Integer(num1) + ""; //自动拆箱
System.out.println(str2);
//方式2:调用String类重载的valueOf()方法,形参列表可以是基本数据类型,也可以是包装类对象的引用
float f1 = 12.3f;
String str3 = String.valueOf(f1);
System.out.println(str3); //"12.3"
Double d1 = new Double(12.4);
String str4 = String.valueOf(d1);
//方式3:调用包装类重写的toString()方法
String str5 = new Integer(num1).toString();
System.out.println(str5);
}
- String类型 --> 基本数据类型、包装类
- 调用包装类的parseXxx(String s)方法,返回值为基本数据类型
//String类型 --> 基本数据类型、包装类:调用包装类的parseXxx(String s)方法
@Test
public void test4() {
String str1 = "123";
int num1 = Integer.parseInt(str1);
System.out.println(num1 + 1);
String str2 = "true";
boolean b1 = Boolean.parseBoolean(str2);
System.out.println(b1); //true
String str3 = "true1";
boolean b2 = Boolean.parseBoolean(str3);
System.out.println(b2); //false
}
2. 包装类例题
例1
- 如下两个题目输出结果相同吗?各是什么:
- 考察到的知识点:
- 三元运算符存在自动类型提升:表达式1和表达式2统一为double类型,所以最终的输出结果才是1.0而不是1
- 对象的多态性:Object o1 = new Integer(1);
- System.out.println()会自动调用toString()方法
- 虚拟方法调用:编译时调用的是父类Object的toString()方法,实际执行的是子类Integer重写的toString()方法
- 包装类重写的toString方法,返回对象的"实际内容"
Object o1 = true ? new Integer(1) : new Double(2.0);
System.out.println(o1); //1.0 三元运算符存在自动类型提升!!!
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2); //1 省略了toString
例2
public void method() {
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j); //false 比较的是两个引用类型变量的地址值
Integer m = 1; //自动装箱,范围在[-128,127]内,实际上没有new新的对象
Integer n = 1; //自动装箱,范围在[-128,127]内,实际上没有new新的对象
//在Integer类的源码中定义了IntegerCache的结构,其中定义了一个Integer类型的数组,
//默认缓存了-128~127的整数,如果使用自动装箱的方式,将[-128,127]范围的值转换为Integer类型的对象时
//会直接从IntegerCache中获取,不用再去new新的对象,所以m和n实际指向了同一个对象实体
System.out.println(m == n); //true
Integer x = 128; //相当于new了一个Integer对象
Integer y = 128; //相当于new了一个Integer对象
System.out.println(x == y); //false
}
例3
利用Vector代替数组处理:从键盘读入学生成绩(以负数代表输入结束),找出最高分,并输出学生成绩等级。
- 提示:数组一旦创建,长度就固定不变,所以在创建数组前就需要知道它的长度。而向量类java.util.Vector可以根据需要动态伸缩。
- 创建Vector对象: Vector v=new Vector();
- 给向量添加元素: v.addElement(Object obj); //obj必须是对象
- 取出向量中的元素: Object obj=v.elementAt(0);
- 注意第一个元素的下标是0,返回值是Object类型的。
- 计算向量的长度: v.size();
- 若与最高分相差10分内: A等; 20分内: B等;30分内: C等;其它: D等
import java.util.Scanner;
import java.util.Vector;
public class Day13_ScoreTest {
public static void main(String[] args) {
//1. 实例化Scanner,用于从键盘获取学生成绩
Scanner scan = new Scanner(System.in);
//2. 创建Vector对象
Vector v = new Vector();
//3.通过for(;;)或while(true)的方式,给Vector中添加数据
int maxScore = 0;
for (;;) {
System.out.print("请输入学生成绩:");
int score = scan.nextInt();
if (score < 0) {
break;
}
if (score > 100) {
System.out.println("输入的数据非法,请重新输入!");
continue;
}
//v.addElement(new Integer(score)); //手动装箱
v.addElement(score); //自动装箱
//4.获取学生成绩最大值
if (maxScore < score) {
maxScore = score;
}
}
//5.遍历Vector中的元素,得到每个学生的成绩,并与最大成绩比较,得到每个学生的等级
for (int i = 0; i < v.size(); i++) {
char level;
Object obj=v.elementAt(i);
//Integer inScore = (Integer)obj;
//int score = inScore.intValue(); //手动拆箱
int score = (Integer)obj; //自动拆箱
if (maxScore - score <=10) {
level = 'A';
} else if (maxScore - score <= 20) {
level = 'B';
} else if (maxScore - score <= 30) {
level = 'C';
} else {
level = 'D';
}
System.out.println("Student-" + i + ", score is" + score + ", grade is" + level);
}
}
}
//3.通过for(;;)或while(true)的方式,给Vector中添加数据
int maxScore = 0;
for (;;) {
System.out.print("请输入学生成绩:");
int score = scan.nextInt();
if (score < 0) {
break;
}
if (score > 100) {
System.out.println("输入的数据非法,请重新输入!");
continue;
}
//v.addElement(new Integer(score)); //手动装箱
v.addElement(score); //自动装箱
//4.获取学生成绩最大值
if (maxScore < score) {
maxScore = score;
}
}
//5.遍历Vector中的元素,得到每个学生的成绩,并与最大成绩比较,得到每个学生的等级
for (int i = 0; i < v.size(); i++) {
char level;
Object obj=v.elementAt(i);
//Integer inScore = (Integer)obj;
//int score = inScore.intValue(); //手动拆箱
int score = (Integer)obj; //自动拆箱
if (maxScore - score <=10) {
level = 'A';
} else if (maxScore - score <= 20) {
level = 'B';
} else if (maxScore - score <= 30) {
level = 'C';
} else {
level = 'D';
}
System.out.println("Student-" + i + ", score is" + score + ", grade is" + level);
}
}
}