面向对象
什么是面向对象编程?
面向:找、拿
对象:东西
面向对象编程:找或拿东西过来编程。
面向对象编程符合人类思维习惯,编程更简单,更好理解。
面向对象学习什么?
对于大多数业务需求来说,是没有现成对象供以使用的,需要学习自己设计对象并使用,前提是需要掌握面向对象的语法。
一、设计对象并使用
在设计对象之前,需要先设计类,再通过类设计对象并使用。
1.1设计类、创建对象并使用
类是什么?
对象是真实存在是具体实例。
类是实物对象设计图,是对象共同特征的描述。
结论:在java中,必须先设计类,才能创建对象并使用。
如何设计类?
类的内容一般由成员变量、成员方法、构造器、代码块、内部类五部分组成。
public class 类名 {
1. 成员变量(代表属性,一般是名词)
2. 成员方法(代表行为,一般是动词)
3. 构造器
4. 代码块
5. 内部类
}
例如对于汽车这一种实物的类:
publicclassCar { // 属性(成员变量)Stringname; doubleprice; // 行为(方法)publicvoidstart() { } publicvoidrun() { } }
如何得到类的对象?
类名 对象名 = new 类名();
如Car c = new Car();
如何使用对象?
访问属性:对象名.成员变量
访问行为:对象名.方法名(...)
示例代码如下:
汽车类
publicclassCar { /*** 成员变量*/Stringname; doubleprice; /*** 方法*/publicvoidstart() { System.out.println(name+"启动了"); } publicvoidrun() { System.out.println("价格是"+price+"的"+name+"正在行驶"); } }
测试类
publicclassTest1 { publicstaticvoidmain(String[] args) { // 创建汽车对象ssCarc=newCar(); Card=newCar(); c.name="奔驰"; // 为c对象的name属性赋值c.price=1000000.0; System.out.println(c.name); System.out.println(c.price); c.start(); // 奔驰启动了 c对象的name属性已经赋值,name为"奔驰"d.start(); // null启动了 d对象的name属性没有赋值,调用d.name属性时name为默认值"null" } }
1.2定义类的几个补充注意事项
1.类名首字母建议大写,且有意义,不能是关键字,满足“驼峰模式”。
2.一个java文件中可以定义多个class类,但只能有一个public类,且public类的类名必须为文件名。实际开发中建议一个文件定义一个class类。
3.成员变量的完整定义格式是:修饰符 数据类型 变量名称( = 初始化值);一般无需指定初始化值,存在默认值,在使用时根据具体对象进行具体赋值。
默认值规则:
byte short char int long 0
double float 0.0
boolean false
String、类、接口、数组等引用类型 null
char默认值为0,但0在ASCⅡ码中没有对应值,因此char型变量默认值无法输出
二、对象在内存中的运行机制
2.1多个对象的内存图
示例代码如下图所示,定义了一个公共的汽车类Car类,然后再Test类中通过Car类实例化了多个Car对象。
其内存执行机制如下:
Step1:首先执行Test类,把.java类文件编译为.class文件,将其中全部内容加载到方法区中。main方法是程序执行的入口,因此首先执行main方法,将main方法提取到栈内存中运行,开始执行main方法中的代码。
Step2:执行main方法第一行代码,第一行代码涉及到Car类,因此将Car类编译为.class文件,将其中全部内容加载到方法区中。
定义一个引用类型的变量c1,由于c1是局部变量,因此会在栈内存中开辟一块区域用以存放c1。
然后在堆内存中开辟一块内存空间,new出一个Car对象,产生该对象的地址,在这块内存空间内又划分为若干个小区域,用于存放Car类的各种属性信息(name、price)和各种成员方法的引用地址,在使用时通过成员方法的引用地址访问加载在方法区中的成员方法。
注:new出来的对象中的成员变量是存放在该对象中的,该对象是存放在堆内存中的,因此new出来的成员变量也是存放在堆内存中的。知识点 0-1
之后再将new出来的Car对象的地址赋值给c1变量。
注:c1变量中存储的实际上是在堆内存中new出来的Car对象的地址。
Step3:执行main方法第二、三行代码,根据存储在c1变量中的堆内存中new出来的Car对象的地址,找到该对象的成员变量name和price,分别将其赋值为”奔驰”和”39.78”。
执行main方法第四、五行代码,根据存储在c1变量中的堆内存中new出来的Car对象的地址,找到该对象的成员变量name和price,并将其变量值分别打印出来。
Step4:执行main方法第六行代码,根据存储在c1变量中的堆内存中new出来的Car对象的地址,找到该对象的成员方法start()的引用地址,再通过该引用地址找到在方法区中的具体方法,并执行。
将start()方法提取到栈内存中执行。由于start()方法是局部变量c1调用的,因此在加载name属性时,获取的是局部变量c1中存储地址指向的new出来的Car对象的name属性值,因此打印值为”奔驰启动了”而不是”null启动了”。
同理,执行main方法第七行代码,打印”价格是:39.78的奔驰跑得快”
Step5:第8-14行代码同第1-7行代码,具体过程不再赘述。
注:Car c1= new Car();与Car c2= new Car();都是存放的在堆内存中new出来的Car对象的地址,不同的是在这个过程中Car对象new了两次,第二次new出来的对象的地址,与第一次的地址不同,二者互相没有任何影响。
2.2两个变量指向同一个对象地址
将存储某个对象地址值的变量赋值给另一变量时,被赋值的变量中存储的数据是该对象的地址值,即两个变量指向同一个对象地址,此时调用二者任意一个变量对该new出来的对象的具体内容做任意改变时,另一个变量在调用时同样会受到影响。
示例代码如下:
学生类
publicclassStudent { Stringname; charsex; Stringhobby; publicvoidstudy() { System.out.println("姓名:"+name+",性别:"+sex+",爱好:"+hobby+"开始学习了"); } }
测试类
publicclassTest { publicstaticvoidmain(String[] args) { // 创建学生对象Studentstu1=newStudent(); stu1.name="小明"; stu1.sex='男'; stu1.hobby="睡觉"; stu1.study(); // 姓名:小明,性别:男,爱好:睡觉开始学习了// 把stu1变量赋值给一个Student类型的变量stu2Studentstu2=stu1; // 将存储在stu1中的new出来的Student对象的地址赋值给stu2System.out.println(stu1); // com.itheima.memory.Student@1540e19dSystem.out.println(stu2); // com.itheima.memory.Student@1540e19d 二者指向同一个Student对象的地址,地址相同stu2.hobby="爱提问"; stu2.study(); // 姓名:小明,性别:男,爱好:爱提问开始学习了 } }
补充知识:垃圾回收
当堆内存中的对象,没有被任何变量引用(指向)时,会被判定为内存中的“垃圾”。
如Student stu = new Student(); // 创建实例化一个Student对象
stu = null; // 将Student类型变量stu中存放的地址值赋为空
此时堆内存中的Student对象没有被任何变量引用(指向),被判定为内存“垃圾”。
Java存在自动垃圾回收器,会定期进行清理。
三、面向对象编程训练:模拟购物车模块
1.需求分析、框架搭建
需求:模拟购物车模块的功能,需要实现添加商品到购物车中去,同时需要提供修改商品的购买数量,结算商品价格功能。
分析:
购物车中的每个商品都是一个对象,需要定义一个商品类。
购物车本身也是一个对象:可以使用数组对象代表它。
完成界面架构,让用户选择操作的功能。
2.添加商品到购物车、查看购物车信息
需求:让用户输入商品信息,并加入到购物车中去,且可立即查看当前购物车信息。
分析:
需要让用户录入商品信息,创建商品对象封装商品信息。
并把商品对象加入到购物车数组中去。
查询购物车信息,就是遍历购物车数组中的每个商品对象。
3.修改购买数量
需求:让用户输入商品id,找出对应的商品修改其购买数量
分析:
定义方法能够根据用户输入的id去购物车数组中查看是否存在该商品对象。
存在返回该商品对象的地址,不存在返回null。
判断返回的对象地址是否存在,存在修改其购买数量,不存在就继续。
4.结算金额
需求:当用户输入了pay命令后,需要展示全部购买的商品信息和总金额。
分析:定义求和变量,遍历购物车数组中的全部商品,累加其单价*购买数量。
示例代码如下:
商品类
publicclassGoods { intid; // 编号,用于唯一确定该商品Stringname; // 商品名称doubleprice; // 商品价格intbuyNumber; // 购买数量}
主调程序
publicclassShopCarTest { publicstaticvoidmain(String[] args) { // 1.定义商品类,用于后期创建商品对象// 2.定义购物车对象 使用一个数组对象表示Goods[] shopCar=newGoods[100]; // Goods类型的数组,里面存放Goods对象,具体值是各个Goods的地址 类本身是一个引用类型// 3.搭建操作架构// 添加死循环,执行完当前命令后可继续选择另一命令,继续执行while (true) { System.out.println("请在如下命令中选择一种执行:"); System.out.println("添加商品到购物车:add"); System.out.println("查询购物车商品:query"); System.out.println("修改购物车商品:update"); System.out.println("结算购买商品的金额:pay"); // 接收输入Scannersc=newScanner(System.in); System.out.println("请选择:"); Stringcommand=sc.next(); switch (command) { case"add": // 添加商品到购物车addGoods(shopCar, sc); break; case"query": // 查询购物车商品queryGoods(shopCar); break; case"update": // 修改购物车商品updateGoods(shopCar, sc); break; case"pay": // 结算购买商品的金额payGoods(shopCar); break; default: // 输入无效命令System.out.println("输入错误!"); } } } /*** create by: 全聚德在逃烤鸭、* description:将商品添加到购物车* create time: 2022/4/4 0004 19:20** @param shopCar* @param sc* @return void*/privatestaticvoidaddGoods(Goods[] shopCar, Scannersc) { // 录入用户输入的购买商品的信息System.out.println("请输入购买商品的编号(不重复)"); intid=sc.nextInt(); System.out.println("请输入购买商品的名称"); Stringname=sc.next(); System.out.println("请输入购买商品的数量"); intbuyNumber=sc.nextInt(); System.out.println("请输入购买商品的价格"); doubleprice=sc.nextDouble(); // 把这些购买商品的信息封装成一个商品对象Goodsgoods=newGoods(); goods.id=id; goods.name=name; goods.buyNumber=buyNumber; goods.price=price; // 把这个商品对象添加到购物车数组中去for (inti=0; i<shopCar.length; i++) { // 判断第i个索引位置是否有内容,若无,则将该Goods对象的地址添加到数组中的第i个索引位置,跳出该循环,否则查找下一个索引if (shopCar[i] ==null) { shopCar[i] =goods; // 将实例化的Goods对象的地址赋值给shopCar数组中的第i个索引break; // 已经找到,无需继续循环 } } System.out.println("商品"+goods.name+"添加成功!"); // 添加成功后给出相应提示queryGoods(shopCar); } /*** create by: 全聚德在逃烤鸭、* description: 查询购物车中的商品对象信息并展示出来* create time: 2022/4/4 0004 20:11** @param shopCar* @return void*/privatestaticvoidqueryGoods(Goods[] shopCar) { System.out.println("***********查询购物车信息如下***********"); System.out.println("商品编号\t\t商品名称\t\t购买数目\t\t商品价格"); // 定义for循环,遍历数组中所有的Goods对象,若不为空,则将此Goods类型变量中存储的地址对应的对象的各种成员属性展示出来for (inti=0; i<shopCar.length; i++) { if (shopCar[i] !=null) { System.out.println(shopCar[i].id+"\t\t"+shopCar[i].name+"\t\t"+shopCar[i].buyNumber+"\t\t"+shopCar[i].price+"\t\t"); } else { break; // 因为购物车中商品添加是从第0个索引顺序添加,所以当第i个索引位置存储的值为null时,说明i及后续索引位置值均为null,无需继续遍历,跳出循环 } } } /*** create by: 全聚德在逃烤鸭、* description: 修改购买数量* create time: 2022/4/4 0004 20:30** @param shopCar* @param sc* @return void*/privatestaticvoidupdateGoods(Goods[] shopCar, Scannersc) { // 定义while循环,修改成功或手动取消后跳出,否则循环执行OUT: while (true) { // 让用户输入要修改商品的idSystem.out.println("请输入要查询的id"); intid=sc.nextInt(); // 调用根据id查询出要修改的商品对象的方法,查询出要修改的商品对象Goodsgoods=getGoodsById(shopCar, id); // 如果Goods类型的goods变量中存储的地址值不为null,则说明找到该商品,进行下一步操作,否则说明没有找到, 给出相应提示并要求用户重新输入if (goods!=null) { System.out.println("请输入"+goods.name+"修改后的商品购买数目:"); intbuyNumber=sc.nextInt(); goods.buyNumber=buyNumber; // 将该商品的购买数目更新为输入的修改后的商品购买数目System.out.println("商品购买数目修改成功!"); // 修改成功后给出相应提示queryGoods(shopCar); // 查询是否修改成功break; // 修改完毕,跳出该while循环 } else { System.out.println("很抱歉,您输入的id有误,请重新输入。"); // 定义while循环,当用户选择在进行请选择继续修改/取消修改输入错误时,重复执行while (true) { System.out.println("请选择继续修改:1\t\t取消修改:2"); intuserChoice=sc.nextInt(); switch (userChoice) { case1: System.out.println("您选择继续修改"); continueOUT; // 用户选择继续修改,跳转到OUT标签标记的while循环处,重新要求用户输入id重新查询case2: System.out.println("您选择取消修改"); return; // 用户选择取消修改,结束当前方法,退回到主调方法default: System.out.println("输入错误,请重新输入"); } } } } } /*** create by: 全聚德在逃烤鸭、* description: 根据id查询出要修改的商品对象* create time: 2022/4/4 0004 20:36** @param shopCar* @param id* @return com.itheima.demo.Goods*/publicstaticGoodsgetGoodsById(Goods[] shopCar, intid) { // 遍历每一个索引,找到数组中地址存储在Goods类型变量中的Goods对象的成员属性id是否与输入的id相同,若相同,返回该Goods对象,否则返回nullfor (inti=0; i<shopCar.length; i++) { // Goods对象若不为空,则继续查看其id是否符合if (shopCar[i] !=null) { // 若id符合,则将该Goods对象返回if (shopCar[i].id==id) { returnshopCar[i]; } } else { break; // 因为购物车中商品添加是从第0个索引顺序添加,所以当第i个索引位置存储的值为null时,说明i及后续索引位置值均为null,无需继续遍历,跳出循环 } } returnnull; // 遍历完所有Goods类型变量存储的地址值不为空的对应的Goods对象,没有符合要求的id,返回null } /*** create by: 全聚德在逃烤鸭、* description: 计算购物车中的商品总金额* create time: 2022/4/4 0004 21:43** @param shopCar* @return void*/privatestaticvoidpayGoods(Goods[] shopCar) { queryGoods(shopCar); // 定义一个求和变量,用于存储累加金额doublemoney=0; // 遍历购物车数组中的全部存储的地址值不为null的Goods类型变量对应的商品对象,累加各个商品对象中的单价*购买数目for (inti=0; i<shopCar.length; i++) { // 若Goods类型的变量中存储的Goods对象的地址值不为空,则将此Goods类型变量中存储的地址对应的对象的商品购买数量属性与商品单价属性相乘,并累加到求和变量中去if (shopCar[i] !=null) { intbuyNumber=shopCar[i].buyNumber; doubleprice=shopCar[i].price; money+= (buyNumber*price); // 累加到求和变量money中 } else { break; // 因为购物车中商品添加是从第0个索引顺序添加,所以当第i个索引位置存储的值为null时,说明i及后续索引位置值均为null,无需继续遍历,跳出循环 } } System.out.println("购物车中的商品总金额为"+money); // 循环结束后,将商品总金额输出 } }
注:Goods[] shopCar= new Goods[100]; // Goods类型的数组,里面存放Goods对象,具体值是各个Goods对象的地址
※※※类本身是一个引用类型,Goods是自定义的引用类型
四、构造器、this关键字
构造器的作用?
用于初始化一个类的对象,并返回对象的地址。讲人话就是可以得到一个对象的地址,通过地址访问该对象。
构造器的格式?
示例代码如下:
publicclassCar { // 无参构造器publicCar() { ... } // 有参数构造器publicCar(Stringa, doubleb) { ... } }
调用构造器得到对象的格式:
类 变量名称 = new 构造器;
如Car car = new Car();Car c2 = new Car (“奔驰”, 39.8);
构造器的分类和作用:
无参数构造器(默认存在的):初始化的对象时,成员变量的数据均采用默认值。
有参数构造器:在初始化对象的时候,同时可以接收参数,为对象的成员变量进行赋值。
示例代码如下:
汽车类
publicclassCar { Stringname; doubleprice; /*** 无参构造器*/publicCar() { System.out.println("无参构造器被调用了"); } /*** 有参构造器*/publicCar(Stringn, doublep) { System.out.println("有参构造器被调用了"); name=n; price=p; } }
测试类
publicclassTest { publicstaticvoidmain(String[] args) { // 通过调用构造器得到对象Carc1=newCar(); // 调用无参构造器c1.name="宝马"; c1.price=38.9; System.out.println(c1.name); System.out.println(c1.price); Carc2=newCar("奔驰", 38.8); // 调用有参构造器System.out.println(c2.name); System.out.println(c2.price); } }
程序运行结果如下:
无参构造器被调用了
宝马
38.9
有参构造器被调用了
奔驰
38.8
构造器的注意事项:
任何类定义出来,默认自带无参数构造器,写不写都有。
一旦定义了有参数构造器,无参数构造器就没有了,如果还想用无参数构造器,此时需要自己写一个无参数构造器。
this关键字
可以出现在构造器、方法中,代表当前对象的地址,可以用于访问当前对象的成员变量、成员方法(this.name意为当前地址所对应的对象的成员变量name)。
五、封装
面向对象的三大特征:封装、继承、多态。
封装:告诉我们,如何正确的设计对象的属性和方法。
※※※封装的原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为。
例如:人画圆这个行为,方法是定义在圆这个对象中,而不是定义在人这个对象中。针对圆这个对象,其中封装的成员变量有半径这个属性,只有得知半径,才能画出这个圆,画圆行为是半径属性的相关行为,因此画圆这个方法需要封装在圆这个对象中,人画圆的时候只需要调用圆中封装的画圆方法即可,除去人之外,小猫、小狗画圆行为同样是调用圆的画圆方法来完成的。
个人理解:
面向对象封装行为(方法)的原则:
主谓宾:将谓语动词这个行为(方法)封装到宾语对象中。
主谓:将谓语动词这个行为(方法)封装到主语对象中。
如何进行更好的封装?
面向对象的三大特征之一,合理隐藏,合理暴露。
①一般会把成员变量使用private隐藏起来,对外就不能直接访问了。
②提供public修饰的getter和setter方法暴露其取值和赋值。在setter方法中对传入的参数值进行校验,合格之后再赋值给对象中的变量,避免了问题数据的注入,提高了程序的安全性
示例代码如下:
学生类
publicclassStudent { // 定义成员变量,使用private修饰,该成员变量只能在此Class类中访问privateintage; /*** 提供成套的getter和setter方法暴露其取值和赋值方法*/publicvoidsetAge(intage) { // 在setter方法中对传入的参数值进行校验,合格之后再赋值给对象中的变量if (age>=0&age<=200) { this.age=age; } else { System.out.println("年龄值为"+age+",数据有误!"); } } publicintgetAge() { returnage; } }
测试类:
publicclassTest { publicstaticvoidmain(String[] args) { Studentstu=newStudent(); // stu.age; // 报错,private修饰符定义的属性只能在本类中使用,在其他的类中获取不到stu.setAge(20); // 调用setter方法,将对象中的成员变量age赋值为20System.out.println(stu.getAge()); // 20 调用getter方法,获取对象中的成员变量age的值Studentstu2=newStudent(); stu2.setAge(-1); // 调用setter方法时,超出范围,返回对应提示System.out.println(stu2.getAge()); // 0 数据赋值失败,在获取时Age仍然为默认值0,避免了问题数据的注入,提高了程序的安全性 } }
封装的好处:
加强了程序代码的安全性。
适当的封装可以提升开发效率,同时可以让程序更容易理解与维护。
六、JavaBean
JavaBean是在现实生活中有对应具体个体(学生类、汽车类、用户类)的类,也可以称为实体类(测试类Test不属于JavaBean),其对象可以用在程序中封装数据。
标准JavaBean须满足如下要求:
①成员变量使用 private 修饰。
②提供每一个成员变量对应的 setXxx() / getXxx()。
③必须提供一个无参构造器,有参构造器可写可不写。
示例代码如下:
用户类
publicclassUser { // 成员变量使用私有private修饰符privateStringname; privatedoubleheight; privatedoublesalary; /*** 提供无参构造器,有参构造器可写可不写*/publicUser() { } publicUser(Stringname, doubleheight, doublesalary) { this.name=name; this.height=height; this.salary=salary; } /*** 必须为成员变量提供成套的getter和setter方法*/publicStringgetName() { returnname; } publicvoidsetName(Stringname) { this.name=name; } publicdoublegetHeight() { returnheight; } publicvoidsetHeight(doubleheight) { this.height=height; } publicdoublegetSalary() { returnsalary; } publicvoidsetSalary(doublesalary) { this.salary=salary; } }
测试类
publicclassTest { publicstaticvoidmain(String[] args) { // 通过无参构造器创建一个对象封装一个用户信息Useruser=newUser(); user.setName("张三"); user.setHeight(185.3); user.setSalary(5.5); System.out.println(user.getName()); // 张三System.out.println(user.getHeight()); // 185.3System.out.println(user.getSalary()); // 5.5// 通过有参构造器创建一个对象封装一个用户信息Useruser2=newUser("李四", 180.5, 3.8); System.out.println(user2.getName()); // 李四System.out.println(user2.getHeight()); // 180.5System.out.println(user2.getSalary()); // 3.8 } }
成员变量、局部变量区别
成员变量和局部变量的区别
区别 |
成员变量 |
局部变量 |
类中位置 |
类中,方法外 |
常见于方法中 |
初始化值 |
初始化时选择默认值 |
在使用之前需要进行赋值 |
内存位置 |
堆内存(知识点 0-1) |
栈内存 |
生命周期 |
随着对象的创建而存在,随着对象的消失而消失 |
随着方法的调用而存在,随着方法的运行结束而消失 |
作用域 |
|
在所属的大括号中 |
七、面向对象综合案例
需求:使用面向对象编程,模仿电影信息的展示。
分析:
①一部电影是一个java对象,需要先设计电影类,再创建电影对象。
②三部电影对象可以采用数组存储起来。
③依次遍历数组中的每个电影对象,取出其信息进行展示。
示例代码如下:
电影类
publicclassMovie { // 定义成员变量privateStringname; privateStringactor; privatedoublescore; // 定义无参构造器publicMovie() { } // 定义有参构造器publicMovie(Stringname, Stringactor, doublescore) { this.name=name; this.actor=actor; this.score=score; } // 创建getter、setter方法publicStringgetName() { returnname; } publicvoidsetName(Stringname) { this.name=name; } publicStringgetActor() { returnactor; } publicvoidsetActor(Stringactor) { this.actor=actor; } publicdoublegetScore() { returnscore; } publicvoidsetScore(doublescore) { this.score=score; } }
测试类
publicclassTest { publicstaticvoidmain(String[] args) { // 1.创建电影类// 2.创建对象,封装电影信息 3.定义一个电影类型的数组,存储3个电影对象Movie[] movies=newMovie[3]; movies[0] =newMovie("长津湖", "吴京", 9.7); movies[1] =newMovie("我和我的父辈", "吴京", 9.6); movies[2] =newMovie("扑水少年", "王川", 9.5); // 4.遍历数组中的每个电影对象,获取对象中的各个成员属性并展示出来System.out.println("电影名\t\t主演\t\t评分"); for (inti=0; i<movies.length; i++) { System.out.println(movies[i].getName() +"\t\t"+movies[i].getActor() +"\t\t"+movies[i].getScore()); } } }
程序运行结果如下:
电影名主演评分
长津湖吴京9.7
我和我的父辈吴京9.6
扑水少年王川9.5
程序在内存中的运行机制如下图所示。
结论:数组变量中存储的元素不是数组本身,而是数组在堆内存中的地址。数组中开辟的三块小区域中各自存储的元素不是对象本身,而是堆内存中各个对象的地址。