类的声明、定义

在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):这是一个带有参数的构造方法,用于初始化 nameage 字段。
  • 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:类只能在当前文件中访问(不过通常类本身不能是 privateprivate 更多地用于类的成员)。
  • 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 对象时会被自动调用,字段 nameage 被初始化为默认值。

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 有三个构造函数:
    • 一个无参构造函数。
    • 一个接受 nameage 参数的构造函数。
    • 一个只接受 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 对象 student1student2,并通过各自的 introduce() 方法输出信息。
  • 每个对象是独立的,student1student2 拥有自己的 namegrade

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(),没有实现。
  • DogCat 类实现了 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):是更严重的程序错误,通常是无法恢复的,比如 OutOfMemoryErrorStackOverflowError 等。

在 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):捕获 NullPointerExceptionArrayIndexOutOfBoundsException 类型的异常。
  • 这样做可以减少代码重复。

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,编译器不会强制要求你捕获或声明这些异常。常见的非受检异常有 NullPointerExceptionArrayIndexOutOfBoundsException 等。

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 块用于执行无论是否发生异常都需要执行的代码。
  • 受检异常和非受检异常:受检异常必须进行处理,而非受检异常可以不处理。
  • 自定义异常:可以根据业务需求定义自己的异常类。
  • **throwthrows**: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 是一个外部局部变量,它在匿名类中被使用。
  • 由于 multiplierfinal,它的值在匿名类中可以被安全访问。

8. 总结

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

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

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

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

在 Java 中,字符串处理是非常常见且重要的任务。Java 提供了多种工具来帮助开发者高效地处理字符串,包括字符串类 StringStringBuilder,以及用于解析字符串的 StringTokenizerScanner。此外,正则表达式(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 中,正则表达式的主要使用类是 PatternMatcher

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

泛型

泛型(Generics)

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

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

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 是类型参数,表示该类可以使用任何类型。
  • 在实例化时,可以指定类型(如 IntegerString 等),并且 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> 是一个泛型接口,接受两个类型参数 KV,分别表示键和值。
  • 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,包括 IntegerDouble 等。

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(如 NumberObject)。

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 表示类型的下界。
  • 类型擦除:泛型在编译时会进行类型擦除,泛型的类型信息在运行时并不可用。

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