# Java 面向对象编程
# Java简介
- Java 是一种以面向对象为核心的多范式编程语言
- Java 是基于 JVM 虚拟机的跨平台语言,一套代码多端部署
Java运行的原理和其他编程语言不同,它基于 JVM 的虚拟机运行:
先将 Java 源代码 (.java文件) 通过解释器 javac 生成字节码 (.class文件),这是一种二进制文件可以在 JVM 中执行,JVM 会把字节码逐行解释为机器指令并在本地机器执行。
所以不论任何操作系统只要有 JVM 就可以运行 Java 程序,从而实现跨平台。
Java 有三个平台,分别为:
- JavaSE:是Java标准版,提供了基础语言功能和核心类库,用于开发桌面应用程序、小型服务器应用
- JavaEE:基于JavaSE,用于构建大型、分布式、高并发的企业级应用
- JavaME:Java精简版,针对嵌入式设备的开发
# Java程序基础
# Java程序基本结构
这是一个最基本的 Java 程序
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!"); //输出 Hello, world!
}
}
因为 Java 是一门面向对象编程的语言,所以一个程序的基本单位是 class(类),跟在后面的 Hello 是这个类的类名
- 类名必须以英文字母开头,习惯以大写字母开头,之后可以是英文字母、数字和下划线的组合
public是访问修饰符,被public修饰的类、方法、变量可以在任何地方访问public类可以从不同包中导入使用,而没有public的私有类只能在同包内访问public static void main(String[] args)是程序入口,JVM 在启动时会寻找main方法作为程序起点main方法作为程序入口点,必须是public,否则 JVM 无法调用该方法启动程序String[] args是一个字符串数组,参数用于接收运行Java程序时传入的参数
# 注释
单行注释:
// 单行注释
多行注释:
/*
多行注释
注释内容
注释结束
*/
# 变量和数据类型
在 Java 中定义一个变量如下:
int x = 1;
该语句定义了一个整型 int 类型的变量,名称为 x,初始值为 1
# 变量的命名规则
- 可以使用字母、数字、
_、$命名,但变量名不能以数字开头 - 变量名区分大小写
- 不能使用 Java 关键字作为变量名
- 建议命名时遵循驼峰命名法
# 基本数据类型
- 整数类型:
byte、short、int、long - 浮点数类型:
float、double - 字符类型:
char - 布尔类型:
boolean
| 数据类型 | 字节数 |
|---|---|
byte | 1 Byte |
short | 2 Byte |
char | 2 Byte |
int | 4 Byte |
float | 4 Byte |
double | 8 Byte |
long | 8 Byte |
# 整数
以下是不同的整数类型所能表达的最大范围:
byte: -128 ~ 127short: -32768 ~ 32767int: -2147483648 ~ 2147483647long: -9223372036854775808 ~ 9223372036854775807
为了方便看数字的位数可以用下划线这样标注,当作为整数类型输出时下划线是不会输出的,输出:2000000000
int a = 2_000_000_000;
long 类型的数据结尾需要加 L,如果不加 L 会被当做 int 类型,这说明 int 类型的值是可以赋值给 long 类型的变量的
long b = 9000000000000000000L;
可以将变量的值用十六进制、二进制或其他进制表示,但输出时的值是按照十进制输出的
int x = 0xff0000; // 十六进制表示的16711680
int y = 0b1000000000; // 二进制表示的512
# 浮点数
对于 float 类型的数据,需要加上 f 后缀,输出时 f 不会被输出
float f1 = 3.14f;
用科学计数法表示
float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
使用 double 类型,值不需要加后缀 f
double d1 = 1.79308;
如果在 double 类型中加了 f 后缀就相当于将 float 值赋给 double 变量时,会发生自动类型提升。
这个过程中可能会出现一些潜在风险,例如
double d1 = 1.79308f;
System.out.println(d1);
执行这段代码输出结果为 1.7930799722671509 而并非是 1.79308,这是因为 1.79308f 是一个 float 类型字面量且转换为二进制时是无限循环小数,所以需要精度截断然后再赋值给 double 类型的变量
控制输出的浮点数位数可以通过格式化输出,%.3f 表示输出到小数点后三位
double value = 1.79308;
System.out.printf("%.3f%n", value); // 输出: 1.793
# 布尔类型
布尔类型 boolean 只有 true 和 false 两个值
boolean b1 = true;
boolean b2 = false;
boolean isGreater = 5 > 3; // 计算结果为true
int age = 12;
boolean isAdult = age >= 18; // 计算结果为false
# 字符类型
char 类型使用单引号 ',且仅有一个字符
char a = 'A';
引用类型
以上的这些数据类型 byte、short、int、long、float、double、char、boolean 都是基本类型,除此以外的其他类型都是引用类型
- 基本类型直接存储值本身,引用类型指向对象的内存地址
- 基本类型在栈内存中直接分配空间存储值,引用类型把引用对象的地址存在栈内存中,实际对象在堆内存中
- 基本类型的默认值为
0,引用类型的默认值为null - 在基本类型中可以直接使用
==比较值是否相等,引用类型中==只能比较引用地址,equals()比较对象内容
String 就是最常见的引用类型,用于表示字符串
String s = "hello";
变量 s 只存储一个地址,这个地址会指向字符串 "hello" 在内存中存放的位置
# 常量
定义变量的时候,如果加上 final 修饰符,这个变量就变成了常量,初始化后该常量就不可再次赋值
final double PI = 3.14;
提示
常量名通常全部大写
# var关键字
为了简化局部变量的声明,Java 10 引入的局部变量类型推断关键字 var,编译器会根据赋值语句自动推断出变量的类型
使用限制
- 只能用于局部变量声明
- 必须在声明时进行初始化
- 不能用于成员变量、方法参数或返回类型
例如,这段代码类型的名称长,写起来比较麻烦
StringBuilder sb = new StringBuilder();
可以统统使用 var ,编译器会自动判断变量的类型
var sb = new StringBuilder();
# 变量的作用域
变量的作用域仅限于它声明时所在的代码块(由括号 {} 包围的代码区域)中
# 运算
# 运算优先级
在 Java 的计算表达式中,运算优先级从高到低依次是:
| 优先级 | 运算符 |
|---|---|
| 1 | () |
| 2 | ! ~ ++ -- |
| 3 | * / % |
| 4 | + - |
| 5 | << >> >>> |
| 6 | & |
| 7 | | |
| 8 | += -= *= /= |
算数运算符
| 运算符 | 说明 |
|---|---|
+ | 加法 |
- | 减法 |
* | 乘法 |
/ | 除法 |
% | 取模 |
++ | 自增 |
-- | 自减 |
关系运算符
| 运算符 | 说明 |
|---|---|
== | 等于 |
!= | 不等于 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
逻辑运算符
| 运算符 | 说明 |
|---|---|
&& | 逻辑与 |
|| | 逻辑或 |
! | 逻辑非 |
位运算符
| 运算符 | 说明 |
|---|---|
& | 按位与 |
| | 按位或 |
^ | 按位异或 |
~ | 按位取反 |
<< | 左移 |
>> | 右移 |
>>> | 无符号右移 |
赋值运算符
| 运算符 | 说明 |
|---|---|
= | 赋值 |
+= | 加法赋值 |
-= | 减法赋值 |
*= | 乘法赋值 |
/= | 除法赋值 |
%= | 取模赋值 |
# 三元运算符
condition ? value1 : value2
condition 是运算条件,如果条件为真则返回值 value1 否则返回 value2,例如:
int max = (a > b) ? a : b;
# 类型自动提升与强制转型
类型自动提升:在表达式计算过程中,Java自动将范围较小的数据类型转换为范围较大的数据类型以避免数据丢失
byte、short、char在运算时会自动提升为int类型- 类型提升优先级顺序:
byte→short→int→long→float→double - 如果表达式中有
double,整个表达式提升为double - 如果表达式中有
float但没有double,整个表达式提升为float - 如果表达式中有
long但没有float或double,整个表达式提升为long
强制转型:是将一个数据类型显式转换为另一个数据类型的操作
一般计算时小范围数会自动提升为大范围的数,如果要将大范围数转换为更小范围的类型就可以使用强制类型转换
使用括号 () 将目标类型括起来,放在要转换的变量或表达式前面
int i = 12345;
short s = (short) i; // 12345
使用强制类型转换具有风险,可能得到错误结果,例如下面的这个例子:
int的两个高位字节直接被截掉了,仅保留了低位的两个字节,所以导致结果错误
public class Main {
public static void main(String[] args) {
int i1 = 1234567;
short s1 = (short) i1; // -10617
System.out.println(s1);
int i2 = 12345678;
short s2 = (short) i2; // 24910
System.out.println(s2);
}
}
# 浮点数运算误差
浮点数 0.1 在计算机中就无法精确表示,因为十进制的 0.1 换算成二进制是一个无限循环小数,无论使用 float 还是 double 都只能存储一个 0.1 的近似值,所以浮点数运算可能会产生误差
public class Main {
public static void main(String[] args) {
double x = 1.0 / 10;
double y = 1 - 9.0 / 10;
// 观察x和y是否相等:
System.out.println(x);
System.out.println(y);
}
}
由于浮点数存在运算误差,所以比较两个浮点数是否相等常常会出现错误的结果。可以通过判断两个浮点数之差的绝对值是否小于一个很小的数,如果小于就默认两个浮点数相等
double r = Math.abs(x - y);
if (r < 0.00001) {
// 可以认为相等
} else {
// 不相等
}
# 短路运算
&&(逻辑与短路运算符)
当左侧操作数为 false 时,整个表达式必然为 false,右侧操作数不会被计算
只有左侧为 true 时,才会计算右侧操作数
||(逻辑或短路运算符)
当左侧操作数为 true 时,整个表达式必然为 true,右侧操作数不会被计算
只有左侧为 false 时,才会计算右侧操作数
// 非短路运算示例
boolean flag = false;
// 即使flag为false,isTrue()仍会被调用
if (flag & isTrue()) {
System.out.println("执行");
}
// 短路运算示例
// isTrue()不会被调用,因为flag为false
if (flag && isTrue()) {
System.out.println("执行");
}
短路运算可以提高程序性能,使用时要注意右侧表达式在程序中的作用
# 字符串连接
在 Java 中可以使用 + 连接任意字符串和其他数据类型
// 字符串连接
public class Main {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "world";
String s = s1 + " " + s2 + "!";
System.out.println(s); // Hello world!
}
}
# 多行字符串
从 Java 13开始,字符串可以用""" """表示多行字符串
public class Main {
public static void main(String[] args) {
String s = """
关关雎鸠,
在河之洲。
窈窕淑女,
君子好逑
""";
System.out.println(s);
}
}
输出结果:
关关雎鸠,
在河之洲。
窈窕淑女,
君子好逑
# 数组
- 数组所有元素初始化为默认值,整型都是
0,浮点型是0.0,布尔型是false - 数组一旦创建,大小就不可改变
- Java数组通过从
0开始的整数索引访问元素
声明数组:
int[] array;
也可以
int array[];
创建数组:
array = new int[10]; // 创建长度为10的整型数组
声明并初始化:
int[] array = new int[10];
int[] array = {1, 2, 3, 4, 5}; // 直接初始化
也可以通过这种方式初始化数组
int[] array = new int[]{3,6,9};
# 多维数组
创建一个4×4的二维数组(Java 数组的索引是从 0 开始的)
// 创建二维数组
int[][] matrix = new int[3][3];
这是 Java 中初始化数组的标准写法,这种方式允许你将数组的声明和初始化分开进行
// 先声明数组变量
int[][] grid;
// 然后再进行初始化
grid = new int[][]{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
还可以使用这种简洁的写法,声明的同时直接初始化
// 初始化二维数组
int[][] grid = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
访问二维数组中的元素,以下代码访问了数组中第一行第二列的元素,并将其赋值给 value
int value = matrix[0][1];
# 数组长度
通过 length 属性可以获取该数组的长度
int[] array;
array = new int[]{1,2,3,4,5};
System.out.print(array.length); // 输出:5
# 输入与输出
这是一个简单的输出程序
public class Main {
public static void main(String[] args) {
int x = 100; // 定义int类型变量x,并赋予初始值100
System.out.print(x); // 打印该变量的值
}
}
与其他的程序设计语言不同,Java是通过 print 方法进行输出的,该方法属于 System 类,定义在 java.lang 包中,但我们写代码时不需要导入它,因为 java.lang 包是 Java 的默认导入包,所有类都会自动导入
输出方法除了 print 还有 println、printf,它们的区别如下
| 方法 | 自动换行 | 格式化 | 说明 | 用途 |
|---|---|---|---|---|
print | 否 | 否 | 输出后不换行,光标停留在最后一个字符之后 | 需要将多个内容连续输出在同一行时 |
println | 是 | 否 | 输出后自动换行,光标移动到下一行行首 | 输出独立的消息、日志记录,或希望每条输出独占一行时 |
printf | 否 | 是 | 支持格式化输出,可使用各种占位符和格式说明符 | 需要控制数字精度、对齐方式、日期格式等特定输出格式时 |
println 方法会在输出其参数后自动添加一个换行符 \n,使后续输出从新的一行开始。它可以不带参数使用,默认输出一个换行符
System.out.println("Hello,");
System.out.println("World!");
输出结果:
Hello,
World!
# 格式化输出
printf 方法用于格式化输出
String name = "Alice";
int age = 25;
double score = 95.5;
System.out.printf("Name: %s, Age: %d, Score: %.2f%n", name, age, score);
输出结果:
Name: Alice, Age: 25, Score: 95.50
占位符
| 占位符 | 含义 | 示例 | 输出结果 |
|---|---|---|---|
%s | 字符串 | System.out.printf("%s", "Hello"); | Hello |
%d | 十进制整数 | System.out.printf("%d", 10); | 10 |
%f | 浮点数 | System.out.printf("%.2f", 3.14159); | 3.14 |
%n | 换行符 | System.out.printf("Line1%nLine2"); | Line1 Line2 |
%c | 字符 | System.out.printf("%c", 'A'); | A |
%b | 布尔值 | System.out.printf("%b", true); | true |
%x | 十六进制整数(小写字母) | System.out.printf("%x", 255); | ff |
%X | 十六进制整数(大写字母) | System.out.printf("%X", 255); | FF |
%o | 八进制整数 | System.out.printf("%o", 8); | 10 |
%t | 日期/时间(需配合特定转换符,如 TY 表示年份) | System.out.printf("%tY", new Date()); | (输出当前年份) |
# 输入
Java 中获取用户输入主要有三种常见方式:使用 Scanner类、BufferedReader配合 InputStreamReader,以及 Console类。其中最常用的就是 Scanner 类
Scanner类来自 java.util 包,它提供了丰富的方法来读取不同类型的数据(如整数、浮点数、字符串等)
使用时我们需要先导入 java.util.Scanner
import java.util.Scanner;
创建 Scanner对象,关联标准输入流 System.in(java.io.InputStream 类的一个实例,用于从标准输入设备读取数据)
Scanner scanner = new Scanner(System.in);
调用 Scanner 对象的方法读取输入的数据,不同数据类型有不同的方法,例如 nextInt() 就是读取输入整数的方法
int num = scanner.nextInt();//读取整数
如果输入的是浮点数要用 nextDouble()
double num = scanner.nextDouble();
如果输入的是字符串 可以使用 nextLine() 或 next() 方法,next() 只接收空格前的字符串,遇到空格就截止了,而 nextLine() 接收整行字符串,运行下面两段代码方便输入 Hello World
String word = scanner.next();
System.out.println(word);
输出结果:
Hello
String line = scanner.nextLine();
System.out.println(line);
输出结果:
Hello World
使用完毕后,调用 close() 方法关闭 Scanner 对象
scanner.close();
# if条件判断
if语句
if (condition) {
// 条件为true时执行的代码
}
if-else语句
if (condition) {
// 条件为true时执行的代码
} else {
// 条件为false时执行的代码
}
if-else语句
if (condition1) {
// condition1为true时执行
} else if (condition2) {
// condition2为true时执行
} else {
// 所有条件都为false时执行
}
# switch多重选择
switch (expression) {
case value1:
// 当expression等于value1时执行的代码
break;
case value2:
// 当expression等于value2时执行的代码
break;
default:
// 当expression不匹配任何case时执行的代码
break;
}
- 每个
case后面通常需要break语句,没有break会继续执行下一个case - case 的值必须是常量表达式,且互不相同
- 当没有匹配的
case时执行default default分支可以在任意位置,但通常放在最后
使用 switch时,如果遗漏了 break 就会造成严重的逻辑错误,而且不易在源代码中发现错误。从 Java12 开始,switch 语句升级为更简洁的表达式语法
int x = 1;
String y = switch (x) {
case 1 -> "value1";
case 2 -> "value2";
case 3 -> "value3";
case 4 -> "value4";
case 5 -> "value5";
case 6 -> "value6";
case 7 -> "value7";
default -> "value";
};
System.out.println(y);
如果需要在 case 中执行更复杂的语句,可以把语句放到 {...} 里,然后,用 yield 返回一个值作为 switch语句的返回值
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
efault -> {
int code = fruit.hashCode();
yield code; // switch语句返回值
}
};
# while循环
while 循环会先判断条件是否成立,若结果为 true 则执行循环语句,若结果为 false 则跳出循环
while (条件表达式) {
...
}
# do while循环
先执行代码块中的代码,然后再判断 while 的条件是否满足,如果满足继续执行循环语句,如果不满足则跳出循环,执行之后的程序
do {
...
} while (条件表达式);
# for循环
for (初始化; 条件表达式; 更新表达式) {
// 循环体
}
for-each循环:
这是一种方便遍历数据的循环方式,适合当你只需要遍历并使用元素的值,而不需要索引位置时使用
for (元素类型 变量名 : 集合或数组) {
// 循环体
}
示例:
String[] names = {"张三", "李四", "王五"};
for (String name : names) {
System.out.println(name);
}
for (String name : names)表示遍历names数组中的每一个元素,每次循环将当前元素赋值给String类型的变量name
# break和contine
break语句用于终止循环,常在for、while、do-while、switch语句中使用continue语句用于跳过当前循环迭代的剩余部分,直接进入下一次循环迭代
# 面向对象编程
# 什么是面向对象?
顾名思义,面向对象编程可不是对着男/女朋友面对面编程
这里的面向和对象编程是一种编程范式(或者说是一种编程思想),它将现实世界中的事物抽象为程序中的对象,通过对象之间的交互来解决问题。
假设在 Java 里定义一个类 class,类就像一个模板,而对象就是这个模板下的具体实例,例如每一辆车都会有自己的特征,比如品牌,颜色,速度等,这些特征就是属性(也叫字段),类就是由这些属性(字段)所构成的模板
// 定义一个类
class Car {
// 属性(成员变量)
String brand;
String color;
int speed;
}
如果要在程序中使用这个定义好的类,就需要为该类编写构造方法,用于创建对象时初始化对象的状态
- 构造方法的名称必须与类名相同
- 构造方法没有返回类型
- 一个类可以有多个构造方法(构造方法重载)
// 构造方法
public Car(String brand, String color) {
this.brand = brand;
this.color = color;
this.speed = 0;
}
除了用于创建对象的构造方法,我们还要给创建好的对象设计其他的活动,例如启动汽车,或者显示汽车的当前速度
// 方法(行为)
//给对象定义一个启动的行为
public void start() {
System.out.println(brand + "汽车启动了");
}
//给对象定义一个显示当前速度的行为
public void accelerate(int increment) {
speed += increment;
System.out.println("当前速度: " + speed + " km/h");
}
现在已经设置好了类 Car,创建一个 Car 的对象,对象通过 new 关键字和构造方法来创建
Car myCar = new Car("丰田", "红色");
这段代码中 Car myCar 中的 Car 是类名,表示数据类型 myCar 是变量名,new Car("丰田", "红色") 中的 Car 是构造方法
一个类可以创建多个对象,每个对象都有自己独立的数据
// 同一个类可以创建多个不同的对象
Car car1 = new Car("丰田", "红色");
Car car2 = new Car("本田", "蓝色");
Car car3 = new Car("奔驰", "黑色");
创建好对象后就可以在该对象中使用之前设置的方法,例如在对象 car1 上使用 start() 方法
// 让对象 car1 使用 start() 方法
car1.start();
完整实例
class Car {
// 属性
String brand;
String color;
int speed;
// 构造方法
public Car(String brand, String color) {
this.brand = brand;
this.color = color;
this.speed = 0;
}
// 方法
public void start() {
System.out.println(brand + "汽车启动了");
}
public void accelerate(int increment) {
speed += increment;
System.out.println("当前速度: " + speed + " km/h");
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
// 创建对象
Car myCar = new Car("丰田", "红色");
myCar.start();
myCar.accelerate(50);
}
}
# 方法
- 方法是一段用来执行特定任务的代码块,它类似于其他编程语言中的函数
- Java 是一种面向对象的语言,所有的函数都必须在类中定义,因此被称为"方法"
- 方法与函数的区别在于:方法需要通过对象调用,而函数是直接调用
方法有三种类型: 实例方法:需要创建对象才能调用的方法
class Car {
// 属性
String brand;
String color;
int speed;
// 构造方法
public Car(String brand, String color) {
this.brand = brand;
this.color = color;
this.speed = 0;
}
// 实例方法
public void start() {
System.out.println(brand + "汽车启动了");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car("丰田", "红色");// 创建对象
myCar.start(); // 调用实例方法 start()
}
}
构造方法:用于创建和初始化对象,构造方法在创建对象时会自动调用,不能像普通方法那样直接调用
public class Car {
String color;
String brand;
int speed;
// 构造方法
public Car(String color, String brand) {
this.color = color;
this.brand = brand;
this.speed = 0;
}
}
- 如果 class 中没有编写构造方法,编译器会自动创建一个没有参数的空构造方法,如果自定义了一个构造方法,编译器就不再自动创建默认的构造方法
- 可以同时定义多个构造方法,但方法名必须相同,参数必须不同(类型、数量、顺序),这叫做构造方法重载
- 如果构造方法没有初始化字段,引用类型的字段默认是
null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false
静态方法:使用 static 关键字修饰,可以通过类名直接调用
public class MathUtils {
public static int multiply(int a, int b) {
return a * b;
}
}
int result = MathUtils.multiply(4, 5); // 直接通过类名调用
static 不仅可以用于静态方法,还可以用于静态字段
实例字段的每一个实例都是独立的,而静态字段是所有实例共享的,无论修改哪个实例的静态字段,其他实例的静态字段也会跟着变化。
以下面这段代码为例:修改的是对象 hong 的 number,但输出的 ming 的 number 也会随之变化。
public class Main {
public static void main(String[] args) {
Person ming = new Person();
Person hong = new Person();
ming.number = 88; // 修改对象ming的number
System.out.println(ming.number); // 输出ming的number
hong.number = 99; // 修改对象hong的number
System.out.println(ming.number); // 再次输出ming的number
}
}
class Person {
public static int number;
}
这是因为它们本质上指向的都是同一个 Person class 的静态字段,而并非是实例自己创建的字段。
静态字段属于类,而不是属于类的任何一个实例对象
# 访问修饰符
上面这段代码中的 public 是访问修饰符
访问修饰符用于控制类、方法、变量等成员的可见性和访问范围,以下是不同访问修饰符的访问范围
| 修饰符 | 同一类 | 同一包 | 子类 | 不同包 |
|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ✅ | ❌ |
| 默认 | ✅ | ✅ | ❌ | ❌ |
private | ✅ | ❌ | ❌ | ❌ |
合理使用这些访问修饰符有助于实现良好的封装性和代码安全性
# this变量
this 是一个引用变量,指向当前正在执行方法的对象实例
我们假设一个情景,当方法中的参数名与字段名冲突时怎么办 ?
public class Person {
private String name;
public void setName(String name) {
name = name; // 命名冲突
}
}
我们可以选择修改方法的参数名,也可以使用 this 来处理
public class Person {
private String name;
public void setName(String name) {
// 使用 this 区分同名的参数和实例变量
this.name = name;
}
}
调用当前类的同名构造函数:
这种设计可以避免了在两个构造函数中重复编写 this.name = name的代码,如果需要修改 name 的赋值逻辑,只需修改一个地方
public class Student {
private String name;
private int age;
// 在第一个方法初始化了 name
public Student(String name) {
this.name = name;
}
public Student(String name, int age) {
this(name); // 调用了第一个方法,完成 name 初始化
this.age = age;
}
}
作为方法参数传递:
b(this) 表示在 a 方法中引用 b方法,当调用a方法时同时也会除法b方法,并且将该对象自身作为参数传入
public class test {
public void a() {
// 将当前对象作为参数传递
b(this);
}
public void b(Calculator calc) {
// 处理逻辑
}
}
# 方法参数
在调用方法时要注意,必须按照方法中的参数定义填写参数
class Person {
...
public void setNameAndAge(String name, int age) {
...
}
}
Person a = new Person();
a.setNameAndAge("Xiao Ming"); // 编译错误:缺少一个参数
a.setNameAndAge(12, "Xiao Ming"); // 编译错误:参数数据类型不对,参数要按照定义时的顺序填写
可变参数是 Java5 引入的一个特性,它允许方法接受不定数量的参数,传入的参数相当于一个数组类型
class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}
也可以通过数组类型实现一个可变参数
class Group {
private String[] names;
public void setNames(String[] names) {
this.names = names;
}
}
但调用需要自己先构造 String[] 会有点麻烦
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"});
# 重载
在一个类中,可以同时定义多个同名的方法,这些方法的方法名相同但参数不同,这就是重载。 重载可以实现:使用同一个方法,当参数不同时执行的程序是不同的
class Hello {
public void hello() {
System.out.println("Hello, world!");
}
public void hello(String name) {
System.out.println("Hello, " + name + "!");
}
public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " + name + "!");
} else {
System.out.println("Hello, " + name + "!");
}
}
}
# 继承
通过继承可以使一个类(子类)获取另一个类(父类)的属性和方法
这段代码中有两个类 Person 和 Man,他们的属性和方法有很多是都是重复的,通过继承就可以减少这些重复的代码
class Person {
private String name;
private int age;
public String getName() {
...
}
public void setName(String name) {
...
}
public int getAge() {
...
}
public void setAge(int age) {
...
}
}
class Man {
private String name;
private int age;
private int score;
public String getName() {
...
}
public void setName(String name) {
...
}
public int getAge() {
...
}
public void setAge(int age) {
...
}
public int getScore() {
...
}
public void setScore(int score) {
...
}
}
通过 extends 关键字来实现继承。下面这段代码中 Man 继承了 Person
class Person {
private String name;
private int age;
public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}
class Man extends Person {
// 继承了 Person 的 name
// 继承了 Person 的 age
private int score;
// 继承了Person 的所有方法
public int getScore() { … }
public void setScore(int score) { … }
}
注意
- 私有方法(
private)和构造方法不能继承,被final修饰的方法可继承使用,但不能覆盖 - 若子类与父类中的字段名同名,在该子类继承时父类的该字段会被隐藏,子类原有的字段会覆盖在父类中继承的
- Java 中的所有类都继承自某个类,如果没有使用
extends继承,编译器会自动加上extends Object,只有Object特殊,它没有父类 - Java只允许一个
class继承自一个类,一个类有且仅有一个父类,一个父类可以有多个子类
# super()
super 是一个关键字,用于引用当前对象的直接父类
使用 super()可以在子类的构造方法中调用父类的构造方法
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // 调用父类构造方法,将 Dog 构造方法接收到的参数 name 传递到 Animal 的构造函数中
this.breed = breed;
}
}
注意
super() 必须是子类构造方法中的第一条语句
使用 super 调用父类的方法
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
super.makeSound(); // 先调用父类的方法
System.out.println("喵喵喵"); // 再添加子类特有的行为
}
}
当子类和父类有同名的成员变量时,使用 super 可以访问父类的变量
class Parent {
String message = "父类的消息";
}
class Child extends Parent {
String message = "子类的消息";
public void printMessages() {
System.out.println(super.message); // 父类的消息
System.out.println(this.message); // 子类的消息
}
}
用 sealed 修饰 class 并通过 permits 可以定义哪些子类可以继承该类
下面这段代码中只有 Rect、Circle、Triangle 可以继承 Shape
public sealed class Shape permits Rect, Circle, Triangle {
...
}
Java会自动向上转型,将子类对象转换为父类类型
class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃狗粮");
}
public void bark() {
System.out.println("汪汪叫");
}
}
// 向上转型
Animal animal = new Dog(); // 自动转型,子类转父类
animal.eat(); // 输出:"狗吃狗粮"(多态)
animal.bark(); // 编译错误:Animal 类没有 bark() 方法
向下转型:将父类对象转换为子类类型,需要显式强制转换,且存在风险
class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃狗粮");
}
public void bark() {
System.out.println("汪汪叫");
}
}
Animal animal = new Dog();
Dog dog = (Dog) animal; // 强制转换为 Dog 类型
dog.eat(); // 输出:狗吃狗粮
dog.bark(); // 输出:汪汪叫
# 多态
同一个接口,在不同类型的对象调用时会根据对象类型选择执行的方法
通常通过继承关系实现多态,例如下面这段代码,Dog 和 Cat 都继承自 Animal,他们都有 makeSound() 方法
- 对象
animal1是Dog类型,调用makeSound()方法时输出:"Dog barks" - 对象
animal2是Cat类型,调用makeSound()方法时输出:"Cat meows"
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Cat meows");
}
}
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // 输出: Dog barks
animal2.makeSound(); // 输出: Cat meows
@Override 是一个注解,它的主要作用是告诉编译器这个方法是要重写父类的方法,如果方法名或参数写错了,编译器会报错,帮助你发现错误。但是 @Override 不是必需的,没有它程序依然可以执行
# 抽象类
# 接口
# 包
在实际开发时我们可能会遇到以下问题:
- 自己定义的类和 JDK 中自带的类或第三方库中的类命名重复
- 遇到需要控制访问权限的场景
- 开发时需要将功能分成多个模块进行开发
使用包就可以解决这些问题,包的主要作用:
- 避免命名冲突:不同包中可以有相同名称的类
- 访问控制:提供包级别的访问权限
- 组织管理:将相关的类放在一块分类管理
包就像一个文件夹,类就像一个个独立的文件
- 每个文件都有且只有一个
public公共类,文件名要与公共类名称一致 - 同一个包内的类可以互相访问,但不同包的类不能访问,需要用
import导入,且只能访问包中的public公共类
我们先创建一个名为 utils 的包,在这个包中创建一个名为 Calculator 的公共类(公共类名与文件名同名)
.
├─ utils
│ └─ Calculator.java
└─ Main.java
这是包 utils 中的 Calculator 的代码,在这个类中我们创建了一个 add 方法用于求和。
我们必须要在文件的最顶端,用 package 关键字声明这个类属于哪个包
// utils/Calculator.java
package utils;
public class Calculator {
public static int add(int a, int b) {
return a + b;
}
}
现在我们在 Main 文件中调用包中类 我们需要先通过 import utils.Calculator; 将这个类引入,然后用 类名.方法名 的方式使用类中的方法 Calculator.add(a, b)
// Main.java
import java.util.Scanner;
import utils.Calculator;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
// 输入
int a = scanner.nextInt();
int b = scanner.nextInt();
// 调用Calculator类中的方法
System.out.println(Calculator.add(a, b));
} finally {
scanner.close();
}
}
}
不仅可以通过 import 导入类,还可以通过完整类名直接使用类
import java.util.Scanner;
// import utils.Calculator;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
// 输入
int a = scanner.nextInt();
int b = scanner.nextInt();
// 通过完整的类名调用
System.out.println(utils.Calculator.add(a, b));
} finally {
scanner.close();
}
}
}
# Java核心类
# 异常处理
在计算机程序中经常会遇到各种各样的错误,一个健壮的程序必须会处理遇到的各种各样的问题,我们可以使用 Java 内置了一套异常处理机制来处理
在Java中异常也是 class 它的继承关系如下:
┌───────────┐
│ Object │
└───────────┘
▲
│
┌───────────┐
│ Throwable │
└───────────┘
▲
┌─────────┴─────────┐
│ │
┌───────────┐ ┌───────────┐
│ Error │ │ Exception │
└───────────┘ └───────────┘
▲ ▲
┌───────┘ ┌────┴──────────┐
│ │ │
┌─────────────────┐ ┌─────────────────┐┌───────────┐
│OutOfMemoryError │... │RuntimeException ││IOException│...
└─────────────────┘ └─────────────────┘└───────────┘
▲
┌───────────┴─────────────┐
│ │
┌─────────────────────┐ ┌─────────────────────────┐
│NullPointerException │ │IllegalArgumentException │...
└─────────────────────┘ └─────────────────────────┘
Throwable是所有异常的父类Error:表示严重系统错误,如内存耗尽OutOfMemoryError,无法使用程序去处理异常Exception:程序可以处理的异常 编译器对RuntimeException及其子类不做强制处理的要求,是否处理要根据具体情况分析,其他Exception的子类若出现异常必须显式处理。
这是一段简单的异常处理的程序:
try {
// 可能出现异常的代码
int result = 10 / 0;
} catch (ArithmeticException e) {
// 处理算术异常
System.out.println("发现异常:" + e.getMessage());
} finally {
// 资源释放
}
try包裹可能出现异常的代码catch发现并处理特定类型的异常finally常用于资源释放,无论是否发生异常都会执行
可以使用多个 catch 语句,每个 catch 分别捕获对应的 Exception 及其子类。 JVM 在捕获到异常后,会从上到下匹配 catch 语句,匹配到某个 catch 后,执行 catch 代码块,然后不再继续往下匹配,这时 catch 的顺序就非常重要了,子类必须写在前面
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e) {
System.out.println("IO error");
} catch (UnsupportedEncodingException e) { // 永远捕获不到
System.out.println("Bad encoding");
}
}
UnsupportedEncodingException 异常是 IOException 的子类。当抛出 UnsupportedEncodingException 异常时,会被 catch (IOException e) { ... } 捕获并执行,这样一来程序会一直以 catch (IOException e) 处理异常,但实际上真正的异常是 UnsupportedEncodingException
finally 有几个特点:
finally语句不是必须的,可写可不写finally总是最后执行
throws 关键字用于给方法声明可能抛出的异常类型,我们不需要在该方法内部写如何处理异常的程序,当出现异常时异常会沿着调用链向上传递,直到被某个调用者处理。
就算没有声明 throws ,但异常仍然会向上抛,声明 throws 是为了告诉编译器这个方法可能会抛出某种异常,强制调用者必须处理这个异常,这时 Java 的安全检查机制
这是一个使用 throws 的例子,我们为 readFile 方法声明了一个异常 IOException ,当我们在主函数中调用 readFile 出现了异常,这个错误会由主函数中的程序处理,而不是由 readFile 方法处理
import java.io.*;
class SimpleFileReader {
// 声明throws:告诉调用者"我可能会出错,你要处理"
public String readFile(String filename) throws IOException {
// 简单读取文件,如果出错直接抛给调用者
FileReader reader = new FileReader(filename);
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
br.close();
return line;
}
}
public class SimpleExample {
public static void main(String[] args) {
SimpleFileReader reader = new SimpleFileReader();
try {
// 调用可能出错的方法
String content = reader.readFile("test.txt");
System.out.println("文件内容: " + content);
} catch (IOException e) {
// 在这里处理异常
System.out.println("出错了!原因: " + e.getMessage());
}
}
}
提示
getMessage() 是用来获取异常信息的方法,每个异常都有一个报错,getMessage() 就是获取这个消息的方法
throw 可以手动抛出一个异常,语法如下:
throw new ExceptionType();
ExceptionType() 可以是内置的异常类,如 Exception、RuntimeException 等,也可以是自定义的异常类,自定义的异常类通常继承于内置异常类
这时一个最简单的使用 throw 的示例:
try {
// 抛出一个异常
throw new RuntimeException("这是一个简单的异常示例");
} catch (RuntimeException e) {
// 捕获并处理异常
System.out.println("捕获到异常: " + e.getMessage());
}
# 自定义异常
在 Java 中想要自定义一个异常有三种途径:
- 直接继承
Throwable
public class DirectThrowableException extends Throwable {
public DirectThrowableException(String message) {
super(message);
}
}
- 继承
Exception(检查型异常)
public class CheckedCustomException extends Exception {
public CheckedCustomException(String message) {
super(message);
}
}
- 继承
RuntimeException(非检查型异常)
public class UncheckedCustomException extends RuntimeException {
public UncheckedCustomException(String message) {
super(message);
}
}
提示
自定义异常必须继承自 Throwable 类或其子类