Java面试八股文-基础篇

1. Java语言特性

1.1 Java的跨平台特性是如何实现的?

Java的跨平台特性主要通过JVM(Java虚拟机)实现,具体过程如下:

  1. 编译阶段:Java源代码(.java文件)通过javac编译器编译成字节码(.class文件),字节码是一种与平台无关的中间代码。
  2. 运行阶段:不同平台(如Windows、Linux、macOS)都有对应的JVM实现,JVM负责将字节码解释或编译为本地机器码执行。
  3. JVM的作用:JVM作为中间层,屏蔽了不同平台的硬件和操作系统差异,使得相同的字节码能够在不同平台上运行。

这种”一次编写,到处运行”的特性是Java的核心优势之一,也是Java能够在企业级应用中广泛应用的重要原因。

示例

1
2
3
4
5
6
// 编写一个简单的Java程序
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}

编译后生成的.class文件可以在任何安装了JVM的平台上运行,无需重新编译。

1.2 Java中的八大基本数据类型

Java中的八大基本数据类型可以分为四大类:整数类型、浮点类型、字符类型和布尔类型。具体如下:

数据类型 字节数 范围 默认值 示例
byte 1 -128 ~ 127 0 byte b = 100;
short 2 -32768 ~ 32767 0 short s = 1000;
int 4 -2^31 ~ 2^31-1 0 int i = 100000;
long 8 -2^63 ~ 2^63-1 0L long l = 100000L;
float 4 约±3.40282347E+38F 0.0f float f = 3.14f;
double 8 约±1.7976931348623157E+308 0.0d double d = 3.14;
char 2 0 ~ 65535 ‘\u0000’ char c = 'A';
boolean 1位 true/false false boolean b = true;

注意事项

  • 整数类型默认是int,浮点数默认是double
  • 声明long类型时,需要在数值后面加L或l
  • 声明float类型时,需要在数值后面加F或f
  • char类型可以存储一个Unicode字符,包括中文字符

示例

1
2
3
4
5
6
7
8
// 基本数据类型的使用
byte age = 25;
int salary = 10000;
long population = 1000000000L;
float pi = 3.14f;
double e = 2.71828;
char grade = 'A';
boolean isStudent = true;

1.3 什么是自动装箱和拆箱?

自动装箱(Autoboxing)是指Java编译器自动将基本数据类型转换为对应的包装类对象的过程。例如,将int转换为Integer,将double转换为Double等。

自动拆箱(Unboxing)是指Java编译器自动将包装类对象转换为对应的基本数据类型的过程。例如,将Integer转换为int,将Double转换为double等。

原理

  • 自动装箱时,编译器会调用包装类的valueOf()方法
  • 自动拆箱时,编译器会调用包装类的xxxValue()方法(如intValue()、doubleValue()等)

示例

1
2
3
4
5
6
7
8
9
10
11
12
// 自动装箱
Integer i = 100; // 相当于 Integer i = Integer.valueOf(100);
Double d = 3.14; // 相当于 Double d = Double.valueOf(3.14);

// 自动拆箱
int j = i; // 相当于 int j = i.intValue();
double e = d; // 相当于 double e = d.doubleValue();

// 在集合中的应用
List<Integer> list = new ArrayList<>();
list.add(100); // 自动装箱,将int转换为Integer
int k = list.get(0); // 自动拆箱,将Integer转换为int

注意事项

  • 自动装箱和拆箱会带来一定的性能开销,在大量操作时应注意优化
  • 包装类对象可能为null,自动拆箱时可能会导致NullPointerException
  • 对于Integer,valueOf()方法会缓存-128到127之间的整数对象,这可能会导致一些意想不到的结果

示例

1
2
3
4
5
6
7
8
9
// 缓存的影响
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true,因为127在缓存范围内

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false,因为128不在缓存范围内
System.out.println(c.equals(d)); // true,equals()方法比较的是值

1.4 Java中的访问修饰符有哪些?

Java中的访问修饰符用于控制类、方法、属性的访问权限,共有四种:

修饰符 本类 同包 子类 其他包
private
default(默认)
protected
public

详细说明

  • private:最严格的访问权限,只能在定义它的类内部访问。适用于类内部的辅助方法或属性。
  • default(默认):没有显式声明访问修饰符时的默认权限,只能在同一个包内访问。
  • protected:可以在同一个包内访问,也可以被不同包中的子类访问。适用于需要被子类继承的方法或属性。
  • public:最宽松的访问权限,可以在任何地方访问。适用于对外提供的接口或方法。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// 父类
package com.example.parent;

public class Parent {
private int privateField = 1;
int defaultField = 2; // 默认访问修饰符
protected int protectedField = 3;
public int publicField = 4;

private void privateMethod() {
System.out.println("private method");
}

void defaultMethod() {
System.out.println("default method");
}

protected void protectedMethod() {
System.out.println("protected method");
}

public void publicMethod() {
System.out.println("public method");
}
}

// 同包子类
package com.example.parent;

public class SamePackageChild extends Parent {
public void accessFields() {
// 可以访问 privateField 吗?不能,因为 private 只能在本类访问
// System.out.println(privateField); // 编译错误

// 可以访问 defaultField 吗?可以,因为同包
System.out.println(defaultField); // 输出 2

// 可以访问 protectedField 吗?可以,因为是子类
System.out.println(protectedField); // 输出 3

// 可以访问 publicField 吗?可以,因为 public 可以在任何地方访问
System.out.println(publicField); // 输出 4
}
}

// 不同包子类
package com.example.child;

import com.example.parent.Parent;

public class DifferentPackageChild extends Parent {
public void accessFields() {
// 可以访问 privateField 吗?不能
// System.out.println(privateField); // 编译错误

// 可以访问 defaultField 吗?不能,因为不同包
// System.out.println(defaultField); // 编译错误

// 可以访问 protectedField 吗?可以,因为是子类
System.out.println(protectedField); // 输出 3

// 可以访问 publicField 吗?可以
System.out.println(publicField); // 输出 4
}
}

// 不同包的非子类
package com.example.other;

import com.example.parent.Parent;

public class DifferentPackageNonChild {
public void accessFields() {
Parent parent = new Parent();

// 可以访问 privateField 吗?不能
// System.out.println(parent.privateField); // 编译错误

// 可以访问 defaultField 吗?不能
// System.out.println(parent.defaultField); // 编译错误

// 可以访问 protectedField 吗?不能,因为不是子类
// System.out.println(parent.protectedField); // 编译错误

// 可以访问 publicField 吗?可以
System.out.println(parent.publicField); // 输出 4
}
}

注意事项

  • 类只能使用 public 或默认访问修饰符
  • 接口中的方法和常量默认是 public 的
  • 局部变量不能使用访问修饰符
  • 访问修饰符的选择应遵循”最小权限原则”,即尽可能使用限制更严格的访问修饰符,以提高代码的安全性和可维护性

2. 面向对象编程

2.1 什么是面向对象编程?

面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,它以对象为中心,将数据和操作数据的方法封装在一起,通过对象之间的交互来实现功能。

OOP的核心思想

  • 一切皆对象:将现实世界中的事物抽象为程序中的对象
  • 封装:将数据和方法封装在对象中,对外提供接口
  • 继承:通过继承实现代码复用
  • 多态:通过多态实现接口复用

OOP的优点

  • 可维护性:代码结构清晰,易于理解和维护
  • 可复用性:通过继承和多态实现代码复用
  • 可扩展性:通过面向接口编程,易于扩展功能
  • 安全性:通过封装保护数据,提高代码的安全性

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 定义一个Person类
public class Person {
// 封装属性
private String name;
private int age;

// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}

// 封装方法
public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int age) {
if (age > 0 && age < 150) {
this.age = age;
}
}

public int getAge() {
return age;
}

// 行为方法
public void sayHello() {
System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
}
}

// 使用Person类
public class Main {
public static void main(String[] args) {
// 创建对象
Person person = new Person("Alice", 25);

// 调用方法
person.sayHello(); // 输出: Hello, my name is Alice, I'm 25 years old.

// 修改属性
person.setAge(26);
person.sayHello(); // 输出: Hello, my name is Alice, I'm 26 years old.
}
}

2.2 什么是封装、继承和多态?

2.2.1 封装(Encapsulation)

封装是指将对象的状态(属性)和行为(方法)封装在一起,通过访问修饰符控制外部对对象内部状态的访问权限。

封装的优点

  • 安全性:保护对象的内部状态,防止外部代码随意修改
  • 可维护性:隐藏实现细节,只对外提供公共接口
  • 灵活性:可以在不影响外部代码的情况下修改内部实现

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class BankAccount {
// 私有属性,外部无法直接访问
private String accountNumber;
private double balance;

// 构造方法
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}

// 公共方法,用于存款
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("存款成功,当前余额:" + balance);
} else {
System.out.println("存款金额必须大于0");
}
}

// 公共方法,用于取款
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("取款成功,当前余额:" + balance);
} else {
System.out.println("取款金额无效");
}
}

// 公共方法,用于获取余额
public double getBalance() {
return balance;
}
}

// 使用BankAccount类
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("123456", 1000.0);

// 可以通过公共方法访问和修改状态
account.deposit(500.0); // 存款成功,当前余额:1500.0
account.withdraw(200.0); // 取款成功,当前余额:1300.0
System.out.println("当前余额:" + account.getBalance()); // 当前余额:1300.0

// 不能直接访问私有属性
// account.balance = 2000.0; // 编译错误
}
}

2.2.2 继承(Inheritance)

继承是指子类继承父类的属性和方法,实现代码复用的机制。

继承的优点

  • 代码复用:子类可以继承父类的属性和方法,避免重复代码
  • 扩展性:子类可以在父类的基础上添加新的属性和方法
  • 多态的基础:继承是实现多态的必要条件

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 父类
public class Animal {
protected String name;

public Animal(String name) {
this.name = name;
}

public void eat() {
System.out.println(name + " is eating");
}

public void sleep() {
System.out.println(name + " is sleeping");
}
}

// 子类继承父类
public class Dog extends Animal {
public Dog(String name) {
super(name); // 调用父类构造方法
}

// 重写父类方法
@Override
public void eat() {
System.out.println(name + " is eating bones");
}

// 子类特有方法
public void bark() {
System.out.println(name + " is barking");
}
}

// 子类继承父类
public class Cat extends Animal {
public Cat(String name) {
super(name); // 调用父类构造方法
}

// 重写父类方法
@Override
public void eat() {
System.out.println(name + " is eating fish");
}

// 子类特有方法
public void meow() {
System.out.println(name + " is meowing");
}
}

// 使用继承
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy");
dog.eat(); // 输出: Buddy is eating bones
dog.sleep(); // 输出: Buddy is sleeping
dog.bark(); // 输出: Buddy is barking

Cat cat = new Cat("Kitty");
cat.eat(); // 输出: Kitty is eating fish
cat.sleep(); // 输出: Kitty is sleeping
cat.meow(); // 输出: Kitty is meowing
}
}

2.2.3 多态(Polymorphism)

多态是指同一方法在不同对象上有不同的行为表现。Java中的多态分为两种:

  1. 编译时多态(静态多态):通过方法重载实现
  2. 运行时多态(动态多态):通过方法重写实现

多态的条件

  • 继承关系
  • 方法重写
  • 父类引用指向子类对象

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 父类
public class Animal {
public void makeSound() {
System.out.println("Animal makes sound");
}
}

// 子类
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}

// 子类
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat meows");
}
}

// 使用多态
public class Main {
public static void main(String[] args) {
// 父类引用指向子类对象
Animal animal1 = new Dog();
Animal animal2 = new Cat();

// 运行时多态:根据实际对象类型调用对应方法
animal1.makeSound(); // 输出: Dog barks
animal2.makeSound(); // 输出: Cat meows

// 编译时多态:方法重载
printInfo(10); // 输出: Integer: 10
printInfo("Hello"); // 输出: String: Hello
}

// 方法重载
public static void printInfo(int num) {
System.out.println("Integer: " + num);
}

public static void printInfo(String str) {
System.out.println("String: " + str);
}
}

2.3 什么是抽象类和接口?

2.3.1 抽象类(Abstract Class)

抽象类是使用abstract关键字修饰的类,它具有以下特点:

  • 不能实例化:抽象类不能直接创建对象,只能被继承
  • 可以包含抽象方法:使用abstract关键字修饰的方法,没有方法体,需要子类实现
  • 可以包含非抽象方法:抽象类可以包含已经实现的方法
  • 可以包含构造方法:抽象类的构造方法用于子类初始化

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 定义抽象类
public abstract class Animal {
// 抽象方法
public abstract void makeSound();

// 非抽象方法
public void eat() {
System.out.println("Animal is eating");
}

// 构造方法
public Animal() {
System.out.println("Animal constructor");
}
}

// 继承抽象类
public class Dog extends Animal {
// 实现抽象方法
@Override
public void makeSound() {
System.out.println("Dog barks");
}

// 可以重写非抽象方法
@Override
public void eat() {
System.out.println("Dog is eating bones");
}
}

// 使用
public class Main {
public static void main(String[] args) {
// 不能直接实例化抽象类
// Animal animal = new Animal(); // 编译错误

// 实例化子类
Animal dog = new Dog();
dog.makeSound(); // 输出: Dog barks
dog.eat(); // 输出: Dog is eating bones
}
}

2.3.2 接口(Interface)

接口是使用interface关键字定义的抽象类型,它具有以下特点:

  • 不能实例化:接口不能直接创建对象,只能被实现
  • Java 8之前:只能包含抽象方法和常量
  • Java 8及以后:可以包含默认方法(default method)和静态方法(static method)
  • 方法默认是public abstract:接口中的方法默认具有public abstract修饰符
  • 常量默认是public static final:接口中的常量默认具有public static final修饰符

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 定义接口
public interface Animal {
// 常量
String CATEGORY = "MAMMAL";

// 抽象方法
void makeSound();

// Java 8+ 默认方法
default void eat() {
System.out.println("Animal is eating");
}

// Java 8+ 静态方法
static void sleep() {
System.out.println("Animal is sleeping");
}
}

// 实现接口
public class Dog implements Animal {
// 实现抽象方法
@Override
public void makeSound() {
System.out.println("Dog barks");
}

// 可以重写默认方法
@Override
public void eat() {
System.out.println("Dog is eating bones");
}
}

// 使用
public class Main {
public static void main(String[] args) {
// 不能直接实例化接口
// Animal animal = new Animal(); // 编译错误

// 实例化实现类
Animal dog = new Dog();
dog.makeSound(); // 输出: Dog barks
dog.eat(); // 输出: Dog is eating bones

// 调用接口的静态方法
Animal.sleep(); // 输出: Animal is sleeping

// 访问接口的常量
System.out.println(Animal.CATEGORY); // 输出: MAMMAL
}
}

2.4 抽象类和接口的区别?

抽象类和接口的区别

特性 抽象类 接口
关键字 abstract class interface
构造方法
方法实现 可以包含非抽象方法 Java 8之前只能包含抽象方法,Java 8及以后可以包含默认方法和静态方法
方法修饰符 可以有各种访问修饰符 方法默认是public abstract
变量 可以有各种类型的变量 只能包含public static final常量
继承关系 单继承,一个类只能继承一个抽象类 多实现,一个类可以实现多个接口
设计理念 强调”is-a”关系,用于代码复用 强调”has-a”关系,用于定义行为规范

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 抽象类示例
public abstract class Vehicle {
// 普通变量
protected String brand;

// 构造方法
public Vehicle(String brand) {
this.brand = brand;
}

// 抽象方法
public abstract void start();

// 非抽象方法
public void stop() {
System.out.println(brand + " is stopping");
}
}

// 接口示例
public interface Flyable {
// 常量
int MAX_HEIGHT = 10000;

// 抽象方法
void fly();

// 默认方法
default void land() {
System.out.println("Landing...");
}
}

// 继承抽象类并实现接口
public class Airplane extends Vehicle implements Flyable {
public Airplane(String brand) {
super(brand);
}

@Override
public void start() {
System.out.println(brand + " is starting");
}

@Override
public void fly() {
System.out.println(brand + " is flying at " + MAX_HEIGHT + " meters");
}
}

// 使用
public class Main {
public static void main(String[] args) {
Airplane airplane = new Airplane("Boeing 747");
airplane.start(); // 输出: Boeing 747 is starting
airplane.fly(); // 输出: Boeing 747 is flying at 10000 meters
airplane.land(); // 输出: Landing...
airplane.stop(); // 输出: Boeing 747 is stopping
}
}

使用场景

  • 抽象类:当多个类有共同的父类,并且需要共享代码时使用
  • 接口:当多个类需要实现相同的行为,但它们之间没有继承关系时使用

3. 字符串操作

3.1 String、StringBuffer和StringBuilder的区别?

String、StringBuffer和StringBuilder是Java中用于处理字符串的三个类,它们的主要区别如下:

特性 String StringBuffer StringBuilder
可变性 不可变 可变 可变
线程安全 安全(不可变) 安全(同步方法) 不安全
效率 低(每次修改都创建新对象) 中(同步开销) 高(无同步开销)
适用场景 字符串不经常修改的场景 多线程环境下的字符串操作 单线程环境下的字符串操作

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// String示例
String str = "Hello";
str = str + " World";
// 实际上创建了3个String对象:"Hello"、" World"、"Hello World"
System.out.println(str); // 输出: Hello World

// StringBuffer示例
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World");
// 只创建了1个StringBuffer对象
System.out.println(sb.toString()); // 输出: Hello World

// StringBuilder示例
StringBuilder sb2 = new StringBuilder("Hello");
sb2.append(" World");
// 只创建了1个StringBuilder对象
System.out.println(sb2.toString()); // 输出: Hello World

// 性能比较
long startTime = System.currentTimeMillis();

// String操作
String s = "";
for (int i = 0; i < 10000; i++) {
s += i;
}
long stringTime = System.currentTimeMillis() - startTime;
System.out.println("String time: " + stringTime + "ms");

// StringBuffer操作
startTime = System.currentTimeMillis();
StringBuffer sb3 = new StringBuffer();
for (int i = 0; i < 10000; i++) {
sb3.append(i);
}
long stringBufferTime = System.currentTimeMillis() - startTime;
System.out.println("StringBuffer time: " + stringBufferTime + "ms");

// StringBuilder操作
startTime = System.currentTimeMillis();
StringBuilder sb4 = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb4.append(i);
}
long stringBuilderTime = System.currentTimeMillis() - startTime;
System.out.println("StringBuilder time: " + stringBuilderTime + "ms");

注意事项

  • 如果字符串不经常修改,使用String
  • 如果在多线程环境下操作字符串,使用StringBuffer
  • 如果在单线程环境下操作字符串,使用StringBuilder(性能最好)

3.2 如何判断两个字符串是否相等?

在Java中,判断两个字符串是否相等有两种方法:

3.2.1 使用equals()方法

作用:判断两个字符串的内容是否相等
适用场景:比较字符串的实际内容

示例

1
2
3
4
5
6
7
String str1 = "Hello";
String str2 = new String("Hello");
String str3 = "Hello";

// 使用equals()方法比较内容
System.out.println(str1.equals(str2)); // 输出: true
System.out.println(str1.equals(str3)); // 输出: true

3.2.2 使用==运算符

作用:判断两个字符串的引用是否指向同一个对象
适用场景:比较字符串对象的内存地址

示例

1
2
3
4
5
6
7
String str1 = "Hello";
String str2 = new String("Hello");
String str3 = "Hello";

// 使用==运算符比较引用
System.out.println(str1 == str2); // 输出: false,因为str1和str2指向不同的对象
System.out.println(str1 == str3); // 输出: true,因为str1和str3指向字符串常量池中的同一个对象

3.2.3 字符串常量池

字符串常量池是Java堆内存中一个特殊的区域,用于存储字符串常量。当创建字符串时,JVM会先检查字符串常量池中是否已存在相同内容的字符串:

  • 如果存在,直接返回常量池中的引用
  • 如果不存在,创建新的字符串并放入常量池

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 直接赋值,使用字符串常量池
String str1 = "Hello"; // 在常量池中创建"Hello"
String str2 = "Hello"; // 直接使用常量池中的"Hello"
System.out.println(str1 == str2); // 输出: true

// 使用new关键字,不使用字符串常量池
String str3 = new String("Hello"); // 在堆中创建新对象
String str4 = new String("Hello"); // 在堆中创建另一个新对象
System.out.println(str3 == str4); // 输出: false

// 使用intern()方法,强制使用字符串常量池
String str5 = new String("Hello").intern();
System.out.println(str1 == str5); // 输出: true

注意事项

  • 通常情况下,我们需要比较的是字符串的内容,所以应该使用equals()方法
  • 只有在需要比较字符串对象是否相同时,才使用==运算符
  • equals()方法是Object类的方法,String类重写了该方法以比较字符串内容
  • 对于null字符串,使用equals()方法会抛出NullPointerException,因此在比较前应该先检查null

3.3 String的intern()方法有什么作用?

String的intern()方法是Java中用于字符串常量池操作的重要方法,其主要作用是:

1. 作用

  • 将字符串添加到字符串常量池中
  • 返回常量池中的引用
  • 如果常量池中已存在该字符串,则直接返回常量池中的引用

2. 工作原理

  • 当调用intern()方法时,JVM会检查字符串常量池中是否已存在该字符串
  • 如果存在,直接返回常量池中的引用
  • 如果不存在,将该字符串添加到常量池中,然后返回引用

3. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 示例1:基本使用
String str1 = new String("Hello"); // 在堆中创建对象
String str2 = str1.intern(); // 将"Hello"添加到常量池并返回引用
String str3 = "Hello"; // 直接从常量池获取引用

System.out.println(str1 == str2); // 输出: false,因为str1是堆中的对象,str2是常量池中的引用
System.out.println(str2 == str3); // 输出: true,因为str2和str3都是常量池中的引用

// 示例2:字符串比较
String s1 = "Java";
String s2 = new String("Java");
String s3 = new String("Java").intern();

System.out.println(s1 == s2); // 输出: false
System.out.println(s1 == s3); // 输出: true

// 示例3:性能优化
long startTime = System.currentTimeMillis();

// 不使用intern()
String[] arr = new String[100000];
for (int i = 0; i < 100000; i++) {
arr[i] = new String("String" + i % 100); // 会创建100000个对象
}
long time1 = System.currentTimeMillis() - startTime;

// 使用intern()
startTime = System.currentTimeMillis();
String[] arr2 = new String[100000];
for (int i = 0; i < 100000; i++) {
arr2[i] = new String("String" + i % 100).intern(); // 只创建100个对象
}
long time2 = System.currentTimeMillis() - startTime;

System.out.println("不使用intern(): " + time1 + "ms");
System.out.println("使用intern(): " + time2 + "ms");

4. 优缺点

优点

  • 减少内存使用:相同内容的字符串只存储一份
  • 提高字符串比较效率:可以使用==运算符进行比较

缺点

  • 可能会增加常量池的内存使用
  • 频繁调用intern()可能会影响性能

5. 使用场景

  • 当需要大量存储相同内容的字符串时
  • 当需要频繁比较字符串时
  • 当需要确保字符串引用相同时

6. 注意事项

  • 在Java 6及之前,字符串常量池位于永久代,空间有限
  • 在Java 7及之后,字符串常量池移至堆中,空间更大
  • 过度使用intern()可能会导致常量池溢出

4. 异常处理

4.1 Java中的异常体系

Java的异常体系是基于类层次结构的,所有异常都继承自Throwable类。异常体系主要分为两大类:

1. 异常层次结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Throwable
├── Error
│ ├── VirtualMachineError
│ │ ├── OutOfMemoryError
│ │ └── StackOverflowError
│ └── AWTError
└── Exception
├── RuntimeException
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ClassCastException
│ └── ArithmeticException
└── CheckedException
├── IOException
│ ├── FileNotFoundException
│ └── IOException
├── SQLException
└── ClassNotFoundException

2. 异常分类

(1) Error(错误)

  • 表示严重的系统错误,程序无法恢复
  • 由JVM抛出,如OutOfMemoryError、StackOverflowError
  • 不需要捕获和处理

(2) Exception(异常)

  • 表示程序运行时的异常情况,程序可以捕获和处理
  • 分为受检异常和非受检异常

3. 受检异常与非受检异常

特性 受检异常(Checked Exception) 非受检异常(Unchecked Exception)
继承关系 继承自Exception但不是RuntimeException 继承自RuntimeException
编译检查 编译器强制要求处理 编译器不强制要求处理
处理方式 必须使用try-catch捕获或throws声明 可以不处理,也可以处理
常见例子 IOException、SQLException、ClassNotFoundException NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException

4. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 受检异常示例
import java.io.FileInputStream;
import java.io.IOException;

public class CheckedExceptionExample {
public static void main(String[] args) {
// 方式1:使用try-catch捕获
try {
FileInputStream fis = new FileInputStream("test.txt");
fis.close();
} catch (IOException e) {
e.printStackTrace();
}

// 方式2:使用throws声明
try {
readFile();
} catch (IOException e) {
e.printStackTrace();
}
}

public static void readFile() throws IOException {
FileInputStream fis = new FileInputStream("test.txt");
fis.close();
}
}

// 非受检异常示例
public class UncheckedExceptionExample {
public static void main(String[] args) {
// 不需要强制处理
String str = null;
System.out.println(str.length()); // 会抛出NullPointerException

int[] arr = new int[5];
System.out.println(arr[10]); // 会抛出ArrayIndexOutOfBoundsException

int a = 10;
int b = 0;
System.out.println(a / b); // 会抛出ArithmeticException
}
}

5. 异常处理的原则

  • 只捕获可以处理的异常
  • 对于无法处理的异常,应该向上抛出
  • 不要捕获所有异常(如catch (Exception e))
  • 异常处理应该尽可能具体
  • 应该在finally块中释放资源
  • 不要在finally块中使用return语句,会覆盖try或catch中的return值

4.2 try-catch-finally的执行顺序?

try-catch-finally是Java中用于异常处理的核心结构,其执行顺序如下:

1. 基本执行顺序

  1. 首先执行try块中的代码
  2. 如果try块中发生异常,执行对应的catch
  3. 无论是否发生异常,finally块都会执行
  4. 如果trycatch中有return语句,finally块会在return之前执行

2. 详细执行流程

情况1:try块中无异常

  • 执行try块中的代码
  • 执行finally块中的代码
  • 执行try块后的代码

情况2:try块中有异常,且被catch捕获

  • 执行try块中异常之前的代码
  • 执行对应的catch块中的代码
  • 执行finally块中的代码
  • 执行catch块后的代码

情况3:try块中有异常,且未被catch捕获

  • 执行try块中异常之前的代码
  • 执行finally块中的代码
  • 异常向上抛出

3. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// 示例1:try块中无异常
public class TryCatchFinallyExample1 {
public static void main(String[] args) {
try {
System.out.println("Executing try block");
int result = 10 / 2;
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Executing catch block");
e.printStackTrace();
} finally {
System.out.println("Executing finally block");
}
System.out.println("Executing after try-catch-finally");
}
}
// 输出:
// Executing try block
// Result: 5
// Executing finally block
// Executing after try-catch-finally

// 示例2:try块中有异常,且被catch捕获
public class TryCatchFinallyExample2 {
public static void main(String[] args) {
try {
System.out.println("Executing try block");
int result = 10 / 0; // 抛出ArithmeticException
System.out.println("Result: " + result); // 不会执行
} catch (ArithmeticException e) {
System.out.println("Executing catch block");
e.printStackTrace();
} finally {
System.out.println("Executing finally block");
}
System.out.println("Executing after try-catch-finally");
}
}
// 输出:
// Executing try block
// Executing catch block
// java.lang.ArithmeticException: / by zero
// at TryCatchFinallyExample2.main(TryCatchFinallyExample2.java:5)
// Executing finally block
// Executing after try-catch-finally

// 示例3:try块中有异常,且未被catch捕获
public class TryCatchFinallyExample3 {
public static void main(String[] args) {
try {
System.out.println("Executing try block");
int result = 10 / 0; // 抛出ArithmeticException
System.out.println("Result: " + result); // 不会执行
} catch (NullPointerException e) {
System.out.println("Executing catch block"); // 不会执行,因为捕获的是NullPointerException
e.printStackTrace();
} finally {
System.out.println("Executing finally block");
}
System.out.println("Executing after try-catch-finally"); // 不会执行,因为异常未被捕获
}
}
// 输出:
// Executing try block
// Executing finally block
// Exception in thread "main" java.lang.ArithmeticException: / by zero
// at TryCatchFinallyExample3.main(TryCatchFinallyExample3.java:5)

// 示例4:try或catch中有return语句
public class TryCatchFinallyExample4 {
public static int test() {
try {
System.out.println("Executing try block");
return 1;
} catch (Exception e) {
System.out.println("Executing catch block");
return 2;
} finally {
System.out.println("Executing finally block");
}
}

public static void main(String[] args) {
int result = test();
System.out.println("Result: " + result);
}
}
// 输出:
// Executing try block
// Executing finally block
// Result: 1

// 示例5:finally块中的return会覆盖try或catch中的return
public class TryCatchFinallyExample5 {
public static int test() {
try {
System.out.println("Executing try block");
return 1;
} catch (Exception e) {
System.out.println("Executing catch block");
return 2;
} finally {
System.out.println("Executing finally block");
return 3; // 会覆盖try中的return
}
}

public static void main(String[] args) {
int result = test();
System.out.println("Result: " + result);
}
}
// 输出:
// Executing try block
// Executing finally block
// Result: 3

4. 注意事项

  • finally块中的代码总是会执行,除非在trycatch块中调用了System.exit()
  • 不要在finally块中使用return语句,会覆盖trycatch中的return
  • 应该在finally块中释放资源,如关闭文件、数据库连接等
  • try-with-resources语句(Java 7+)可以自动关闭实现了AutoCloseable接口的资源,简化代码

4.3 throw和throws的区别?

throw和throws是Java中用于异常处理的两个关键字,它们的区别如下:

1. 基本区别

特性 throw throws
位置 方法内部 方法声明处
作用 抛出具体的异常对象 声明方法可能抛出的异常类型
数量 一次只能抛出一个异常对象 可以声明多个异常类型
语法 throw new Exception(); public void method() throws Exception1, Exception2
执行 会立即终止当前方法的执行 不会终止方法的执行,只是声明异常

2. 详细说明

(1) throw

  • 用于在方法内部抛出具体的异常对象
  • 抛出异常后,当前方法的执行会立即终止
  • 抛出的异常需要被捕获或向上传递
  • 可以抛出受检异常或非受检异常

(2) throws

  • 用于在方法声明处声明该方法可能抛出的异常类型
  • 声明异常后,方法的执行不会立即终止
  • 声明的异常需要由调用者处理或继续向上传递
  • 只能声明受检异常,非受检异常不需要声明

3. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// throw示例
public class ThrowExample {
public static void main(String[] args) {
try {
divide(10, 0);
} catch (ArithmeticException e) {
System.out.println("Caught exception: " + e.getMessage());
}
}

public static int divide(int a, int b) {
if (b == 0) {
// 抛出具体的异常对象
throw new ArithmeticException("Division by zero");
}
return a / b;
}
}

// throws示例
import java.io.IOException;

public class ThrowsExample {
public static void main(String[] args) {
try {
readFile();
} catch (IOException e) {
System.out.println("Caught exception: " + e.getMessage());
}
}

// 声明方法可能抛出IOException
public static void readFile() throws IOException {
// 可能会抛出IOException的代码
throw new IOException("File not found");
}
}

// 结合使用throw和throws
public class ThrowThrowsExample {
public static void main(String[] args) {
try {
validateAge(15);
} catch (IllegalArgumentException e) {
System.out.println("Caught exception: " + e.getMessage());
}
}

// 声明方法可能抛出IllegalArgumentException
public static void validateAge(int age) throws IllegalArgumentException {
if (age < 18) {
// 抛出具体的异常对象
throw new IllegalArgumentException("Age must be at least 18");
}
System.out.println("Age is valid");
}
}

4. 注意事项

  • throw抛出的异常如果是受检异常,必须在方法声明中使用throws声明,或者在方法内部使用try-catch捕获
  • throws声明的异常必须是Exception或其子类
  • 对于非受检异常(RuntimeException及其子类),不需要在方法声明中使用throws声明
  • 使用throwthrows时,应该选择合适的异常类型,避免使用过于宽泛的异常类型

5. 其他基础问题

5.1 Java中的main方法为什么是public static void?

Java中的main方法是程序的入口点,其签名为public static void main(String[] args),每个部分都有其特定的作用:

1. 各部分的作用

(1) public

  • 访问修饰符,确保JVM可以从外部访问该方法
  • 如果使用private或protected,JVM将无法访问该方法,导致程序无法启动

(2) static

  • 静态修饰符,确保JVM可以直接调用该方法,不需要创建对象
  • 因为在程序启动时,还没有创建任何对象,所以必须使用static

(3) void

  • 返回值类型,表明main方法不返回任何值
  • 因为main方法是程序的入口点,JVM不需要其返回值

(4) main

  • 方法名,JVM规定的固定名称,不能修改

(5) String[] args

  • 方法参数,用于接收命令行参数
  • args是一个字符串数组,存储从命令行传入的参数

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MainMethodExample {
// 标准的main方法签名
public static void main(String[] args) {
System.out.println("Hello, World!");

// 打印命令行参数
System.out.println("Command line arguments:");
for (int i = 0; i < args.length; i++) {
System.out.println("Argument " + (i + 1) + ": " + args[i]);
}
}

// 错误的main方法签名
// private static void main(String[] args) { // 访问权限不足
// System.out.println("Hello");
// }

// 错误的main方法签名
// public void main(String[] args) { // 缺少static
// System.out.println("Hello");
// }

// 错误的main方法签名
// public static int main(String[] args) { // 返回值类型错误
// System.out.println("Hello");
// return 0;
// }
}

3. 命令行参数示例

假设编译后的类名为MainMethodExample,运行时可以传入命令行参数:

1
java MainMethodExample arg1 arg2 arg3

输出:

1
2
3
4
5
Hello, World!
Command line arguments:
Argument 1: arg1
Argument 2: arg2
Argument 3: arg3

4. 注意事项

  • main方法的签名必须严格按照public static void main(String[] args)的格式
  • args参数的名称可以修改,但类型必须是String[]
  • 在Java 5及以后,args参数可以使用可变参数语法:public static void main(String... args)
  • main方法可以被其他方法调用,但通常不这样做,因为它是程序的入口点
  • 如果一个类中没有main方法,JVM将无法直接运行该类

5.2 什么是Java的反射机制?

Java的反射机制是指在运行时动态获取类的信息(如类名、属性、方法等)并操作对象的能力。通过反射,程序可以在运行时加载、探测和使用编译时完全未知的类。

1. 反射的核心类

  • Class:表示类的类型信息
  • Constructor:表示类的构造方法
  • Method:表示类的方法
  • Field:表示类的属性
  • Modifier:表示修饰符

2. 反射的主要功能

  • 在运行时获取类的信息:类名、父类、接口、属性、方法等
  • 在运行时创建对象:通过构造方法创建实例
  • 在运行时调用方法:调用对象的方法,包括私有方法
  • 在运行时访问属性:访问和修改对象的属性,包括私有属性
  • 在运行时动态加载类:加载编译时未知的类

3. 反射的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 1. 获取Class对象
Class<?> clazz = Class.forName("java.lang.String");

// 2. 获取类的信息
System.out.println("Class name: " + clazz.getName());
System.out.println("Super class: " + clazz.getSuperclass().getName());

// 3. 获取构造方法并创建对象
Constructor<?> constructor = clazz.getConstructor(String.class);
Object obj = constructor.newInstance("Hello, Reflection!");
System.out.println("Created object: " + obj);

// 4. 获取方法并调用
Method method = clazz.getMethod("length");
int length = (int) method.invoke(obj);
System.out.println("Length: " + length);

// 5. 获取属性(String类的value属性是私有字段)
Field field = clazz.getDeclaredField("value");
field.setAccessible(true); // 设置可访问私有字段
char[] value = (char[]) field.get(obj);
System.out.println("Value: " + new String(value));

// 6. 动态加载类
Class<?> dynamicClass = Class.forName("java.util.ArrayList");
Object list = dynamicClass.newInstance();
Method addMethod = dynamicClass.getMethod("add", Object.class);
addMethod.invoke(list, "Item 1");
addMethod.invoke(list, "Item 2");
Method sizeMethod = dynamicClass.getMethod("size");
int size = (int) sizeMethod.invoke(list);
System.out.println("List size: " + size);
}
}

4. 反射的优缺点

优点

  • 灵活性高:可以在运行时动态操作类和对象
  • 解耦:减少类之间的依赖关系
  • 可扩展性:便于框架的设计和实现
  • 可以操作私有成员:突破访问限制

缺点

  • 性能开销:反射操作比直接调用慢
  • 代码可读性差:反射代码通常比较复杂
  • 安全隐患:可能会破坏封装性
  • 编译时类型检查缺失:容易出现运行时错误

5. 反射的应用场景

  • 框架设计:如Spring、Hibernate等框架广泛使用反射
  • 动态代理:实现AOP等功能
  • 序列化和反序列化:如JSON、XML等格式的转换
  • 类加载器:动态加载类
  • 测试工具:如JUnit等测试框架
  • 依赖注入:如Spring的IoC容器

6. 注意事项

  • 反射操作会降低性能,应避免在性能敏感的代码中频繁使用
  • 反射可能会破坏封装性,应谨慎使用
  • 反射操作需要处理多种异常,代码复杂度较高
  • 在使用反射访问私有成员时,需要设置setAccessible(true)

7. 反射与注解的结合

反射经常与注解结合使用,通过反射获取类、方法、属性上的注解信息,实现更灵活的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.lang.annotation.*;
import java.lang.reflect.Method;

// 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test {
String value();
}

// 使用注解
class TestClass {
@Test("测试方法1")
public void testMethod1() {
System.out.println("Test method 1");
}

@Test("测试方法2")
public void testMethod2() {
System.out.println("Test method 2");
}
}

// 通过反射获取注解信息
public class AnnotationReflectionExample {
public static void main(String[] args) throws Exception {
Class<?> clazz = TestClass.class;
Method[] methods = clazz.getDeclaredMethods();

for (Method method : methods) {
if (method.isAnnotationPresent(Test.class)) {
Test testAnnotation = method.getAnnotation(Test.class);
System.out.println("Method: " + method.getName() + ", Annotation value: " + testAnnotation.value());
method.invoke(clazz.newInstance());
}
}
}
}

5.3 什么是Java的注解?

Java的注解(Annotation)是一种特殊的标记,用于为代码添加元数据(metadata)。注解本身不会直接影响代码的执行,但可以被编译器、工具或运行时环境使用。

1. 注解的基本概念

  • 元数据:描述数据的数据,注解就是一种元数据
  • 标记:注解作为一种标记,可以添加到类、方法、属性、参数等元素上
  • 不改变代码行为:注解本身不会改变代码的执行逻辑
  • 可被处理:注解可以被编译器、工具或运行时环境处理

2. 注解的分类

(1) 内置注解

  • @Override:标记方法重写父类方法
  • @Deprecated:标记已过时的元素
  • @SuppressWarnings:抑制编译器警告
  • @SafeVarargs:抑制可变参数的类型安全警告
  • @FunctionalInterface:标记函数式接口

(2) 元注解

  • @Retention:指定注解的保留策略
  • @Target:指定注解可以应用的目标元素
  • @Documented:指定注解是否包含在JavaDoc中
  • @Inherited:指定注解是否可以被继承
  • @Repeatable:指定注解是否可重复使用

(3) 自定义注解

  • 用户根据需要定义的注解

3. 注解的语法

1
2
3
4
5
6
// 定义注解
[元注解]
public @interface 注解名称 {
// 注解属性
类型 属性名() [default 默认值];
}

4. 注解的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 1. 内置注解示例
public class BuiltInAnnotationExample {
// @Override:标记方法重写父类方法
@Override
public String toString() {
return "BuiltInAnnotationExample";
}

// @Deprecated:标记已过时的方法
@Deprecated
public void oldMethod() {
System.out.println("This method is deprecated");
}

// @SuppressWarnings:抑制编译器警告
@SuppressWarnings("unchecked")
public void suppressWarningExample() {
List list = new ArrayList(); // 未指定泛型,会产生警告
list.add("Hello");
}

// @FunctionalInterface:标记函数式接口
@FunctionalInterface
interface MyFunction {
void apply();
}
}

// 2. 元注解示例
@Retention(RetentionPolicy.RUNTIME) // 保留到运行时
@Target(ElementType.METHOD) // 只能应用于方法
@Documented // 包含在JavaDoc中
@Inherited // 可以被继承
public @interface CustomAnnotation {
String value() default "default value";
int number() default 0;
}

// 3. 自定义注解示例
public class CustomAnnotationExample {
@CustomAnnotation(value = "test", number = 1)
public void testMethod() {
System.out.println("Test method with custom annotation");
}

@CustomAnnotation // 使用默认值
public void defaultMethod() {
System.out.println("Default method with custom annotation");
}
}

5. 注解的保留策略

  • RetentionPolicy.SOURCE:仅在源码中保留,编译时丢弃
  • RetentionPolicy.CLASS:保留到编译后的字节码中,但运行时不加载(默认)
  • RetentionPolicy.RUNTIME:保留到运行时,可以通过反射获取

6. 注解的目标元素

  • ElementType.TYPE:类、接口、枚举
  • ElementType.FIELD:字段
  • ElementType.METHOD:方法
  • ElementType.PARAMETER:参数
  • ElementType.CONSTRUCTOR:构造方法
  • ElementType.LOCAL_VARIABLE:局部变量
  • ElementType.ANNOTATION_TYPE:注解类型
  • ElementType.PACKAGE:包
  • ElementType.TYPE_PARAMETER:类型参数(Java 8+)
  • ElementType.TYPE_USE:类型使用(Java 8+)

7. 注解的处理

(1) 编译时处理

  • 编译器根据注解生成代码、检查错误等
  • 例如,@Override注解会检查方法是否真的重写了父类方法

(2) 运行时处理

  • 通过反射获取注解信息并处理
  • 例如,Spring框架通过反射获取@Autowired注解来实现依赖注入

(3) 编译后处理

  • 使用注解处理器(Annotation Processor)在编译后生成代码
  • 例如,Lombok库通过注解处理器生成getter、setter等方法

8. 注解的应用场景

  • 框架配置:如Spring、Hibernate等框架使用注解进行配置
  • 代码生成:如Lombok使用注解生成重复代码
  • 测试工具:如JUnit使用注解标记测试方法
  • 代码检查:如FindBugs使用注解检查代码质量
  • 依赖注入:如Spring使用@Autowired注解实现依赖注入
  • AOP:如Spring使用@Aspect注解实现切面编程

9. 注解与反射的结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.lang.annotation.*;
import java.lang.reflect.Method;

// 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Log {
String value() default "";
}

// 使用注解
class Service {
@Log("执行添加操作")
public void add() {
System.out.println("Add operation");
}

@Log("执行删除操作")
public void delete() {
System.out.println("Delete operation");
}
}

// 通过反射处理注解
public class AnnotationProcessingExample {
public static void main(String[] args) throws Exception {
Service service = new Service();
Class<?> clazz = service.getClass();
Method[] methods = clazz.getDeclaredMethods();

for (Method method : methods) {
if (method.isAnnotationPresent(Log.class)) {
Log logAnnotation = method.getAnnotation(Log.class);
System.out.println("执行方法:" + method.getName() + ",操作:" + logAnnotation.value());
method.invoke(service);
System.out.println("方法执行完成\n");
}
}
}
}

10. 注意事项

  • 注解本身不会改变代码的执行逻辑,需要通过工具或框架来处理
  • 过多使用注解可能会使代码变得难以理解
  • 注解的处理会增加一定的性能开销
  • 自定义注解时,需要合理选择保留策略和目标元素

5.4 Java 8的新特性有哪些?

Java 8是Java语言的一个重要版本,引入了许多新特性,大幅提升了代码的简洁性和表达能力。以下是Java 8的主要新特性:

1. Lambda表达式

概念:Lambda表达式是一种简洁的匿名函数,可以作为参数传递给方法或存储在变量中。

语法(参数列表) -> 表达式(参数列表) -> { 代码块 }

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 传统匿名内部类
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello, Java 7!");
}
};

// Lambda表达式
Runnable runnable2 = () -> System.out.println("Hello, Java 8!");

// 带参数的Lambda表达式
Comparator<Integer> comparator = (a, b) -> a.compareTo(b);

// 带代码块的Lambda表达式
BiFunction<Integer, Integer, Integer> add = (a, b) -> {
int sum = a + b;
return sum;
};

2. 函数式接口

概念:只有一个抽象方法的接口,用于支持Lambda表达式。

注解@FunctionalInterface

常见函数式接口

  • Function<T, R>:接收一个参数,返回一个结果
  • Consumer<T>:接收一个参数,无返回值
  • Supplier<T>:无参数,返回一个结果
  • Predicate<T>:接收一个参数,返回布尔值
  • BiFunction<T, U, R>:接收两个参数,返回一个结果

示例

1
2
3
4
5
6
7
8
9
10
11
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}

// 使用Lambda表达式实现函数式接口
Calculator add = (a, b) -> a + b;
Calculator subtract = (a, b) -> a - b;

System.out.println(add.calculate(10, 5)); // 输出: 15
System.out.println(subtract.calculate(10, 5)); // 输出: 5

3. 方法引用

概念:使用::操作符引用已有方法,简化Lambda表达式。

类型

  • 静态方法引用:ClassName::staticMethod
  • 实例方法引用:instance::instanceMethod
  • 构造方法引用:ClassName::new
  • 类的实例方法引用:ClassName::instanceMethod

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 静态方法引用
Function<String, Integer> parseInt = Integer::parseInt;
System.out.println(parseInt.apply("123")); // 输出: 123

// 实例方法引用
String str = "Hello";
Supplier<Integer> length = str::length;
System.out.println(length.get()); // 输出: 5

// 构造方法引用
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();

// 类的实例方法引用
Function<String, String> toUpperCase = String::toUpperCase;
System.out.println(toUpperCase.apply("hello")); // 输出: HELLO

4. 流(Stream)API

概念:用于处理集合的高级API,支持链式操作和并行处理。

主要操作

  • 中间操作:filter、map、sorted、distinct等
  • 终端操作:collect、forEach、reduce、count等

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

// 过滤并收集
List<String> filteredNames = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
System.out.println(filteredNames); // 输出: [Alice, Charlie, David]

// 映射并收集
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(nameLengths); // 输出: [5, 3, 7, 5, 3]

// 排序
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNames); // 输出: [Alice, Bob, Charlie, David, Eve]

// 并行处理
int sum = IntStream.range(1, 1000000)
.parallel()
.sum();
System.out.println(sum); // 输出: 499999500000

5. 默认方法和静态方法

概念:接口中可以定义默认方法和静态方法。

默认方法:使用default关键字,提供方法的默认实现
静态方法:使用static关键字,直接在接口中定义静态方法

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface Vehicle {
// 抽象方法
void start();

// 默认方法
default void stop() {
System.out.println("Vehicle stopping");
}

// 静态方法
static void honk() {
System.out.println("Beep beep!");
}
}

class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starting");
}

// 可以重写默认方法
@Override
public void stop() {
System.out.println("Car stopping");
}
}

// 使用
Car car = new Car();
car.start(); // 输出: Car starting
car.stop(); // 输出: Car stopping
Vehicle.honk(); // 输出: Beep beep!

6. 新的日期时间API

概念:引入了java.time包,提供了更清晰、更强大的日期时间处理功能。

主要类

  • LocalDate:日期
  • LocalTime:时间
  • LocalDateTime:日期时间
  • ZonedDateTime:带时区的日期时间
  • Duration:时间间隔
  • Period:日期间隔

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 获取当前日期
LocalDate today = LocalDate.now();
System.out.println("Today: " + today); // 输出: 2026-04-12

// 创建指定日期
LocalDate date = LocalDate.of(2026, Month.APRIL, 12);
System.out.println("Date: " + date); // 输出: 2026-04-12

// 获取当前时间
LocalTime now = LocalTime.now();
System.out.println("Now: " + now); // 输出: 14:30:45.123

// 创建指定时间
LocalTime time = LocalTime.of(14, 30, 45);
System.out.println("Time: " + time); // 输出: 14:30:45

// 日期时间
LocalDateTime dateTime = LocalDateTime.now();
System.out.println("DateTime: " + dateTime); // 输出: 2026-04-12T14:30:45.123

// 格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
System.out.println("Formatted: " + formattedDateTime); // 输出: 2026-04-12 14:30:45

// 解析
LocalDateTime parsedDateTime = LocalDateTime.parse("2026-04-12 14:30:45", formatter);
System.out.println("Parsed: " + parsedDateTime); // 输出: 2026-04-12T14:30:45

// 计算
LocalDate tomorrow = today.plusDays(1);
System.out.println("Tomorrow: " + tomorrow); // 输出: 2026-04-13

LocalDate lastMonth = today.minusMonths(1);
System.out.println("Last month: " + lastMonth); // 输出: 2026-03-12

7. Optional类

概念:用于处理可能为null的值,避免NullPointerException。

主要方法

  • of():创建非null的Optional
  • ofNullable():创建可能为null的Optional
  • empty():创建空的Optional
  • isPresent():检查值是否存在
  • get():获取值(如果不存在则抛出异常)
  • orElse():获取值,如果不存在则返回默认值
  • orElseGet():获取值,如果不存在则通过Supplier获取默认值
  • orElseThrow():获取值,如果不存在则抛出指定异常
  • map():转换值
  • flatMap():转换为另一个Optional
  • filter():过滤值

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 创建Optional
Optional<String> optional1 = Optional.of("Hello");
Optional<String> optional2 = Optional.ofNullable(null);
Optional<String> optional3 = Optional.empty();

// 检查值是否存在
System.out.println(optional1.isPresent()); // 输出: true
System.out.println(optional2.isPresent()); // 输出: false
System.out.println(optional3.isPresent()); // 输出: false

// 获取值
System.out.println(optional1.get()); // 输出: Hello
// System.out.println(optional2.get()); // 抛出NoSuchElementException

// 获取值或默认值
System.out.println(optional2.orElse("Default")); // 输出: Default
System.out.println(optional3.orElseGet(() -> "Generated Default")); // 输出: Generated Default

// 转换值
Optional<Integer> lengthOptional = optional1.map(String::length);
System.out.println(lengthOptional.get()); // 输出: 5

// 过滤值
Optional<String> filteredOptional = optional1.filter(s -> s.length() > 3);
System.out.println(filteredOptional.isPresent()); // 输出: true

// 链式操作
String result = optional1
.map(String::toUpperCase)
.filter(s -> s.startsWith("H"))
.orElse("Default");
System.out.println(result); // 输出: HELLO

8. Nashorn JavaScript引擎

概念:Java 8引入的新JavaScript引擎,替代了Rhino,提供了更好的性能和兼容性。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class NashornExample {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

// 执行JavaScript代码
engine.eval("print('Hello, Nashorn!')"); // 输出: Hello, Nashorn!

// 调用JavaScript函数
engine.eval("function add(a, b) { return a + b; }");
Object result = engine.eval("add(10, 5)");
System.out.println("10 + 5 = " + result); // 输出: 10 + 5 = 15

// 访问Java对象
engine.put("name", "Java");
engine.eval("print('Hello, ' + name)"); // 输出: Hello, Java
}
}

9. 其他新特性

  • 类型注解:可以在类型声明处使用注解
  • 重复注解:可以在同一元素上重复使用相同的注解
  • 方法参数名称:在编译时保留方法参数名称,便于反射获取
  • 并行数组操作Arrays类新增了并行操作方法,如parallelSortparallelSetAll
  • CompletableFuture:提供了更强大的异步编程支持

10. 总结

Java 8的新特性大幅提升了代码的简洁性、可读性和表达能力,特别是Lambda表达式、Stream API和新的日期时间API,已经成为现代Java开发的标配。这些特性使得Java代码更加优雅、高效,同时也为函数式编程在Java中的应用奠定了基础。