Java笔记本

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

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,这只是把它从项目中移除,然后!!打开模块所在文件夹,物理删除,才是真正完全删除。

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));   

转义符的作用

防止字符被误解

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

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

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

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

表示非打印字符

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

枚举

//纯状态枚举  常见于 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(),获取当前时间

静态成员变量与静态方法

一、静态成员变量

  • 静态成员变量使用 static 修饰,属于类级别,而非对象,即所有对象共享同一份静态变量。
  • 类加载时完成初始化,且只执行一次
  • 可以通过类名直接访问,无需创建对象。

二、静态变量的初始化方式

1)静态代码块,用于对静态变量进行复杂初始化,在类第一次加载到 JVM 时执行一次

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() 是静态方法,调用前类已被加载,因此静态代码块先执行。

示例 2:静态代码块 vs 构造方法

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();
        Demo d2 = new Demo();
    }
}

输出:

main开始
静态代码块
构造方法
构造方法

说明:静态代码块仅在类首次加载时执行一次,构造方法在每次 new 对象时执行。

2)声明时直接初始化

public class MyClass {
    public static int staticVariable = 42;
}

类加载时 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
    }
}

只有 静态方法 才能在不创建对象的情况下访问或修改静态变量。

三、静态方法

  • static 修饰的方法,属于类本身,不依赖任何实例。

  • 通过类名直接调用,无需创建对象。

public class MathUtils {
    public static int square(int n) {
        return n * n;
    }
}

int result = MathUtils.square(5); // 直接通过类名调用

四、访问静态成员变量

int value = MyClass.staticVariable;   // ✅ 推荐
MyClass obj = new MyClass();
System.out.println(obj.staticVariable); // ✅ 可行,但不推荐

静态成员属于类,与对象无关;推荐使用类名访问。

变量修饰符final、static...

在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这把锁并执行临界区内的代码。其他试图进入的线程必须等待,直到当前线程释放锁。

常见使用形式

类型 写法 锁对象 用途
同步方法(实例) public synchronized void foo() this 保护实例变量
同步方法(静态) public static synchronized void foo() 类对象(ClassName.class 保护静态变量
同步代码块 synchronized (obj) {...} 任意对象 灵活控制锁粒度

1)同步代码块:

class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
            System.out.println(Thread.currentThread().getName() + " -> " + count);
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Runnable task = counter::increment;
        new Thread(task, "A").start();
        new Thread(task, "B").start();
    }
}

2)同步方法

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + " -> " + count);
    }
}

public class Test {
    public static void main(String[] args) {
        Counter c = new Counter();
        for (int i = 0; i < 5; i++) {
            new Thread(c::increment).start();
        }
    }
}

synchronized 修饰实例方法时,锁的是当前对象(this)。 同一个对象的同步方法是互斥的

同一个对象(p1),T1T2 都调用 p1.print(),因为 synchronized 锁的是 p1,所以 T1 和 T2 必须一个执行完,另一个才能进入方法

不同对象(p2),T3T4 调用的是另一个对象 p2.print()

因此,T1T2 顺序执行T3T4 也顺序执行,它们之间互不影响。

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));
    }
}

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) { ... } // 合法重载
    

优点:提高代码的灵活性和可扩展性。通过统一接口调用不同实现,提高可替换性。

继承与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"); }
}

加载类时

A   // 父类静态块先执行
C   // 再执行子类静态块

实例化子类对象时

B   // 父类构造函数先执行
D   // 再执行子类构造函数

记忆口诀:静态先父后子,构造先父后子。(析构则相反,先子后父)

new Child()的时候-> 输出顺序为 ACBD

Super关键字

super 用于在子类中访问父类的内容:

1)访问父类的成员

当子类与父类存在同名成员时,子类中的成员会**隐藏(shadow)**父类的同名成员。可用 super 显式访问父类版本。

2)调用父类的构造方法

创建子类对象时,一定会先调用父类构造函数。若父类没有无参构造函数,则子类必须显式调用父类构造函数,否则编译报错。使用 super(参数) 调用父类构造方法。

示例:

class Parent {
    int num = 10;

    Parent(int num) {
        this.num = num;
        System.out.println("Parent constructor: num = " + num);
    }

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

class Child extends Parent {
    int num = 20;

    Child(int num) {
        super(num);  // 调用父类构造函数
        System.out.println("Child constructor: num = " + num);
    }

    void print() {
        System.out.println("Child num: " + num);       // 子类字段
        System.out.println("Parent num: " + super.num); // 父类字段
        super.display();  // 调用父类方法
    }
}

public class Main {
    public static void main(String[] args) {
        Child obj = new Child(30);
        System.out.println("---- 调用 print() ----");
        obj.print();
    }
}

运行结果:

Parent constructor: num = 30
Child constructor: num = 30
---- 调用 print() ----
Child num: 20
Parent num: 30
Parent method

注意:若父类定义了构造函数(无论是否有参),编译器不会再自动生成默认无参构造函数

如果父类仅有有参构造函数,子类必须显式调用 super(...) 来选择合适的父类构造函数。

如果有有参+无参,那子类可以不显示调用super(...),系统会自动帮你调用父类的无参构造函数。

抽象类

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");
    }
}

如何使用抽象类

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

  1. 通过子类继承并实现抽象方法:

    Animal animal = new Dog();
    animal.makeSound(); // 输出:Dog barks
    
  2. 使用匿名内部类

    Animal a = new Animal() {
        @Override
        public void makeSound() {
            System.out.println("Anonymous sound");
        }
    };
    

如何算作实现抽象方法

  • 抽象类的子类必须实现全部抽象方法否则该子类也必须声明为抽象类
  • 实现接口时,可选择性实现方法,未实现的交由子类完成。

接口

一组行为规范契约。接口中的变量默认是 public static final 常量。方法默认是 public abstract(Java 8 之后可包含 defaultstatic 方法)。

interface SmartDevice {
    String DEFAULT_BRAND = "Generic";

    void turnOn(); // 抽象方法

    default void updateFirmware() {
        System.out.println("Updating firmware...");
    }

    static void checkConnection() {
        System.out.println("Checking network...");
    }
}

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

接口特性

  • 支持 多继承(一个类可以实现多个接口)。
  • 接口之间可以使用 extends 继承。
  • 默认方法(default)可提供接口级的默认实现。

抽象类 vs 接口

特性 抽象类 接口
关键字 abstract + class interface
实例化 ❌ 不能 ❌ 不能
方法类型 抽象 + 具体 抽象(默认)、默认方法、静态方法
变量 普通成员变量 public static final 常量
继承 extends(单继承) implements(多实现)
关系 可实现接口 可被类实现、可继承接口
适用场景 表示**“是什么”**(共性与实现) 表示**“能做什么”**(行为约定)

四种内部类

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();
    }
}

易错题:

class A {
    static class B {
        void hello() {
            System.out.println("hello from static A.B");
        }
    }
}

class C implements A.B {
    public void hello() {
        System.out.println("hello from C");
    }
}

会编译报错,A.B 是一个 非静态内部类(inner class),它隐式地依赖外部类 A 的实例。

2.静态内部类

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

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

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

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

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

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

public class Solution {
    // 外部类的静态成员
    private static String author = "AlgoMaster";
    // 外部类的实例成员
    private int solveCount = 0;

    // 静态内部类:二叉树节点
    public static class TreeNode {
        int val;
        TreeNode left, right;

        TreeNode(int val) { 
            this.val = val; 
        }

        public void showInfo() {
            // ✅ 可以直接访问外部类的静态成员
            System.out.println("Created by: " + author);

            // ❌ 不能直接访问外部类的实例成员
            // System.out.println("solveCount: " + solveCount);
    }

    // 算法函数:递归求最大深度
    public int maxDepth(TreeNode root) {
        solveCount++; // 访问实例成员
        if (root == null) return 0;
        return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
    }

    // 测试
    public static void main(String[] args) {
        // ✅ 静态内部类可以脱离外部类实例独立创建
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);

        // 展示静态内部类对外部类成员的访问特性
        root.showInfo();

        // 使用外部类实例来跑算法
        Solution sol = new Solution();
        System.out.println("Max Depth: " + sol.maxDepth(root)); // 输出 3
    }
}
  • 如果把辅助类直接写成顶级类,可能导致包里出现一堆“小工具类”。
  • 把它们作为静态内部类放到外部类里,可以让代码结构更紧凑,命名更直观。

例子:在算法题里定义 TreeNodeListNode 常作为 Solution 的静态内部类。

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

JAVA笔试题总结

1)构造函数用于初始化当前类的对象,不能被继承。子类会调用父类的构造函数(通过 super),但这不是继承。

2)try-finally

public static int func() {
    try {
        return 1; // 这个返回值被"吞噬"
    } finally {
        return 0; // 这个返回值覆盖了try中的值
    }
}
// 结果: 返回 0

因此极不推荐在 finally 中使用 return

3)关于 Java 的包和部署,下列说法正确的是(单选题)?

A. Java 虚拟机在使用 JAR 的 class 之前必须先将其解压缩

B. 编译包中的类时,完整名称必须和目录结构一致

C. 同一源文件中的不同类可以放到不同的包中

D. JAR 是保存 .class 文件的标准目录

解析:

  • A 错误:JVM 可以直接读取 JAR 中的 .class 文件,无需解压(JAR 本质是 ZIP 格式,JVM 能直接加载)。
  • B 正确:Java 要求包名必须与文件目录结构完全匹配(例如 com.example.Test需放在 com/example/Test.java中),否则编译失败。
  • C 错误:同一 .java 源文件中的多个类默认属于同一个包,且只能有一个 public 类,不能拆分到不同包中。
  • D 错误:JAR 是压缩文件(归档格式),不是标准目录;标准目录是文件系统本身的文件夹结构。
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
取消