一、类与对象
1.1 简单介绍
为什么引入?
现有技术解决:Object01.java
import java.util.Scanner;
public class Object01 {
public static void main(String[] args) {
/*
需求:
张老太太有两只猫,一只叫小白,今年3岁,白色;另一只叫小黑,今年12岁,黑色。
请编写一个程序,当用户输入猫的名字时,就显示该猫的名字年龄,颜色。
如果用户输入错误,则显示 张老太太没有这只猫。
*/
Scanner scan = new Scanner(System.in);
// 传统方法
// 数组存放
// 缺点:无法同时保存不同的数据类型
// 只能通过[下标]获取信息,名字和内容的对应关系不明确
// 不能体现猫的行为
String cat1[] = {"小白", "3", "白色"};
String cat2[] = {"小黑", "12", "黑色"};
System.out.println("请输入小猫的名字");
String cat = scan.next();
if (cat.equals(cat1[0])) {
for (int i = 0; i < cat1.length; i++) {
System.out.print(cat1[i] + " ");
}
} else if (cat.equals(cat2[0])) {
for (int i = 0; i < cat2.length; i++) {
System.out.print(cat2[i] + " ");
}
} else {
System.out.println("张老太太没有这只猫");
}
// 传统方法
// 单独变量解决 缺点:不利于数据的管理
// 第1只猫信息
String cat1Name = "小白";
int cat1Age = 3;
String cat1Color = "白色";
// 第2只猫信息
String cat2Name = "小花";
int cat2Age = 100;
String cat2Color = "花色";
}
}
现有技术解决的缺点分析:
- 不利于数据的管理
- 效率低
1.2 类与对象的关系示意图
说明:
- 类就是数据类型,比如 Cat
- 对象就是一个具体的实例
1.3 案例演示
Object02.java
public class Object02 {
public static void main(String[] args) {
// 使用OOP面向对象解决
// 实例化一只猫/创建一只猫/把猫实例化 (一个意思)
// 解读:
// 1. new Cat() 创建一只猫(猫对象)
// 2. Cat cat1 = new Cat(); 把创建的猫赋给 cat1
// 3. cat1 就是一个对象
Cat cat1 = new Cat();
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";
cat1.weight = 10.5;
// 创建第二只猫,并赋给 cat2
// cat2 也是一个对象(猫对象)
Cat cat2 = new Cat();
cat2.name = "小花";
cat2.age = 100;
cat2.color = "花色";
cat2.weight = 20.2;
// 访问对象的属性
System.out.println("第1只猫信息 " + cat1.name + " "
+ cat1.age + " " + cat1.color + " " + cat1.weight);
System.out.println("第2只猫信息 " + cat2.name + " "
+ cat2.age + " " + cat2.color + " " + cat2.weight);
}
}
// 使用面向对象的方式来解决养猫问题
//
// 定义一个猫类 Cat -> 自定义的数据类型
class Cat {
// 属性
String name; // 名字
int age; // 年龄
String color; // 颜色
double weight; // 体重
}
1.4 类与对象的区别和联系
- 类是抽象的,概念的,代表一类事物,比如人类、猫类…,即它是数据类型
- 对象是具体的,实际的,代表一个具体事物,即 是实例
- 类是对象的模板,对象是类的一个个体,对应一个实例
1.5 对象在内存中的存在形式(重要)
1.6 创建对象的方法
先声明再创建
Cat cat;
cat = new Cat();直接创建
Cat cat = new Cat();
1.7 类与对象的内存分配机制(重要)
根据代码,分析
Person p1 = new Person();
p1.age = 10;
p1.name = "小明";
Person p2 = p1; // 把p1赋给p2,即让p2指向p1
System.out.println(p2.age);
p2.age = 15;
System.out.println(p1.age);
Java内存的结构分析:
- 栈:一般存放基本数据类型(局部变量)
- 堆:存放对象(Cat cat,数组等)
- 方法区:常量池(常量、比如字符串),类加载信息
Java创建对象的流程简单分析:
Person p = new Person();
p.name = “jack”;
p.age = 10;
- 先加载Person类信息(属性和方法信息,只会加载一次)
- 在堆中分配空间,进行默认初始化(看规则)
- 把地址赋给p,p就指向对象
- 进行指定初始化 比如:p.name = “jack”; p.age = 10;
二、属性(成员变量)
2.1 基本介绍
- 从概念或叫法上看:成员变量 = 属性 = field(字段) (即 成员变量是用来表示属性的)。
- 属性是类的一个组成部分,一般是基本数据类型,也可是引用类型(对象,数组)。
比如我们前面定义猫类的 int age 就是属性
2.2 属性的注意事项和细节说明
- 属性的定义语法同变量,示例:访问修饰符 属性类型 属性名;
访问修饰符:用于控制属性的访问范围
4种访问修饰符:public,protected,默认,private - 属性 的定义类型可以为任意类型,包含基本类型和引用类型
- 属性如果不赋值,有默认值,规则和数组一致
2.3 案例演示
PropertiesDetail.java
public class PropertiesDetail {
public static void main(String[] args) {
// 创建Person对象
// p1 是对象名(对象引用)
// new Person() 创建的对象空间(数据) 才是真正的对象
Person p1 = new Person();
// 对象的属性默认值,遵守数组规则:
// int:0,short:0,byte:0,long:0,float:0.0,
// double:0.0,char:\u0000,boolean:false,String:null
System.out.println("\n当前这个人的信息");
System.out.println("age=" + p1.age + " name=" + p1.name + " sal="
+ p1.sal + " isPass=" + p1.isPass);
}
}
class Person {
// 四个属性
int age;
String name;
double sal;
boolean isPass;
}
2.4 如何访问属性
基本语法
对象名.属性名;
三、成员方法
3.1 基本介绍
在某些情况下,我们需要定义成员方法(简称方法) 比如人类:除了有一些属性(年龄、姓名…..),还有一些行为(说话、跑步、通过学习可以做算术题)。这时就要用成员方法才能完成。现在要求对Person类完善。
3.2 案例演示
添加speak 成员方法,输出 我是一个好人
2. 添加cal01 成员方法,可以计算从1+…+1000的结果
3. 添加cal02 成员方法,该方法可以接收一个数n,计算1+…+n的结果
4. 添加getSum 成员方法,可以计算两个数的和
Method01.java ```java import java.util.Scanner; public class Method01 {public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 方法使用
// 1. 方法写好后如果不调用,不会主动运行
// 2. 先创建对象,然后调用方法即可
Person p1 = new Person(); // 创建对象
p1.speak(); // 调用方法
p1.cal01();
p1.cal02(100); // 调用cal02方法,同时给形参n = 100
// 调用 getSum方法,同时 num1 = 10, num2 = 20
// 把方法 getSum方法 返回的值 赋给 变量returnSum
int returnSum = p1.getSum(10, 20);
System.out.println("两数之和等于" + returnSum);
} }
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(“我是一个好人”); }
// 添加cal01 成员方法,可以计算从1+...+1000的结果
public void cal01() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
System.out.println("sum=" + sum);
}
// 添加cal02 成员方法,该方法可以接收一个数n,计算1+...+n的结果
// 解读:
// 1. (int n) 形参列表,表示当前有一个形参 n 可以接收用户输入
public void cal02(int n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
System.out.println("sum=" + sum);
}
// 添加getSum 成员方法,可以计算两个数的和
// 解读:
// 1. public:表示方法是公开的
// 2. int:表示方法执行后返回一个 int 值
// 3. getSum:方法名
// 4. (int num1,int num2) 形参列表,2个形参,可以接收用户输入的两个数
// 5. return sum; 表示把 sum 的值返回
public int getSum(int num1,int num2) {
int sum = num1 + num2;
return sum;
}
}
<a name="rrHnA"></a>
#### 3.3 方法的调用机制原理(重要)
![](https://cdn.nlark.com/yuque/0/2021/jpeg/12677686/1630395986738-5200d278-c0cc-4803-ab53-b092aff0a031.jpeg)<br />说明:
1. 当程序执行到方法时,就会开辟一个独立的空间(栈空间)
1. 当方法执行完毕,或者执行到 return语句时,就会返回
1. 返回到调用方法的地方
1. 返回后继续执行方法后面的代码(独立空间自动销毁)
1. 当 main方法(栈)执行完毕,整个程序退出
<a name="ei7qY"></a>
#### 3.4 成员方法的必要性
看一个需求:<br />请遍历一个数组,输出数组的各个元素值。<br />**Method02.java**
```java
public class Method02 {
public static void main(String[] args) {
// 请遍历一个数组,输出数组的各个元素值。
int[][] map = {{0, 0, 1}, {1, 1, 1}, {1, 1, 3}};
// 遍历map数组
// 传统方法: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();
}
// ...要求再次遍历map数组
// 传统方法:再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();
}
// ...再次遍历
// 传统方法冗余度太高,同样的代码写很多次
// 如果同样的代码某个地方需要做同样的修改,很麻烦
// 使用方法完成输出
MyTools tool = new MyTools();
tool.printArr(map); // map必须填写,方法里的只是形参,这是是实参
// ...再次遍历
tool.printArr(map);
}
}
// 把遍历输出map的功能,写到一个类的方法中,然后调用该方法即可
class MyTools {
// 方法,接收一个二维数组
public void printArr(int[][] map) { // 这里的map是形参 名字可以随意定义 不影响
System.out.println("==== 使用方法输出的map数组 ====");
// 对传入的map数组进行遍历输出
for (int i = 0; i < map.length; i++) { // 方法里面的map要保持和形参名字一致
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
成员方法的好处:
- 提高代码的复用性
- 可以将实现的细节封装起来,然后供其他用户来调用即可
3.5 成员方法的定义
访问修饰符 返回数据类型 方法名(参数列表..) { // 方法体
语句;
return 返回值;
}
- 访问修饰符:控制方法的使用范围 如public
- 返回数据类型:表示成员方法输出,void 表示没有返回值
- 形参列表:表示成员方法输入
- 方法主体:表示为了实现某一功能代码块
- return 语句不是必须的
3.6 方法的注意事项和使用细节
访问修饰符
有四种(public,protected,默认,private),如果不写就是默认访问
返回数据类型
- 一个方法最多有一个返回值,如果想返回多个值,可以借用数组
- 返回类型可以为任意类型,包含基本类型或引用类型
- 如果方法要求有返回数据类型,则方法体中最后的执行语句必须为 return 值; 而且要求返回值类型必须和 return 的值类型一致或兼容
- 如果方法是 void ,则方法体中可以没有 return 语句,或者只写 return;
方法名
遵循驼峰命名法,最好见名知义,表达出该功能的意思即可,
比如 得到两个数的和 getSum,开发中按照规范
参数列表
- 一个方法可以有0个参数,也可以有多个参数,中间用逗号隔开
- 参数类型可以为任意类型,包含基本类型和引用类型
- 调用带参数的方法时,一定对应着参数列表传入相同类型或兼容类型的参数
- 方法定义时的参数成为形式参数,简称形参;方法调用时的参数成为实际参数,简称实参,实参和形参的类型要一致或兼容,个数、顺序必须一致
方法体
里面写完成功能具体的语句,可以为输入、输出、变量、运算、分支、循环、方法调用,但里面不能再定义方法!即:方法不能嵌套定义。
方法调用细节说明
- 同一个类中的方法调用:直接调用即可。比如: print(参数);
- 跨类中的方法A类调用B类方法:需要通过对象名调用。比如:对象名.方法名(参数);
- 特别说明:跨类的方法调用和方法的访问修饰符相关。
⏱ 小练习
- 编写类 AA ,方法:判断一个数是奇数odd 还是偶数even,返回boolean ```java
public class MethodExercise01 {
public static void main(String[] args) {
int num = 1;
AA a = new AA();
if (a.isOdd(num)) {
System.out.println(num + "是奇数");
} else {
System.out.println(num + "是偶数");
}
}
} class AA { public boolean isOdd(int i) { // if (i % 2 != 0) { // return true; // } else { // return false; // }
// return i % 2 !=0 ? true : false;
return i % 2 != 0;
}
}
2. 根据行、列、字符 打印对应的行数和列数的字符,比如:行:4,列:4,字符 #,打印相应的效果
```java
public class MethodExercise01 {
public static void main(String[] args) {
AA a = new AA();
a.printChar(4,4,'#');
}
}
class AA {
public void printChar(int row,int column,char c) {
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
System.out.print(c + " ");
}
System.out.println();
}
}
}
3.7 成员方法传参机制
方法的传参机制对我们今后的编程非常重要,一定要搞的清清楚楚明明白白。
基本数据类型的传参机制
MethodParameter01.java
public class MethodParameter01 {
public static void main(String[] args) {
int a = 10;
int b = 20;
AA obj = new AA();
obj.swap(a,b);
System.out.println(" a=" + a + " b=" + b);
}
}
class AA {
public void swap(int a,int b) {
System.out.println("交换前 a=" + a + " b=" + b);
int temp = a;
a = b;
b = temp;
System.out.println("交换后 a=" + a + " b=" + b);
}
}
分析代码的运行流程及结果
结论:基本数据类型,传递的是值(值拷贝),形参的任何改变不影响实参!
引用数据类型的传参机制
MethodParameter02.java
public class MethodParameter02 {
public static void main(String[] args) {
/*
看一个案例
B类中编写一个方法 test100,可以接收一个数组,
在方法中修改该数组,看看原数组是否变化?
B类中编写一个方法 test200,可以接受一个Person(age,sal)对象,
在方法中修改该对象属性,看看原来的对象是否变化?
*/
int arr[] = {1,2,3};
B b = new B();
b.test100(arr); //调用方法
System.out.println("main中的arr数组");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
Person p1 = new Person();
p1.age = 10;
p1.sal = 100.8;
b.test200(p1);
System.out.println(p1.age);
}
}
class B {
public void test100(int[] a) {
a[0] = 100;
System.out.println("test100方法中的arr数组");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
}
public void test200(Person p) {
p.age = 50;
}
}
class Person {
int age;
double sal;
}
思考:如果 test200 中是 p = null; 或者 p = new Person(); main中 执行 b.test200(p1); 后会输出什么?
⏱ 小练习
编写一个方法copyPerson,可以复制一个Person对象,返回复制的对象。
克隆对象,注意要求新对象和原来的对象是两个独立的对象,只是它们的属性相同。
public class MethodExercise {
public static void main(String[] args) {
MyTools tool = new MyTools();
Person p1 = new Person();
p1.name = "Jack";
p1.age = 18;
Person p2 = tool.copyPerson(p1);
//到此 p1 和 p2 是两个独立的对象,下面验证
p1.name = "Bob";
p1.age = 20;
System.out.println("拷贝的新对象p2的属性 name=" + p2.name + " age=" + p2.age);
System.out.println("原对象p1修改后的属性 name=" + p1.name + " age=" + p1.age);
// 也可通过输出对象的 HashCode 看看对象是否是一个
}
}
class Person {
String name;
int age;
}
class MyTools {
//编写一个方法copyPerson,可以复制一个Person对象,返回复制的对象。
//克隆对象,注意要求新对象和原来的对象是两个独立的对象,只是它们的属性相同。
//
//编写方法的思路
//1. 方法的返回类型 Person
//2. 方法的名字 copyPerson
//3. 方法的形参 (Person p)
//4. 方法体,创建一个新对象,并复制属性,返回即可
public Person copyPerson(Person p) {
// 创建一个新的Person
Person p2 = new Person();
p2.name = p.name; // 把原来对象的name赋给p2
p2.age = p.age; // 把原来对象的age赋给p2
return p2;
}
}
四、方法递归调用
4.1 基本介绍
递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂问题,同时可以让代码变得简洁。
4.2 递归能解决什么问题?
- 各种数学问题:8皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子的问题(Google编程大赛)
- 各种算法中也会使用到递归,比如快速排序,归并排序,二分查找,分治算法等。
- 将用栈解决的问题 -> 递归代码比较简洁
4.3 递归调用机制
结合下面代码进行分析
public class Recursion01 {
public static void main(String[] args) {
T t = new T();
t.test(5);
}
}
class T {
public void test(int n) {
if (n > 2) {
test(n - 1);
}
System.out.println("n=" + n);
}
}
阶乘问题:结合上图试分析递归过程
public class Recursion02 {
public static void main(String[] args) {
T t = new T();
System.out.println(" = " + t.factorial(5));
}
}
class T {
public int factorial(int n) {
if (n == 1) {
System.out.print(n);
return 1;
} else {
System.out.print(n + " * ");
return factorial(n - 1) * n;
}
}
}
4.4 递归重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响,比如 n 变量
- 如果 方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
⏱ 小练习
请使用递归的方式求出斐波那契数1,1,2,3,5,8,13….给你一个整数n,求出第n个数的值是多少 ```java import java.util.Scanner; public class RecursionExercise01 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("请问要输出第几位斐波那契数?");
int n = scan.nextInt();
T t = new T();
System.out.println(t.resNum(n));
} }
class T { public int resNum(int n) { if (n > 0) { if (n == 1 || n == 2) { return 1; } else { return resNum(n - 1) + resNum(n - 2); } } else { return 0; } } }
2. 猴子吃桃问题:有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天都是如此,当到第10天时,想再吃时(即还没吃),发现只有1个桃子了。问:最初有多少个桃子?
```java
public class RecursionExercise02 {
public static void main(String[] args) {
Monkey monkey = new Monkey();
System.out.println(monkey.peach(1));
}
}
class Monkey {
public int peach(int day) {
if (day == 10) {
return 1;
} else {
return (peach(day + 1) + 1) * 2;
}
}
}
4.5 迷宫问题
- 小球得到的路径,和程序员设置的找路策略有关:即路的上下左右的顺序相关
- 再得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
- 测试回溯现象
扩展思考:如何求出最短路径?
思路:(1)穷举 (2)图
MiGong.java ```java public class MiGong {public static void main(String[] args) {
// 思路
// 1. 先创建迷宫,用二维数组表示
// 2. 先规定 map 数组的元素值:0表示可以走,1表示障碍物
int[][] map = new int[8][7];
// 3. 将最上面的一行和最下面的一行,全部设置为1
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
// 4. 将最右面的一列和最左面的一列,全部设置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
map[2][2] = 1; // 测试回溯
// 输出当前的迷宫地图
System.out.println("===== 当前地图 =====");
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();
}
// 使用findWay给老鼠找路
T t = new T();
t.findWay(map, 1, 1);
// 输出找到的路线
System.out.println("\n===== 找到的路线 =====");
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();
}
} }
class T { // 使用递归回溯的思想来解决老鼠出迷宫
// 解读
// 1. findWay方法就是专门来找出迷宫的路径
// 2. 如果找到,就返回 true,否则返回 false
// 3. map 就是二维数组,即表示迷宫
// 4. i,j就是老鼠的位置,初始化的位置为(1,1)
// 5. 因为我们是递归的找路,所以我们先规定 map数组各个值的含义
// 0 表示可以走 1 表示障碍物 2 表示已走过可以走 3 表示走过,但是是障碍物(死路)
// 6. 当 map[6][5] = 2 就说明找到通路,就可以结束,否则就继续找
// 7. 先确定老鼠找路的策略 下->右->上->左
public boolean findWay(int[][] map,int i,int j) {
if (map[6][5] == 2) { // 说明已经找到通路
return true;
} else {
if (map[i][j] == 0) { // 当前这个位置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 {
map[i][j] = 3;
return false;
}
} else { // map[i][j] = 1,2,3
return false;
}
}
}
// 修改找路策略,看看路径是否有变化
// 下->右->上->左 ==> 上->右->下->左
public boolean findWay2(int[][] map,int i,int j) {
if (map[6][5] == 2) { // 说明已经找到通路
return true;
} else {
if (map[i][j] == 0) { // 当前这个位置0,说明可以走
// 我们假定可以走通
map[i][j] = 2;
// 使用找路策略,来确定该位置是否真的可以走通
if (findWay2(map, i - 1, j)) { // 先走上
return true;
} else if (findWay2(map, i, j + 1)) { // 走右
return true;
} else if (findWay2(map, i + 1, j)) { // 走下
return true;
} else if (findWay2(map, i, j - 1)) { // 走左
return true;
} else {
map[i][j] = 3;
return false;
}
} else { // map[i][j] = 1,2,3
return false;
}
}
}
}
<a name="o87PE"></a>
#### 4.6 汉诺塔问题
**汉诺塔**(Tower of Hanoi),又称**河内塔**,是一个源于[印度](https://baike.baidu.com/item/%E5%8D%B0%E5%BA%A6/121904)古老传说的[益智玩具](https://baike.baidu.com/item/%E7%9B%8A%E6%99%BA%E7%8E%A9%E5%85%B7/223159)。[大梵天](https://baike.baidu.com/item/%E5%A4%A7%E6%A2%B5%E5%A4%A9/711550)创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令[婆罗门](https://baike.baidu.com/item/%E5%A9%86%E7%BD%97%E9%97%A8/1796550)把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。<br />[点击查看【codepen】](https://codepen.io/finnhvman/embed/gzmMaa)<br />**HanoiTower.java**
```java
public class HanoiTower {
public static void main(String[] args) {
Tower tow = new Tower();
tow.move(5, 'A', 'B', 'C');
}
}
class Tower {
// 方法
// num 表示要移动的个数,a, b , c分别表示A塔,B塔,C塔
public void move(int num, char a, char b, char c) {
// 如果只有一个盘 num = 1
if (num == 1) {
System.out.println(a + "->" + c);
} else {
// 如果有多个盘,可以看作两个,最下面的和上面的所有
// 1. 先移动上面所有的盘到 B,借助 C
move(num - 1, a, c, b);
// 2. 把最下面的这个盘,移动到C
System.out.println(a + "->" + c);
// 2. 再把 B塔的所有盘,移动到C,借助A
move(num - 1, b, a, c);
}
}
}
4.7 八皇后问题
八皇后问题,是个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击。即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
public class EightQueen {
public static void main(String[] args) {
// map[]表示棋盘,假设map[] = {2, 5, 7, 0, 4, 6, 1 ,3}
// 则 map[0] = 2 表示第一(层)行第三列放置第一颗皇后棋子
// map[1] = 5 表示第二(层)行第六列放置第二颗皇后棋子
// ......
// map[7] = 3 表示第八(层)行第四列放置第八颗皇后棋子
int[] map = new int[8];
Q q = new Q(); // 新建q对象
q.findWay(map,0); // 引用q对象的findWay方法,
//传入棋盘,第一颗棋子先放在第一层第一列
System.out.println(q.count); // 输出总方法数
}
}
class Q {
int count; // 用于记录成功摆放的次数 全局变量 默认初始值为0
public void findWay(int[] map, int queen) {
if (queen == 8) { // 如果八颗皇后全部放在了棋盘上
for (int i = 0; i < 8; i++) { //循环遍历数组
System.out.print((i + 1) + " " + (map[i] + 1) + "\t");
} // 将这次摆放结果输出
System.out.println();
count++; // 成功计数器+1
} else {
for (int i = 0; i < 8; i++) { // 找到这一层(行)可以放置的位置(列)
map[queen] = i;
if (isSafe(map, queen)) { // 如果当前位置可以放置
findWay(map, queen + 1); // 进入下一层尝试
// 下一层也会找当前层可以放置的位置(列),并再进入下一层
// 层层遍历,最终如果没有将八颗棋子都放在棋盘上,
// 由于不满足上面的if (queen == 8) 所以结果不会输出,等于舍弃了这次
// 直到找到可以将八颗棋子都放在棋盘上的结果,即map数组每一位都成功赋值
// 会将这次摆放结果输出,并寻找下一个正确的摆放结果
// 直至第一层每一列都尝试了,全部结果也就都找到了
}
}
}
}
public boolean isSafe(int[] map, int queen) {
for (int i = 0; i < queen; i++) {
// 这一层放置的位置依次与前面层放置的位置对比
// 由于这一次放置的是相对之前最下面的一层,所以肯定不会与之前在同一层
// map[i] == map[queen] 表示放置的位置与之前层的某个位置同列
// Math.abs(i - queen) == Math.abs(map[i] - map[queen]) 表示同斜线
// Math.abs() 表示取绝对值,保证结果大于零
// 这里可以借助等腰直角三角形理解
// 假设 到这一层 棋盘的结构如下 此时 queen = 2
// 第一层 { #, 0, 0, 0, 0, 0, 0, 0 } 这里 # 表示 map[0] = 0,queen = 0
// 第二层 { 0, 0, #, 0, 0, 0, 0, 0 } 这里 # 表示 map[1] = 2,queen = 1
// 第三层 { 0, 0, 0, #, 0, 0, 0, 0 } 这里 # 表示 map[2] = 3,queen = 2 当前
// 第二层的#和它正下方的0以及第三层(当前层)的# 组成了一个等腰直角三角形
// 由于i < queen时,i++ 所以当i = 1时
// map[i] - map[queen] = 2 - 3
// i - queen = 1 - 2
// Math.abs(i - queen) == Math.abs(map[i] - map[queen]) = 1
// 此时构成了等腰直角三角形 即当前层所在列和之前层所在列处在了同一斜线上
if (map[i] == map[queen] || Math.abs(i - queen) == Math.abs(map[i] - map[queen])) {
return false; // 同列或者同斜线都返回不能放置
}
}
return true; //不是同列或者同斜线,可以放置
}
}
五、方法重载(OverLoad)
5.1 基本介绍
Java种允许同一个类中,多个同名方法的存在,但要求 形参列表不一致!
比如:System.out.println(); out 是 PrintStream 类型
重载的好处:
- 减轻了起名的麻烦
- 减轻了记名的麻烦
5.2 案例演示
public class OverLoad01 {
public static void main(String[] args) {
MyCalculator mc = new MyCalculator();
System.out.println(mc.calculate(5,3));
System.out.println(mc.calculate(5,.3));
System.out.println(mc.calculate(.5,3));
System.out.println(mc.calculate(5,3,6));
}
}
class MyCalculator {
// 两个整数的和
public int calculate(int n1, int n2) {
return n1 + n2;
}
// 一个整数 一个double 的和
public double calculate(int n1, double n2) {
return n1 + n2;
}
// 一个double 一个整数 的和
public double calculate(double n1, int n2) {
return n1 + n2;
}
// 三个整数的和
public int calculate(int n1, int n2, int n3) {
return n1 + n2 + n3;
}
}
5.3 方法重载注意事项和细节
- 方法名:必须相同
- 参数列表:必须不相同(参数类型或个数或顺序,至少有一样不同,形参名无所谓)
- 返回类型:无要求
⏱ 小练习
判断题:
与 void show(int a, char b, double c) { } 构成重载的有 b c d e- void show(int x, char y, double z) { } // 二者相同
- int show(int a, double c, char b) { }
- void show(int a, double c, char b) { }
- boolean show(int c, char b) { }
- void show(double c) { }
- double show(int x, char y, double z) { } // 二者相同
- void shows() { } // 方法名不同
编写程序,类Methods中定义三个重载方法并调用。方法名为m。三个方法分别接收一个 int 参数、两个 int 参数、一个字符串参数。分别执行平方运算并输出结果、相乘并输出结果、输出字符串信息。在主类的main() 方法中分别用参数区别调用三个方法。
在 Methods 类,定义三个重载方法 max(),第一个方法,返回两个 int 值中的最大值,第二个方法,返回两个 double 值中的最大值,第三个方法,返回三个 double 值中的最大值,并分别调用三个方法。
public class OverLoadExercise {
public static void main(String[] args) {
Methods me = new Methods();
me.m(6);
me.m(5,4);
me.m("java is a game");
System.out.println(me.max(3,6));
System.out.println(me.max(.6,.8));
System.out.println(me.max(.3,.2,9)); // 可以运行,9 自动转成 double
// 但是如果再新建一个 double max(double x, double y, int z) 方法
// 会调用新的这个方法 新方法和原来的不会冲突,依然符合重载
}
}
class Methods {
public void m(int i) {
int j = 0;
j = i * i;
System.out.println(i + "的平方 = " + j);
}
public void m(int i, int j) {
int k = 0;
k = i * j;
System.out.println(i + " × " + j + " = " + k);
}
public void m(String x) {
System.out.println(x);
}
public int max(int i, int j) {
return i > j ? i : j;
}
public double max(double i, double j) {
return i > j ? i : j;
}
public double max(double x, double y, double z) {
double max = x > y ? x : y;
return max > z ? max : z;
}
}
六、可变参数
6.1 基本介绍
Java允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。通过可变参数技术实现。
基本语法:
访问修饰符 返回类型 方法名(数据类型… 形参名) {
……
}
6.2 案例演示
VarParameter01.java
public class VarParameter01 {
public static void main(String[] args) {
VarMethod var = new VarMethod();
System.out.println("所求的和" + var.sum(15,56,17));
}
}
class VarMethod {
// 可以计算2个数的和,3个数的和,4,5.....
// 可以使用方法重载
/*
public int sum(int n1, int n2) {
return n1 + n2;
}
public int sum(int n1, int n2, int n3) {
return n1 + n2 + n3;
}
public int sum(int n1, int n2, int n3, int n4) {
return n1 + n2 + n3 + n4;
}
// ......
*/
// 上面三个方法名称相同,功能相同,参数个数不同 -> 使用可变参数优化
// 解读
// 1. int... 表示接收的是可变参数,类型是int,即参数可以接收多个int(0-多)
// 2. 使用可变参数时,可以当作数组来使用 即 nums 可以当作数组
// 3. 遍历 nums 数组,求和即可
public int sum(int... nums) {
System.out.println("接收的参数个数:" + nums.length);
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}
6.3 可变参数注意事项和使用细节
- 可变参数的实参可以为0个或任意个
- 可变参数的实参可以为数组
- 可变参数的本质就是数组
- 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后
- 一个形参列表中只能出现一个可变参数
⏱ 小练习
有三个方法,分别实现返回姓名和两门课成绩(总分),返回姓名和三门课成绩(总分),返回姓名和五门课成绩(总分)。封装成一个方法。
VarParameterExercise.java
public class VarParameterExercise {
public static void main(String[] args) {
VarMethod var = new VarMethod();
var.showScore("小王", 66, 78.5);
var.showScore("小白", 56, 89.5, 98);
var.showScore("小真", 79, 86, 90.5, 99,100);
}
}
class VarMethod {
public void showScore(String name, double... score) {
double sum = 0;
for (int i = 0; i < score.length; i++) {
sum += score[i];
}
System.out.println(name + "的" + score.length + "门课总分为" + sum);
}
}
七、作用域
7.1 基本介绍
面向对象中,变量作用域是非常重要的知识点,相对来说不是特别好理解 ,请注意认真思考,深刻掌握变量作用域。
- 在 Java 编程中,主要的变量就是属性(成员变量)和局部变量。
- 我们说的局部变量一般是指在成员方法中定义的变量。
- Java中作用域的分类
全局变量:也就是属性,作用域为整个类体
局部变量:也就是除了属性之外的其他变量,作用域为定义它的代码块中 - 全局变量可以不赋值,直接使用,因为有默认值,局部变量必须赋值后,才能使用,因为没有默认值
7.2 案例演示
Scope01.java
public class Scope01 {
public static void main(String[] args) {
Cat cat = new Cat();
cat.cry();
cat.eat();
}
}
class Cat {
// 全局变量:也就是属性,作用域为整个类体
// 属性在定义时,可以直接赋值
int age = 10; // 可以不赋值,有默认值
public void cry() {
// 1. 局部变量一般是指在成员方法中定义的变量
// 2. n 和 name 就是局部变量
// 3. n 和 name 的作用域在 cry 方法中
int n = 10; // 必须赋值,因为没有默认值
String name = "Jack";
System.out.println("在 cry 方法中使用属性 age= " + age);
}
public void eat() {
System.out.println("在 eat 方法中使用属性 age= " + age);
// System.out.println("在 eat 方法中使用cry方法中的局部变量name" + name); // 会报错
}
}
7.3 作用域的注意事项和使用细节
- 属性和局部变量可以重名,访问时遵循就近原则。
- 在同一个作用域中,比如在同一个成员方法中,两个局部变量,不能重名。
- 属性声明周期较长,伴随着对象的创建而创建,伴随着对象的销毁而销毁。局部变量,生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的结束而销毁。即存在于一次方法调用中。
- 作用域范围不同:
全局变量/属性:可以被本类使用,或其他类使用(通过对象调用)
局部变量:只能在本类中对应的方法中使用 - 修饰符不同
全局变量/属性可以加修饰符(public,protected,private 等)
局部变量不可以加修饰符
八、构造方法/构造器
8.1 基本介绍
前面我们在创建对象时,是先把一个对象创建后,再给它的属性赋值
现在通过构造器,可以实现在创建对象同时,就可以指定这个对象的各个属性
构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。
基本语法
[ 修饰符 ] 方法名(形参列表) {
方法体;
}
8.2 构造器的说明和特点
- 构造器的修饰符可以默认
- 构造器没有返回值
- 方法名 和类名字 必须一样
- 参数列表 和 成员方法一样的规则
- 构造器的调用 由系统完成
即在创建对象时,系统会自动的调用该类的构造器完成对对象的初始化
8.3 案例演示
Constructor01.java
public class Constructor01 {
public static void main(String[] args) {
// 当我们new 一个对象时,直接通过构造器指定名字和年龄
Person p1 = new Person("Smith", 80);
System.out.println(p1.name);
System.out.println(p1.age);
}
}
// 在创建人类的对象时,就直接指定这个对象的年龄和姓名
class Person {
String name;
int age;
// 构造器
// 解读:
// 1. 构造器没有返回值,也不能写void
// 2. 构造器的名称和类Person一样
// 3. (String pName, int pAge) 是构造器形参列表,规则和成员方法一样
public Person(String pName, int pAge) {
System.out.println("构造器被调用");
name = pName;
age = pAge;
}
}
8.4 构造器的注意事项和使用细节
- 一个类可以定义多个不同的构造器,即构造器重载
比如:我们可以再给Person类定义一个构造器,用来创建对象的时候,只指定人名,不需要指定年龄 - 构造器名和类名要相同
- 构造器没有返回值
- 构造器是完成对象的初始化,不是创建对象
- 在创建时,系统会自动的调用该类的构造方法
- 如果程序员没有定义构造方法,系统会自动给类生成一个默认无参构造方法(也叫默认构造方法),比如 Person(){},使用 javap指令 反编译看看
- 一旦定义了自己的构造器,默认的构造器就覆盖了,就不能再使用默认的无参构造器,除非显式的定义一下,即:Person(){}
ConstructorDetail.java
public class ConstructorDetail {
public static void main(String[] args) {
Person p1 = new Person("King", 40); // 调用第一个构造器
System.out.println(p1.name + "\t" + p1.age);
Person p2 = new Person("Ling"); // 调用第二个构造器
System.out.println(p2.name + "\t" + p2.age);
}
}
class Person {
String name;
int age; // 默认0
// 第一个构造器
public Person(String pName, int pAge) {
name = pName;
age = pAge;
}
// 第二个构造器,只指定人名
public Person(String pName) {
name = pName;
}
}
⏱ 小练习
在前面定义的Person类中添加两个构造器:
第一个无参构造器:利用构造器设置所有人的age属性初始值都为18
第二个带pName和pAge两个参数的构造器:使得每次创建Person对象的同时初始化对象的age属性值和name属性值。分别使用不同 的构造器,创建对象。
8.5 对象创建流程分析
class Person{
int age = 90;
String name;
Person(String n, int a) {
name = n;
age = a;
}
}
Person p = new Person("小倩", 20);
流程分析:
- 加载Person类信息(Person.class),只会加载一次
- 在堆中分配空间(地址)
- 完成对象初始化
- 默认初始化
- 显式初始化
- 构造器初始化
- 把对象在堆中的地址返回给p(p是对象名,也可以理解成对象引用)
九、this 关键字
9.1 基本介绍
当对象被创建时,默认隐含一个this方法,this指向对象本身。可用于在对象的成员方法中调用对象的属性(即全局变量),以此和成员变量同名的局部变量区分开来。
9.2 案例演示
This01.java
public class This01 {
public static void main(String[] args) {
Dog dog1 = new Dog("小花", 5);
dog1.info();
}
}
class Dog {
String name;
int age;
// public Dog(String dName, int dAge) { // 构造器
// name = dName;
// age = dAge;
// }
// 如果我们构造器的形参,能够直接写成属性名,就更好了
public Dog(String name, int age) { // 构造器
// this.name 就是当前对象的属性name
this.name = name;
// this.name 就是当前对象的属性age
this.age = age; // 如果不加this,前后两个age都指的是局部变量形参age
}
public void info() { // 成员方法
System.out.println(name + "\t" + age + "\t");
}
}
9.3 this 的注意事项和使用细节
- this 关键字可以用来访问本类的属性、方法、构造器
- this 用于区分当前类的属性和局部变量
- 访问成员方法的语句:this.方法名(参数列表)
- 访问构造器语法:this(参数列表);注意只能在构造器中使用,即只能 构造器中访问其他构造器
且 对 this 的调用必须是构造器中的第一个语句
- this 不能再类定义外部使用,只能在类定义的方法中使用
ThisDetail.java
public class ThisDetail {
public static void main(String[] args) {
T t1 = new T();
t1.f2();
}
}
class T {
public void f1() {
System.out.println("f1() 方法...");
}
public void f2() {
System.out.println("f2() 方法...");
// 调用本类的 f1
// 第一种方式
f1();
// 第二种方式
this.f1();
}
}
⏱ 小练习
定义Person类,里面有 name、age 属性,并提供 compareTo 比较方法,用于判断是否和另一个人相等,名字和年龄完全一样,就返回 true ,否则返回 false
public class ThisExercise {
public static void main(String[] args) {
Person p1 = new Person("小明", 12);
Person p2 = new Person("小明", 15);
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 this.name.equals(p.name) && this.age == p.age; // 重点理解
}
}
十、本章作业
编写类 A01,定义方法 max,实现求某个 double 数组的最大值,并返回
编写类 A02,定义方法 find,实现查找某字符串数组中的元素查找,并返回索引,如果找不到,返回-1
编写类 Book,定义方法 updatePrice,实现更改某本书的,具体:如果价格 > 150,则改为150,如果价格 > 100,则改为100,否则不更改。
编写类 A03,实现数组的复制功能copyArr,输入旧数组,返回一个新数组,元素和旧数组一样
定义一个圆类 Circle,定义属性:半径,提供显示周长功能的方法,提供显示圆面积的方法
编程创建一个 Cale 计算类,在其中定义2个变量表示两个操作数,定义四个方法实现求和、差、乘、商(要求除数为0的时候,要给出提示) 并创建两个对象,分别测试
设计一个Dog类,有名字、颜色和年龄属性,定义输出方法 show() 显示其信息。并创建对象,进行测试、【提示 this.属性】
- 给定一个Java程序的代码如下所示,则编译运行后,输出结果是
public class Test {
int count = 9;
public void count1() {
count = 10;
System.out.println( “count1=” + count );
}
public void count2() {
System.out.println(“count1=” + count++);
}
public static void main(String args[]) {
new Test().count1(); // 匿名对象 输出 count1=10
Test t1 = new Test(); // 新 t1对象
t1.count2(); // 输出 count1=9
t1.count2(); // 输出 count1 = 10
}
定义 Music类,里面有音乐名 name、音乐时长 times 属性,并有播放 play 功能和返回本身属性信息的功能方法 getlnfo
试写出以下代码的运行结果
class Demo {
int i = 100;
public void m( ) {
int j = i++;
System.out.println(“i=” + i); // 101
System.out.println(“j=” + j); // 100
}
}
class Test {
public static void main(String[] args) {
Demo d1 = new Demo();
Demo d2 = d1;
d2.m( );
System.out.println(d1.i); // 101
System.out.println(d2.i); // 101
}
}
在测试方法中,调用method方法,代码如下,编译正确!试写出method方法的定义形式,调用语句为:System.out.printin( method( method( 10.0, 20.0), 100);
答案:[ public ] double method(double num1, double num2) { }创建一个Employee类,属性有(名字,性别,年龄,职位,薪水),提供3个构造方法,可以初始化
- (名字,性别,年龄,职位,薪水)
- (名字,性别,年龄)
- (职位,薪水)
要求充分复用构造器
public class Homework12 {
public static void main(String[] args) {
}
}
class Employee {
String name;
char gender;
int age;
String job;
double salary;
public Employee(String job, double salary) {
this.job = job;
this.salary = salary;
}
public Employee(String name, char gender, int age) {
this.name = name;
this.gender = gender;
this.age = age;
}
public Employee(String job, double salary, String name, char gender, int age) {
this(name, gender, age);
// this(job, salary); 不能再使用,因为不在首句
this.job = job;
this.salary = salary;
}
}
- 将对象作为参数传递给方法
题目要求:
- 定义一个 Circle 类,包含一个double型的radius属性代表圆的半径,一findArea( ) 方法返回圆的面积。
- 定义一个类 PassObject,在类中定义一个方法printAreas( ),该方法的定义如下:
public void printAreas(Circle c, int times)//方法签名
- 在printAreas方法中打印输出1到times之间的每个整数半径值,以及对应的面积。
例如,times为5,则输出半径1,2,3,4,5,以及对应的圆面积。
在main方法中调用printAreas()方法,调用完毕后输出当前半径值。程序运行结果如国所示
public class Homework13 {
public static void main(String[] args) {
Circle c = new Circle();
PassObject obj = new PassObject();
obj.printAreas(c,5);
}
}
class Circle {
double radius;
public double findArea() {
return Math.PI * radius * radius;
}
// 添加方法setRadius,修改对象的半径值
public void setRadius(double radius) {
this.radius = radius;
}
}
class PassObject {
public void printAreas(Circle c, int times) {
System.out.println("Radius\tArea");
for (double i = 1; i <= times; i++) {
c.setRadius(i);
// c.radius = i;
System.out.println(i + "\t" + c.findArea());
}
}
}
- 扩展题,学员自己做.
有个人Tom,设计他的成员变量,成员方法,可以电脑猜拳。
电脑每次都会随机生成0,1,2
0 表示石头 1 表示剪刀 2 表示布,并要可以显示Tom的输赢次数(清单)
import java.util.Random;
import java.util.Scanner;
public class Homework14 {
public static void main(String[] args) {
Tom t = new Tom();
// 用来记录最后输赢的次数
int isWinCount = 0;
// 创建一个二维数组,用来接收局数,Tom出拳情况以及电脑出拳情况
int[][] arr1 = new int[3][3];
int j = 0;
// 创建一个一维数组,用来接收输赢情况
String[] arr2 = new String[3];
Scanner scanner = new Scanner(System.in);
for (int i = 0; i < 3; i++) {
// 获取玩家出的拳
System.out.println("请输入你要出的拳头 (0-拳头, 1-剪刀, 2-布):");
int num = scanner.nextInt();
t.setTomGuessNum(num);
int tomGuess = t.getTomGuessNum();
arr1[i][j + 1] = tomGuess;
// 获取电脑出的拳
int comGuess = t.computerNum();
arr1[i][j + 2] = comGuess;
// 将玩家猜的拳与电脑作比较
String isWin = t.vsComputer();
arr2[i] = isWin;
arr1[i][j] = t.count;
// 对每一局的情况进行输出
System.out.println("========================================");
System.out.println("局数\t玩家出\t电脑出\t输赢情况");
System.out.println(t.count + "\t" + tomGuess + "\t" + comGuess + "\t" + isWin);
System.out.println("========================================");
System.out.println("\n\n");
isWinCount = t.winCount(isWin);
}
// 对游戏的最终结果进行输出
System.out.println("局数\t玩家的出拳\t电脑的出拳\t\t输赢情况");
for (int a = 0; a < arr1.length; a++) {
for (int b = 0; b < arr1[a].length; b++) {
System.out.print(arr1[a][b] + "\t\t");
}
System.out.print(arr2[a]);
System.out.println();
}
System.out.println("你赢了" + isWinCount + "次");
}
}
class Tom {
// 玩家出拳的类型
int tomGuessNum;
// 电脑出拳的类型
int comGuessNum;
// 玩家赢的次数
int winCountNum;
// 比赛的次数
int count = 1;
public void showInfo() {
//.....
}
/**
* 电脑随机生成猜拳的数字的方法
* @return
*/
public int computerNum() {
Random r = new Random();
comGuessNum = r.nextInt(3); // 方法 返回0-2的随机数
return comGuessNum;
}
/**
* 设置玩家猜拳数字的方法
* @param tomGuessNum
*/
public void setTomGuessNum(int tomGuessNum) {
if (tomGuessNum > 2 || tomGuessNum < 0) {
// 抛出异常
throw new IllegalArgumentException("数字输入错误");
}
this.tomGuessNum = tomGuessNum;
}
public int getTomGuessNum() {
return tomGuessNum;
}
/**
* 比较猜拳的结果
* @return 玩家赢返回true,否则返回false
*/
public String vsComputer() {
if (tomGuessNum == comGuessNum) {
return "平手";
} else if ((tomGuessNum + 1) % 3 == comGuessNum) {
return "你赢了";
} else {
return "你输了";
}
}
/**
* 记录玩家赢的次数
*/
public int winCount(String s) {
count++;
if (s.equals("你赢了")) {
winCountNum++;
}
return winCountNum;
}
}
学习参考(致谢):
- B站 @程序员鱼皮 Java学习一条龙
- B站 @韩顺平 零基础30天学会Java