面向对象编程的三大特征:封装、集成、多态
一、面向对象编程
1.理解对象的含义
1)创建一个类:
//定义一个类,用于理解对象
public class Test {
//定义类的变量
String name;
int age;
}
2)创建一个对象使用Test类
public class OopIntroduce {
public static void main(String[] args) {
//创建对象
Test t1 = new Test();
//通过t1对象使用类的变量并赋值
t1.name = "张三";
t1.age = 18;
System.out.println("姓名:" + t1.name + ",年龄:" + t1.age);
//输出结果:姓名:张三,年龄:18
//创建对象2
Test t2 = new Test();
//通过t2对象使用类的变量并赋值
t2.name = "李四";
t2.age = 19;
System.out.println("姓名:" + t2.name + ",年龄:" + t2.age);
//输出结果:姓名:李四,年龄:19
}
}
3)对类进行优化:将打印功能定义在类中,使用时直接调用避免重复代码
//定义一个类,用于理解对象
public class Test {
//定义类的变量
String name;
int age;
//定义方法用于打印类的变量
public void print() {
System.out.println("姓名:" + name + ",年龄:" + age);
}
}
4)调用打印功能输出信息
public class OopIntroduce {
public static void main(String[] args) {
//创建对象
Test t1 = new Test();
//通过t1对象使用类的变量并赋值
t1.name = "张三";
t1.age = 18;
//调用类的方法
t1.print();
//输出结果:姓名:张三,年龄:18
//创建对象2
Test t2 = new Test();
//通过t2对象使用类的变量并赋值
t2.name = "李四";
t2.age = 19;
//调用类方法
t2.print();
//输出结果:姓名:李四,年龄:19
}
}
**总结:
①类可以看做是对象的设计图或模板,需要先设计好再通过创建对象来使用
②通过new关键字,每new一次类就会得到一个新的对象
③对象就是一种特殊的数据结构
**
2.构造器
1)创建一个类,用于理解构造器
//定义类理解构造器
public class constructor {
String name;
int age;
//定义无参构造器
public constructor() {
System.out.println("无参构造方法");
}
//定义有参构造器
public constructor(String n, int a) {
name = n;
age = a;
}
}
2)测试constructor类,理解构造器的用法
public class Test1 {
public static void main(String[] args) {
// 创建对象
constructor c = new constructor(); //创建对象的同时调用无参构造方法,打印输出:无参构造方法
c.name = "张三";
c.age = 18;
System.out.println("姓名:" + c.name + ",年龄:" + c.age);
//打印结果:无参构造方法
//打印结果:姓名:张三,年龄:18
//创建对象2,理解有参构造方法
constructor c1 = new constructor("李四", 22);
System.out.println("姓名:" + c1.name + ",年龄:" + c1.age);
//打印结果:姓名:李四,年龄:22
}
}
总结:
①创建一个对象时,就会自动调用类的无参构造器(类中未定义的情况下,无参构造器默认存在)
②有参构造器的作用:方便创建对象时直接对变量赋值(完成对象的初始化)
③如果定义了有参构造器,那么无参构造器默认消失,若还想使用,需手动创建一个无参构造器
④可以存在多个名称相同的构造器,通过参数控制差异(方法重载)
⑤创建对象时可通过参数的使用来控制想使用的构造器
⑥构造器名字需与类名一致,且构造器名称前面不需要写返回值类型(public 构造器名称)
3.this关键字
作用:解决变量名冲突
1)示例:创建一个类,理解当类中的成员变量与方法内部变量一致时的冲突
public class constructor {
//定义类的成员变量
String name;
//定义方法并定义方法内部参数name接收数据
public void print(String name) {
System.out.println(name + "的朋友是:" + name);
}
}
2)创建对象使用类中功能并打印结果
public class Test1 {
public static void main(String[] args) {
// 创建对象
constructor c = new constructor();
// 给类中的成员变量name赋值
c.name = "张三";
// 调用方法,同时传数据"李四"给方法的内部参数name
c.print("李四");
//打印结果:李四的朋友是:李四
}
}
3)解决办法:在类的方法中使用this关键字,打印结果即可正常显示(指定name为类的成员变量并非方法内部参数)
public class constructor {
//定义类的成员变量
String name;
//定义方法并定义方法内部参数name接收数据
public void print(String name) {
//使用this关键字指定类的成员变量name与方法参数name进行区分
System.out.println(this.name + "的朋友是:" + name);
}
}
总结:
①this就是一个变量,用在方法中表示拿到当前对象,哪个对象调用方法即指向哪个对象,用于指的对象中的成员变量
②用于解决类的成员变量名称与方法内部变量名称一致时的冲突
4.封装
面对对象的三大特征之一(封装、继承、多态)
封装的设计要求:合理隐藏、合理暴露(比如汽车的发动机、变速箱即被合理隐藏;刹车、油门、方向盘即被合理暴露)
1)封装示例:
public class EncapsulationTest {
String name;
//1、隐藏成员变量:使用private修饰即可,变量只能在当前类中访问,无法在其他类中访问
private int age;
//2、提供赋值成员变量age的set方法,并要求赋值的数据必须大于0
public void setAge(int age) {
if (age > 0) {
this.age = age;
}else {
System.out.println("年龄必须大于0");
}
}
//3、提供获取成员变量age的get方法
public int getAge() {
return age;
}
}
2)使用
public class Test {
public static void main(String[] args) {
EncapsulationTest et = new EncapsulationTest();
//调用set方法对成员变量在合理范围内赋值
et.setAge(18);
//调用get方法获取成员变量
System.out.println(et.getAge());
//打印结果:18
}
}
总结:
①封装就是为实现某功能时将要处理的数据以及处理数据的方法设计到一个类中
②设计要求:合理暴露、合理隐藏
③公开(在任意类中访问)类中的成员使用public进行修饰,隐藏(只能本类中访问)类中的成员使用private进行修饰
5.实体类(特殊类)
1)创建一个类(只负责数据的存储),类中的成员变量全部私有,并提供public修饰的set/get方法,且必须提供一个无参构造器
//定义实体类(特殊类),理解其用法与作用
public class SpecialClass {
//定义隐藏的属性
private String name;
private int age;
//定义无参构造方法
public SpecialClass() {
}
//定义有参构造方法
public SpecialClass(String name, int age) {
this.name = name;
this.age = age;
}
//定义set和get方法
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;
}
}
2)创建一个类,专门负责业务数据的处理
// 定义公共类TestOperate用于处理对象SpecialClass的属性和业务
public class TestOperate {
// 声明私有成员变量sc,类型为SpecialClass(用于存储外部传入的对象)
private SpecialClass sc;
// 定义有参构造函数,接收SpecialClass类型参数(接收new SpecialClass类之后得到的对象名称)
public TestOperate(SpecialClass sc)
{
// 将传入的sc参数赋值(new创建的对象名)给当前对象的成员变量sc(实现依赖注入)
this.sc = sc;
}
// 定义公共方法operate(无返回值)
public void operate()
{
// 调用sc对象的getName()和getAge()方法,拼接并打印信息
System.out.println("姓名:" + sc.getName() + ",年龄:" + sc.getAge());
}
}
3)直接使用SpecialClass类和通过TestOperate类处理打印功能的区别
public class Test {
public static void main(String[] args) {
// 创建对象s1
SpecialClass s1 = new SpecialClass();
// 使用set方法给对象的属性赋值
s1.setName("张三");
s1.setAge(18);
// 使用get方法获取属性值并打印
System.out.println(s1.getName() + " " + s1.getAge());
System.out.println("--------------------------------");
// 创建对象s2并通过有参数构造方法给对象属性赋值
SpecialClass s2 = new SpecialClass("李四", 23);
// 使用get方法获取属性值并打印
System.out.println(s2.getName() + " " + s2.getAge());
System.out.println("--------------------------------");
// 创建TestOperate类的对象to并通过有参构造方法给传入SpecialClass类的对象名
TestOperate to = new TestOperate(s2);
// 调用operate方法打印有参构造方法传入SpecialClass类的对象名包含的属性值
to.operate();
}
}
总结:
①实体类成员变量必须私有,且需要提供public修饰的set和get方法来访问,且必须定义一个无参构造器(操作规范:虽然在没有定义有参构造器的情况下,编译器会自动生成默认的无参构造器,但一些框架通过反射创建对象时或反序列化JSON为对象时同样需要无参构造器,此时无论是否存在有参构造器,都应显式定义无参构造器)
②实体类仅仅是一个保存数据的Java类,可以用来创建对象存储某个事物的数据
③一般使用中需要将存储数据与处理业务数据功能相分离,实现解耦使系统更灵活
6.static静态变量
1)通过static关键字定义静态变量
public class StaticDemo {
// 定义静态成员变量
static String name;
// 定义普通成员变量
int age;
}
2)理解静态变量
public class Test {
public static void main(String[] args) {
// 静态成员变量可以直接通过类名访问(普通成员变量则需要先创建对象才能访问)
StaticDemo.name = "张三";
System.out.println(StaticDemo.name);//输出结果:张三
StaticDemo sd = new StaticDemo();
sd.name = "李四"; //通过对象访问静态成员变量
System.out.println(StaticDemo.name); //输出结果:李四
}
}
3)静态变量应用场景示例:统计类被创建了多少个对象
定义类:
public class StaticDemo2 {
//定义静态变量count
public static int count = 0;
//定义无参构造器,实现每次创建对象,count加1
public StaticDemo2()
{
count++;
}
}
使用类并计数:
public class Test2 {
public static void main(String[] args) {
//创建3次对象
StaticDemo2 sd1 = new StaticDemo2();
StaticDemo2 sd2 = new StaticDemo2();
StaticDemo2 sd3 = new StaticDemo2();
//输出静态变量count的值
System.out.println(StaticDemo2.count); //结果:3
}
}
总结:
①类中的成员变量分为两种:无static修饰的变量为实例变量(对象的变量),有static修饰的变量为静态变量(类变量)
②静态变量属于类,与类一起加载一次,在内存中只有一份,会被所有对象共享
③推荐使用方式:类名.静态变量名 与 实例变量(属于对象,每个对象中都有一份)的调用方式区分
④在当前类中访问类自己的静态变量,直接通过变量名使用即可,不需要加类名.的方式访问
7.static静态方法
1)定义静态方法与实例方法
public class StaticDemo3 {
private double a;
//定义静态方法
public static void print() {
System.out.println("hello world");
}
//定义实例方法
public void show() {
System.out.println( a < 60 ? "不及格" : "及格");
}
public void setA(double a) {
this.a = a;
}
}
2)理解区别
public class Test3 {
public static void main(String[] args) {
//通过类名直接调用静态方法
StaticDemo3.print(); //打印结果:hello world
//创建对象
StaticDemo3 sd = new StaticDemo3();
//调用实例方法
sd.setA(55);
sd.show(); //打印结果:不及格
}
}
总结:
①如果定义方法只是为了做一个功能切不需要访问对象的变量数据,可直接使用static修饰,定义为静态方法(例如打印固定信息)
②如果定义的方法是对象的行为,需要访问对象的数据,则定义为不加static关键字的实例方法
③静态方法访问方式:类名.静态方法名,也可以用对象名.实例方法名访问(不推荐,属于多此一举,第⑦条可进行限制)
④在同一个类中调用静态方法可直接通过方法名访问,不用再写类名.静态方法名
⑤静态方法常见应用场景:做工具类(工作类中都是一些静态方法,每个方法用来完成一个功能,提高代码复用),比如一个系统登录和注册都会涉及到填写验证码的情况就可以定义验证码功能的静态方法放入工具类进行复用
⑥为什么工具类要用静态方法而不用实例方法:实例方法需要创建对象才能调用,只是为了调用方法而创建对象会提升内存占用,静态方法直接使用类名调用,可节省内存
⑦工具类没有创建对象的需求,可直接将构造器使用private修饰变为私有(private classname() {}),避免存在创建对象使用静态方法的情况
注意事项:
①静态方法中可以直接访问静态成员变量,不可直接访问实例成员变量(原因:访问实例成员变量涉及访问对象的变量数据,参考上方总结①,但可间接访问:新建对象,再用对象名.实例变量名)
②实例方法中既可以访问静态成员变量,也可以访问实例成员变量(案例:比如6.3的案例中无参构造器对创建对象的数量count++(静态变量)进行记录)
③实例方法中可以出现this关键字,静态方法不可以出现this关键字(原因:this代表的是对象名,静态方法调用无需创建对象,参考上方总结③⑦)
8.实践案例
需求:实现打印全部电影信息和根据编码查询电影信息功能
1)创建电影信息实体类,并定义有参构造器方便创建电影(对象)信息数据时赋值
//定义电影信息类
public class FilmInfo {
//定义实例属性
private int num = 0; //影片编号
private String name = ""; //影片名称
private double price = 0; //影片价格
//存在有参构造器需定义个无参构造器
public FilmInfo() {}
//定义有参构造器
public FilmInfo(int num, String name, double price) {
this.num = num;
this.name = name;
this.price = price;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
2)创建打印电影信息和查询电影信息功能的类
import java.util.Scanner;
public class FilmOperate {
// 保存电影信息
public FilmInfo[] films;
//接收输入的数组,并保存在成员变量中
public FilmOperate(FilmInfo[] films) {
this.films = films;
}
//打印电影信息
public void operate() {
//遍历电影对象数组
for (int i = 0; i < films.length; i++) {
System.out.println(films[i].getNum() + " " + films[i].getName() + " " + films[i].getPrice());
}
}
//根据输入的编号查询电影信息
public void queryFilm() {
System.out.println("请输入编号:");
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
//遍历电影对象数组
for (int i = 0; i < films.length; i++) {
//判断编号是否相等
if (films[i].getNum() == num) {
System.out.println(films[i].getNum() + " " + films[i].getName() + " " + films[i].getPrice());
return;
}
}
System.out.println("没有此编号的影片");
}
}
3)创建电影信息和对象后使用打印电影信息和查询电影信息功能
public class Test {
public static void main(String[] args) {
//使用数组创建6个filmInfo对象
FilmInfo[] fl = new FilmInfo[6];
//调用有参构造方法为6个数组元素(对象)赋值
fl[0] = new FilmInfo(1, "唐顿庄园", 9.5);
fl[1] = new FilmInfo(2, "速度与激情", 9.2);
fl[2] = new FilmInfo(3, "勇敢的心", 9.1);
fl[3] = new FilmInfo(4, "猫鼠游戏", 9.7);
fl[4] = new FilmInfo(5, "飞驰人生", 9.4);
fl[5] = new FilmInfo(6, "唐人街探案", 9.3);
//调用电影操作类实现打印电影信息和根据编号查询电影信息功能
FilmOperate to = new FilmOperate(fl);
to.operate();
to.queryFilm();
}
}
二、继承
1.理解继承的含义
需求:程序需要管理学校人员信息,老师需要保存的信息有:姓名、年龄、性别、学科,学生需要保持的信息有:姓名、年龄、性别、身高、体重。由于姓名、年龄、性别存在重合,若是在老师类和学生类中分别定义会导致重复代码,增加冗余。继承的解决思路:将存在重合的变量(姓名、年龄、性别)单独定义一个父类,再由子类(老师类、学生类)直接继承使用。
1)将重合变量定义在peopleinfo类进行封装
public class PeopleInfo {
private String name;
private int age;
private char sex;
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 char getSex() {
return sex;
}
public void setSex(char sex) {
this.sex = sex;
}
}
2)创建老师对象继承peopleinfo类且只需定义学科变量
// 使用extends关键字表示子类teacher继承父类peopleinfo
public class Teacher extends PeopleInfo{
private String subject;
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
}
3)创建学生对象继承peopleinfo类且只需定义身高、体重变量
//使用extends关键字表示子类student继承父类peopleinfo
public class Student extends PeopleInfo{
private double height;
private double weight;
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
}
4)使用继承后的子类对变量进行赋值与打印
public class Test {
public static void main(String[] args) {
// 创建Teacher对象
Teacher t = new Teacher();
// 对父类PeopleInfo的变量进行赋值
t.setName("张三");
t.setAge(28);
t.setSex('男');
// 对子类Teacher的变量进行赋值
t.setSubject("math");
System.out.println(t.getName()+t.getAge()+t.getSex()+t.getSubject());//张三28男math
// 创建Student对象
Student s = new Student();
// 对父类PeopleInfo的变量进行赋值
s.setName("李四");
s.setAge(15);
s.setSex('女');
// 对子类Student的变量进行赋值
s.setHeight(170.5);
s.setWeight(92.3);
System.out.println(s.getName()+s.getAge()+s.getSex()+s.getHeight()+s.getWeight());//李四15女170.5 92.3
}
}
总结:
①当多个不同的类需要使用到相同属性的变量时,可将相同的属性定义在父类中通过子类(多个不同的类)继承父类来使用,提高代码复用性,减少冗余
②继承语法:public class A extends B ,A为当前创建的子类,B为父类
③子类只能继承父类的公有变量和方法(比如private私有的变量需要使用public set/get后使用)
④通过new 子类创建的对象实际是又子类与父类共同完成
2.修饰符权限
作用:限制类中成员(变量、方法、构造器)能够被访问的范围,一般情况下常用private与public,缺省与protected遇到时知道含义(访问范围)即可
示例:
public class Father {
// private表示私有方法,只能在本类中访问
private void fatherMethod()
{
System.out.println("fatherMethod");
}
// 缺省方法,本类和同一个包中的类都可以访问
void method()
{
System.out.println("method");
}
// protected方法,本类、同一个包中的类、子类(跨包继承)都可以访问
protected void protectedMethod()
{
System.out.println("protectedMethod");
}
// public方法,任何类都可以访问
public void publicMethod()
{
System.out.println("publicMethod");
}
}
3.继承的特点
1)单继承:一个类只能继承一个父类,无法同时继承两个父类,但支持多层继承的,示例:
class A
{
}
class B extends A
{
}
// C无法直接继承A+B,但可以B先继承A再由C继承B
class C extends B
{
}
2)Java中所有的类都是Object类的子类,示例:
A 和 B 中都未定义方法却可以使用hashCode()方法(Object类中的方法)。注意:一个类要么直接继承Object,要么间接继承Object,要么默认继承Object(下图中A为默认继承,B为间接继承)
3)继承的就近原则与super关键字:当子类与父类存在相同成员(变量或方法)时,访问时优先默认访问子类的成员;若想使用父类成员可用super关键字
public class ExtendsTest {
public static void main(String[] args) {
B b = new B();
b.show();
}
}
class A
{
String name = "A的name";
}
class B extends A
{
String name = "B的name";
public void show()
{
String name = "B中方法的name";
System.out.println(name); // 输出B中方法的name而非B类中的name,就近优先原则
System.out.println(this.name); // 输出B的name,this关键字指定使用对象的成员
System.out.println(super.name); // 输出A的name,super关键字指定使用父类的成员
}
}
4.继承方法重写
public class ExtendsTest {
public static void main(String[] args) {
B b = new B();
b.show();
}
}
class A
{
public void show()
{
System.out.println("123");
}
}
class B extends A
{
@Override // 重写方法需使用override注解,可以提高代码的可读性,更规范,也避免错误(使用后会自动检查是否存在错误)
public void show()
{
System.out.println("456");
}
}
总结:
①父类的方法无法满足需求时可在子类中定义相同名称的方法和相同参数进行方法重写
②子类重写父类方法时,访问权限必须大于或等于父类方法的权限,例如父类方法权限为protected,子类重写的方法需大于(public)或等于(protected)父类方法权限
③重写方法的返回值类型必须与被重写方法的返回值类型一致或范围更小
④私有方法(参考2.2中总结③:私有成员无法继承)和静态方法(直接类名.方法名访问)不能被重写
⑤重写方法需添加@Override注解,提高代码可读性,使用注解还能自动检查问题,避免错误(例如检查上述总结中的每条注意事项)
⑥八字规范:“声明不变,重新实现”
5.方法重写的常见场景
1)创建类并封装以及提供构造器
public class ExtendsTest {
private String name;
private int age;
public ExtendsTest() {
}
public ExtendsTest(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 setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
2)打印对象并理解打印结果
public class Test {
public static void main(String[] args) {
ExtendsTest et = new ExtendsTest("张三", 18);
System.out.println(et); //打印结果为:oop.extends3demo.ExtendsTest@1b6d3586
}
}
打印结果为:包名.类名@对象的内存地址,包名:oop.extends3demo,类名:ExtendsTest,对象内存地址:1b6d3586
为什么会打印该结果?因为打印对象et时,默认使用了Object类的toString方法(ExtendsTest默认继承Object类,参考2.3.2),未被省略的写法为:System.out.println(et.toString);
3)查看Object类的toString方法,getClass().getName()输出内容对应上条结果的:oop.extends3demo.ExtendsTest,Integer.toHexString(hashCode())输出内容对应上条结果的:1b6d3586
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
4)在ExtendsTest类中重写toString方法(添加以下内容),使打印对象时可以直接打印对象内容而不是打印:包名.类名@内存地址
public String toString() {
return "name:" + name + " age:" + age;
}
5)再次打印对象即可输出对象内容信息
public class Test {
public static void main(String[] args) {
ExtendsTest et = new ExtendsTest("张三", 18);
System.out.println(et); // name:张三 age:18
}
}
6.子类构造器的特点
1)子类中的构造器都必须先调用父类构造器再执行自己类中的构造器,示例:
public class Test {
public static void main(String[] args) {
B b = new B();
//执行结果:1.父类构造器执行了 2.子类构造器执行了
}
}
class A {
public A()
{
System.out.print("1.父类构造器执行了");
}
}
class B extends A {
public B()
{
// 当前第一行默认带了super()
System.out.print("2.子类构造器执行了");
}
}
2)应用场景:创建对象通过有参构造器初始化成员变量时,需在子类的有参构造方法中使用super关键字先给父类成员变量赋值然后再给子类的成员变量赋值
创建父类:
public class PeopleInfo {
private String name;
private int age;
public PeopleInfo() {
}
public PeopleInfo(String name, int age) {
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;
}
}
创建子类:
public class Teacher extends PeopleInfo {
private String subject;
public Teacher(String subject) {
this.subject = subject;
}
public Teacher(String name, int age, String subject) {
// super常见使用场景:为对象中父类包含的成员变量进行赋值
super(name, age);
this.subject = subject;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
}
初始化并打印结果:
public class Test {
public static void main(String[] args) {
Teacher t = new Teacher("张三", 18, "math");
System.out.println("姓名:" + t.getName() + ",年龄:" + t.getAge() + ",科目:" + t.getSubject());
//输出结果:姓名:张三,年龄:18,科目:math
}
}
7.通过this调用兄弟构造器用法
应用场景示例:初始化个人信息(姓名,年龄,国家)时,姓名、年龄存在差异,但国家都等于中国的情况下,创建不同的有参构造器接收初始化数据
public class ThisDemo {
private String name;
private int age;
private String country;
// 当录入人员的国家都是中国的时候使用该构造器只需要输入姓名和年龄即可
public ThisDemo(String name, int age) {
// this(...)调用兄弟构造器,将接收到的name和age还有固定的country值传给兄弟构造器完成初始化数据
this(name, age, "中国");
}
// 上个有参构造器的兄弟构造器
public ThisDemo(String name, int age, String country) {
this.name = name;
this.age = age;
this.country = country;
}
}
总结:
①this(…)的作用:在构造器中调用本类其他构造器
②通过有参构造器初始化成员变量时,当某个成员的值是固定不变时,可以定义不同的有参构造器然后使用this(…)调用兄弟构造器实现初始化,减少在创建对象初始化变量时的填写内容,节省时间
③在构造器中使用super(…)或this(…)必须写在第一行,且两者不能同时出现在一个构造器中,否则会报错。原因:当使用this(…)调兄弟构造器时,兄弟构造器第一行默认存在super(…),所以使用this(…)后无需再使用super(…),否则会导致父类构造器重复调用;同理,若使用this(…)时不在第一行,可能会导致先调用父类构造器,然后this(…)指向的兄弟构造器又再次调用父类构造器,导致父类构造器重复调用
三、多态
1.理解多态
1)创建父类-动物
public class Animal {
String name = "动物";
public void move()
{
System.out.println("动物可以移动");
}
}
2)创建子类-鸟
public class Bird extends Animal{
String name = "鸟儿";
//重写方法:体现鸟儿可以通过飞的形式移动
@Override
public void move(){
System.out.println("鸟儿可以飞");
}
}
3)创建子类-狗
public class Dog extends Animal{
String name = "狗";
//重写方法:体现狗可以通过跑的形式移动
@Override
public void move()
{
System.out.println("狗可以跑");
}
}
4)理解多态(对象多态和行为多态)
public class Test {
public static void main(String[] args) {
//对象多态,父类对象引用子类对象
Animal a1 = new Dog();
//行为多态:动物可以移动的行为重写为狗可以通过跑移动
a1.move(); //打印结果:狗可以跑。方法:编译看左边(Animal)运行看右边(Dog)
System.out.println(a1.name); //打印结果:动物。变量:编译运行都看左边(Animal)
//对象多态,父类对象引用子类对象
Animal a2 = new Bird();
//行为多态:动物可以移动的行为重写为鸟儿可以通过飞移动
a2.move(); //打印结果:鸟儿可以飞。方法:编译看左边(Animal)运行看右边(Brid)
System.out.println(a2.name); //打印结果:动物。变量:编译运行都看左边(Animal)
}
}
总结:
①多态是继承情况下一种现象,表现为:对象多态、行为多态。
②多态的前提:有继承关系 、存在父类引用子类对象(对象多态) 、存在方法重写(行为多态)
③多态是对象、行为的多态,成员变量不涉及多态
④行为多态的理解:编译看左边(父类),运行看右边(子类)
2.多态的好处
1.多态形式下,右边对象是解耦合的,更利于拓展和维护。例如左边对象是汽车,右边对象是轮胎,当轮胎爆胎时,可以直接更换其他轮胎,轮胎对于汽车就是解耦合的。
2.定义方法时,使用父类的类型作为入参,可以接收一切子类对象,扩展性更强更便利
public class Test {
public static void main(String[] args) {
//对象多态,父类对象引用子类对象
Animal a1 = new Dog();
Animal a2 = new Bird();
print(a1); //打印结果:狗可以跑
print(a2); //打印结果:鸟儿可以飞
//不使用多态创建对象
Dog d1 = new Dog();
print(d1); //打印结果:狗可以跑,即使创建对象未使用多态,也可以将子类对象作为参数传递给父类类型作为参数的方法
}
//多态的好处:父类类型可作为参数接收子类对象
public static void print(Animal a)
{
a.move();
}
}
注意事项:多态下不能使用子类独有功能,列如Dog子类中创建eat方法,但父类不包含该方法,则多态下无法使用eat方法(存在方法未被重写的情况)
3.多态下的类型转换
默认使用第一种:自动类型转换,强制类型转换使用场景:解决上条注意事项(多态下不能使用子类独有功能)的问题
1)分别在Dog和Brid类新增eat方法,且不在父类中新增,实现独有功能
public class Dog extends Animal{
String name = "狗";
@Override
public void move()
{
System.out.println("狗可以跑");
}
//新增独有功能eat
public void eat()
{
System.out.println("狗吃骨头");
}
}
public class Bird extends Animal{
String name = "鸟儿";
@Override
public void move(){
System.out.println("鸟儿可以飞");
}
//新增独有功能eat
public void eat(){
System.out.println("鸟儿吃小虫");
}
}
2)通过强制类型转换在多态下使用子类的独有功能
public class Test {
public static void main(String[] args) {
//对象多态,父类对象引用子类对象
Animal a1 = new Dog();
//强制类型转换:父类对象转换成子类对象从而实现多态下使用子类的独有功能eat()
Dog d1 = (Dog)a1;
d1.eat(); //打印结果:狗吃骨头
}
}
3)强制类型转换异常的情况:强转时即使对象类型不同,但只要存在继承情况则不会报错,但运行时则会报错:java.lang.ClassCastException(强制类型转换异常)
public class Test {
public static void main(String[] args) {
//对象多态,父类对象引用子类对象
Animal a1 = new Dog();
//强制类型转换:将狗类型(a1)强转为鸟类型
Bird b1 = (Bird) a1; //运行会报错:java.lang.ClassCastException
}
}
4)Java推进强制类型转换使用instanceof判断
public class Test {
public static void main(String[] args) {
//对象多态,父类对象引用子类对象
Animal a1 = new Dog();
//创建对象
Bird b1 = new Bird();
//当b1的真实类型是Bird时,在多态下调用Bird的eat方法
print(b1); //打印结果:鸟儿吃小虫
print(a1); //打印结果:狗吃骨头
}
public static void print(Animal a)
{
//使用instanceof判断类型一致再进行转换
//如果入参对象真实类型是Dog,则进行转换并使用独立功能eat
if(a instanceof Dog){
Dog d = (Dog)a;
d.eat();
//如果入参对象真实类型是Bird,则进行转换并使用独立功能eat
} else if (a instanceof Bird) {
Bird b = (Bird)a;
b.eat();
}
}
}
总结:
①多态下的类型转换分为:自动类型转换(默认)、强制类型转换
②强制类型转换为了解决多态下无法调用子类独有功能的问题
③存在继承关机即可进行强制类型转换,编译阶段不会报错,但运行时如果发现对象的真实类型与强转后的类型不同(例如将狗转成鸟)会报错java.lang.ClassCastException
④可使用instanceof判断当前对象的真实类型是否正确,否则不进入强转环节
四、案例
需求:实现加油支付小程序
①可以存储加油卡信息(卡号、车主姓名、余额)
②充值余额满5000为金卡,支付打8折,且满200赠送洗车服务,充值满2000为银卡,支付打9折
1)创建父类存储卡信息
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//使用lombok定义set/get方法
@Data
//使用lombok定义无参构造方法
@NoArgsConstructor
//使用lombok定义有参构造方法
@AllArgsConstructor
public class CardInfo {
//定义加油卡的属性
private String cardId;
private String cardName;
private double money;
//定义加油卡的充值方法
public void saveMoney(double money) {
this.money += money;
}
//定义加油卡的消费方法
public void takeMoney(double money) {
this.money -= money;
}
}
2)创建金卡子类并重写支付方法(打8折)
public class GoldCard extends CardInfo{
//将初始化数据传递给父类
public GoldCard(String cardId, String cardName, double money) {
super(cardId, cardName, money);
}
//金卡消费涉及打折,需重写消费方法
@Override
public void takeMoney(double money) {
//判断余额是否充足
if (getMoney() < money * 0.8){
System.out.println("余额不足!");
return;
}
System.out.println("本次消费金额:" + money);
System.out.println("打折后金额:" + money * 0.8);
//更新卡余额
setMoney(getMoney() - money * 0.8);
System.out.println("消费成功!余额为:" + getMoney());
//判断是否消费满200赠送洗车小票
if (money * 0.8 >= 200){
printTicket();
}else {
System.out.println("本次消费未达到200,不能免费洗车");
}
}
//打印洗车小票
public void printTicket(){
System.out.println("消费金额满200,赠送洗车小票一张");
}
}
3)创建银卡子类并重写支付方法(打9折)
public class SilverCard extends CardInfo{
//将初始化数据传递给父类
public SilverCard(String cardId, String cardName, double money) {
super(cardId, cardName, money);
}
//银卡消费涉及打折,需重写消费方法
@Override
public void takeMoney(double money) {
//判断余额是否充足
if (getMoney() < money * 0.9){
System.out.println("余额不足!");
return;
}
System.out.println("本次消费金额:" + money);
System.out.println("打折后金额:" + money * 0.9);
//更新卡余额
setMoney(getMoney() - money * 0.8);
System.out.println("消费成功!余额为:" + getMoney());
}
}
4)创建金卡和银卡并使用消费功能
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
//创建金卡对象
GoldCard goldCard = new GoldCard("G001", "张三", 5000);
pay(goldCard);
//创建银卡对象
SilverCard silverCard = new SilverCard("S001", "李四", 2000);
pay(silverCard);
}
//定义支付功能,使用多态以父类为数据类型,将子类对象作为参数传入
public static void pay(CardInfo c){
System.out.println("请输入消费金额:");
Scanner sc = new Scanner(System.in);
double money = sc.nextDouble();
c.takeMoney(money);
}
}