类的声明、定义

在Java中,类是对象的蓝图或模板,定义了对象的属性(字段)和行为(方法)。下面是关于类的声明和定义的一些基础知识。

1. 类的基本声明和定义

一个类的声明包括类名、类体以及其中的成员(字段、方法等)。

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClassName {
// 字段(属性)
int field1;
String field2;

// 构造方法
public ClassName() {
// 初始化字段
}

// 方法
public void method1() {
// 方法体
}
}

解释:

  • public:修饰符,表示该类是公共的,可以被其他类访问。
  • class ClassName:声明一个类,ClassName 是类名,按照Java命名规范,类名通常以大写字母开头。
  • {}:类体,包含类的字段和方法。
  • int field1;:字段声明,field1 是类的一个属性,数据类型为 int。
  • public ClassName():构造方法,用于创建类的实例。
  • void method1():类的方法,void 表示该方法不返回值。

2. 类的构造方法

构造方法在创建对象时调用,它用于初始化对象的状态。构造方法的名字与类名相同,并且没有返回值类型。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person {
// 字段
String name;
int age;

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

// 方法
public void greet() {
System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
}
}

解释:

  • Person(String name, int age):这是一个带有参数的构造方法,用于初始化 name 和 age 字段。
  • this.name = name;:this 关键字指的是当前对象的实例,name 是参数传递的值。

3. 创建类的实例

一旦定义了类,你就可以通过构造方法来创建类的实例(对象)。

例子:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
// 创建 Person 类的实例
Person person1 = new Person("Alice", 30);
person1.greet(); // 输出:Hello, my name is Alice and I am 30 years old.
}
}

解释:

  • Person person1 = new Person("Alice", 30);:创建了一个 Person 对象,传递了构造方法所需的参数 "Alice" 和 30。
  • person1.greet();:调用 greet 方法,输出对象的介绍信息。

4. 类的访问修饰符

Java 中的类可以使用不同的访问修饰符来控制其访问范围:

  • public:类是公共的,任何其他类都可以访问。
  • private:类只能在当前文件中访问(不过通常类本身不能是 private,private 更多地用于类的成员)。
  • protected:类只能在同一包内或其子类中访问。
  • 默认(无修饰符):类只能在同一包内访问。

5. 总结

  • 类的定义包括类名、字段、构造方法和方法。
  • 构造方法用于初始化对象的字段。
  • 类可以有不同的访问修饰符,控制类及其成员的可见性。

构造函数的定义

构造函数的定义

构造函数(Constructor)是类中的一种特殊方法,用于在创建对象时初始化该对象的状态。构造函数的名字与类名相同,并且没有返回类型。构造函数在创建对象时自动调用,可以用于初始化对象的成员变量。

1. 构造函数的基本语法

1
2
3
4
5
6
7
8
9
public class ClassName {
// 类的字段(成员变量)
type field;

// 构造函数
public ClassName() {
// 初始化字段
}
}

关键点:

  • 构造函数的名称:构造函数的名字必须与类的名字完全相同。
  • 没有返回类型:构造函数没有返回类型(包括 void)。
  • 自动调用:构造函数在创建对象时自动调用,不需要显式调用。

2. 构造函数的类型

构造函数有两种类型:

  • 无参构造函数(默认构造函数)
  • 带参构造函数(自定义构造函数)

3. 无参构造函数

无参构造函数没有参数,通常用于创建对象时不需要初始化字段的情况。如果没有定义任何构造函数,Java 会自动提供一个默认的无参构造函数。

例子:

1
2
3
4
5
6
7
8
9
10
11
public class Person {
String name;
int age;

// 无参构造函数
public Person() {
// 默认值初始化
name = "Unknown";
age = 0;
}
}

解释:

  • public Person():这是一个无参构造函数,在创建 Person 对象时会被自动调用,字段 name 和 age 被初始化为默认值。

4. 带参构造函数

带参构造函数可以让你在创建对象时传递参数,从而为对象的字段赋予特定的值。

例子:

1
2
3
4
5
6
7
8
9
10
public class Person {
String name;
int age;

// 带参构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}

解释:

  • public Person(String name, int age):带参构造函数,用于在创建对象时提供具体的初始化值。
  • this.name = name; 和 this.age = age;:this 关键字指的是当前对象,防止局部变量和字段重名。

5. 构造函数的重载

Java 允许构造函数的重载,这意味着一个类可以有多个构造函数,只要它们的参数列表不同。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Person {
String name;
int age;

// 无参构造函数
public Person() {
name = "Unknown";
age = 0;
}

// 带参构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}

// 另一个带参构造函数
public Person(String name) {
this.name = name;
this.age = 0;
}
}

解释:

  • 类 Person 有三个构造函数:
    • 一个无参构造函数。
    • 一个接受 name 和 age 参数的构造函数。
    • 一个只接受 name 参数的构造函数。

6. 构造函数的默认行为

如果你没有显式定义构造函数,Java 会为你提供一个默认的无参构造函数,且该构造函数不会做任何初始化工作。

默认构造函数的示例:

1
2
3
4
5
6
7
8
9
public class Person {
String name;
int age;
}

// 默认构造函数隐式提供:
// public Person() {
// super();
// }

7. 构造函数与 this()

this() 关键字用于调用当前类的另一个构造函数。它只能在构造函数内调用,且必须是构造函数的第一行。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
String name;
int age;

// 带两个参数的构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}

// 带一个参数的构造函数
public Person(String name) {
this(name, 0); // 调用另一个构造函数
}
}

解释:

  • 在 Person(String name) 构造函数中,通过 this(name, 0) 调用另一个构造函数,传递默认的年龄值 0。

8. 构造函数与继承

当一个类继承另一个类时,子类会继承父类的构造函数,但父类的构造函数需要显式调用,特别是当父类没有无参构造函数时。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Animal {
String name;

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

public class Dog extends Animal {
String breed;

public Dog(String name, String breed) {
super(name); // 调用父类的构造函数
this.breed = breed;
}
}

解释:

  • super(name);:在 Dog 类的构造函数中,调用了父类 Animal 的构造函数来初始化 name 字段。

总结:

  • 构造函数 用于在创建对象时初始化对象的状态。
  • 它的名称与类名相同,没有返回类型。
  • 可以有多个构造函数(构造函数重载),根据传入的参数来初始化对象。
  • this() 用于在构造函数中调用其他构造函数。
  • super() 用于调用父类的构造函数。

对象的实例化及使用

对象的实例化及使用

在 Java 中,对象实例化指的是使用 new 关键字创建类的实例,并通过实例访问类的成员(字段、方法等)。实例化过程将类的定义转化为内存中的一个具体对象,允许你对其进行操作。

1. 实例化一个对象

通过 new 关键字,我们可以实例化一个类,创建一个对象。new 关键字会调用类的构造函数来初始化对象。

语法:

1
ClassName objectName = new ClassName();

例子:

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
public class Person {
String name;
int age;

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

// 方法
public void greet() {
System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
}
}

public class Main {
public static void main(String[] args) {
// 创建 Person 类的实例(对象)
Person person1 = new Person("Alice", 30);

// 使用实例访问对象的方法
person1.greet(); // 输出:Hello, my name is Alice and I am 30 years old.
}
}

解释:

  • Person person1 = new Person("Alice", 30);:使用 new 关键字实例化 Person 类,传递构造函数的参数 "Alice" 和 30 来初始化 person1 对象。
  • person1.greet();:通过 person1 访问类中的 greet 方法,输出对象的信息。

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 Car {
String model;
int year;

// 构造方法
public Car(String model, int year) {
this.model = model;
this.year = year;
}

// 方法
public void displayInfo() {
System.out.println("Model: " + model + ", Year: " + year);
}
}

public class Main {
public static void main(String[] args) {
// 实例化对象
Car car1 = new Car("Tesla", 2022);

// 访问对象的字段
System.out.println("Car model: " + car1.model); // 输出:Car model: Tesla

// 调用对象的方法
car1.displayInfo(); // 输出:Model: Tesla, Year: 2022
}
}

解释:

  • car1.model:访问 car1 对象的 model 字段。
  • car1.displayInfo();:调用 car1 对象的 displayInfo() 方法,输出车的详细信息。

3. 通过对象调用方法

在实例化对象后,可以通过对象调用类中的方法。方法可以是实例方法(非静态方法)或静态方法(类方法)。

例子:实例方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Calculator {
// 实例方法:加法
public int add(int a, int b) {
return a + b;
}
}

public class Main {
public static void main(String[] args) {
// 创建 Calculator 对象
Calculator calc = new Calculator();

// 调用 add 方法
int result = calc.add(10, 20);
System.out.println("Result: " + result); // 输出:Result: 30
}
}

例子:静态方法

静态方法是属于类本身而不是某个对象的,因此可以通过类名直接调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MathUtils {
// 静态方法:加法
public static int add(int a, int b) {
return a + b;
}
}

public class Main {
public static void main(String[] args) {
// 直接通过类名调用静态方法
int result = MathUtils.add(10, 20);
System.out.println("Result: " + result); // 输出:Result: 30
}
}

解释:

  • calc.add(10, 20):实例化 Calculator 对象 calc 后,调用 add 方法。
  • MathUtils.add(10, 20):直接通过类名调用 add 静态方法,不需要创建对象。

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
public class Student {
String name;
int grade;

public Student(String name, int grade) {
this.name = name;
this.grade = grade;
}

public void introduce() {
System.out.println("Hi, I am " + name + " and I am in grade " + grade);
}
}

public class Main {
public static void main(String[] args) {
// 实例化多个对象
Student student1 = new Student("Alice", 5);
Student student2 = new Student("Bob", 6);

// 调用每个对象的方法
student1.introduce(); // 输出:Hi, I am Alice and I am in grade 5
student2.introduce(); // 输出:Hi, I am Bob and I am in grade 6
}
}

解释:

  • 创建了两个 Student 对象 student1 和 student2,并通过各自的 introduce() 方法输出信息。
  • 每个对象是独立的,student1 和 student2 拥有自己的 name 和 grade。

5. 对象的生命周期

Java 中的对象在实例化时被分配到堆内存中,当没有任何引用指向该对象时,Java 会通过垃圾回收(GC)机制自动销毁该对象。

对象生命周期的关键点:

  • 对象创建:通过 new 关键字实例化对象。
  • 对象使用:通过对象引用访问和修改对象的字段、调用方法。
  • 对象销毁:当对象不再被任何引用变量引用时,垃圾回收器会自动回收该对象所占的内存。

6. 总结

  • 对象实例化:通过 new 关键字创建类的对象,并调用构造函数初始化对象。
  • 访问对象字段和方法:通过对象引用来访问或修改对象的字段,调用对象的方法。
  • 静态与实例方法:实例方法通过对象调用,静态方法通过类名调用。
  • 多重实例化:可以创建多个对象,且每个对象具有独立的状态。

面向抽象的编程

面向抽象的编程(Abstraction)

面向抽象的编程是面向对象编程(OOP)中的一个核心概念,旨在隐藏实现的细节,仅暴露必要的功能或接口给用户。通过抽象,程序员可以专注于高层次的逻辑,而不必关心底层的实现细节,从而提高代码的可维护性、可扩展性和可重用性。

1. 抽象的定义

抽象是一种编程技术,通过将复杂系统的具体细节隐藏起来,只保留与外部交互所必需的功能。它有两个主要形式:

  • 抽象类:提供模板,但不能直接实例化。
  • 接口(Interface):提供一个规范,要求实现类遵守。

2. 抽象的优点

  • 简化复杂性:通过隐藏复杂实现,程序员可以专注于接口层次。
  • 增强可维护性:具体实现的更改不会影响到使用抽象的代码。
  • 提高可扩展性:通过抽象,新的功能可以在不改变现有代码的情况下扩展。
  • 促进代码复用:相同的抽象接口可以被多个实现类共享和复用。

3. 抽象类(Abstract Class)

抽象类是无法直接实例化的类。它可以包含已实现的方法和未实现的方法(抽象方法)。抽象类用于定义共有行为,并强制其子类实现某些方法。

3.1 抽象类的语法:

1
2
3
4
5
6
7
8
9
abstract class Animal {
// 抽象方法,子类必须实现
public abstract void sound();

// 非抽象方法,子类可以继承和使用
public void sleep() {
System.out.println("The animal is sleeping.");
}
}

3.2 解释:

  • abstract 关键字标识抽象类。
  • sound() 是一个抽象方法,它没有方法体,表示所有继承 Animal 的类必须实现这个方法。
  • sleep() 是一个普通方法,子类可以直接继承并使用。

3.3 继承抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Bark");
}
}

public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.sound(); // 输出:Bark
dog.sleep(); // 输出:The animal is sleeping.
}
}

解释:

  • Dog 类继承了 Animal 类,必须实现 sound() 方法。
  • sleep() 方法直接被 Dog 继承并使用。

4. 接口(Interface)

接口定义了一组抽象方法(没有方法体),并且所有实现该接口的类必须提供具体的实现。接口是实现抽象的另一种方式,它定义了类的行为,但不提供具体实现。

4.1 接口的语法:

1
2
3
4
5
interface Animal {
// 抽象方法(没有方法体)
void sound();
void sleep();
}

4.2 解释:

  • interface 关键字定义接口。
  • 所有的方法都默认为 public abstract,即使不加修饰符,也代表抽象方法。

4.3 实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Dog implements Animal {
@Override
public void sound() {
System.out.println("Bark");
}

@Override
public void sleep() {
System.out.println("Dog is sleeping.");
}
}

public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.sound(); // 输出:Bark
dog.sleep(); // 输出:Dog is sleeping.
}
}

解释:

  • Dog 类实现了 Animal 接口,并提供了 sound() 和 sleep() 方法的具体实现。
  • 使用 implements 关键字表明一个类实现了某个接口。

5. 抽象类 vs 接口

特性 抽象类 接口
是否能有构造函数 有 没有
是否能有字段 可以有字段(成员变量) 只能有 public static final 字段
是否可以实现方法 可以有实现的方法(非抽象方法) 只能有方法声明,不能有方法实现
是否支持多继承 不支持多继承(单继承) 支持多继承(一个类可以实现多个接口)
访问修饰符 可以有各种访问修饰符(public, private, protected) 默认是 public

6. 使用抽象的例子:支付系统

假设我们有一个支付系统,要求不同的支付方式(如支付宝、微信支付、信用卡支付)都有一个统一的支付接口。

6.1 定义支付接口

1
2
3
interface Payment {
void pay(double amount);
}

6.2 实现支付接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Alipay implements Payment {
@Override
public void pay(double amount) {
System.out.println("Paying " + amount + " using Alipay.");
}
}

class WeChatPay implements Payment {
@Override
public void pay(double amount) {
System.out.println("Paying " + amount + " using WeChat Pay.");
}
}

class CreditCardPay implements Payment {
@Override
public void pay(double amount) {
System.out.println("Paying " + amount + " using Credit Card.");
}
}

6.3 使用抽象接口

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Payment payment1 = new Alipay();
Payment payment2 = new WeChatPay();
Payment payment3 = new CreditCardPay();

payment1.pay(100.0); // 输出:Paying 100.0 using Alipay.
payment2.pay(200.0); // 输出:Paying 200.0 using WeChat Pay.
payment3.pay(300.0); // 输出:Paying 300.0 using Credit Card.
}
}

解释:

  • Payment 接口定义了一个支付的方法,所有的支付方式类都实现了这个接口。
  • 每种支付方式类提供了具体的支付实现,调用接口方法时,会执行具体类的逻辑。

7. 抽象的总结

  • 抽象类:允许部分方法实现,子类必须实现抽象方法。
  • 接口:完全不提供实现,只定义方法的签名,类必须实现接口并提供具体实现。
  • 抽象帮助我们将复杂的系统设计为具有更高复用性的模块,通过隐藏实现细节,暴露接口,使得系统更加灵活和可扩展。
  • 使用抽象类和接口时,可以方便地实现多态(Polymorphism),允许不同对象通过统一接口进行交互。

面向接口的编程(含lambda表达式)

面向接口的编程(Interface-based Programming)

面向接口的编程(Interface-based Programming)是一种通过接口而非具体实现来编写代码的编程方式。通过接口,代码可以被设计得更加灵活、可扩展和解耦。接口定义了类的行为,而不关心具体的实现,通常用于实现多态和依赖注入等设计模式。

1. 接口的基本概念

在 Java 中,接口(interface)是一个只包含常量和抽象方法(没有方法体)的类。接口用于指定类必须提供的行为,而不关心具体的实现。

1.1 接口的定义和实现

接口的定义:

1
2
3
interface Animal {
void sound(); // 抽象方法
}

接口的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Dog implements Animal {
@Override
public void sound() {
System.out.println("Bark");
}
}

class Cat implements Animal {
@Override
public void sound() {
System.out.println("Meow");
}
}

public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.sound(); // 输出:Bark

Animal cat = new Cat();
cat.sound(); // 输出:Meow
}
}

解释:

  • Animal 接口定义了一个方法 sound(),没有实现。
  • Dog 和 Cat 类实现了 Animal 接口,并提供了 sound() 方法的具体实现。
  • Animal 类型的引用可以指向任何实现了 Animal 接口的对象。

2. 面向接口的编程的优势

  • 解耦:代码的依赖关系更加松散,因为实现类的具体实现被封装在接口后面。
  • 多态性:通过接口,类可以实现多种不同的行为。不同的实现类可以通过统一的接口来调用。
  • 灵活性:接口的使用使得系统更加灵活,可以通过接口轻松切换不同的实现。

3. Java 8 引入的 Lambda 表达式

在 Java 8 中,Lambda 表达式和函数式接口(functional interface)被引入,为面向接口编程提供了更简洁和表达力更强的方式。Lambda 表达式是一个匿名函数,可以用来实现接口的抽象方法,尤其是对于只包含一个抽象方法的接口(函数式接口)非常有用。

3.1 函数式接口

一个函数式接口是指仅包含一个抽象方法的接口,Java 8 引入了 @FunctionalInterface 注解来明确标记接口为函数式接口。Lambda 表达式的目标是实现函数式接口。

1
2
3
4
@FunctionalInterface
interface Calculator {
int add(int a, int b); // 抽象方法
}

3.2 Lambda 表达式的语法

Lambda 表达式可以用来简化接口实现的代码,尤其是在处理函数式接口时。其基本语法如下:

1
(parameters) -> expression

例如,使用 Lambda 表达式实现 Calculator 接口:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
// 使用 Lambda 表达式实现接口
Calculator calculator = (a, b) -> a + b;

// 调用 add 方法
int result = calculator.add(10, 20);
System.out.println("Result: " + result); // 输出:Result: 30
}
}

解释:

  • (a, b) -> a + b 是一个 Lambda 表达式,它实现了 Calculator 接口的 add 方法。
  • calculator.add(10, 20) 调用了通过 Lambda 表达式实现的 add 方法,返回值为 30。

4. Lambda 表达式与函数式接口的结合

Lambda 表达式通常与函数式接口一起使用,下面是几个常见的函数式接口的示例:

4.1 Runnable 接口

Runnable 是一个没有返回值的函数式接口,它常用于线程的创建。

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
// 使用 Lambda 表达式实现 Runnable 接口
Runnable task = () -> System.out.println("Task is running in a thread");

// 启动一个新线程
new Thread(task).start();
}
}

4.2 Comparator 接口

Comparator 接口用于对象的比较,通常在排序时使用。我们可以通过 Lambda 表达式提供自定义的比较规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Arrays;
import java.util.Comparator;

public class Main {
public static void main(String[] args) {
String[] names = {"John", "Alice", "Bob"};

// 使用 Lambda 表达式对数组进行排序
Arrays.sort(names, (s1, s2) -> s1.length() - s2.length());

// 输出排序后的数组
for (String name : names) {
System.out.println(name);
}
}
}

解释:

  • (s1, s2) -> s1.length() - s2.length() 是一个 Lambda 表达式,表示根据字符串的长度进行排序。

5. 常见的函数式接口

Java 8 提供了许多内置的函数式接口,常见的包括:

  • **Consumer<T>**:接受一个输入参数并执行某些操作,但没有返回值。
    1
    Consumer<String> print = str -> System.out.println(str);
  • **Supplier<T>**:不接受输入参数,返回一个结果。
    1
    Supplier<Double> randomValue = () -> Math.random();
  • **Function<T, R>**:接受一个输入参数,并返回一个结果。
    1
    Function<String, Integer> stringLength = str -> str.length();
  • **Predicate<T>**:接受一个输入参数,返回一个布尔值,用于测试某个条件。
    1
    Predicate<Integer> isEven = num -> num % 2 == 0;

6. 方法引用(Method Reference)

方法引用是 Lambda 表达式的简写形式,允许直接引用现有方法而不需要重写其逻辑。方法引用有几种形式:

  • 静态方法引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class MathUtils {
    public static int add(int a, int b) {
    return a + b;
    }
    }

    public class Main {
    public static void main(String[] args) {
    // 使用方法引用调用静态方法
    Calculator calculator = MathUtils::add;
    int result = calculator.add(10, 20);
    System.out.println(result); // 输出:30
    }
    }
  • 实例方法引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Person {
    public void greet() {
    System.out.println("Hello!");
    }
    }

    public class Main {
    public static void main(String[] args) {
    Person person = new Person();
    // 使用方法引用调用实例方法
    Runnable greetTask = person::greet;
    greetTask.run(); // 输出:Hello!
    }
    }
  • 构造函数引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Person {
    String name;

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

    public class Main {
    public static void main(String[] args) {
    // 使用构造函数引用
    Supplier<Person> personSupplier = () -> new Person("Alice");
    Person person = personSupplier.get();
    System.out.println(person.name); // 输出:Alice
    }
    }

7. 总结:面向接口的编程与 Lambda 表达式

  • 面向接口的编程:通过接口定义行为,允许灵活的扩展和替换具体的实现,提高代码的可维护性和解耦性。
  • Lambda 表达式:简化了函数式接口的实现,让代码更加简洁和易读。Lambda 表达式可以用于简化回调、事件监听、线程等多种场景。
  • 函数式接口:是与 Lambda 表达式紧密结合的接口类型,它们定义了单一抽象方法,允许使用 Lambda 表达式实现该方法。
  • 方法引用:方法引用是 Lambda 表达式的一种简化形式,使代码更加简洁和表达性更强。

异常处理

异常处理(Exception Handling)

异常处理是程序设计中的一个重要概念,目的是处理程序在运行过程中可能发生的错误,以保证程序能够稳定运行。Java 提供了强大的异常处理机制,可以有效地捕获、处理和抛出异常。

1. 异常的基本概念

  • 异常(Exception):程序执行过程中遇到的问题,通常是无法预料的错误,比如文件未找到、网络连接失败等。
  • 错误(Error):是更严重的程序错误,通常是无法恢复的,比如 OutOfMemoryError,StackOverflowError 等。

在 Java 中,所有异常和错误类都继承自 Throwable 类。异常分为两大类:

  • 受检异常(Checked Exception):必须处理的异常。它是 Exception 类的子类,但不是 RuntimeException 的子类。
  • 非受检异常(Unchecked Exception):可以不处理的异常。它是 RuntimeException 类的子类。

2. 异常处理的基本结构

Java 的异常处理使用 try-catch-finally 语句块。基本语法如下:

1
2
3
4
5
6
7
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 异常处理代码
} finally {
// 必须执行的代码(无论是否发生异常)
}

2.1 try 块:包含可能抛出异常的代码。

2.2 catch 块:用来捕获异常并进行处理。如果 try 块中的代码抛出异常,控制会转到匹配的 catch 块。

2.3 finally 块:用于执行无论是否发生异常都需要执行的代码,通常用于资源的释放,比如关闭文件流、数据库连接等。

3. 示例:简单的异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExceptionDemo {
public static void main(String[] args) {
try {
// 可能抛出异常的代码
int result = 10 / 0; // ArithmeticException
} catch (ArithmeticException e) {
// 捕获并处理异常
System.out.println("Error: " + e.getMessage());
} finally {
// 最终执行的代码
System.out.println("This is the finally block.");
}
}
}

解释:

  • try 块中的 10 / 0 会抛出 ArithmeticException。
  • 异常被 catch 块捕获并处理,输出 "Error: / by zero"。
  • finally 块无论异常是否发生,都会执行,输出 "This is the finally block."。

4. 常见的异常类型

  • **ArithmeticException**:数学运算异常,如除以零。
  • **NullPointerException**:空指针异常,访问一个空对象引用。
  • **ArrayIndexOutOfBoundsException**:数组下标越界异常。
  • **FileNotFoundException**:文件未找到异常。
  • **IOException**:输入输出异常。

5. 多重 catch 块

Java 7 引入了多重 catch 语法,可以捕获多个异常类型,并通过 | 连接多个异常类。

1
2
3
4
5
6
7
8
9
10
11
12
public class MultiCatchDemo {
public static void main(String[] args) {
try {
String str = null;
System.out.println(str.length()); // NullPointerException
int[] arr = new int[2];
arr[5] = 10; // ArrayIndexOutOfBoundsException
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
System.out.println("Caught an exception: " + e);
}
}
}

解释:

  • catch (NullPointerException | ArrayIndexOutOfBoundsException e):捕获 NullPointerException 或 ArrayIndexOutOfBoundsException 类型的异常。
  • 这样做可以减少代码重复。

6. 自定义异常

除了使用 Java 提供的异常类型外,还可以创建自己的异常类。自定义异常通常用于程序中特定的错误处理需求。

6.1 自定义异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个自定义异常类
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}

public class CustomExceptionDemo {
public static void main(String[] args) {
try {
int age = -1;
if (age < 0) {
throw new InvalidAgeException("Age cannot be negative!");
}
} catch (InvalidAgeException e) {
System.out.println("Exception: " + e.getMessage());
}
}
}

解释:

  • InvalidAgeException 是一个自定义的异常类,继承自 Exception 类。
  • 在 main 方法中,如果 age 小于 0,就抛出这个自定义异常。

7. 受检异常 vs 非受检异常

7.1 受检异常(Checked Exceptions)

受检异常是 Java 编译器强制要求处理的异常。如果代码中可能抛出受检异常,必须使用 try-catch 块来捕获异常,或者通过 throws 声明将异常抛给调用者。

1
2
3
4
5
6
7
8
9
public class CheckedExceptionDemo {
public static void main(String[] args) {
try {
throw new java.io.IOException("IOException occurred");
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
}

7.2 非受检异常(Unchecked Exceptions)

非受检异常(也称为运行时异常)继承自 RuntimeException,编译器不会强制要求你捕获或声明这些异常。常见的非受检异常有 NullPointerException、ArrayIndexOutOfBoundsException 等。

1
2
3
4
5
6
public class UncheckedExceptionDemo {
public static void main(String[] args) {
int[] arr = new int[2];
System.out.println(arr[5]); // 会抛出 ArrayIndexOutOfBoundsException
}
}

8. throws 关键字

throws 用于声明一个方法可能抛出的异常。当方法内部可能会抛出受检异常时,需要使用 throws 将异常抛出给调用者处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThrowsDemo {
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
System.out.println("Exception caught: " + e);
}
}

// 方法声明可能抛出 IOException
public static void test() throws java.io.IOException {
throw new java.io.IOException("IOException in test method");
}
}

解释:

  • test() 方法声明了它可能抛出 IOException 异常,所以调用 test() 时需要捕获异常。
  • 使用 throws 关键字将异常传递给调用者进行处理。

9. throw 关键字

throw 用于在方法内部抛出一个异常。与 throws 不同,throw 是用来实际抛出异常的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThrowDemo {
public static void main(String[] args) {
try {
validateAge(-1); // 抛出 InvalidAgeException
} catch (InvalidAgeException e) {
System.out.println(e.getMessage());
}
}

public static void validateAge(int age) throws InvalidAgeException {
if (age < 0) {
throw new InvalidAgeException("Age cannot be negative!");
}
}
}

解释:

  • throw new InvalidAgeException("Age cannot be negative!") 抛出一个自定义异常。

10. 总结

  • 异常处理:通过 try-catch-finally 语句可以捕获和处理程序中的异常,finally 块用于执行无论是否发生异常都需要执行的代码。
  • 受检异常和非受检异常:受检异常必须进行处理,而非受检异常可以不处理。
  • 自定义异常:可以根据业务需求定义自己的异常类。
  • **throw 和 throws**:throw 用于抛出异常,throws 用于声明方法可能抛出的异常。
  • 异常的作用:异常处理可以提高程序的健壮性,防止程序在遇到错误时崩溃。

匿名类

匿名类(Anonymous Class)

匿名类是 Java 中一种没有名字的类。它通常用于临时创建实现了某个接口或者继承了某个类的类的实例,适用于当你只需要该类的一次性实现时。匿名类提供了一种简洁的方式来表达某些行为,而不需要定义一个额外的类。

1. 匿名类的定义

匿名类的基本语法如下:

1
2
3
new 类名或接口名() {
// 继承类或实现接口的方法
};

1.1 匿名类的特点

  • 匿名类没有类名,它是一个局部类,通常在创建该类的地方直接实例化。
  • 匿名类可以实现接口或者继承类。
  • 匿名类通常用于临时的类实现,不能单独重用。

2. 匿名类的示例:实现接口

匿名类最常见的用途是实现接口。当你需要创建一个接口的实现类,并且只在一个地方使用时,使用匿名类是一个非常简洁的方式。

示例:实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Greeting {
void greet(String name);
}

public class AnonymousClassDemo {
public static void main(String[] args) {
// 使用匿名类实现 Greeting 接口
Greeting greeting = new Greeting() {
@Override
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
};

greeting.greet("Alice"); // 输出:Hello, Alice!
}
}

解释:

  • Greeting 接口只有一个方法 greet(),我们通过匿名类来实现它。
  • 在创建匿名类时,不需要显式地定义类名,直接在 new Greeting() 后定义接口方法的实现。

3. 匿名类的示例:继承类

匿名类也可以继承一个类并重写其方法。当你需要临时创建一个类,并覆盖其某些方法时,匿名类也是一种非常有效的方式。

示例:继承类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal {
public void sound() {
System.out.println("Animal sound");
}
}

public class AnonymousClassDemo {
public static void main(String[] args) {
// 使用匿名类继承 Animal 类
Animal dog = new Animal() {
@Override
public void sound() {
System.out.println("Bark");
}
};

dog.sound(); // 输出:Bark
}
}

解释:

  • Animal 类有一个 sound() 方法。
  • 使用匿名类创建了一个 Animal 的子类,并重写了 sound() 方法来输出 “Bark”。

4. 匿名类的特点与限制

  • 局部类:匿名类是局部类,通常只能在其所在的代码块中使用(如 main() 方法)。它不能在其他地方重复使用。
  • 只能有一个方法:匿名类通常用于实现接口或者重写单个方法,因此它没有构造函数和其他成员。
  • 不能有构造函数:匿名类不能有显式的构造函数,因此实例化时只能使用默认构造方法。

5. 使用匿名类的场景

匿名类常用于以下场景:

  • 事件监听器:在 GUI 编程中(如 Swing 或 AWT),匿名类常用来实现事件监听器。
  • 回调函数:在一些回调机制中,匿名类是实现接口或抽象方法的一种便捷方式。
  • 一次性使用的接口实现:当你只需要一次性使用某个接口的实现时,匿名类能简化代码。

示例:事件监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.awt.*;
import java.awt.event.*;

public class AnonymousClassEventListener {
public static void main(String[] args) {
Frame frame = new Frame("Anonymous Class Example");

// 使用匿名类创建事件监听器
Button button = new Button("Click Me");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});

frame.add(button);
frame.setSize(300, 200);
frame.setVisible(true);
}
}

解释:

  • 在 addActionListener() 方法中,我们使用匿名类来实现 ActionListener 接口。
  • 匿名类提供了 actionPerformed() 方法的实现,当按钮被点击时,它会输出 “Button clicked!”。

6. 匿名类与 Lambda 表达式的比较

在 Java 8 中,Lambda 表达式被引入,提供了一种更简洁的方式来实现函数式接口。与匿名类相比,Lambda 表达式使代码更加简洁,尤其是在实现单一方法的接口时。

示例:Lambda 表达式 vs 匿名类

1
2
3
4
5
6
7
8
9
10
// 使用匿名类实现接口
Runnable task1 = new Runnable() {
@Override
public void run() {
System.out.println("Task is running!");
}
};

// 使用 Lambda 表达式实现接口
Runnable task2 = () -> System.out.println("Task is running!");

区别:

  • 匿名类在实现接口时需要明确地声明类体,包括 @Override 和方法的实现。
  • Lambda 表达式通过 () 和 -> 符号简洁地表示方法实现,更加简洁。

7. 匿名类与局部变量

在匿名类中,可以使用外部方法中的局部变量,但有一些限制:

  • 局部变量必须是 final 或 隐式 final,即变量的值不能在匿名类内部发生改变。
  • 这是因为匿名类可能会在外部方法执行完毕后仍然存在,因此局部变量的值必须在匿名类创建时就确定。

示例:访问外部局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AnonymousClassDemo {
public static void main(String[] args) {
final int multiplier = 2; // 必须是 final 或隐式 final

Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Multiplied value: " + (5 * multiplier));
}
};

task.run(); // 输出:Multiplied value: 10
}
}

解释:

  • multiplier 是一个外部局部变量,它在匿名类中被使用。
  • 由于 multiplier 是 final,它的值在匿名类中可以被安全访问。

8. 总结

  • 匿名类是没有名字的类,常用于临时实现接口或继承类的场景。
  • 匿名类在语法上较为简洁,但限制较多,无法重用,并且不能有构造函数。
  • 匿名类常见应用:事件监听器、回调机制和一次性接口实现。
  • 与 Lambda 表达式相比,匿名类更为冗长,而 Lambda 表达式提供了更简洁的语法。

匿名类在许多应用中仍然非常有用,特别是当你需要快速实现接口或者重写类的某个方法时。如果你有更多关于匿名类的问题,或者想了解 Lambda 表达式等其他高级特性,随时告诉我!

字符串的处理(字符串、正则表达式、stringtokenizer、scanner)

字符串的处理(String, 正则表达式, StringTokenizer, Scanner)

在 Java 中,字符串处理是非常常见且重要的任务。Java 提供了多种工具来帮助开发者高效地处理字符串,包括字符串类 String 和 StringBuilder,以及用于解析字符串的 StringTokenizer 和 Scanner。此外,正则表达式(Regular Expressions)也是字符串处理中的一项强大工具。

1. 字符串类:String

1.1 字符串的创建与操作

在 Java 中,字符串是不可变的对象。String 类提供了许多方法来操作字符串。

示例:常见的 String 方法

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
public class StringDemo {
public static void main(String[] args) {
String str = "Hello, World!";

// 字符串长度
System.out.println("Length: " + str.length()); // 输出:13

// 字符串拼接
String str2 = " Java";
String result = str + str2;
System.out.println(result); // 输出:Hello, World! Java

// 字符串查找
System.out.println("Index of 'World': " + str.indexOf("World")); // 输出:7

// 字符串替换
System.out.println(str.replace("World", "Java")); // 输出:Hello, Java!

// 字符串转大写
System.out.println(str.toUpperCase()); // 输出:HELLO, WORLD!

// 字符串转小写
System.out.println(str.toLowerCase()); // 输出:hello, world!
}
}

解释:

  • length():返回字符串的长度。
  • indexOf(String str):返回指定子字符串首次出现的索引。
  • replace():替换字符串中的部分内容。
  • toUpperCase() / toLowerCase():将字符串转换为大写或小写。

1.2 字符串的比较

String 类提供了以下方法用于字符串的比较:

  • equals(String str):比较两个字符串的内容是否相同(区分大小写)。
  • equalsIgnoreCase(String str):比较两个字符串的内容是否相同(忽略大小写)。
  • compareTo(String str):按照字典顺序比较两个字符串。
1
2
3
4
5
6
7
8
9
10
11
public class StringCompareDemo {
public static void main(String[] args) {
String str1 = "apple";
String str2 = "banana";
String str3 = "apple";

System.out.println(str1.equals(str3)); // 输出:true
System.out.println(str1.equalsIgnoreCase("APPLE")); // 输出:true
System.out.println(str1.compareTo(str2)); // 输出:负数 (因为 "apple" 小于 "banana")
}
}

2. 正则表达式(Regular Expressions)

正则表达式(Regex)是字符串模式匹配的一种工具。在 Java 中,正则表达式的主要使用类是 Pattern 和 Matcher。

2.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.regex.*;

public class RegexDemo {
public static void main(String[] args) {
String text = "The quick brown fox jumps over the lazy dog.";

// 编译正则表达式
Pattern pattern = Pattern.compile("fox");

// 创建匹配器
Matcher matcher = pattern.matcher(text);

// 查找匹配项
if (matcher.find()) {
System.out.println("Found 'fox' at index: " + matcher.start()); // 输出:Found 'fox' at index: 16
}

// 替换匹配项
String result = matcher.replaceAll("cat");
System.out.println(result); // 输出:The quick brown cat jumps over the lazy dog.
}
}

解释:

  • Pattern.compile():编译正则表达式。
  • matcher.find():查找匹配项。
  • matcher.start():获取匹配项的起始位置。
  • matcher.replaceAll():替换所有匹配的字符串。

2.2 常用的正则表达式

  • .:匹配任何单个字符。
  • \d:匹配任何数字字符(0-9)。
  • \w:匹配字母、数字或下划线。
  • \s:匹配任何空白字符(如空格、制表符等)。
  • []:字符集,匹配括号内的任意字符。
  • +:匹配前面的子表达式 1 次或多次。
  • *:匹配前面的子表达式 0 次或多次。
  • ^:匹配输入的开始位置。
  • $:匹配输入的结束位置。

示例:邮箱验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EmailValidation {
public static void main(String[] args) {
String email = "test@example.com";
String regex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";

Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(email);

if (matcher.matches()) {
System.out.println("Valid email address");
} else {
System.out.println("Invalid email address");
}
}
}

3. StringTokenizer

StringTokenizer 是一个较老的类,用于将字符串分割为多个标记(tokens)。它通常用于解析以分隔符为基础的字符串。

示例:使用 StringTokenizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.StringTokenizer;

public class StringTokenizerDemo {
public static void main(String[] args) {
String sentence = "Hello, how are you doing today?";

// 创建 StringTokenizer 对象
StringTokenizer tokenizer = new StringTokenizer(sentence, " ,?");

// 遍历标记
while (tokenizer.hasMoreTokens()) {
System.out.println(tokenizer.nextToken());
}
}
}

解释:

  • StringTokenizer 用于将字符串拆分为多个标记。第二个参数是分隔符,可以是一个或多个字符。
  • nextToken() 返回当前标记并移动到下一个标记。

注意:

StringTokenizer 已经被标记为过时(deprecated),推荐使用 split() 方法或者 Scanner 类来代替它。

4. Scanner 类

Scanner 是一个功能强大的类,可以用于从不同的输入源读取数据,如键盘输入、文件、字符串等。它还可以通过正则表达式来分割字符串。

4.1 Scanner 用法:读取字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Scanner;

public class ScannerDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner("Hello 123 world 456");

// 读取单个单词
String word1 = scanner.next();
String word2 = scanner.next();

// 读取整数
int num1 = scanner.nextInt();
int num2 = scanner.nextInt();

System.out.println("Words: " + word1 + " " + word2); // 输出:Hello world
System.out.println("Numbers: " + num1 + " " + num2); // 输出:123 456

scanner.close();
}
}

4.2 Scanner 用法:使用正则分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.Scanner;

public class ScannerRegexDemo {
public static void main(String[] args) {
String input = "apple,banana,orange";
Scanner scanner = new Scanner(input);

// 使用正则表达式分割
scanner.useDelimiter(",");

while (scanner.hasNext()) {
System.out.println(scanner.next());
}

scanner.close();
}
}

解释:

  • next() 方法返回下一个输入项(例如,单词或数字)。
  • nextInt()、nextDouble() 等方法用于读取特定类型的输入。
  • useDelimiter() 方法允许使用正则表达式作为分隔符。

5. 总结

  • String 是 Java 中用于处理字符串的基础类,提供了丰富的方法来操作和比较字符串。
  • 正则表达式 提供强大的字符串模式匹配功能,Pattern 和 Matcher 类用于在 Java 中使用正则表达式。
  • StringTokenizer 用于将字符串分割为多个标记,但已经不推荐使用,建议使用 split() 或 Scanner。
  • Scanner 提供了更灵活的方式来解析字符串和读取输入,支持使用正则表达式来分割字符串。

泛型

泛型(Generics)

泛型(Generics)是 Java 中的一种强大机制,它允许你在编写类、接口和方法时使用类型参数。通过使用泛型,Java 提供了对类型的抽象,从而增强了代码的可重用性、类型安全性和可读性。

泛型允许我们编写适用于不同数据类型的代码,而不必为每种数据类型编写重复的代码。泛型常用于集合类中,比如 List、Set 和 Map 等。

1. 泛型的基础

泛型的核心思想是类型参数化,它使得代码在编译时能更好地检查类型,避免了运行时的类型错误。

1.1 泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义一个泛型类
class Box<T> {
private T value;

public T getValue() {
return value;
}

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

public class GenericClassDemo {
public static void main(String[] args) {
Box<Integer> intBox = new Box<>(); // 创建一个 Integer 类型的 Box
intBox.setValue(100);
System.out.println("Integer Value: " + intBox.getValue());

Box<String> strBox = new Box<>(); // 创建一个 String 类型的 Box
strBox.setValue("Hello, World!");
System.out.println("String Value: " + strBox.getValue());
}
}

解释:

  • Box<T> 是一个泛型类,其中 T 是类型参数,表示该类可以使用任何类型。
  • 在实例化时,可以指定类型(如 Integer、String 等),并且 Box 类的方法将根据指定类型进行类型安全检查。

1.2 泛型方法

泛型不仅可以用于类,也可以用于方法。我们可以定义一个泛型方法,使得方法能够处理不同类型的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GenericMethodDemo {
// 泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}

public static void main(String[] args) {
Integer[] intArray = {1, 2, 3};
String[] strArray = {"Hello", "World"};

// 调用泛型方法
printArray(intArray); // 输出:1 2 3
printArray(strArray); // 输出:Hello World
}
}

解释:

  • 泛型方法 printArray 可以接受任何类型的数组,并打印出数组中的元素。
  • 方法的类型参数 <T> 放在方法的返回类型前面,表示该方法可以接受任意类型的数组。

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
interface Pair<K, V> {
K getKey();
V getValue();
}

class ConcretePair<K, V> implements Pair<K, V> {
private K key;
private V value;

public ConcretePair(K key, V value) {
this.key = key;
this.value = value;
}

public K getKey() {
return key;
}

public V getValue() {
return value;
}
}

public class GenericInterfaceDemo {
public static void main(String[] args) {
Pair<Integer, String> pair = new ConcretePair<>(1, "One");
System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
}
}

解释:

  • Pair<K, V> 是一个泛型接口,接受两个类型参数 K 和 V,分别表示键和值。
  • ConcretePair 是一个实现了 Pair 接口的泛型类,它实现了 getKey() 和 getValue() 方法,返回 K 类型和 V 类型的值。

3. 泛型与通配符(Wildcard)

在泛型中,通配符(Wildcard) 是一个非常有用的概念,通常用于方法的参数类型上,表示接受任何类型。通配符有以下几种常见的形式:

3.1 ?(通配符)

? 表示任意类型。它可以在泛型中作为类型的占位符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WildcardDemo {
public static void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}

public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<String> strList = List.of("a", "b", "c");

printList(intList); // 输出:1 2 3
printList(strList); // 输出:a b c
}
}

解释:

  • 使用 List<?> 可以接受任何类型的 List,这意味着方法 printList 可以接受任何类型的 List。

3.2 上界通配符:? extends T

上界通配符 ? extends T 表示接受 T 类型及其子类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UpperBoundWildcardDemo {
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}

public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);

printNumbers(intList); // 输出:1 2 3
printNumbers(doubleList); // 输出:1.1 2.2 3.3
}
}

解释:

  • List<? extends Number> 可以接受任何类型是 Number 的子类的 List,包括 Integer、Double 等。

3.3 下界通配符:? super T

下界通配符 ? super T 表示接受 T 类型及其父类型。

1
2
3
4
5
6
7
8
9
10
11
12
public class LowerBoundWildcardDemo {
public static void addNumbers(List<? super Integer> list) {
list.add(1); // 可以安全地添加 Integer 或其子类
list.add(2); // 可以安全地添加 Integer 或其子类
}

public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // 输出:[1, 2]
}
}

解释:

  • List<? super Integer> 表示接受 Integer 类型及其父类型的 List(如 Number 或 Object)。

4. 泛型的类型擦除

Java 中的泛型在编译时会进行类型擦除(Type Erasure)。这意味着,泛型类型的信息在编译时会被移除,并且被替换成原始类型。类型擦除的目的是为了与 Java 的兼容性,使得泛型代码能够在运行时与非泛型代码一起工作。

4.1 示例:类型擦除

1
2
3
4
5
6
7
8
public class TypeErasureDemo {
public static void main(String[] args) {
Box<Integer> intBox = new Box<>();
Box<String> strBox = new Box<>();

System.out.println(intBox.getClass() == strBox.getClass()); // 输出:true
}
}

解释:

  • 尽管 Box<Integer> 和 Box<String> 是不同类型的泛型类,但它们的实际类型在编译后都会变成 Box(原始类型)。
  • 因此,intBox.getClass() 和 strBox.getClass() 会返回相同的 Box 类型。

5. 总结

  • 泛型类:通过使用类型参数,可以创建适用于多种类型的类。
  • 泛型方法:允许方法根据不同的类型进行操作,使得方法更具通用性。
  • 泛型接口:接口也可以使用泛型,定义多个类型参数。
  • 通配符:? 表示任意类型,? extends T 表示类型的上界,? super T 表示类型的下界。
  • 类型擦除:泛型在编译时会进行类型擦除,泛型的类型信息在运行时并不可用。

泛型提高了代码的类型安全性和可重用性,减少了类型转换的错误。