Java核心技术· 卷一(11版)笔记(第 4-6章)

第四章 对象与类

Java是一种面向对象编程语言,因此它的基本构建块是对象和类。

一个类是一个模板或蓝图,用于描述具有相同属性和方法的对象的特征。类可以看作是一种自定义数据类型,其中包含实例变量(属性)和方法。类中的方法定义了可以在该类的对象上执行的操作。

实例化一个类意味着创建该类的对象,也就是从类模板中创建具有属性和方法的实际实例。通过实例化一个类,我们可以访问该类中的所有属性和方法,并且每个实例都具有自己的状态。

Java的类和对象可以通过访问修饰符(public、private、protected)来控制访问。Public访问修饰符允许任何程序都可以使用类和对象,而Private和Protected访问修饰符则限制了对类和对象的访问。

4.1 面对对象编程概况

面向过程编程和面向对象编程是两种不同的编程范式。以下是它们之间的比较:

  1. 面向过程编程:
  • 将程序归结为一系列的函数和过程;
  • 重点在于实现功能的具体步骤;
  • 可读性差,维护难度较大;
  • 适合简单的程序设计。
  1. 面向对象编程:
  • 将程序分解为对象,每个对象有其特定的属性和方法;
  • 重点在于对象之间的互动和协作;
  • 代码复用性高,可读性好,维护难度较小;
  • 适合大型和复杂的程序设计。

总的来说,面向对象编程是一种更加高级、灵活和强大的编程方式,尤其适合于开发大型软件和项目。而面向过程编程则更适合于简单的程序设计,如小型、独立的脚本等。

面向过程编程示例代码:

public class Main {
    public static void main(String[] args) {
        int num1 = 10;
        int num2 = 20;
        int sum = add(num1, num2);
        System.out.println(sum);
    }

    public static int add(int num1, int num2) {
        return num1 + num2;
    }
}

面向对象编程示例代码:

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator(10, 20);
        int sum = calculator.add();
        System.out.println(sum);
    }
}

public class Calculator {
    private int num1;
    private int num2;

    public Calculator(int num1, int num2) {
        this.num1 = num1;
        this.num2 = num2;
    }

    public int add() {
        return num1 + num2;
    }
}

在面向过程编程中,代码是由一系列函数组成的。在上面的示例代码中,add() 函数被用来计算两个整数的和。

而在面向对象编程中,代码是由一系列对象组成的。在上面的示例代码中,使用 Calculator 类创建了一个对象,该对象包含两个整数值。add() 函数是 Calculator 类的一个方法,用于计算这两个整数的和。

通过比较两种不同的编程范式,可以看出在面向对象编程中,代码更具有可读性、可维护性和可扩展性。

在Java中,除了基本数据类型(如int和double)外,所有数据类型都是类类型。因此,创建对象是Java编程中最常见的操作之一。

以下是一个简单的Java类和对象的示例图:

+--------------+
|   Car Class  |
+--------------+
|   - brand    |
|   - color    |
|   - speed    |
+--------------+
|   + drive()  |
|   + stop()   |
|   + honk()   |
+--------------+

在这个图示中,我们看到了一个名为"Car"的类,它有三个属性:brand(品牌)、color(颜色)和speed(速度)。它还有三种方法:drive()、stop()和honk()。

我们还可以看到类的属性和方法是使用不同的符号来表示的。属性使用"-“(减号)来表示私有属性,方法使用”+"(加号)来表示公有方法。这个图示还展示了类的封装性,类的属性和方法只能在类的内部使用。

现在,我们可以实例化这个类并创建一个实际的"Car"对象:

+------------------+
|     Car Object   |
+------------------+
|    brand: Toyota  |
|    color: blue    |
|    speed: 60 mph  |
+------------------+

我们可以看到这个对象有特定的值,即品牌为Toyota、颜色为蓝色、速度为60英里每小时。这个对象还拥有类中的方法,可以让我们驱动车辆、停车或按喇叭。这就是Java中的类和对象。

4.1.1 类

Java 类是一种蓝图或模板,用于创建Java对象。它描述了对象的属性和行为。类具有以下特征:

  1. 属性:Java类可以有属性,也称为字段或变量。这些属性描述了对象的特征。

  2. 方法:Java类可以有方法,也称为函数或操作。这些方法执行对象的操作并改变对象的状态。

  3. 构造器:Java类可以有构造器,用于初始化对象的属性。

  4. 封装:Java类支持封装,即将属性和方法组合在一起以保护数据和行为。

  5. 继承:Java类可以继承其他类的属性和方法,以扩展现有类的功能。

  6. 多态性:Java类支持多态性,即同一方法可以在不同的类中具有不同的实现。

  7. 抽象类:Java类可以是抽象的,即只定义了一些方法和属性,但没有实现它们。抽象类需要子类来实现它们。

以下是一个简单的Java类的定义和实例化代码示例:

public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
}

// 实例化一个Person对象
Person john = new Person("John", 30);

// 获取属性值
String name = john.getName();
int age = john.getAge();

在这个例子中,我们定义了一个名为 Person 的类,它有两个属性:nameage,以及一个构造函数来初始化这些属性。类还有两个方法来获取 nameage 属性的值。

我们创建了一个名为 johnPerson 对象,并通过构造函数将其 nameage 属性设置为 “John” 和 30。然后,我们使用 getName()getAge() 方法获取该对象的属性值。

4.1.2 对象

Java对象是Java程序中的实例化对象,它是内存中的数据结构,包含了属性和方法。Java对象可以通过类、接口和数组等方式进行实例化,每个对象都有自己的状态和行为。

在Java中,所有对象都继承自Java.lang.Object类,它提供了一些常用的方法和属性,例如equals()、hashCode()、toString()等。

Java对象的创建和销毁都是由JVM自动管理的,通过垃圾回收器来释放不再被引用的对象的内存。在Java程序中,对象的引用不允许直接访问对象内部的属性和方法,只能通过对象的公共方法来访问和操作对象。

Java对象在面向对象编程中扮演着重要的角色,通过对象的封装、继承和多态等特性,可以实现代码的复用、抽象和灵活性。

Java对象的实例化需要通过new关键字来完成,具体步骤如下:

  1. 定义类:先创建一个类,描述了对象的属性和方法。例如,我们定义了一个Person类:
public class Person {
    // 属性
    private String name;
    private int age;
    
    // 构造函数
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 方法
    public void display() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
    }
}
  1. 实例化对象:使用new关键字创建对象,如下所示:
Person p = new Person("Tom", 18);

在这里,p是一个Person对象的引用,使用new创建一个Person实例,并且传递了构造函数所需要的参数。

  1. 调用方法:通过对象引用调用对象的方法,如下所示:
p.display();

这里调用了Person对象的display()方法,输出了对象的属性值。

4.1.3 识别类

在 Java 中,可以使用以下语句来识别一个类:

public class MyClass {
    // class body
}

其中,MyClass 是类的名称,可以根据需求进行更改。类名必须以字母开头,可以包含字母、数字和下划线。

类体中可以包含变量、方法、构造函数等元素。

除了使用 public 访问修饰符之外,还可以使用 privateprotected 和默认(即不加访问修饰符)访问修饰符来限制类的访问权限。

例如:

public class MyClass {
    private int x;
    protected double y;
    String z;
    
    public MyClass(int a, double b, String c) {
        x = a;
        y = b;
        z = c;
    }
    
    public int getX() {
        return x;
    }
    
    protected void setY(double newValue) {
        y = newValue;
    }
    
    void printZ() {
        System.out.println(z);
    }
}

上述代码定义了一个名为 MyClass 的类,其中包含了三个成员变量、一个构造函数和三个方法。其中,变量 x 的访问权限为 private,只能在类内部访问和修改;变量 y 的访问权限为 protected,只能在类内部和子类中访问和修改;变量 z 的访问权限为默认,即同一包内的其他类可以访问。

构造函数用于创建类的实例,方法 getX 用于获取变量 x 的值,方法 setY 用于设置变量 y 的值,方法 printZ 用于打印变量 z 的值。

4.1.4 类之间的关系

Java 类和类之间的关系包括以下几种:

  1. 继承关系(Inheritance):一个类可以通过继承另一个类来获取其属性和方法,并可以在此基础上进行扩展或修改。

  2. 实现关系(Implementation):一个类可以实现一个或多个接口,通过实现接口中定义的方法,来实现特定的功能。

  3. 依赖关系(Dependency):一个类在实现某个功能时,需要调用其他类的方法或使用其对象,此时就存在依赖关系。

  4. 关联关系(Association):两个类之间的对象存在关联,通常是通过一个类中的成员变量引用另一个类的对象来实现。

  5. 聚合关系(Aggregation):表示整体与部分之间的关系,整体对象包含一些部分对象,但整体对象和部分对象可以存在独立的生命周期。

  6. 组合关系(Composition):表示整体与部分之间的关系,整体对象包含一些部分对象,但整体对象和部分对象的生命周期是相同的。

Java类之间的关系可以用UML图示表示出来,以便更好地理解和设计类之间的关系。

以下是上述各种关系的示例代码:

  1. 继承关系
// 父类
public class Animal {
    public void move() {
        System.out.println("动物在移动");
    }
}

// 子类继承父类
public class Dog extends Animal {
    public void bark() {
        System.out.println("狗在叫");
    }
}
  1. 实现关系
// 接口
public interface Flyable {
    void fly();
}

// 实现接口
public class Bird implements Flyable {
    public void fly() {
        System.out.println("鸟在飞");
    }
}
  1. 关联关系
// 学校类
public class School {
    private List<Student> students;

    public School() {
        this.students = new ArrayList<Student>();
    }

    public void addStudent(Student student) {
        this.students.add(student);
    }
}

// 学生类
public class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }
}
  1. 依赖关系
// 依赖关系
public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }

    public void start() {
        this.engine.start();
    }
}

// 被依赖类
public class Engine {
    public void start() {
        System.out.println("引擎启动");
    }
}
  1. 聚合关系
// 聚合关系
public class School {
    private List<Classroom> classrooms;

    public School(List<Classroom> classrooms) {
        this.classrooms = classrooms;
    }

    public int getTotalStudents() {
        int total = 0;
        for (Classroom classroom : this.classrooms) {
            total += classroom.getStudents().size();
        }
        return total;
    }
}

// 聚合类
public class Classroom {
    private List<Student> students;

    public Classroom(List<Student> students) {
        this.students = students;
    }

    public List<Student> getStudents() {
        return this.students;
    }
}

// 被聚合类
public class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }
}
  1. 组合关系
// 组合关系
public class Computer {
    private CPU cpu;
    private Memory memory;

    public Computer() {
        this.cpu = new CPU();
        this.memory = new Memory();
    }
}

// 组合类1
public class CPU {
    public void start() {
        System.out.println("CPU启动");
    }
}

// 组合类2
public class Memory {
    public void start() {
        System.out.println("内存启动");
    }
}

4.2 使用预制类

Java 预制类是指在 Java 标准库中已经预先定义好的类,它们都有特定的功能和用途,可以帮助 Java 开发者更加方便地进行编程。Java 预制类常用的包括java.lang、java.util、java.io、java.nio、java.math、java.net等。常用的 Java 预制类包括:

  1. Object类:是所有类的基类,提供了一些通用的方法,如 equals()、hashCode()和toString()。

  2. String类:用于存储字符串,常用方法包括charAt()、indexOf()、substring()、length()、equals()。

  3. Math类:提供了许多常用的数学函数,如sin()、cos()、sqrt()、pow()、random()等。

  4. ArrayList类:是一个动态数组,可以随意添加和删除元素,提供了一些常用的方法,如add()、remove()、get()、size()等。

  5. HashMap类:是一个键值对的集合,用于存储一些对象,提供了一些常用的方法,如put()、get()、remove()、size()等。

  6. File类:用于描述文件和目录,提供了一些常用的方法,如exists()、getName()、isDirectory()、isFile()等。

  7. Date类:用于处理日期和时间,提供了一些常用的方法,如getTime()、compareTo()、toString()等。

这些预制类能够满足大部分常见的编程需求,可以大大提高开发效率。

4.2.1 对象与对象变量

在Java中,对象是一个具体的实例,是一段分配的内存空间,包含类的属性和方法。而对象变量则是指向对象的引用,是对象的地址。可以把对象变量看作是一个指向对象的指针,它指向一个对象,并且可以通过该变量访问该对象的属性和方法。对象变量是存储在堆栈中的,而对象本身是存储在堆中的。

举个例子,假设有一个类叫做Person,我们可以将其实例化为一个对象,并使用对象变量引用该对象:

Person person1 = new Person();

在上面的语句中,我们创建了一个对象person1,并将其引用保存在变量person1中。此时,person1是一个对象变量,它指向一个Person对象,而这个对象包含了Person类的属性和方法。

后续我们可以通过对象变量访问该对象的属性和方法:

person1.setName("Tom");
person1.setAge(20);
System.out.println(person1.getName());
System.out.println(person1.getAge());

在上面的例子中,我们通过对象变量person1访问了Person对象的属性和方法,通过setName方法设置了对象的属性name为Tom,通过setAge方法设置了对象的属性age为20,然后通过getName和getAge方法分别获取了对象的属性name和age的值,并输出到控制台。

总之,对象是实际存在的实例,而对象变量是指向对象的引用。通过对象变量可以访问对象的属性和方法。

4.2.2 Java类库中的LocalDate类

LocalDate类是Java 8中新引入的日期类之一,用于表示日期,不包含时间和时区信息。它是不可变的,线程安全的,并提供了多种实用方法来处理常见的日期操作。

LocalDate类提供了以下方法:

  • now():返回当前本地日期。
  • of(int year, int month, int dayOfMonth):根据指定的年、月、日构造LocalDate实例。
  • parse(CharSequence text):将字符串解析为LocalDate实例。
  • getYear()、getMonth()、getDayOfMonth():获取年、月、日。
  • plusDays(long daysToAdd)、minusDays(long daysToSubtract):增加或减少指定天数后返回一个新的LocalDate实例。
  • isBefore(LocalDate other)、isAfter(LocalDate other)、isEqual(LocalDate other):比较两个LocalDate实例的大小关系。
  • with(TemporalAdjuster adjuster):返回一个调整后的LocalDate实例,可以使用提供的TemporalAdjuster实现自定义调整逻辑。

例如:

import java.time.LocalDate;

public class TestLocalDate {
    public static void main(String[] args) {
        LocalDate now = LocalDate.now();
        System.out.println("当前日期:" + now);

        LocalDate date = LocalDate.of(2022, 8, 15);
        System.out.println("指定日期:" + date);

        LocalDate parsedDate = LocalDate.parse("2022-08-15");
        System.out.println("解析后的日期:" + parsedDate);

        LocalDate tomorrow = now.plusDays(1);
        System.out.println("明天的日期:" + tomorrow);

        boolean isBefore = date.isBefore(now);
        boolean isAfter = date.isAfter(now);
        boolean isEqual = date.isEqual(parsedDate);
        System.out.println(date + "是否在" + now + "之前:" + isBefore);
        System.out.println(date + "是否在" + now + "之后:" + isAfter);
        System.out.println(date + "是否等于" + parsedDate + ":" + isEqual);
    }
}

输出结果:

当前日期:2022-08-14
指定日期:2022-08-15
解析后的日期:2022-08-15
明天的日期:2022-08-15
2022-08-15是否在2022-08-14之前:false
2022-08-15是否在2022-08-14之后:true
2022-08-15是否等于2022-08-15:true

LocalDate API属性列表如下:

  1. static LocalDate now():返回当前本地日期。
  2. static LocalDate of(int year, int month, int dayOfMonth):返回指定的日期。
  3. static LocalDate parse(CharSequence text):从文本字符串解析LocalDate。
  4. int getYear():获取年份。
  5. Month getMonth():获取月份。
  6. int getMonthValue():获取月份值,1-12。
  7. int getDayOfMonth():获取月中的天数。
  8. DayOfWeek getDayOfWeek():获取星期几。
  9. int getDayOfYear():获取年中的天数。
  10. boolean isLeapYear():判断是否为闰年。
  11. LocalDate withYear(int year):返回修改了年份的日期。
  12. LocalDate withMonth(int month):返回修改了月份的日期。
  13. LocalDate withDayOfMonth(int dayOfMonth):返回修改了月中天数的日期。
  14. LocalDate plusYears(long years):返回增加了指定年数的日期。
  15. LocalDate plusMonths(long months):返回增加了指定月数的日期。
  16. LocalDate plusWeeks(long weeks):返回增加了指定周数的日期。
  17. LocalDate plusDays(long days):返回增加了指定天数的日期。
  18. LocalDate minusYears(long years):返回减少了指定年数的日期。
  19. LocalDate minusMonths(long months):返回减少了指定月数的日期。
  20. LocalDate minusWeeks(long weeks):返回减少了指定周数的日期。
  21. LocalDate minusDays(long days):返回减少了指定天数的日期。
  22. boolean isAfter(ChronoLocalDate other):判断是否在另一个日期之后。
  23. boolean isBefore(ChronoLocalDate other):判断是否在另一个日期之前。
  24. boolean isEqual(ChronoLocalDate other):判断是否与另一个日期相等。
  25. String format(DateTimeFormatter formatter):按指定格式格式化日期为字符串。

4.2.3 更改器方法和访问器方法

在Java中,更改器方法和访问器方法都是用于对象属性的操作。

更改器方法,也称为setter方法,是用于修改对象属性值的方法。通常以set开头,后面加上属性名,例如setAge(int age),表示设置对象的年龄属性为给定的age值。

访问器方法,也称为getter方法,是用于获取对象属性值的方法。通常以get开头,后面加上属性名,例如getAge(),表示获取对象的年龄属性值。

以下是一个示例代码,展示了如何使用更改器方法和访问器方法:

public class Person {
    private int age;
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
}

//...

Person person = new Person();
person.setAge(30); // 使用更改器方法设置年龄属性为30
int age = person.getAge(); // 使用访问器方法获取年龄属性值
System.out.println(age); // 输出30

需要注意的是,Java中还有一种特殊的访问器方法,称为is方法。它通常用于返回boolean类型的属性值,一般以is开头,例如isMarried(),用于返回是否已婚的属性值。

4.3 用户自定义类

用户自定义类是一种程序员自己定义的数据类型。它可以包含属性(成员变量)和方法(成员函数),并能够对自身进行操作。 用户自定义类通常用于表示程序中的现实概念。例如,一个人可以被定义为一个用户自定义类,类中包含姓名、年龄、性别等属性,以及吃饭、睡觉、工作等方法。用户自定义类可以通过实例化来创建对象,并在程序中进行调用。它可以提高程序的可读性、可维护性和可扩展性,是面向对象编程中非常重要的概念。

以下是一个 Java 用户自定义类的示例:

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

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void printInfo() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

这个类有两个私有属性 nameage,一个构造方法和三个公有方法。构造方法初始化 nameage 属性,公有方法可以用来获取和设置这些属性,并打印信息。下面是如何实例化和使用 Person 对象的示例代码:

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("Bob", 25);
        Person person2 = new Person("Alice", 30);

        person1.printInfo(); // 输出: Name: Bob, Age: 25
        person2.printInfo(); // 输出: Name: Alice, Age: 30

        person1.setName("Bobby");
        person2.setAge(31);

        person1.printInfo(); // 输出: Name: Bobby, Age: 25
        person2.printInfo(); // 输出: Name: Alice, Age: 31
    }
}

在这个例子中,我们通过调用 new 关键字创建了两个 Person 对象 person1person2。我们可以通过它们的方法获取和设置其属性,例如 person1.setName("Bobby")。最后,我们调用了 person1.printInfo()person2.printInfo() 打印对象的信息。

4.3.1 如何定义一个 Employee类

以下是一个简单的Java Employee类示例,它具有基本的属性和方法:

public class Employee {
    private String name;
    private int age;
    private String jobTitle;
    private double salary;

    public Employee(String name, int age, String jobTitle, double salary) {
        this.name = name;
        this.age = age;
        this.jobTitle = jobTitle;
        this.salary = salary;
    }

    // Getter and Setter methods
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getJobTitle() {
        return jobTitle;
    }

    public void setJobTitle(String jobTitle) {
        this.jobTitle = jobTitle;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    // Other methods
    public String toString() {
        return "Name: " + name + ", Age: " + age + ", Job Title: " + jobTitle + ", Salary: " + salary;
    }

    public void giveRaise(double amount) {
        salary += amount;
    }
}

这个类包含了一个构造方法用来初始化对象,四个私有属性(name,age,jobTitle和salary),以及各种Getter和Setter方法来访问和更新这些属性。此外,还包括一个toString方法,可以将Employee对象以字符串形式表示,以及一个giveRaise方法,用于增加员工的薪水。

可以使用以下代码来创建一个Employee对象:

Employee emp = new Employee("John Smith", 30, "Manager", 50000.0);
System.out.println(emp);

这将输出以下内容:

Name: John Smith, Age: 30, Job Title: Manager, Salary: 50000.0

4.3.2 多源文件使用

在Java中,如果我们的程序是由多个源文件组成的,我们需要使用一些特殊的技巧来编译和运行它。以下是一些基本的步骤:

  1. 将每个类都编写在独立的源文件中,并确保每个文件的文件名与类名相同,并以.java扩展名结尾。
  2. 在使用其他类的类中,需要使用import语句引入这些类的包。
  3. 在编译程序时,需要将所有的源文件一起编译,可以使用javac命令加上所有的源文件来进行编译。例如:javac A.java B.java C.java
  4. 在运行程序时,需要指定包含main()方法的类的名称。例如:java MyClass

注意:如果程序中使用的类是Java标准库中的类,那么可以直接在程序中使用。如果是自己编写的类,则需要将它们编译成.class文件,并将它们与程序一起存储。

假设我们有一个项目,包含以下两个类:Person.java和Main.java。Person类用于表示一个人的信息,Main类用于执行程序。

Person.java

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 void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("Alice", 25);
        Person person2 = new Person("Bob", 30);

        person1.display();
        person2.display();
    }
}

如果在同一个文件中运行程序,可以通过以下方式实现:

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("Alice", 25);
        Person person2 = new Person("Bob", 30);

        person1.display();
        person2.display();
    }

    static 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 void setAge(int age) {
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        public void display() {
            System.out.println("Name: " + name + ", Age: " + age);
        }
    }
}

但是,如果我们想把Person类和Main类分别保存在不同的文件中,该如何实现呢?

我们需要在同一个包(package)中创建两个文件:Person.java和Main.java,分别放置Person类和Main类的代码。

Person.java

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 void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("Alice", 25);
        Person person2 = new Person("Bob", 30);

        person1.display();
        person2.display();
    }
}

接下来,我们可以通过命令行编译和运行程序。

首先,进入项目目录,使用以下命令编译Person.java和Main.java:

javac Person.java Main.java

编译成功后,会在同一个目录下生成Person.class和Main.class文件。

最后,使用以下命令运行程序:

java Main

程序将输出以下结果:

Name: Alice, Age: 25
Name: Bob, Age: 30

4.3.3 从构造器开始

好的,从构造器开始,一个构造器是一种特殊类型的方法,在创建对象时被调用。构造器的作用是初始化对象的属性,为对象分配内存空间,并返回指向该对象的引用。

Java中的构造器有以下特点:

  1. 构造器的名称必须与类名完全相同。
  2. 构造器没有返回类型,也不需要使用void关键字来指定返回类型。
  3. 如果一个类没有定义任何构造器,则编译器会自动提供一个不带参数的默认构造器,如果该类有父类,则默认构造器会调用父类的不带参数的构造器。
  4. 如果定义了一个或多个构造器,则编译器不会再提供默认构造器。
  5. 构造器可以被重载,即可以定义多个具有不同参数列表的构造器。

下面是一个简单的Java类及其构造器的示例:

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

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

    // getter和setter方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

在上面的代码中,我们定义了一个名为Person的类,并定义了一个构造器,用于初始化Person类的实例。该构造器接受两个参数,即name和age,用于设置Person对象的name和age属性。同时,我们还定义了getter和setter方法,用于获取和设置Person对象的属性。

这里还有一个Java类的例子,包含构造函数和实例化代码:

public class Person {
    
    private String name;
    private int age;
    
    // 构造函数
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 实例化代码
    public static void main(String[] args) {
        // 创建Person实例
        Person person = new Person("Tom", 21);
        
        // 输出实例的名字和年龄
        System.out.println("Name: " + person.name);
        System.out.println("Age: " + person.age);
    }
}

在这个例子中,我们定义了一个Person类,其中包含了一个构造函数和一个通过main方法实例化的代码块。在构造函数中,我们使用传递进来的参数来初始化类的私有成员变量。在实例化代码中,首先创建了Person类的一个实例person,然后通过该对象的成员变量来输出实例的名字和年龄。

4.3.4 用var声明局部对象

从Java 10版本开始,可以使用var关键字在局部变量的类型推断中声明变量。使用var关键字可以使代码更简洁,同时不会损失类型安全性。

例如:

var str = "Hello, World!"; // 推断str的类型为String
var num = 42; // 推断num的类型为int
var list = new ArrayList<String>(); // 推断list的类型为ArrayList<String>

需要注意的是,使用var关键字声明变量时,必须要在初始化时同时进行类型推断。这意味着以下代码是不合法的:

var str; // 错误:需要初始化变量
str = "Hello, World!";

此外,var关键字仅适用于局部变量。字段、方法参数、返回类型等仍需要显式地声明类型。

下面展示用与不用var的区别

使用var类型:

var num = 10;
var name = "John";
var list = new ArrayList<String>();

不使用var类型:

int num = 10;
String name = "John";
List<String> list = new ArrayList<String>();

使用var类型的优点是可以让代码更加简洁和易于理解,而且可以避免在声明类型时出现错误,例如类型声明错误,或者在使用不同的数据结构时需要频繁地更改变量类型。

然而,使用var类型也有一些限制。例如,它只能在局部变量中使用,不能用于方法参数或返回类型,而且也不能用于null值。

此外,使用var类型还需要注意以下几点:

  • 变量名应具有描述性。
  • 变量类型应明显,易于理解。
  • 仅在代码具有清晰逻辑结构和明确类型时使用。
  • 尽可能避免在一行代码中声明多个变量。

下面是使用var关键字进行类型推断的示例代码:

var str = "Hello, world!";  // 推断出str的类型为String
var num = 10;               // 推断出num的类型为int
var list = new ArrayList<String>(); // 推断出list的类型为ArrayList<String>
list.add("Java");
list.add("Python");
list.add("Ruby");
for (var item : list) {     // 推断出item的类型为String
    System.out.println(item);
}

使用var关键字可以更简洁地声明变量,并且在编写Lambda表达式等代码时也更加方便。但是要注意不要过度使用,保证代码的可读性和可维护性。

4.3.5 使用null引用

Java中的null是一个特殊值,用于表示对象引用变量没有指向任何有效的对象。如果变量没有被赋值,或者被赋值为null,那么它就是一个null引用。

例如,下面的代码示例创建了一个字符串变量,但没有为其赋值:

String str;

此时,str变量被默认初始化为null,因为它没有指向任何字符串对象。

在代码中使用null引用时,需要小心避免空指针异常(NullPointerException)。因为对一个null引用进行方法调用、属性访问或数组访问操作都会导致空指针异常。因此,在使用一个对象引用变量之前,必须确保它不是null,例如:

if (str != null) {
    System.out.println(str.length());
}

下面是使用null引用的Java示例代码:

String text = null;
if (text == null) {
   System.out.println("text is null");
} else {
   System.out.println("text is not null");
}

在上面的代码中,我们声明了一个字符串变量text,并将其设置为null。然后,我们使用if语句检查该变量是否为null。如果是null,就打印出“text is null”;否则,将打印出“text is not null”。

4.3.6 隐式参数和显式参数

Java中的隐式参数和显式参数可以用于方法调用或构造函数调用。

隐式参数指的是方法调用或构造函数调用中传递的对象本身。在调用者中,隐式参数是被调用方法或构造函数的对象。例如,以下代码中的隐式参数是str:

String str = "Hello, world!";
str.toUpperCase(); // 隐式参数是 str

显式参数是作为方法调用或构造函数调用的一部分显式传递的参数。例如,以下代码中的显式参数是num1和num2:

int num1 = 5;
int num2 = 10;
Math.max(num1, num2); // 显式参数是 num1 和 num2

在方法的定义中,显式参数是方法签名的一部分,用于指定方法的输入参数。例如,以下方法定义中的num1和num2是显式参数:

public int addNumbers(int num1, int num2) {
   return num1 + num2;
}

在调用此addNumbers方法时,需要显式传递num1和num2的值作为参数:

int result = addNumbers(5, 10); // 显式传递参数

总之,隐式参数和显式参数都是Java中方法调用或构造函数调用的重要组成部分,隐式参数是被调用方法或构造函数的对象,而显式参数是调用者显式指定的输入参数。

隐式参数示例:

public class ImplicitParameterExample {
    private int x;
    private int y;

    public ImplicitParameterExample(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void add() {
        int sum = x + y;
        System.out.println("The sum of x and y is: " + sum);
    }

    public static void main(String[] args) {
        ImplicitParameterExample example = new ImplicitParameterExample(2, 3);
        example.add();
    }
}

在上述示例中,add()方法使用了类中的xy成员变量,这些成员变量在调用add()方法时并没有显式地传递,而是隐式地使用了类的实例变量。

显式参数示例:

public class ExplicitParameterExample {
    public static void print(String message) {
        System.out.println(message);
    }

    public static void main(String[] args) {
        String message = "Hello, world!";
        print(message);
    }
}

在上述示例中,print()方法需要一个String类型的参数,该参数在调用print()方法时被显式地传递。main()方法中定义了一个message变量,并将其传递给print()方法的参数。

4.3.7 封装的优点

  1. 提高安全性:封装可以将数据和方法隐藏起来,用户无法直接访问和修改,有效地保护了数据的安全性。

  2. 简化调用:封装将数据和方法集成在一起,用户只需要调用一个接口即可完成多个操作,简化了调用过程,提高了代码的可读性。

  3. 降低耦合性:封装可以将一个对象的实现细节隐藏起来,让对象的使用者只关心对象提供的接口,从而降低对象之间的耦合性。

  4. 方便扩展:封装将实现细节隐藏起来,这样可以保证在修改实现细节时不会影响客户端代码的使用,方便进行扩展和维护。

  5. 提高代码复用性:封装可以将代码封装成一个对象,方便在多个地方使用同一组代码,提高了代码的复用性。

封装是面向对象编程的核心之一。在Java中,封装可以通过访问修饰符来实现。一个类的属性和方法可以使用访问修饰符来限制他们的访问级别。以下是一个简单的Java类和如何使用封装来保护其属性和方法:

public class Person {
   private String name;   // 使用private修饰符限制对属性的访问
   private int age;
 
   public Person(String name, int age) {   // 一个构造函数用于创建Person对象
      this.name = name;
      this.age = age;
   }
 
   public void setName(String name) {   // 使用public方法来控制对属性的访问
      this.name = name;
   }
 
   public String getName() {
      return name;
   }
 
   public void setAge(int age) {
      this.age = age;
   }
 
   public int getAge() {
      return age;
   }
}

在这个例子中,我们使用了private修饰符限制了对Person类的name和age属性的访问。我们提供了public方法setName和setAge来设置属性,以及public方法getName和getAge来获取属性的值。

现在我们可以在另一个类中实例化Person对象并调用其方法:

public class Main {
   public static void main(String[] args) {
      Person p1 = new Person("Alice", 24);   // 创建一个Person对象
      System.out.println(p1.getName());   // 输出:Alice
      p1.setAge(25);   // 更改属性的值
      System.out.println(p1.getAge());   // 输出:25
   }
}

我们可以看到,通过使用封装,我们可以控制对类的属性的访问并保护其内容。同样,我们可以通过公共方法来实现对属性的设置和获取。这种封装使得代码更加安全和可靠。

4.3.8 基于类的访问权限

Java中基于类的访问权限是指使用Java访问修饰符来控制类中的成员(属性和方法)的访问级别。Java中有四种访问修饰符,分别是:

  1. public:所有类都可以访问该成员。

  2. private:只有该类内部可以访问该成员。

  3. protected:该类及其子类可以访问该成员。

  4. 默认(无修饰符):同一个包内的类可以访问该成员。

在Java程序中,通过使用这些访问修饰符可以灵活地控制数据的访问权限,从而保证程序的安全性和可读性。

以下是一个Java程序示例,演示如何使用类的访问权限:

package com.example;

public class ExampleClass {
    // 私有成员变量,只能在ExampleClass中访问
    private int privateVariable;

    // 默认的成员变量访问权限,只能在同一包中的类中访问
    int defaultVariable;

    // 受保护的成员变量,只能在ExampleClass及其子类中访问
    protected int protectedVariable;

    // 公共成员变量,可以在任何位置访问
    public int publicVariable;

    // 私有方法,只能在ExampleClass中调用
    private void privateMethod() {
        // 执行私有任务
    }

    // 默认访问权限的方法,只能在同一包中的类中调用
    void defaultMethod() {
        // 执行默认任务
    }

    // 受保护方法,可以在ExampleClass及其子类中调用
    protected void protectedMethod() {
        // 执行受保护的任务
    }

    // 公共方法,可以在任何地方调用
    public void publicMethod() {
        // 执行公共任务
    }
}

可以看到,上面的代码示例中,类中的成员变量和成员方法都有不同的访问权限。通过使用这些不同的访问权限,可以控制外部代码可以访问和调用哪些成员。

4.3.9 私有方法

Java中私有方法是指在类中声明的方法,其访问权限为private,只能在该类中被调用。私有方法通常用于封装内部实现细节,隐藏实现细节,提高代码的安全性和可维护性。

私有方法的语法格式如下:

private 返回值类型 方法名(参数列表) {
    // 方法体
}

在类中,可以通过调用私有方法来完成某些特定的功能。私有方法可以调用其他的私有方法,也可以调用public或protected方法,但不能调用同类中的默认方法,因为默认方法是同包可见的,而私有方法只能在当前类中被访问。

私有方法通常用于辅助公共方法或其他私有方法完成某些特定的功能。在设计类的时候,应该尽量多使用私有方法,以保证类的安全性和可维护性。

以下是Java中的一个私有方法示例:

public class MyClass {
    private int add(int x, int y) {
        return x + y;
    }
    
    public int publicMethod(int x, int y) {
        return add(x, y);
    }
}

在上面的代码中,add方法是私有方法,只能在MyClass中使用。publicMethod方法是公共方法,它在外部可以被访问并调用。在publicMethod方法中,我们调用了add方法来执行加法操作。由于add方法是私有的,它只能在MyClass中被调用,而不能在外部使用。

4.3.10 final 实例字段

final 实例字段是指在Java中用final关键字修饰的实例变量,一旦被初始化后,其值就不能再被修改。final实例字段可以在声明时直接赋值,也可以在构造方法中赋值,但是不能在其他方法中进行赋值。final实例字段通常用于定义常量或不可变的对象。

例如:

public class MyClass {
    final int myConstantValue = 10;
    final String myImmutableObject;

    public MyClass(String value) {
        myImmutableObject = value;
    }

    public void method() {
        // 编译错误,不能修改final实例字段的值
        // myConstantValue = 20;
        // myImmutableObject = "new value";
    }
}

在上面的示例中,myConstantValue被定义为一个常量,无论怎样都不能被修改。而myImmutableObject在构造方法中被赋值后,其值也不能再被修改。

4.4 静态字段和静态方法

静态字段和静态方法是在类级别上定义的成员,可以通过类名直接访问,不需要创建类的实例。

静态字段是指在类中使用 static 关键字声明的字段,它们是类共享的变量,任何一个实例都可以访问到它们,并且它们的值对于所有实例都是相同的。静态字段通常用于存储与类相关的常量或计数器等数据。

静态方法是指在类中使用 static 关键字声明的方法,它们不需要依赖于类的实例,可以直接使用类名调用。静态方法通常用于实现与类相关的辅助方法或工具方法。

总之,静态字段和静态方法是在类级别上定义的成员,它们不需要依赖于类的实例,可以直接使用类名调用。

4.4.1 静态字段

静态字段是与类关联的变量,而不是与类的每个实例对象关联的变量。以下是一个简单的示例,演示如何使用静态字段:

public class MyClass {
    // 静态字段
    public static int count = 0;

    // 实例变量
    public int id;

    public MyClass() {
        id = count++;
    }

    public static void main(String[] args) {
        MyClass obj1 = new MyClass();
        MyClass obj2 = new MyClass();

        System.out.println("obj1 id: " + obj1.id); // 输出 0
        System.out.println("obj2 id: " + obj2.id); // 输出 1

        System.out.println("count: " + MyClass.count); // 输出 2
    }
}

在上面的示例中,count是一个静态字段,它被所有类的实例共享,而id是每个实例的实例变量。在构造函数中,我们将id设置为count的当前值,并递增count的值。这意味着每个新对象将具有不同的id值,但count的值将在所有实例之间共享。在main方法中,我们创建两个对象,并显示它们的idcount的值。

静态字段通常用于实现全局变量或常量,例如Pi的值或全局计数器。它们也可以用于跟踪类的所有实例。然而,静态字段应该谨慎使用,因为它们可以导致全局状态和可变性,进而导致代码的复杂性和难以调试。

4.4.2 静态常量

Java中的静态常量是指被声明为静态和final的变量,即在程序运行期间其值不可改变且为类所有。静态常量通常用于表示一个常量值,如数学常数π(Math.PI)和一周的天数(Calendar.DAY_OF_WEEK)。静态常量的定义一般遵循以下格式:

public static final 数据类型 常量名 = 值;

其中public表示公共访问权限,static表示静态,final表示不可变,数据类型为常量的数据类型,常量名为常量的名称,值为常量的具体值。

静态常量的访问方式为:类名.常量名,如Math.PI、Calendar.DAY_OF_WEEK等。

下面是一个静态常量Java的实例:

public class Circle {
    
    public static final double PI = 3.14;
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    public double getArea() {
        return PI * radius * radius;
    }
    
    public double getPerimeter() {
        return 2 * PI * radius;
    }
}

这里我们定义了一个Circle类,在其中声明了一个静态常量PI,它的值是3.14。我们还定义了一个radius实例变量,和两个实例方法,getArea和getPerimeter,用于计算圆的面积和周长。

由于PI是一个静态常量,所以所有的Circle对象都可以访问它,而不需要创建一个新的实例变量。

4.4.3 静态方法

Java中的静态方法是指在类中定义的方法,使用 static 关键字修饰,可以在不创建类实例的情况下被调用。静态方法可以通过类名直接调用,不需要先创建类的实例。静态方法可以访问类的静态变量,但不能访问非静态变量。

静态方法的特点如下:

  1. 静态方法不依赖于对象,可以直接调用。

  2. 静态方法不能使用 this 关键字,因为 this 表示当前对象,而静态方法并不属于任何对象。

  3. 静态方法可以访问类的静态变量,但不能直接访问非静态变量。

  4. 静态方法不能被重写,因为它们与类绑定在一起,而不是与对象绑定在一起。

以下是一个静态方法的Java示例:

public class Example {
    public static void main(String[] args) {
        int num1 = 5;
        int num2 = 10;
        
        int result = sum(num1, num2);
        System.out.println("The sum of " + num1 + " and " + num2 + " is: " + result);
    }
    
    public static int sum(int a, int b) {
        return a + b;
    }
}

以上代码中,我们定义了一个静态方法 sum,它接收两个整数参数并返回它们的和。在 main 方法中,我们调用了 sum 方法,并将其返回值存储在 result 变量中。最后,我们将结果打印到控制台上。因为 sum 方法是静态的,所以我们可以在 main 方法中直接调用它,而不需要创建 Example 类的对象。

4.4.4 工厂方法

Java中的工厂方法是一种创建对象的设计模式,其目的是隐藏对象创建的逻辑细节,并将其封装在工厂类中。它包含一个工厂类和一个或多个产品类。工厂类提供一个或多个创建对象的静态方法,这些方法根据传递给它们的参数选择并实例化适当的产品类。这样,客户端代码不需要知道对象的具体创建方式,只需使用工厂类提供的方法即可获取所需的对象。

工厂方法模式通常使用抽象工厂类或接口来定义工厂方法,这样可以确保工厂类的灵活性和可扩展性。具体的工厂类可以实现抽象工厂类或接口,并根据需要创建产品对象。客户端代码只需调用工厂类中的方法即可获取所需的产品对象。这种方式使得客户端代码与具体产品类之间解耦,从而提高了代码的可维护性和可扩展性。

工厂方法模式在Java中广泛应用于创建复杂对象、实现依赖注入和解耦合等场景。常见的工厂方法包括简单工厂、工厂方法、抽象工厂等。

  1. 普通工厂方法示例:
public class ProductFactory {
    
    public static Product createProduct(String type) {
        if (type.equals("A")) {
            return new ProductA();
        } else if (type.equals("B")) {
            return new ProductB();
        } else {
            throw new IllegalArgumentException("Invalid product type.");
        }
    }
}
  1. 多个工厂方法示例:
public class ProductFactory {
    
    public static Product createProductA() {
        return new ProductA();
    }
    
    public static Product createProductB() {
        return new ProductB();
    }
}
  1. 静态工厂方法示例:
public class Product {
    
    private int id;
    private String name;
    private double price;
    
    private Product(int id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    
    public static Product createProduct(int id, String name, double price) {
        return new Product(id, name, price);
    }
}
  1. 抽象工厂方法示例:
public interface ProductFactory {
    
    Product createProduct();
}

public class ProductAFactory implements ProductFactory {
    
    @Override
    public Product createProduct() {
        return new ProductA();
    }
}

public class ProductBFactory implements ProductFactory {
    
    @Override
    public Product createProduct() {
        return new ProductB();
    }
}

4.4.5 main方法

在Java中,main方法是程序的入口点。它是一个特殊的静态方法,必须在类中被定义,用于启动Java应用程序。

main方法的格式如下:

public static void main(String[] args) {
    // 程序代码
}

其中:

  • public 表示该方法为公共方法,可以从其他类中调用;
  • static 表示该方法为静态方法,可以在没有创建对象的情况下调用;
  • void 表示该方法没有返回值;
  • main 表示方法名,不能改变;
  • String[] args 表示传递给main方法的参数,是一个字符串数组,它可以为空。

在main方法中可以编写程序的主体逻辑,例如打印输出、调用其他方法等。当Java程序被执行时,JVM会自动找到main方法并执行其中的代码。

4.5 方法参数

方法参数是指在调用方法时传递给方法的数据或变量。在方法定义中,参数用于指定方法需要接收的数据类型和数量。当调用方法时,需要传递与方法定义中指定的参数类型和数量相匹配的实际参数。

例如,以下是一个Java方法的定义:

public void printName(String name) {
   System.out.println("Name is: " + name);
}

这个方法接受一个字符串类型的参数名为name,并且在方法体中使用这个参数。下面是如何调用这个方法并传递参数的例子:

printName("John");

在这个例子中,实际参数是字符串“John”,它与定义的参数类型相匹配。当方法被调用时,该字符串将被传递到方法中,并在控制台上输出“Name is: John”。

下面是一个Java方法的示例,它接受两个整数参数:

public static void add(int a, int b) {
    int result = a + b;
    System.out.println("The sum of " + a + " and " + b + " is " + result);
}

在这个示例中,add方法有两个整数参数:ab。该方法执行加法运算并打印出运算结果。调用此方法时,需要提供两个整数值作为参数。例如,可以这样调用add方法:

add(3, 5);

这将输出以下内容:

The sum of 3 and 5 is 8

4.6 对象构造

Java对象构造是指在代码中创建Java对象的过程。Java中的对象是通过类构造的,类是一种数据结构,描述了对象的属性和行为。对象构造主要涉及以下几个方面:

  1. 创建对象:在Java中,使用关键字“new”创建一个对象。例如:MyClass myObject = new MyClass();

  2. 初始化对象:在创建对象后,需要设置对象的属性,即进行初始化操作。可以使用构造方法和setter方法来完成,构造方法是初始化对象时调用的方法,而setter方法是在对象创建后随时可以调用的方法。

  3. 继承和多态:Java支持继承和多态的特性,可以在一个类中创建其他类的对象,并重写或覆盖其方法。

  4. 销毁对象:Java具有垃圾回收机制,Java虚拟机会周期性地扫描内存中未使用的对象,将其销毁并回收内存。

总之,Java对象构造是Java程序中的重要部分,能够帮助我们创建出具有特定属性和行为的对象实例,并在程序运行中进行管理和销毁。

4.6.1 重载

重载(overloading)是一种编程技术,它允许在同一作用域内定义多个具有相同名称但参数个数、类型或顺序等不同的方法或函数,以便根据调用时传入的参数的不同,自动选择合适的方法或函数进行执行。通过重载,可以简化代码、提高代码的可读性和可维护性。

在Java、C++等语言中,方法或函数的重载是通过在同一类或命名空间中定义具有同名但参数个数、类型或顺序等不同的方法或函数实现的。在Python中,由于其动态类型和参数个数可变的特性,不支持直接的重载,但可以通过使用默认参数、可变参数、关键字参数等特性模拟重载。

Java中的重载可以根据方法的参数类型、个数、顺序的不同来进行区分。

例如:

public void print(int num) {
    System.out.println(num);
}

public void print(String str) {
    System.out.println(str);
}

public void print(int num, String str) {
    System.out.println(num + " " + str);
}

这个类中定义了三个同名方法print,但参数列表不同。当我们在调用这个方法时,编译器会根据我们所传递的参数类型、个数、顺序来选择调用哪一个方法。

例如,我们可以这样调用:

print(1);           // 调用第一个方法
print("Hello");     // 调用第二个方法
print(1, "Hello");  // 调用第三个方法

Java还支持对构造方法进行重载,同样也是根据参数列表的不同来进行区分。

4.6.2 默认字段初始化

在Java中,如果我们没有在声明时明确地为字段初始化,则Java会为我们提供默认值。以下是Java中默认字段初始化的规则:

  1. 整型(byte、short、int、long)和浮点型(float、double):默认值为0。

  2. 字符型(char):默认值为’\u0000’(null字符)。

  3. 布尔型(boolean):默认值为false。

  4. 引用类型(类、接口、数组):默认值为null。

例如,如果我们有以下代码:

public class Example{
   int exampleInt;
   double exampleDouble;
   boolean exampleBoolean;
   String exampleString;
   Object exampleObject;
   
   public static void main(String[] args){
      Example example = new Example();
      System.out.println(example.exampleInt); //输出0
      System.out.println(example.exampleDouble); //输出0.0
      System.out.println(example.exampleBoolean); //输出false
      System.out.println(example.exampleString); //输出null
      System.out.println(example.exampleObject); //输出null
   }
}

在这个示例中,我们没有初始化任何字段,因此Java会为它们提供默认值。运行这个程序将输出上面列出的相应值。

4.6.3 无参数的构造器

无参数的构造器是一种特殊类型的构造器,它没有任何参数。在Java中,如果我们没有显式地定义任何构造器,则默认情况下会提供一个无参数的构造器。

以下是一个示例代码片段,其中包含一个无参数的构造函数:

public class MyClass {
  public MyClass() {
    // 这是一个无参数的构造函数
  }
}

在上面的示例中,MyClass类中有一个名为MyClass()的构造函数。这个构造函数没有任何参数,因此它被称为无参数的构造函数。

以下是Java中无参数的构造器代码实例:

public class ExampleClass {
  private int x;

  // 无参数构造器
  public ExampleClass() {
    x = 0; // 将x初始化为0
  }

  // 其他方法和属性
  public void setX(int value) {
    x = value;
  }
  public int getX() {
    return x;
  }
}

// 在另一个类中使用无参数构造器创建ExampleClass对象
public class Main {
  public static void main(String[] args) {
    ExampleClass obj = new ExampleClass(); // 使用无参数构造器创建对象
    int xValue = obj.getX(); // 获取x的值(应该是0)
    System.out.println("x的值为:" + xValue); // 输出:x的值为:0
  }
}

4.6.4 显式字段初始化

Java中的显式字段初始化是指在声明变量时,为其赋初始值。在Java中,每个变量都必须初始化,否则会编译错误。对于实例变量,可以在类体中进行赋值,也可以在构造方法中初始化。而对于静态变量,可以在类体中直接进行赋值。

显式字段初始化的语法格式如下:

访问修饰符 数据类型 变量名 = 初始值;

例如,定义一个整型变量并赋初值:

public class Example {
    int x = 0;
    //...
}

也可以在构造方法中进行初始化:

public class Example {
    int x;
    
    public Example(int x) {
        this.x = x;
    }
    //...
}

显式字段初始化可以提高程序的可读性和可维护性,使代码更加简洁清晰。同时也可以避免因为忘记初始化而导致的运行时异常。

以下是Java中的显式字段初始化实例代码的示例:

public class Car {
    private String make = "Toyota";
    private String model = "Corolla";
    private int year = 2021;
    private double price = 20000.00;
    
    // Constructor
    public Car() {
        System.out.println("New car created:");
        System.out.println("Make: " + make);
        System.out.println("Model: " + model);
        System.out.println("Year: " + year);
        System.out.println("Price: $" + price);
    }
}

在这个示例中,每个字段都被显式地初始化了一个默认值。在构造函数中,这些字段的值被打印出来。当创建一个新的Car实例时,输出将显示以下内容:

New car created:
Make: Toyota
Model: Corolla
Year: 2021
Price: $20000.0

4.6.5 参数名

Java 参数名指的是在方法或构造函数中定义的变量名称。参数名通常用于指定在方法或构造函数中使用的值的标识符,以便在方法或构造函数体中引用这些值。Java 参数名的命名规则与变量名相同,必须以字母或下划线开头,可以包含字母、数字和下划线,长度可以任意。参数名通常应该是有意义的、清晰明了的,以便其他程序员理解。在调用方法或构造函数时,必须提供参数的值,这些值将被赋给对应的参数名。Java 参数名的类型和值的表示方式取决于参数类型的数据类型。

Java参数名的格式通常使用小驼峰命名法,即第一个单词首字母小写,后续单词首字母大写。例如:

public void printName(String firstName, String lastName) {
    System.out.println("Name: " + firstName + " " + lastName);
}

在上述方法中,参数名分别为firstName和lastName。这符合Java的命名习惯,并且使代码易于阅读和理解。

这里有一个示例代码,演示如何在Java中使用参数名:

public class Main {
    public static void main(String[] args) {
        // 创建一个Person对象并设置属性
        Person person = new Person();
        person.setName("Alice");
        person.setAge(25);
        
        // 调用printPerson方法并传入Person对象
        printPerson(person);
    }
    
    // 定义一个printPerson方法,该方法接收一个名为p的Person参数,并打印该对象的属性
    public static void printPerson(Person p) {
        System.out.println("Name: " + p.getName());
        System.out.println("Age: " + p.getAge());
    }
}

// 定义一个Person类,该类包含名字和年龄属性的setter和getter方法
class Person {
    private String name;
    private int age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
}

在上面的代码中,我们定义了一个Person类,它包含一个name属性和一个age属性,以及用于设置和获取这些属性的settergetter方法。然后,我们创建一个Person对象,并将其传递给printPerson()方法,该方法使用参数名p来引用该对象。最后,printPerson()方法打印出Person对象的nameage属性。

4.6.6 调用另外一个构造器

Java中可以通过使用this关键字来调用同一个类中的另一个构造器,或者使用super关键字来调用父类中的构造器。

使用this关键字:

public class Person {
    String name;
    int age;

    public Person() {
        this("Unknown", 0);
    }

    public Person(String name) {
        this(name, 0);
    }

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

在上面的例子中,当调用不同的构造器时,使用了this关键字来调用其他的构造器。例如,在第一个构造器中,使用了this(“Unknown”, 0)来调用第三个构造器,并提供了默认参数值。

使用super关键字:

如果需要调用父类的构造器,则可以使用super关键字。例如:

class Animal {
    int age;

    public Animal(int age) {
        this.age = age;
    }
}

class Dog extends Animal {
    String name;

    public Dog(String name, int age) {
        super(age);
        this.name = name;
    }
}

在上面的例子中,Dog类的构造器中使用了super(age)来调用父类Animal的构造器,并提供了一个age参数。这样就可以在Dog类中使用Animal类的age属性。

在Java中,调用另外一个构造器有以下注意事项:

  1. 使用this关键字调用构造器必须是第一条语句。这是因为构造器中的第一条语句默认是隐式地调用了父类的无参构造器,如果使用this调用构造器不放在第一条语句位置,就会导致父类构造器被调用多次或者没有被调用的情况。

  2. 使用this关键字调用构造器时,只能调用同一个类中的其他构造器,不能调用父类或子类的构造器。

  3. 如果要使用this关键字调用构造器,必须保证被调用的构造器已经存在,且被调用的构造器不会递归调用自己。

  4. 使用super关键字调用父类构造器,语法类似于this关键字,但是需要注意的是,调用父类构造器必须放在子类构造器的第一条语句。

  5. 如果子类构造器没有显式地调用父类构造器,Java编译器会隐式地在子类构造器中插入一条调用父类的无参构造器的语句,因此如果父类中没有无参构造器,就必须在子类构造器中显式地调用父类构造器。

  6. 如果子类自己定义了构造器,不管是有参还是无参的,Java编译器都不会再帮它插入调用父类无参构造器的语句,因此必须显式地调用父类构造器。

4.6.7 初始化块

Java中的初始化块是一段代码块,在创建对象时自动执行。它可以用来初始化对象的属性,或者执行一些其它操作。Java中有两种类型的初始化块:静态初始化块和实例初始化块。

静态初始化块用于初始化静态变量,而实例初始化块用于初始化实例变量。静态初始化块在类加载时执行,实例初始化块在对象创建时执行。

以下是一个使用静态初始化块和实例初始化块的示例:

public class MyClass {
    static {
        // 静态初始化块
        // 初始化静态变量
    }

    {
        // 实例初始化块
        // 初始化实例变量
    }

    // 构造方法
    public MyClass() {
        // 初始化代码
    }
}

注意:

  1. 静态初始化块只能访问静态变量。
  2. 实例初始化块可以访问实例变量和静态变量。
  3. 静态初始化块只会执行一次,而实例初始化块每次创建对象时都会执行。

静态初始化块:

public class MyClass {
    static {
        // 静态初始化块
        System.out.println("静态初始化块");
    }
}

实例初始化块:

public class MyClass {
    // 成员变量
    int num;
    
    {
        // 实例初始化块
        num = 10;
        System.out.println("实例初始化块,num的值为:" + num);
    }
}

4.6.8 对象构析和finalize

对象构析(Destructor)是一个特殊的成员函数,它在对象被销毁时自动调用,用于清理对象占用的资源。在C++中,对象构析的名称与类名称相同,但前面加上了一个波浪号()作为前缀,如ClassName()。对象构析不能被直接调用,而是在以下情况下自动调用:

  1. 对象被删除(delete)
  2. 对象超出了作用域(Scope)
  3. 程序结束

当对象被销毁时,对象构析被调用,它可以执行必要的清理操作,如释放内存、关闭文件等。

Finalize是Java语言中的一个方法,它是Object类中定义的一个方法,用于在垃圾回收器回收Java对象时执行清理操作。Finalize方法会在对象被垃圾回收的过程中调用一次,但是无法保证它会在对象被回收前运行,也无法保证它会在应用程序终止前运行。

和对象构析一样,Finalize方法可以用来清理对象占用的资源,但是由于无法保证它被执行的时间和次数,不能将Finalize方法用作释放重要资源的主要手段。

总的来说,对象构析和Finalize都是用于对象清理的方法,但是对象构析是C++中的概念,而Finalize是Java中的概念,在使用时需要注意它们的不同之处。同时,在管理资源的时候,最好使用RAII(Resource Acquisition Is Initialization)技术,通过构造函数来获取资源,通过析构函数来释放资源,这样更加安全和可靠。

对象构析和finalize是Java中用于管理对象生命周期的两个重要机制。以下是一个示例代码:

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
        System.out.println(name + " is born.");
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(name + " is dead.");
    }

    public static void main(String[] args) {
        Person p1 = new Person("John");
        Person p2 = new Person("Mary");
        p1 = null;
        p2 = null;
        System.gc();
    }
}

在该代码中,我们定义了一个Person类,该类具有一个name属性和一个构造函数。在构造函数中,我们会输出一条语句,表示该对象已经创建。在类中我们还定义了一个finalize方法,当我们调用System.gc()方法时,JVM将会调用该方法,表示该对象即将被销毁。

在main函数中,我们创建了两个Person对象,然后将其置为null,表示不再需要对该对象的引用。最后我们调用了System.gc()方法,告诉JVM进行垃圾回收。经过执行后,我们可以看到如下输出:

John is born.
Mary is born.
John is dead.
Mary is dead.

可以看到,当我们调用System.gc()方法时,JVM会调用每个对象的finalize方法。在该示例中,我们简单地输出了一条语句,表示该对象已经被销毁。

4.7 包

Java 包(package)是用于组织和管理 Java 类和接口的机制。在一个 Java 包内可以包含一组相关的类和接口。通过使用包,Java程序员可以更好地组织自己的代码,并避免名称空间冲突。Java 包的命名规则为:使用小写字母和"."来分隔不同的单词。例如,javax.swing 包中包含了一些 Java GUI 组件,而java.util包中包含了一些常用的类和接口,如List、Map等。可以使用 import 语句来引入需要的包和类。例如:import java.util.*; 表示引入 java.util 包下的所有类和接口。

4.7.1 包名

Java中包名的规范和格式如下:

  1. 包名应该全部小写。

  2. 包名应该以域名的倒序为前缀,例如:com.example.test。

  3. 包名中只能包含字母、数字和下划线。

  4. 包名应该简短、具有描述性和易于记忆。应当避免使用缩写和数字作为包名。

  5. 包名中不能出现Java关键字和保留字。

例如:

正确的包名:com.example.test

错误的包名:example.test.com(顺序不正确)

错误的包名:com.Example.Test(大小写不正确)

错误的包名:com.example.test1(包名中不能有数字)

错误的包名:com.example.public(public是Java关键字)

4.7.2 类的导入

在Java中,通过导入(import)可以将其他类或接口中的成员引入到当前的程序中,以便能够直接使用这些成员。

在Java中,可以使用以下语法来导入类:

import package1.package2.ClassName;

其中,package1和package2分别表示包名,ClassName表示要导入的类名。如果要导入的类位于默认包中,则可以使用以下语法:

import ClassName;

在一个Java程序中,可以同时导入多个类,多个类之间使用逗号分隔。例如:

import java.util.ArrayList;
import java.util.List;

如果要导入一个包中的所有类,可以使用通配符(*)来代替类名。例如:

import java.util.*;

需要注意的是,在使用导入语句之前必须先声明包名或类名,否则会出现编译错误。另外,在同一个包下的类可以直接访问,不需要导入。

4.7.3 静态导入

静态导入是Java 5中引入的一种新的语法,它允许在程序中直接导入一个类的静态成员,而不必使用类名来访问这些成员。例如:

import static java.lang.Math.PI;

public class Test {
    public static void main(String[] args) {
        double r = 2.5;
        double area = PI * r * r;
        System.out.println("圆的面积是:" + area);
    }
}

在上面的例子中,我们使用了静态导入语句import static java.lang.Math.PI导入了Math类中的静态常量PI,这样就可以直接在程序中使用PI了。

需要注意的是,静态导入只能导入静态成员,不能导入非静态成员。而且,如果导入了多个同名的静态成员,那么这些成员必须使用不同的名字来引用。

4.7.4 在包中增加类

要在包中增加类,可以按照以下步骤进行:

  1. 创建一个新的Java类,并编写代码。

  2. 将该Java类文件保存到一个新的.java文件中,并将该文件放置在包的文件夹中。

  3. 在类文件的开头添加包声明语句,以指定该文件所属的包。例如:

    package com.example.myproject;
    
  4. 在需要使用该类的Java文件中,使用import语句导入该类。例如:

    import com.example.myproject.MyClass;
    
  5. 在Java代码中创建该类的实例,并调用其方法或访问其属性。例如:

    MyClass myObj = new MyClass();
    myObj.myMethod();
    

需要注意的是,为了确保包中的类能够被其他Java文件正确导入和使用,需要遵循Java包命名规范,即包名应该使用小写字母,并且应该与域名倒置后的字符串相同。例如,com.example.myproject就是一个合法的包名。

注意事项:

  • 包名和文件夹名需要保持一致。
  • 类名和文件名需要保持一致。
  • 如果要使用其他包中的类,需要在源文件开头使用import语句导入。

4.7.5 包访问

在Java中包访问是指在同一包中可以访问另一个类的方法和属性,但在不同包中则不能访问。

Java中的包访问限定符是一种访问控制机制,它用于控制类、接口、变量和方法在包内的可见性。

包访问权限是默认的访问权限,如果没有指定访问权限,Java类和接口将自动具有包访问权限。这意味着只有在同一个包内的其他类和接口才能访问它。

包访问限定符通常用于实现封装,即将类、接口、变量和方法隐藏在包内,以防止其他包中的代码不小心修改或访问它们。

在Java中,可以使用关键字package将类、接口、变量和方法放在一个包中。例如:

package com.example;

class MyClass {
  void myMethod() {
    // ...
  }
}

在上述示例中,MyClass将被放置在名为com.example的包中,并且只有在这个包内的其他类和接口才能访问MyClass。如果没有指定访问权限,MyClass将具有默认的包访问权限。

这里是一个简单的示例:

假设我们有两个类,分别在同一包中:

package com.example;

class A {
    void sayHello() {
        System.out.println("Hello from class A!");
    }
}
package com.example;

class B {
    void callSayHello() {
        A a = new A();
        a.sayHello();
    }
}

在这个示例中,类B可以访问类A的方法sayHello(),因为它们在同一个包中。

如果将类B移动到不同的包中,如下所示:

package com.example2;

class B {
    void callSayHello() {
        A a = new A(); // 编译错误:无法访问A
        a.sayHello(); // 编译错误:无法访问A
    }
}

此时,类B将无法访问类A的方法和属性,因为它们不在同一个包中。

4.7.6 类路径

Java的类路径是指Java虚拟机(JVM)在启动时查找类文件的位置。当Java程序使用类或包时,JVM会按照特定的顺序在类路径中查找相应的类文件。Java的类路径可以由以下三种方式设置:

  1. 环境变量:设置环境变量CLASSPATH来指定类路径,多个路径用分号或冒号分隔。

  2. 命令行参数:在启动Java程序时,可以通过命令行参数-cp或-classpath来指定类路径。

  3. 默认值:当没有设置环境变量或命令行参数时,JVM会使用默认的类路径,包括系统类库和当前工作目录。

在类路径中,可以包含目录、Jar文件和Zip文件,以及它们的组合。

以下是设置Java类路径的代码示例:

  1. 使用环境变量设置类路径:
set CLASSPATH=C:\myproject\lib;C:\myproject\classes
  1. 使用命令行参数设置类路径:
java -classpath C:\myproject\lib;C:\myproject\classes com.example.MyClass
  1. 在Java程序中设置类路径:
public class MyClass {
    public static void main(String[] args) {
        String classpath = System.getProperty("java.class.path");
        System.out.println("Class path: " + classpath);
    }
}

以上代码会输出当前Java程序的类路径。程序运行时,可以通过命令行参数-cp或-classpath来指定新的类路径,或者使用System.setProperty方法设置系统属性java.class.path。

4.7.7 设置类路径

假设我们有一个项目,其中包含以下两个文件:

  • Main.java:包含主要的程序逻辑。
  • Util.java:包含一些辅助函数。

其中 Main.java 引用了 Util.java 中定义的函数。为了让程序成功编译和运行,需要在编译和运行时设置类路径。

下面是一个示例程序,展示如何设置类路径:

// Main.java
public class Main {
    public static void main(String[] args) {
        // 调用 Util 类中的辅助函数
        int result = Util.add(1, 2);
        System.out.println("1 + 2 = " + result);
    }
}
// Util.java
public class Util {
    public static int add(int a, int b) {
        return a + b;
    }
}

假设这两个文件保存在 /Users/username/MyProject/ 目录下。在编译 Main.java 文件时,需要指定类路径。假设我们将编译后的文件保存在 /Users/username/MyProject/bin/ 目录下,可以使用以下命令编译:

javac -cp /Users/username/MyProject/ -d /Users/username/MyProject/bin/ /Users/username/MyProject/Main.java

这里使用了 -cp 参数来指定类路径,即 /Users/username/MyProject/ 目录。同时,使用 -d 参数指定编译后的文件保存目录为 /Users/username/MyProject/bin/

在运行 Main.class 文件时,同样需要设置类路径。使用以下命令运行:

java -cp /Users/username/MyProject/bin/ Main

这里使用了 -cp 参数来指定类路径,即 /Users/username/MyProject/bin/ 目录。同时,指定了要运行的主类为 Main

4.8 JAR文件

JAR(Java Archive)文件是一种包含 Java 类、资源文件和元数据的文件格式。通常用于将多个类和相关的文件打包在一起,以便在多个应用程序之间共享和重用。使用 JAR 文件可以方便地将应用程序和库分发给其他开发人员和用户,也可以方便地管理和更新应用程序和库的版本。

JAR 文件通常使用 jar 工具创建,该工具可以在 JDK 中找到。以下是使用 jar 工具创建 JAR 文件的示例命令:

jar cvf MyLibrary.jar -C classes .

这里:

  • jar 是 JAR 工具的命令。
  • cvf 表示创建(c)、显示进度信息(v)和指定输出文件名(f)。
  • MyLibrary.jar 是要创建的 JAR 文件的名称。
  • -C classes . 表示将 classes 目录中的所有文件添加到 JAR 文件中,-C 选项指定了添加文件时的目录位置,. 表示当前目录。

上述命令将创建名为 MyLibrary.jar 的 JAR 文件,其中包含 classes 目录中的所有文件。

要运行 JAR 文件中的程序,可以使用以下命令:

java -jar MyLibrary.jar

这里使用了 -jar 选项来指定要运行的文件是 JAR 文件。JVM 将查找 MyLibrary.jar 文件中的 META-INF/MANIFEST.MF 文件,以确定要运行的主类。例如,MANIFEST.MF 文件中可能包含以下内容:

Manifest-Version: 1.0
Main-Class: com.example.MyApplication

这指定了 com.example.MyApplication 类为 JAR 文件的主类,当在 JAR 文件中运行时,JVM 将从该类的 main() 方法开始执行。

4.8.1创建JAR文件

在Java中,可以使用Java自带的jar命令创建JAR文件。以下是一些简单步骤:

  1. 编写Java代码并将其编译为class文件。

  2. 创建manifest.txt文件,其中包含以下内容:

    Main-Class: MainClass
    

    其中 MainClass 是包含main()方法的类的名称。

  3. 在命令行中使用以下命令创建JAR文件:

    jar cvfm MyJar.jar manifest.txt *.class
    

    该命令语法是:

    jar cvfm [jar文件名] [manifest文件名] [要包含的所有class文件]
    
  4. 如果一切顺利,您将会在同一目录下创建一个名为MyJar.jar的JAR文件。

值得注意的是,您可以使用jar命令的其他选项,例如压缩和添加其他文件。下面是一些常用选项:

  • c: 创建新的JAR文件
  • v: 显示详细输出
  • f: 指定JAR文件名
  • m: 包含指定的清单文件
  • e: 指定主类
  • x: 提取指定的JAR文件
  • t: 列出指定JAR文件中的内容

希望这可以帮助您创建自己的JAR文件!

以下是一个简单的示例,演示如何将多个 Java 类打包成一个 JAR 文件,并从中运行一个程序:

首先,我们有两个 Java 类,分别为 HelloWorld.javaGreetings.java

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        Greetings.greet();
    }
}

// Greetings.java
public class Greetings {
    public static void greet() {
        System.out.println("Hello, World!");
    }
}

接下来,我们使用以下命令将这两个类打包成一个 JAR 文件:

jar cvf HelloWorld.jar HelloWorld.class Greetings.class

这将创建一个名为 HelloWorld.jar 的 JAR 文件,其中包含 HelloWorld.classGreetings.class 两个类文件。

要从 JAR 文件中运行程序,可以使用以下命令:

java -cp HelloWorld.jar HelloWorld

这里使用了 -cp(或 -classpath)选项来指定类路径,将 JAR 文件的位置添加到类路径中。然后指定要运行的主类为 HelloWorld,这将启动 HelloWorld.main() 方法,并调用 Greetings.greet() 方法打印 “Hello, World!”。

注意,JAR 文件必须包含一个包含 main() 方法的类,或者包含一个 META-INF/MANIFEST.MF 文件,该文件指定了 JAR 文件的主类。在本例中,我们将 HelloWorld 类指定为主类,并使用 java -cp 命令运行 JAR 文件。

4.8.2 清单文件

清单文件(Manifest file)是一种文本文件,用于描述文件清单中的项目清单,例如软件包、应用程序、网页或其他项目。清单文件通常包含项目名称、版本号、作者、许可证信息和其他相关信息。在某些情况下,清单文件还包含文件列表、文件哈希值和其他元数据。清单文件可以用于验证文件是否完整、识别文件来源和实现版本控制。它还可以用于管理依赖项、插件和其他相关项目。常见的清单文件格式包括XML、JSON和文本文件格式。

在Java中,清单文件是一种特殊的文本文件,通常命名为MANIFEST.MF。在创建JAR文件时,您需要添加一个清单文件,以描述JAR文件中包含的内容。

以下是Java清单文件的一些操作:

  1. 创建清单文件

您可以使用文本编辑器创建清单文件,格式如下:

Manifest-Version: 1.0
Main-Class: com.example.MyMainClass

其中,Manifest-Version指定了清单文件的版本,Main-Class指定了JAR文件的入口点。您还可以添加其他属性(键值对),以描述JAR文件中包含的内容。

  1. 将清单文件添加到JAR文件中

要将清单文件添加到JAR文件中,请使用jar命令的-m选项,如下所示:

jar cvfm MyJar.jar Manifest.txt com/example/*.class

这将创建一个名为MyJar.jar的JAR文件,并将清单文件Manifest.txt添加到JAR文件中。界定符m告诉jar命令使用指定的清单文件,f指定JAR文件名,v显示详细输出,c创建新的JAR文件。

  1. 查看JAR文件的清单文件

要查看JAR文件的清单文件,请使用命令:

jar tf MyJar.jar

这将列出JAR文件中的所有条目,包括清单文件。

  1. 更新清单文件

要更新JAR文件的清单文件,请使用以下命令:

jar uf MyJar.jar Manifest.txt

这将更新JAR文件中的清单文件并保留其他内容不变。

  1. 提取清单文件

要提取JAR文件中的清单文件,请使用以下命令:

jar xf MyJar.jar META-INF/MANIFEST.MF

这将提取JAR文件中的清单文件,并将其保存为名为MANIFEST.MF的文件。

总之,清单文件是Java JAR文件中的重要组成部分,帮助管理和描述文件。使用Java的jar命令和各种文本编辑器,可以轻松地创建、修改和提取JAR文件中的清单文件。

4.8.3 可执行JAR文件

可执行JAR文件是一种Java JAR文件,它包含应用程序的所有代码和资源,并且可以在不安装JDK或其他Java运行时环境的情况下运行。以下是创建可执行JAR文件的步骤:

  1. 创建Manifest文件

可执行JAR文件需要一个特殊的清单文件,它告诉JVM如何执行程序。您可以使用任何文本编辑器创建清单文件,并将其命名为Manifest.mf。清单文件应该包含以下内容:

Manifest-Version: 1.0
Main-Class: mypackage.MyClass

其中MyClass是您的程序中的类的名称,mypackage是包的名称。

  1. 编写Java代码

在创建可执行JAR文件之前,您需要先编写Java代码,将其编译为.class文件。在编写代码时,请记住将其组织为包,以便将其放入JAR文件中。

  1. 创建JAR文件

使用Java的jar工具,将.class文件打包成JAR文件。例如,如果您的类文件在bin目录中,则可以使用以下命令:

jar cfm myapp.jar Manifest.mf -C bin .

这将创建一个名为myapp.jar的JAR文件,并将Manifest.mf文件添加到JAR文件中。-C选项指定要包含在JAR文件中的目录,"."表示所有文件。

  1. 运行JAR文件

通过在命令行上输入以下命令来运行JAR文件:

java -jar myapp.jar

这将运行JAR文件中的主类。

  1. 打包和分发

最后,将可执行JAR文件打包在zip文件或其他压缩文件中,并将其分发给用户即可。

总之,创建可执行JAR文件的重要步骤包括创建清单文件,编写Java代码,使用jar工具创建JAR文件,并通过命令行运行JAR文件。创建可执行JAR文件是Java程序分发的一种方便和可移植方式。

4.8.4 关于命令行选项的说明

Java命令行选项是用于控制Java虚拟机(JVM)运行的选项。以下是一些常用的Java命令行选项的说明:

  • -classpath-cp: 指定类路径,可以是目录、JAR文件或ZIP文件。
  • -Xms-Xmx: 分别指定JVM的最小和最大堆空间大小,例如-Xms512m -Xmx1024m表示最小堆大小为512MB,最大堆大小为1GB。
  • -jar: 指定要运行的可执行JAR文件。
  • -Dproperty=value: 设置系统属性,例如-Duser.home=/home/user
  • -verbose: 输出JVM运行信息,等效于-verbose:gc -verbose:class
  • -version: 显示Java版本信息。
  • -help-h: 显示Java帮助信息。

Java工具命令行选项是用于控制Java开发工具的选项,例如javac编译器、jar打包工具、javadoc文档生成工具等。以下是一些常用的Java工具命令行选项的说明:

  • javac选项:
    • -d: 指定编译输出目录。
    • -classpath-cp: 指定编译依赖的类路径。
    • -source-target: 分别指定源代码和目标字节码的版本。
    • -verbose: 输出编译过程信息。
  • jar选项:
    • -c: 创建新的JAR文件。
    • -x: 从JAR文件中提取文件。
    • -f: 指定JAR文件名。
    • -m: 指定主清单文件。
    • -e: 指定可执行类。
    • -C: 指定文件路径,例如-C /path/to/directory .
  • javadoc选项:
    • -d: 指定文档输出目录。
    • -classpath-cp: 指定文档依赖的类路径。
    • -sourcepath: 指定源代码路径。
    • -subpackages: 指定要生成文档的子包。
    • -windowtitle: 指定文档窗口标题。
    • -encoding: 指定源代码编码格式。

除了这些命令行选项之外,Java还有其他工具命令行选项,例如keytool用于管理密钥和证书、jarsigner用于签名和验签JAR文件等。在使用Java工具命令行选项时,应注意选项的含义和用法,以确保正确地控制Java工具的运行。建议在命令行上使用-help-h选项以获取帮助信息。

4.9 文档注释

文档注释(Document Comment),也被称为javadoc注释,是Java中一种特殊的注释格式,用于生成API文档。文档注释以/**开头,以*/结尾,中间包含对类、方法、字段等的描述,以及一些标记(Tag),例如@param@return等,用于生成API文档。

文档注释可以包含以下内容:

  • 类、接口、枚举类型的描述
  • 构造方法、方法、字段的描述
  • 参数、返回值、异常的说明
  • 代码示例、版本信息、作者信息等

下面是一个带有文档注释的Java类的示例:

/**
 * 这是一个示例类,用于演示文档注释的使用。
 */
public class ExampleClass {
    /**
     * 这是一个示例方法,返回一个字符串。
     *
     * @param name 名称
     * @return 字符串
     * @throws IllegalArgumentException 如果名称为null或空字符串
     */
    public String exampleMethod(String name) throws IllegalArgumentException {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Name cannot be null or empty");
        }
        return "Hello, " + name + "!";
    }
}

在文档注释中,使用@param标记说明方法的参数,使用@return标记说明方法的返回值,使用@throws标记说明方法可能抛出的异常。在生成API文档时,javadoc会根据文档注释中的标记生成相应的文档内容。

文档注释是Java中良好的编程习惯之一,它可以提高代码的可读性和可维护性,同时也方便其他开发人员使用API。因此,在编写Java代码时,应尽量为类、接口、方法、字段等添加文档注释。

4.9.1 注释插入

Java 文档注释是一种特殊注释,用于为 Java 类、接口、方法或变量等的文档提供说明和描述。Java 文档注释的格式是以双星号开头,每行注释以星号开头,最后以星号结尾,例如:

/**
 * 这是一个示例类,用于演示 Java 文档注释的用法。
 * <p>
 * 这个类包含了以下功能:
 * <ul>
 * <li>示例方法1</li>
 * <li>示例方法2</li>
 * </ul>
 */
public class ExampleClass {

  /**
   * 这是示例方法1,用于演示 Java 文档注释的用法。
   * 
   * @param arg1 参数1的说明
   * @param arg2 参数2的说明
   * @return 返回值的说明
   */
  public String exampleMethod1(String arg1, int arg2) {
    // 这是示例方法1的代码
    return "example";
  }

  /**
   * 这是示例方法2,用于演示 Java 文档注释的用法。
   * 
   * @param arg 参数的说明
   */
  public void exampleMethod2(String arg) {
    // 这是示例方法2的代码
  }
}

在 Eclipse 或其他集成开发环境中,输入“/ **”并按下 Enter 键,将自动生成一些格式化的注释内容。您可以根据需要修改和添加注释信息。建议在编写代码时尽可能多地使用 Java 文档注释,以便后续维护和代码重用。

4.9.2 类注释

在Java中,类注释是指在类声明之前使用 Java 文档注释的形式为该类提供说明和描述。类注释应描述该类的用途、实现细节和注意事项等信息,以便其他开发人员能够更好地理解和使用该类。

通常,类注释应该包含以下信息:

  • 类的用途和功能。
  • 类的设计原则和模式。
  • 类的输入和输出,包括参数类型、返回类型和抛出异常类型等。
  • 其他与该类相关的信息,如作者、版本、修改记录、依赖关系等。

类注释一般以“/ **”开头,在类声明之前进行插入。例如:

/**
 * 这是一个示例类,用于演示类注释的用法。
 * <p>
 * 这个类包含了以下功能:
 * <ul>
 * <li>示例方法1</li>
 * <li>示例方法2</li>
 * </ul>
 * 
 * @version 1.0
 */
public class ExampleClass {

  /**
   * 这是示例方法1,用于演示类注释的用法。
   * 
   * @param arg1 参数1的说明
   * @param arg2 参数2的说明
   * @return 返回值的说明
   */
  public String exampleMethod1(String arg1, int arg2) {
    // 这是示例方法1的代码
    return "example";
  }

  /**
   * 这是示例方法2,用于演示类注释的用法。
   * 
   * @param arg 参数的说明
   */
  public void exampleMethod2(String arg) {
    // 这是示例方法2的代码
  }
}

需要注意的是,类注释应该足够清晰和详细,以便其他人能够轻松地了解该类的用途和实现细节。同时,类注释应该与代码保持同步,随着代码的更新和修改进行相应的更新和修改。

4.9.3 方法注释

在Java中,方法注释是指使用 Java 文档注释为方法提供说明和描述。方法注释应该描述该方法的用途、参数、返回值和抛出的异常等信息,以便其他开发人员能够更好地理解和使用该方法。

通常,方法注释应该包含以下信息:

  • 方法的用途和功能。
  • 方法的输入和输出,包括参数类型、返回类型和抛出异常类型等。
  • 方法的实现细节和实现注意事项等信息。

方法注释一般以“/ **”开头,在方法声明之前进行插入。例如:

/**
 * 这是示例方法,用于演示方法注释的用法。
 * 
 * @param arg1 参数1的说明
 * @param arg2 参数2的说明
 * @return 返回值的说明
 * @throws Exception 异常类型和说明
 */
public String exampleMethod(String arg1, int arg2) throws Exception {
  // 这是示例方法的代码
  return "example";
}

需要注意的是,在编写方法注释时,应该尽可能详细和清晰地说明方法的用途和实现细节,以便其他开发人员能够更好地理解和使用该方法。同时,注释应该与代码保持同步,随着代码的更新和修改进行相应的更新和修改。

4.9.4 字段注释

在Java中,字段注释是指使用 Java 文档注释为字段提供说明和描述。字段注释应该描述该字段的用途、类型、可见性和默认值等信息,以便其他开发人员能够更好地理解和使用该字段。

通常,字段注释应该包含以下信息:

  • 字段的用途和功能。
  • 字段的数据类型和可见性,包括public、private、protected等。
  • 字段的默认值和赋值方式等信息。

字段注释一般以“/ **”开头,在字段声明之前进行插入。例如:

/**
 * 这是示例字段,用于演示字段注释的用法。
 */
public String exampleField = "example";

需要注意的是,在编写字段注释时,应该尽可能详细和清晰地说明该字段的用途和实现细节,以便其他开发人员能够更好地理解和使用该字段。同时,注释应该与代码保持同步,随着代码的更新和修改进行相应的更新和修改。

4.9.5 通用注释

在Java中,常用的通用注释包括类注释、方法注释和字段注释。这些注释可以帮助提高代码的可读性和可维护性,同时也方便其他人阅读和理解代码。

以下是Java中常用的通用注释样例:

  1. 类注释
/**
 * 描述类的用途和功能。
 * 可以在这里对类的使用说明进行补充。
 */
public class ExampleClass {
    //class body
}
  1. 方法注释
/**
 * 描述方法的用途和功能。
 * @param param1 参数1的说明
 * @param param2 参数2的说明
 * @return 返回值的说明
 * @throws Exception 抛出异常的情况说明
 */
public returnType exampleMethod(param1, param2) throws Exception {
    //method body
}
  1. 字段注释
/**
 * 字段的用途和功能。
 * 可以在这里对字段的说明进行补充。
 */
public type exampleField = value;

需要注意的是,在使用通用注释时,应该根据具体情况进行适当的修改和补充,以便更好地描述和说明代码的用途和实现细节。同时,注释应该与代码保持同步,随着代码的更新和修改进行相应的更新和修改。

4.9.6 包注释

Java中,可以使用包注释来描述一个Java包的作用和功能。包注释应该位于包声明之前,格式如下:

/**
 * 包的描述和说明。
 */
package com.example.packageName;

其中,包的描述和说明应该简明扼要地说明该包的功能和用途。需要注意的是,包注释应该使用Javadoc注释风格,并且应该在描述和说明之后加上一个空行,以便区分和美观。

包注释可以方便其他人了解该包的作用和用途,同时也可以提高代码的可读性和可维护性。因此,在编写Java代码时,建议给每个包都添加相应的注释说明。

4.10 类设计技巧

Java的类设计技巧包括以下几点:

  1. 封装性:将类的内部细节隐藏起来,只公开必要的属性和方法,提供访问接口,可以有效地保护数据,防止不合理或不安全的操作。

  2. 继承性:通过继承和派生,可以在现有的类基础上扩展出新的类,重用原有的代码,提高代码的复用性和可维护性。

  3. 多态性:通过多态机制,同一种方法可以有不同的实现方式,使得代码更加灵活和扩展性更高,减少代码的冗余。

  4. 抽象性:通过接口、抽象类等抽象机制,将具体的实现与接口分离,提高代码的复用性、可维护性和可扩展性。

  5. 组合和聚合:通过组合和聚合的方式,可以将多个类组合成一个复杂的类或对象,提高代码的复用性和可维护性。

  6. 设计模式:使用常用的设计模式,如单例模式、工厂模式、观察者模式、策略模式等,可以帮助开发人员更好地组织代码,提高代码的复用性和可维护性。

综上所述,Java的类设计技巧是多方面的,需要开发人员在实践中不断总结和提高,以获得更好的代码质量和开发效率。

第五章 继承

Java 继承是一种创建新类的方式,在已有类的基础上创建一个新类。子类继承父类的所有属性和方法,并且可以添加自己的属性和方法,这样可以避免重复代码,并且可以更好地组织和管理代码。

在 Java 中,使用关键字 extends 来实现继承,子类可以继承父类的公有属性和方法,但不可继承私有属性和方法。同时,子类可以重写(Override)父类的方法,以实现自己的功能。

示例代码:

// 父类
public class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

// 子类
public class Cat extends Animal {
    public void meow() {
        System.out.println("Cat is meowing");
    }

    @Override
    public void eat() {
        System.out.println("Cat is eating");
    }
}

// 测试代码
public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.eat(); // 输出 "Cat is eating"
        cat.meow(); // 输出 "Cat is meowing"
    }
}

在上面的示例代码中,Cat 类继承了 Animal 类,并添加了自己的 meow() 方法。在重写父类的 eat() 方法时,子类实现了自己的功能。在测试代码中,可以看到 Cat 对象可以调用继承来的 eat() 方法和自己添加的 meow() 方法。

5.1 类、超类和子类

在面向对象的编程中,一个类可以被其他多个类继承,并且一个类只能有一个父类,这种关系称为类的继承关系。在继承关系中,父类也被称为超类(Superclass)或基类(Base class),而子类(Subclass)则是继承父类的类。

超类(父类)拥有所有子类共有的属性和方法,这些属性和方法在子类中都可以被直接调用。子类则可以在继承超类的基础上,增加自己的属性和方法,从而实现更多的功能。

在 Java 中,使用关键字 extends 声明一个类继承另一个类。例如:

class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

class Cat extends Animal {
    public void meow() {
        System.out.println("Cat is meowing");
    }
}

在上面的代码中, Cat 类继承了 Animal 类,Animal 类就是 Cat 类的超类。Cat 类继承了 Animal 类的 eat() 方法,同时也定义了自己的 meow() 方法。

需要注意的是,私有成员变量和方法不能被继承。如果需要在子类中访问父类中的私有成员,可以通过父类提供的公有方法访问。

例如:

class Animal {
    private int age;
    public void setAge(int age) {
        this.age = age;
    }
    public int getAge() {
        return age;
    }
}

class Cat extends Animal {
    public void setCatAge(int age) {
        setAge(age);
    }
}

在上面的代码中,Animal 类中的 age 成员变量是私有的,Cat 类中的 setCatAge() 方法调用了父类的 setAge() 方法,从而实现了对父类中私有成员变量的访问。

5.1.1 定义子类

在 Java 中,定义子类的语法如下:

class Subclass extends Superclass {
    // 子类的成员变量和方法
}

其中,Subclass 是子类的名称,Superclass 是父类的名称。子类继承父类中的所有非私有成员变量和方法,并可以添加自己的成员变量和方法。

在子类定义中,我们可以重写父类的方法,即在子类中定义一个方法和父类中方法的名称、参数列表以及返回类型相同,但是实现方式不同的方法。

例如,我们可以将上一篇答案中的 Animal 类和 Cat 类进行修改,使 Cat 类重写了 Animal 类中的 eat() 方法:

class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat is eating");
    }

    public void meow() {
        System.out.println("Cat is meowing");
    }
}

在上面的代码中,Cat 类重写了 Animal 类中的 eat() 方法,同时自己也定义了 meow() 方法。

需要注意的是,如果一个方法在父类和子类中有不同的实现方式,当我们通过子类来调用这个方法时,实际调用的是子类中的方法。但是,当我们通过父类的引用来调用这个方法时,实际调用的是父类中的方法。这种行为称为多态。

5.1.2覆盖的方法

Java中,子类可以覆盖超类的方法。覆盖(Override)是指在子类中重新实现超类中已有的方法,而不是创建一个新的方法。

覆盖超类方法的基本要求:

  • 方法名、参数列表和返回类型必须与超类方法相同。
  • 子类方法访问权限不能低于超类方法访问权限。
  • 子类方法抛出的异常类型不能超出超类方法抛出的异常类型范围。
  • 子类方法不能使用超类方法中被声明为final的变量。

示例:

class Animal {
    public void makeSound() {
        System.out.println("The animal makes a sound");
    }
}

class Cat extends Animal {
    // 覆盖超类方法
    public void makeSound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Cat cat = new Cat();

        // 调用超类方法
        animal.makeSound();

        // 调用子类方法
        cat.makeSound();
    }
}

输出结果:

The animal makes a sound
Meow

5.1.3 子类构造器

在Java中,子类可以拥有自己的构造器。子类构造器必须调用超类构造器,通常使用super()语句。

子类构造器的基本要求:

  • 子类构造器的第一行必须调用超类构造器,可以使用super()或者super(参数)语句。
  • 如果子类构造器未显式调用超类构造器,则编译器会默认在子类构造器的第一行插入super()语句。
  • 子类构造器可以调用本类的其他构造器,但必须是在第一行,同时使用this()语句。

示例:

class Animal {
    private String name;

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

    public String getName() {
        return name;
    }
}

class Cat extends Animal {
    private int age;

    public Cat(String name, int age) {
        // 调用超类构造器
        super(name);
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat("Tom", 3);

        System.out.println("Name: " + cat.getName());
        System.out.println("Age: " + cat.getAge());
    }
}

输出结果:

Name: Tom
Age: 3

5.1.4 继承层次

在Java中,有一个基础层次(Base Class Hierarchy)或者基础类库(Base Class Library),这是Java平台提供的一组预定义类和接口,可以用于开发Java应用程序。

基础类库分为三个部分:

  1. Java核心类库(Java Core Libraries):提供了Java语言基础功能,例如字符串、集合、输入输出、网络、多线程等。
  2. Java标准扩展(Java Standard Extensions):提供了一些标准的扩展功能,例如XML处理、JDBC数据库连接、Java管理扩展等。
  3. Java可选包(Java Optional Packages):提供了一些可选的、更加特定的功能,例如Java 3D、Java EE、Java ME等。

基础类库中最常用的是Java核心类库,其中一些最常用的包括:

  • java.lang:提供了Java语言的基础类和接口,例如Object、String、Number等。
  • java.util:提供了一些通用的工具类和集合类型,例如List、Map、Set等。
  • java.io:提供了输入/输出操作的类和接口,例如File、InputStream、OutputStream等。
  • java.net:提供了网络编程的类和接口,例如Socket、URL等。
  • java.awt:提供了基础的GUI组件和绘图功能,例如Frame、Button、Graphics等。

其他更加具体的功能需要使用Java标准扩展或Java可选包中的类库。

Java中所有的类都直接或间接地从Object类继承。这就意味着每一个Java类都具备Object类的方法和属性。

除了Object类,Java中的类可以通过extends关键字实现继承。子类可以继承父类的属性和方法,并且可以扩展和重写父类的方法。这样,子类就可以通过父类的方法和属性进行代码的重用和快速开发。

而在Java中,类的继承关系可以形成继承层次。在继承层次中,每个子类只有一个直接的父类,但是一个父类可以拥有多个子类。

例如,假设我们有一个动物类Animal,它有一些属性和方法。现在我们想要创建一个狗类Dog,它继承自Animal类,并且有自己的一些属性和方法。此时,我们就可以使用extends关键字:

class Animal {
    String name;
    int age;

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

class Dog extends Animal {
    String breed;

    public void bark() {
        System.out.println("Dog is barking");
    }
}

在这个例子中,Dog类继承了Animal类,并且拥有自己的breed属性和bark方法。因此,Animal是Dog类的父类,Dog是Animal类的子类,它们形成了一个简单的继承层次。

5.1.5 多态

Java多态是指,同一类型的对象,在不同的情况下可能会表现出不同的行为。这个概念是面向对象编程中非常重要的一个概念。

实现多态主要依靠两个机制:继承和方法重写。如果一个子类继承了一个父类,那么它可以重写父类中的方法,从而改变该方法的行为。在程序运行时,如果用父类类型声明一个对象,并且该对象实际上是子类的一个实例,那么调用该对象中重写的方法时,将执行子类中的方法。

下面是一个简单的例子,说明Java多态的概念:

// 父类
class Animal {
    public void makeSound() {
        System.out.println("Animal is making a sound");
    }
}

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

// 测试类
public class Test {
    public static void main(String[] args) {
        // 用父类类型声明一个Dog对象
        Animal animal = new Dog();
        // 调用重写的方法
        animal.makeSound();
    }
}

在这个例子中,我们定义了一个Animal类和一个继承自Animal的Dog类。在Test类中,我们用Animal类型声明了一个Dog对象,并且调用了它的makeSound方法。由于Dog类重写了Animal类中的makeSound方法,因此调用该方法时,实际执行的是Dog类中的makeSound方法,输出的内容是“Dog is barking”。

这就是Java多态的实现方式:用父类类型声明一个对象,并且在运行时实际执行子类中的方法。这种方式可以提高代码的灵活性和可重用性,同时也可以使代码更易于维护和扩展。

5.1.6 理解方法调用

在Java中方法的调用可以通过以下三种方式来实现:

  1. 基于静态绑定的方法调用:
    静态绑定是指在编译时就能确定方法调用的具体实现,由编译器进行绑定。当编译器发现一个方法调用时,它会检查调用者和被调用方法的类型信息,并确定调用哪个具体实现,这个过程叫做静态绑定。静态绑定是通过类的继承关系进行实现的。在Java中如果子类中定义了和父类中相同的方法,则子类会覆盖父类的方法,如果在父类中调用该方法,则会动态绑定到子类的方法。

  2. 基于动态绑定的方法调用:
    动态绑定是指在程序运行时才能确定方法调用的具体实现,由JVM进行绑定。当JVM在程序运行时发现一个方法调用时,会检查对象的实际类型并确定调用哪个具体实现,这个过程叫做动态绑定。动态绑定是通过对象的实际类型进行实现的。在Java中,如果一个类实现了一个接口,那么在调用该接口的方法时,JVM会动态绑定到实现该接口的类的方法。

  3. 基于接口的方法调用:
    Java中的接口提供了一种关键字级别的多态性,允许将不同的对象绑定到相同的接口类型,并以相同的方式来对这些对象进行操作。实现了某个接口的类必须实现该接口中定义的所有方法,这样在调用该接口的方法时就可以动态绑定到实现该接口的类的方法。

通过这三种方式,Java实现了多态性,使得程序的设计更加灵活和可扩展。

5.1.7阻止继承:final类和方法

在Java中,final关键字可以用于类、方法和变量上。使用final关键字声明的类、方法或变量不能再被继承、覆盖或修改。

  1. final类:
    使用final关键字声明的类称为final类。final类不能被其他类继承。final类通常用在不希望被修改的类上,比如String类和Math类就是final类。

  2. final方法:
    使用final关键字声明的方法称为final方法。final方法不能被子类覆盖(重写),但是可以被子类继承并直接使用。final方法通常用在不希望被子类修改的方法上,比如Object类中的getClass()方法就是final方法。

总的来说,使用final关键字可以有效地防止继承和覆盖,增强程序的安全性和稳定性。但是使用final关键字也有一定的局限性,可能会影响程序的灵活性和扩展性,因此需要在实际开发中根据具体情况进行选择。

5.1.8 强制类型转换

Java中,类之间的强制类型转换分为向上转型和向下转型两种。

  1. 向上转型

向上转型是将子类对象转换为父类对象的过程。这种转换不需要特殊的语法,只需要使用父类的引用指向子类的对象即可,例如:

Animal animal = new Cat(); // 将Cat对象向上转型为Animal对象

上述代码中,Cat是Animal的子类,通过将Cat对象赋值给Animal类型的变量,就完成了向上转型。

向上转型可以让程序变得更加灵活,因为我们可以使用父类的引用来操作子类对象,而不需要知道具体的子类类型。但是,由于向上转型后,无法再使用子类中特有的方法和成员变量,因此需要谨慎使用。

  1. 向下转型

向下转型是将父类对象转换为子类对象的过程。这种转换需要使用强制类型转换的语法,在某些情况下,可能会出现类型转换异常的问题。例如:

Animal animal = new Cat();
Cat cat = (Cat) animal; // 将Animal对象向下转型为Cat对象

上述代码中,我们首先使用Animal类型的引用指向Cat类型的对象,然后将Animal对象强制转换为Cat类型的对象,完成了向下转型的过程。

需要注意的是,向下转型存在一定的风险,因为如果将一个父类对象转换为一个与其类型不兼容的子类对象,就会出现类型转换异常。因此,在进行向下转型时,需要先使用 instanceof 运算符来判断一个对象是否属于某个类,从而避免类型转换异常的问题。例如:

Animal animal = new Cat();
if (animal instanceof Cat) {
    Cat cat = (Cat) animal; // 将Animal对象向下转型为Cat对象
}

5.1.9抽象类

Java中,抽象类是一种不能被实例化的类,它只能被用作其他类的父类,用于定义一些抽象方法和抽象属性,具体的实现由子类来完成。以下是Java抽象类的一些特点:

  1. 抽象类不能被实例化,也就是不能创建抽象类的对象。

  2. 抽象类可以包含抽象方法和非抽象方法,抽象方法没有方法体,只有方法签名,而非抽象方法有具体的实现。

  3. 子类继承抽象类后,必须要实现所有的抽象方法才能被实例化,否则这个子类也必须声明为抽象类。

  4. 抽象类可以继承其他的抽象类,也可以实现接口。

下面是一个简单的抽象类的例子:

abstract class Shape {
    protected String color;

    public Shape(String color) {
        this.color = color;
    }

    public abstract double area();

    public abstract String toString();
}

class Circle extends Shape {
    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public String toString() {
        return "Circle{" +
                "color='" + color + '\'' +
                ", radius=" + radius +
                '}';
    }
}

class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "color='" + color + '\'' +
                ", width=" + width +
                ", height=" + height +
                '}';
    }
}

上述代码中,Shape类是一个抽象类,它包含了一个抽象方法 area() 和一个抽象方法 toString(),这两个方法的具体实现由它的子类 Circle 和 Rectangle 来完成。Circle和Rectangle类继承自Shape类,并实现了它的抽象方法,因此它们可以被实例化并使用。

5.1.10 受保护访问

Java中,受保护访问控制符 protected 用来控制只有本类和其子类中的方法可以访问该成员变量或成员方法,其他的类不能访问。具体实现方法如下:

  1. 将需要受保护的成员变量或成员方法声明为 protected

  2. 子类中可以直接访问父类中的 protected 成员变量或成员方法。

  3. 子类中的成员变量或成员方法可以继承父类的 protected 成员变量或成员方法,但是不能访问父类实例的私有成员变量或成员方法。

下面是一个简单的例子:

public class Animal {
    protected String name;

    protected void eat() {
        System.out.println("Animal is eating.");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("Dog is barking.");
    }
    
    public void eatFood() {
        eat(); // 可以访问父类的 protected 方法
        System.out.println("Dog is eating food.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.bark(); // 可以直接访问父类的 protected 成员变量
        dog.eatFood(); // 子类中可以调用父类的 protected 方法
    }
}

在上述代码中,父类 Animal 中的 nameeat() 方法被声明为 protected。子类 Dog 继承了父类 Animal 并且可以调用父类的 protected 方法 eat(),但是不能访问父类实例的私有成员变量或成员方法。在 Main 类中,可以通过子类对象访问父类的 protected 成员变量和成员方法。

5.2 Object:所有类的超类

Object类是Java中的一个基础类,所有其他类都是Object类的子类。它定义了所有类都具有的基本行为和属性,例如:

  • 每个对象都具有一个toString()方法,可以将对象转换为字符串表示形式。
  • 每个对象都具有一个equals(Object obj)方法,可以比较对象是否相等。
  • 每个对象都具有一个hashCode()方法,可以生成对象的哈希码,用于比较对象的唯一性。
  • 每个对象都具有一个getClass()方法,可以获取对象的类。

由于所有类都是Object类的子类,因此它们都继承了这些基本属性和方法。此外,Object类还提供了一些其他方法,例如wait()和notify()等方法,用于多线程编程。

5.2.1 Object 类型的变量

在Java中,所有类都是Object类的子类,因此可以使用Object类型的变量来引用任何Java对象。例如:

Object obj1 = new String("Hello"); // obj1引用一个String对象
Object obj2 = new Integer(42); // obj2引用一个Integer对象

由于Object类是所有类的父类,所以通过使用Object类型的变量,可以在编写通用代码时更加灵活。例如,可以编写一个方法,接受任何类型的对象作为参数:

public void doSomething(Object obj) {
    // 使用obj引用的对象执行某些操作
}

在调用这个方法时,可以传递任何类型的对象作为参数:

doSomething(new String("Hello")); // 传递一个String对象
doSomething(new Integer(42)); // 传递一个Integer对象

在方法内部,可以使用obj变量引用传递的对象,并执行特定的操作,例如调用对象的方法、获取对象的属性等等。但需要注意的是,在使用Object类型的变量时,需要进行类型转换才能调用特定类型的方法或属性。例如:

public void doSomething(Object obj) {
    if (obj instanceof String) {
        String str = (String) obj;
        System.out.println(str.toUpperCase());
    } else if (obj instanceof Integer) {
        Integer num = (Integer) obj;
        System.out.println(num.intValue());
    }
}

在上面的例子中,根据obj引用的对象的类型,使用不同的类型转换将obj变量转换为特定的类型,然后调用特定类型的方法或属性。

5.2.2 equals 方法

在Java中,Object类中的equals方法用于比较两个对象是否相等。默认情况下,equals方法实现的是引用比较,即比较两个对象是否是同一个对象的引用。如果要对类的实例属性进行比较,则需要在子类中重写equals方法。

重写equals方法要遵循以下规则:

  • 自反性:对于任何非null的引用值x,x.equals(x)应该返回true。
  • 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)也应该返回true。
  • 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也应该返回true。
  • 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)总是一致地返回true或false。
  • 非空性:对于任何非null的引用值x,x.equals(null)应该返回false。

下面是一个重写equals方法的例子:

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

    // 构造方法、getter和setter方法省略

    @Override
    public boolean equals(Object obj) {
        if (obj == this) { // 自反性
            return true;
        }
        if (!(obj instanceof Person)) { // 类型检查
            return false;
        }
        Person other = (Person) obj;
        return Objects.equals(name, other.name) && age == other.age; // 比较实例属性值
    }
}

在上面的例子中,重写了Person类的equals方法,比较了实例属性name和age的值。其中,使用了Objects.equals方法来比较两个对象是否相等,这个方法会处理null对象的情况,避免出现空指针异常。同时,还实现了equals方法的其他规则。

5.2.3 相等测试与继承

在Java中,当一个类需要进行相等测试时,通常需要重写equals方法。但是,需要注意的是,如果这个类是继承自另一个类,那么它的equals方法需要继承自父类的equals方法,同时也要根据子类自己的需求进行实现。

一般来说,子类需要重写父类的equals方法,有以下两种情况:

  1. 父类的equals方法不满足子类的相等测试需求。
  2. 父类的equals方法满足子类的相等测试需求,但子类的实例属性有新增或修改。

对于第一种情况,子类需要根据自己的需求重写equals方法。如果父类的equals方法满足子类的相等测试需求,那么子类可以不重写equals方法。但需要注意,如果子类新增了实例属性,那么子类就需要实现自己的equals方法,将新增的实例属性值也纳入相等测试的范围内。

例如,可以考虑一个Animal类和它的子类Dog类:

public class Animal {
    private String name;
    private int age;
    // 构造方法、getter和setter方法省略
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof Animal)) {
            return false;
        }
        Animal other = (Animal) obj;
        return Objects.equals(name, other.name) && age == other.age;
    }
}

public class Dog extends Animal {
    private String breed;
    // 构造方法、getter和setter方法省略
    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) { // 继承自父类的相等测试
            return false;
        }
        if (!(obj instanceof Dog)) { // 类型检查
            return false;
        }
        Dog other = (Dog) obj;
        return Objects.equals(breed, other.breed);
    }
}

在上面的例子中,Dog类继承自Animal类,并新增了一个实例属性breed,需要进行相等测试。为了符合equals方法的规则,需要先调用父类的equals方法,再比较新增的实例属性值breed。

5.2.4 hashCode方法

在Java中,hashCode方法是Object类中的一个方法,它返回一个对象的哈希值。哈希值是一个int类型的整数,它可以用于快速比较对象的值,以及实现一些数据结构,如哈希表和哈希集合。

在Java中,equals方法和hashCode方法是相互关联的。如果两个对象equals方法返回true,则它们的hashCode方法返回值必须相等;反之,如果两个对象的hashCode方法返回值相等,则它们的equals方法并不一定返回true,因为不同对象的hashCode可能会有冲突。

因此,当我们重写一个类的equals方法时,通常也需要同时重写hashCode方法,以保证equals方法与hashCode方法的一致性。一般来说,重写hashCode方法需要满足以下规则:

  1. 如果两个对象通过equals方法比较相等,则它们的hashCode方法返回值必须相等;
  2. 如果两个对象通过equals方法比较不相等,则它们的hashCode方法返回值不一定不相等(但是,为了提高哈希算法的效率,可以尽可能使不相等的对象的hashCode方法返回值不同);
  3. 如果一个类的equals方法被重写,则它的hashCode方法也必须被重写;
  4. 如果一个类的hashCode方法被重写,则它的equals方法也必须被重写。

需要注意的是,在重写hashCode方法时,我们需要使用相同的属性来计算哈希值,以保证equals方法与hashCode方法的一致性。同时,为了避免哈希冲突,最好使用每个属性的哈希码的异或操作,再结合一个质数作为系数来计算哈希值,例如:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + Objects.hashCode(name);
    result = prime * result + age;
    result = prime * result + Objects.hashCode(breed);
    return result;
}

在上面的例子中,我们使用了名称、年龄和品种这三个属性来计算哈希值,其中31是一个质数,它可以减少哈希冲突的概率。

在Java中,hashCode方法通常用于和equals方法一起实现自定义的对象比较和哈希集合/哈希表的存储。下面是一些常见的使用方法:

  1. 重写hashCode方法:如果你的自定义类需要实现自己的比较功能,那么你就需要重写hashCode方法来保证equals方法和hashCode方法的一致性。在重写hashCode方法时,你需要根据你类中的属性值计算哈希码。通常情况下,你可以选择使用Objects类的hash方法来处理这些属性值。例如:

    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }
    
  2. 存储对象:在使用哈希集合或哈希表存储对象时,系统会根据对象的哈希值来计算对象的存储位置。因此,你需要确保你的自定义类的hashCode方法返回值是唯一的。如果你的自定义类没有实现自己的hashCode方法,那么系统会默认使用对象的地址作为哈希值,这可能不会满足你的需求。

  3. 比较对象:当你使用自定义类进行比较时,equals方法并不能确保对象的唯一性。因此,你需要在equals方法中同时比较对象的哈希值以确保对象的一致性。例如:

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        return Objects.equals(name, other.name) && age == other.age && Objects.equals(sex, other.sex);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }
    

    在上述代码中,我们在equals方法中使用Objects.equals方法比较对象的所有属性。对于hashCode方法,我们使用Objects.hash方法来计算哈希值。由于我们已经在equals方法中考虑了所有属性,因此,如果两个对象的equals方法返回true,那么它们的hashCode方法也必须返回相同的值。

5.2.5 toString方法

在Java中,toString方法是Object类中定义的一个方法,它返回一个字符串,表示该对象的文本描述。默认情况下,Object类的toString方法将返回对象的类名、该对象在内存中的地址等信息,这些信息对于我们了解对象并没有太大的帮助。因此,通常我们需要重写toString方法,以便于我们自己定义需要输出的信息。

以下是toString方法的使用方法:

  1. 重写toString方法:在自定义类中,我们可以重写toString方法,以便于输出有意义的信息。例如:

    public class Person {
        private String name;
        private int age;
        
        // 构造函数、getter和setter方法省略
    
        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + "]";
        }
    }
    

    在上述代码中,我们重写了Person类的toString方法,将对象的name和age属性输出到一个字符串中,以方便我们查看该对象的信息。

  2. 调用toString方法:当我们需要查看一个对象的信息时,可以通过调用对象的toString方法来获得对象的文本描述。例如:

    public static void main(String[] args) {
        Person p = new Person("Tom", 18);
        System.out.println(p.toString());
    }
    

    在上述代码中,我们创建了一个Person对象,并通过调用该对象的toString方法,将该对象的信息输出到控制台上。

需要注意的是,在Java中,当我们将一个对象放入字符串中时,该对象的toString方法会自动调用。例如:

Person p = new Person("Tom", 18);
String str = "Hello, " + p;

在上述代码中,当我们将p对象放入字符串中时,该对象的toString方法会自动调用,以方便于我们输出有意义的信息。

5.3泛型数组列表

泛型数组列表在Java中被称为ArrayList,它是一个可变的数组实现,用于存储一个列表的元素。ArrayList具有以下特点:

  1. 该列表可以存储任何类型的元素,包括自定义类和基本数据类型。

  2. 该列表大小可以动态调整,可以根据需要动态增加或减少元素。

  3. 具有快速检索元素和删除元素的能力。

  4. 由于它是基于数组的实现,因此访问元素的时间是常数时间。

以下是一个泛型ArrayList的示例,用于存储字符串类型的元素:

import java.util.ArrayList;

public class GenericArrayListExample {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("hello");
        list.add("world");
        list.add("!");

        for (String s : list) {
            System.out.println(s);
        }
    }
}

在这个示例中,我们创建了一个ArrayList对象,并将三个字符串元素添加到列表中。然后,我们使用for-each循环遍历列表,并打印每个元素。该程序的输出如下:

hello
world
!

5.3.1 声明数组列表

在Java中,声明和创建数组列表的语法如下:

ArrayList<数据类型> 变量名 = new ArrayList<数据类型>();

其中,数据类型指定了数组列表中要存储的元素类型,变量名是数组列表的名称。例如,以下示例声明了一个字符串类型的数组列表:

ArrayList<String> myList = new ArrayList<String>();

这将创建一个空的字符串类型数组列表,可以使用add()方法向其中添加元素。同样地,我们可以声明其他类型的数组列表,如以下示例所示:

ArrayList<Integer> myList = new ArrayList<Integer>();
ArrayList<Double> myList = new ArrayList<Double>();
ArrayList<Character> myList = new ArrayList<Character>();

需要注意的是,数组列表使用了Java的泛型语法,使用尖括号<>指定了数组列表中要存储的元素类型。在声明时,我们需要根据实际需求选择相应的数据类型。

另外可以用var配合定义ArrayList。

var staff= new ArrayList<Employee>();

在Java编程语言中,"var"不是关键字。从Java 10版本开始,引入了局部变量类型推断的功能,可以用"var"来代替变量类型。使用"var"声明变量时,编译器会根据初始化表达式的类型自动推断变量类型,从而简化了变量声明的过程。

例如,可以使用"var"声明一个整数变量并初始化它:

var myNum = 10;

在编译时,编译器会自动推断出"myNum"的类型为"int",因为它被初始化为10。

需要注意的是,"var"只能用于局部变量的声明,不能用于成员变量、方法参数和返回类型的声明。此外,推断出的变量类型一旦确定,就不能被修改。因此,使用"var"时需要确保初始化表达式的类型可以被正确推断。

5.3.2 访问数组列表元素

访问数组列表元素的常用方式如下:

  1. 使用get()方法:使用这个方法可以通过元素的索引来获取该位置上的元素值。
ArrayList<String> myList = new ArrayList<String>();
myList.add("Apple");
myList.add("Banana");
myList.add("Orange");

String secondElement = myList.get(1);
  1. 使用循环:可以使用for循环或者foreach循环来遍历数组列表的所有元素。
ArrayList<String> myList = new ArrayList<String>();
myList.add("Apple");
myList.add("Banana");
myList.add("Orange");

// 使用for循环遍历
for (int i = 0; i < myList.size(); i++) {
    System.out.println(myList.get(i));
}

// 使用foreach循环遍历
for (String fruit : myList) {
    System.out.println(fruit);
}

输出结果如下:

Apple
Banana
Orange
Apple
Banana
Orange
  1. 使用迭代器:迭代器是一种可以遍历数组列表的对象,可以使用iterator()方法来获取迭代器对象。
ArrayList<String> myList = new ArrayList<String>();
myList.add("Apple");
myList.add("Banana");
myList.add("Orange");

// 使用迭代器遍历
Iterator<String> iterator = myList.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

输出结果和上面的循环遍历相同:

Apple
Banana
Orange

5.3.3 类型化与初始数组列表的兼容性

Java中的泛型是类型化的,可以使用泛型来限定数组列表中的元素类型。在初始化数组列表时,可以指定元素类型,这样数组列表中只能添加符合该类型的元素。

例如,下面的代码创建了一个元素类型为String的数组列表:

ArrayList<String> myList = new ArrayList<String>();

在这个数组列表中,只能添加字符串类型的元素。如果试图向其中添加其他类型的元素,编译器就会发生错误。

// 向数组列表中添加字符串类型的元素
myList.add("Apple");
myList.add("Orange");

// 编译错误:类型错误,不能向数组列表中添加整数类型的元素
myList.add(10);

当然,如果不想指定元素类型,也可以使用通配符?来表示任意类型,例如:

ArrayList<?> myList = new ArrayList<?>();

这样就可以向数组列表中添加任意类型的元素,但在使用时需要注意类型转换和类型安全。

5.4 对象包装器与自动装箱

Java中的对象包装器是指将基本数据类型转换为对应的对象类型。例如,将int类型转换为Integer类型,将double类型转换为Double类型等等。

自动装箱是指将基本数据类型自动转换为对应的对象类型。例如,在需要使用Integer类型的地方,直接将int型变量赋值给Integer类型变量,编译器会自动将int类型转换为Integer类型,这个过程就是自动装箱。

自动装箱可以方便地进行基本数据类型与对象类型的转换,避免了手动转换的繁琐过程。但是,Java中的对象包装器在一些情况下会带来一定的性能损失,因为对象包装器需要占用额外的内存空间。因此,在大量处理基本数据类型的场景中,建议使用基本数据类型而不是对象包装器。

对象包装器和自动装箱是Java中常用的重要特性,可以将基本数据类型转换为对象类型,以便更方便地处理它们。以下是对象包装器和自动装箱的代码示例:

  1. 自动装箱示例
int num = 10;
Integer numObj = num; // 自动装箱成Integer对象
System.out.println(numObj); // 输出:10
  1. 手动装箱示例
int num = 10;
Integer numObj = Integer.valueOf(num); // 手动装箱成Integer对象
System.out.println(numObj); // 输出:10
  1. 自动拆箱示例
Integer numObj = 10;
int num = numObj; // 自动拆箱成int类型
System.out.println(num); // 输出:10
  1. 手动拆箱示例
Integer numObj = 10;
int num = numObj.intValue(); // 手动拆箱成int类型
System.out.println(num); // 输出:10
  1. 对象包装器的常用方法
Integer numObj = 10;
System.out.println(numObj.toString()); // 输出:"10"
System.out.println(numObj.equals(10)); // 输出:true
System.out.println(numObj.compareTo(5)); // 输出:1,即大于5
System.out.println(numObj.MAX_VALUE); // 输出:2147483647
System.out.println(numObj.MIN_VALUE); // 输出:-2147483648
  1. 对象包装器的常用静态方法
System.out.println(Integer.parseInt("123")); // 输出:123,将字符串转换为int
System.out.println(Integer.valueOf("123")); // 输出:123,将字符串转换为Integer对象
System.out.println(Integer.toHexString(16)); // 输出:"10",将int转换为16进制字符串
System.out.println(Integer.toBinaryString(16)); // 输出:"10000",将int转换为2进制字符串

这些示例展示了Java中对象包装器和自动装箱的基本用法和常用方法,使用它们可以简化代码并提高效率。

5.5 参数数量可变的方法

Java参数数量可变的方法是指使用不定数量的参数来定义方法。这种方法的定义方式为在参数类型后面加上省略号(…),表示可以接受任意数量的该类型参数。在方法内部,不定数量的参数会被自动转换为数组。

以下是Java参数数量可变的方法的示例:

public static int add(int... nums) {
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    return sum;
}

这个方法接受一个可变数量的int类型参数,可以接受任意数量的int类型参数。例如:

int result = add(1, 2, 3, 4);
System.out.println(result); // 输出:10

也可以不传递参数,此时nums数组的长度为0。

int result = add();
System.out.println(result); // 输出:0

需要注意的是,在同一个方法中只能有一个可变数量的参数,并且必须是最后一个参数。否则编译器会报错。

5.6枚举类

Java中枚举类是一种特殊的类,用于将固定的常量集合起来。它是一种比常量更好的方式来表示一组常量,在代码中更加易读、更加健壮、更加优雅。枚举类在Java 1.5版本中被引入,它是一个类,因此具有类的特性,如继承、实现接口、可以定义构造函数、成员变量和成员方法等。

枚举类的定义语法如下:

enum Season {
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER
}

上面的代码定义了一个季节的枚举类,包含了四个常量:SPRING、SUMMER、AUTUMN、WINTER。

枚举类的常用方法:

  1. name():返回枚举常量的名称。
  2. ordinal():返回枚举常量的序数(从0开始)。
  3. values():返回包含枚举类中所有常量的数组。
public enum Season {
    SPRING("春天"), SUMMER("夏天"), AUTUMN("秋天"), WINTER("冬天");

    private String desc;

    Season(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }
}

上面的代码定义了一个季节的枚举类,每个季节常量都有一个对应的描述信息。这个类中还有一个getDesc()方法,用于获取枚举常量的描述信息。

以下是enum类的API表。

API描述
values()返回枚举类中所有的枚举常量,以数组的形式返回
valueOf(String)根据枚举常量名返回枚举常量
name()返回枚举常量的名称
ordinal()返回枚举常量在枚举类中的序号,序号从0开始
compareTo(E o)比较枚举常量在枚举类中的次序,返回比较结果
equals(Object)比较枚举常量是否相同,返回布尔值
getDeclaringClass()返回枚举常量所在的枚举类
hashCode()返回枚举常量的哈希值
toString()返回枚举常量的字符串表示
getEnumConstants()返回枚举类中所有枚举常量的数组
name(String)根据枚举常量名返回对应的枚举常量,如果没有则抛出IllegalArgumentException异常
valuesOf(Class, String)根据指定枚举类型和枚举常量名返回对应的枚举常量,如果没有则抛出IllegalArgumentException异常
valuesOf(Class)返回指定枚举类型的所有枚举常量

5.7 反射

Java中的反射是指通过程序运行时获取一个对象的类和成员信息,并可以操作对象的成员、方法和构造函数等。以下是Java中反射的一些常见用法和相关API:

  1. 获取对象的Class信息:

获取一个对象的Class对象,可以使用对象的getClass()方法,也可以使用Class.forName()方法(需要指定类的全限定名),例如:

String str = "Hello, world!";
Class<?> clazz1 = str.getClass();  // 使用对象的getClass()方法
Class<?> clazz2 = Class.forName("java.lang.String");  // 使用Class.forName()方法
  1. 获取类的构造函数信息:

通过Class对象可以获取构造函数信息,使用Class的getConstructors()方法可以获取所有public的构造函数,使用getDeclaredConstructors()方法可以获取所有构造函数,例如:

Class<?> clazz = String.class; // 获取String类的Class对象
Constructor<?>[] constructors = clazz.getDeclaredConstructors(); // 获取String类的所有构造函数
  1. 获取类的成员变量信息:

通过Class对象可以获取成员变量的信息,使用Class的getFields()方法可以获取public的成员变量,使用getDeclaredFields()方法可以获取所有成员变量,例如:

Class<?> clazz = String.class; // 获取String类的Class对象
Field[] fields = clazz.getDeclaredFields(); // 获取String类的所有成员变量
  1. 获取类的方法信息:

通过Class对象可以获取方法的信息,使用Class的getMethods()方法可以获取public的方法,使用getDeclaredMethods()方法可以获取所有方法,例如:

Class<?> clazz = String.class; // 获取String类的Class对象
Method[] methods = clazz.getMethods(); // 获取String类的所有public方法

除此之外,还有很多其他的反射API,例如操作对象的成员变量、方法和构造函数等。反射提供了一种灵活的方式来操作对象,但也带来了很多安全问题和性能问题,在使用反射时需要慎重考虑。

5.7.1 Class类

Class类作为Java反射机制的核心,在Java SE中提供了大量的方法,可以用来获取类的信息、操作类对象等。下面是Class类中常用的方法。

  1. 获取类信息
  • getName():获取Class对象所表示的类的名称。
  • getSimpleName():获取Class对象所表示的类简称。
  • getCanonicalName():获取Class对象所表示的类的规范化名称。
  • isInterface():判断Class对象是否表示一个接口。
  • isArray():判断Class对象是否表示一个数组。
  • isPrimitive():判断Class对象是否表示一个基本类型。
  • getModifiers():获取Class对象所表示的类的修饰符。
  • getPackage():获取Class对象所表示的类的包信息。
  • getClassLoader():获取Class对象所使用的类加载器。
  1. 获取类的构造方法
  • getConstructors():获取Class对象所表示的类的所有公共构造方法。
  • getDeclaredConstructors():获取Class对象所表示的类的所有构造方法,包括私有构造方法。
  1. 获取类的字段
  • getFields():获取Class对象所表示的类的所有公共字段。
  • getDeclaredFields():获取Class对象所表示的类的所有字段,包括私有字段。
  1. 获取类的方法
  • getMethods():获取Class对象所表示的类的所有公共方法。
  • getDeclaredMethods():获取Class对象所表示的类的所有方法,包括私有方法。
  1. 获取类的注解
  • getAnnotations():获取Class对象所表示的类的所有注解。
  • getAnnotation(Class annotationClass):获取Class对象所表示的类的指定注解。
  • getDeclaredAnnotations():获取Class对象所表示的类的所有直接声明的注解。
  1. 获取类的泛型信息
  • getGenericSuperclass():获取Class对象所表示的类的直接超类的泛型信息。
  • getGenericInterfaces():获取Class对象所表示的类实现的所有接口的泛型信息。
  • getTypeParameters():获取Class对象所表示的类的类型参数信息。
  1. 操作类对象
  • newInstance():根据Class对象创建一个新的实例,要求类必须有无参构造方法。
  • isInstance(Object obj):判断给定的对象是否属于Class对象所表示的类或其子类的实例。
  • cast(Object obj):将给定的对象强制转换为Class对象所表示的类型。

以上是常用的Class类方法,还有一些不常用的方法这里就不一一列举了。

下面是Class类中所有的方法以及示例代码:

  1. 获取类信息
  • getName():
Class<?> clazz = String.class;
String name = clazz.getName();
System.out.println("Name: " + name); // 输出:Name: java.lang.String
  • getSimpleName():
Class<?> clazz = String.class;
String simpleName = clazz.getSimpleName();
System.out.println("Simple Name: " + simpleName); // 输出:Simple Name: String
  • getCanonicalName():
Class<?> clazz = String.class;
String canonicalName = clazz.getCanonicalName();
System.out.println("Canonical Name: " + canonicalName); // 输出:Canonical Name: java.lang.String
  • isInterface():
Class<?> clazz = String.class;
boolean isInterface = clazz.isInterface();
System.out.println("Is Interface: " + isInterface); // 输出:Is Interface: false
  • isArray():
Class<?> clazz = String[].class;
boolean isArray = clazz.isArray();
System.out.println("Is Array: " + isArray); // 输出:Is Array: true
  • isPrimitive():
Class<?> clazz = int.class;
boolean isPrimitive = clazz.isPrimitive();
System.out.println("Is Primitive: " + isPrimitive); // 输出:Is Primitive: true
  • getModifiers():
Class<?> clazz = String.class;
int modifiers = clazz.getModifiers();
System.out.println("Modifiers: " + modifiers); // 输出:Modifiers: 17

其中,17表示public、final类。

  • getPackage():
Class<?> clazz = String.class;
Package packageInfo = clazz.getPackage();
System.out.println("Package: " + packageInfo); // 输出:Package: package java.lang, Java Platform API Specification, version 1.8
  • getClassLoader():
Class<?> clazz = String.class;
ClassLoader classLoader = clazz.getClassLoader();
System.out.println("Class Loader: " + classLoader); // 输出:Class Loader: jdk.internal.loader.ClassLoaders$AppClassLoader@4b67cf4d
  1. 获取类的构造方法
  • getConstructors():
Class<?> clazz = String.class;
Constructor<?>[] constructors = clazz.getConstructors();
for (Constructor<?> constructor : constructors) {
    System.out.println("Constructor: " + constructor);
}
  • getDeclaredConstructors():
Class<?> clazz = String.class;
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : declaredConstructors) {
    System.out.println("Declared Constructor: " + constructor);
}
  1. 获取类的字段
  • getFields():
Class<?> clazz = String.class;
Field[] fields = clazz.getFields();
for (Field field : fields) {
    System.out.println("Field: " + field);
}
  • getDeclaredFields():
Class<?> clazz = String.class;
Field[] declaredFields = clazz.getDeclaredFields();
for (Field field : declaredFields) {
    System.out.println("Declared Field: " + field);
}
  1. 获取类的方法
  • getMethods():
Class<?> clazz = String.class;
Method[] methods = clazz.getMethods();
for (Method method : methods) {
    System.out.println("Method: " + method);
}
  • getDeclaredMethods():
Class<?> clazz = String.class;
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method method : declaredMethods) {
    System.out.println("Declared Method: " + method);
}
  1. 获取类的注解
  • getAnnotations():
Class<?> clazz = String.class;
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
    System.out.println("Annotation: " + annotation);
}
  • getAnnotation(Class annotationClass):
Class<?> clazz = String.class;
Deprecated deprecated = clazz.getAnnotation(Deprecated.class);
System.out.println("Deprecated: " + deprecated);
  • getDeclaredAnnotations():
Class<?> clazz = String.class;
Annotation[] declaredAnnotations = clazz.getDeclaredAnnotations();
for (Annotation annotation : declaredAnnotations) {
    System.out.println("Declared Annotation: " + annotation);
}
  1. 获取类的泛型信息
  • getGenericSuperclass():
Class<?> clazz = String.class;
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println("Generic Superclass: " + genericSuperclass);
  • getGenericInterfaces():
Class<?> clazz = String.class;
Type[] genericInterfaces = clazz.getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
    System.out.println("Generic Interface: " + genericInterface);
}
  • getTypeParameters():
class MyClass<T, E> {
}

Class<?> clazz = MyClass.class;
TypeVariable<?>[] typeParameters = clazz.getTypeParameters();
for (TypeVariable<?> typeParameter : typeParameters) {
    System.out.println("Type Parameter: " + typeParameter);
}
  1. 操作类对象
  • newInstance():
Class<?> clazz = String.class;
String str = clazz.newInstance();
System.out.println("New Instance: " + str);
  • isInstance(Object obj):
Class<?> clazz = String.class;
boolean isInstance = clazz.isInstance("Hello, World!");
System.out.println("Is Instance: " + isInstance);
  • cast(Object obj):
Class<?> clazz = String.class;
Object obj = "Hello, World!";
String str = clazz.cast(obj);
System.out.println("Cast: " + str);

5.7.2 声明异常入门

在 Java 中,异常是指程序在运行时可能出现的错误或异常情况。对于这些异常情况,我们需要对它们进行处理或者传递给调用者。通常情况下,Java 中的异常可以分为两类:

  1. 检查型异常(Checked Exception)

这种异常在编译时就会被检查出来,如果不进行处理就无法通过编译。比如,IOException、SQLException 等。

  1. 非检查型异常(Unchecked Exception)

这种异常是在运行时才会出现,不需要在编译时进行捕捉。比如,NullPointerException、ArrayIndexOutOfBoundsException 等。

在 Java 中,我们可以通过 throws 关键字来声明异常。语法格式如下:

public void method() throws Exception {
    // 方法体
}

上面的代码表示,该方法可能会抛出 Exception 异常。

如果一个方法可能会抛出多个异常,可以使用逗号分隔的方式声明,如下所示:

public void method() throws Exception1, Exception2 {
    // 方法体
}

在方法内部,如果发生了异常情况,可以使用 throw 关键字抛出异常,如下所示:

public void method() throws Exception {
    if (someCondition) {
        throw new Exception();
    }
}

在实际开发中,我们需要根据具体的业务场景来选择抛出哪些异常。需要注意的是,如果一个方法声明了某种异常,在该方法的调用者中,必须进行对应的异常捕获或者再次抛出该异常。否则,编译器就会报错。

简单来说,异常的处理方式有两种:

  1. try-catch-finally 块
try {
    // 可能会抛出异常的代码块
} catch (Exception e) {
    // 异常处理逻辑
} finally {
    // finally 块中的代码总是会被执行
}

try 块中,需要编写可能会抛出异常的代码。如果在执行期间发生了异常,程序会跳转到 catch 块中,执行对应的异常处理逻辑,并且继续执行 finally 块中的代码。

  1. throws 关键字

在方法声明时使用 throws 关键字声明该方法可能抛出的异常:

public void method() throws Exception {
    // 方法体
}

这种方式就是将异常的处理交给方法的调用者去处理。如果调用者不处理,那么异常就会一直往上抛,直到 JVM 捕获该异常并终止程序的执行。

总之,异常处理是 Java 程序设计中非常重要的一部分,它可以让我们更好地掌控程序的运行流程,提高程序的健壮性和可靠性。

5.7.3 资源

Java 中的资源文件可以包括各种类型的数据,比如文本、图像、音频、配置文件等等,这些文件通常以特定格式存储,比如文本文件以 .txt、配置文件以 .properties、图像以 .jpg/.png 等扩展名来保存。以下是几种 Java 中常见的资源文件类型及其用途:

  • .properties 文件:通常用于存储程序配置数据,比如数据库连接、服务器地址等等。
  • .xml 文件:常用于存储程序配置信息、数据传输等。
  • .txt 文件:常用于存储文本数据,比如用户日志、数据导入导出等。
  • .jpg、.png、.gif 等图像文件:用于存储图像数据。
  • .wav、.mp3 等音频文件:常用于存储音频数据。

Java 中访问资源文件主要有以下方式:

  • 相对路径:使用当前项目的相对路径定位资源文件。
  • 绝对路径:使用系统的绝对路径定位资源文件。
  • 类路径:使用类加载器加载资源文件,可以在 jar 包中获取资源文件。

在 Java 中可以通过以下代码访问资源文件:

// 使用相对路径获取资源文件
File file = new File("resources/text.txt");

// 使用类路径获取资源文件
InputStream inputStream = getClass().getResourceAsStream("/resources/text.txt");

// 读取资源文件内容
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}

需要注意的是,在读取资源文件时需要进行异常处理,比如文件不存在等异常情况。

5.7.4 利用反射分析类的能力

利用反射分析类的能力,可以在程序运行时动态地获取类信息,并进行相关操作。常见的利用反射分析类的能力的场景有:

  1. 获取一个类的所有成员变量和方法,并进行相应的操作。比如可以获取一个类的所有方法名,然后根据方法名执行相应的方法。

  2. 动态创建对象。利用反射可以在程序运行时动态地创建一个对象。

  3. 修改类中的私有成员变量。利用反射可以获取类中的私有成员变量,然后进行修改。

  4. 获取注解信息。利用反射可以获取类或方法上的注解信息,并根据注解信息进行相应的操作。

总的来说,利用反射分析类的能力可以让我们在程序运行时动态地获取类信息,并进行相应的操作,从而增加程序的灵活性和可扩展性。

5.7.5 使用反射在运行时分析对象

在Java中,可以使用反射机制在运行时分析对象的属性和方法。具体步骤如下:

  1. 获取对象的Class对象

使用对象的getClass()方法获取该对象的Class对象,例如:

Object obj = new MyClass();
Class clazz = obj.getClass();
  1. 获取对象的属性信息

通过Class对象的getField()方法或getDeclaredField()方法获取对象的属性信息,例如:

Field field = clazz.getDeclaredField("fieldName");

其中getDeclaredField()方法可以获取所有类型的属性,而getField()方法只能获取公有的属性。

  1. 获取对象的方法信息

通过Class对象的getMethod()方法或getDeclaredMethod()方法获取对象的方法信息,例如:

Method method = clazz.getDeclaredMethod("methodName");

其中getDeclaredMethod()方法可以获取对象的所有方法,而getMethod()方法只能获取公有的方法。

  1. 调用对象的方法

使用Method对象的invoke()方法调用对象的方法,例如:

method.invoke(obj, arg1, arg2, ...);

其中第一个参数是要调用方法的对象,后面的参数是方法的参数列表。

通过以上步骤,可以利用反射在运行时分析对象的属性和方法,并且可以调用对象的方法。需要注意的是,反射的使用可能会影响程序的性能,因此只有在必要的情况下才应该使用反射。

5.7.6 使用反射编写泛型反射数组代码

下面是使用反射编写泛型反射数组的代码:

import java.lang.reflect.Array;

public class GenericArray<T> {
    private T[] array;

    public GenericArray(Class<T> type, int size) {
        array = (T[]) Array.newInstance(type, size);
    }

    public void set(int index, T value) {
        array[index] = value;
    }

    public T get(int index) {
        return array[index];
    }

    public T[] getArray() {
        return array;
    }

    public static void main(String[] args) {
        GenericArray<String> genericArray = new GenericArray<>(String.class, 5);
        genericArray.set(0, "Hello");
        genericArray.set(1, "World");
        String[] array = genericArray.getArray();
        for (String s : array) {
            System.out.println(s);
        }
    }
}

在这个例子中,我们定义了一个泛型类GenericArray,该类可以创建一个指定类型和大小的数组。在构造函数中,我们使用反射创建了一个指定类型和大小的数组。在set()方法中,我们通过反射设置数组元素的值,并在get()方法中通过反射获取数组元素的值。在getArray()方法中,我们返回了该数组。

在main()方法中,我们创建了一个指定类型为String,大小为5的GenericArray对象,并在数组中设置了两个元素。然后我们通过getArray()方法获取了该数组,并使用for-each循环打印出了数组中的所有元素。

注意,由于Java中的泛型擦除机制,我们无法直接创建一个泛型数组。因此,我们使用了反射来创建一个指定类型的数组。

5.7.7调用任意方法和构造器

在Java中,可以使用反射机制调用任意方法和构造器。下面是一些示例代码:

  1. 调用无参构造器

使用Class类的newInstance()方法可以调用一个类的无参构造器。示例代码如下:

Class<MyClass> clazz = MyClass.class;
MyClass obj = clazz.newInstance();
  1. 调用有参构造器

首先,我们需要通过Class类的getDeclaredConstructor()方法获取到指定方法的Constructor对象,然后使用Constructor类的newInstance()方法调用该构造器。示例代码如下:

Class<?> clazz = MyClass.class;
Constructor<?> constructor = clazz.getDeclaredConstructor(int.class, String.class);
MyClass obj = (MyClass)constructor.newInstance(42, "Hello World");

在以上示例中,我们调用了一个有两个参数的构造器,并传入适当的参数。

  1. 调用任意方法

首先,我们需要通过Class类的getMethod()或getDeclaredMethod()方法获取到指定方法的Method对象,然后使用Method类的invoke()方法调用该方法。示例代码如下:

Class<?> clazz = MyClass.class;
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("myMethod", int.class, String.class);
Object result = method.invoke(obj, 42, "Hello World");

在以上示例中,我们调用了一个名称为myMethod的方法,并传入适当的参数。请注意,invoke()方法返回的是Object类型,需要进行强制类型转换。

需要注意的是,使用反射机制调用方法和构造器可能会导致性能下降,因此应该谨慎使用。

5.8 继承的设计技巧

  1. 使用继承实现代码复用

例如,我们可以定义一个基类Animal,其中包含一些所有动物都有的属性和方法,例如名称、年龄、颜色、移动方式等。然后我们可以定义一些子类,例如Dog、Cat、Bird等,它们继承了Animal类的属性和方法,并且可以根据需要重写一些方法以实现自己的特性。

public class Animal {
    private String name;
    private int age;
    private String color;

    public Animal(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }

    public void move() {
        System.out.println("I am moving.");
    }
}

public class Dog extends Animal {
    public Dog(String name, int age, String color) {
        super(name, age, color);
    }

    public void bark() {
        System.out.println("Woof!");
    }

    @Override
    public void move() {
        System.out.println("I am walking.");
    }
}

public class Cat extends Animal {
    public Cat(String name, int age, String color) {
        super(name, age, color);
    }

    public void meow() {
        System.out.println("Meow!");
    }

    @Override
    public void move() {
        System.out.println("I am running.");
    }
}
  1. 使用多态性增强代码的灵活性

例如,我们可以定义一个函数来处理Animal类型的参数,但是实际传入的参数可以是Dog、Cat等子类的实例对象。这样可以根据具体参数的类型来选择调用哪个子类的方法,从而实现更灵活的代码。

public void processAnimal(Animal animal) {
    animal.move();
}
  1. 使用抽象类和接口来定义规范

例如,我们可以定义一个接口IShape来表示一些形状的规范,例如矩形、圆形等。每个具体的形状都可以实现这个接口,并且可以根据自己的特性来实现相应的方法。

public interface IShape {
    double getArea();
    double getPerimeter();
}

public class Rectangle implements IShape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }

    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
}

public class Circle implements IShape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}
  1. 不要过度使用继承

例如,我们可以考虑使用组合来代替继承,可以将一个类的实例作为另一个类的属性,从而达到代码复用的目的。

public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        // do some driving
        engine.stop();
    }
}

public interface Engine {
    void start();
    void stop();
}

public class GasolineEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Gasoline engine starts.");
    }

    @Override
    public void stop() {
        System.out.println("Gasoline engine stops.");
    }
}

public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Electric engine starts.");
    }

    @Override
    public void stop() {
        System.out.println("Electric engine stops.");
    }
}
  1. 使用组合代替继承

例如,我们可以定义一个类来表示一个人,这个类包含一些基本的属性和方法,例如姓名、年龄、性别等。然后我们可以定义一些其他的类,例如Student、Teacher等,他们包含了Person类的实例作为属性,并且可以根据需要实现自己的特定属性和方法。

public class Person {
    private String name;
    private int age;
    private String gender;

    public Person(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public void work() {
        System.out.println("I am working.");
    }
}

public class Student {
    private Person person;
    private String school;

    public Student(Person person, String school) {
        this.person = person;
        this.school = school;
    }

    public void study() {
        System.out.println("I am studying at " + school);
    }
}

public class Teacher {
    private Person person;
    private String subject;

    public Teacher(Person person, String subject) {
        this.person = person;
        this.subject = subject;
    }

    public void teach() {
        System.out.println("I am teaching " + subject);
    }
}

第六章 接口、lambda表达式和内部类

Java接口、lambda表达式和内部类都是Java语言特性。它们都可以用来实现代码重用和库开发,但是它们的实现方式有所不同。

Java接口是一组抽象方法的集合,可以被其他类实现。它定义了类的行为,在Java中非常重要。接口允许Java类之间的通信,使得不同类的实现更加灵活。

Java 8 引入了lambda表达式,它是一个匿名函数,可以作为参数传递给其他方法或函数。lambda表达式可以使Java代码更加简洁和易读。它可以用来简化代码,例如在集合和数组等Java数据类型中进行过滤、映射和聚合等操作。

内部类是一个类内部定义的类。它可以在其他类中使用,但是只能访问定义它的类的成员变量和方法。内部类可以用来实现一些特定的功能,例如实现接口、继承抽象类或者覆盖父类的方法等。内部类也可以用来实现Java程序中的回调机制。

总的来说,Java接口、lambda表达式和内部类都是Java语言中实现代码重用和库开发的重要特性。在Java开发中的选择取决于特定的场景和需求。

6.1接口

接口是Java语言中的一种抽象类型,它用于定义一组抽象方法。

6.1.1接口的概念

接口是Java语言中的一种抽象类型,它用于定义一组抽象方法。接口中的方法只有声明,而没有实现,因此接口本身无法被实例化。其他类可以实现该接口并提供实现方法,这就使得这些类具有了相同的行为,从而使得这些类可以互相替代。这种实现接口的方式也称为“接口继承”。

Java中的接口通过关键字“interface”进行声明,具体的语法格式如下:

interface InterfaceName {
    // 方法声明
}

其中,InterfaceName是接口的名称,接口中的方法声明可以是没有方法体的抽象方法,也可以是有方法体的默认方法。

在Java中,一个类可以实现多个接口,通过实现接口,类可以获得接口中声明的所有方法,并对这些方法进行实现。接口的实现语法格式如下:

class ClassName implements InterfaceName1, InterfaceName2, ... {
    // 类实现接口的方法
}

通过实现接口,类可以提供一种相同的行为方式,这对于实现代码的重用和库开发非常有帮助。此外,接口还可以用于实现多态性,在Java中多态性是非常重要的概念,它使得Java程序更加易于扩展和维护。

6.1.2 接口的属性

Java接口不允许定义属性(成员变量),只能定义方法。接口中定义的方法都是抽象方法,没有方法体,只有方法的声明。这是因为接口的主要作用是定义一组规范(即接口中的方法),而不是提供具体的实现。因此,接口中只能定义方法,而不能定义属性。

但是可以声明一个接口类型的变量,在实现类中赋值。

例如:

interface MyInterface {
    void myMethod();
}

class MyClass implements MyInterface {
    public void myMethod() {
        System.out.println("MyMethod is called.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyInterface obj = new MyClass();
        obj.myMethod();
    }
}

在上述代码中,我们声明了一个接口类型的变量obj,并将其赋值为MyClass实例。这样做的好处是,在MyClass类中实现了MyInterface接口的方法,我们可以通过obj变量来调用这个方法,而不用关心MyClass类的具体实现。这也体现了接口的多态性。

6.1.3 接口与抽象类

接口和抽象类都是Java中的抽象概念,但它们之间有很多区别。

  1. 接口中只能声明常量和抽象方法,不能包含具体的方法实现或实例变量;而抽象类中可以包含具体的方法实现和实例变量。
  2. 接口中的方法默认是public的,不能用其他访问修饰符来修饰;而抽象类中的方法可以有不同的访问修饰符。
  3. 一个类可以实现多个接口,但只能继承一个抽象类。
  4. 接口中的方法必须全部被实现,而抽象类中的抽象方法可以被子类选择性地实现。

在设计上,应该优先使用接口而不是抽象类,因为接口更灵活,可以更好地支持可重用性和组件化。如果一个类只需要继承一个抽象类或需要共享某些状态,那么抽象类可能会更有用。

以下是一个接口和抽象类的代码示例:

// 接口
public interface Animal {
    int LEGS = 4; // 接口中的常量

    void makeSound(); // 抽象方法
}

// 抽象类
public abstract class Shape {
    protected int x;
    protected int y;

    public Shape(int x, int y) {
        this.x = x;
        this.y = y;
    }

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

    public void move(int deltaX, int deltaY) {
        x += deltaX;
        y += deltaY;
    }
}

一个类实现接口时必须实现接口中的所有抽象方法,如以下代码:

public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("汪汪汪");
    }
}

一个类继承抽象类时必须实现抽象类中的所有抽象方法,如以下代码:

public class Circle extends Shape {
    private int radius;

    public Circle(int x, int y, int radius) {
        super(x, y);
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("画一个圆,圆心坐标为(" + x + ", " + y + "),半径为" + radius);
    }
}

6.1.4 静态和私有方法

Java中静态方法是指属于类而不属于对象的方法,它可以通过类名直接调用而不需要创建类的实例。私有方法则是指只有在类内部才能访问的方法,不能被类的外部或子类调用。

以下是静态方法和私有方法的代码示例:

public class MathUtils {
    public static int sum(int a, int b) { //静态方法
        return a + b;
    }

    public static int multiply(int a, int b) { //静态方法
        return a * b;
    }

    private int subtract(int a, int b) { //私有方法
        return a - b;
    }
}

静态方法可以通过类名直接调用:

int a = 10;
int b = 5;
int sum = MathUtils.sum(a, b); //调用静态方法
int product = MathUtils.multiply(a, b); //调用静态方法

私有方法只能在类内部被调用:

public class MathUtils {
    public static int sum(int a, int b) { 
        return a + b;
    }

    public static int multiply(int a, int b) { 
        return a * b;
    }

    private int subtract(int a, int b) { 
        return a - b;
    }

    public int calculate(int a, int b) { 
        int result = sum(a, b) + multiply(a, b) - subtract(a, b); //在类内部调用私有方法
        return result;
    }
}

6.1.5 默认方法

Java 8引入了一种新的方法——默认方法(Default Method),也称为接口默认方法。默认方法是在接口中实现的方法,可以在不破坏现有接口实现的基础上添加新的方法,从而更方便地扩展接口。

以下是默认方法的代码示例:

public interface Animal {
  void eat();

  default void move() { //默认方法
    System.out.println("The animal is moving");
  }
}

public class Dog implements Animal {
  public void eat() {
    System.out.println("The dog is eating");
  }
}

public class Main {
  public static void main(String[] args) {
    Dog dog = new Dog();
    dog.eat(); //调用Dog类中的eat方法
    dog.move(); //调用Animal接口中的默认方法
  }
}

在上面的代码中,我们定义了一个Animal接口,其中除了eat方法外,还定义了一个move默认方法。在Dog类实现Animal接口时,只需要实现eat方法,而不需要实现move方法,因为move方法已经在接口中提供了默认实现。

当我们使用Dog类创建一个实例并调用move方法时,实际上是调用了Animal接口中的默认实现。如果Dog类自己实现了move方法,则该方法将覆盖接口中的默认实现。

需要注意的是,接口默认方法只能被在Java 8及以后版本中编译的类所调用。如果您希望在早期版本的Java中使用接口默认方法,请使用抽象类或传统的实现方式。

6.1.6 解决默认方法冲突

默认方法冲突是指当一个类实现多个接口时,这些接口中具有相同名称和参数的默认方法。在这种情况下,编译器无法确定应该调用哪个默认方法,因此会产生冲突。

为了解决默认方法冲突问题,可以采用以下几种方法:

  1. 接口中不包含相同名称和参数的默认方法。这是最简单的解决方法,但并不总是可行。

  2. 在实现类中覆盖默认方法,自己实现方法体。这是最基本的解决方法,但可能会导致大量代码重复。

  3. 使用super关键字调用指定接口的默认方法,来明确调用哪个默认方法。例如:

public interface A {
    default void foo() {
        System.out.println("A.foo");
    }
}

public interface B {
    default void foo() {
        System.out.println("B.foo");
    }
}

public class C implements A, B {
    @Override
    public void foo() {
        A.super.foo(); // 明确调用A接口的默认方法
    }
}

在C类中,由于实现了A和B两个接口,因此A和B中都有一个名称和参数相同的默认方法foo。通过在C类中覆盖foo方法,并使用A.super.foo()调用A接口中的默认方法,我们可以解决默认方法的冲突问题。

需要注意的是,如果一个类实现了多个接口,而这些接口中具有相同的抽象方法,那么这个类必须实现这个抽象方法,否则会导致编译错误。

6.1.7 接口与回调

在Java中,回调(Callback)是一种常见的设计模式,它允许我们将某个对象的方法传递给另一个对象,在合适的时候被调用以便执行某些特定的任务。

接口与回调密切相关,因为回调通常需要创建一个接口,然后在该接口中定义一个或多个方法,以指定在事件发生时需要执行的代码。下面是一个简单的示例,它演示了如何使用接口和回调来实现事件处理:

// 定义一个回调接口
interface EventHandler {
   void handle();
}

// 定义一个事件源类,它可以添加和删除回调方法
class EventSource {
   private EventHandler eventHandler;
   
   public void setEventHandler(EventHandler eventHandler) {
      this.eventHandler = eventHandler;
   }
   
   public void triggerEvent() {
      // 触发事件并调用回调方法
      if (eventHandler != null) {
         eventHandler.handle();
      }
   }
}

// 定义一个事件处理器类,它实现了回调接口
class MyEventHandler implements EventHandler {
   @Override
   public void handle() {
      System.out.println("事件发生了,需要执行一些操作...");
   }
}

// 在主方法中测试回调
public class Main {
   public static void main(String[] args) {
      EventSource eventSource = new EventSource();
      MyEventHandler eventHandler = new MyEventHandler();
      eventSource.setEventHandler(eventHandler);
      eventSource.triggerEvent();
   }
}

在上面的示例中,EventSource是一个事件源类,它可以添加和删除回调方法。MyEventHandler是一个实现了回调接口的事件处理器,它包含了在事件发生时需要执行的代码。在主方法中,我们首先创建了一个事件源对象和一个事件处理器对象,然后将事件处理器对象添加到事件源对象中,最后触发事件并调用回调方法。

需要注意的是,回调通常用于异步编程中,因为它可以让我们在某个任务完成时自动调用指定的方法,而不需要显式地等待这个任务完成。此外,回调还可以让我们将代码分离为多个模块,提高代码的可复用性和可维护性。

6.1.8Comparater 接口

Comparator接口是Java中一个非常常用的接口,它定义了比较两个对象大小的方法。Comparator接口含有两个方法:compare和equals。

compare方法签名如下:

int compare(T o1, T o2);

该方法用于比较两个对象的大小。如果第一个对象比第二个对象小,则返回一个负整数;如果第一个对象比第二个对象大,则返回一个正整数;如果两个对象相等,则返回0。

equals方法签名如下:

boolean equals(Object obj);

该方法用于比较两个Comparator对象是否相等。

Comparator接口通常用于希望排序方式不同于Java自带的排序方式的集合。例如,Java自带的排序方式是按照元素自然顺序进行排序,但在某些场景下,我们希望按照其他方式(如长度、字典序等)进行排序,这时我们就可以使用Comparator接口来自定义排序方式。

以下是一个使用Comparator接口的示例代码:

import java.util.Comparator;
import java.util.Arrays;

class LengthComparator implements Comparator<String> {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

public class Main {
    public static void main(String[] args) {
        String[] arr = {"hello", "world", "java"};
        Arrays.sort(arr, new LengthComparator());
        System.out.println(Arrays.toString(arr));
    }
}

在上面的代码中,我们自定义了一个比较器LengthComparator,它实现了Comparator接口,并重写了compare方法,按字符串长度从小到大排序。然后通过Arrays.sort方法对字符串数组进行排序,传入了自定义的比较器LengthComparator,输出结果为:[java, hello, world]。

6.1.9 对象克隆

在Java中,对象克隆(Object cloning)是指创建一个新的对象,该对象的内容与另一个现有对象相同。对象克隆在Java中是通过Cloneable接口和clone()方法来实现的。

Cloneable接口是一个标记接口,它没有任何方法,只是用来标记一个类可以被克隆,即实现Cloneable接口的类可以使用clone()方法进行对象克隆。如果一个类没有实现Cloneable接口,调用其clone()方法时将会抛出CloneNotSupportedException异常。

clone()方法是Object类中的一个protected方法,它可以在子类中重新定义。该方法用于创建并返回当前对象的一份拷贝,拷贝的方法是通过复制对象的所有字段来实现的,如果字段是基本类型,则会复制其值;如果字段是引用类型,则只会复制引用地址,而不是实际的对象。

以下是一个使用clone()方法的示例代码:

class Person implements Cloneable {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 重写clone()方法
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    // toString()方法
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person("Tom", 20);
        Person p2 = (Person) p1.clone();
        System.out.println(p1);
        System.out.println(p2);
        System.out.println(p1 == p2);
    }
}

在上面的代码中,我们定义了一个Person类,并实现了Cloneable接口以及clone()方法。在程序中,我们创建了一个Person对象p1,然后通过p1.clone()方法创建了一个新的Person对象p2,将p1的内容复制到p2中。输出结果为:

Person{name='Tom', age=20}
Person{name='Tom', age=20}
false

可以看到,p1和p2是两个独立的对象,但它们的内容相同。注意,由于clone()方法是Object类中的protected方法,因此在子类中重写该方法时,要将该方法的访问修饰符改为public。

6.2 lambda 表达式

Lambda表达式是Java8中引入的一个新特性,可以用简洁的语法来表示某个函数式接口(Functional Interface)的实现。Lambda表达式的语法类似于函数,可以把Lambda表达式看作是一个匿名函数,它没有名称、修饰符和返回值类型,但具有参数列表和函数体。

Lambda表达式语法的一般形式为:

(parameter list) -> expression

或者

(parameter list) -> { statements; }

其中,参数列表指定了Lambda表达式的输入参数,箭头符号 -> 将参数列表和Lambda表达式的主体部分分隔开来,expression或statements指定了Lambda表达式的计算过程或执行内容。

例如,我们可以使用Lambda表达式来实现一个简单的排序功能:

List<String> names = Arrays.asList("Tom", "Jerry", "Jack", "Peter");
// 以字母顺序对列表进行排序
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
System.out.println(names); // [Jack, Jerry, Peter, Tom]

在上面的代码中,我们调用了Collections.sort()方法对字符串列表进行排序,Lambda表达式 (s1, s2) -> s1.compareTo(s2) 表示比较两个字符串大小。其中,s1和s2是输入参数,调用s1.compareTo(s2)方法进行比较,这个Lambda表达式等同于下面的匿名类:

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

可以看到,使用Lambda表达式可以极大地简化代码,而且使代码更加易读易懂。Lambda表达式适用于任何函数式接口,如果接口只有一个抽象方法,则可以使用Lambda表达式来实现该方法的具体内容。

6.2.1 为什么引入Lambda表达式

引入Lambda表达式的主要原因是为了简化Java程序的语法,使得开发者能够更容易地编写和阅读代码。Lambda表达式是一种匿名函数,可以将其传递给方法或作为参数使用。Lambda表达式使得Java程序可以更加精简和简洁,同时也能够提高程序的可读性和可维护性。

另外,Lambda表达式也使得Java程序可以更加灵活,能够更好地支持函数式编程。在函数式编程中,将函数作为一等公民对于编写高质量的程序至关重要。Lambda表达式提供了这种支持,可以使Java程序在处理集合、过滤数据等方面变得更加高效和简单。

6.2.2 Lambda表达式的语法

Java中lambda表达式的基本语法如下:

(parameters) -> expression

或者

(parameters) -> { statements; }

其中,parameters表示Lambda表达式的参数,多个参数用逗号隔开;expressionstatements表示Lambda表达式的主体。

下面是一些使用Lambda表达式的实例代码:

  1. 简单的加法操作:
Calculator sum = (x, y) -> x + y;
System.out.println(sum.calculate(3, 4)); // 输出结果为7

上面的代码使用了一个函数式接口Calculator,其中定义了一个计算方法calculate。我们使用Lambda表达式来创建一个匿名函数,将其传递给calculate方法。

  1. 判断一个数是否为偶数:
NumberChecker isEven = x -> x % 2 == 0;
System.out.println(isEven.check(4)); // 输出结果为true
System.out.println(isEven.check(5)); // 输出结果为false

上面的代码使用了一个函数式接口NumberChecker,其中定义了一个检查方法check。我们使用Lambda表达式来创建一个匿名函数,将其传递给check方法。

  1. 对列表中的元素进行排序:
List<Student> students = new ArrayList<>();
students.add(new Student("John", "A", 15));
students.add(new Student("Jane", "B", 12));
students.add(new Student("Dave", "B", 10));
Collections.sort(students, (s1, s2) -> s1.getAge() - s2.getAge());
System.out.println(students);

在上面的代码中,我们使用了Lambda表达式来定义一个比较器来对学生列表中的元素进行排序。Lambda表达式中的s1s2分别表示要比较的两个学生对象,我们通过比较它们的年龄来进行排序。

  1. 使用filter()函数过滤列表中的奇数:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenNumbers = numbers.stream()
                                    .filter(x -> x % 2 == 0)
                                    .collect(Collectors.toList());
System.out.println(evenNumbers);

在上面的代码中,我们使用了Lambda表达式作为filter()方法中的参数,用于过滤列表中的奇数。最终我们使用collect()方法将过滤后的元素收集到一个新的列表中。

6.2.3 函数式接口

Java函数式接口是指只包含一个抽象方法的接口。函数式接口是Java 8中引入的新特性,作为Lambda表达式的基础。

在Java中,函数式接口是指只包含一个抽象方法的接口,例如:

@FunctionalInterface
public interface MyInterface {
    void doSomething();
}

上面的代码定义了一个函数式接口MyInterface,其中只包含一个抽象方法doSomething()。由于函数式接口只包含一个抽象方法,因此我们可以使用Lambda表达式来实现该接口。

在Java 8中,为了方便Lambda表达式的使用,我们可以使用@FunctionalInterface注解来标记一个接口是否是函数式接口:

@FunctionalInterface
public interface MyInterface {
    void doSomething();
    void doAnotherThing(); // 不是抽象方法,会报错
}

如果一个接口标记了@FunctionalInterface注解,但实际上包含多个抽象方法,则会在编译时报错,提示该接口不是一个函数式接口。

Java 8中提供了一些内置的函数式接口,例如:

  1. java.util.function.Consumer<T>:表示接受一个输入参数并且不返回结果的操作。

  2. java.util.function.Supplier<T>:表示没有输入参数并且返回结果的操作。

  3. java.util.function.Predicate<T>:表示接受一个输入参数并返回一个布尔值的操作。

  4. java.util.function.Function<T, R>:表示接受一个输入参数并返回一个结果的操作。

  5. java.util.function.BiFunction<T, U, R>:表示接受两个输入参数并返回一个结果的操作。

下面是一些使用函数式接口的实例代码:

  1. Consumer接口使用:
List<String> names = Arrays.asList("John", "Jane", "Dave");
names.forEach(name -> System.out.println("Hello, " + name));

在上面的代码中,我们使用Consumer接口来打印每个元素,使用Lambda表达式来实现Consumer接口。

  1. Supplier接口使用:
Supplier<Integer> randomInt = () -> (int) (Math.random() * 100);
System.out.println(randomInt.get());

在上面的代码中,我们使用Supplier接口来生成一个随机数,使用Lambda表达式来实现Supplier接口。

  1. Predicate接口使用:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Predicate<Integer> isEven = x -> x % 2 == 0;
numbers.stream().filter(isEven).forEach(System.out::println);

在上面的代码中,我们使用Predicate接口来判断一个数是否为偶数,使用Lambda表达式来实现Predicate接口。

  1. Function接口使用:
Function<String, Integer> strToInt = Integer::parseInt;
System.out.println(strToInt.apply("123")); // 输出结果为123

在上面的代码中,我们使用Function接口将字符串转换为整数,使用Lambda表达式来实现Function接口。

6.2.4 方法引用

Java 方法引用是一种简化代码的方式,它将一个方法的引用作为一个变量传递或返回,而不需要显式地调用该方法。它可以在 Lambda 表达式中使用,用于引用已经存在的方法并将其作为参数传递给函数式接口,或将其赋值给函数式接口的变量。

Java 方法引用可以分为四种类型:

  1. 静态方法引用:使用类名::方法名的语法格式来引用静态方法。

  2. 实例方法引用:使用实例对象::方法名的语法格式来引用实例方法。

  3. 构造方法引用:使用类名::new 的语法格式来引用构造方法。

  4. 数组构造方法引用:使用类型[]::new 的语法格式来引用数组构造方法。

下面是一个使用方法引用的例子:

// 静态方法引用
Function<Integer, Integer> square = Calculator::square;

// 实例方法引用
String str = "Hello World";
Function<Integer, Character> at = str::charAt;

// 构造方法引用
Supplier<Calculator> supplier = Calculator::new;

// 数组构造方法引用
Function<Integer, int[]> arrayCreator = int[]::new;

在上面的例子中,我们使用了不同类型的方法引用来创建不同类型的函数式接口。静态方法引用和实例方法引用创建了一个接收参数并返回结果的函数式接口,构造方法引用和数组构造方法引用则创建了一个不接收参数但返回结果的函数式接口。

6.2.5 构造器引用

构造器引用是Java中方法引用的一种特殊形式,用于引用构造方法。在语法上,构造器引用的格式为ClassName::new,其中ClassName表示类名。

构造器引用可用于创建任意类型的对象,包括内部类、匿名类和Lambda表达式等。它通常用于函数式接口的实例化。

下面是一个使用构造器引用创建对象的例子:

// 定义一个函数式接口
interface ProductFactory<T> {
    T create(int id, String name, double price);
}

// 定义一个商品类
class Product {
    int id;
    String name;
    double price;
    
    public Product(int id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}

// 使用构造器引用创建商品对象
ProductFactory<Product> factory = Product::new;
Product product = factory.create(1, "Apple", 5.0);

在这个例子中,定义了一个函数式接口ProductFactory,它有一个抽象方法create用于创建商品对象。使用构造器引用Product::new,即可将构造方法作为函数式接口的实现。最后,通过factory.create方法创建商品对象,并赋值给变量product

6.2.6 变量作用域

Java变量作用域是指变量所定义的范围,也就是变量在什么地方可以被访问和使用。在Java中,变量作用域可以分为以下几个类型:

  1. 类变量(静态变量)作用域:类变量定义于类中,可以被类的所有对象和方法调用,作用域为整个类。

  2. 实例变量作用域:实例变量定义于类中,但在方法之外,只能在类的方法内调用,作用域为整个类的实例。

  3. 局部变量作用域:局部变量定义于方法或代码块中,只在方法或代码块内可见,作用域为方法或代码块内部。

  4. 方法参数作用域:方法参数作为局部变量定义于方法内,在整个方法内都可见。

下面通过示例来说明Java变量作用域的特点:

public class ScopeExample {
    // 类变量
    static int x = 10;
    
    // 实例变量
    int y = 20;
    
    public static void main(String[] args) {
        ScopeExample obj = new ScopeExample();
        obj.methodA();
    }
    
    public void methodA() {
        // 局部变量
        int z = 30;
        
        // 方法参数
        int w = 40;
        
        System.out.println("x = " + x);    // 类变量作用域
        System.out.println("y = " + y);    // 实例变量作用域
        System.out.println("z = " + z);    // 局部变量作用域
        System.out.println("w = " + w);    // 方法参数作用域
    }
}

在这个例子中,定义了一个类ScopeExample,其中包含了类变量、实例变量、方法、局部变量和方法参数。在main方法中,创建了ScopeExample类的实例,并调用了它的方法methodA()。在方法methodA()中,分别输出了各种类型变量的值。

输出结果为:

x = 10
y = 20
z = 30
w = 40

可以看出,类变量和实例变量的作用域比较广,可以在整个类中被调用,而局部变量和方法参数的作用域比较短,仅在方法内部有效。

6.2.7 处理Lambda表达式

处理Lambda表达式,主要涉及到以下几个方面:

  1. 函数式接口:Lambda表达式必须要依赖于函数式接口。函数式接口是指只包含一个抽象方法的接口。Lambda表达式与函数式接口的关系可以看做是一种“匹配”。

  2. 语法:Lambda表达式包含三个部分:参数列表、箭头符号和方法体。例如(参数列表) -> {方法体}。

  3. 方法引用:Lambda表达式可以使用方法引用,将方法作为参数传递到另一个方法中。方法引用和Lambda表达式的效果是一样的,但是方法引用更加简洁,易于理解。

  4. 变量作用域:Lambda表达式可以访问在它外部定义的变量,但是这些变量必须是final或者effectively final的。Lambda表达式中不能修改这些变量的值。

下面列举一个简单的Lambda表达式的示例:

public class LambdaExample {
    public static void main(String[] args) {
        // 使用Lambda表达式实现Runnable接口
        Runnable r = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(i);
            }
        };
        
        // 使用Lambda表达式实现Comparator接口
        List<String> list = Arrays.asList("apple", "banana", "orange");
        Collections.sort(list, (a, b) -> a.compareTo(b));
        
        // 使用方法引用
        List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
        nums.forEach(System.out::println);
    }
}

在这个例子中,使用Lambda表达式分别实现了Runnable接口和Comparator接口,并且还使用了方法引用。需要注意的是,Lambda表达式的参数列表可以为空,但是箭头符号是必须的。而在使用Comparator接口时,需要实现compare方法,并且返回值为int类型,返回值是负数、零或正数,表示第一个参数小于、等于或大于第二个参数。最后,在使用方法引用时,需要使用“::”符号来引用方法,例如System.out::println表示引用System.out的println()方法。

在Java 8中,常用的函数式接口有以下几种:

  1. Predicate:该接口接受一个参数,并返回一个布尔值结果。通常用来过滤集合元素。
Predicate<Integer> even = n -> n % 2 == 0;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenNumbers = filterNumbers(numbers, even);
  1. Function:该接口接受一个参数,并返回一个结果。通常用来对集合元素进行转换。
Function<String, Integer> stringToInteger = s -> Integer.parseInt(s);
List<String> strNumbers = Arrays.asList("1", "2", "3", "4", "5");
List<Integer> numbers = convertToNumbers(strNumbers, stringToInteger);
  1. Consumer:该接口接受一个参数,但没有返回值。通常用来对集合元素进行操作。
Consumer<String> print = s -> System.out.println(s);
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
printNames(names, print);
  1. Supplier:该接口不接受任何参数,但返回一个结果。通常用来生成一些值。
Supplier<String> hello = () -> "Hello, world!";
String message = hello.get();
  1. UnaryOperator:该接口接受一个参数,并返回一个结果,该结果的类型和参数的类型相同。通常用来对集合元素进行修改。
UnaryOperator<Integer> square = n -> n * n;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = modifyNumbers(numbers, square);
  1. BinaryOperator:该接口接受两个参数,并返回一个结果,该结果的类型和参数的类型相同。通常用来进行一些操作,例如求和、求积等。
BinaryOperator<Integer> sum = (a, b) -> a + b;
int result = sumNumbers(numbers, sum);

这些函数式接口都可以使用Lambda表达式来实现,比较简洁明了。

以下是 Java 中基本类型函数式接口的表格:

函数式接口参数类型返回类型说明
BooleanSupplierboolean返回一个 boolean 值
DoubleSupplierdouble返回一个 double 值
IntSupplierint返回一个 int 值
LongSupplierlong返回一个 long 值
DoubleUnaryOperatordoubledouble接受一个 double 值返回一个 double 值
IntUnaryOperatorintint接受一个 int 值返回一个 int 值
LongUnaryOperatorlonglong接受一个 long 值返回一个 long 值
DoubleBinaryOperatordouble, doubledouble接受两个 double 值返回一个 double 值
IntBinaryOperatorint, intint接受两个 int 值返回一个 int 值
LongBinaryOperatorlong, longlong接受两个 long 值返回一个 long 值
DoubleFunctiondoubleR接受一个 double 值返回一个 R 类型值
IntFunctionintR接受一个 int 值返回一个 R 类型值
LongFunctionlongR接受一个 long 值返回一个 R 类型值
ToDoubleFunctionTdouble接受一个 T 类型值返回一个 double 值
ToIntFunctionTint接受一个 T 类型值返回一个 int 值
ToLongFunctionTlong接受一个 T 类型值返回一个 long 值
DoubleToIntFunctiondoubleint接受一个 double 值返回一个 int 值
DoubleToLongFunctiondoublelong接受一个 double 值返回一个 long 值
IntToDoubleFunctionintdouble接受一个 int 值返回一个 double 值
IntToLongFunctionintlong接受一个 int 值返回一个 long 值
LongToDoubleFunctionlongdouble接受一个 long 值返回一个 double 值
LongToIntFunctionlongint接受一个 long 值返回一个 int 值
DoublePredicatedoubleboolean接受一个 double 值返回一个 boolean 值
IntPredicateintboolean接受一个 int 值返回一个 boolean 值
LongPredicatelongboolean接受一个 long 值返回一个 boolean 值
DoubleConsumerdouble接受一个 double 值,不返回任何值
IntConsumerint接受一个 int 值,不返回任何值
LongConsumerlong接受一个 long 值,不返回任何值
ObjDoubleConsumerT, double接受一个 T 值和一个 double 值,不返回任何值
ObjIntConsumerT, int接受一个 T 值和一个 int 值,不返回任何值
ObjLongConsumerT, long接受一个 T 值和一个 long 值,不返回任何值

6.2.8 再谈Comparator

Comparator 和 Lambda 表达式的复合使用可以实现更加灵活和复杂的排序逻辑。我们可以使用 Comparator 的链式调用来组合多个 Lambda 表达式。

假设我们有一个名为 Employee 的类,它有两个属性:name(String 类型)和 salary(double 类型),我们想要按照 salary 从高到低排序,如果两个 Employee 的 salary 相等,则按照 name 排序。示例代码如下:

List<Employee> employees = Arrays.asList(
    new Employee("Alice", 5000),
    new Employee("Bob", 6000),
    new Employee("Charlie", 5000),
    new Employee("David", 7000)
);

// 按照 salary 从高到低排序,如果 salary 相等则按照 name 排序
employees.sort(Comparator.comparingDouble(Employee::getSalary).reversed().thenComparing(Employee::getName));
System.out.println(employees);

在上面的示例中,我们使用了 Comparator 的静态方法 comparingDouble 和 thenComparing 方法来创建一个复合的 Comparator 实例。comparingDouble 方法接受一个 Function 接口的参数,用于指定比较的属性,我们使用 Employee::getSalary 来表示获取 Employee 的 salary 属性。reversed 方法用于反转比较顺序,即从高到低排序。thenComparing 方法可以在前一个 Comparator 的基础上再添加一个比较条件,我们使用 Employee::getName 来表示按照 name 排序。

需要注意的是,使用 Comparator 的链式调用时,每个 Comparator 都是不可变的,这意味着我们可以在调用链中创建任意数量的中间比较器,它们都不会对源数据进行任何修改。

6.3 内部类

Java 内部类是一种嵌套在另一个类中的类,它可以访问外部类的成员变量和方法,也可以定义自己的成员变量和方法。Java 内部类有以下几种形式:

  1. 成员内部类(Member Inner Class):定义在类体中的普通类。
  2. 局部内部类(Local Inner Class):定义在方法或代码块中的类。
  3. 匿名内部类(Anonymous Inner Class):没有类名的内部类,用于创建实现某个接口或继承某个类的对象。
  4. 静态内部类(Static Inner Class):定义在类体中的静态类。

下面是各种类型内部类的示例代码:

  1. 成员内部类:
public class OuterClass {
    private int x = 10;

    public class InnerClass {
        public int getX() {
            return x;
        }
    }
}

// 创建内部类对象
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
System.out.println(inner.getX()); // 输出 10

在上面的代码中,我们定义了名为 InnerClass 的成员内部类,它可以访问外部类的私有成员变量 x,我们可以通过创建 OuterClass 和 InnerClass 的对象来访问内部类的方法。

  1. 局部内部类:
public class OuterClass {
    private int x = 10;

    public void method() {
        class InnerClass {
            public int getX() {
                return x;
            }
        }

        InnerClass inner = new InnerClass();
        System.out.println(inner.getX()); // 输出 10
    }
}

// 创建外部类对象并调用方法
OuterClass outer = new OuterClass();
outer.method();

在上面的代码中,我们定义了名为 InnerClass 的局部内部类,它定义在 method 方法中,可以访问外部类的私有成员变量 x。要创建局部内部类的对象,必须在方法中进行,如上述代码所示。

  1. 匿名内部类:
public interface MyInterface {
    void method();
}

public class OuterClass {
    public void method() {
        MyInterface inner = new MyInterface() {
            @Override
            public void method() {
                System.out.println("Hello");
            }
        };

        inner.method(); // 输出 Hello
    }
}

// 创建外部类对象并调用方法
OuterClass outer = new OuterClass();
outer.method();

在上面的代码中,我们定义了一个接口 MyInterface,然后在 OuterClass 类中创建了一个方法,这个方法中创建了一个没有类名的匿名内部类,它实现了 MyInterface 接口,并重写了其中的 method 方法。我们可以直接调用内部类的 method 方法。

  1. 静态内部类:
public class OuterClass {
    private static int x = 10;

    public static class InnerClass {
        public int getX() {
            return x;
        }
    }
}

// 创建静态内部类对象
OuterClass.InnerClass inner = new OuterClass.InnerClass();
System.out.println(inner.getX()); // 输出 10

在上面的代码中,我们定义了名为 InnerClass 的静态内部类,可以直接通过 OuterClass.InnerClass 的方式创建内部类的对象,不需要创建外部类的对象。由于静态内部类不依赖于外部类,因此可以访问外部类的静态成员变量,但不能访问非静态成员变量。

6.3.1 使用内部类访问对象状态

在Java中,内部类可以访问其外部类的私有变量和方法,因此可以通过内部类来访问外部对象的状态。

考虑以下示例:

public class OuterClass {
    private int x = 10;

    public void doSomething() {
        InnerClass inner = new InnerClass();
        inner.printX();
    }

    public class InnerClass {
        public void printX() {
            System.out.println("x is " + x);
        }
    }
}

在这个示例中,我们定义了一个名为OuterClass的外部类,它具有一个私有变量x和一个名为doSomething的公共方法。OuterClass还包含一个名为InnerClass的内部类,该类具有一个名为printX的方法,该方法可以访问OuterClassx变量。

doSomething方法中,我们创建一个InnerClass对象,然后调用printX方法。该方法将打印x is 10,因为它可以访问OuterClassx变量。

因此,通过使用内部类,我们可以轻松地访问外部对象的状态。

6.3.2 内部类的特殊语法规则

Java中内部类的定义通常需要遵循一些特殊的语法规则,以下是一些常见的规则:

  1. 成员内部类必须定义在其外部类的内部(即类的内部),而不能定义在方法或代码块中。

  2. 成员内部类可以访问外部类的所有成员,包括私有成员,而且外部类也可以访问内部类的所有成员。

  3. 如果内部类的作用域仅限于外部类的某个方法中,则可以将内部类定义为局部内部类。

  4. 局部内部类只能在定义它的方法中访问,不能定义任何静态成员,也不能访问其外部类的非final局部变量。

  5. 如果内部类是静态内部类,则不能使用外部类的非静态成员,只能使用外部类的静态成员。

  6. 可以在内部类中定义静态成员,但如果内部类本身不是静态的,则静态成员只能在内部类中嵌套的静态类中使用。

  7. 匿名内部类是一种没有名字的内部类,通常用于实现单次使用的接口或抽象类方法。

  8. 匿名内部类不能有构造方法,因为它没有命名。

总的来说,内部类的特殊语法规则是为了实现Java中的类与类之间的嵌套关系,并提供更加灵活的语言特性。理解这些规则对于正确使用内部类非常重要。

6.3.3 内部类是否有用、必要和安全

内部类在Java语言中有其特殊的应用场景,以下是内部类的一些主要用途:

  1. 实现特定的接口或抽象类方法:通过定义一个实现接口或抽象类方法的匿名内部类,可以方便地在调用方法的时候提供实现。

  2. 封装:内部类可以被用来实现封装,可以在外部类中定义一个私有内部类,使其只能在该类中使用,从而达到封装的目的,同时使得代码更加清晰易读。

  3. 事件处理:对于事件驱动的编程模型,可以使用内部类来实现事件监听器,当事件触发时内部类会捕获事件处理并执行相应的操作。

  4. 简化代码:内部类可以大大简化代码,尤其是在需要定义许多具有相同功能的类时,可以使用内部类来实现代码复用。

然而,内部类也存在一些安全和必要性的问题:

  1. 内部类的嵌套层次较深时会导致代码的可读性变差,增加代码维护的难度。

  2. 内部类可能与外部类之间产生混淆和歧义,加大程序的复杂度,增加调试难度。

  3. 内部类的使用会增加程序的内存负担,降低程序运行效率。

因此,在使用内部类时,需要平衡可读性、程序复杂度、内存占用以及程序运行效率等因素,谨慎地选择合适的应用场景和使用方式,以保证代码的可维护性和可扩展性。

6.3.4局部内部类

局部内部类是定义在方法体内的嵌套类,其作用域仅限于定义它的方法中。局部内部类可以访问所在方法的局部变量和方法参数,但这些变量必须声明为final类型。示例代码如下:

public class OuterClass {
    public void method() {
        final int x = 10;

        class LocalInnerClass {
            public void print() {
                System.out.println("x = " + x);
            }
        }

        LocalInnerClass localInner = new LocalInnerClass();
        localInner.print();
    }
}

在上面的代码中,LocalInnerClass是一个局部内部类,它访问了x这个局部变量,因此必须将x声明为final类型。在method方法中创建了LocalInnerClass对象并调用它的print方法,输出x的值为10。

6.3.5由外部方法访问变量

在Java中,可以通过getter和setter方法让外部方法访问类中的变量。getter方法用于获取变量的值,setter方法用于修改变量的值。

以下是一个示例代码:

public class MyClass {
    private int myVariable;

    public void setMyVariable(int newValue) {
        myVariable = newValue;
    }

    public int getMyVariable() {
        return myVariable;
    }
}

在上面的代码中,myVariable是私有变量,外部方法没有直接访问它的权限。但是,通过setMyVariablegetMyVariable方法,外部方法可以设置和获取该变量的值。

例如,在另一个类中调用setMyVariablegetMyVariable方法:

public class AnotherClass {
    public static void main(String[] args) {
        MyClass myObject = new MyClass();
        myObject.setMyVariable(5);
        System.out.println(myObject.getMyVariable());
    }
}

在上面的代码中,我们创建了一个MyClass对象,并通过setMyVariable方法将myVariable设置为5。然后,我们使用getMyVariable方法获取该变量的值,并将其打印在控制台上。输出结果为5

6.3.6匿名内部类

Java中的匿名内部类是没有名字的内部类,它是一种方便临时创建一个类的方式。通常情况下,当需要为某个父类或接口创建一个子类或实现类时,我们需要先定义一个子类或实现类,并且给它起一个名字,然后再创建类的实例。

但是,如果只需要在某个地方创建一个只使用一次的子类或实现类,此时定义一个类可能会显得过于繁琐和冗余。这时可以使用匿名内部类来简化代码。匿名内部类可以同步定义并实例化,且它的定义和创建过程是同时进行的。

匿名内部类的语法格式如下:

new 父类构造器(参数列表)或接口(){
    //匿名内部类的类体部分
};

其中,父类构造器和接口是必须的,类体部分是匿名内部类的具体实现。匿名内部类的使用通常与某个接口、抽象类或具体父类相关。

以下是一个匿名内部类的示例代码:

public class AnonymousInnerClassExample {
    interface Greeting {
        void greet();
    }

    public static void main(String[] args) {
        Greeting greeting = new Greeting() {
            public void greet() {
                System.out.println("Hello, world!");
            }
        };
        greeting.greet();
    }
}

在上面的代码中,我们定义了一个接口Greeting,它有一个greet()方法。然后,在main()方法中,我们创建了一个匿名内部类,该匿名内部类实现了Greeting接口,并重写了greet()方法。在匿名内部类的类体部分中,我们输出了一行字符串。最后,通过greeting.greet()调用了匿名内部类中的greet()方法,输出了字符串“Hello, world!”。

需要注意的是,匿名内部类不能有显式的构造方法,因为它没有名字。因此,我们只能使用父类的构造方法来构造匿名内部类的实例。

在使用Java中的匿名内部类时,需要注意以下几点:

  1. 匿名内部类必须先定义,再实例化。具体语法格式为:

    new 父类构造器(参数列表) 或 实现接口(){
        //匿名内部类的类体部分
    };
    
  2. 匿名内部类只能使用一次,它没有类名,不能重复使用。

  3. 匿名内部类可以实现接口或者继承父类,但不能同时实现接口和继承父类,因为Java中不支持多重继承。

  4. 匿名内部类可以访问外部类的成员变量和方法,但是要求它们必须是final类型的(Java 8之后,对于实际未被修改的局部变量和参数,编译器默认会把它们隐式地转换为final类型,因此可以在匿名内部类中访问它们)。

  5. 匿名内部类中可以定义自己的变量和方法,但只能在匿名内部类中使用。

  6. 匿名内部类中不允许定义静态成员、静态方法以及静态初始化块。

  7. 匿名内部类中可以使用Lambda表达式,从而进一步简化代码。

除此之外,匿名内部类的定义和使用方式与普通的类基本相同,可以参考Java中的内部类。

6.3.7 静态内部类

Java中的静态内部类是指在一个类的内部定义的类,使用static关键字修饰。它与普通内部类的区别在于,静态内部类可以直接通过外部类名直接访问,而不需要创建外部类的对象。

静态内部类的特点:

  1. 静态内部类可以访问外部类的静态成员和方法,但不能访问外部类的非静态成员和方法。

  2. 静态内部类可以定义静态成员和方法,也可以定义非静态成员和方法。但非静态成员和方法不能被静态内部类的静态成员和方法直接访问,需要通过对象的方式访问。

  3. 静态内部类可以定义在另一个类的内部,也可以定义在方法内部。

静态内部类的语法:

public class OuterClass {
    // 外部类的成员变量和方法
    public static class StaticInnerClass {
        // 静态内部类的成员变量和方法
    }
}

静态内部类的使用方法:

// 创建静态内部类的对象
OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();

// 访问静态内部类的成员变量和方法
inner.staticMember;
inner.staticMethod();

Java中的静态内部类是一个类的嵌套公共静态类。下面是需要注意的几点:

  1. 静态内部类不能访问外部类的非静态成员变量和非静态成员方法,只能访问外部类的静态成员变量和静态成员方法。

  2. 静态内部类可以直接使用外部类的静态成员和方法,不需要创建外部类的实例。

  3. 可以在静态内部类中定义静态方法和成员变量,这些成员变量和方法只能在静态内部类中使用,不能在外部类中使用。

  4. 静态内部类没有外部类实例引用,因此不会造成内存泄漏。

  5. 静态内部类与外部类的实例无关,可以独立于外部类进行实例化和使用。

  6. 静态内部类可以在其它类中被实例化,不受外部类的控制。

  7. 静态内部类中的静态变量和方法在类加载时会被初始化。

6.4 服务加载类

服务加载类(ServiceLoader)是JavaSE 6引入的一个类,用于在运行时动态地查找和加载实现某个或某些接口的服务提供者。它的作用是使应用程序具有更好的灵活性和可扩展性,因为这样应用程序可以在运行时动态地加载任意实现服务接口的类,而不需要在编译时确定具体实现类。

服务加载类指的是 Java 中的 ServiceLoader 类,用于加载服务提供者接口的实现类。它的作用是为了支持模块化和扩展性,允许一个模块提供某个服务的实现,并能够被其他模块调用和使用。使用 ServiceLoader 类需要以下步骤:

  1. 定义服务接口,即指定服务的行为和规范;
  2. 实现服务接口的具体类,即提供服务的实现;
  3. 在具体类所在的模块中,创建一个 META-INF/services/接口全名 文件,其中包含提供服务的具体类的全限定名;
  4. 使用 ServiceLoader.load(接口类) 方法加载服务实现类。

当我们使用 ServiceLoader.load() 方法时,它会在类路径下搜索 META-INF/services/接口全名 文件,读取其中的内容,并将提供服务的实现类实例化并返回。这样,我们可以通过接口来使用不同的实现类,而无需显式地创建和维护这些实现类的对象。

下面是一个简单的示例,展示如何使用ServiceLoader加载服务提供者:

定义接口:

package com.example.service;

public interface HelloService {
    void sayHello();
}

定义接口实现类:

package com.example.provider;

import com.example.service.HelloService;

public class HelloServiceImpl implements HelloService {
    public void sayHello() {
        System.out.println("Hello");
    }
}

在provider模块的META-INF/services目录下创建文件com.example.service.HelloService:

com.example.provider.HelloServiceImpl

客户端代码:

package com.example.client;

import com.example.service.HelloService;

import java.util.ServiceLoader;

public class Client {
    public static void main(String[] args) {
        ServiceLoader<HelloService> helloServiceLoader = ServiceLoader.load(HelloService.class);
        for (HelloService helloService : helloServiceLoader) {
            helloService.sayHello();
        }
    }
}

运行客户端代码,输出:

Hello

从输出可以看出,ServiceLoader成功地加载了HelloServiceImpl的实例,并调用了sayHello()方法。

在上面的示例中,我们定义了一个HelloService接口,然后实现了它的一个具体实现类HelloServiceImpl,并将它注册到了ServiceLoader中。客户端代码通过调用ServiceLoader.load方法加载HelloService接口的实现类,然后遍历返回的实例,调用sayHello方法。

6.5 代理

Java中的代理模式是一种常用的结构型设计模式,它允许一个对象在不改变其原有行为的基础上,为其提供额外的功能。

Java中的代理有两种:静态代理和动态代理。

静态代理需要为每个被代理类编写代理类,而动态代理则不需要。Java中的动态代理机制利用反射机制在运行时动态地生成代理类,从而避免了手动编写代理类的工作。

动态代理的实现原理是基于Java的反射机制和接口,需要实现Java.lang.reflect.InvocationHandler接口。代理对象会调用InvocationHandler接口中的invoke方法,并将方法名、方法参数以及代理对象本身传给该方法。在invoke方法中通过反射调用被代理对象的相应方法实现代理功能。

下面是一个简单的动态代理示例,演示了如何在运行时动态地创建代理类:

定义接口:

public interface HelloService {
    void sayHello(String name);
}

定义实现类:

public class HelloServiceImpl implements HelloService {
    public void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
}

定义代理类:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class HelloServiceProxy implements InvocationHandler {
    private Object target;

    public HelloServiceProxy(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before calling " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After calling " + method.getName());
        return result;
    }

    public Object getProxy() {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
}

客户端代码:

public class Client {
    public static void main(String[] args) {
        HelloService helloService = new HelloServiceImpl();
        HelloServiceProxy proxy = new HelloServiceProxy(helloService);
        HelloService helloServiceProxy = (HelloService) proxy.getProxy();
        helloServiceProxy.sayHello("Bob");
    }
}

在客户端代码中,首先创建了被代理对象HelloServiceImpl,然后用HelloServiceProxy来代理HelloServiceImpl。HelloServiceProxy通过实现InvocationHandler接口来实现代理功能,并通过Proxy.newProxyInstance方法在运行时动态地生成代理类。最后,客户端调用代理类的sayHello方法,代理类会在调用被代理对象的sayHello方法前后加入额外的逻辑。

从输出结果可以看出,代理类成功地调用了被代理对象的sayHello方法,并且在调用前后加入了Before calling和After calling的逻辑。

Before calling sayHello
Hello, Bob
After calling sayHello

静态代理和动态代理是Java中两种常见的代理模式。

静态代理是由程序员手动编写代理类来代替被代理的对象,代理类和被代理类的接口要实现相同的方法,通过调用代理类的方法,实现对被代理对象的调用。静态代理的优点是简单易懂,但是如果被代理的对象较多,就要写很多代理类,编码量大,维护难度高。

动态代理是在程序运行时动态生成代理类,不需要手动编写,可以代理任何实现了接口的类。动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。JDK动态代理是Java提供的默认代理机制,基于接口实现,能够代理实现了接口的类;CGLIB动态代理则是通过动态生成子类的方式,能够代理没有实现接口的类。动态代理的优点是可以实现通用的代理方法,但是其性能比静态代理略低。

下面是静态代理和动态代理的示例代码:

静态代理示例代码:

// 定义接口
interface IUserService {
    void save();
}

// 实现接口的被代理类
class UserServiceImpl implements IUserService {

    @Override
    public void save() {
        System.out.println("保存用户信息");
    }
}

// 定义代理类
class UserServiceProxy implements IUserService {

    private IUserService userService;

    public UserServiceProxy(IUserService userService) {
        this.userService = userService;
    }

    @Override
    public void save() {
        System.out.println("记录日志");
        userService.save();
        System.out.println("发送消息");
    }
}

// 测试代码
public class Test {
    public static void main(String[] args) {
        IUserService userService = new UserServiceImpl();
        userService = new UserServiceProxy(userService);
        userService.save(); // 执行代理类的方法
    }
}

动态代理示例代码:

// 定义接口
interface IUserService {
    void save();
}

// 实现接口的被代理类
class UserServiceImpl implements IUserService {

    @Override
    public void save() {
        System.out.println("保存用户信息");
    }
}

// 测试代码
public class Test {
    public static void main(String[] args) {
        IUserService userService = new UserServiceImpl();
        // 获取动态代理类
        IUserService proxy = (IUserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(), (proxyObj, method, args1) -> {
                    System.out.println("记录日志");
                    Object result = method.invoke(userService, args1);
                    System.out.println("发送消息");
                    return result;
                });
        proxy.save(); // 执行代理类的方法
    }
}

6.5.1 何时使用代理

在Java中使用代理有多种情况,以下列举几个常见的:

  1. 网络代理:在网络请求时,可以通过代理服务器来转发请求,此时需要使用Java提供的Proxy类或HttpURLConnection类的代理设置方法来设置代理。

  2. 安全代理:在需要控制访问权限时,可以使用代理模式来实现安全代理,即代理对象控制访问原始对象的权限。

  3. 远程代理:在分布式系统中,可以使用远程代理来实现对象的远程访问,即代理对象在不同的机器上,用于代理远程对象的访问。

  4. 缓存代理:在需要对原始对象进行缓存时,可以使用代理模式来实现缓存代理,即代理对象缓存原始对象的结果,避免重复计算。

总之,在需要对原始对象进行控制访问、远程访问、缓存等操作时,都可以使用代理模式来实现。

6.5.2 创建代理对象

创建Java代理对象的一般步骤如下:

  1. 定义一个接口,该接口是代理类和被代理类的公共接口。
  2. 创建一个InvocationHandler实现类,该类实现了java.lang.reflect.InvocationHandler接口,并重写了invoke()方法。
  3. 在InvocationHandler实现类中调用被代理类的方法,同时对方法进行增强。
  4. 使用java.lang.reflect.Proxy的静态方法newProxyInstance()创建代理对象。

下面是一个具体的示例代码:

定义一个接口:

public interface Animal {
    void eat();
}

创建一个InvocationHandler实现类:

public class AnimalInvocationHandler implements InvocationHandler {
    private Animal animal;

    public AnimalInvocationHandler(Animal animal) {
        this.animal = animal;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before eat...");
        Object result = method.invoke(animal, args);
        System.out.println("After eat...");
        return result;
    }
}

创建被代理类:

public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating...");
    }
}

创建代理对象:

Animal dog = new Dog();
AnimalInvocationHandler handler = new AnimalInvocationHandler(dog);
Animal animalProxy = (Animal) Proxy.newProxyInstance(
    Dog.class.getClassLoader(),
    new Class[] { Animal.class },
    handler
);
animalProxy.eat();

输出结果:

Before eat...
Dog is eating...
After eat...

6.5.3 代理类的特性

代理类是一种设计模式,它能够为其他对象提供一种代理或中介的角色,以控制对这些对象的访问。代理类的特性包括:

  1. 代理类与被代理类具有相同的接口:代理类与被代理类应该具有相同的方法,以确保代理类能够完全替代被代理类。

  2. 代理类可以在被代理类的基础上增加额外的功能:代理类可以在调用被代理类的方法前后,增加额外的逻辑或功能,以实现更多的控制或增强效果。

  3. 代理类可以隐藏被代理类的实现细节:代理类可以隐藏被代理类的实现细节,使得客户端可以更加简单地使用被代理类,并且防止客户端直接访问被代理类。

  4. 代理类可以提高系统的性能:代理类可以缓存被代理类的结果或延迟被代理类的初始化,从而提高系统的性能。

  5. 代理类可以解决远程访问问题:代理类可以代理远程对象,使得客户端可以通过网络调用远程对象的方法,从而解决远程访问问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值