Java笔记本

zy123
2025-03-21 /  0 评论 /  0 点赞 /  12 阅读 /  11155 字
最近更新于 09-03

Java笔记本

IDEA基础操作

Intellij Idea创建Java项目:

  1. 创建空项目
  2. 创建Java module
  3. 创建包 package edu.whut.xx
  4. 创建类,类名首字母必须大写!

IDEA快捷键:

Ctrl + L 格式化代码
Ctrl + / 注释/取消注释当前行
Ctrl + D 复制当前行或选中的代码块
Ctrl + N 查找类
shift+shift 在文件中查找代码
alt+ enter “意图操作” “快捷修复” 可以1:service接口类跳转到实现 2:补全函数的返回值

调试快捷键:

快捷键 功能
Shift + F9 调试当前程序
F8 单步执行(不进入方法)
F7 单步执行(进入方法)
Shift + F8 跳出当前方法
Alt + F9 运行到光标处
Ctrl + F2 停止调试
缩写 生成的代码 说明
psvm public static void main(String[] args) {} 生成 main 方法
sout System.out.println(); 打印到控制台
fori for (int i = 0; i < ; i++) {} 生成 for 循环
iter for (Type item : iterable) {} 生成增强 for 循环
new Test().var Test test = new Test(); 自动补全变量声明

从exsiting file中导入模块

方法一:复制整个模块到项目文件夹,并导入模块的 *.iml 文件,这种方式保留了模块原有的配置信息。

方法二:新建一个模块,然后将原模块的 src 文件夹下的包复制过去,这种方式更灵活,可以手动调整模块设置。

删除模块:

模块右键,remove module,这只是把它从项目中移除,然后!!打开模块所在文件夹,物理删除,才是真正完全删除。

转义符的作用

防止字符被误解

  • 在字符串中,一些字符(如 "\)有特殊的含义。例如,双引号用于标识字符串的开始和结束,反斜杠通常用于转义。所以当你希望在字符串中包含这些特殊字符时,你需要使用转义符来告诉解析器这些字符是字符串的一部分,而不是特殊符号。
  • 例如,\" 表示在字符串中包含一个双引号字符,而不是字符串的结束标志。

"Hello \"World\"" => 结果是:Hello "World" (双引号被转义)

"C:\\Program Files\\App" => 结果是:C:\Program Files\App(反斜杠被转义)

如果只是"C:\Program Files\App" 那么路径就会报错

表示非打印字符

  • 转义符可以用于表示一些不可见的或非打印的控制字符,如换行符(\n)、制表符(\t)等。这些字符无法直接通过键盘输入,所以使用转义符来表示它们。

Java基础语法

  1. 二进制:0b 八进制:0 十六进制:0x

  2. System.out.println() 方法中,"ln" 代表 "line",表示换行。因此,println 实际上是 "print line" 的缩写。这个方法会在输出文本后自动换行.

System.out.println("nihao "+1.3331);  #Java 会自动将数值转换为字符串
  1. 一维数组创建:
// 方式1:先声明,再指定长度(默认值为0、null等)
int[] arr1 = new int[10];  // 创建一个长度为10的int数组

// 方式2:使用初始化列表直接创建数组
int[] arr2 = {1, 2, 3, 4, 5};  // 创建并初始化一个包含5个元素的int数组

String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"};

// 方式3:结合new关键字和初始化列表创建数组(常用于明确指定类型时)
int[] arr3 = new int[]{1, 2, 3, 4, 5};  // 与方式2效果相同
  1. 字符串创建
String str = "Hello, World!";  //(1)直接赋值

String str = new String("Hello, World!");   //使用 new 关键字

char[] charArray = {'H', 'e', 'l', 'l', 'o'};  
String str = new String(charArray);			 //通过字符数组创建
  1. switch-case
public class SwitchCaseExample {
    public static void main(String[] args) {
        // 定义一个 int 类型变量,作为 switch 的表达式
        int day = 3;
        String dayName;

        // 根据 day 的值执行相应的分支
        switch(day) {
            case 1:
                dayName = "Monday"; // 当 day 为 1 时
                break; // 结束当前 case
            case 2:
                dayName = "Tuesday"; // 当 day 为 2 时
                break;
            case 3:
                dayName = "Wednesday"; // 当 day 为 3 时
                break;
            case 4:
                dayName = "Thursday"; // 当 day 为 4 时
                break;
            case 5:
                dayName = "Friday"; // 当 day 为 5 时
                break;
            case 6:
                dayName = "Saturday"; // 当 day 为 6 时
                break;
            case 7:
                dayName = "Sunday"; // 当 day 为 7 时
                break;
            default:
                // 如果 day 不在 1 到 7 之间
                dayName = "Invalid day";
        }

        // 输出最终结果
        System.out.println("The day is: " + dayName);
    }
}
  1. 强制类型转换
double sqrted=Math.sqrt(n);
int soft_max=(int) sqrted;
  1. Math库常用方法
Math.pow(3, 2));     
Math.sqrt(9));        
Math.abs(a));        
Math.max(a, b));   
Math.min(a, b));   

枚举

//纯状态枚举  常见于 switch-case、简单条件判断。
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT

}
OperationType opType = OperationType.INSERT; // 声明并初始化

public void execute(OperationType type, Object entity) {
        switch (type) {
            case INSERT:
                insertEntity(entity);
                break;
            case UPDATE:
                updateEntity(entity);
                break;
            default:
                throw new IllegalArgumentException("Unsupported operation: " + type);
        }
    }
// 携带数据的枚举, 适合“常量 + 不变数据”的场景,如 星期、货币、错误码等。
public enum DayOfWeek {
    //创建7个  DayOfWeek 类型的对象,分别传入构造参数chineseName和dayNumber,它们叫“枚举常量”
    MONDAY("星期一", 1),
    TUESDAY("星期二", 2),
    WEDNESDAY("星期三", 3),
    THURSDAY("星期四", 4),
    FRIDAY("星期五", 5),
    SATURDAY("星期六", 6),
    SUNDAY("星期日", 7);

    // 枚举属性
    private final String chineseName;
    private final int dayNumber;

    // 构造方法
    DayOfWeek(String chineseName, int dayNumber) {
        this.chineseName = chineseName;
        this.dayNumber = dayNumber;
    }

    // 方法
    public String getChineseName() {
        return chineseName;
    }

    public int getDayNumber() {
        return dayNumber;
    }
}

// 使用示例
public class Main {
    public static void main(String[] args) {
        DayOfWeek today = DayOfWeek.MONDAY;
        System.out.println(today.getChineseName());  // 输出: 星期一
        System.out.println(today.getDayNumber());    // 输出: 1
    }
}

枚举类构造方法必须是 private的,默认就是private的,这意味着只能在枚举内部使用这个构造方法。

枚举类你只需要使用,而不用创建对象,类内部已经定义好了MONDAY、TUESDAY...对象。

Java传参方式

基本数据类型

  • 传递方式:按传递 每次传递的是变量的值的副本**,对该值的修改不会影响原变量**。例如:intdoubleboolean 等类型。

引用类型(对象)

  • 传递方式:对象引用的副本传递 传递的是对象引用的一个副本,指向同一块内存区域。因此,方法内部通过该引用修改对象的状态,会影响到原对象。如数组、集合、String、以及其他所有对象类型。
  • Integer 属于引用类型,变量 Integer a = 10;中的 a是一个引用,它指向堆中存储的 Integer对象。

注意

StringBuilder s = new StringBuilder();
s.append("hello");
String res = s.toString(); // res = "hello"

s.append(" world");        // s = "hello world"
System.out.println(res);   // 输出还是 "hello"

浅拷贝深拷贝

不可变对象

一个对象一旦被创建并初始化,它的状态(其内部代表的数据)就再也无法被改变,如Integer、String。

不可变”在代码中的具体表现:所有修改操作都返回新对象

String s1 = "Hello";
String s2 = s1.concat(" World"); // 不是修改s1,而是创建新字符串

字符串常量池

String a = "1";
String b = "1";

变量 a和变量 b会指向同一个内存中的 String对象

原理:常量池中有,就直接返回其引用;没有,就创建一个放进去再返回。

存放位置:

  1. Java 7 之前:字符串常量池逻辑上属于方法区(Method Area)运行时常量池(Runtime Constant Pool) 的一部分。而方法区的具体实现是 永久代(PermGen)
    • 问题:永久代大小有限且难以调整,容易发生 OutOfMemoryError: PermGen space
  2. Java 7 开始字符串常量池被从永久代移动到了 Java 堆(Heap) 中。
  3. Java 8 及以后:永久代被彻底移除,取而代之的是元空间(Metaspace)(用于存类元信息、方法码等)。而字符串常量池依然留在堆中

浅拷贝

拷贝对象本身,但内部成员(例如集合中的元素)只是复制引用,新旧对象的内部成员指向同一份内存。如果内部元素是不可变的(如 Integer、String 等),这种拷贝通常足够。如果元素是可变对象,修改其中一个对象可能会影响另一个。

回溯法用的就是浅拷贝,因为List<Integer> path; 中间的Integer是不可变对象。

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

//new ArrayList<>(list)的底层行为:
//1.新建一个空的 ArrayList实例。
//2.将原集合 list中的所有元素引用逐个复制到新集合中。
//3.返回这个新集合。
//4.新集合和原集合是两个完全独立的容器,只是内容(元素引用)相同。
List<Integer> shallowCopy = new ArrayList<>(list);

可变对象,浅拷贝修改对象会出错!

List<Box> list = new ArrayList<>();
list.add(new Box(1));
list.add(new Box(2));
list.add(new Box(3));

List<Box> shallowCopy = new ArrayList<>(list);
shallowCopy.get(0).value = 10;  // 修改 shallowCopy 中第一个 Box 的 value

System.out.println(list);         // 输出: [10, 2, 3],因为同一 Box 对象被修改
System.out.println(shallowCopy);    // 输出: [10, 2, 3]

深拷贝

不仅复制对象本身,还递归地复制其所有内部成员,从而生成一个完全独立的副本。即使内部元素是可变的,修改新对象也不会影响原始对象。

// 深拷贝 List<MyObject> 的例子
List<MyObject> originalList = new ArrayList<>();
originalList.add(new MyObject(10));
originalList.add(new MyObject(20));

List<MyObject> deepCopy = new ArrayList<>();
for (MyObject obj : originalList) {
    deepCopy.add(new MyObject(obj));  // 每个元素都创建一个新的对象
}

image-20250901220834319

日期

在Java中:

  • 代表年月日的类型是 LocalDateLocalDate 类位于 java.time 包下,用于表示没有时区的日期,如年、月、日。
  • 代表年月日时分秒的类型是 LocalDateTimeLocalDateTime 类也位于 java.time 包下,用于表示没有时区的日期和时间,包括年、月、日、时、分、秒。

LocalDateTime.now(),获取当前时间

Lambda表达式

函数式接口:有且仅有一个抽象方法的接口。

@FunctionalInterface 注解:这是一个可选的注解,用于表示接口是一个函数式接口。虽然不是强制的,但它可以帮助编译器识别意图,并检查接口是否确实只有一个抽象方法。

这个时候可以用Lambda代替匿名内部类!!!

public class LambdaExample {
    
    // 定义函数式接口,doSomething 有两个参数
    @FunctionalInterface
    interface MyInterface {
        void doSomething(int a, int b);
    }
    
    public static void main(String[] args) {
        // 使用匿名内部类实现接口方法
        MyInterface obj = new MyInterface() {
            @Override
            public void doSomething(int a, int b) {
                System.out.println("参数a: " + a + ", 参数b: " + b);
            }
        };
        obj.doSomething(5, 10);
    }
    
    public static void main(String[] args) {
        // 使用 Lambda 表达式实现接口方法
        MyInterface obj = (a, b) -> {
            System.out.println("参数a: " + a + ", 参数b: " + b);
        };
        obj.doSomething(5, 10);
    }
}

lambda表达式格式(参数列表) -> { 代码块 }(参数列表) ->表达式

如果上述MyInterface接口的doSomething()方法不接受任何参数并且没有返回值:

// Lambda 表达式(无参数)
MyInterface obj = () -> {
     System.out.println("doSomething 被调用,无参数!");
};

以下是lambda表达式的重要特征:

可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。

可选的参数圆括号():一个参数无需定义圆括号,但无参数或多个参数需要定义圆括号。

可选的大括号{}:如果主体只有一个语句,可以不使用大括号

可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,使用大括号需显示retrun;如果函数是void则不需要返回值。

// 定义一个函数式接口,只有一个抽象方法
interface Calculator {
    int add(int a, int b);
}

public class LambdaReturnExample {
    public static void main(String[] args) {
        // 例子1:单个表达式,不使用大括号和 return 关键字
        Calculator calc1 = (a, b) -> a + b;
        System.out.println("calc1: " + calc1.add(5, 3));  // 输出:8

        // 例子2:使用大括号,需要显式使用 return 关键字
        Calculator calc2 = (a, b) -> {
            return a + b;
        };
        System.out.println("calc2: " + calc2.add(5, 3));  // 输出:8
    }
}

示例1:

list.forEach这个方法接受一个函数式接口作为参数。它只有一个抽象方法 accept(T t)因此,可以使用 lambda 表达式来实现

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
public class Main {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Apple", "Banana", "Cherry", "Date");

        // 使用 Lambda 表达式迭代列表,这段 lambda,就是在“实现” void accept(String item) 这个方法——把每个元素传给 accept,然后打印它。
        list.forEach(item -> System.out.println(item));
    }
}

示例2:为什么可以使用 Lambda 表达式自定义排序

因为**Comparator<T> 是一个函数式接口**,只有一个抽象方法 compare(T o1, T o2)

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);  // 唯一的抽象方法
    
    // 其他方法(如 thenComparing、reversed)都是默认方法或静态方法,不影响函数式接口特性
}
public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Jane", "Adam", "Dana");

        // 使用Lambda表达式排序
        Collections.sort(names, (a, b) -> a.compareTo(b));

        // 输出排序结果
        names.forEach(name -> System.out.println(name));
    }
}

静态成员变量的初始化

静态成员变量属于类级别,在类加载时完成初始化。初始化方式主要有两种:

1.静态初始化块(Static Initialization Block)

例1:

public class MyClass {
    static int num1, num2;
    
    // 第一个静态代码块
    static {
        num1 = 1;
        System.out.println("静态代码块1执行");
    }
    // 第二个静态代码块
    static {
        num2 = 3;
        System.out.println("静态代码块2执行");
    }
    
    // 主方法
    public static void main(String[] args) {
        System.out.println("main方法执行");
    }
}

输出:

静态代码块1执行
静态代码块2执行
main方法执行

说明: 类加载时依次执行所有静态代码块,然后执行 main 方法。

静态初始化块会在类第一次加载到 JVM 时执行一次,用于对静态变量做复杂的初始化。这里的main是MyClass类的静态方法,因此使用这个main方法的时候必然已经加载了MyClass类,所以是这个输出顺序!!!

例2:

class Demo {
    static {
        System.out.println("静态代码块");
    }

    Demo() {
        System.out.println("构造方法");
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println("main开始");
        Demo d1 = new Demo();  // 第一次 new
        Demo d2 = new Demo();  // 第二次 new
    }
}

main开始
静态代码块   // 类第一次被加载时执行一次
构造方法     // 第一次 new 时执行
构造方法     // 第二次 new 时执行

注意,这里使用的是Test类中的main方法进行测试,而不是Demo类中的main方法!!!

2.在声明时直接初始化

public class MyClass {
    // 直接在声明时初始化静态成员变量
    public static int staticVariable = 42;
}

3.通过静态方法赋值(运行时)

必须是静态方法!!!

public class GlobalCounter {
    public static int currentCount;

    // 一个静态方法,用于在运行时初始化或修改静态变量
    public static void initializeCounter(int startValue) {
        currentCount = startValue;
    }

    public static void incrementCounter() {
        currentCount++;
    }
}
// 在程序的其他地方调用:
public class Main {
    public static void main(String[] args) {
        // 在运行时进行初始化赋值
        GlobalCounter.initializeCounter(100);
        System.out.println(GlobalCounter.currentCount); // 输出: 100

        // 在运行时修改其值
        GlobalCounter.incrementCounter();
        System.out.println(GlobalCounter.currentCount); // 输出: 101
    }
}

静态成员变量的访问不需要创建 MyClass 的实例,可以直接通过类名访问:

int value = MyClass.staticVariable;
MyClass obj = new MyClass();
System.out.println("obj.num1 = " + obj.staticVariable);  #通过实例访问也可以

静态方法

静态方法属于类级别,不依赖于任何具体实例

静态方法访问规则:

  • 可以直接访问:

    • 类中的其他静态成员变量。
    • 类中的静态方法。
  • 不能直接访问:

    • 非静态成员变量。

    • 非静态方法(必须通过对象实例访问)。

public class MyClass {
    private static int staticVar = 10;
    private int instanceVar = 20;

    // 静态方法:可以直接访问静态成员
    public static void staticMethod() {
        System.out.println(staticVar); // 正确:访问静态成员变量
        // System.out.println(instanceVar); // 错误:不能直接访问非静态成员变量

        // 如需要访问非静态成员,必须先创建对象实例
        MyClass obj = new MyClass();
        System.out.println(obj.instanceVar); // 正确:通过对象实例访问非静态成员变量
    }

    // 非静态方法:可以访问所有成员
    public void instanceMethod() {
        System.out.println(staticVar);    // 正确:访问静态成员变量
        System.out.println(instanceVar);  // 正确:访问非静态成员变量
    }
}

调用静态方法:

MyClass.staticMethod();   // 通过类名直接调用静态方法

继承与super关键字

继承

class Parent {
    static {
        System.out.println("A");
    }
    Parent() {
        System.out.println("B");
    }
}

class Child extends Parent {
    static {
        System.out.println("C");
    }
    Child() {
        System.out.println("D");
    }
}

加载类时

  • 父类静态代码块先执行
  • 再执行子类静态代码块

实例化子类对象时

  • 调用子类构造函数之前,会先调用父类构造函数
  • 所以顺序是:父类构造 → 子类构造

析构函数是先调用子类,再调父类。

Super关键字

super 关键字有两种主要的使用方法:访问父类的成员和调用父类的构造方法。

1)访问父类的成员

可以使用 super 关键字来引用父类的字段或方法。这在子类中存在同名的字段或方法时特别有用。

因为父类的成员变量和方法都是默认的访问修饰符,可以继承给子类,而子类也定义了同名的 xxx,发生了变量隐藏(shadowing)。

2)调用父类的构造方法

当创建子类对象时,首先会调用父类的构造函数然后再调用子类的构造函数

可以使用 super 关键字调用父类的构造方法。这通常在子类的构造方法中使用,用于显式地调用父类的构造方法。

class Parent {
    int num = 10;  // 父类字段

    // 有参构造方法
    Parent(int num) {
        this.num = num;  // 初始化父类的 num 字段
        System.out.println("Parent class constructor with num = " + num);
    }

    void display() {
        System.out.println("Parent class method");
    }
}

class Child extends Parent {
    int num = 20;  // 子类同名字段,隐藏了父类的 num

    // 有参构造方法,调用父类的有参构造方法
    Child(int num) {
        super(num);  // 调用父类的构造方法并传递参数
        System.out.println("Child class constructor with num = " + num);
    }

    void print() {
        System.out.println("Child class num: " + num);         // 访问子类字段
        System.out.println("Parent class num: " + super.num); // 访问父类被隐藏的字段
        display();                                           // 调用子类重写的方法
        super.display();                                     // 明确调用父类的方法
    }
}

public class Main {
    public static void main(String[] args) {
        // 使用有参构造方法创建对象
        Child obj = new Child(30);  
        System.out.println("---- Now calling print() ----");
        obj.print();
    }
}

运行结果:

Parent class constructor with num = 30
Child class constructor with num = 30
---- Now calling print() ----
Child class num: 20
Parent class num: 30
Parent class method
Parent class method

如果父类写了任何构造函数(无论是有参还是无参),编译器就不会再自动生成默认的无参构造函数

变量修饰符

在Java中,变量的修饰符应该按照规定的顺序出现,通常是这样的:

  1. 访问修饰符:public、protected、private,或者不写(默认为包级访问)。
  2. 非访问修饰符:final、static、abstract、synchronized、volatile等。
  3. 数据类型:变量的数据类型,如 int、String、class 等。
  4. 变量名:变量的名称。
public static final int MAX_COUNT = 100; #定义常量
protected static volatile int counter;  #定义成员变量

虽然final、static都是非访问修饰符,但是一般都是 static final ,不推荐反过来!!!

final关键字

final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:

  1. 修饰类:类不能继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;
  2. 修饰变量:该变量为常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
  3. 修饰符方法:方法不能重写

全限定名

全限定名(Fully Qualified Name,简称 FQN)指的是一个类或接口在 Java 中的完整名称,包括它所在的包名。例如:

  • 对于类 Integer,其全限定名是 java.lang.Integer
  • 对于自定义的类 DeptServiceImpl,如果它位于包 edu.zju.zy123.service.impl 中,那么它的全限定名就是 edu.zju.zy123.service.impl.DeptServiceImpl

使用全限定名可以消除歧义,确保指定的类型在整个项目中唯一无误。

使用场景:

Spring AOP 的 Pointcut 表达式

MyBatis的XML映射文件的namespace属性

synchronized

它的核心在于选择一个对象作为“锁”(也称为“监视器”或“互斥量”)

synchronized (lockObject) {
    // 需要同步的代码块(临界区)
}

  • lockObject:这是一个对象引用,它作为锁。任何Java对象都可以充当锁。
  • { ... }:大括号内的代码就是“临界区”。JVM保证同一时刻,只有一个线程可以持有 lockObject这把锁并执行临界区内的代码。其他试图进入的线程必须等待,直到当前线程释放锁。
分类 写法 锁对象 作用域 示例
同步方法 实例方法 当前实例 (this) 保护实例变量 public synchronized void method() {...}
静态方法 类的Class对象 保护静态变量 public static synchronized void method() {...}
同步代码块 锁实例 指定实例对象 灵活控制临界区 synchronized (this) {...}
synchronized (obj) {...}
锁类 类的Class对象 全局锁,保护静态资源 synchronized (MyClass.class) {...}

MyClass.class是一个特殊的表达式,叫做 “类字面量”MyClass.class的目的就是获取到 MyClass.class类这个唯一的 Class对象。无论创建了多少个 MyClass.class的实例(虽然单例模式只有一个),所有线程在执行到 synchronized (LazySingleton.class)时,都是在竞争同一把锁

同步代码块:

public class AlternatePrint {
    private static int count = 1;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (count <= 100) {
                synchronized (lock) {
                    if (count % 2 == 1) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                        lock.notify();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "Thread-1").start();

        new Thread(() -> {
            while (count <= 100) {
                synchronized (lock) {
                    if (count % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                        lock.notify();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "Thread-2").start();
    }
}

同步方法:同一对象的 synchronized方法是互斥的。

class BankAccount {
    private int balance;

    public BankAccount(int balance) {
        this.balance = balance;
    }

    // 同步方法:锁的是当前实例 (this)
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + " 正在取款:" + amount);
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " 取款失败,余额不足!");
        }
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount(100);

        // 创建两个线程,模拟同时取钱
        Thread t1 = new Thread(() -> account.withdraw(80), "线程A");
        Thread t2 = new Thread(() -> account.withdraw(80), "线程B");

        t1.start();
        t2.start();
    }

}

JAVA面向对象

public class Dog {
    // 成员变量
    private String name;

    // 构造函数
    public Dog(String name) {
        this.name = name;
    }

    // 一个函数:让狗狗“叫”
    public void bark() {
        System.out.println(name + " says: Woof! Woof!");
    }

    // (可选)获取狗狗的名字
    public String getName() {
        return name;
    }

    // 测试主方法
    public static void main(String[] args) {
        Dog myDog = new Dog("Buddy");
        myDog.bark();               // 输出:Buddy says: Woof! Woof!
        System.out.println("Name: " + myDog.getName());
    }
}

访问修饰符

public(公共的)

使用public修饰的成员可以被任何其他类访问,无论这些类是否属于同一个包。 例如,如果一个类的成员被声明为public,那么其他类可以通过该类的对象直接访问该成员。

protected(受保护的)

使用protected修饰的成员可以被同一个包中的其他类访问,也可以被不同包中的子类访问。 与包访问级别相比,protected修饰符提供了更广泛的访问权限。

default (no modifier)(默认的,即包访问级别)

如果没有指定任何访问修饰符,则默认情况下成员具有包访问权限。 在同一个包中的其他类可以访问默认访问级别的成员,但是在不同包中的类不能访问。

private(私有的):

使用private修饰的成员只能在声明它们的类内部访问,其他任何类(子类也不行!)都不能访问这些成员。 这种访问级别提供了最高的封装性和安全性。

如果您在另一个类中实例化了包含私有成员的类,那么您无法直接访问该类的私有成员。但是,您可以通过公共方法来间接地访问和操作私有成员。

public class PrivateExample {
    private int privateVar = 30;
    
    // 公共方法,用于访问私有成员
    public int getPrivateVar() {
        return privateVar;
    }
}

则每个实例都有自己的一份拷贝,只有当变量被声明为 static 时,变量才是类级别的,会被所有实例共享。

修饰符不仅可以用来修饰成员变量和方法,也可以用来修饰类。顶级类只能使用 public 或默认(即不写任何修饰符,称为包访问权限)。内部类可以使用所有访问修饰符(publicprotectedprivate 和默认),这使得你可以更灵活地控制嵌套类的访问范围。

public class OuterClass {
    // 内部类使用private,只能在OuterClass内部访问
    private class InnerPrivateClass {
        // ...
    }
    
    // 内部类使用protected,同包以及其他包中的子类可以访问
    protected class InnerProtectedClass {
        // ...
    }
    
    // 内部类使用默认访问权限,只在同包中可见
    class InnerDefaultClass {
        // ...
    }
    
    // 内部类使用public,任何地方都可访问(但访问时需要通过OuterClass对象)
    public class InnerPublicClass {
        // ...
    }
}

JAVA三大特性

封装

封装指隐藏对象的状态信息(属性),不允许外部对象直接访问对象的内部信息(private实现)。但是可以提供一些可以被外界访问的方法(public)来操作属性。

继承

[修饰符] class 子类名 extends 父类名{
   类体部分
}

//class C extends A, B { } // 错误:C 不能同时继承 A 和 B

Java只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。

Java支持多层继承(A → B → C )。

Java继承了父类非私有的成员变量和成员方法,但是请注意:子类是无法继承父类的构造方法的。

多态

指在面向对象编程中,同样的消息(方法调用)可以在不同的对象上触发不同的行为。

  1. 方法重写(Override):动态多态;子类从父类继承的某个实例方法无法满足子类的功能需要时,需要在子类中对该实例方法进行重新实现,这样的过程称为重写,也叫做覆写、覆盖。

    要求

    • 必须存在继承关系(子类继承父类)。
    • 子类重写的方法的访问修饰符不能比父类更严格(可以相同或更宽松)。
    • 方法名、参数列表和返回值类型必须与父类中的方法完全相同(Java 5 以后支持协变返回类型,即允许返回子类型)。
  2. 向上转型(Upcasting):动态多态;子类对象可以赋值给父类引用,这样做可以隐藏对象的真实类型,只能调用父类中声明的方法

    class Animal {
        public void makeSound() {
            System.out.println("Animal makes sound");
        }
    }
    
    class Dog extends Animal {
        @Override
        public void makeSound() {
            System.out.println("Dog barks");
        }
    
        public void fetch() {
            System.out.println("Dog fetches the ball");
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            Animal animal = new Dog(); // 向上转型
            animal.makeSound(); // 调用的是 Dog 重写的 makeSound() 方法
            // animal.fetch(); // 编译错误:Animal 类型没有 fetch() 方法
        }
    }
    

    多态实现总结:继承 + 重写 + 父类引用指向子类对象 = 多态

  3. 方法重载(Overload):静态多态;方法名相同但参数列表不同。当调用这些方法时,会根据传递的参数类型或数量选择相应的方法。

    参数类型不同参数数量不同参数顺序不同;返回类型可以不同,但仅返回类型不同不构成重载

    void print(int a) { ... }
    void print(String a) { ... } // 合法重载
    
    void log(String msg) { ... }
    void log(String msg, int level) { ... } // 合法重载
    
    void save(int id, String name) { ... }
    void save(String name, int id) { ... } // 合法重载
    

抽象类和接口

抽象类:

可以包含抽象方法(abstract)和具体方法(有方法体)。但至少有一个抽象方法。

注意: 抽象类不能被实例化。抽象类中的抽象方法必须显式地用 abstract 关键字来声明。而接口中的方法不用abstract 。抽象类可以 implements 接口,此时无需定义自己的抽象方法也可以。

抽象类可以实现接口中的所有方法,此时它也可以继续保持 abstract

如果一个子类继承了抽象类,通常必须实现抽象类中的所有抽象方法,否则该子类也必须声明为抽象类。例如:

abstract class Animal {
    // 抽象方法,没有方法体
    public abstract void makeSound();
    
    // 普通方法
    public void sleep() {
        System.out.println("Sleeping...");
    }
}

// 正确:子类实现了所有抽象方法
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

// 错误:如果不实现 makeSound() 方法,则 Dog 必须也声明为抽象类

如何使用抽象类

由于抽象类不能直接实例化,我们通常有两种方法来使用抽象类:

  1. 定义一个新的子类 创建一个子类继承抽象类并实现所有抽象方法,然后使用子类实例化对象:

    Animal animal = new Dog();
    animal.makeSound(); // 输出:Dog barks
    
  2. 使用匿名内部类 使用匿名内部类实现抽象类相当于临时创建了一个未命名的子类,并且立即实例化了这个子类的对象。

    Animal animal = new Animal() {
        @Override
        public void makeSound() {
            System.out.println("Anonymous animal sound");
        }
    };
    animal.makeSound(); // 输出:Anonymous animal sound
    

如何算作实现抽象方法

public interface StrategyHandler<T, D, R> {
    StrategyHandler DEFAULT = (T, D) -> null;
    R apply(T requestParameter, D dynamicContext) throws Exception;
}

public abstract class AbstractStrategyRouter<T, D, R> implements StrategyMapper<T, D, R>, StrategyHandler<T, D, R> {

    @Getter
    @Setter
    protected StrategyHandler<T, D, R> defaultStrategyHandler = StrategyHandler.DEFAULT;

    public R router(T requestParameter, D dynamicContext) throws Exception {
        StrategyHandler<T, D, R> strategyHandler = get(requestParameter, dynamicContext);
        if(null != strategyHandler) return strategyHandler.apply(requestParameter, dynamicContext);
        return defaultStrategyHandler.apply(requestParameter, dynamicContext);
    }

}

这里 AbstractStrategyRouter 属于是定义了普通方法 router ,但是 从接口继承下来的 applyget 方法扔没有实现,将交由继承AbstractStrategyRouter的非抽象子类来实现。

接口(Interface): 定义了一组方法的规范,侧重于行为的约定。接口中的所有方法默认是抽象的(Java 8 之后可包含默认方法和静态方法),不包含成员变量(除了常量)。

interface SmartDevice {
    // 常量
    String DEFAULT_BRAND = "Generic";

    // 抽象方法
    void turnOn();

    // 默认方法
    default void updateFirmware() {
        System.out.println("Downloading firmware update...");
    }

    // 静态方法
    static void checkConnection() {
        System.out.println("Checking network connection...");
    }
}

class SmartLight implements SmartDevice {
    @Override
    public void turnOn() {
        System.out.println("Light is on");
    }
}

public class Main {
    public static void main(String[] args) {
        SmartLight light = new SmartLight();
        light.turnOn();           // 输出: Light is on
        light.updateFirmware();    // 输出: Downloading firmware update...
        
        SmartDevice.checkConnection(); // 输出: Checking network connection...
        System.out.println(SmartDevice.DEFAULT_BRAND); // 输出: Generic
    }
}

抽象类和接口的区别

  1. 方法实现

    接口

    • Java 8 前:所有方法都是抽象方法,只包含方法声明。
    • Java 8 及以后:可包含默认方法(default methods)和静态方法。

    抽象类

    • 可以同时包含抽象方法(不提供实现)和具体方法(提供实现)。
  2. 继承:

    • 类实现接口时,使用关键字 implements
    • 类继承抽象类时,使用关键字 extends
  3. 多继承

    • 类可以实现多个接口(多继承)。
    • 类只能继承一个抽象类(单继承)。

四种内部类

1.成员内部类

定义位置:成员内部类定义在外部类的成员位置

访问权限:可以无限制地访问外部类的所有成员,包括私有成员、静态成员变量

实例化方式:需要先创建外部类的实例,然后才能创建内部类的实例。

修改限制:不能有静态字段和静态方法(除非声明为常量final static)。成员内部类属于外部类的一个实例,不能独立存在于类级别上。

用途:适用于内部类与外部类关系密切,需要频繁访问外部类成员的情况。

public class Outer {
    private static int staticVar = 10; // 外部类静态变量
    private int instanceVar = 20;      // 外部类实例变量

    // 非静态内部类
    class Inner {
        void print() {
            System.out.println("静态变量: " + staticVar); // 直接访问外部类静态变量
            System.out.println("实例变量: " + instanceVar); // 直接访问外部类实例变量
        }
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner(); // 创建内部类实例
        inner.print();
    }
}

2.静态内部类

没有静态类,但是有静态内部类!

定义位置:定义在外部类内部,但使用static修饰。

访问权限:只能直接访问外部类的静态成员,访问非静态成员需要通过外部类实例。

实例化方式:可以直接创建,不需要外部类的实例。

修改限制:可以有自己的静态成员。

用途:适合当内部类工作不依赖外部类实例时使用,常用于实现与外部类关系不那么密切的帮助类。

public class OuterClass {
    // 外部类的静态成员
    private static int staticVar = 10;
    // 外部类的实例成员
    private int instanceVar = 20;
    
    // 静态内部类
    public static class StaticInnerClass {
        public void display() {
            // 可以直接访问外部类的静态成员
            System.out.println("staticVar: " + staticVar);
            // 下面这行代码会报错,因为不能直接访问外部类的实例成员
            // System.out.println("instanceVar: " + instanceVar);
            
            // 如果确实需要访问实例成员,可以通过创建外部类的对象来访问
            OuterClass outer = new OuterClass();
            System.out.println("通过外部类实例访问 instanceVar: " + outer.instanceVar);
        }
    }
    
    public static void main(String[] args) {
        // 直接创建静态内部类的实例,不需要外部类实例
        OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();
        inner.display();
    }
}

静态内部类可以不实例化,直接使用其静态成员,但非静态成员仍需实例化。

3.局部内部类

定义位置:局部内部类定义在一个方法或任何块内(如:if语句、循环语句内)。

访问权限:只能访问所在方法final或事实上的final(即不被后续修改的)局部变量和外部类的成员变量(同成员内部类)。

实例化方式:只能在定义它们的块中创建实例。

修改限制:同样不能有静态字段和方法。

用途:适用于只在方法或代码块中使用的类,有助于将实现细节隐藏在方法内部。

public class OuterClass {
    public void startThread() {
        class LocalInnerClass implements Runnable {
            @override
            public void run() {
                System.out.println("局部内部类中的线程正在运行...");
            }
        }
        LocalInnerClass localInner = new LocalInnerClass();
        Thread thread = new Thread(localInner);
        thread.start();
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.startThread();
    }
}

4.匿名内部类

new 父类/接口() { 
    // 实现或重写方法
    @Override
    void method() { ... }
}

在定义的同时直接实例化,而不需要显式地声明一个子类的名称。

用途:适用于创建一次性使用的实例,通常用于接口或抽象类的实现。但匿名内部类并不限于接口或抽象类,只要是final 的普通类,都有机会通过匿名内部类来“现场”创建一个它的子类实例

abstract class Animal {
    public abstract void makeSound();
}

public class Main {
    public static void main(String[] args) {
        // 匿名内部类:临时创建一个 Animal 的子类并实例化
        Animal dog = new Animal() {  // 注意这里的 new Animal() { ... }
            @Override
            public void makeSound() {
                System.out.println("汪汪汪!");
            }
        };
        dog.makeSound(); // 输出:汪汪汪!
    }
}

如何理解?可以对比普通子类(显式定义),即显示定义了Dog来继承Animal

// 抽象类或接口
abstract class Animal {
    public abstract void makeSound();
}

// 显式定义一个具名的子类
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("汪汪汪!");
    }
}

public class Main {
    public static void main(String[] args) {
        // 实例化具名的子类
        Animal dog = new Dog();
        dog.makeSound(); // 输出:汪汪汪!
    }
}

容器

image-20250820201702360

Collection

在 Java 中,Collection 是一个接口,它表示一组对象的集合。Collection 接口是 Java 集合框架中最基本的接口之一,定义了一些操作集合的通用方法,例如添加、删除、遍历等。

所有集合类(例如 ListSetQueue 等)都直接或间接地继承自 Collection 接口。

  • boolean add(E e):将指定的元素添加到集合中(可选操作)。
  • boolean remove(Object o):从集合中移除指定的元素(可选操作)。
  • boolean contains(Object o):如果集合中包含指定的元素,则返回 true
  • int size():返回集合中的元素个数。
  • void clear():移除集合中的所有元素。
  • boolean isEmpty():如果集合为空,则返回 true
public class CollectionExample {
    public static void main(String[] args) {
        // 创建一个 Collection 对象,使用 ArrayList 作为实现类
        Collection<String> fruits = new ArrayList<>();

        // 添加元素到集合中
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");
        System.out.println("添加元素后集合大小: " + fruits.size());  // 输出集合大小

        // 检查集合是否包含某个元素
        System.out.println("集合中是否包含 'Banana': " + fruits.contains("Banana"));

        // 从集合中移除元素
        fruits.remove("Banana");
        System.out.println("移除 'Banana' 后集合大小: " + fruits.size());

        // 清空集合
        fruits.clear();
        System.out.println("清空集合后,集合是否为空: " + fruits.isEmpty());
    }
}

Iterator

在 Java 中,Iterator 是一个接口,遍历集合元素。Collection 接口中定义了 iterator() 方法,返回一个 Iterator 对象。

Iterator 接口中包含以下主要方法:

  1. hasNext():如果迭代器还有下一个元素,则返回 true,否则返回 false
  2. next():返回迭代器的下一个元素,并将迭代器移动到下一个位置。
  3. remove():从迭代器当前位置删除元素。该方法是可选的,不是所有的迭代器都支持。
import java.util.ArrayList;
import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        // 创建一个 ArrayList 集合
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        int size = list.size(); // 获取列表大小
		System.out.println("Size of list: " + size); // 输出 3

        // 获取集合的迭代器
        Iterator<Integer> iterator = list.iterator();

        // 使用迭代器遍历集合并输出元素
        while (iterator.hasNext()) {
            Integer element = iterator.next();
            System.out.println(element);
        }
    }
}

ArrayList

ArrayListList 接口的一种实现,而 List 接口又继承自 Collection 接口。包括 add()remove()contains() 等。

image-20240227133714509

HashSet

image-20240227150219184

HashMap

image-20240227152019078

 // 使用 entrySet() 方法获取 Map 中所有键值对的集合,并使用增强型 for 循环遍历键值对
System.out.println("Entries in the map:");
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println("Key: " + key + ", Value: " + value);
}

PriorityQueue

默认是小根堆,输出1,2,5,8

import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) {
        // 创建一个 PriorityQueue 对象
        PriorityQueue<Integer> pq = new PriorityQueue<>();

        // 添加元素到队列
        pq.offer(5);
        pq.offer(2);
        pq.offer(8);
        pq.offer(1);

        // 打印队列中的元素
        System.out.println("Elements in the priority queue:");
        while (!pq.isEmpty()) {
            System.out.println(pq.poll());
        }
    }
}
  1. offer() 方法用于将元素插入到队列中
  2. poll() 方法用于移除并返回队列中的头部元素
  3. peek() 方法用于返回队列中的头部元素但不移除它。

JAVA异常处理

public class ExceptionExample {
    // 方法声明中添加 throws 关键字,指定可能抛出的异常类型
    public static void main(String[] args) throws SomeException, AnotherException {
        try {
            // 可能会抛出异常的代码块
            if (someCondition) {
                throw new SomeException("Something went wrong");
            }
        } catch (SomeException e) {
            // 处理 SomeException 异常
            System.out.println("Caught SomeException: " + e.getMessage());
        } catch (AnotherException e) {
            // 处理 AnotherException 异常
            System.out.println("Caught AnotherException: " + e.getMessage());
        } finally {
            // 不管是否发生异常,都会执行的代码块
            System.out.println("End of try-catch block");
        }
    }
}
    // 自定义异常类,继承自 Exception 类
public class SomeException extends Exception {
    // 构造方法,用于设置异常信息
    public SomeException(String message) {
        // 调用父类的构造方法,设置异常信息
        super(message);
    }
}

JAVA泛型

在类、接口或方法定义时,用类型参数来替代具体的类型,编译时检查类型安全,运行时通过类型擦除映射到原始类型。

<T> 用于 定义泛型类型

  • 接口方法 的定义中,<T> 是用来指定一个占位符,表示这个类或方法可以接受任何类型。
  • T 在这里是 类型参数,你可以在类、接口或方法内使用它来代替具体的类型。
public class Box<T> {   // <T> 定义了一个泛型类,T 是类型参数
    private T value;     // 使用 T 来表示某种类型

    public void set(T value) {   // 使用 T 来表示参数类型
        this.value = value;
    }

    public T get() {  // 使用 T 来表示返回类型
        return value;
    }
}

定义一个泛型类

// 定义一个“盒子”类,可以装任何类型的对象
public class Box<T> {
    private T value;

    public Box() {}

    public Box(T value) {
        this.value = value;
    }

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

T 是类型参数(Type Parameter),可任意命名(常见还有 E、K、V 等)。

使用:

public class Main {
    public static void main(String[] args) {
        // 创建一个只装 String 的盒子
        Box<String> stringBox = new Box<>();
        stringBox.set("Hello Generics");
        String s = stringBox.get();  // 自动类型推断为 String
        System.out.println(s);

        // 创建一个只装 Integer 的盒子
        Box<Integer> intBox = new Box<>(123);
        Integer i = intBox.get();
        System.out.println(i);
    }
}

定义一个泛型方法

有时候我们只想让某个方法支持多种类型,而不必为此写泛型类,就可以在方法前加上类型声明:

public class Utils {
    //[修饰符] <T> 返回类型 方法名(参数列表) { … }
    // 泛型方法:打印任意类型的一维数组
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}

方法签名中 <T> 表示这是一个泛型方法,注意这里不是指返回值!!!这个返回值是void!!!

调用时,编译器会根据传入实参自动推断 T

使用

public class Main {
    public static void main(String[] args) {
        String[] names = {"Alice", "Bob", "Charlie"};
        Utils.printArray(names);
        // 等价于 Utils.<String>printArray(names);

        Integer[] nums = {10, 20, 30};
        Utils.printArray(nums);
        // 等价于 Utils.<Integer>printArray(nums);
    }
}

好用的方法

toString()

**Arrays.toString()**转一维数组

**Arrays.deepToString()**转二维数组 这个方法是是用来将数组转换成String类型输出的,入参可以是long,float,double,int,boolean,byte,object 型的数组。

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        // 一维数组示例
        int[] oneD = {1, 2, 3, 4, 5};
        System.out.println("一维数组输出: " + Arrays.toString(oneD));
        
        // 二维数组示例
        int[][] twoD = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };
        // 使用 Arrays.deepToString() 输出二维数组
        System.out.println("二维数组输出: " + Arrays.deepToString(twoD));
    }
}

自定义对象的toString() 方法

每个 Java 对象默认都有 toString() 方法(可以根据需要覆盖)

当直接打印一个没有重写 toString() 方法的对象时,其输出格式通常为:

java.lang.Object@15db9742

当打印重写toString() 方法的对象时:

class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person);  //会自动调用对象的 toString() 方法
        //Person{name='Alice', age=30}
    }
}

对象拷贝属性

 public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        //对象属性拷贝
        BeanUtils.copyProperties(employeeDTO, employee,"id");
 }

employeeDTO的内容拷贝给employee,跳过字段为"id"的属性。

StartOrStopDTO dto = new StartOrStopDTO(1, 100L);
        // 用 Builder 拷贝 id 和 status
Employee employee = Employee.builder()
                            .id(dto.getId())
                            .status(dto.getStatus())
                            .build();

Java 8 Stream API

SpaceUserRole role = SPACE_USER_AUTH_CONFIG.getRoles()
    .stream()                                 // 1
    .filter(r -> r.getKey().equals(spaceUserRole)) // 2
    .findFirst()                              // 3
    .orElse(null);                            // 4

stream()List<SpaceUserRole> 转换成一个 Stream<SpaceUserRole>,Stream 是 Java 8 引入的对集合进行函数式操作的管道。

.filter(r -> r.getKey().equals(spaceUserRole)) filter 接受一个 Predicate<T>(这里是从每个 SpaceUserRole r 中调用 r.getKey().equals(...)),只保留“满足该条件”的元素,其余都丢弃。

.findFirst() 在过滤后的流中,取第一个元素,返回一个 Optional<SpaceUserRole>。即使流是空的,它也会返回一个空的 Optional,而不会抛异常。

.orElse(null)Optional 中取值:如果存在就返回该值,不存在就返回 null

等价于下面的老式写法(Java 7 及以前):

SpaceUserRole role = null;
for (SpaceUserRole r : SPACE_USER_AUTH_CONFIG.getRoles()) {
    if (r.getKey().equals(spaceUserRole)) {
        role = r;
        break;
    }
}

类加载器和获取资源文件路径

在Java中,类加载器的主要作用是根据**类路径(Classpath)**加载类文件以及其他资源文件。

启动类加载器(Bootstrap ClassLoader):加载 Java 运行时环境的核心类库,包括 Java 的标准库和 JVM 必须的类,比如 java.lang.* 包中的类。

扩展类加载器:加载 Java 扩展目录中的类库,这些库通常是 ext 目录中的 JAR 文件(例如,$JAVA_HOME/jre/lib/ext/)。

系统类加载器:加载应用程序的类路径(classpath)下的类和资源文件,通常用于加载项目中的类和 JAR 包。

自定义类加载器:这个类加载器是由开发者自定义实现的,用于加载非标准的类或动态加载类。它是 Java 提供的类加载机制的扩展,允许你根据特定需求来自定义类加载的行为。

双亲委派机制的基本原理:

双亲委派机制的核心思想是:一个类加载器在加载类时,首先将请求委托给它的父类加载器,只有当父类加载器无法加载该类时,当前加载器才会自己去加载

原因:

1.避免类的重复加载

2.保证核心类的安全性,如 java.lang.Object 类等是 Java 核心类库的一部分,必须由启动类加载器(Bootstrap ClassLoader)加载。

类路径是JVM在运行时用来查找类文件和资源文件的一组目录或JAR包。在许多项目(例如Maven或Gradle项目)中,src/main/resources目录下的内容在编译时会被复制到输出目录(如target/classes),src/main/java 下编译后的 class 文件也会放到这里。

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           └── App.java
│   └── resources/
│       ├── application.yml
│       └── static/
│           └── logo.png
└── test/
    ├── java/
    │   └── com/
    │       └── example/
    │           └── AppTest.java
    └── resources/
        └── test-data.json

映射到 target/ 后:

target/
├── classes/                    ← 主代码和资源的输出根目录
│   ├── com/
│   │   └── example/
│   │       └── App.class       ← 编译自 src/main/java/com/example/App.java
│   ├── application.yml         ← 复制自 src/main/resources/application.yml
│   └── static/
│       └── logo.png            ← 复制自 src/main/resources/static/logo.png
└── test-classes/               ← 测试代码和测试资源的输出根目录
    ├── com/
    │   └── example/
    │       └── AppTest.class   ← 编译自 src/test/java/com/example/AppTest.java
    └── test-data.json          ← 复制自 src/test/resources/test-data.json


// 获取 resources 根目录下的 emp.xml 文件路径
String empFileUrl = this.getClass().getClassLoader().getResource("emp.xml").getFile(); 

// 获取 resources/static 目录下的 tt.img 文件路径
URL resourceUrl = getClass().getClassLoader().getResource("static/tt.img");
String ttImgPath = resourceUrl != null ? resourceUrl.getFile() : null;

  1. this.getClass():获取当前对象(即调用该代码的对象)的 Class 对象。
  2. .getClassLoader():获取该 Class 对象的类加载器(ClassLoader)。
  3. .getResource("emp.xml"):从类路径中获取名为 "emp.xml" 的资源,并返回一个 URL 对象,该 URL 对象指向 "emp.xml" 文件的位置。
  4. .getFile():从 URL 对象中获取文件路径部分,即获取 "emp.xml" 文件的绝对路径字符串。

**类路径(Classpath)**是 Java 虚拟机(JVM)用于查找类文件和其他资源文件的一组路径。

是的,类加载器的主要作用之一确实是从类路径中加载类文件(.class 文件)以及其他资源(如图片、配置文件等)。在 Java 项目启动时,类加载器不仅会加载类文件,还会把这些类文件转换为 Java 程序可以使用的 Class 对象,并将它们放入 运行时数据区,即 方法区(Method Area)堆区(Heap)

反射

反射技术的关键之一是能够在 运行时动态加载 类的字节码并将其转换为 Class 对象,并以编程的方法解刨出类中的各个成分(成员变量、方法、构造器等)。

.class 是静态的文件,加载到内存并解析后,就会创建一个对应的 java.lang.Class 实例即 Class对象,是动态的。

1668575796295

反射技术例子:IDEA通过反射技术就可以获取到类中有哪些方法,并且把方法的名称以提示框的形式显示出来,所以你能看到这些提示了。

1668576426355

获取类的字节码(Class对象)

有三种方法:

public class Test1Class{
    public static void main(String[] args){
        Class c1 = Student.class;
        System.out.println(c1.getName()); //获取全类名:edu.whut.pojo.Student
        System.out.println(c1.getSimpleName()); //获取简单类名: Student
        
        Class c2 = Class.forName("edu.whut.pojo.Student"); //全类名
        System.out.println(c1 == c2); //true
        
        Student s = new Student();
        Class c3 = s.getClass();
        System.out.println(c2 == c3); //true
    }
}

类的 Class 对象(字节码对象)是类的模具,不会直接存放数据。

类的 实例对象obj是用 new 出来的,它代表着 实际的运行时对象,会在堆(Heap)里开辟内存,存放实例字段的数据。每次 new 一下,就会有一个独立的实例,它们共享同一个 Class,但各自的数据独立。

1.获取类的元信息

  • 类名getName()getSimpleName()getPackage()
  • 父类和接口getSuperclass()getInterfaces()
  • 修饰符getModifiers()(配合 Modifier 工具类解析 public、private、abstract 等)
  • 注解getAnnotation() / getAnnotations() 获取类上的注解!!!

2.获取类的构造器

定义类

public class Cat{
    private String name;
    private int age;
    
    public Cat(){}
    
    private Cat(String name, int age){
    }
}

获取构造器列表

public class TestConstructor {
    
    @Test
    public void testGetAllConstructors() {
        // 1. 获取类的 Class 对象
        Class<?> c = Cat.class;
        
        // 2. 获取类的全部构造器(包括public、private等)
        Constructor<?>[] constructors = c.getDeclaredConstructors();
        
        // 3. 遍历并打印构造器信息
        for (Constructor<?> constructor : constructors) {
            System.out.println(
                constructor.getName() + 
                " --> 参数个数:" + constructor.getParameterCount()
            );
        }
    }
}

c.getDeclaredConstructors() 会返回所有声明的构造器(包含私有构造器),而 c.getConstructors() 只会返回公共构造器。

constructor.getParameterCount() 用于获取该构造器的参数个数。

获取某个构造器:指定参数类型!

public class Test2Constructor(){
    @Test
    public void testGetConstructor(){
        //1、反射第一步:必须先得到这个类的Class对象
        Class c = Cat.class;
          
        /2、获取private修饰的有两个参数的构造器,第一个参数String类型,第二个参数int类型
        Constructor constructor = c.getDeclaredConstructor(String.class,int.class);
        
        constructor.setAccessible(true);  //禁止检查访问权限,可以使用private构造函数
        Cat cat=(Cat)constructor.newInstance("叮当猫",3);   //初始化Cat对象

    }
}

c.getDeclaredConstructor(String.class, int.class):根据参数列表获取特定的构造器。

如果构造器是private修饰的,先需要调用setAccessible(true) 表示禁止检查访问控制,然后再调用newInstance(实参列表) 就可以执行

构造器,完成对象的初始化了。

Constructor 本身就是用来创建对象实例的,它的职责是生成实例,而不是操作某个已经存在的实例。

3.获取类的成员变量

获取类的成员变量

方法 说明
public Field[] getFields() 获取类的全部成员变量(只能获取 public 修饰的)
public Field[] getDeclaredFields() 获取类的全部成员变量(只要存在就能拿到)
public Field getField(String name) 获取类的某个成员变量(只能获取 public 修饰的)
public Field getDeclaredField(String name) 获取类的某个成员变量(只要存在就能拿到)

设置与获取字段值

方法 说明
void set(Object obj, Object value) 设置字段值
Object get(Object obj) 获取字段值
public void setAccessible(boolean flag) 设置为 true,表示禁止检查访问控制(暴力反射)

不管是设置值还是获取值,都需要:

  1. 获取 Field 对象 —— 先通过 Class 对象拿到目标字段的 Field 实例。

  2. 指定目标实例 —— 操作字段时必须传入具体的对象实例,告诉 JVM 要修改或读取哪一个对象的该字段。

  3. 处理访问权限 —— 如果字段是私有的,需要调用 setAccessible(true) 来关闭 Java 的访问检查(俗称“暴力反射”)。

import java.lang.reflect.Field;

public class ReflectionExample {

    // 示例类
    public static class MyClass {
        public String publicField = "Public Field";
        private String privateField = "Private Field";
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 创建 MyClass 的实例
        MyClass obj = new MyClass();

        // 获取 MyClass 的 Class 对象
        Class<?> clazz = obj.getClass();

        // 获取 public 字段
        Field publicField = clazz.getField("publicField");
        System.out.println("Public Field: " + publicField.get(obj));  // 获取并输出 publicField 的值

        // 获取 private 字段(使用 getDeclaredField)
        Field privateField = clazz.getDeclaredField("privateField");

        // 设置私有字段为可访问(通过 setAccessible)
        privateField.setAccessible(true);
        System.out.println("Private Field: " + privateField.get(obj));  // 获取并输出 privateField 的值

        // 修改 private 字段的值
        privateField.set(obj, "New Private Value");
        System.out.println("Updated Private Field: " + privateField.get(obj));  // 获取修改后的值
    }
}

4.获取类的成员方法

1668580761089

获取单个指定的成员方法:第一个参数填方法名、第二个参数填方法中的参数类型

1668581678388

执行:第一个参数传入一个对象实例,然后是若干方法参数(无参可不写)...

1668581800777

示例:Cat 类与测试类

public class Cat {
    private String name;
    public int age;

    public Cat() {
        this.name = "Tom";
        this.age = 1;
    }

    public void meow() {
        System.out.println("Meow! My name is " + this.name);
    }

    private void purr() {
        System.out.println("Purr... I'm a happy cat!");
    }
}
public class FieldReflectionTest {
    
    @Test
    public void testMethodAccess() throws Exception {
        // 1. 获取 Cat 类的 Class 对象
        Class<?> catClass = Cat.class;

        // 2. 创建 Cat 对象实例
        Cat cat = new Cat();

        // ----------------------
        // A. 获取并调用 public 方法
        // ----------------------
        // 获取名为 "meow"、无参数的方法
        Method meowMethod = catClass.getMethod("meow");
        // 调用该方法
        meowMethod.invoke(cat);

        // ----------------------
        // B. 获取并调用 private 方法
        // ----------------------
        // 获取名为 "purr"、无参数的私有方法
        Method purrMethod = catClass.getDeclaredMethod("purr");
        purrMethod.setAccessible(true);  // 关闭权限检查
        purrMethod.invoke(cat);
    }
}

注解

在 Java 中,注解用于给程序元素(类、方法、字段等)添加元数据,这些元数据可被编译器、工具或运行时反射读取,以实现配置、检查、代码生成以及框架支持(如依赖注入、AOP 等)功能,而不直接影响代码的业务逻辑。

比如:Junit框架的 @Test 注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。

再比如:@Override 注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。

使用注解

元注解

是修饰注解的注解

@Retention(RetentionPolicy.SOURCE)  //只在源码阶段保留,编译后 `.class` 文件中不会有这个注解信息。
@Retention(RetentionPolicy.RUNTIME)  //指定注解的生命周期,即在运行时有效,可用于反射等用途。

@Target(ElementType.TYPE)     //类上的注解(包含类、接口、枚举等类型)
@Target(ElementType.METHOD)     //方法上的注解
@Target(ElementType.FIELD)     //字段上的注解

注意,若想在运行时通过反射读取,只能设置@Retention(RetentionPolicy.RUNTIME)

定义注解

使用 @interface 定义注解

// 定义注解
@Retention(RetentionPolicy.RUNTIME)  // 生命周期:运行时保留,可反射获取
@Target(ElementType.METHOD)           // 目标:作用于方法
public @interface MyAnnotation {
    String description() default "This is a default description";
    int value() default 0;
}

用法:

// 1. 只传 value,可省略属性名
@MyAnnotation(5)
public void someMethod() {}

// 2. 多属性赋值必须指明名称
@MyAnnotation(value = 5, description = "Specific description")
public void anotherMethod() {}

// 3. 使用默认值
@MyAnnotation
public void defaultMethod() {}

解析注解

在 Java 中,注解本质上是类的元数据,要在运行时获取注解信息,必须依赖反射 API 来读取。

下面示例展示了如何通过反射获取方法上的自定义注解 @MyAnnotation

1.定义示例类与注解

// 自定义注解
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可反射获取
@Target(ElementType.METHOD)         // 仅可作用于方法
public @interface MyAnnotation {
    String value();
}

// 使用注解
public class MyClass {
    @MyAnnotation(value = "specific value")
    public void myMethod() {
        // 方法实现
    }
}

2.通过反射获取注解

import java.lang.reflect.Method;

public class AnnotationReader {

    public static void main(String[] args) throws NoSuchMethodException {
        // 获取MyClass的Class对象
        Class<MyClass> obj = MyClass.class;

        // 获取myMethod方法的Method对象
        Method method = obj.getMethod("myMethod");

        // 获取方法上的MyAnnotation注解实例
        MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);

        if (annotation != null) {
            // 输出注解的value值
            System.out.println("注解的value: " + annotation.value());
        }
    }
}
// 反射调用链:
// Class → 通过类加载器获取类的运行时描述对象;
// Method → 从 Class 对象中获取方法的反射对象;
// getAnnotation() → 从方法反射对象中获取注解实例。

3.快速判断注解是否存在

if (method.isAnnotationPresent(MyAnnotation.class)) {
    // 如果存在MyAnnotation注解,则执行相应逻辑
}

Junit 单元测试

步骤

1.导入依赖 将 JUnit 框架的 jar 包添加到项目中(注意:IntelliJ IDEA 默认集成了 JUnit,无需手动导入)。

2.编写测试类

  • 为待测业务方法创建对应的测试类。
  • 测试类中定义测试方法,要求方法必须为 public 且返回类型为 void

3.添加测试注解 在测试方法上添加 @Test 注解,确保 JUnit 能自动识别并执行该方法。

4.运行测试 在测试方法上右键选择“JUnit运行”。

  • 测试通过显示绿色标志;
  • 测试失败显示红色标志。
public class UserMapperTest {
    @Test
	public void testListUser() {
    	UserMapper userMapper = new UserMapper();
    	List<User> list = userMapper.list();
    	Assert.assertNotNull("User list should not be null", list);
   		list.forEach(System.out::println);
	}
}

注意,如果需要使用依赖注入,需要在测试类上加@SpringBootTest注解

它会启动 Spring 应用程序上下文,并在测试期间模拟运行整个 Spring Boot 应用程序。这意味着你可以在集成测试中使用 Spring 的各种功能,例如自动装配、依赖注入、配置加载

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testListUser() {
        List<User> list = userMapper.list();
        Assert.assertNotNull("User list should not be null", list);
        list.forEach(System.out::println);
    }
}

写了@Test注解,那么该测试函数就可以直接运行!若一个测试类中写了多个测试方法,可以全部执行!

image-20240307173454288

原理可能是:

//自定义注解
@Retention(RetentionPolicy.RUNTIME) //指定注解在运行时可用,这样才能通过反射获取到该注解。
@Target(ElementType.METHOD)  //指定注解可用于方法上。
public @interface MyTest {
}

public class AnnotationTest4 {
    
    @MyTest
    public void test() {
        System.out.println("===test4===");
    }

    public static void main(String[] args) throws Exception {
        AnnotationTest4 instance = new AnnotationTest4();

        // 1. 获取 Class 对象
        Class<?> clazz = AnnotationTest4.class;

        // 2. 获取类中声明的所有方法
        Method[] methods = clazz.getDeclaredMethods();

        // 3. 遍历方法,执行带 @MyTest 的方法
        for (Method method : methods) {
            if (method.isAnnotationPresent(MyTest.class)) {
                method.invoke(instance); // 反射调用方法
            }
        }
    }
}

在Springboot中,如何快速生成单元测试?

选中类名,右键: image-20240815093428359

© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
取消