191-220类与对象/面向对象/属性和成员变量/创建对象和访问属性/内存分配机制/成员方法/方法调用机制/传参机制/克隆对象/递归
一、引出类与对象
这个章节讲解面向对象编程基础,内容如下:
1、类与对象
成员方法
成员方法的传参机制
overload
可变参数
作用域
构造器
this
引出类与对象:
首先来看一个养猫的问题:
看一个养猫猫的问题
张老太太养了两只猫猫:一只名字叫做小白,今年3岁,白色。还有一只叫做小花,今年100岁,花色。请编写一个程序,当用户输入小猫的名字的时候,就显示该猫的名字,年龄,颜色。如果用户输入的小猫名字错误,则显示张老太太没有这只猫。
首先考虑能不能使用传统方法来解决,看看传统方法有没有什么缺陷,从而引出类与对象。
传统方式-使用单独的变量来解决
String cat1Name = "小白";
int cat1Age = 3;
String cat1Color = "白色";
String cat2Name = "小花";
int cat2Age = 100;
String cat2Color = "花色";
上面记录了两只猫的信息,如果后面我们的猫非常多怎么办,而且我们要增加猫的信息,比如爱好,体重等
使用单独变量来做不利于数据的管理,使用变量将一只猫的信息拆解了。
第二种能不能使用数组呢?
我们将猫的信息放到数组中可不可以呢?
String[] cat1 = {"小白", "3", "白色"};
String[] cat2 = {"小花", "100", "花色"};
为了都能放到同一个数组中,统一了数据类型,数据类型都是String,这样造成数据类型体现不出来了。
比如还要添加一个体重,我们也只能"2.3",将来取得小猫的属性,我们只能通过下标取下,下标是难以体现出来我们取得是什么属性得。
我们希望得是有一个变量名来体现具体我们取得是这个猫咪得什么属性。即,只能通过下标获取信息,这样造成变量名字和内容得对应关系不明确。而且不能体现猫得行为。
现有技术解决得缺点:
不利于数据得管理
效率低
引出我们得新知识点类与对象。
Java设计者引入这个类与对象,就是OOP编程,根本原因就是现有得技术不能完美得解决新得需求。
二、类与对象的概述
一个程序就是一个世界,面向对象过程中,我们会使用对象体现各种事物,组成对象的我们可以理解为属性和行为。
小狗当成一个对象,小狗有年龄,颜色,体重等,小狗还会跑,还会叫,还会吃东西等。提到对象的时候要反应过来对象是由属性和行为构成的。
类与对象的关系示意图:
比如name,age,color,还有一些行为,run cry,eat等等
一个猫类有属性和行为,猫类就是你自己定义的一个数据类型,int是一个整型,也是一种数据类型,是系统提供的,也是数据类型,猫类也是数据类型是我们程序员自定义的数据类型,通过这个我们可以知道类是数据类型,人类,车类都是数据类型。
对象又是什么呢?
100是一个int类型的对象,int这种可以对应很多的整数。
猫类也可以创建很多的猫对象,猫对象就是具体的一只猫。
猫对象也可以是各种猫。即:将事物的共性提取出来成为类,通过类可以的到各种各样的对象。
1、类就是数据类型,比如Cat
2、对象就是一个具体的实例
从类到对象的过程可以说成:
创建一个对象
实例化一个对象
把类实例化
三、养猫问题-面向对象
定义一个猫类
class Cat {
String name;
int age;
String color;
}
在主类main中实例化一只猫
Cat cat1 = new Cat();
new Cat();是创建了一只猫
然后将创建的这个猫赋值给cat1
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";
Cat cat2 = new Cat();
创建第二支猫赋值给cat2
cat2.name = "小花";
cat2.age = 100;
cat2.color = "花色";
我们只要得到了这个对象就能够管理多个属性,将来我们还可以在类中添加行为,更加方便。
怎么访问对象的属性呢?
System.out.println("第一只猫的信息"+cat1.name+" "+cat1.age+" "+cat1.color);
System.out.println("第二只猫的信息" + cat2.name+" "+cat2.age+" "+cat2.color);
想要增加属性只需要在类中写上新的属性,然后通过对象点的形式就能够找到了。
总结:
类是抽象的,概念的,代表一类事物,比如人类、猫类等,即它是数据类型。
对象是具体的,实际的,代表一个具体事物,即是实例
类是对象的模板,对象是类的一个个体,对应一个实例。
四、类与对象
对象在内存中的存在形式是怎么样的?
Cat cat = new Cat();
cat.name = "小白";
cat.age = 12;
cat.color = "白色";
五、属性和成员变量
前面提到的属性也叫做成员变量
class Cat {
String name;
int age;
String color;
属性也叫做成员变量,英文名叫做field(字段)
}
属性是类的一个组成部分,一般是基本数据类型,也可以是引用数据类型,比如对象和数组,前面定义的猫类的int age 就是一个属性。
class Car {
String name;
double price;
String color;
String[] master;
}
六、属性的注意事项和细节说明
1、属性的定义语法同变量一样,即:
访问修饰符 属性类型 属性名;
2、属性的定义类型可以任意类型,包含基本类型或者引用类型
3、属性如果不赋值,有默认值的,规则和数组一致。
访问修饰符是什么东西呢?
class Car {
protected String name;
double price;
String color;
String[] master;
}
具体访问修饰符到面向对象中级的时候会讲。
为什么要在面向对象编程中级阶段讲解访问修饰符呢?
因为要讲明白访问修饰符之前需要讲解包。
访问修饰符:
控制属性的访问范围,有四种访问修饰符
public protected 默认 private
前面讲到数组的使用和注意事项的时候讲到有默认值:
int 0 short 0 byte 0 long 0 float 0.0
double 0.0 char \u0000 boolean false
String null
class Person {
int age;
String name;
double sal;
boolean isPass;
}
// 主类main
Person p1 = new Person();
注意new Person();才是真的对象
p1只是一个引用,或者说是这个对象的名字
既然是名字,这个对象的名字就是可以变成任何的名字
打印出默认值
System.out.println("\n当前这个人的信息");
System.out.println("age="+p1.age+" name="+p1.name+" sal="+p1.sal+" isPass="+p1.isPass);
七、如何创建对象与如何访问属性
1、声明再创建
Cat cat; 栈部分有了cat,当前为null
cat = new Cat();
堆部分有了真正的对象,将栈的cat指向Cat对象
使得我们可以通过cat找到这个Cat对象
2、直接创建对象
Cat cat = new Cat();
如何访问属性
对象名.属性名
cat.name;
cat.age;
cat.color;
八、类与对象的内存分配机制
定义一个人类Person,包括名字,年龄
我们来看看一段代码:
Person p1 = new Person();
p1.age = 10;
p1.name = "小明";
Person p2 = p1;
System.out.println(p2.age);
执行第一条语句会加载Person类信息
1、属性信息
2、方法信息
加载到方法区中
然后执行new Person();在堆中创建空间,刚刚创建的时候属性没有值,当前是默认值0和nul,因为一个是age一个是String
赋给p1之后会在栈中创建p1指向堆中的这个空间,这个时候已经将第一条语句完成
p1.age = 10
会对这个属性赋值
p1.name = "小明";
这个小明会放到方法区中的常量池,这个有一个地址
然后这个对象的name指向到这个常量池的地址,
Person p2 = p1; 同样会在栈中创建一个p2指向这个p1的空间p1和p2指向同一个堆中的对象
比如有一个小孩,出生的时候叫做小狗子,上学了有一个名字王小明,两个指的都是他
最后访问age输出,结果当然也是10
九、小结
java内存中有一个叫做栈的东西,一般用来存放基本数据类型(局部变量)
堆用来存放对象,比如数组,cat对象
方法区中存放常量池比如”小明“,类加载信息也是存放到方法区的
Person p = new Person();
p.name = "jack";
p.age = 10;
首先加载类的信息,类的信息只会加载一次
然后再堆中分配空间,进行默认值初始化,然后将堆中的地址赋值给栈中的p变量
然后通过p可以访问到堆中的对象的属性进行指定初始化
将默认值更改成指定的值
十、加深内存印象
看一个练习题,并分析画出内存布局图,进行分析
// 我们看看下面一段代码,会输出什么信息:
Person a = new Person();
a.age = 10;
a.name = "小明";
Person b;
b = a;
System.out.println(b.name);
b.age = 200;
b = null;
System.out.println(a.age);
System.out.println(b.age);
内存
十一、成员方法
基本介绍
在某些情况下,我们需要定义成员方法(简称方法)。比如人类:除了有一些属性外(年龄,姓名...),我们人类还有一些行为比如:可以说话、跑步..通过学习还可以做算术题,这时就要使用成员方法才能完成。现在要求对Person类进行完善。
成员方法快速入门
1、添加speak成员方法输出,我是一只好人
2、添加cal01成员方法,可以计算从1++1000的结果
3、添加cal02成员方法,该方法可以接收一个数n,计算从1++n的结果
4、添加getSum成员方法可以计算两个数的和
class Person {
String name;
int age;
// 方法,成员方法
// 添加speak 成员方法,输出”我是一个好人“
// 1、public 表示方法是公开的
// 2、void表示方法没有返回值
// 3、speak() speak表示方法名 ()表示形参列表
// 4、{}表示方法体,可以写我们要执行的代码
// 5、System.out.println("我是一个好人");方法就是输出一句话
public void speak() {
System.out.println("我是一个好人");
}
}
// 主类main方法
Person p1 = new Person();
p1.speak(); // 调用方法
十二、成员方法二
继续在Person类中添加成员方法
public void cal01 {
//循环完成可以计算1++1000的结果
int res = 0;
for (int i = 1; i <= 1000; i++) {
res += i;
}
System.out.println("计算结果="+res);
}
// 主类main
p1.cal01();
这个方法比较笨拙,我们需要可以接收一个n,指定的从1加到某一个结束的结果
(int n)是形参列表,表示当前有一个形参n,可以接收用户的输出
public void cal02(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
}
System.out.println("计算结果 = " + res);
}
// 主类中main
p1.cal02(5);传参
p1.cal02(10);
方法就相当于一个工具,可以随时拿过来使用
// 添加getSum成员方法,可以计算两个数的和
public int getSum(int num1, int num2) {
int res = num1 + num2;
return res;
}
public 表示方法是公开的
int表示方法执行后,返回值是一个int类型
getSum方法名
(int num1, int num2)形参列表,两个形参,可以接收用户传入的两个数
return res表示将res的值返回到调用的地方
// 主类main
int returnRes = p1.getSum(10, 20);
System.out.println("getSum方法返回的值="+returnRes);
十三、方法调用机制原理图
Person p1 = new Person();是在主栈main中执行的
这个空间叫做栈空间,叫做main栈,在主方法中执行的
int returnRes = p1.getSum(10, 20);
按照我们的方法调用机制栈这里会再开一个独立的栈空间
独立会有一个保护机制,不同栈空间的数据是分离开来的
当我们执行到getSum的时候
这个地方会将10赋值给num1,20赋值给num2,根据顺序来赋值
由于是基本数据类型进行的是值拷贝,然后计算出res
return 返回res
当我们的代码执行到res的时候会返回到调用的地方
int res = p1.getSum(10, 20);
底层会记录一个地址,返回的时候会知道返回到什么地方去。
返回来之后这个独立的栈就会消失,相当于getSum这个栈就会被释放了
没有就会继续执行main栈下面的语句
输出最终的结果
方法调用小结:
当程序执行到方法的时候就会开辟一个独立的空间,这个空间叫做栈空间,当一个方法执行完毕或者执行到return语句的时候就会返回,返回到什么地方去呢?
返回到调用方法的地方
返回后继续执行后面的代码
如果main中的代码也执行完毕,main栈也释放
相当于整个程序都退出了
十四、成员方法的必要性
成员方法存在的意义究竟是什么呢?为什么要有成员方法呢?
这里有一个需求:
请遍历一个数组,输出数组的各个元素
int[][] map = {{0,0,1}, {1,1,1}, {1,1,3}};
先使用传统的方法来解决
int[][] map = {{0,0,1},{1,1,1},{1,1,3}};
传统的方法直接for遍历
for(int i = 0; i < map.length; i++) {
for(int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
如果我们在某个位置需要再次遍历呢?
直接复制一份呢!?
不好,通过非常笨拙的方式复制粘贴不好
出现重复代码,并且如果以后要对于间隔的符号不空格进行间隔,实用Tab进行间隔呢?
其他方式遍历呢?
这种对于代码的可维护性不好
我们如果放到方法中就会非常方便
我们将输出的功能放到一个类的方法中然后调用该方法即可。
class MyTools {
public void printArr(int[][] map) {
for(int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
// main用方法完成输出
MyTools tool = new MyTools();
需要遍历的时候直接使用方法遍历
tool.printArr(map);
如果再次需要遍历
tool.printArr(map);
并且如果想要修改直接修改类中的方法即可
所有调用方法的地方统统得到改变
成员方法的好处:
提高了代码的复用性
可以将实现的细节封装起来,然后提供给其他用户来调用即可。
提供开发的效率,便于多人开发。
十五、成员方法定义详细介绍
成员方法定义的详细介绍
访问修饰符 返回数据类型 方法名(参数列表){// 方法体
语句;
return 返回值;
}
1、参数列表,表示成员方法输入cal(int n);
2、数据类型,返回的类型,表示成员方法输出,void表示没有返回值
3、方法主体,表示为了实现某功能的代码块
4、return语句不是必须的。
方法的这个参数列表准确来说叫做形参列表
十六、方法的注意事项和细节1
访问修饰符有四种,如果不写表示是默认的访问修饰符
四种分别是哪四种:
public
protected
默认
private
返回数据类型
1、一个方法最多只能有一个返回值,思考:如何返回多个结果呢?
如果返回多个结果可以返回数组
class AA {
public int[] getSumAndSub(int n1, int n2){
int[] resArr = new int[2];
resArr[0] = n1 + n2;
resArr[1] = n1 - n2;
return resArr;
}
}
AA a = new AA();
int[] res = a.getSumSub(1, 4);
System.out.println("sum = " + res[0]);
System.out.println("sub = " + res[1]);
2、返回类型可以为任意类型,包含基本类型或者是引用数据类型,比如数组和对象
3、如果方法要求有返回数据类型,则方法体中最后的执行语句必须是return 值;
而且要求返回值类型必须和return的类型一样或者兼容
public double f1() {
double d1 = 1.1 * 3;
return d1; // 返回类型一样double
}
double d1 = 1.1 * 3;
int n = 100;
return n; // 这个是可以的int -> double
但是如果要求是返回int,你返回double就不行了
不能兼容
4、如果方法是void,则方法体中可以没有写return语句,或者只写return;
public void f2 () {
return ;
}
方法名
需要遵循驼峰命名发,最好见名知意,表达出该功能的意思即可。比如得到两个数的和getSum
十七、成员方法注意事项和使用细节2
1、一个方法可以有0个参数,也可以有多个参数,中间使用逗号隔开,比如getSum(int n1, int n2)
2、参数类型可以为任意类型,包含基本类型或者引用类型printArr(int[][] map)
3、调用带参数的方法时,一定要对应着参数列表传入相同类型或者兼容类型的参数。
4、方法定义时的参数成为形式参数,简称形参,方法调用的时候的参数称为实际参数,简称为实参,实参和形参的类型要一致或者兼容,个数、顺序必须一致!
方法体:
里面的具体语句可以是输入,输出,变量,运算,分支,循环,方法调用,但是里面不能再定义方法!即方法中不能再嵌套定义。
十八、成员方法注意事项和使用细节3
1、同一个类中的方法调用:直接调用即可,比如
print(参数);
class A {
public void print(int n) {
System.out.println("print()方法被调用n=" + n);
}
public void sayOK() {
print(10);
System.out.println("继续执行sayOK()");
}
}
A a = new A();
a.sayOK();
2、跨类中的方法A类调用B类方法:需要通过对象名调用。比如对象名.方法名(参数)
public void m1() {
System.out.println("m1()方法被调用");
B b = new B();
b.hi();
System.out.println("m1()方法继续执行");
}
3、特别说明:跨类的方法调用和方法的访问修饰符相关,先暂时这么提,后面讲到访问修饰符的时候再说。
十九、成员方法的练习
1、编写一个类AA,有一个方法:判断一个数是奇数odd还是偶数,返回boolean
class AA {
// 思路
// 1、方法的返回类型boolean
// 2、方法的名字 isOdd
// 3、方法的形参 int num
// 4、方法体,判断
public boolean isOdd(int num) {
// 可以使用if else
// 可以使用三元运算符
// 也可以直接返回表达式结果
return num % 2 != 0;
}
}
AA a = new AA();
if (a.isOdd(1)) {
System.out.println("是奇数");
} else {
System.out.println("是偶数");
}
// 根据行、列、字符打印对应行数和列数的字符,
// 比如:行:4,列:4,字符#,则打印对应的效果
/*
####
####
####
####
*/
public void print(int row, int col, char c) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
System.out.print(c);
}
System.out.println();
}
}
a.print(4, 4, '#');
二十、成员方法传参机制1
// 01
public void swap(int a, int b) {
int tmp = a;
a = b;
b = temp;
System.out.println("a = " + a + "\tb = " + b);
}
01
总结:对于基本数据类型传递的是值,进行的是值拷贝,形参的任何改变不会影响到实参。
二十一、引用类型的传参机制2
B类中编写一个方法test100,可以接收一个数组,在方法中修改该数组,看看原来的数组是否变化?
B类中编写一个方法test200,可以接收一个Person(age, sal)对象,在方法中修改对象的属性,看看原来的对象是否变化。
class B {
public void test100(int[] arr){
arr[0] = 200;
System.out.println("test100的数组");
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
}
}
B b = new B();
int[] arr = {1, 2, 3};
b.test100(arr);
System.out.println("main的arr数组")
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
最终结果都是200 2 3
内存结构
总结:
引用类型传递的是地址,传递的其实也是值,但是这个值是地址而已,可以通过形参影响实参
第二个在b类中编写一个方法test200,可以接收一个Person对象,在方法中修改该对象的属性,看看原来的对象是否也变化。
class Person {
String name;
int age;
}
然后再B类中
创建test200
public void test200(Person p) {
p.age = 10000;
}
Person p = new Person();
p.name = "jack";
p.age = 10;
b.test200(p);
System.out.println("main的p.age = " + p.age);
10000
内存结构
二十二、成员方法传参机制3
如果我们将p.age = 10000;注释
将p = null;看看会发生什么呢?
将这里传过去的p的指向null了,但是main中的p还是指向这个人对象的,年龄就没有发生改变,还是10
现在我们将p = null;也注释掉,
test200中写如下代码
p = new Person();
p.name = "tom";
p.age = 99;
那么主类main中的p.age又会是什么呢?
输出的还是10
创建完成这个对象之后,这个对象是局部的一个p,随着方法空间的释放被当作垃圾回收。
二十三、克隆对象
1、编写一个MyTools类,编写一个方法可以打印二维数组的数据。
之前写过,略
2、编写一个方法copyPerson,可以复制一个Person对象,返回复制的对象,克隆对象,注意要求得到的新对象和原来的对象是两个独立的对象,只是它们的属性相同。
class Person {
String name;
int age;
}
class MyTools {
public Person copyPerson(Person p) {
Person p2 = new Person();
p2.name = p.name;
p2.age = p.age;
return p2;
}
}
Person p = new Person();
p.name = "milan";
p.age = 100;
MyTools tools = new MyTools();
Person p 2 = tools.copyPerson(p);
到此p和p2是两个独立的对象,属性相同。
System.out.println("p的属性age = " + p.age + "名字=" +p.name);
System.out.println("p2的属性age = " + p2.age + "名字=" + p2.name);
可以通过hashCode看看对象是否是同一个对象
或者通过对象比较看看是否是同一个对象
这里通过比较的方式,因为比较判断的是引用
System.out.println(p == p2); false
二十四、方法递归调用
什么是方法递归调用:
简单来说,递归就是方法自己调用自己,每次调用时传入不同的变量,递归有利于编程者解决复杂问题,同时可以让代码变得简洁。
public void f() {
f();
}
递归能够解决什么问题呢?
1、各种数学问题,比如8皇后问题,汉诺塔问题,阶乘问题,迷宫问题,球和篮子的问题(google编程大赛)
2、各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等。
3、将使用栈来解决的问题->递归代码比较简洁
八皇后问题说明:
八皇后问题,是一个古老著名的问题,是回溯算法的典型案例。该问题是国际西洋棋手马克斯·贝瑟尔于1848年提出:再8*8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行,同一列或者同一斜线上,问有多少种摆法。
// 两个小案例来理解递归调用的本质
// 输出什么?
public void test(int n) {
if (n > 2) {
test(n - 1);
}
System.out.println("n = " + n);
}
内存分析
在讲阶乘之前,如果将之前的
System.out.println("n = " + n);
放到else中就只有n=2会输出
public int factorial(int n) {
if(n == 1) {
return 1;
} else {
return factorial(n-1) * n;
}
}
内存分析
递归在使用的时候必须要遵守的重要规则:
1、执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
2、方法的局部变量是独立的,不会相互影响,比如n变量
3、如果方法中使用的是引用类型变量,比如数组,就会共享该引用类型的数据。
4、递归必须向退出递归条件逼近,否则就是无限递归,出现StackOverflowError,俗称死龟了
5、当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
做两道方法递归的题目:
1、请使用递归的方式求出斐波那契数列
1,1,2,3,5,8,13...给你一个整数n,求出它的值是多少
从第三个数字开始,是前两个数的和
public int fibonacci(int n) {
if (n >= 1) {
if(n == 1 || n == 2){
return 1;
} else {
return fibonacci(n-1) + fibonacci(n-2);
}
} else {
System.out.println("要求输入的是n>=1的整数");
return -1;
}
}
2、猴子吃桃子问题:有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个。当到达第十天的时候,想再吃,发现只有一个桃子了。问题:最初的时候有多少个桃子?
思路分析:是一个逆向的过程
day 10 的时候有1个
day 9 的时候有
(day10 + 1) * 2 = 4
然后不断推
public int peach(int day) {
if (day == 10) {
return 1;
} else if (day >= 1 && day <= 9) {
return (peach(day + 1) + 1) *2;
} else {
System.out.println("day在1-10");
return -1;
}
}














浙公网安备 33010602011771号