第七章:面向对象编程(基础部分)
总体内容
类与对象
引出类与对象
- 问题:
- 以往的知识解决不了
- 类与对象体现为:对象的属性和行为
类与对象概述
类与对象的关系示意图
类与对象的区别和联系
类到对象的三种说法意思是一致的
属性概念及其细节
- 概念
- 细节
2.1 注意:new Cat()真正开辟一个内存空间(真正的对象),cat1只是一个对象名(对象引用)
类与对象快速入门案例
- 问题:
- 实现步骤
2.1 创建一个猫类
2.2 实例化两个猫对象
注意:new Cat()真正开辟一个内存空间(真正的对象),cat1只是一个对象名(对象引用)
2.3 访问猫对象的属性并打印
对象内存布局
类与对象内存分配机制
- 问题
- 内存分配机制
2.1 创建对象语句
对象创建流程文字描述
2.2 赋值语句
2.3 Person p2 = p1(就好像一个人有两个名字,但是它们指向的是同一个人)
2.4 访问对象的属性
根据对象指向的地址,找到该属性的地址
引申:java内存的结构分析
练习
创建对象的两种方法及访问属性
-
先声明再创建
在内存中cat先指向一个空地址,等到new的时候在堆中开辟一块空间,把这块地址赋给cat对象
-
直接创建
成员方法(简称方法)
成员方法快速入门(4个小例子)
-
输出一句话
1.1 在类中写方法
1.2 调用方法
先创建一个对象,然后调用这个方法
-
在方法体内计算得到一个结果
2.1 代码
2.2 调用方法
-
一个形参(接收一个数,计算得到结果)
3.1 注意形参:类型 形参名,调用时写实参
3.2 调用方法
-
两个形参以及返回值(计算两个数的和)
4.1 代码
4.2 调用方法(用一个变量接收返回值)
方法调用机制
使用成员方法的好处
- 传统的方法内容冗余而且修改方法的时候需要每一个都进行修改
- 成员方法:复用性好,啥时需要啥时调用,而且方法修改时只需要修改类中的成员方法即可,也可以把细节封装起来,供其它成员调用
方法的定义和使用时的细节
- 定义
- 使用的细节
2.1 细节第一部分
解读细节:一个方法最多有一个返回值(如果有多个返回结果的解决方法–>返回类型定义成数组)
调用时,需要用相应的数据类型去接收这个返回值,如这里返回类型是int[],则需要定义一个int[]的数组接收,用数组[下标]来访问返回的多个元素
2.2 细节第二部分
2.3 细节第三部分
解读细节:方法调用:同一类中直接调用
解读细节:方法调用:跨类中的方法A调用B类中的方法,需要通过B的对象名调用 (例如在main类中调用其它类的方法)
深入理解方法调用的内存布局
方法练习题(2道)
- 编写类,类里有一个方法:判断一个数是奇数还是偶数,返回boolean(注意:这里返回值的书写有3种方法以及接收布尔类型的2种方法)
1.1 返回值的书写的3种方法
① 最普通的方法if判断
class Cat{
public boolean jio(int num){
if(num % 2 == 0){
return true;
}else{
return false;
}
}
}
② 三元运算符
class Cat{
public boolean jio(int num){
return num % 2 == 0 ? true ; false;
}
}
③ (推荐)直接返回
class Cat{
public boolean jio(int num){
return num % 2 == 0;
}
}
1.2 接收布尔类型的2种方法
① 普通方法:定义一个变量接收返回的布尔值,然后用这个布尔值去做if判断
public class Object01 {
public static void main(String[]args) {
Cat cat = new Cat();
//定义一个变量接收返回的布尔值,然后用这个布尔值去做if判断
boolean isJiO = cat.jio(1);
if(isJiO){
System.out.println(inNum+"是一个偶数");
}else{
System.out.println(inNum+"是一个奇数");
}
}
}
① 推荐方法:直接用返回值在if中判断(因为返回值就是一个布尔值)
public class Object01 {
public static void main(String[]args) {
Cat cat = new Cat();
//直接用返回值在if中判断(因为返回值就是一个布尔值)
if(cat.jio(1)){
System.out.println(inNum+"是一个偶数");
}else{
System.out.println(inNum+"是一个奇数");
}
}
}
- 根据输入的行,列及字符打印对应行数,列数和字符(简单,直接打印不需要返回值)
2.1 在自定义类中编写方法:
2.2 在main类中调用方法:
2.3 输出结果
※方法传参机制(非常重要)
基本数据类型传参机制(值传递)
注:parameter:参数
- 问题(答案:执行完成员方法后再次输出这两个变量的值–>10,20(没有被成员方法所改变))
- 分析的内存图(重要)
2.1 结论
基本数据类型:实参传递给形参的是值,所以形参的任何改变不会影响实参
2.2 分析的内存图
解读重点理解:
① main方法和成员方法是两个独立的栈空间
② main中的a和b是main方法空间中存储的基本数据类型
③ 成员方法中的a和b是成员方法空间中存储的基本数据类型
④ 基本数据类型实参赋给形参:是main中实参的数值赋给了成员方法中形参(注意基本数据类型赋的是数值)
⑤ 调用成员方法时程序会在成员方法中找a,b变量,再把a和b的数值进行交换(实参将数值传给形参,所以成员方法中数值的变化不影响实参)
⑥ 在调用结束后,再次输出a和b,程序会在main中寻找a,b变量,然后输出,结果为10,20
引用数据类型传参机制(地址传递)
- 问题
- 分析的内存图(重要)
2.1 结论
引用数据类型:实参传递给形参的值是地址,所以形参的改变会影响实参
2.2 分析的内存图
解析步骤
① 创建对象b:b指向堆中的一个地址
② 创建数组arr:arr指向堆中的一个地址
③ b对象调用成员方法,将arr作为实参传递给形参(引用数据类型:将arr的地址赋给形参–>实参和形参指向堆中同一地址,所以形参的改变会影响实参)
④ 执行成员方法,改变形参中数组的值(因为实参形参指向堆中同一地址,所以改变形参也就改变了实参)
⑤ 成员方法返回数组后,继续执行main方法下面的代码,找到main方法中的数组进行遍历
⑥ 传入对象也同理:创建对象p:p指向堆中的一个地址,该地址中存放了对象中的属性,将该对象作为实参传递给形参时,会把该对象指向的地址赋给形参,这样实参和形参指向堆中同一地址,在成员方法中改变形参也就改变了main方法中的实参
传入数组
传入对象
引用数据类型的两个小例子
- 在成员方法里修改对象p = null,main方法输出是10
1.1 实参把对象指向的地址赋给形参,在成员方法中把形参p赋为null,说明形参指向的地址变成了空(null),而实参指向的仍为原先指向的地址
1.2 也就是:
形参指向地址 = 实参指向地址
形参指向地址 = null
此时没有改变实参指向地址
在main中输出对象时,会在main栈找到实参对象,并输出,所以结果是10
- 在成员方法里修改对象p = new Person(),main方法输出是10
2.1 重点是:方法开出的新栈中,形参一开始是和实参指向的对象是一致的,但是在方法中,new Person()在堆中开辟了一块新空间,形参p指向了这块新空间,所以形参的变化影响不了实参,这块在堆中新创建的空间,在方法结束后就销毁了
克隆对象(独立对象,只是属性相同)
- 要求
- 代码(注意事项在代码的注释里)
注意1:调用方法时要写括号,括号内要考虑是否有参数
注意2:编写方法时要考虑
①返回类型–这里返回类型是自定义的数据类型Person
②方法名
③ 形参
④方法体:创建新对象,复制属性,返回新对象
创建新对象,在堆中开辟新空间,把形参传入的属性复制过来,返回就行
注意3:这里的name是string(引用数据类型),把p.name指向的地址赋给了pNew.name(所以会相互影响)–> 注意 变量,形参和实参的内存分配机制
public class Object01 {
public static void main(String[]args) {
Cat cat = new Cat();
//创建要进行复制的对象
Person p = new Person();
p.name = "王胖子";
p.age = 20;
//接收复制完成的对象
//注意:调用方法时要写括号,括号内要考虑是否有参数
Person pNew = cat.copyPerson(p);
System.out.println("p的属性 name = "+ p.name +" age = "+ p.age );
System.out.println("pNew的属性 name = "+ pNew.name +
" age = "+ pNew.age );
//验证p和pNew指向的地址不同
System.out.println(p == pNew);
}
}
class Person{
String name;
int age;
}
class Cat{
// 编写方法时要考虑
// 1. 返回类型--这里返回类型是自定义的数据类型Person
// 2. 方法名
// 3. 形参
// 4. 方法体:创建新对象,复制属性,返回新对象
// 4.1 创建新对象,在堆中开辟新空间,把形参传入的属性复制过来,返回就行
public Person copyPerson(Person p){
Person pNew = new Person();
//注意:这里的name是string(引用数据类型),
//把p.name指向的地址赋给了pNew.name(所以会相互影响)
pNew.name = p.name;
pNew.age = p.age;
//也可以不写这一句,返回对象是pNew,这样堆中就有两个对象空间(写了以后p空间销毁)
p = pNew;
return p;
}
}
方法递归调用(recursion)
介绍
- 递归的介绍
想解决一个问题就必须解决上一个,上上个…问题,层层递归再层层返回,得到问题的解 - 递归能解决的问题
※递归调用机制(重要)
打印问题(根据内存讲解递归调用机制)
- 代码在图上有
- 讲解这个内存图:
2.1 从main栈开始,执行第一条语句创建对象,该对象指向堆中的一个地址
2.2t1.text(4);
调用方法会开辟一个新栈(text栈),实参传给形参数值,此时n = 4 ,执行方法中的内容,n > 2所以执行text(n - 1) (第一次调用)
2.3 每次调用方法都会在栈中开辟新空间,开辟的新栈中n = 3,n > 2,所以又执行text(n - 1) (第二次调用)
2.4 又开辟一个新空间,开辟的新栈中n = 2,不满足n > 2,所以不再调用方法,继续执行输出语句,输出n = 2 ,该调用结束,销毁该方法栈 (第三次调用)
2.5 text(2)方法结束后返回到调用text(2)的位置(在哪调用就返回到哪里),继续执行输出语句,输出n = 3,text(3)调用结束,销毁该方法栈
2.6 text(3)方法结束后返回到调用text(3)的位置,继续执行输出语句,输出n = 4,text(4)调用结束,销毁该方法栈,返回main方法,main方法中没有其它语句,所以程序结束
注意:每一个方法栈都要完整的执行方法
阶乘问题(factorial)–>内存讲解
从栈的顶层开始返回,递归的返回了1 * 2 * 3 * 4 * 5
递归的重要规则
递归调用的两个练习
斐波那契数(正递归)
- 问题要求
- 思路
2.1 当n = 1 斐波那契数 是1
2.2 当n = 2 斐波那契数 是1
2.3 当n >= 3 斐波那契数 是前两个数的和
2.4 这就是一个递归的思路:想知道第8个数就得知道8前两个数,要想知道8前两个数,又得算出8前两个数的前两个数,以此类推
2.5 递归计算前两个数,直到前两个数是n = 1和n = 2 - 代码
// import java.util.Scanner;
public class Object02 {
public static void main(String[]args) {
Tools tools = new Tools();
int n = -1;
//有return就要有变量接收
int num1 = tools.recursion1(n);
//判断方法返回值,如果不为-1则输出该斐波那契语句
if(num1 != -1){
System.out.println("当n = "+ n +"时对应的斐波那契数 = " + num1);
}
}
}
class Tools{
public int recursion1(int n){
if(n >= 1){
if(n == 1 || n == 2){
return 1;
}else{
//递归计算前两个数,直到前两个数是n = 1和n = 2
return recursion1(n - 2) + recursion1(n - 1);
}
}else{
//光写输出时不行的,必须要有返回值!!!
System.out.println("输入的数要大于等于1");
return -1;
}
}
}
※猴子吃桃子问题(递归逆推)
- 问题要求
- 思路分析 逆推
2.1.day = 10 有1个桃子
2.2 day = 9 有(day10 + 1)* 2 = 4
2.3 day = 8 有(day9 + 1)* 2 = 10
2.4发现规律为:前一天桃子数 = (后一天桃子数 + 1)* 2
2.5这就是递归思想,想求第1天桃子数,就得知道第2天的,第三天的,第四天的… - 代码
public class Object02 {
public static void main(String[]args) {
Tools tools = new Tools();
int n = 30;
int num1 = tools.recursion2(n);
if(num1 != -1){
System.out.println("第"+ n +"天的桃子有" + num1 + "个");
}
}
}
class Tools{
public int recursion2(int day){
//思路分析 逆推
//1. day = 10 有1个桃子
//2. day = 9 有(day10 + 1)* 2 = 4
//3. day = 8 有(day9 + 1)* 2 = 10
//4. 发现规律为:前一天桃子数 = (后一天桃子数 + 1)* 2
//这就是递归思想,想求第1天桃子数,
//就得知道第2天的,第三天的,第四天的...
if(day == 10){//记录递归最上层的条件
return 1;
}else if(day >= 1 && day <= 9){
return (recursion2(day + 1) + 1) * 2;
}else{
System.out.println("请输入1 - 10之间的数");
return -1;
}
}
}
做法自总结
- 递归方法注意是否有形参和返回类型
- 第一步先写递归终止的返回数据,比如第一个例子中前两天的数值,第二例子中,第10天的数值
- 第二步写进行递归的方法返回值,这个是根据找到的规律写的,比如第一个例子中返回前两个数相加
return recursion1(n - 2) + recursion1(n - 1);
第二个例子中返回前一天的数值return (recursion2(day + 1) + 1) * 2;
- 如何用递归表示: 第一个例子:方法的目的是递归计算前两个数相加的和,那么
recursion1(n - 2)
就表示n-2前面两个数相加的和,recursion1(n - 1)
就表示n-1前面两个数相加的和,比如第二个例子:方法的目的是计算前一天的桃子树,那么recursion2(day + 1)
就表示day后一天的桃子树,然后用规律计算即可(根据递归目的直接传参) - 所以递归一定要明确返回值的意义是什么(返回前一天的数,返回前两个数的和),要得到这个返回值的递归规律是什么,以及如何用递归表示(看第4条)
※递归调用的三个案例
老鼠出迷宫
1. 创建迷宫,设置障碍物
思路:
① 创建迷宫,用二维数组表示迷宫int[][] map = int[8][7]
② 先规定map数组的元素值:0表示可以走,1表示障碍物
③ 将最上面一行,最下面一行,最左边一行和最右边一行设置为障碍(也就是赋为1)
④ 把没有规律的障碍单独设为1
public class MiGong {
public static void main(String[]args) {
//① 创建迷宫,用二维数组表示迷宫
//② 先规定map数组的元素值:0表示可以走,1表示障碍物
int[][] map = new int[8][7];
//③ 将最上面一行,最下面一行设置为障碍(也就是赋为1)
for(int i = 0;i < 7;i++){
map[0][i] = 1;
map[7][i] = 1;
}
//③ 将最左边一行和最右边一行设置为障碍(也就是赋为1)
for(int i = 0;i < 8;i++){
map[i][0] = 1;
map[i][6] = 1;
}
//④ 把没有规律的障碍单独设为1
map[3][1] = 1;
map[3][2] = 1;
//打印迷宫
for(int i = 0;i < 8;i++){
for(int j = 0;j < 7;j++){
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
迷宫效果:
※2. 找出迷宫的路径
思路(递归逆推的思想):
① 在自定义类中定义一个方法findWay()来找迷宫的路径
② 返回值:如果找到就返回true,否则返回false
因为在if中要根据方法返回值来判断是否能走通,所以这里返回类型用了布尔
③ 形参:map 是二维数组,表示地图,i 和 j是老鼠的位置,初始化位置为(1 , 1)
④ 方法体:递归的找路,用数组中的数字表示找到的路径–>先规定map数组中各个值的含义 0表示可以走,1表示障碍物,2表示该点能走出迷宫,3表示走过但是走不通是死路
⑤ 递归结束的条件:能走出迷宫即出口位置被置为2–>map[6][5] = 2
⑥ 确定找出路的策略:下–>右–>上–>左
public class MiGong {
public static void main(String[]args) {
//1
//① 创建迷宫,用二维数组表示迷宫
//② 先规定map数组的元素值:0表示可以走,1表示障碍物
int[][] map = new int[8][7];
//③ 将最上面一行,最下面一行设置为障碍(也就是赋为1)
for(int i = 0;i < 7;i++){
map[0][i] = 1;
map[7][i] = 1;
}
//③ 将最左边一行和最右边一行设置为障碍(也就是赋为1)
for(int i = 0;i < 8;i++){
map[i][0] = 1;
map[i][6] = 1;
}
//④ 把没有规律的障碍单独设为1
map[3][1] = 1;
map[3][2] = 1;
//输出当前的迷宫
System.out.println("====当前地图情况====");
for(int i = 0;i < 8;i++){
for(int j = 0;j < 7;j++){
System.out.print(map[i][j] + " ");
}
System.out.println();
}
//2
//使用findWay找出迷宫的路径
Tool tool = new Tool();
tool.findWay(map, 1, 1);
//输出找路后的迷宫
System.out.println("====找路后的地图情况====");
for(int i = 0;i < 8;i++){
for(int j = 0;j < 7;j++){
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
class Tool{
//在自定义类中定义一个方法findWay()来找迷宫的路径
//返回值:如果找到就返回true,否则返回false
//形参:map 是二维数组,表示地图,i 和 j是老鼠的位置,初始化位置为(1 , 1)
//方法体:递归的找路,用数组中的数字表示找到的路径
// 先规定map数组中各个值的含义:
// 0表示可以走,1表示障碍物,
// 2表示该点能走出迷宫,3表示走过但是走不通是死路
public boolean findWay(int[][] map,int i,int j){
//递归先想递归结束的条件:能走出迷宫即出口位置被置为2
if(map[6][5] == 2){
return true;
}else{
//当前位置为0,说明可以走,1,2,3均不能进行找路
if(map[i][j] == 0){
//先假定map[i][j]能走出迷宫,
//不然每次返回true的时候还得写这句
map[i][j] = 2;
if(findWay(map, i + 1, j)){//下(递归返回该点是否能走出迷宫)
return true;
}else if(findWay(map, i, j + 1)){//右
return true;
}else if(findWay(map, i - 1, j)){//上
return true;
}else if(findWay(map, i, j - 1)){//左
return true;
}else{
//如果下右上左都走不通说明这个点走不出迷宫,置为3
map[i][j] = 3;
return false;
}
}else{
return false;
}
}
}
}
找路情况
⑦ 修改找出路的策略:上–>右–>下–>左
public boolean findWay2(int[][] map,int i,int j){
if(map[6][5] == 2){
return true;
}else{
if(map[i][j] == 0){
map[i][j] = 2;
if(findWay(map, i - 1, j)){//上
return true;
}else if(findWay(map, i, j + 1)){//右
return true;
}else if(findWay(map, i + 1, j)){//下
return true;
}else if(findWay(map, i, j - 1)){//左
return true;
}else{
//如果下右上左都走不通说明这个点走不出迷宫,置为3
map[i][j] = 3;
return false;
}
}else{
return false;
}
}
}
找路情况
3. 测试回溯现象
3.1 修改这段代码使得迷宫效果如图
map[2][2] = 1;
3.2 执行效果如图
策略要求是:下右上左
所以会先调用向下递归,发现下右上左都走不通,所以该点置为3,返回结果是false,将这个结果返回到调用它的位置(递归回溯现象),然后继续判断向右的递归
汉诺塔
思路:
- 返回值:每次移动都输出该盘从哪移哪(输出语句),所以返回值为void
- 形参:num为要移动的盘数,a, b, c分别表示A塔,B塔,C塔
第一个位置是初始塔,第二个位置是借助塔,第三个位置是盘最终移到的塔- 方法体:如果只有一个盘,直接从a移到c即可 (递归先考虑递归的顶层)
- 如果有多个盘,可以看成两个,最下面和最上 面的所有盘(这三步就是放盘子的重要步骤了)
(1) 移动上面所有盘到 b,借助 c
(2) 把最下面的盘移动到 c
(3) 把 b塔的所有盘移动到 c,借助 a注意形参的位置和实参的位置:
- 形参:第一个位置是初始塔,第二个位置是借助塔,第三个位置是盘最终移到的塔
- 输出语句一直都是
System.out.println( a + "->" + c);
但是输出的内容却不同这是因为传入的实参不同- 实参:main方法调用时传入的实参是
tower.move(2, 'A', 'B', 'c');
,也就是a = ‘A’,b = ‘B’,c = ‘C’,在递归调用时move(num - 1, b, a, c);
也就是传进去的实参是:a = ‘B’,b = ‘A’,c = ‘C’,自然输出 a + “->” + c时输出的是B->C- 输出语句书写时是使用形参表示的,在执行时把实参赋给形参,所以要想输出不同内容,要为方法传入不同的实参
public class HanoiTower {
public static void main(String[]args) {
Tower tower = new Tower();
int num = 2;
char a = 'A';
char b = 'B';
char c = 'C';
tower.move(num, a, b, c);
}
}
class Tower{
//思路:
//1. 返回值:每次移动都输出该盘从哪移哪(输出语句),所以返回值为void
//2. 形参:num为要移动的盘数,a, b, c分别表示A塔,B塔,C塔
//第一个位置是初始塔,第二个位置是借助塔,第三个位置是盘最终移到的塔
public void move(int num,char a,char b,char c){
//3. 方法体:如果只有一个盘,直接从a移到c即可
if(num == 1){
System.out.println("num = "+ num + "从" + a + "->" + c);
}else{
//4. 如果有多个盘,可以看成两个,最下面和最上 面的所有盘(num - 1)
//(1) 移动上面所有盘到 b,借助 c
move(num - 1, a, c, b);
//(2) 把最下面的盘移动到 c
System.out.println("num = "+ num + "从" + a + "->" + c);
//(3) 把 b塔的所有盘移动到 c,借助 a
move(num - 1, b, a, c);
}
}
}
八皇后问题(暂定)
问题说明
思路分析
方法重载
重载介绍与好处
System是类,out是System类的对象,println()是成员方法,这个方法里面可以接收不同类型的数据,是因为out对象里面有很多println()方法,只是其形参列表不同(同一类中方法名相同,形参列表不同叫方法重载)
重载的使用细节
形参列表必须不同
- 方法名必须相同
- 参数类型,个数,顺序至少有一个不同
- (int a,int b)和(int b,int a)的形参列表是相同的,参数类型相同,参数名不同是不影响的 (主要看参数对应的类型是否不同以及方法名是否相同)
- 重载和返回类型无关,和方法名和形参列表有关
重载简单的练习
- 第一题:根据不同类型形参找到不同方法,输出对应内容
第二题:根据不同类型的形参,返回不同类型的最大值
注意的细节:
可变参数
可变参数的介绍
可变参数入门案例
- 问题
- 使用
计算2个变量和,3个变量和,4,5……
对于方法名称相同,功能相同,只是参数类型不同->可以使用可变参数
- int…表示接收的是可变参数,类型是int,即可以接收多个int(0…多个)
- 使用可变参数时,形参可以当作数组来使用,即nums可以当作数组(可变参数的本质是数组)
- 遍历nums求和即可
- 代码
public class VarMethod {
public static void main(String[]args) {
T t = new T();
System.out.println(t.sum(1, 5, 100));
}
}
class T{
//计算2个变量和,3个变量和,4,5……
//对于方法名称相同,功能相同,只是参数类型不同->可以使用可变参数
//1. int…表示接收的是可变参数,类型是int,即可以接收多个int(0…多个)
//2. 使用可变参数时,形参可以当作数组来使用,即nums可以当作数组
//3. 遍历nums求和即可
public int sum(int... nums){
int res = 0;
for(int i = 0;i < nums.length;i++){
res += nums[i];
}
return res;
}
}
可变参数的细节
细节2:
细节4:
第一个实参给第一个形参,剩下的给可变参数
细节5:一个方法最多一个可变参数
可变参数的练习
- 问题
- 注意
返回姓名和两门成绩总分->返回类型String
- 代码
public class VarMethod {
public static void main(String[]args) {
T t = new T();
System.out.println(t.showScore("王胖子",90));
System.out.println(t.showScore("高瘦子",89,90));
System.out.println(t.showScore("小艺",93,78,86));
}
}
class T{
// 返回姓名和两门成绩总分->返回类型String
public String showScore(String name,double... scores){
double sum = 0;
for(int i = 0;i < scores.length;i++){
sum += scores[i];
}
return name + scores.length +"门课的成绩总分为 " + sum;
}
}
※类中作用域(scope)
作用域基本使用
① 属性(成员变量)–>全局变量,可以在任何成员方法中使用,定义时可以不赋值有默认值
② 局部变量(成员方法中或者代码块中定义的)–> 只能在定义它的代码块中使用,定义时必须赋值否则会报错
作用域使用细节
解读细节1,2
解读细节4(跨类访问对象属性的两种方式)
构造方法(构造器)
基本介绍
看一个小例子
构造器介绍
语法以及说明
快速入门练习
在对象中定义属性
在构造器中初始化属性创建对象时把属性值传递给对象)
public class Constructor {
public static void main(String[]args) {
//当我们new一个对象时,直接通过构造器指定名字和年龄
Person p = new Person("王胖子",20);
System.out.println("name:" + p.name);
System.out.println("age:" + p.age);
}
}
//需求:在创建人类对象时就直接指定这个对象的年龄和姓名
class Person{
//定义属性
String name;
int age;
//※由构造器初始化属性(创建对象时把属性值传递给对象)
public Person(String pname,int page){
//把传递进来的属性值赋值给属性
name = pname;
age = page;
}
}
使用细节
解读细节6,7
构造器的小练习
1. 定义无参构造器
需求
public class Constructor {
public static void main(String[]args) {
// 输出name: null , age: 18
Person p1 = new Person();
System.out.println("p1 name:" + p1.name + " p1 age:" + p1.age);
}
}
class Person {
String name;
int age;
//第一个构造器:利用构造器设置所有人的年龄初始值都为18
public Person(){
age = 18;
}
}
2. 定义有参构造器
需求
public class Constructor {
public static void main(String[]args) {
// 输出name: "王胖子" , age: 20
Person p2 = new Person("王胖子",20);
System.out.println("p2 name:" + p2.name + " p2 age:" + p2.age);
}
}
class Person {
String name;
int age;
//第二个构造器:在创建对象的同时初始化对象的age值和name值
public Person(String pname,int page){
name = pname;
age = page;
}
}
对象创建的流程分析(引入构造器)-面试题
解读内存图
- 在方法区加载Person类的信息,只加载一次
new Person
在堆中为对象分配空间,根据类信息在该地址定义两个属性- 属性默认初始化–>age = 0,name = null
- 属性显式初始化–>age = 90,name = null
("小倩",20)
属性构造器初始化–>age = 20,name = 小倩Person p
把对象在堆中的地址返回给栈中的p(p是对象名/对象引用)
图示
类定义的完善
this关键字
this关键字的引出
this介绍
用小故事来理解this
- 王胖子说我的眼–>这个眼是指王胖子的眼
- 高瘦子说我的眼–>这个眼是指高瘦子的眼
- 不同的对象说我的指向的是不同个体的属性
- 就像this,每个this指向的是不同的对象(this代表当前对象)
代码理解:
this.name = name;
中this.name指的是当前对象的name
this的总结和本质(内存图)
简单的说:哪个对象调用,this就代表哪个对象
内存图
this使用的细节
解读细节2(访问属性):
区分属性和局部变量
解读细节3(访问成员方法):
解读细节4(访问构造器):
只能在构造器中访问构造器
this的练习
注意:
//注意这样的写法,当返回值是布尔值的时候,可以return 表达式 return this.name.equals(p.name) && this.age == p.age;
public class Constructor {
public static void main(String[]args) {
// 在创建对象时就初始化用到了构造器
Person p1 = new Person("王胖子",20);
Person p2 = new Person("高瘦子",23);
//当前对象this就是p1,传入的对象时p2
System.out.println("p1和p2比较的结果为:" + p1.compareTo(p2));
}
}
class Person {
String name;
int age;
// 构造器
public Person(String name,int age){
this.name = name;
this.age = age;
}
//比较方法
public boolean compareTo(Person p){
//注意这样的写法,当返回值是布尔值的时候,可以return 表达式
return this.name.equals(p.name) && this.age == p.age;
}
}
本章案例
1. 求double数组最大值
注意
- 先完成正常业务,再考虑它的健壮性(如果输入为{}或null)
- 数组有两种特殊情况需要处理 ①{} ②null(需要在执行功能代码时提前判断)
2.1 先判断是否数组是否为null,再判断数组是否为{}
2.2 若为null则不能使用.length(空指针)
2.3 若为{}则取不到数组第一个元素- 返回值由double改成Double(double的包装类)
3.1 因为Double类的值可以为空 (引用数据类型可以为null)
public class HomeWork {
public static void main(String[]args) {
//数组有两种特殊情况需要处理①{}②null
double[] nums = {};
A01 a1 = new A01();
Double res = a1.max(nums);
if(res != null) {
System.out.println("数组最大值是: " + res);
}else {
System.out.println("数组输入有误:数组不能为null 或 {}");
}
}
}
class A01 {
//需求: 求某个double数组的最大值,并返回
//先完成正常业务,再考虑它的健壮性(如果输入为{}或null)
//注意:返回值由double改成Double(double的包装类)
//因为Double类的值可以为空(引用数据类型可以为null)
public Double max(double[] nums) {
//先判断是否数组是否为null,再判断数组是否为{}
//若为null则不能使用.length(空指针)
//若为{}则取不到数组第一个元素
if(nums != null && nums.length > 0) {
//先假定第一个元素是最大值
double maxNum = nums[0];
for(int i = 1;i < nums.length;i++) {
if(nums[i] > nums[i - 1]) {
maxNum = nums[i];
}
}
return maxNum;
}else {
return null;
}
}
}
输出:
2. 查找某字符串是否在字符串数组中,并返回索引(易)
本题注意:
- 在方法中return时会返回值,结束该方法
- 所以用好return语句的位置
看好下面这段代码
for(int i = 0;i < strs.length;i++){
if(findstr.equals(strs[i])) {
//如果找到就返回索引,方法结束
return i;
}
}
//如果找不到才会执行到这里,返回-1
return -1;
public class HomeWork {
public static void main(String[]args) {
//作业2
String[] strs = {"wpz" , "gsz" , "nihao"};
String findstr = "wpz";
A02 a2 = new A02();
int index = a2.find(strs,findstr);
if(index != -1) {
System.out.println(findstr + "的索引位置为:" + index);
}else{
System.out.println("数组中未找到" + findstr);
}
}
}
class A02 {
public int find(String[] strs,String findstr){
if(strs != null){
for(int i = 0;i < strs.length;i++){
if(findstr.equals(strs[i])) {
//如果找到就返回索引,方法结束
return i;
}
}
//如果找不到才会执行到这里,返回-1
return -1;
}else {
return -1;
}
}
}
输出:
3. 更改类中书的价格(使用this)
分析
- 类名:Book
- 属性:name,price
- 提供一个构造器初始化属性
- 方法:updatePrice
4.1 功能是更改类中属性的内容
4.2 返回值:void(改变类中属性无需返回值)
4.3 形参:()(改变类中属性直接使用类中属性即可,不需接收形参)
public class HomeWork {
public static void main(String[]args) {
// 作业3
Book book = new Book("中华上下五千年",400);
book.info();//更新前信息 400
book.updatePrice();//更新价格
book.info();//更新后信息 150
}
}
//分析
//类名:Book
//属性:name,price
//提供一个构造器初始化属性
//方法:updatePrice
//功能是更改类中属性的内容
//返回值:void(改变类中属性无需返回值)
//形参:()(改变类中属性直接使用类中属性即可,不需接收形参)
class Book {
String name;
double price;
public Book(String name,double price) {
this.name = name;
this.price = price;
}
//功能是更改类中属性的内容
public void updatePrice() {
if(this.price > 150) {
this.price = 150;
}else if(this.price >100) {
this.price = 100;
}
}
//显示书籍情况
public void info() {
System.out.println("书名 = " + this.name + "\t" +
"价格 = " + this.price);
}
}
输出:
4. 数组复制(易)
分析
- 类名:A03
- 方法:copyArr
2.1 功能:输入一个新数组,返回一个新数组,元素和就数组一样
2.2 返回值:int[] (新数组)
2.3 方法名:copyArr
2.4 形参:int[] (旧数组)
public class HomeWork {
public static void main(String[]args) {
// 作业4
int[] arrOld = {1, 4 , 6, 8};
A03 a3 = new A03();
a3.info(arrOld);//打印旧数组
int[] arrNew = a3.copyArr(arrOld);//复制
a3.info(arrNew);//打印新数组
}
}
//分析
//类名:A03
//方法:copyArr
//功能:输入一个新数组,返回一个新数组,元素和就数组一样
//返回值:int[] (新数组)
//方法名:copyArr
//形参:int[] (旧数组)
class A03 {
//数组拷贝
public int[] copyArr(int[] arrOld) {
if(arrOld != null) {
//在堆中创建一个数组,长度为arrOld.length
int[] arrNew = new int[arrOld.length];
//遍历旧数组,将元素拷贝到新数组
for(int i = 0;i < arrOld.length;i++){
arrNew[i] = arrOld[i];
}
return arrNew;
}else {
return null;
}
}
//显示拷贝数组
public void info(int[] arr) {
for(int i = 0;i < arr.length;i++){
System.out.print(arr[i] + "\t");
}
System.out.println();
}
}
输出:
5. 计算圆面积和周长(使用this)易
分析
- 类名:Circle
- 属性:redius
- 提供一个构造器初始化属性
- 方法①:
4.1 方法名:area
4.2 功能:计算面积
4.3 返回值:double
4.4 形参:无(使用类中的属性–>构造器初始化的)- 方法②:
5.1 方法名:len
5.2 功能:计算周长
5.3 返回值:double
5.4 形参:无(使用类中的属性–>构造器初始化的)
public class HomeWork {
public static void main(String[]args) {
// 作业5
Cicle cicle = new Cicle(1);
System.out.println("面积:" + cicle.area());
System.out.println("周长:" + cicle.len());
}
}
class Cicle {
double redius;
//构造器
public Cicle(double redius){
this.redius = redius;
}
//面积
public double area() {
return Math.PI * this.redius * this.redius;
}
//周长
public double len() {
return 2 * Math.PI * this.redius;
}
}
输出:
6. 两个数和差乘商,除数为0要提示(使用this)易
注意
如果没有相应返回类型的返回值,需要返回null时,用到了基本数据类型的包装类(和案例1的第三条意思一样)
分析
- 类名:Cale
- 属性:num1,num2
- 提供一个构造器初始化属性
- 方法①:
4.1 方法名:chu
4.2 功能:计算两数相除,如果除数为0要提示
4.3 返回值:double
4.4 形参:无(使用类中的属性–>构造器初始化的)- 剩下三个方法大同小异不写了
public class HomeWork {
public static void main(String[]args) {
// 作业6
Cale cale1 = new Cale( 8 , 2 );
cale1.info();//显示cale1的和差积商
System.out.println("==============");
Cale cale2 = new Cale( 8 , 0 );
cale2.info();//显示cale2的和差积商
}
}
class Cale {
double num1;
double num2;
//构造器
public Cale(double num1,double num2){
this.num1 = num1;
this.num2 = num2;
}
//和
public double sum() {
return this.num1 + this.num2;
}
//差
public double minus() {
return this.num1 - this.num2;
}
//积
public double mul() {
return this.num1 * this.num2;
}
//商
public Double div() {
if(this.num2 != 0){
return this.num1 / this.num2;
}else {
return null;
}
}
//展示结果
public void info() {
System.out.println(this.num1 + " + " + this.num2 + " = " + sum());
System.out.println(this.num1 + " - " + this.num2 + " = " + minus());
System.out.println(this.num1 + " * " + this.num2 + " = " + mul());
Double res = div();
if(res != null) {
System.out.println(this.num1 + " / " + this.num2 + " = " + res);
}else {
System.out.println("除数num2不能为0!");
}
}
}
输出:
7. 程序解读
- 方法中没有创建变量直接写变量名的,方法就会去全局变量中找,如果方法中没有定义同名的变量,则可以引用全局变量可以省略this
- 后++是先输出后++
- 新创建对象后属性在新开辟的空间中也会重新初始化哦
引申:匿名对象
匿名对象只能使用一次
8. 用String返回歌名和时长
引用
9. 程序解读(易)
10. 思考方法返回类型与形参
11. 复用构造器(this访问构造器)
12. 将对象作为参数传递给方法
注意的问题:
- 这里主要想强调,用一个对象根据不同的属性值求解
1.1 如果将一个定义好的对象传进去,那么其属性值是固定的,无法计算不同属性值的面积
1.2 所以要在Circle1中定义一个改变当前对象属性值的方法- 在属性初始化后可以使用set×××()来修改属性值
public class HomeWork {
public static void main(String[]args) {
//作业12
Circle1 c1 = new Circle1();
PassObject p = new PassObject();
p.printAreas(c1,5);
}
}
class Circle1 {
double radius;
//构造器
public Circle1(double radius) {
this.radius = radius;
}
public Circle1(){
}
//返回当前对象的面积
public double findArea(double radius) {
return Math.PI * radius * radius;
}
//修改当前对象属性值
public void setRadius(double radius) {
this.radius = radius;
}
}
class PassObject {
//如果将一个定义好的对象传进去,那么其属性值是固定的,无法计算不同属性值的面积
//所以要在Circle1中定义一个改变当前对象属性值的方法
public void printAreas(Circle1 c1,int times) {
System.out.println("Radius\tArea");
for(int i = 1;i <= times;i++){
c1.setRadius(i);//修改当前对象的属性值
System.out.print(c1.radius + "\t");
System.out.print(c1.findArea(c1.radius));
System.out.println();
}
}
}
13. 石头剪刀布(扩展)
import java.util.Scanner;
public class HomeWork {
public static void main(String[]args) {
Play13 p13 = new Play13();
p13.manu(p13);
}
}
class Play13 {
//用户输入的石头剪刀布
int user;
//机器生成的石头剪刀布
int maRes;
//用户输赢数组
int[] userRes = new int[10];
//当前数组下标
int index;
//表示菜单选择
int manuRes;
//构造器
public Play13() {}
public Play13(int user) {
this.user = user;
}
//菜单
public void manu(Play13 p13){
Scanner sn = new Scanner(System.in);
this.manuRes = -1;
System.out.println("====菜单====");
System.out.println("1. 开始对局");
System.out.println("2. 查看所有对局结果");
System.out.println("3. 结束对局");
System.out.print("请输入您的选择:");
manuOption(p13);
}
public void manuOption(Play13 p13) {
Scanner sn = new Scanner(System.in);
//判断选项进行对应操作(调用对应方法)
do{
this.manuRes = sn.nextInt();
if(this.manuRes == 1){
//1. 开始对局,进行用户操作
manuUser(p13);
}else if(this.manuRes == 2) {
//2. 查看所有对局结果
totalRes(p13);
}else if(this.manuRes == 3) {
System.out.println("====使用结束,再见====");
return;
}else {
System.out.println("输入选项有误,请重新输入(1,2,3):");
}
}while(this.manuRes != 1&&this.manuRes != 2&&this.manuRes != 3);
}
//1. 开始对局,进行用户操作
//1.1 将用户输入的数存到类中setUser(userInfo)
//1.2 显示输赢show(p13)
public void manuUser(Play13 p13) {
Scanner sn = new Scanner(System.in);
//变量userInfo:表示用户输入的石头剪刀布
int userInfo = -1;
System.out.println("====对局开始====");
System.out.println("0表示石头,1表示剪刀,2表示布");
System.out.print("请输入您的选择(0,1,2):");
do{
userInfo = sn.nextInt();
if(userInfo == 0||userInfo == 1||userInfo == 2) {
p13.setUser(userInfo);
System.out.println(show(p13));
System.out.print("请选择您下一步选择的菜单选项:");
manuOption(p13);
}else{
System.out.println("输入有误,请重新输入:");
}
}while(userInfo != 0&&userInfo != 1&&userInfo != 2);
}
//1.1 设置用户输入的数字
public void setUser(int user) {
this.user = user;
}
//1.2 将数字转换为字符串:赢/输/平局
//用到了isWin()
//用到了changeShow()
public String show(Play13 p13) {
System.out.println("====本局结果====");
this.userRes[index++] = p13.isWin();
return changeShow(this.userRes[index-1]);
}
//将数字转换为字符串:赢/输/平局
public String changeShow(int index1) {
if(index1 == 3) {
return "输了";
}else if(index1 == 2) {
return "赢了";
}else{
return "平局";
}
}
//isWin()判断是否输赢
//返回结果3是输,1是平,2是赢
//用到了randomNum()是机器生成数
//用到了nameString(res)是把数字表示的石头剪刀布转换为字符串表示
public int isWin() {
this.maRes = randomNum();
System.out.println("机器出" + nameString(this.maRes));
System.out.println("我 出" + nameString(this.user));
if(this.user == this.maRes) {
return 1;
}else if(this.user == 2 && this.maRes == 0) {
return 2;
}else if(this.user == 0 && this.maRes == 1) {
return 2;
}else if(this.user == 1 && this.maRes == 2) {
return 2;
}else{
return 3;
}
}
//随机生成0,1,2(石头,剪刀,布)
public int randomNum() {
while(true){
int res = (int)(Math.random() * 10);
if(res == 0 || res == 1 || res == 2){
return res;
}
}
}
//将数字转换为字符串:石头剪刀布
public String nameString(int res) {
if(res == 0) {
return "石头";
}else if(res == 1) {
return "剪刀";
}else{
return "布";
}
}
//2. 查看所有对局结果
public void totalRes(Play13 p13) {
if(this.userRes[0] != 0) {
System.out.println("====所有对局结果====");
System.out.println("我\t机器\t输赢情况");
for(int i = 0;i < this.userRes.length;i++){
if(userRes[i] != 0) {
System.out.println(nameString(this.user) + "\t" +
nameString(this.maRes) + "\t" + changeShow(userRes[i]));
}
}
System.out.print("请选择您下一步选择的菜单选项:");
manuOption(p13);
}else {
System.out.println("暂时没有进行过的对局");
System.out.print("请选择您下一步选择的菜单选项:");
manuOption(p13);
}
}
}
输出: