Java基础学习——第四章 面向对象编程(上)
一、类与对象(面向对象的两个要素)
1. Java面向对象的三条主线
- 类及类的成员:属性、方法、构造器;代码块、内部类
- 面向对象的三大特征:封装性、继承性、多态性、(抽象性)
- 其他关键字:this、super、static、final、abstract、interface、package、import等
2. 类和对象的概念
- 类:是对==一类事物的描述,是抽象的==、概念上的定义
- 对象:是实际存在的该类事物的==具体的个体,因而也称为实例==,对象是通过类new出来的
- 面向对象程序设计的重点就是设计类
- 设计类,其实就是设计==类的成员==:属性、方法、构造器;代码块、内部类
3. 理解“万事万物皆对象”
- Java将功能、结构等封装到类中,通过类的实例化,来调用具体的功能和结构
- 涉及到Java与前端的Html、后端的数据库交互时,都体现为类和对象
4. 类的成员之:属性和方法
- 属性:类中的成员变量(属性 = 成员变量 = field = 域或字段)
- 方法:类中的成员方法(方法 = 成员方法 = 函数 = method)
5. 类和对象的使用(基本步骤)
- 创建类,设计类的成员(如属性和方法)
- 创建类的对象 = 类的实例化 = 实例化类(通过new的方式)
- 对象的使用(调用对象的结构):属性(“对象.属性”)、方法(“对象.方法”)
import java.util.Scanner;
//测试类
public class Day08_PersonTest {
public static void main(String[] args) {
//********************* 创建类Person的第一个对象 = 类的实例化 = 实例化类(new)
Person p1 = new Person();
//创建Scanner类的对象scan
Scanner scan = new Scanner(System.in);
//创建数组
String[] arr = new String[2];
//********************* 调用对象的结构:属性、方法
//调用属性:"对象.属性"
//在Person类中没有给name属性(成员变量)赋值,输出String类型变量的初始化值
System.out.println(p1.name); //null
//在Person类中已经给age属性(成员变量)赋过值了
System.out.println(p1.age); //1
//给对象的属性赋值
p1.name = "Tom";
p1.isMale = true;
//调用方法:"对象.方法"
p1.eat();
p1.sleep();
p1.talk("Chinese"); //"Chinese"传给了talk方法的language变量
//********************* 创建类Person的第二个对象
Person p2 = new Person();
System.out.println(p2.name); //null
//********************* 创建类Person的第三个对象
//将p1中存放的对象地址值赋给了另一个引用类型变量p3,此时p1和p3共同指向堆空间中同一个对象实体
Person p3 = p1;
System.out.println(p3.name); //Tom
p3.age = 10;
System.out.println(p1.age); //10
}
}
//创建类
class Person {
//属性
String name;
int age = 1;
boolean isMale;
//方法
public void eat() {
System.out.println("人可以吃饭");
}
public void sleep() {
System.out.println("人可以睡觉");
}
public void talk(String language) {
System.out.println("人可以说话,使用的是:" + language);
}
}
6. 对象的内存解析
Person p1 = new Person();
- p1是Person类的对象的引用类型变量,p1的值 = 指向堆空间中对象实体的地址值
- 引用类型变量p1声明在main()方法中,方法体内部声明的变量都是局部变量,故存放在栈空间中
- 创建Person类时声明的三个属性name、age、isMale都是成员变量(方法体外,类体内声明的变量),存放在堆空间中。堆空间中的数组实体存放的是各个元素,而堆空间中的对象实体存放的是各个属性
- 属性age在创建类时赋的初始值是1;属性name和isMale在创建类时未赋值,故进行相应类型变量的初始化:boolean类型变量isMale的初始化值是false;属性name是String类的对象的引用类型变量,故初始化值是null
p1.name = "Tom";
p1.isMale = true;
- 给对象的属性赋值。注意,属性name的值 = 指向方法区字符串常量池中字符串对象"Tom"的地址值
Person p2 = new Person();
System.out.println(p2.name);
- 因为引用类型变量p2指向的对象是new出来的,故在堆空间中重新开辟了一块内存空间存放新的对象实体
此时p1和p2中存放的是不同的地址值,分别指向堆空间中两个相互独立的对象实体 - 如果创建了一个类的多个对象,则每个对象都拥有一套独立的类的属性(非static的)
如果我们修改其中一个对象某个属性A的值,并不会影响另一个对象的属性A的值
Person p3 = p1;
System.out.println(p3.name); //Tom
p3.age = 10;
System.out.println(p1.age); //10
- 将引用类型变量p1中存放的地址值赋给另一个引用类型变量p3(在方法内声明的局部变量,存放在栈中),p1和p3共同指向堆空间中同一个对象实体。此时只是新声明了一个引用类型变量,而不是新创建了一个对象
7. 匿名对象的使用
- 定义:创建(new)对象时,没有显式的将地址值赋给一个引用类型变量(变量名)
- 特点:匿名对象只能调用一次
- 使用:将匿名对象传给方法的形参,此时形参的值为该匿名对象在堆空间中的地址值
public class Day09_InstanceTest {
public static void main(String[] args) {
Phone p = new Phone();
p.price = 1999;
p.showPrice(); //手机的价格为:1999
//匿名对象的特征
new Phone().price = 1999;
new Phone().showPrice(); //手机的价格为:0
//匿名对象的使用
PhoneMall mall = new PhoneMall();
//将Phone类型的匿名对象传给show方法的形参,此时形参的值为该匿名对象在堆空间中的地址值
mall.show(new Phone());
}
}
class Phone {
int price; //价格
public void showPrice() {
System.out.println("手机的价格为:" + price);
}
}
class PhoneMall {
//这里的形参phone是Phone类的对象实体的引用类型变量
public void show(Phone phone) {
System.out.println(phone); //形参phone的值为对象实体在堆空间中的地址值
phone.showPrice();
}
}
二、类的成员之一:属性(成员变量)
1. 属性(成员变量) vs 局部变量
1.1 相同点
1.1.1 定义变量的格式相同
数据类型 变量名 = 变量值;
1.1.2 先声明,后使用
1.1.3 变量都有其对应的作用域
1.2 不同点
1.2.1 在类中声明的位置不同
- 属性(成员变量): 在方法(函数)体外,类体内声明的变量
实例变量:不以static修饰,需要通过对象来访问
类变量(静态变量):以static修饰,表示是"静态"的,不需要通过实例化一个类的对象来访问 - **局部变量:**在方法(函数)体内部声明的变量
形参:方法、构造器中定义的变量
方法局部变量:在方法内定义
代码块局部变量:在代码块内定义
1.2.2 权限修饰符不同
- **属性(成员变量):**可以在声明属性时,指明其权限修饰符
Java规定的4种权限修饰符:public、private、缺省(不写)、protected - **局部变量:**不可以使用权限修饰符
1.2.3 默认初始化值不同
-
**属性(成员变量):**类的属性,根据其类型,都有默认初始化值
整型(byte、short、int、long):0
浮点型(float、double):0.0
字符型(char):0或’\u0000’(表示ASCII码为0的字符null,而不是字符’0’)
布尔型(boolean):false
引用数据类型(如类、数组、接口等):null
-
**局部变量:**没有默认初始化值,因此我们在调用局部变量之前,一定要显式赋值。
特别的,形参在调用时赋值即可(如talk方法中的language形参,只需在调用方法时给形参传递一个值即可)
1.2.4 在内存空间中加载的位置不同
- **属性(成员变量):**堆空间(非static)
- **局部变量:**栈空间
class User {
//********************* 属性(成员变量)
//实例变量:不以static修饰,需要通过对象来访问
public String name; //public:公有的,可以在类,子类及类的外部使用;
private int age; //private:私有的,只能在类中使用,不能在子类及类的外部使用;
protected boolean isMale; //protected:受保护的,可以在类及子类中使用,不能在类的外部使用;
//********************* 局部变量
//形参:方法、构造器中定义的变量
public void talk(String language) { //language:形参
System.out.println("我们使用" + language);
}
//方法局部变量:在方法内定义的变量
public void eat() {
String food = "披萨"; //food:形参
}
}
2. 属性(成员变量)赋值的先后顺序
- 默认初始化值
- 显式初始化值(在类中声明对象时即赋一个初始化值)
- 构造器中初始化
- 通过"对象.方法"和"对象.属性"的方式赋值
三、类的成员之二:方法
方法:描述类应该具有的功能
比如Math类的sqrt()方法;Scanner类的nextXxx()方法;Arrays类的sort()方法等
1. 方法的声明
权限修饰符 返回值类型 方法名(形参列表) {
方法体
}
//此外static、final、abstract也可以用来修饰方法
//客户类
class Customer {
//*************属性
String name;
int age;
boolean isMale;
//*************方法
//无返回值无形参
public void eat() {
System.out.println("客户吃饭");
}
//无返回值有形参
public void sleep(int hour) {
System.out.println("休息了" + hour + "个小时");
}
//有返回值无形参
public String getName() {
return name; //类的方法可以调用类的属性
}
//有返回值有形参
public String getNation(String nation) {
String info = "我的国籍是" + nation;
return info;
}
}
1.1 权限修饰符
- Java规定的4种权限修饰符:public、private、缺省(不写)、protected
1.2 返回值类型
- 有返回值 vs 没有返回值
- 如果方法有返回值,则必须在方法声明时指定返回值的类型。同时,方法中需要使用return关键字返回指定类型的变量或常量:“return 变量或常量;”
- 返回值可以是任意数据类型的,可以是数组也可以是对象
class Customer {
//这种情况会报错,因为只有当 age > 18 时才有返回值
public String getName() {
if (age > 18) {
return name;
}
}
//这种情况不报错,因为任何情况下都有返回值
public String getName() {
if (age > 18) {
return name;
} else {
return "Tom";
}
}
}
- 如果方法没有返回值,则声明时使用void来表示。通常,没有返回值的方法不需要使用return,如果使用 “return;” 则一旦遇到return关键字就结束方法
1.3 方法名
- 方法名属于标识符,遵守标识符的规则和规范,见名知意
1.4 形参列表
- 方法可以声明0个、1个或多个形参
- 形参可以是任意数据类型的。可以作为数组的引用类型变量,存放堆空间中数组实体的地址值;也可以作为类的对象的引用类型变量,存放堆空间中对象实体的地址值
- 格式:数据类型1 形参1, 数据类型2 形参2, …
2. return关键字的使用
- 使用范围:方法体内
- 作用:①结束方法;②对于有返回值的方法,“return 变量或常量;” 返回指定类型的变量或常量
- 注意:return关键字后不可再声明执行语句
3. 方法的使用
- 方法体内可以调用当前类的属性、方法(调用时无需实例化对象,直接写属性名或者方法名;或者使用this修饰符来表示当前对象的属性或方法)
- 方法体内可以创建(new)当前类的对象
- 但在main方法内调用当前类的非静态(Non-static)方法时,需要实例化当前类的对象,再调用对象的方法。这是因为main方法是静态(static)的,在静态上下文中不能引用非静态方法
- 一般来说,方法体内一般调用的是其他方法,但也可以调用自己本身,这就是==递归==
- 方法内不可以定义方法
4. 例题
例1:Person类的创建
//Person类
public class Person {
//属性
String name;
int age;
int sex;
//方法
public void study() {
System.out.println("studying");
}
public void showAge() {
System.out.println(age);
}
public int addAge(int i) {
age += i;
return age;
}
}
//测试类
public class Day08_PersonTest {
public static void main(String[] args) {
//*****实例化一个Person类的对象
Person p1 = new Person();
//给对象的属性赋值
p1.name = "Tom";
p1.age = 18;
p1.sex = 1;
//调用对象的方法
p1.study();
p1.showAge();
int newAge = p1.addAge(2);
System.out.println(p1.name + "的新年龄为:" + newAge); //20
//此时p1指向的对象的age属性值也为20,其实就是把age属性的值赋给了一个新变量newAge
System.out.println(p1.age);; //20
//*****实例化第二个Person类的对象
Person p2 = new Person();
p2.showAge(); //0 初始化的值
p2.addAge(10);
p2.showAge(); //20
}
}
例2:类Circle计算圆的面积
- 利用面向对象的编程方法,设计类Circle计算圆的面积
//Circle类
public class Circle {
double radius;
public double computeArea() {
double area = Math.PI * radius * radius;
return area;
}
}
//测试类
public class Day08_CircleTest {
public static void main(String[] args) {
Circle c1 = new Circle();
c1.radius = 2;
double c1Area = c1.computeArea();
System.out.println(c1Area);
}
}
例3:打印一个m*n的*型矩形
- 声明一个Rectangle方法(m和n两个形参),在方法中打印一个m*n的*型矩形,并计算该矩形的面积, 将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印
public class Day08_RectangleTest {
public static void main(String[] args) {
Day08_RectangleTest test = new Day08_RectangleTest();
int area = test.Rectangle(10, 8);
System.out.println(area);
//或者不用定义新的变量,直接输出Rectangle方法的返回值
//System.out.println(test.Rectangle(10, 8));
}
public int Rectangle(int m, int n) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
System.out.print("*" + " ");
}
System.out.println();
}
return m * n;
}
}
例4:对象数组
- 定义类Student,包含三个属性:学号number(int),年级state(int),成绩score(int)。 创建20个学生对象,学号为1到20,年级和成绩都由随机数确定
- 问题一:打印出3年级(state值为3)的学生信息
- 问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息
- ①生成随机数 Math.random(),返回值类型double; ②四舍五入取整 Math.round(double d),返回值类型long
public class Day08_StudentTest {
public static void main(String[] args) {
//声明一个Student类型的对象数组(类似于声明一个String类型的数组),长度为20
Student[] stu = new Student[20];
//实例化Student类的对象
for (int i = 0; i < stu.length; i++) {
//数组中的元素都是Student类的引用类型变量,这一步相当于给数组元素赋值(指向Student类的对象实体的地址值)
stu[i] = new Student();
//给每个Student类的对象的属性赋值
stu[i].number = i + 1;
//年级:[1, 6]的随机数
stu[i].state = (int) Math.round(Math.random() * (6 - 1 + 1) + 1);
//年级:[0, 100]的随机数
stu[i].score = (int) Math.round(Math.random() * (100 - 0 + 1) + 0);
}
//遍历对象数组
for (int i = 0; i < stu.length; i++) {
//每个引用类型元素指向的Student类的对象实体的地址值
System.out.println(stu[i]);
//每个引用类型元素指向的Student类的对象实体的属性
//System.out.println("学号:" + stu[i].number + ",年级:" + stu[i].state + ",成绩:" + stu[i].score);
System.out.println(stu[i].info());
}
System.out.println("***************************");
//问题一:打印出3年级(state值为3)的学生信息
for (int i = 0; i < stu.length; i++) {
if (stu[i].state == 3) {
System.out.println(stu[i].info());
}
}
System.out.println("***************************");
//问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息
for (int i = 0; i < stu.length - 1; i++) {
for (int j = 0; j < stu.length - 1 - i; j++) {
//注意:stu[j]表示对象数组的每个元素,均为引用类型变量,它们的值都是地址值,不能比较大小
//但他们指向的对象实体的属性score可以比较大小
if (stu[j].score > stu[j + 1].score) {
// //方法一:交换指向的对象实体的属性
// int temp1 = stu[j].number;
// stu[j].number = stu[j + 1].number;
// stu[j + 1].number = temp1;
// int temp2 = stu[j].state;
// stu[j].state = stu[j + 1].state;
// stu[j + 1].state = temp2;
// int temp3 = stu[j].score;
// stu[j].score = stu[j + 1].score;
// stu[j + 1].score = temp3;
//方法二:交换指向的对象实体,即交换对象数组元素存放的地址值
//对象的引用类型变量的赋值,而并非对象的复制。将引用类型变量stu[j]指向的对象的地址值赋给了temp
Student temp = stu[j];
stu[j] = stu[j + 1];
stu[j + 1] = temp;
}
}
}
//遍历按成绩排序后的学生信息
for (int i = 0; i < stu.length; i++) {
System.out.println(stu[i].info());
}
}
}
class Student {
int number; //学号
int state; //年级
int score; //成绩
//显示学生信息的方法
public String info() {
return "学号:" + number + ",年级:" + state + ",成绩:" + score;
}
}
- 改进:将操作数组的方法封装到方法中
public class Day08_StudentTest {
public static void main(String[] args) {
//声明一个Student类型的对象数组(类似于声明一个String类型的数组),长度为20
Student[] stu = new Student[20];
//实例化Student类的对象
for (int i = 0; i < stu.length; i++) {
//数组中的元素都是Student类的引用类型变量,这一步相当于给数组元素赋值(指向Student类的对象实体的地址值)
stu[i] = new Student();
//给每个Student类的对象的属性赋值
stu[i].number = i + 1;
//年级:[1, 6]的随机数
stu[i].state = (int) Math.round(Math.random() * (6 - 1 + 1) + 1);
//年级:[0, 100]的随机数
stu[i].score = (int) Math.round(Math.random() * (100 - 0 + 1) + 0);
}
//实例化一个类的对象,调用类中的方法
Day08_StudentTest test = new Day08_StudentTest();
//遍历对象数组
test.print(stu);
System.out.println("***************************");
//问题一:打印出3年级(state值为3)的学生信息
test.searchState(stu, 3);
System.out.println("***************************");
//问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息
test.sortScore(stu);
//遍历按成绩排序后的学生信息
test.print(stu);
}
//遍历Student[]数组的方法
public void print(Student[] stu) {
for (int i = 0; i < stu.length; i++) {
System.out.println(stu[i].info());
}
}
//查找指定年级的学生信息
public void searchState(Student[] stu, int state) {
for (int i = 0; i < stu.length; i++) {
if (stu[i].state == state) {
System.out.println(stu[i].info());
}
}
}
//使用冒泡排序按学生成绩排序
public void sortScore(Student[] stu) {
for (int i = 0; i < stu.length - 1; i++) {
for (int j = 0; j < stu.length - 1 - i; j++) {
//注意:stu[j]表示对象数组的每个元素,均为引用类型变量,它们的值都是地址值,不能比较大小
//但他们指向的对象实体的属性score可以比较大小
if (stu[j].score > stu[j + 1].score) {
//对象的引用类型变量的赋值,而并非对象的复制。将引用类型变量stu[j]指向的对象的地址值赋给了temp
Student temp = stu[j];
stu[j] = stu[j + 1];
stu[j + 1] = temp;
}
}
}
}
}
class Student {
int number; //学号
int state; //年级
int score; //成绩
//显示学生信息的方法
public String info() {
return "学号:" + number + ",年级:" + state + ",成绩:" + score;
}
}
例5:自定义数组的工具类
public class Day09_ArrayUtil {
//遍历数组
public void print(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i] + " ");
}
}
//求数组元素的最大值
public int getMax(int[] arr) {
int maxValue = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > maxValue) {
maxValue = arr[i];
}
}
return maxValue;
}
//求数组元素的最小值
public int getMin(int[] arr) {
int minValue = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i];
}
}
return minValue;
}
//求数组元素的和
public int getSum(int[] arr) {
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
//求数组元素的平均值
public int getAvg(int[] arr) {
//方法内调用当前类的方法时,不用实例化对象,直接写方法名
return getSum(arr) / arr.length;
}
//反转数组
public void reverse(int[] arr) {
for (int i = 0; i < arr.length / 2; i++) {
int temp = arr[i];
arr[i] = arr[arr.length - 1 - i];
arr[arr.length - 1 - i] = temp;
}
}
//复制数组
public int[] copy(int[] arr) {
int[] copyArr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
copyArr[i] = arr[i];
}
return copyArr;
}
//数组排序(冒泡排序)
public void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
//查找数组中指定元素(二分法查找)
public int getIndex(int[] arr, int dest) {
//二分法适用的条件:排序后的数组
sort(arr);
//查找范围的首索引,初始为0
int start = 0;
//查找范围的末索引,初始为数组最后一个元素的索引
int end = arr.length - 1;
int index = -1;
while (start <= end) {
int mid = (start + end) / 2;
if (dest == arr[mid]) {
index = mid;
break;
} else if (dest < arr[mid]) {
end = arr[mid] - 1;
} else {
start = arr[mid] + 1;
}
}
return index;
}
//冒泡排序+二分法查找
public int searchIndex(int[] arr, int dest) {
//先对数组进行冒泡排序
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
//使用二分法查找
int start = 0;
int end = arr.length - 1;
int index = -1;
while (start <= end) {
int mid = (start + end) / 2;
if (dest == arr[mid]) {
index = mid;
break;
} else if (dest < arr[mid]) {
end = arr[mid] - 1;
} else {
start = arr[mid] + 1;
}
}
return index;
}
}
5. 方法的重载
- 重载的概念:在同一个类中,允许存在一个以上同名的方法,只要它们的参数个数或者参数类型不同即可。(两同一不同:同一类、同名的方法;不同的形参列表)
- 举例:Arrays类中重载的sort() / binarySearch()
- 注意:判断是否是方法重载,与方法的权限修饰符、返回值类型、形参变量名、方法体都没有关系。只需要看形参的个数、类型及类型的顺序是否不同
public class Day09_OverLoadTest {
//方法的重载
public void getSum(int i, int j) {
System.out.println(i + j);
}
public void getSum(double i, double j) { //不同类型的形参
System.out.println(i + j);
}
public void getSum(String s, int i) {
}
public void getSum(int i, String s) { //形参的类型交换次序也算重载
}
//不算方法的重载
//public int getSum(int i, int j) {
// return 0;
//}
//
//public int getSum(int m, int n) {
// return 0;
//}
//
//private void getSum(int i, int j) {
//}
}
6. 可变个数形参的方法
- 格式:数据类型 … 变量名
public void show(String ... strs) {}
- 当调用可变个数形参的方法时,传入的参数个数可以是0个、1个或者多个
- 可变个数形参的方法与同一类中方法名相同、形参列表不同的方法之间构成重载,可以共存
public void show(String ... strs) {}
public void show(String strs) {} //二者构成重载
- 可变个数形参的方法与同一类中方法名相同、形参为相同类型数组的方法之间不构成重载,不能共存
public void show(String ... strs) {}
public void show(String[] strs) {} //二者不构成重载,Java认为二者的形参列表是一致的
- 如果想要获取可变个数形参的每个参数,可以将其看成是一个数组
public void show(String ... strs) {
for (int i = 0; i < strs.length; i++) { //可变个数形参也具有length属性
System.out.print(strs[i] + " ")
}
}
- 可变个数形参必须声明在形参列表的末尾,且最多只能在形参列表中声明一个可变个数形参
public void show(int i, String ... strs) {} //合法的
public void show(String ... strs, int i) {} //不合法的
7. 方法形参的值传递机制
7.1 变量的值传递
- 对于==基本数据类型的变量==,传递的是变量实际存储的数据值
int m = 10;
int n = m;
System.out.println(n); //10
n = 20;
System.out.println(m); //10
- 对于==引用数据类型的变量==,传递的是变量所指向的对象(或数组)实体在堆空间中的地址值
Order o1 = new Order();
Order o2 = o1; //将引用类型变量o1保存的对象地址值赋给o2,o1和o2指向同一个对象实体
o1.orderId = 1001;
System.out.println(o2.orderId); //1001
o2.orderId = 1002;
System.out.println(o1.orderId); //1002
7.2 方法形参的值传递
7.2.1 区分形参和实参
- 形参:在声明方法时,小括号(形参列表)里的参数
- 实参:在调用方法时,实际传递给形参的数据
7.2.2 形参的值传递机制
- 对于==基本数据类型的形参==,实参传递给形参的是实参实际存储的数据值
public class Day09_ValueTransferTest {
public static void main(String[] args) {
//实参m和n
int m = 10;
int n = 20;
System.out.println("m = " + m + ", n = " + n); //m = 10, n = 20
Day09_ValueTransferTest v = new Day09_ValueTransferTest();
//此时只是swap方法内的形参进行了值的交换,main方法内的实参并没有交换值
v.swap(m, n);
System.out.println("m = " + m + ", n = " + n); //m = 10, n = 20
}
//交换两个变量值的方法
public void swap(int m, int n) {
//形参m和n
int temp = m;
m = n;
n = temp;
}
}
- 注意,此时只是swap方法内的形参进行了值的交换,main方法内的实参并没有交换值
- 对于==引用数据类型的形参==,实参传递给形参的是实参所指向的对象(或数组)实体在堆空间中的地址值
public class Day09_ValueTransferTest {
public static void main(String[] args) {
Data data = new Data();
data.m = 10;
data.n = 20;
System.out.println("m = " + data.m + ", n = " + data.n); //m = 10, n = 20
//交换对象data的属性m和n的值
Day09_ValueTransferTest v = new Day09_ValueTransferTest();
v.swap(data);
System.out.println("m = " + data.m + ", n = " + data.n); //m = 20, n = 10
}
//交换两个变量值的方法
public void swap(Data data) {
int temp = data.m;
data.m = data.n;
data.n = temp;
}
}
class Data {
int m;
int n;
}
- 注意,此时swap方法内的形参data(引用数据类型)和main方法内的实参(引用数据类型)data的值都等于同一个对象实体在堆空间中的地址值
7.3 例题
例1:不同数据类型的形参的值传递
public class TransferTest3 {
public static void main(String args[]) {
TransferTest3 test = new TransferTest3();
test.first();
}
public void first() {
int i = 5;
Value v = new Value();
v.i = 25;
second(v, i);
System.out.println(v.i + " " + i); //20 5
}
public void second(Value v, int i) {//将first方法内形参i的值作为实参传递给second方法内的形参i
i = 0; // second方法内的形参i的值由开始的5变成了0
v.i = 20; //此时second方法内的形参v和first方法内的形参v都指向同一个对象实体
Value val = new Value();
//将应用类型变量val指向的对象实体的地址值赋给了second方法内的形参v
//此时second方法内的形参v和first方法内的形参v指向两个不同的对象实体
v = val;
System.out.println(v.i + " " + i); //15 0
}
}
class Value {
int i = 15;
}
例2:网红题
- 要求:在method方法调用后,仅打印出 a = 100,b = 200, 请写出method方法的代码
public class Test {
public static void main(String args[]) {
int a = 10;
int b = 10;
method(a, b);
System.out.println("a = " + a);
System.out.println("b = " + b);
}
//代码编写处
}
错误做法:因为在调用method方法时,只是将main方法内的实参a和b的实际值传给了method方法内的形参a和b。在method方法内修改形参a和b的值,对main方法内的实参a和b没有任何影响!
public class Test {
public static void main(String args[]) {
int a = 10;
int b = 10;
method(a, b);
System.out.println("a = " + a);
System.out.println("b = " + b);
}
public void method(int a, int b) {
a *= 10;
b *= 20;
}
}
此题考察的并不是方法形参的值传递,正确做法不用了解。
例3:将对象作为参数传递给方法
- 定义一个Circle类,包含一个double型的radius属性代表圆的半径,一个findArea()方法返回圆的面积
- 定义一个类PassObject,在类中定义一个方法printAreas(),该方法的定义如下:
public void printAreas(Circle c, int time)
在printAreas方法中打印输出1到time之间的每个整数半径值,以及对应的面积。例如,time为5,则输出半径1,2,3,4,5,以及对应的圆面积。 - 在main方法中调用printAreas()方法,调用完毕后输出当前半径值。
class Circle {
double radius; //半径
//计算圆的面积
public double findArea() {
return Math.PI * radius * radius;
}
}
class PassObject {
public static void main(String[] args) {
PassObject test = new PassObject();
Circle c = new Circle()
test.printAreas(c, 5);
System.out.println("now radius is:" + c.radius);
}
public void printAreas(Circle c, int time) {
System.out.println("Radius\t\tArea");
for (int i = 1; i <= time; i++) {
c.radius = i;
System.out.println(c.radius + "\t\t" + c.findArea());
}
c.radius += 1;
}
}
8. 递归方法
- 递归的概念:在方法体内调用该方法本身
- 方法的递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制
- 递归一定要==向已知方向递归==,否则这种递归就变成了无穷递归,类似于死循环,可能会导致栈溢出
- 有规律的数列题可以考虑使用递归求解
8.1 递归的例题
例1:n以内自然数的和
public class Day09_RecursionTest {
public int getSum(int n) {
if (n == 1) {
return 1;
} else {
return n + getSum(n - 1);
}
}
}
例2:n的阶乘
public class Day09_RecursionTest {
public int getMultip(int n) {
if (n == 1) {
return 1;
} else {
return n * getMultip(n - 1);
}
}
}
例3:递归一定要向已知方向递归,否则就是无穷递归
- 已知有一个数列,f(0) = 1,f(1) = 4,f(n + 2) = 2 * f(n + 1) + f(n),n > 0,求f(10)
public class Day09_RecursionTest {
public int f(int n) {
if (n == 0) {
return 1;
} else if (n == 1) {
return 4;
} else {
//千万不能写成 return f(n + 2) - 2 * f(n + 1); 不是向已知方向递归,会导致栈溢出
return 2 * f(n - 1) + f(n - 2);
}
}
}
例4:斐波那契数列
- 计算斐波那契数列的第n个值:1 1 2 3 5 8 13 21 34 55 ……;规律:第n个数等于前两个数之和
public class Day09_RecursionTest {
public int fibonacci(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 1;
} else {
return f(n - 1) + f(n - 2);
}
}
}
四、面向对象的特征一:封装性(封装与隐藏)
1. 高内聚,低耦合
- 高内聚:类的内部数据操作细节自己完成,不允许外部干涉
- 低耦合:仅对外暴露少量的方法用于使用
2. 属性的封装性体现
将类的属性私有化(private),并提供公共的(public)方法来获取(getxxx)和设置(setxxx)该属性的值
- **封装:**正常情况下,通过 “对象.属性” 给对象的属性赋值仅受属性的数据类型和存储范围的限制,而没有其他限制条件。但在实际问题中,对象的属性值通常有明确的范围,此时就需要添加额外的限制条件。这些条件不能在声明属性时添加,但可以通过声明方法来添加,如下例中的 setLegs() 方法。
- **隐藏:**同时,我们需要避免用户再使用 “对象.属性” 的方式对属性赋值,因此需要将属性声明为 private 的
- private:私有的,表示只能在类中使用,不能在子类及类的外部使用
public class Day10_AnimalTest {
public static void main(String[] args) {
Animal a = new Animal();
// a.legs = 4; //'legs' has private access in 'test.Animal'
a.setLegs(6);
}
}
class Animal{
//对legs属性的封装和隐藏
//private表示只能在类中使用,不能在子类及类的外部使用
private int legs;
//设置legs属性值的方法
public void setLegs(int l) {
if (l >= 0 && l % 2 == 0) {
legs = l;
} else {
legs = 0;
//或者抛出一个异常
}
}
//获取legs属性值的方法
public int getLegs() {
return legs;
}
}
3. 权限修饰符
3.1 Java的4种权限修饰符
- 按权限从小到大排列:private、default(缺省)、protected、public
- 权限修饰符可以用来修饰类及类的成员,体现类及类的成员在被调用时的可见性的大小
3.2 对类的权限修饰:只能用public和default(缺省)
- 对于类的权限修饰只能用public和default(缺省)
- public类可以在任意地方被访问
- default(缺省)类只能被同一个包(package)内的类访问
3.3 对类的成员的权限修饰:4种都可以
- 置于类的成员(属性、方法、构造器、内部类)的定义前,用来限制对象对该类成员的访问权限
- 代码块不能使用权限修饰符
五、类的成员之三:构造器
1. 构造器的作用
- 创建类的对象:类名 对象名 = new 构造器;
- 给对象进行初始化: 声明构造器时还可以给对象的属性赋初始化值
2. 声明构造器的格式
- 权限修饰符 类名(形参列表){}
class Person {
//属性
String name;
int age;
//构造器1
public Person(String n) {
name = n; //通过这种方式,一旦new一个新对象,其name属性就初始化为形参n的值
}
//构造器2: 构造器的重载
public Person(String n, int a) {
name = n;
age = a;
}
//方法
public void eat() {
System.out.println("人吃饭");
}
}
3. 说明
- 构造器与类同名,但是后面多了一对括号
- 一个类中,至少会存在一个构造器。如果没有显式的定义构造器,系统会默认提供一个无参的构造器,且该默认无参构造器与类的权限保持一致;一旦显式的定义了构造器,系统就不再提供默认的无参构造器
- 声明构造器的格式与声明方法的格式比较相似,但构造器的声明不包含返回值类型,不能有return语句返回值,不能被static、final、synchronized、abstract、native修饰
- 不要把构造器看成一种特殊的方法,构造器只在创建对象时使用
- **构造器的重载:**在同一个类中可以声明多个构造器
4. 构造器例题
编写两个类,TriAngle和TriAngleTest,其中TriAngle类中声明私有的底边长base和高height,同时声明公共方法访问私有变量。此外,提供类必要的构造器。另一个类中使用这些公共方法,计算三角形的面积。
public class Day10_TriAngleTest {
public static void main(String[] args) {
TriAngle test = new TriAngle(1.0, 1.0);
System.out.println(test.getBase()); //1.0
System.out.println(test.getHeight()); //1.0
test.setBase(2.0);
test.setHeight(2.0);
System.out.println(test.getBase()); //2.0
System.out.println(test.getHeight()); //2.0
System.out.println(test.getArea()); //2.0
}
}
class TriAngle {
//声明私有的属性
private double base;
private double height;
//构造器1:空参
public TriAngle() {
}
//构造器2:
public TriAngle(double b, double h) {
base = b;
height = h;
}
//声明公共方法访问私有属性
public void setBase(double b) {
if (b > 0) {
base = b;
} else {
System.out.println("请输入大于0的底边长");
}
}
public double getBase() {
return base;
}
public void setHeight(double h) {
if (h > 0) {
height = h;
} else {
System.out.println("请输入大于0的高");
}
}
public double getHeight() {
return height;
}
//计算三角形面积的方法
public double getArea() {
return 0.5 * base * height;
}
}
扩展知识1:JavaBean
- 定义:JavaBean是一种Java语言写成的可重用组件
- JavaBean是指符合如下标准的Java类:
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
public class Customer {
private int id;
private String name;
//空参构造器
public Customer() {
}
public void setId(int i) {
id = i;
}
public int getId() {
return id;
}
public void setName(String n) {
name = n;
}
public String getName() {
return name;
}
}
扩展知识2:UML类图
六、关键字:this
1. 问题的由来
- 在对类的属性进行封装时,我们希望 “setxxx” 方法中的形参"见名知义",故使用this关键字来修饰当前类的属性,与方法中的同名形参进行区分
- 在设计类的构造器时,如果构造器中条件的声明过于冗余,我们希望可以在构造器中调用当前类的其他构造器
2. this关键字的使用
- 用来修饰属性和方法
- 用来调用构造器
2.1 this修饰属性和方法:表示当前对象
- this修饰属性和方法:this理解为当前对象(在方法中)或当前正在创建的对象(在构造器中)!!!!!
- p1.setAge(1):p1指向的对象调用setAge()方法,则setAge()中的this.age相当于p1.age
- Boy类中的marry方法的形参是Girl类型的,在Girl类中调用时,可以直接传入一个this表示当前Girl类的对象(调用该方法的对象)
class Boy {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void marry(Girl girl) {
System.out.println("我想娶:" + girl.getName());
}
class Girl {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void marry(Boy boy) {
System.out.println("我想嫁给:" + boy.getName());
//this表示当前对象(调用该方法的对象)
boy.marry(this);
}
/**
* @Description 比较两个对象的大小:当前对象(调用该方法的对象,即this) VS 形参对象(传递给形参的地址值指向的对象,即girl)
* @param girl
* @return 正数:当前对象大;负数:当前对象小;0:当前对象和形参对象相等
*/
public int compare(Girl girl) {
//this表示当前调用该方法的Girl类型对象,而girl表示传递给形参的地址值指向的对象
if (this.age > girl.age) {
return 1;
} else if (this.age < girl.age) {
return -1;
} else {
return 0;
}
}
}
- 在类的==方法==中,我们可以使用 “this.属性” 或 “this.方法” 来调用当前对象的属性和方法。但通常情况下,我们都选择省略 this;如果方法的形参和当前类的属性重名时,则必须要用this关键字显式的修饰该属性(成员变量),表明该变量是属性,不是形参。
- 在类的==构造器==中,我们可以使用 “this.属性” 或 “this.方法” 来调用当前类当前正在创建的对象的属性和方法。但通常情况下,我们都选择省略 this;如果构造器的形参和当前类的属性重名时,则必须要用this关键字显式的修饰该属性(成员变量),表明该变量是属性,不是形参。
2.2 this调用构造器:this(形参列表)
- 在类的构造器中,可以显式的使用 “this(形参列表)” 的方式,调用当前类中指定(通过形参列表的形式来指定)的其他构造器
- 构造器中不能通过 “this(形参列表)” 的方式来调用自己,只能调用当前类的其他构造器
- 如果一个类中有n个构造器,则最多只能有n-1个构造器调用了其他构造器
- 规定:如果要在构造器中调用当前类的其他构造器,则必须要将 “this(形参列表)” 声明在首行;一个构造器内部最多只能声明一个 “this(形参列表)” ,即最多只能调用一个其他构造器
public class Day10_ThisTest {
public static void main(String[] args) {
Person1 p1 = new Person1();
p1.setAge(1); //p1指向的对象调用setAge()方法,则setAge()中的this.age相当于p1.age
System.out.println(p1.getAge());
}
}
class Person1 {
private String name;
private int age;
//********************** this在构造器中的使用
public Person1() {
}
public Person1(String name) {
this.name = name; //若形参和属性重名,this不能省略
}
public Person1(String name, int age) {
this(name); //在构造器中调用当前类中指定的其他构造器,放在首行
this.age = age; //若形参和属性重名,this不能省略
}
//********************** this修饰属性和方法
public void setName(String name) {
this.name = name; //若形参和属性重名,this不能省略
}
public String getName() {
return this.name; //可以省略
}
public void setAge(int age) {
this.age = age; //若形参和属性重名,this不能省略
}
public int getAge() {
return this.age; //可以省略
}
public void eat() {
System.out.println("吃饭");
this.drink(); //可以省略
}
public void drink() {
System.out.println("喝水");
}
}
3. this关键字例题
3.1 例1:this关键字的综合应用+UML类图
class Boy {
private String name;
private int age;
public Boy() {
}
public Boy(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 void marry(Girl girl) {
System.out.println("我想娶:" + girl.getName());
}
public void shout() {
if (this.age >= 22) {
System.out.println("你到法定结婚年龄了");
} else {
System.out.println("你还没到法定结婚年龄");
}
}
}
class Girl {
private String name;
private int age;
public Girl() {
}
public Girl(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 marry(Boy boy) {
System.out.println("我想嫁给:" + boy.getName());
//this表示当前对象(调用该方法的对象)
boy.marry(this);
}
/**
* @Description 比较两个对象的大小:当前对象(调用该方法的对象,即this) VS 形参对象(传递给形参的地址值指向的对象,即girl)
* @param girl
* @return 正数:当前对象大;负数:当前对象小;0:当前对象和形参对象相等
*/
public int compare(Girl girl) {
//this表示当前调用该方法的Girl类型对象,而girl表示传递给形参的地址值指向的对象
if (this.age > girl.age) {
return 1;
} else if (this.age < girl.age) {
return -1;
} else {
return 0;
}
}
}
实验:Account_Customer_Bank
题目要求
-
按照如下的 UML 类图,创建相应的类,提供必要的结构。在提款方法 withdraw()中,需要判断用户余额是否能够满足提款数额的要求,如果不能, 应给出提示。deposit()方法表示存款。
-
按照如下的 UML 类图,创建相应的类,提供必要的结构
-
按照如下的 UML 类图,创建相应的类,提供必要的结构
- addCustomer 方法必须依照参数(姓,名)构造一个新的 Customer 对象,然后把它放到 customers 数组中。还必须把 numberOfCustomer 属性的值加 1。
- getNumOfCustomers 方法返回 numberofCustomers 属性值。
- getCustomer 方法返回与给出的 index 参数相关的客户。
- 创建 BankTest 类,进行测试
思路
- 在银行对象创建后,如果想添加客户对象及其对应的账户对象,需要首先调用银行对象的 addCustomer() 方法创建一个客户对象,addCustomer() 会将客户对象的地址值传给银行对象的customers属性(对象数组)的元素,此时可以通过调用银行对象的 getCustomer() 方法获取对象数组指定索引的客户对象,并调用客户对象的setAccount() 方法,实例化一个新的账户对象。注意,虽然我们在调用 setAccount() 方法时给形参传了一个匿名对象的地址值,但在该方法内部,该地址值会直接传递给客户对象的 account属性,相当于将银行、客户和账户三者链接了起来
- 此时如果想对账户进行操作(存钱或取钱),首先应调用银行对象的 getCustomer() 方法获取对象数组指定索引的客户对象,然后调用客户对象的getAccount() 方法获取账户对象,然后再对账户对象进行操作,如==bank.getCustomer(0).getAccount().withdraw(500);==
银行 => 客户 => 账户:连续操作 - 在对数组进行赋值之前,一定要对先对其进行初始化,确定其数组长度(即在堆空间中开辟一块新的数组空间),否则会报空指针的错误
public class Day10_BankTest {
public static void main(String[] args) {
//创建一个银行
Bank bank = new Bank();
//银行添加一个客户
bank.addCustomer("Jane", "Smith");
//银行 => 客户 => 账户:连续操作
//通过银行获取客户对象,然后在通过客户对象调用setAccount方法链接一个新的账户对象
bank.getCustomer(0).setAccount(new Account1(2000));
//通过银行获取客户对象,然后在通过客户对象调用getAccount()方法获取账户对象,然后再调用账户对象的withdraw()方法
bank.getCustomer(0).getAccount().withdraw(500);
double balance = bank.getCustomer(0).getAccount().getBalance();
System.out.println(balance);
}
}
class Account1 {
//属性
private double balance;
//构造器
public Account1(double init_balance) {
this.balance = init_balance;
}
//获取balance值
public double getBalance() {
return balance;
}
//取钱
public void withdraw(double amt) {
if (balance < amt) {
System.out.println("余额不足,取款失败");
} else {
balance -= amt;
System.out.println("成功取出:" + amt);
}
}
//存钱
public void deposit(double amt) {
if (amt > 0) {
balance += amt;
System.out.println("成功存入:" + amt);
}
}
}
class Customer2 {
//属性
private String firstName;
private String lastName;
private Account1 account; //account属性是自定义类Account的引用类型变量
//构造器
public Customer2(String f, String i) {
this.firstName = f;
this.lastName = i;
}
//方法
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public Account1 getAccount() {
return account;
}
public void setAccount(Account1 account) {
this.account = account; //将形参的地址值赋给当前对象的account属性
}
}
class Bank {
//属性
private Customer2[] customers; //对象数组
private int numberOfCustomer;
//构造器
public Bank() {
customers = new Customer2[10]; //对象数组初始化,否则会报空指针错误
}
//添加客户
public void addCustomer(String f, String l) {
Customer2 customer = new Customer2(f, l);
//将new出来的客户对象的地址值赋给数组元素,使数组元素指向客户对象,这一步一定要确保对象数组已经初始化过了,否则会报空指针错误
customers[numberOfCustomer] = customer;
numberOfCustomer++;
//customers[numberOfCustomer++] = customer;
}
//获取客户的个数
public int getNumberOfCustomer() {
return numberOfCustomer;
}
//获取指定索引的客户对象
public Customer2 getCustomer(int index) {
if (index >= 0 && index < numberOfCustomer) {
return customers[index];
} else {
return null;
}
}
}
七、关键字:package/import
1. package关键字的使用
- 为了更好地对项目中的接口、类进行管理,提出了package(包)的概念
- 使用package声明接口、类所属的包,声明在源文件的首行
- 包名属于标识符,遵循标识符的命名规则和规范
- 包名中每 “.” 点一次,就代表一层文件目录
- 同一个包下,不能声明同名的接口、类;不同的包下,可以声明同名的接口、类
2. MVC设计模式
3. import关键字的使用
- 在源文件中显式的使用import结构导入指定包下的类、接口
- import声明的位置在包的声明和类的声明之间
- 如果需要导入多个类、接口,依次写出即可
- 使用 “xxx.*” 的方式,表示导入xxx包下的所有类、接口
import java.util.* //表示调入java.util下的所有类和接口
- 如果使用的类、接口时在java.lang包下定义的,则无需导包
- 如果使用的类、接口是当前包下定义的,则无需导包
- 如果在源文件中使用了不同包下的同名的类,则至少有一个类需要使用全类名的方式显式
- 使用 “xxx.*” 的方式表明可以调用xxx包下的所有结构,但如果使用xxx的子包下的结构,仍需要显式的导入
- import static:导入指定类或接口中的静态结构:属性或方法