【Java】2025版一天学会Java基础到高级(上)

84 阅读41分钟

@[toc]

Java

创建项目

Java项目的代码结构:

  • 工程 project
    • 模块 module
      • 包 package
        • 类 class
  1. 创建项目 image-20250926093939148
  2. 创建模块 image-20250926094023132
  3. 创建包 image-20250926094210354
  4. 创建类(类名不需要带后缀) image-20250926094325471

基础语法

注释

  • 单行注释:// 注释内容
  • 多行注释:/* 注释内容 */
  • 文档注释:/** 注释内容 */

字面量

定义:就是程序中能直接书写的数据,学这个知识的重点是:搞清楚lava程序中数据的书写格式。

字面量分类:

字面量类型说明示例
整数字面量表示整数值,可以是十进制、八进制、十六进制或二进制形式。默认是int类型,可加Ll后缀表示long类型。123(十进制)
0123(八进制)
0x7B(十六进制)
0b1111011(二进制)
200L(long类型)
浮点数字面量表示带小数的数值,可以是普通小数形式或科学计数法形式。默认是double类型,可加Ff后缀表示float类型。3.14(double类型)
3.14F(float类型)
1.23e2(科学计数法)
字符字面量用单引号括起来的单个字符,可以是字母、数字、符号或转义字符。'A''1''中''\n'(换行符)、'\t'(制表符)
布尔字面量表示逻辑真或假的值,只有两个取值。truefalse
字符串字面量用双引号括起来的字符序列,可以包含任意字符,包括转义字符。"Hello World""Java""你好""\n"(换行字符串)
空字面量表示对象引用不指向任何对象,仅用于引用类型。null

image-20250926100541683

变量

定义格式:

image-20250926100718970

📖知识扩展:比特

计算机中表示数据的最小单元。数据在计算机中的最小储存单元:字节(byte,简称B)是,一个字节占8个比特位(bit,简称b),因此1B=8b image-20250926101739085

进制计算
  1. 十进制转二进制的算法:除二取余法 image-20250926101535872
  2. 二进制转十进制:8421法 image-20250926150546781
  3. 八进制:每3位二进制作为一个单元,最小的是0,最大的是7,共8个数字:image-20250926150838332
  4. 十六进制:每4位二进制作为一个单元,最小数是0,最大数是15,共16个数字: image-20250926151001175

JaVa程序中支持书写二进制、八进制、十六进制的数据,分别需要以0B或者0b00X或者0x开头。

数据类型

基本数据类型:4大类8种

image-20250926151445471

注意事项:

  • long类型: image-20250926151711731
  • float类型: image-20250926151746683

关键字和标识符

关键字

  • Java 语法已经“占用”的单词,如 publicclassintif……不能拿来当名字

标识符

  • 程序员自己起的名字,如类名 HelloWorld、变量名 age、方法名 getSum……不能跟关键字重复,且必须遵守“字母/下划线/$ 开头,后接字母、数字、下划线或 $”的规则。

标识符使用规则:

  1. 只能由 字母(A–Z、a–z、汉字等 Unicode 字母)、数字(0–9)、下划线 _美元符号 $ 组成;
  2. 第一个字符 不能是数字

因此,以下都是合法标识符示例:

类别合法举例
纯英文ageMAX_VALUE$root_temp
驼峰命名studentNamegetTotalScore
常量风格PICACHE_SIZE
含 Unicode 字母价格变量①π(不推荐,但合法)

不能出现的字符:空格、连字符 -、运算符 +*/、标点 . , ; : 等,也 不能跟 67 个关键字同名(如 intclasstruenull 等)。

方法

方法是一种用于执行特定任务或操作的代码块,代表一个功能,它可以接收数据进行处理,并返回一个处理后的结果。

方法的完整格式:

修饰符 返回值类型 方法名(形参列表){
    方法体代码(需要执行的功能代码)
    return 返回值;
}

例:

public static int add(int a, int b) {   // 修饰符  返回类型  方法名(参数)
    return a + b;                       // 方法体
}
// 调用
int sum = add(3,5); // sum = 8

⚠注意:

  1. 返回类型用 void 表示“什么都不返回”。
  2. 参数列表可空 ( ),也可多个 (int x, double y)
  3. 方法名遵循小驼峰,见名知义。
  4. 想复用就“调方法”,想灵活就“传参数”,想拿到结果就 return

方法的其他形式:

  • 方法不需要接受参数:

    修饰符 void 方法名(形参列表){
        方法体代码(需要执行的功能代码)
    }
    

    如果方法没有返回结果,返回值类型必须声明void

    // 调用
    方法名()
    
方法重载

一个类中,出现多个方法的名称相同,但是它们的形参列表是不同的,那么这些方法就称为方法重载了。

重点:方法重载只关心方法名称相同,形参列表不同(类型不同,个数不同,顺序不同)。 image-20250926171152756

return

无返回值的方法中可以直接通过单独的return;立即结束当前方法的执行。 image-20250926171413256

类型转换

类型范围小的变量,可以直接赋值给类型范围大的变量。

image-20250926171610115

例:

byte b = 10;
int  i = b;      // 自动转,OK
long L = i;      // 自动转,OK
double d = L;    // 自动转,OK

// 反方向不行,会编译报错
int x = 3.14;    // ❌ double → int,必须强转
强类型转换

类型范围大的变量,不可以直接赋值给类型范围小的变量,会报错,需要强制类型转换过去

格式:

类型 变量2 = (类型) 变量1

image-20250926172353829

⚠注意:

  • 强制类型转换可能造成数据(丢失)溢出;
  • 浮点型强转成整型,直接丢掉小数部分,保留整数部分返回
自动类型提升

在表达式中,小范围类型的变了,会自动转换成表达式中较大范围的类型,再参与运算。

image-20250926173122074

⚠注意:

  • 表达式的最终结果类型由表达式中的最高类型决定。
  • 在表达式中,byte\short\char 是直接转换成int类型再参与运算。

分支结构

if分支

根据条件的真或假,来决定执行某段代码。

结构:

if(条件表达式){
    // 代码;
}

if-else分支:

if (条件表达式) {
    // 代码1;
} else {
    // 代码2;
}

多条件分支:

if (条件表达式) {
    // 代码1;
} else if (条件表达式) {
    // 代码2;
} else if (条件表达式) {
    // 代码3;
} else {
    // 代码4;
}
switch分支

是通过比较值是否相等,来决定执行哪条语句。

结构:

switch (变量) {
    case1 -> 语句1;
    case2 -> 语句2;
    ...
    default -> 默认语句;
}

⚠注意:如果不编写break会出现穿透现象

int month = 2;
switch (month) {
    case 2:
    case 3:
    case 4:
        System.out.println("春天");
        break;
}
// 输出 "春天"

📖知识扩展:

ifswitch的区别:

  • if 是“范围题”,switch 是“单选题”
对比点if / else ifswitch
判断类型任意布尔表达式(>、<、&&、都行)只能对比单个值(==)
适合场景区间、范围、复杂条件固定几个候选值(如 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钟常见死循环:

  1. while

    while (true) { ... }
    
  2. for

    for (;;) { ... }
    
  3. do-while

    do { ... } while (true);
    

什么时候用死循环

  1. 服务器(7×24 监听)
  2. 游戏引擎(不断刷新画面)
  3. 菜单(重复等待用户输入)
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)用来控制类、接口、变量、方法、构造器的可见性范围。

按“从宽到严”依次是:

  1. 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();   // 任何地方都能调到
        }
    }
    
  2. 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(); // ❌ 不同包非子类视角
        }
    }
    
  3. (default) → 仅同包可见(不写任何修饰符)

    package com.foo;
    
    class Hidden {          // 不写修饰符,包私有
        void foo() {}
    }
    
    package com.bar;
    class Outsider {
        void test() {
            // new Hidden();  // ❌ 不同包完全不可见
        }
    }
    
  4. private → 仅本类内部可见

    public class Counter {
        private int count = 0;
    
        public void inc() {
            count++;   // 本类内部随便用
        }
    
        private void reset() {   // 连子类都看不到
            count = 0;
        }
    }
    

速查表(✅ = 可见,❌ = 不可见)

范围publicprotected(default)private
本类
同包其他类
不同包子类
不同包非子类

封装

封装 是将对象的属性和行为包装起来,并通过访问控制(如 private)隐藏内部细节,只暴露必要的接口(如 public 方法)。目的就是为了提高安全性(防止非法访问)和提高可维护性(内部修改不影响外部使用)。

封装步骤:

  1. 将成员变量设为private
  2. 提供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 方法中不能使用 thissuper

继承

继承允许一个类(子类)获得另一个类(父类)的属性和方法,实现代码复用。使用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;
        }
    }
    
子类构造器

特点:子类的全部构造器,都会先调用父类的构造器,再调用自己。

image-20251021104510234

子类构造器是如何实现调用父类构造器的:

  • 默认情况下,子类全部构造器的第一行代码都是super()(写不写都有),它会调用父类的无参数构造器。

多态

多态字面意思是“多种形态”。在 Java 中,它指的是:同一个方法调用,在不同对象上会产生不同的行为

举个生活中的例子: 你按“开机键”,对电脑来说是开机,对电视来说是打开电视,对空调来说是启动制冷——同一个动作(开机),不同对象(电脑/电视/空调)做出不同的反应。这就是多态!

⭕多态的前提条件(必须同时满足):

  1. 继承(或实现接口)
  2. 方法重写(子类重写父类的方法)
  3. 父类引用指向子类对象(这是关键!)

举个例子:

// 父类
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("叽叽~");
        }
    }
    
  • 便于维护和解耦:你只需要面向父类编程,不用关心具体是哪个子类。

⚠️ 注意事项:

  • 成员变量没有多态! 如果父类和子类有同名变量,访问的是编译时类型(即左边的类型)的变量。
  • 静态方法也没有多态! 静态方法属于类,不是对象,调用时看的是引用类型(左边)。
  • 多态下不能调用子类独有方法!
多态类型转换
  1. 向上转型(子 → 父) 语法: Parent p = new Child(); 特点:自动完成,一定成功,但只能调用父类声明的方法(除非子类重写了)。
  2. 向下转型(父 → 子) 语法: 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关键字

作用是将声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量

  1. 修饰普通变量(局部变量或者成员变量):

    public class Example {
        public static void main(String[] args) {
            final int x = 10;
            // x = 20; ❌ 编译错误!不能修改 final 变量
            System.out.println(x); // 输出:10
        }
    }
    
  2. 修饰成员变量:必须在声明时赋值,或在构造器中赋值(只能选一种方式,且只能赋一次)。

    public class Person {
        private final String name; // 声明但未赋值
    
        public Person(String name) {
            this.name = name; // ✅ 可以在构造器中赋值
        }
        // name 之后不能再改!
    }
    
  3. final修饰方法不能被子类重写:

    class Animal {
        public final void sleep() {
            System.out.println("动物睡觉");
        }
    }
    
    class Dog extends Animal {
        // @Override
        // public void sleep() { } ❌ 编译错误!不能重写 final 方法
    }
    
  4. final修饰类不能被继承:

    final class MathUtils {
        public static int add(int a, int b) {
            return a + b;
        }
    }
    
    // class MyMath extends MathUtils { } ❌ 编译错误!不能继承 final 类
    
  5. 常用与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 的,而且全部大写是惯例。

特点:

image-20251021162944393

  • 枚举类都是最终类,不可以被继承,枚举类都是继承java.lang.Enum类的。
  • 枚举类的第一行只能罗列一些名称,这些名称都是变了,并且每个常量都会记住枚举类的一个对象。
  • 枚举类的构造器都是私有的,因此枚举类对外不能创建对象。

抽象类

抽象类是用 abstract 关键字修饰的类,它不能被直接实例化(不能用 new 创建对象),通常用来作为其他类的“模板”或“基类”

💡 类比: 抽象类就像“水果”这个概念——你可以有“苹果”“香蕉”,但不能说“给我一个水果”(因为“水果”太抽象了,不是一个具体的东西)。

❓为什么需要抽象类

现实世界中,有些类本身就是没有实际意义的,只是为了定义通用行为,让子类可以实现具体细节。

比如:

  • 动物会“叫”,但“动物”本身怎么叫?不知道!
  • 具体到“狗”是“汪汪”,“猫”是“喵喵”——这些由子类决定。

✅抽象类写法

  1. 定义抽象类和抽象方法:

    // 抽象类
    abstract class Animal {
        // 普通方法(可以有方法体)
        public void sleep() {
            System.out.println("动物在睡觉");
        }
    
        // 抽象方法(没有方法体,用 abstract 修饰)
        public abstract void makeSound(); // 子类必须实现这个方法!
    }
    
  2. 子类继承抽象类,并实现抽象方法:

    class Dog extends Animal {
        @Override
        public void makeSound() {
            System.out.println("汪汪!");
        }
    }
    
    class Cat extends Animal {
        @Override
        public void makeSound() {
            System.out.println("喵喵~");
        }
    }
    
  3. 使用:(不能直接创建对象new Animal,因为抽象类不能被实例化

    public class Main {
        public static void main(String[] args) {
            // Animal a = new Animal(); ❌ 编译错误!抽象类不能实例化
    
            Animal dog = new Dog(); // ✅ 可以用父类引用指向子类对象(多态)
            dog.makeSound();        // 输出:汪汪!
            dog.sleep();            // 输出:动物在睡觉
        }
    }
    
模板方法模式

提供一个方法作为完成某类功能的模板,模板方法封装了每个实现步骤,但允许子类提供特定步骤的实现。可以提高代码复用、并简化子类设计。

⭕举个例子:

做饮料的通用流程是:

  1. 烧水
  2. 冲泡(咖啡 or 茶) ← 这一步不同
  3. 倒进杯子
  4. 加调料(糖/牛奶) ← 这一步也可能不同

这个流程就是“模板”,而“冲泡”和“加调料”是可变的步骤。

代码实现:

  1. 定义抽象父类(模板):

    // 抽象类:定义做饮料的模板
    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 保证流程不能被改。

  2. 子类实现具体步骤:

    // 做咖啡
    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("加柠檬...");
        }
    }
    
  3. 使用:

    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();
        }
    }
    
  4. 输出:

    === 制作咖啡 ===
    烧开水...
    用沸水冲泡咖啡粉...
    倒入杯中...
    加糖和牛奶...
    
    === 制作茶 ===
    烧开水...
    用沸水泡茶叶...
    倒入杯中...
    加柠檬...
    

接口类

接口定义了"一个类能做什么",但不关心具体做了什么。

接口用来被类实现的,实现接口的类称为实现类,一个类可以同时实现多个接口。

✅接口的写法

  1. 定义接口:

    // 接口用 interface 关键字定义
    public interface Flyable {
        // 接口中的方法默认是 public abstract(可以省略)
        void fly(); // 所有实现类必须实现这个方法
    }
    
  2. 类实现接口:(用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("飞机起飞了!");
        }
    }
    
  3. 使用接口:

    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 的!
        }
    }
    
  • 一个类实现多个接口,如果多个接口中存在同名的默认方法,可以不冲突,这个类重写方法即可

案例:"支付通道"

需求:系统要支持微信\支付宝\银行卡三种支付,未来还能扩展

  1. 先定义接口:

    public interface Payment {
        /**
         * 支付
         * @param cents 金额,单位分
         * @return true 成功
         */
        boolean pay(long cents);
    }
    
  2. 三种实现:

    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;
        }
    }
    
  3. 未来拓展:

    public class OrderService {
        public void checkout(Payment p, long amount) {
            if (p.pay(amount)) {
                // 更新订单状态
            }
        }
    }
    
JDK8接口增强

JDk8开始,接口新增了三种形式的方法:

  1. 默认方法:普通实例方法

    public interface A {
        // 必须加default修饰
        default void go(){}
    }
    
    // 调用:通过实现类
    class Aimp imppements A{
    }
    Aimp a = new Aimp();
    a.go()
    
  2. 私有方法:私有的实例方法

    private void run(){}
    
    // 调用:使用接口中的其他实例方法来调用
    
  3. 静态方法:使用static修饰,默认会加上public修饰

    static void show(){}
    
    // 调用:只能使用当前接口名来调用
    A.show()
    
接口与抽象类

接口 vs 抽象类

对比项接口(Interface)抽象类(Abstract Class)
关键字interfaceabstract class
继承方式implementsextends
多继承✅ 一个类可实现多个接口❌ 只能单继承
方法Java 8+ 可有默认/静态方法,其余是抽象方法可有普通方法 + 抽象方法
成员变量只能是 public static final 常量可以是任意类型变量
设计目的定义“能力”(能做什么)定义“是什么” + 部分实现
构造器❌ 没有✅ 有(子类调用)

相同点:

  1. 都是抽象形式,都可以抽象方法,都不成创建对象
  2. 都是派生子类形式(抽象类继承子类,接口需要实现类)
  3. 继承抽象类或者实现接口都必须重写完他们的抽象方法
  4. 都能支持多态,都能够实现解耦合

不同点:

  1. 抽象类中可以定义类的全部普通成员,接口只能定义常量,抽象方法
  2. 抽象类只能被类单继承,接口可以被多个类实现,
  3. 一个类继承抽象类就不能在继承其他类,一个类实现了接口(还可以继承其他类或者实现其他接口)

类中的成分

代码块

代码块是类中的五大成分之一

类的五大成分:成员变量\构造器\方法\代码块\内部类

代码块分类:

  • 静态代码块:

    • 格式: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()

特点:

  1. 成员内部类中可以直接访问外部类的静态成员和静态方法,也可以直接访问外部类的实例成员

    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();          // 一行创建 + 调用
        }
    }
    
  2. 成员内部类的实例方法中,可以直接拿到当前寄生的外部类对象:外部类名.this

    public 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()

特点:

  1. 静态内部类中可以直接访问外部类的静态成员。
  2. 静态内部类中不可以直接访问外部类的实例成员。
局部内部类

局部内部类是定义在方法中\代码块中\构造器等执行体中。

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代表字符串,它的对象可以封装字符串数据,并提供了很多方法完成对字符串的处理。

  1. 创建字符串对象,封装字符串数据
  2. 调用String提供的操作字符串数据的方法
封装String方法

创建字符串对象的方式:

  1. Java程序中的所有字符串文字(例如“abc”)都为此类的对象。

    String s1 = "hello"
    
  2. 调用String类的构造器初始化字符串对象。 image-20251022165300198

String创建对象的区别:

  • 只要是以”.”方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只存储一份
  • 通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。
调用String方法

image-20251022165650345

下面只将两个比较常见的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')

常用方法汇总:

image-20251022171002247

异常

异常体系

Error:代表系统级别异常

Exception:程序出现的异常

  • 运行异常:运行阶段出现的异常(代码写的错误)
    • 空值异常
  • 编译异常:编译阶段出现的异常(提醒你代码易错点)

异常处理

异常的作用

  1. 异常是用来定位程序bug的关键信息

  2. 可以作为方法内部的一种特殊返回值,以便通知上层调用者,方法的执行问题

    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)

实现步骤:

  1. 继承Exception

    // 自定义异常类
    class DivisionByZeroException extends Exception {
        
    }
    
  2. 重写Exception构造器

    // 自定义异常类
    class DivisionByZeroException extends Exception {
        public DivisionByZeroException(String message) {
            super(message);
        }
    }
    
  3. 使用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<>(); ❌
包装类

包装类就是把基本数据类型的数据包装成对象的类型。

基本数据类型对应的包装类
byteByte
shortShort
intInteger
longLong
charCharacter
floatFloat
doubleDouble
booleanBoolean

使用方法:

  • 过时用法:

    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); // 自动拆箱
包装类其他功能
  1. 可以把基本类型的数据转换成字符串类型

  2. 可以把字符串类型的数值转换成数值本身对应的真实数字

    String str = '100'
    int i = Integer.parseInt(str)
    int i = Integer.valueOf(str)	//也可以直接使用 valueOf
    

集合框架

集合是一种容器,用来装数据的,类似于数组,但集合的大小可变,在开发中也是非常常用。

Collection单列集合

单列集合就是每个元素(数据)只包含一个值。

Collection集合的分类:

image-20251027151711934

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遍历方式

  1. 迭代器iterater():是用来遍历集合的专用方式

    Iterator<String> i = array.iterator();	// 得到迭代器对象
    
    // 取数据
    i.next();
    
    // 使用while循环遍历
    while(i.hasNext()){
        String name = i.next()
        sout(name)
    }
    
  2. 增强for循环

    // 格式:
    for(元素的数据类型 变量名 : 数组或者集合){
    	// 方法体    
    }
    
    // 例子
    for(String name : names){
        sout(name)
    }
    
  3. 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使用方法

  1. 导包

    import java.util.ArrayList;
    import java.util.List;
    
  2. 创建List对象

    // 推荐写法:接口指向实现类
    List<String> list = new ArrayListM<>();
    
    // 其他写法
    ArrayList<String> list = new ArrayList<>();
    
  3. 使用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遍历方式

  1. 通过keySet遍历

    for(String name : ages.keySet()){
    	Integer age = ages.get(name);
    	System.out.println(name + "的年龄:" + age);
    }
    
  2. 遍历Values

    for(Integer age : ages.values()){
    	System.out.println("年龄" +  age)
    }
    
  3. 通过entrySet

    for(Map.Entry<String,Integer> entry : ages.entrySet()){
    	String name = entry.getKey();
    	Integer age = entry.getValue();
    	System.out.println(name + "-->" + age);
    }