@[toc]
Java
创建项目
Java项目的代码结构:
- 工程 project
- 模块 module
- 包 package
- 类 class
- 包 package
- 模块 module
- 创建项目
- 创建模块
- 创建包
- 创建类(类名不需要带后缀)
基础语法
注释
- 单行注释:
// 注释内容 - 多行注释:
/* 注释内容 */ - 文档注释:
/** 注释内容 */
字面量
定义:就是程序中能直接书写的数据,学这个知识的重点是:搞清楚lava程序中数据的书写格式。
字面量分类:
| 字面量类型 | 说明 | 示例 |
|---|---|---|
| 整数字面量 | 表示整数值,可以是十进制、八进制、十六进制或二进制形式。默认是int类型,可加L或l后缀表示long类型。 | 123(十进制)0123(八进制)0x7B(十六进制)0b1111011(二进制)200L(long类型) |
| 浮点数字面量 | 表示带小数的数值,可以是普通小数形式或科学计数法形式。默认是double类型,可加F或f后缀表示float类型。 | 3.14(double类型)3.14F(float类型)1.23e2(科学计数法) |
| 字符字面量 | 用单引号括起来的单个字符,可以是字母、数字、符号或转义字符。 | 'A'、'1'、'中'、'\n'(换行符)、'\t'(制表符) |
| 布尔字面量 | 表示逻辑真或假的值,只有两个取值。 | true、false |
| 字符串字面量 | 用双引号括起来的字符序列,可以包含任意字符,包括转义字符。 | "Hello World"、"Java"、"你好"、"\n"(换行字符串) |
| 空字面量 | 表示对象引用不指向任何对象,仅用于引用类型。 | null |
变量
定义格式:
📖知识扩展:比特
计算机中表示数据的最小单元。数据在计算机中的最小储存单元:字节(byte,简称B)是,一个字节占8个比特位(bit,简称b),因此1B=8b
进制计算
- 十进制转二进制的算法:除二取余法
- 二进制转十进制:8421法
- 八进制:每3位二进制作为一个单元,最小的是0,最大的是7,共8个数字:
- 十六进制:每4位二进制作为一个单元,最小数是0,最大数是15,共16个数字:
JaVa程序中支持书写二进制、八进制、十六进制的数据,分别需要以0B或者0b、0、0X或者0x开头。
数据类型
基本数据类型:4大类8种
注意事项:
- long类型:
- float类型:
关键字和标识符
关键字:
- Java 语法已经“占用”的单词,如
public、class、int、if……不能拿来当名字。
标识符:
- 程序员自己起的名字,如类名
HelloWorld、变量名age、方法名getSum……不能跟关键字重复,且必须遵守“字母/下划线/$开头,后接字母、数字、下划线或$”的规则。
标识符使用规则:
- 只能由 字母(A–Z、a–z、汉字等 Unicode 字母)、数字(0–9)、下划线
_和 美元符号$组成; - 第一个字符 不能是数字。
因此,以下都是合法标识符示例:
| 类别 | 合法举例 |
|---|---|
| 纯英文 | age MAX_VALUE $root _temp |
| 驼峰命名 | studentName getTotalScore |
| 常量风格 | PI CACHE_SIZE |
| 含 Unicode 字母 | 价格 变量① π(不推荐,但合法) |
不能出现的字符:空格、连字符 -、运算符 +*/、标点 . , ; : 等,也 不能跟 67 个关键字同名(如 int、class、true、null 等)。
方法
方法是一种用于执行特定任务或操作的代码块,代表一个功能,它可以接收数据进行处理,并返回一个处理后的结果。
方法的完整格式:
修饰符 返回值类型 方法名(形参列表){
方法体代码(需要执行的功能代码)
return 返回值;
}
例:
public static int add(int a, int b) { // 修饰符 返回类型 方法名(参数)
return a + b; // 方法体
}
// 调用
int sum = add(3,5); // sum = 8
⚠注意:
- 返回类型用
void表示“什么都不返回”。 - 参数列表可空
( ),也可多个(int x, double y)。 - 方法名遵循小驼峰,见名知义。
- 想复用就“调方法”,想灵活就“传参数”,想拿到结果就
return。
方法的其他形式:
-
方法
不需要接受参数:修饰符 void 方法名(形参列表){ 方法体代码(需要执行的功能代码) }如果方法没有返回结果,返回值类型必须声明
void// 调用 方法名()
方法重载
一个类中,出现多个方法的名称相同,但是它们的形参列表是不同的,那么这些方法就称为方法重载了。
重点:方法重载只关心方法名称相同,形参列表不同(类型不同,个数不同,顺序不同)。
return
无返回值的方法中可以直接通过单独的return;立即结束当前方法的执行。
类型转换
类型范围小的变量,可以直接赋值给类型范围大的变量。
例:
byte b = 10;
int i = b; // 自动转,OK
long L = i; // 自动转,OK
double d = L; // 自动转,OK
// 反方向不行,会编译报错
int x = 3.14; // ❌ double → int,必须强转
强类型转换
类型范围大的变量,不可以直接赋值给类型范围小的变量,会报错,需要强制类型转换过去
格式:
类型 变量2 = (类型) 变量1
⚠注意:
- 强制类型转换可能造成数据(丢失)溢出;
- 浮点型强转成整型,直接丢掉小数部分,保留整数部分返回
自动类型提升
在表达式中,小范围类型的变了,会自动转换成表达式中较大范围的类型,再参与运算。
⚠注意:
- 表达式的最终结果类型由表达式中的最高类型决定。
- 在表达式中,byte\short\char 是直接转换成
int类型再参与运算。
分支结构
if分支
根据条件的真或假,来决定执行某段代码。
结构:
if(条件表达式){
// 代码;
}
if-else分支:
if (条件表达式) {
// 代码1;
} else {
// 代码2;
}
多条件分支:
if (条件表达式) {
// 代码1;
} else if (条件表达式) {
// 代码2;
} else if (条件表达式) {
// 代码3;
} else {
// 代码4;
}
switch分支
是通过比较值是否相等,来决定执行哪条语句。
结构:
switch (变量) {
case 值1 -> 语句1;
case 值2 -> 语句2;
...
default -> 默认语句;
}
⚠注意:如果不编写break会出现穿透现象。
int month = 2;
switch (month) {
case 2:
case 3:
case 4:
System.out.println("春天");
break;
}
// 输出 "春天"
📖知识扩展:
if和switch的区别:
- if 是“范围题”,switch 是“单选题”
对比点 if / else if switch 判断类型 任意布尔表达式(>、<、&&、 都行) 只能对比单个值(==) 适合场景 区间、范围、复杂条件 固定几个候选值(如 1~7、A/B/C) 支持类型 所有类型 基本型+enum+String(Java7+) 写法 灵活但冗长 简洁、可读高 性能 差别极小,别纠结 现代 JVM 会优化,同样别纠结
for循环
for循环会自动重复那段代码。
结构:
for (初始化; 继续条件; 步进) {
// 循环体
}
例:从 1 数到 10,每数一次打印一次。
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
while循环
先判断,再执行;适合“不知道次数”的场景。
结构:
while (继续条件) {
// 循环体
}
do-while循环
先执行一次,再判断;至少跑一遍
结构:
do {
// 循环体
} while (继续条件)
循环嵌套
循环中又包含循环:
for(...){
for(...){
// ...
}
}
外部循环每循环一次,内部循环会全部执行完一轮。
最经典例子:打印矩形星号
for (int i = 1; i <= 3; i++) { // 外层:控制行
for (int j = 1; j <= 5; j++) { // 内层:控制列
System.out.print("*"); // 同一行连续打印
}
System.out.println(); // 换行
}
输出:
*****
*****
*****
执行顺序(想象秒表):
- 外层 i=1 → 内层 j 从 1 跑到 5 → 换行
- 外层 i=2 → 内层 j 再从 1 跑到 5 → 换行
- 外层 i=3 → 重复一次,结束。
死循环
死循环 = 停不下来的循环
条件永远为 true,程序一直转圈,除非手动停止或break。
3钟常见死循环:
-
while:while (true) { ... } -
for:for (;;) { ... } -
do-while:do { ... } while (true);
什么时候用死循环?
- 服务器(7×24 监听)
- 游戏引擎(不断刷新画面)
- 菜单(重复等待用户输入)
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请输入指令(q退出): ");
String cmd = sc.nextLine();
if ("q".equals(cmd)) {
break; // 用户敲 q 才结束
}
System.out.println("你输入了: " + cmd);
}
break 和 continue
break:跳出并结束当前所在循环的执行。
⚠注意:只能用于结束所在循环,或者结束所在switch分支的执行。
continue:用于跳出当前循环的当次执行,直接进入循环的下一次执行。
⚠注意:只能在循环中进行使用。
例:
while (条件) {
语句A;
if (xxx) break; // 直接跳出 while
语句B;
if (yyy) continue; // 回到条件判断,不再执行语句C
语句C;
}
一句话总结区别:
break直接掀桌子 —— 立刻退出整个循环,一去不回头。continue跳过当次 —— 只跳过本轮剩余语句,继续下一轮循环。
数组
数组是一个数据容器,可用来存储一批同类型的数据。
静态初始化数组
静态初始化数组就是在定义的时候就确定了数据。
完整版:
数据类型[] 数组名 = new 数据类型[] {元素1,元素2, ...}
例:
int[] arr = new int[] {10, 20, 30, 40, 50};
简化版(比较常用):
数据类型[] 数组名 = {元素1,元素2, ...}
数组访问:数组名[索引]
获取数组长度:数组名.length
动态初始化数组
只确定数组的类型和存储数据的容量,不事先存入具体的数据。
结构:
数据类型[] 数组名 = new 数据类型[长度]
添加数组元素:
数组名[长度] = 元素
二维数组
静态初始化:
数据类型[][] 数组名 = new 数据类型[][]{元素1,元素2,...}
动态初始化:
数据类型[][] 数据名 = new 数据类型[长度1][长度2]
访问二维数组:数据名[行索引][列索引]
添加数组元素:
面向对象
对象
对象是类的实例每个对象在堆内存中拥有独立的存储空间。
对象包含:
- 状态(State):由成员变量(字段)表示。
- 行为(Behavior):由方法(函数)表示。
- 标识(Identity):每个对象在 JVM 中有唯一地址(即使内容相同,也是不同对象)。
格式:
Student s1 = new Student("张三");
Student s2 = new Student("张三");
System.out.println(s1 == s2); // false,两个不同对象
成员变量
成员变量是在类中、方法外定义的变量,用于表示对象的状态(属性)。每个对象(实例)都有自己的一份成员变量副本(除非是 static 的)。
特点:
- 作用域:整个类都可见。
- 生命周期:随着对象的创建而存在,随着对象的销毁而消失。
- 可以有访问修饰符。
- 可以被
static修饰,变成类变量。
示例:
public class Student {
// 成员变量
private String name; // 实例变量
private int age;
public static String school = "清华大学"; // 静态成员变量(类变量)
}
成员方法
成员方法是在类中定义的、用于描述对象行为的函数。它通常用于操作成员变量或执行特定任务。
特点:
- 可以访问本类中的成员变量和其他成员方法。
- 可以有参数、返回值。
- 也可以被
static修饰,变成类方法(通过类名直接调用)。 - 同样可以有访问修饰符。
示例:
public class Student {
private String name;
private int age;
// 成员方法(实例方法)
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void introduce() {
System.out.println("我叫 " + name + ",今年 " + age + " 岁。");
}
// 静态成员方法(类方法)
public static void printSchool() {
System.out.println("学校:" + school);
}
}
对象生命周期
- 创建:
new关键字 → 在堆中分配内存 → 调用构造器初始化。 - 使用:通过引用变量调用方法或访问属性。
- 销毁:当对象不再被引用时,成为垃圾(Garbage),由 JVM 的**垃圾回收器(GC)**自动回收。
构造器
构造器用于创建对象时初始化对象的状态。它的名字必须和类名完全相同,且没有返回值类型(连 void 都不能写)
默认构造器
如果你没有写任何构造器,java 会自动提供一个无参的默认构造器。
public class Student {
// 编译器自动添加: Student(){}
}
自定义构造器
你可以定义带参数的构造器来初始化属性:
public class Student {
private String name;
private int age;
// 无参构造器
public Student() {}
// 有参构造器
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
// 使用
Student s1 = new Student(); // 调用无参构造器
Student s2 = new Student("张三", 18); // 调用有参构造器
✅ 构造器可以重载(多个构造器,参数不同)。
this关键字
this 代表当前对象的引用,常用于:
-
区分成员变量和局部变量(当参数名和成员变量名相同时):
public Student(String name) { this.name = name; // this.name 是成员变量,name 是参数(局部变量) } -
在构造器中调用其他构造器(必须是第一行):
public Student() { this("未知", 0); // 调用另一个构造器 } -
返回当前对象(较少用,用于链式调用):
public Student setName(String name) { this.name = name; return this; } // 使用:s.setName("李四").setAge(20);
⭕this调用兄弟构造器
在任意类的构造器中,是可以通过this()区调用该类的其他构造器。
public class Student {
private int id;
private String name;
private int age;
/* 1. 全参构造器:终极入口 */
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
/* 2. 两参构造器:调用兄弟构造器补默认值 */
public Student(int id, String name) {
this(id, name, 18); // 把 age 默认成 18
}
/* 3. 无参构造器:继续套娃 */
public Student() {
this(0, "匿名"); // 再往前套一层
}
}
⚠注意:super()和this()必须写在构造器的第一行,并且两者不能同时出现。
权限修饰符
Java 的权限修饰符(Access Modifiers)用来控制类、接口、变量、方法、构造器的可见性范围。
按“从宽到严”依次是:
-
public → 全局可见
// 文件:com/foo/Util.java package com.foo; public class Util { public static void hello() { System.out.println("hello"); } } // 文件:com/bar/Main.java package com.bar; import com.foo.Util; public class Main { public static void main(String[] args) { Util.hello(); // 任何地方都能调到 } } -
protected → 同包 + 子类可见
package com.foo; public class Father { protected void say() {} } // 同包非子类 package com.foo; class Neighbor { void test() { new Father().say(); // ✅ 同包可见 } } // 不同包子类 package com.bar; import com.foo.Father; class Son extends Father { void test() { say(); // ✅ 子类内部可见 new Father().say(); // ❌ 不同包非子类视角 } } -
(default) → 仅同包可见(不写任何修饰符)
package com.foo; class Hidden { // 不写修饰符,包私有 void foo() {} } package com.bar; class Outsider { void test() { // new Hidden(); // ❌ 不同包完全不可见 } } -
private → 仅本类内部可见
public class Counter { private int count = 0; public void inc() { count++; // 本类内部随便用 } private void reset() { // 连子类都看不到 count = 0; } }
速查表(✅ = 可见,❌ = 不可见)
范围 public protected (default) private 本类 ✅ ✅ ✅ ✅ 同包其他类 ✅ ✅ ✅ ❌ 不同包子类 ✅ ✅ ❌ ❌ 不同包非子类 ✅ ❌ ❌ ❌
封装
封装 是将对象的属性和行为包装起来,并通过访问控制(如 private)隐藏内部细节,只暴露必要的接口(如 public 方法)。目的就是为了提高安全性(防止非法访问)和提高可维护性(内部修改不影响外部使用)。
封装步骤:
- 将成员变量设为
private - 提供
public的getter和setter方法
public class Person {
private String name;
private int age;
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
}
}
}
实体类
实体类是专门用来封装数据的类。
例如:
public class User {
// User就是实体类
private String username;
private String password;
public User() {} // 无参构造器
// getter 和 setter
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
静态变量
用 static 修饰的成员变量,也叫 类变量。
🔍 什么是静态变量?
- 用
static关键字修饰的变量属于类本身,而不是类的某个对象。 - 所有对象共享同一个静态变量。
- 在类加载时就分配内存,程序结束才释放。
public class Counter {
public static int count = 0; // 静态变量
public Counter() {
count++; // 每创建一个对象,count 加 1
}
}
// 使用
new Counter();
new Counter();
System.out.println(Counter.count); // 输出 2
所有对象共享同一个值,在类加载的时候初始化,通过类名.变量名来进行访问。
静态方法
用 static 修饰的方法,也叫 类方法。
它的最大特点是:不需要创建对象,直接通过类名就能调用!
不能访问非静态成员(因为非静态属于对象,而静态方法不依赖对象),常用于工具方法。
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
// 使用
int sum = MathUtils.add(3, 5); // 不需要创建对象!
❗ 注意:static 方法中不能使用 this 和 super。
继承
继承允许一个类(子类)获得另一个类(父类)的属性和方法,实现代码复用。使用extends关键字:
class Animal {
protected String name;
public void eat() {
System.out.println(name + " 在吃东西");
}
}
class Dog extends Animal {
public void bark() {
System.out.println(name + " 在汪汪叫");
}
}
// 使用
Dog dog = new Dog();
dog.name = "旺财";
dog.eat(); // 继承自 Animal
dog.bark(); // 自己的方法
关键点:
-
Java只支持单继承(一个类只能有一个直接父类)
-
子类可以重写父类方法
-
构造子类对象时,会先调用父类构造器(默认调用
super())class Dog extends Animal { public Dog(String name) { super(); // 调用父类无参构造器(可省略) this.name = name; } // 或者 public Dog(String name) { super(); // 必须在第一行 this.name = name; } }
子类构造器
特点:子类的全部构造器,都会先调用父类的构造器,再调用自己。
子类构造器是如何实现调用父类构造器的:
- 默认情况下,子类全部构造器的第一行代码都是
super()(写不写都有),它会调用父类的无参数构造器。
多态
多态字面意思是“多种形态”。在 Java 中,它指的是:同一个方法调用,在不同对象上会产生不同的行为。
举个生活中的例子: 你按“开机键”,对电脑来说是开机,对电视来说是打开电视,对空调来说是启动制冷——同一个动作(开机),不同对象(电脑/电视/空调)做出不同的反应。这就是多态!
⭕多态的前提条件(必须同时满足):
- 继承(或实现接口)
- 方法重写(子类重写父类的方法)
- 父类引用指向子类对象(这是关键!)
举个例子:
// 父类
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类1
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪!");
}
}
// 子类2
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵~");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 父类引用指向子类对象(多态的核心写法!)
Animal a1 = new Dog(); // 实际是 Dog 对象
Animal a2 = new Cat(); // 实际是 Cat 对象
a1.makeSound(); // 输出:汪汪!
a2.makeSound(); // 输出:喵喵~
}
}
关键点解析:
Animal a1 = new Dog();- 编译时类型:
Animal(左边) - 运行时类型:
Dog(右边)
- 编译时类型:
- 调用
makeSound()时,实际执行的是子类重写后的方法(不是父类的!) - 这就是 “编译看左边,运行看右边” 的经典口诀!
✅多态的好处:
-
代码灵活、可扩展:比如以后加一个
Bird类,只要继承Animal并重写makeSound(),不用改主程序!class Bird extends Animal { @Override public void makeSound() { System.out.println("叽叽~"); } } -
便于维护和解耦:你只需要面向父类编程,不用关心具体是哪个子类。
⚠️ 注意事项:
- 成员变量没有多态! 如果父类和子类有同名变量,访问的是编译时类型(即左边的类型)的变量。
- 静态方法也没有多态! 静态方法属于类,不是对象,调用时看的是引用类型(左边)。
- 多态下不能调用子类独有方法!
多态类型转换
- 向上转型(子 → 父)
语法:
Parent p = new Child();特点:自动完成,一定成功,但只能调用父类声明的方法(除非子类重写了)。 - 向下转型(父 → 子)
语法:
Child c = (Child) p;特点:必须显式强转,可能失败(运行期抛ClassCastException),强转前用instanceof判断。
在强转前,建议使用instanceof关键字进行判断当前对象的真实类型,在进行强转。
变量名 instanceof 类型
class Animal {
void eat() { System.out.println("animal eat"); }
}
class Cat extends Animal {
@Override
void eat() { System.out.println("cat eat fish"); }
void climb() { System.out.println("cat climb tree"); }
}
public class Demo {
public static void main(String[] args) {
/* 1. 向上转型:自动、安全 */
Animal a = new Cat(); // a 的编译类型 = Animal,运行类型 = Cat
a.eat(); // 动态绑定 → cat eat fish
// a.climb(); // 编译错误:Animal 没有 climb()
/* 2. 向下转型:先判断再强转 */
if (a instanceof Cat) { // 运行期检查“真实对象”是不是 Cat
Cat c = (Cat) a; // 安全通过
c.climb(); // 现在能调用子类独有方法
}
/* 3. 错误示例:类型不符 */
Animal dog = new Animal();
// Cat wrong = (Cat) dog; // 运行期抛 ClassCastException
}
}
单例类
final关键字
作用是将声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
-
修饰普通变量(局部变量或者成员变量):
public class Example { public static void main(String[] args) { final int x = 10; // x = 20; ❌ 编译错误!不能修改 final 变量 System.out.println(x); // 输出:10 } } -
修饰成员变量:必须在声明时赋值,或在构造器中赋值(只能选一种方式,且只能赋一次)。
public class Person { private final String name; // 声明但未赋值 public Person(String name) { this.name = name; // ✅ 可以在构造器中赋值 } // name 之后不能再改! } -
final修饰方法不能被子类重写:
class Animal { public final void sleep() { System.out.println("动物睡觉"); } } class Dog extends Animal { // @Override // public void sleep() { } ❌ 编译错误!不能重写 final 方法 } -
final修饰类不能被继承:
final class MathUtils { public static int add(int a, int b) { return a + b; } } // class MyMath extends MathUtils { } ❌ 编译错误!不能继承 final 类 -
常用与
static连用:定义全局常量的标准写法public class Constants { public static final double PI = 3.14159; public static final String APP_NAME = "MyApp"; } // 调用:System.out.println(Constants.PI); // 3.14159
常量
使用static final修是的成员变量就被称为常量。作用是用于记录系统的配置信息。
⚠注意:常量名称的命名规范是全大写英文单词,多个单词通过下划线连接。
单例模式
作用:确保某个类只能创建一个对象。
实现步骤:
-
私有化构造器:确保单例类对外不能创建太多对象。
private 类名(){} -
定义一个静态变量:用于记住本类的一个唯一对象
public static final 类名 对象名称 = new 类名() // 或者私有化 private static 类名 对象名称 = new 类名() -
定义一个类方法:用于返回这个类的唯一对象
public static 类名 静态方法名(){ return 对象名称 } // 通过 类名.静态方法() 来调用。
**饿汉式:**类一加载,就先把实例创建好了
- 优点:简单、天然线程安全
- 缺点:如果一直没用到这个对象,会浪费一点内存(但对初学者完全不是问题)
例子:
public class Singleton {
// 1. 在类内部创建唯一实例(static 表示属于类,只有一份)
private static Singleton instance = new Singleton();
// 2. 私有构造方法:防止外部用 new 创建对象
private Singleton() {
// 空着就行,什么都不用写
}
// 3. 提供一个公共方法,让别人能拿到这个唯一实例
public static Singleton getInstance() {
return instance;
}
// 示例功能:打印一句话
public void showMessage() {
System.out.println("我是唯一的 Singleton 实例!");
}
}
使用:
public class Main {
public static void main(String[] args) {
// 不能这样写:Singleton s = new Singleton(); // ❌ 编译错误!构造方法是 private
// 正确方式:通过 getInstance() 获取
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
s1.showMessage(); // 输出:我是唯一的 Singleton 实例!
// 验证是不是同一个对象
System.out.println(s1 == s2); // 输出:true(说明确实是同一个)
}
}
**懒汉式:**使用对象时,才会开始创建对象。
- 好处:节省内存(如果一直没用,就不创建)
- 注意:基础版懒汉式在多线程下不安全,但我们先不考虑多线程(你还没学到),只关注单线程下的逻辑
例子:
public class LazySingleton {
// 1. 先不创建实例,初始为 null
private static LazySingleton instance = null;
// 2. 私有构造方法,防止外部 new
private LazySingleton() {
// 空着就行
}
// 3. 提供获取实例的方法:用到时才创建
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton(); // 第一次调用时才创建
}
return instance;
}
// 示例方法
public void doSomething() {
System.out.println("懒汉单例正在工作...");
}
}
使用:
public class Main {
public static void main(String[] args) {
// 第一次调用:创建对象
LazySingleton s1 = LazySingleton.getInstance();
s1.doSomething(); // 输出:懒汉单例正在工作...
// 第二次调用:直接返回已有对象
LazySingleton s2 = LazySingleton.getInstance();
System.out.println(s1 == s2); // 输出:true(是同一个对象)
}
}
枚举类
枚举是一种特殊的类,用来表示一组固定的常量值。
比如:
- 一周的星期:
MONDAY,TUESDAY, ...,SUNDAY- 季节:
SPRING,SUMMER,AUTUMN,WINTER- 订单状态:
PENDING,SHIPPED,DELIVERED,CANCELLED这些值是有限的、确定的、不会变的,就非常适合用枚举。
你可能会想:
“我直接用
"MONDAY"或数字1表示星期不行吗?”
但这样有风险:
- 容易拼错:
"Mondy"❌ - 语义不清:
status = 2是什么意思? - 编译器无法检查合法性
而枚举是类型安全的:只能用预定义的几个值,写错了编译都通不过!
语法:
修饰符 enum 枚举类名{
名称1,名称2,...;
其他成员...
}
使用:
public class Main {
public static void main(String[] args) {
// 声明一个 Day 类型的变量
Day today = Day.MONDAY;
// 可以比较(用 ==,安全!)
if (today == Day.MONDAY) {
System.out.println("今天是周一,加油!");
}
// 打印枚举值
System.out.println("今天是:" + today); // 输出:今天是:MONDAY
// 遍历所有枚举值
for (Day d : Day.values()) {
System.out.println(d);
}
}
}
例子:
// 定义一个表示星期的枚举
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
⚠ 注意:枚举值默认是
public static final的,而且全部大写是惯例。
特点:
- 枚举类都是最终类,不可以被继承,枚举类都是继承
java.lang.Enum类的。 - 枚举类的第一行只能罗列一些名称,这些名称都是变了,并且每个常量都会记住枚举类的一个对象。
- 枚举类的构造器都是私有的,因此枚举类对外不能创建对象。
抽象类
抽象类是用 abstract 关键字修饰的类,它不能被直接实例化(不能用 new 创建对象),通常用来作为其他类的“模板”或“基类”。
💡 类比: 抽象类就像“水果”这个概念——你可以有“苹果”“香蕉”,但不能说“给我一个水果”(因为“水果”太抽象了,不是一个具体的东西)。
❓为什么需要抽象类
现实世界中,有些类本身就是没有实际意义的,只是为了定义通用行为,让子类可以实现具体细节。
比如:
- 动物会“叫”,但“动物”本身怎么叫?不知道!
- 具体到“狗”是“汪汪”,“猫”是“喵喵”——这些由子类决定。
✅抽象类写法
-
定义抽象类和抽象方法:
// 抽象类 abstract class Animal { // 普通方法(可以有方法体) public void sleep() { System.out.println("动物在睡觉"); } // 抽象方法(没有方法体,用 abstract 修饰) public abstract void makeSound(); // 子类必须实现这个方法! } -
子类继承抽象类,并实现抽象方法:
class Dog extends Animal { @Override public void makeSound() { System.out.println("汪汪!"); } } class Cat extends Animal { @Override public void makeSound() { System.out.println("喵喵~"); } } -
使用:(不能直接创建对象new Animal,因为抽象类不能被实例化)
public class Main { public static void main(String[] args) { // Animal a = new Animal(); ❌ 编译错误!抽象类不能实例化 Animal dog = new Dog(); // ✅ 可以用父类引用指向子类对象(多态) dog.makeSound(); // 输出:汪汪! dog.sleep(); // 输出:动物在睡觉 } }
模板方法模式
提供一个方法作为完成某类功能的模板,模板方法封装了每个实现步骤,但允许子类提供特定步骤的实现。可以提高代码复用、并简化子类设计。
⭕举个例子:
做饮料的通用流程是:
- 烧水
- 冲泡(咖啡 or 茶) ← 这一步不同
- 倒进杯子
- 加调料(糖/牛奶) ← 这一步也可能不同
这个流程就是“模板”,而“冲泡”和“加调料”是可变的步骤。
代码实现:
-
定义抽象父类(模板):
// 抽象类:定义做饮料的模板 abstract class Beverage { // 模板方法:final 防止子类修改流程顺序 public final void prepare() { boilWater(); // 烧水 brew(); // 冲泡(子类实现) pourInCup(); // 倒进杯子 addCondiments(); // 加调料(子类实现) } // 共同步骤:父类直接实现 void boilWater() { System.out.println("烧开水..."); } void pourInCup() { System.out.println("倒入杯中..."); } // 不同步骤:交给子类实现(抽象方法) abstract void brew(); // 冲泡 abstract void addCondiments(); // 加调料 }✅
prepare()是模板方法,用final保证流程不能被改。 -
子类实现具体步骤:
// 做咖啡 class Coffee extends Beverage { @Override void brew() { System.out.println("用沸水冲泡咖啡粉..."); } @Override void addCondiments() { System.out.println("加糖和牛奶..."); } } // 做茶 class Tea extends Beverage { @Override void brew() { System.out.println("用沸水泡茶叶..."); } @Override void addCondiments() { System.out.println("加柠檬..."); } } -
使用:
public class Main { public static void main(String[] args) { System.out.println("=== 制作咖啡 ==="); Beverage coffee = new Coffee(); coffee.prepare(); // 调用模板方法 System.out.println("\n=== 制作茶 ==="); Beverage tea = new Tea(); tea.prepare(); } } -
输出:
=== 制作咖啡 === 烧开水... 用沸水冲泡咖啡粉... 倒入杯中... 加糖和牛奶... === 制作茶 === 烧开水... 用沸水泡茶叶... 倒入杯中... 加柠檬...
接口类
接口定义了"一个类能做什么",但不关心具体做了什么。
接口用来被类实现的,实现接口的类称为实现类,一个类可以同时实现多个接口。
✅接口的写法
-
定义接口:
// 接口用 interface 关键字定义 public interface Flyable { // 接口中的方法默认是 public abstract(可以省略) void fly(); // 所有实现类必须实现这个方法 } -
类实现接口:(用implements)
public class Bird implements Flyable { @Override public void fly() { System.out.println("小鸟在天空飞翔!"); } } public class Airplane implements Flyable { @Override public void fly() { System.out.println("飞机起飞了!"); } } -
使用接口:
public class Main { public static void main(String[] args) { Flyable f1 = new Bird(); Flyable f2 = new Airplane(); f1.fly(); // 小鸟在天空飞翔! f2.fly(); // 飞机起飞了! } }
特点:
- 接口不能创建对象
- 一个类可以实现多个接口
- 接口可以继承接口,且支持"多继承"
⚠注意事项:
-
一个接口继承多个接口,如果接口中方法签名冲突,此时不支持多继承,也不支持多实现:
interface A { void show(); } interface B { String show(); } interface C extends A,b{ // 报错... } -
一个类继承父类,又同时实现了接口,如果父类中和接口中有同名方法,实现类会先用父类
// 1. 接口 interface Greet { void say(); } // 2. 父类——已经给了具体实现 class Father { public void say() { System.out.println("Father 说:嗨"); } } // 3. 子类:继承 Father 同时实现 Greet // 因为 Father 已有具体实现,所以不强制重写 class Son extends Father implements Greet { } // 4. 测试 public class Demo { public static void main(String[] args) { Greet g = new Son(); // 向上转型为接口类型 g.say(); // 到底用谁?——用 Father 的! } } -
一个类实现多个接口,如果多个接口中存在同名的默认方法,可以不冲突,这个类重写方法即可
案例:"支付通道"
需求:系统要支持微信\支付宝\银行卡三种支付,未来还能扩展
-
先定义接口:
public interface Payment { /** * 支付 * @param cents 金额,单位分 * @return true 成功 */ boolean pay(long cents); } -
三种实现:
public class WechatPay implements Payment { public boolean pay(long cents) { System.out.println("微信扫码支付了 " + cents + " 分"); return true; } } public class AliPay implements Payment { public boolean pay(long cents) { System.out.println("支付宝扣款 " + cents + " 分"); return true; } } public class BankPay implements Payment { public boolean pay(long cents) { System.out.println("银行卡扣款 " + cents + " 分"); return true; } } -
未来拓展:
public class OrderService { public void checkout(Payment p, long amount) { if (p.pay(amount)) { // 更新订单状态 } } }
JDK8接口增强
JDk8开始,接口新增了三种形式的方法:
-
默认方法:普通实例方法
public interface A { // 必须加default修饰 default void go(){} } // 调用:通过实现类 class Aimp imppements A{ } Aimp a = new Aimp(); a.go() -
私有方法:私有的实例方法
private void run(){} // 调用:使用接口中的其他实例方法来调用 -
静态方法:使用static修饰,默认会加上public修饰
static void show(){} // 调用:只能使用当前接口名来调用 A.show()
接口与抽象类
接口 vs 抽象类
| 对比项 | 接口(Interface) | 抽象类(Abstract Class) |
|---|---|---|
| 关键字 | interface | abstract class |
| 继承方式 | implements | extends |
| 多继承 | ✅ 一个类可实现多个接口 | ❌ 只能单继承 |
| 方法 | Java 8+ 可有默认/静态方法,其余是抽象方法 | 可有普通方法 + 抽象方法 |
| 成员变量 | 只能是 public static final 常量 | 可以是任意类型变量 |
| 设计目的 | 定义“能力”(能做什么) | 定义“是什么” + 部分实现 |
| 构造器 | ❌ 没有 | ✅ 有(子类调用) |
相同点:
- 都是抽象形式,都可以抽象方法,都不成创建对象
- 都是派生子类形式(抽象类继承子类,接口需要实现类)
- 继承抽象类或者实现接口都必须重写完他们的抽象方法
- 都能支持多态,都能够实现解耦合
不同点:
- 抽象类中可以定义类的全部普通成员,接口只能定义常量,抽象方法
- 抽象类只能被类单继承,接口可以被多个类实现,
- 一个类继承抽象类就不能在继承其他类,一个类实现了接口(还可以继承其他类或者实现其他接口)
类中的成分
代码块
代码块是类中的五大成分之一
类的五大成分:成员变量\构造器\方法\代码块\内部类
代码块分类:
-
静态代码块:
-
格式:
static() -
特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次
public class Test { static { System.out.println("--------静态代码块--------"); } public static void main(String[] args) { System.out.println("========main方法========="); } } // 输出: //--------静态代码块-------- //========main方法========= -
作用:完成类的初始化
-
-
实例代码块:
-
格式:
{} -
特点:每次创建对象时,执行实例代码块,并在构造器前执行
public class Test { { System.out.println("--------实例代码块--------"); } public static void main(String[] args) { System.out.println("========main方法========="); new Test(); new Test(); new Test(); } } -
作用:和构造器一样,都是用来完成对象的初始化
-
内部类
如果一个类定义在另一个类的内部,这个类就是内部类。
public class Car {
public class Engine {}
}
成员内部类
成员内部类:无static修饰,属于外部类的对象特有的
public class Outer {
public class Inner{
public void show {}
}
}
调用:
外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();
Outer.Inner oi = new Outer().new Inner();
oi.show()
特点:
-
成员内部类中可以直接访问外部类的静态成员和静态方法,也可以直接访问外部类的实例成员
public class Outer { private static String staticField = "静态字段"; private String instanceField = "实例字段"; private static void staticMethod() { System.out.println("静态方法"); } private void instanceMethod() { System.out.println("实例方法"); } class Inner { // 成员内部类 void visit() { System.out.println(staticField); // 静态字段 staticMethod(); // 静态方法 System.out.println(instanceField); // 实例字段 instanceMethod(); // 实例方法 } } public static void main(String[] args) { new Outer().new Inner().visit(); // 一行创建 + 调用 } } -
成员内部类的实例方法中,可以直接拿到当前寄生的外部类对象:
外部类名.thispublic class Outer { private String name = "外部类"; class Inner { private String name = "内部类"; void show() { String name = "局部变量"; System.out.println(name); // 局部变量 System.out.println(this.name); // 内部类字段 System.out.println(Outer.this.name); // 外部类字段 } } public static void main(String[] args) { new Outer().new Inner().show(); } }
静态内部类
有static修饰的内部类,属于外部类自己持有。
public class Outer{
// 静态内部类
public static class Inner{
public void show(){}
}
}
调用:外部类名.内部类名 对象名 = new 外部类.内部类()
Outer.Inner in = new Outer.Inner();
inner.show()
特点:
- 静态内部类中可以直接访问外部类的静态成员。
- 静态内部类中不可以直接访问外部类的实例成员。
局部内部类
局部内部类是定义在方法中\代码块中\构造器等执行体中。
public class Test {
public static void main(String[] args){}
public static void go(){
class A{}
abstract class B{}
interface C{}
}
}
匿名内部类
是一种特殊的局部内部类,所谓匿名就是不需要为这个类声明名字,默认有一个隐藏的名字。
语法:
new 类或接口(参数...){
类体(一般是方法重写);
}
例如:
Animal a = new Animal(){
@Override
public void cry(){}
};
a.cry()
特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
**实际名字:
外部**类名.$编号.class
作用:用于更方便的创建一个子类对象
⭕常见使用形式:
通常作为一个对象参数传输给方法。
public class Test {
public static void main(String[] args) {
Swim s1 = new Swim() {
@Override
public void swimming() {
System.out.println("学生开始游泳~");
}
};
start(s1);
System.out.println("============");
Swim s2 = new Swim() {
@Override
public void swimming() {
System.out.println("老师开始游泳~");
}
};
start(s2);
}
interface Swim{
void swimming();
}
// 实现类
public static void start(Swim s){
System.out.println("开始游");
s.swimming();
System.out.println("结束游");
}
}
函数式编程
使用lambda函数去替代某些匿名内部类对象,从而让程序更加简洁。
lambda表达式
lambda表达式是JDK8新增的一种语法,代表函数;可以用于替代并简化函数式接口的匿名内部类。
语法:(参数列表) -> { 语句块 }
- 无参写
(),一个参数可省括号,一条语句可省{}和return。
匿名内部类写法:
new Thread(new Runnable() { @Override public void run() { System.out.println("hello"); } }).start();转换成lambda写法:
new Thread(() -> System.out.println("hello")).start();
lambda表达式只能替代函数式接口的匿名内部类。函数式接口就是有且仅有一个抽象方法的接口。在接口上加上@FuncationalInterface注解即可。
// 1. 函数式接口 → Lambda 合法
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
Calculator c = (x, y) -> x + y; // ✅ 编译通过
// 2. 接口里多一个抽象方法 → 不是函数式接口 → Lambda 非法
interface NotFunc {
void do1();
void do2(); // 多了一个
}
NotFunc f = () -> {}; // ❌ 编译错误:NotFunc 不是函数式接口
⭕省略写法
- 参数类型全部可以省略不写
- 如果只有一个参数,产生类型省略的同时
()也可以省略,但多个参数不能省略() - 如果lambda表达式中方法体只有一行代码,大括号可以不写,同时要省略封号
;,如果这行代码是return语句,也必须去掉return
方法引用
静态方法引用
语法:类名::静态方法
如果某个Lambda表达式里只是调用一个静态方法,并且“→”前后参数的形式一致,就可以使用静态方法引用。
实例方法引用
语法:对象名::实例方法
如果某个Lambda表达式里只是通过对象名称调用并且“→”前后参数的形式一致,就可以使用实例方法引用。
特定类型方法引用
语法:特定类名称::方法
如果某个Lambda表达式里只是调用一个特定类型的实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,则此时就可以使用特定类型的方法引用。
构造器引用
语法:类名::new
如果某个Lambda表达式里只是在创建对象,并且“→”首前后参数情况一致,就可以使用构造器引用。
常用API
String
String代表字符串,它的对象可以封装字符串数据,并提供了很多方法完成对字符串的处理。
- 创建字符串对象,封装字符串数据
- 调用String提供的操作字符串数据的方法
封装String方法
创建字符串对象的方式:
-
Java程序中的所有字符串文字(例如“abc”)都为此类的对象。
String s1 = "hello" -
调用String类的构造器初始化字符串对象。
String创建对象的区别:
- 只要是以”.”方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只存储一份;
- 通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。
调用String方法
下面只将两个比较常见的API。
public boolean equals(Object anObject)
作用:比较两个字符串的内容是否完全相同。
- 是区分大小写的。
- 如果传入的不是
String类型,会返回false。 - 与
==不同,==比较的是内存地址,而equals比较的是内容。
示例:
String a = "hello";
String b = new String("hello");
System.out.println(a.equals(b)); // 输出 true
System.out.println(a == b); // 输出 false
public String substring(int beginIndex, int endIndex)
作用:截取字符串的一部分,返回一个新的字符串。
beginIndex:起始位置(包含)。endIndex:结束位置(不包含)。- 字符串索引从 0 开始。
- 如果
beginIndex == endIndex,返回空字符串""。
示例:
String str = "hello world";
String sub = str.substring(0, 5);
System.out.println(sub); // 输出 "hello"
ArrayList
本质就是一个可变长度数组。
基本使用:
ArrayList<> list = new ArrayList<>()
list.add('111')
常用方法汇总:
异常
异常体系
Error:代表系统级别异常
Exception:程序出现的异常
- 运行异常:运行阶段出现的异常(代码写的错误)
- 空值异常
- 编译异常:编译阶段出现的异常(提醒你代码易错点)
异常处理
异常的作用
-
异常是用来定位程序bug的关键信息
-
可以作为方法内部的一种特殊返回值,以便通知上层调用者,方法的执行问题
throw new Exception('异常')
抛出异常
在方法中使用throws关键字,可以将方法内部出现的异常抛出去去给调用者处理
方法 throws 异常1,异常2...{
...
}
捕获异常
直接捕获程序出现的异常
try{
// 监视可能出现异常的代码
}catch(异常类型1 变量){
// 处理异常
}catch(异常类型2 变量){
// 处理异常
}
例子:
public class Test {
public static void main(String[] args) {
int numerator = 10; // 被除数
int denominator = 0; // 除数
try{
test(numerator, denominator);
}catch(Exception e){
System.out.println("错误:不能除以零!");
}finally{
// finally块:无论是否捕获异常,都会执行
System.out.println("程序执行完毕!");
}
}
public static void test(int numerator, int denominator) throws Exception {
int result = numerator / denominator;
System.out.println("结果是:" + result);
}
}
自定义异常
Java无法为这个世界上全部的问题都提供异常类来代表,如果企业自己的某种问题, 想通过异常来表示,以便用异常来管理该问题,那就需要自己来定义异常类了。
- 自定义运行时异常(定义一个异常类继承RuntimeException)
- 自定义编译时异常(定义一个异常类继承Exception)
实现步骤:
-
继承
Exception// 自定义异常类 class DivisionByZeroException extends Exception { } -
重写
Exception构造器// 自定义异常类 class DivisionByZeroException extends Exception { public DivisionByZeroException(String message) { super(message); } } -
使用throw抛出自定义异常
public static void test(int numerator, int denominator) throws DivisionByZeroException { if (denominator == 0) { // 如果除数为0,抛出自定义异常 throw new DivisionByZeroException("不能除以零!"); } int result = numerator / denominator; System.out.println("结果是:" + result); }
例子:
package throwDemo;
// 自定义异常类
class DivisionByZeroException extends Exception {
public DivisionByZeroException(String message) {
super(message);
}
}
public class Test {
public static void main(String[] args) {
int numerator = 10; // 被除数
int denominator = 0; // 除数
try {
test(numerator, denominator);
} catch (Exception e) {
// 捕获自定义异常
System.out.println("错误:" + e.getMessage());
} finally {
// finally块:无论是否捕获异常,都会执行
System.out.println("程序执行完毕!");
}
}
public static void test(int numerator, int denominator) throws DivisionByZeroException {
if (denominator == 0) {
// 如果除数为0,抛出自定义异常
throw new DivisionByZeroException("不能除以零!");
}
int result = numerator / denominator;
System.out.println("结果是:" + result);
}
}
泛型
定义类、接口、方法时,同时声明了一个或者多个类型变量称为泛型类、泛型接口,泛型方法、它们统称为泛型。
作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!
本质就是把具体的数据类型作为参数传给类型变量。
泛型的语法通常使用尖括号<>来定义类型参数。例如,List<T>表示一个可以存储类型为T的元素的列表。
泛型类
定义了一个可以存储任意类型数据的容器。
语法:
修饰符 class 类名<类型变量1,类型变量2,...>{
//...
}
注意:类型变量建议用大写的英文字母,常用的有:E、T、K、V 等
例子:
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
public class Test {
public static void main(String[] args) {
// 创建一个存储Integer的Box
Box<Integer> intBox = new Box<>();
intBox.setContent(10);
System.out.println("整数内容:" + intBox.getContent());
// 创建一个存储String的Box
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello, World!");
System.out.println("字符串内容:" + stringBox.getContent());
}
}
泛型接口
泛型接口允许你在接口级别上使用类型参数。下面是一个泛型接口的例子,它定义了一个可以存储任意类型数据的队列。
语法:
修饰符 interface 接口名<类型变量1,类型变量2...>{
}
例子:
public interface Queue<T> {
void enqueue(T item); // 入队
T dequeue(); // 出队
boolean isEmpty(); // 检查队列是否为空
}
public class ArrayQueue<T> implements Queue<T> {
private T[] data;
private int front;
private int rear;
public ArrayQueue(int capacity) {
data = (T[]) new Object[capacity];
front = 0;
rear = 0;
}
@Override
public void enqueue(T item) {
if ((rear + 1) % data.length == front) {
throw new IllegalStateException("队列已满");
}
data[rear] = item;
rear = (rear + 1) % data.length;
}
@Override
public T dequeue() {
if (front == rear) {
throw new IllegalStateException("队列为空");
}
T item = data[front];
front = (front + 1) % data.length;
return item;
}
@Override
public boolean isEmpty() {
return front == rear;
}
}
public class Test {
public static void main(String[] args) {
Queue<Integer> intQueue = new ArrayQueue<>(5);
intQueue.enqueue(1);
intQueue.enqueue(2);
System.out.println("出队:" + intQueue.dequeue());
System.out.println("出队:" + intQueue.dequeue());
}
}
泛型方法
泛型方法允许你在方法级别上使用类型参数。下面是一个泛型方法的例子,它交换两个变量的值。
语法:
修饰符<类型变量1,类型变量2...> 返回值类型 方法名(形参列表){
}
通配符:
- 就是
?,可以在“使用泛型”的时候代表一切类型;E T K V是在定义泛型的时候使用。
上下限:
- 泛型上限:
?extends Car能接受的必须是Car或者其子类 - 泛型下限:
?super Car能接受的必须是Car或者其父类
例子:
public class Test {
// 泛型方法
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
System.out.println("交换前:" + java.util.Arrays.toString(intArray));
swap(intArray, 1, 3);
System.out.println("交换后:" + java.util.Arrays.toString(intArray));
String[] stringArray = {"a", "b", "c", "d", "e"};
System.out.println("交换前:" + java.util.Arrays.toString(stringArray));
swap(stringArray, 1, 3);
System.out.println("交换后:" + java.util.Arrays.toString(stringArray));
}
}
泛型支持的类型
泛型只支持对象类型(引用数据类型),不支持基本数据类型。
Array<int> list = new ArrayList<>(); ❌
包装类
包装类就是把基本数据类型的数据包装成对象的类型。
| 基本数据类型 | 对应的包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| char | Character |
| float | Float |
| double | Double |
| boolean | Boolean |
使用方法:
-
过时用法:
Integer i = new Integer(100); -
建议用法:
Integer i = Integer.valueOf(100);
自动装箱
基本数据类型的数据可以直接变成包装对象的数据,不需要额外做任何操作。
Integer i = 100;
// 等同于 Integer i = Integer.valueOf(100);
自动拆箱 (了解)
把包装类型的对象直接给基本类型的数据
int it = i;
ArrayList <Integer> list = new ArrayList<>();
list.add(110); // 自动拆箱
int res = list.get(1); // 自动拆箱
包装类其他功能
-
可以把基本类型的数据转换成字符串类型
-
可以把字符串类型的数值转换成数值本身对应的真实数字
String str = '100' int i = Integer.parseInt(str) int i = Integer.valueOf(str) //也可以直接使用 valueOf
集合框架
集合是一种容器,用来装数据的,类似于数组,但集合的大小可变,在开发中也是非常常用。
Collection单列集合
单列集合就是每个元素(数据)只包含一个值。
Collection集合的分类:
Collection常用方法
| 方法名 | 说明 |
|---|---|
| public boolean add(E e) | 把给定的对象添加到当前集合中 |
| public void clear() | 清空集合中所有的元素 |
| public boolean remove(E e) | 把给定的对象在当前集合中删除 |
| public boolean contains(Object ob) | 判断当前集合中是否包含给定的对象 |
| public boolean isEmpty() | 判断当前集合是否为空 |
| public init size() | 返回集合中元素的个数 |
| public Object[] toArray() | 把集合中的元素存储到数组中 |
Collection遍历方式
-
迭代器
iterater():是用来遍历集合的专用方式Iterator<String> i = array.iterator(); // 得到迭代器对象 // 取数据 i.next(); // 使用while循环遍历 while(i.hasNext()){ String name = i.next() sout(name) } -
增强for循环:
// 格式: for(元素的数据类型 变量名 : 数组或者集合){ // 方法体 }// 例子 for(String name : names){ sout(name) } -
lambda表达式:
names.forEach(n -> sout(n));
循环遍历的区别:只有迭代器遍历才能解决`并发修改异常问题。
知识补充:认识并发修改异常问题
遍历集合的同时又存在增删集合元素的行为时可能出现业务异常,这种现象被称为
并发修改异常问题。
List系列集合特点就是元素是有序、可重复、有索引,并且可以通过索引来访问元素。
List实现类:
Java中的List是一个接口,不能直接实例化,需要使用它的实现类来实现,常见的实现类有:
ArrayList:基于动态数组实现,查询快,增删慢。LinkedList:基于双向链表来实现,增删快,查询慢。
List常用方法:
add(E e) | 添加元素到末尾 |
|---|---|
add(int index, E element) | 在指定位置插入元素 |
get(int index) | 获取指定位置的元素 |
set(int index, E element) | 修改指定位置的元素 |
remove(int index) | 删除指定位置的元素 |
remove(Object o) | 删除第一个匹配的元素 |
size() | 返回元素个数 |
isEmpty() | 判断是否为空 |
contains(Object o) | 判断是否包含某个元素 |
indexOf(Object o) | 返回元素第一次出现的索引 |
clear() | 清空所有元素 |
List使用方法:
-
导包
import java.util.ArrayList; import java.util.List; -
创建List对象
// 推荐写法:接口指向实现类 List<String> list = new ArrayListM<>(); // 其他写法 ArrayList<String> list = new ArrayList<>(); -
使用List对象
list.add('添加内容')
案例:
ArrayList:
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
// 创建一个存储字符串的List
List<String> fruits = new ArrayList<>();
// 添加元素
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橙子");
System.out.println(fruits); // [苹果, 香蕉, 橙子]
// 在索引1处插入
fruits.add(1, "葡萄");
System.out.println(fruits); // [苹果, 葡萄, 香蕉, 橙子]
// 获取元素
String first = fruits.get(0);
System.out.println("第一个元素:" + first); // 苹果
// 修改元素
fruits.set(2, "芒果");
System.out.println(fruits); // [苹果, 葡萄, 芒果, 橙子]
// 删除元素
fruits.remove(0); // 删除索引0的元素
System.out.println(fruits); // [葡萄, 芒果, 橙子]
// 获取大小
System.out.println("大小:" + fruits.size()); // 3
// 遍历List
System.out.println("遍历方式1:for循环");
for (int i = 0; i < fruits.size(); i++) {
System.out.println(fruits.get(i));
}
System.out.println("遍历方式2:增强for循环");
for (String fruit : fruits) {
System.out.println(fruit);
}
System.out.println("遍历方式3:forEach + Lambda(Java 8+)");
fruits.forEach(System.out::println);
}
}
- List集合
LinkedList
LinkedList 特有方法(ArrayList 没有)
| 方法 | 说明 |
|---|---|
addFirst(E e) | 在链表开头插入元素 |
addLast(E e) | 在链表末尾插入元素(等同于 add) |
getFirst() | 获取第一个元素 |
getLast() | 获取最后一个元素 |
removeFirst() | 删除并返回第一个元素 |
removeLast() | 删除并返回最后一个元素 |
这些方法让 LinkedList 非常适合做 栈(Stack) 或 队列(Queue) 的实现。
案例:
LinkedList:
import java.util.LinkedList;
public class SimpleLinkedListDemo {
public static void main(String[] args) {
// 创建一个 LinkedList
LinkedList<String> fruits = new LinkedList<>();
// 添加几个水果
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橙子");
// 打印列表
System.out.println("水果列表:" + fruits); // [苹果, 香蕉, 橙子]
// 在最前面加一个
fruits.addFirst("葡萄");
System.out.println("在开头加上葡萄:" + fruits); // [葡萄, 苹果, 香蕉, 橙子]
// 在最后面加一个(和 add 一样)
fruits.addLast("芒果");
System.out.println("在末尾加上芒果:" + fruits); // [葡萄, 苹果, 香蕉, 橙子, 芒果]
// 删除第一个
fruits.removeFirst();
System.out.println("删除第一个:" + fruits); // [苹果, 香蕉, 橙子, 芒果]
// 删除最后一个
fruits.removeLast();
System.out.println("删除最后一个:" + fruits); // [苹果, 香蕉, 橙子]
// 获取第一个和最后一个
System.out.println("现在的第一个:" + fruits.getFirst()); // 苹果
System.out.println("现在的最后一个:" + fruits.getLast()); // 橙子
// 打印总共有几个
System.out.println("一共 " + fruits.size() + " 种水果");
}
}
- Set集合
Set系列集合特点是元素是无序的、不可以重复、无索引。
你可以把
set想象成一个"去重的List"
Set实现类
HashSet:最重用,无序,基于哈希表实现的Set集合。查询速度快LinkedHashSet:按照插入顺序,查询速度比HashSet稍慢TreeSet:按照自然顺序或者自定义规则排序,适合需要排序的场景。
Set常用方法
| 方法 | 说明 |
|---|---|
add(E e) | 添加元素(如果已存在,添加失败,返回 false) |
remove(Object o) | 删除指定元素 |
contains(Object o) | 判断是否包含某个元素 |
size() | 获取元素个数 |
isEmpty() | 是否为空 |
clear() | 清空所有元素 |
案例
HashSet去重:
import java.util.HashSet;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
// 创建一个 Set 存放字符串
Set<String> names = new HashSet<>();
// 添加元素(尝试添加重复的)
names.add("张三");
names.add("李四");
names.add("王五");
names.add("张三"); // 重复了!
System.out.println("Set 内容:" + names);
// 输出:Set 内容:[张三, 李四, 王五] → 自动去重!
System.out.println("大小:" + names.size()); // 3
// 判断是否包含
System.out.println("包含张三吗?" + names.contains("张三")); // true
// 删除
names.remove("李四");
System.out.println("删除李四后:" + names); // [张三, 王五]
// 遍历 Set(不能用下标!)
System.out.println("遍历方式1:增强for循环");
for (String name : names) {
System.out.println(name);
}
System.out.println("遍历方式2:forEach + Lambda");
names.forEach(System.out::println);
}
}
三种Set的对比演示:
import java.util.*;
public class SetCompare {
public static void main(String[] args) {
Set<String> hashSet = new HashSet<>();
Set<String> linkedHashSet = new LinkedHashSet<>();
Set<String> treeSet = new TreeSet<>();
// 同时添加这些元素
String[] data = {"Bob", "Alice", "Charlie", "Alice"}; // Alice 重复
for (String s : data) {
hashSet.add(s);
linkedHashSet.add(s);
treeSet.add(s);
}
System.out.println("HashSet(无序,去重):" + hashSet);
// 可能输出:[Bob, Charlie, Alice] → 顺序不确定
System.out.println("LinkedHashSet(插入顺序):" + linkedHashSet);
// 输出:[Bob, Alice, Charlie] → 按你添加的顺序
System.out.println("TreeSet(自动排序):" + treeSet);
// 输出:[Alice, Bob, Charlie] → 按字母升序排列
}
}
⚠注意事项:
- 添加重复元素虽然不会报错,但是
add()会返回false - HashSet中的有且只能存一个
null - TreeSet中不能存
null,否则会报错
Map双列集合
双列集合就是每个元素包含两个值(键值对)。
生活中的例子:
- 姓名 ➜ 手机号(通过名字查电话)
- 单词 ➜ 中文意思(通过英文查中文)
- 身份证号 ➜ 学生信息
Map常用实现类:
HashMap:最常用,无序,查询快LinkedHashMap:按插入顺序排序TreeMap:按Key的自然顺序或者自定义顺序排序
Map核心方法:
| 方法 | 说明 |
|---|---|
put(K key, V value) | 添加或更新一个键值对 |
get(Object key) | 根据 key 获取 value,如果 key 不存在返回 null |
remove(Object key) | 删除指定 key 的键值对 |
containsKey(Object key) | 判断是否包含某个 key |
containsValue(Object value) | 判断是否包含某个 value |
size() | 获取键值对的数量 |
isEmpty() | 是否为空 |
clear() | 清空所有数据 |
keySet() | 获取所有 key 的集合(Set) |
values() | 获取所有 value 的集合(Collection) |
entrySet() | 获取所有“键值对”的集合(Set<Map.Entry<K,V>>) |
案例:
HashMap基本使用:
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
// 创建一个 Map:姓名 -> 年龄
Map<String, Integer> ages = new HashMap<>();
// 添加数据
ages.put("张三", 25);
ages.put("李四", 30);
ages.put("王五", 28);
ages.put("张三", 26); // Key 重复,会覆盖之前的值
System.out.println("所有人:" + ages);
// 输出:{张三=26, 李四=30, 王五=28}
// 查询
System.out.println("张三的年龄:" + ages.get("张三")); // 26
System.out.println("赵六的年龄:" + ages.get("赵六")); // null(不存在)
// 判断是否存在 key
if (ages.containsKey("李四")) {
System.out.println("找到了李四!");
}
// 删除
ages.remove("王五");
System.out.println("删除王五后:" + ages); // {张三=26, 李四=30}
// 获取总数
System.out.println("共有 " + ages.size() + " 个人");
}
}
Map遍历方式
-
通过
keySet遍历for(String name : ages.keySet()){ Integer age = ages.get(name); System.out.println(name + "的年龄:" + age); } -
遍历
Valuesfor(Integer age : ages.values()){ System.out.println("年龄" + age) } -
通过
entrySetfor(Map.Entry<String,Integer> entry : ages.entrySet()){ String name = entry.getKey(); Integer age = entry.getValue(); System.out.println(name + "-->" + age); }