文章目录


1、类与对象

面向对象程序设计(简称 OOP)是当今主流的程序设计范型,它已经取代了 20 世纪 70 年代的"结构化"过程化程序设计开发技术。

从一开始学习 Java 这门技术时,我们就会了解到 Java 是完全面向对象的,必须熟悉 OOP 才能够编写 Java 程序。同样的 Java 之所以简单而具有优势,就是因为面向对象所带来的方便。这种方式免去了 C++ 中反复而难以理解的指针和多继承,可以让程序员以优雅的思维方式进行复杂的编程。而这之中最为核心也是最基础的部分就是类与对象。

1.1、关于类

类(class)是构造对象的模板或蓝图。我们可以将类想象成制作小甜饼的切割机,将对象想象为小甜饼。由类构造(construct)对象的过程称为创建类的实例(instance)。

标准的 Java 库提供了几千个类,可以用于用户界面设计、日期、日历和网络程序设计。尽管如此,还是需要在 Java 程序中创建一些自己的类,以便描述应用程序所对应的问题域中的对象。

一个类一般包含以下几部分:

  1. 类名:要遵循大驼峰命名法(UpperCamelCase),如 PersonCar
  2. 属性:也称为字段或成员变量,用于存储对象的状态信息。属性通常使用访问修饰符(如 privateprotectedpublic)来控制其可见性;
  3. 方法:也称为成员方法,用于执行操作或表示对象的行为。方法可以访问和修改对象的属性,同样的,方法通常也使用访问修饰符来控制其可见性;
  4. 构造方法:特殊的方法,用于在创建对象时初始化对象的状态。构造方法的名称必须与类名相同。

以下是一个 Car 类的示例,描述了一个汽车的属性和行为:

package com.lizhengi;

/**
 * 汽车类,描述汽车的属性和行为(类)
 * 组成部分:
 * 1. 成员变量(属性):描述汽车的特性
 * 2. 构造方法:用于创建汽车对象实例
 * 3. 方法:定义汽车的行为
 *
 * @author Lizhengi
 */
public class Car {

    /* 成员变量(属性)*/
    /** 汽车品牌 */
    private String make;
    /** 汽车型号 */
    private String model;
    /** 出厂年份 */
    private int year;

    /**
     * 构造方法,用于创建 Car 对象实例
     *
     * @param make  汽车品牌
     * @param model 汽车型号
     * @param year  出厂年份
     */
    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    /**
     * 启动引擎的方法
     */
    public void startEngine() {
        System.out.println("The engine is starting...");
    }
}
1.2、关于对象

对象与类的关系是:对象的共性抽象为类,类的实例化就是对象。

对象(object)是类的实例,是使用类的构造方法创建的实体。对象代表现实生活中的实体,它拥有类中定义的属性和方法。每个对象都有自己的一组属性值,这些值定义了对象的状态。通过调用对象的方法,可以改变对象的状态或执行某些操作。

例如,汽车(Car)类可以有很多对象,每个对象代表不同的汽车。每辆汽车都有自己的品牌、型号和出厂年份,但它们都是基于同一个类(Car)创建的。

要想使用 OOP,一定要清楚对象的三个主要特性:

  1. Behavior(对象的行为):可以对对象施加哪些操作,或可以对对象施加哪些方法?
  2. State(对象的状态):当施加那些方法时,对象如何响应?
  3. Identity(对象的标识):如何辨别具有相同行为与状态的不同对象?
1.2.1、Behavior(对象的行为)

对象的行为是指对象能够执行的操作或方法。这些操作定义了对象可以做什么。行为通常通过方法来实现,方法可以对对象的状态进行操作或进行其他操作。行为是类中定义的,是对象可以执行的具体功能。

例如,汽车对象的行为可以包括启动引擎(startEngine)、加速(accelerate)、刹车(brake)等。通过调用这些方法,能够使对象执行相应的操作。

public class Car {
    private String make;
    private String model;
    private int year;

    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    /**
     * 启动引擎的方法
     */
    public void startEngine() {
        System.out.println("The engine is starting...");
    }

    /**
     * 加速的方法
     */
    public void accelerate() {
        System.out.println("The car is accelerating...");
    }

    /**
     * 刹车的方法
     */
    public void brake() {
        System.out.println("The car is braking...");
    }
}
1.2.2、State(对象的状态)

对象的状态是指对象在某一时刻的属性值。对象的状态由其属性(成员变量)的值决定。通过改变属性的值,可以改变对象的状态。状态反映了对象在特定时刻的特征和条件。

例如,汽车对象的状态可以包括汽车品牌(make)、车型(model)和出厂年份(year)。这些属性的值定义了对象的当前状态。

public class Car {
    private String make;
    private String model;
    private int year;

    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    /* Getter 和 Setter 方法 */

    /**
     * 获取汽车品牌
     *
     * @return 汽车品牌
     */
    public String getMake() {
        return make;
    }

    /**
     * 设置汽车品牌
     *
     * @param make 汽车品牌
     */
    public void setMake(String make) {
        this.make = make;
    }

    /**
     * 获取汽车型号
     *
     * @return 汽车型号
     */
    public String getModel() {
        return model;
    }

    /**
     * 设置汽车型号
     *
     * @param model 汽车型号
     */
    public void setModel(String model) {
        this.model = model;
    }

    /**
     * 获取出厂年份
     *
     * @return 出厂年份
     */
    public int getYear() {
        return year;
    }

    /**
     * 设置出厂年份
     *
     * @param year 出厂年份
     */
    public void setYear(int year) {
        this.year = year;
    }
}
1.2.3、Identity(对象的标识)

对象的标识是指每个对象在内存中的唯一标识。即使两个对象具有相同的状态和行为,它们在内存中也是不同的实体。对象的标识使得可以区分具有相同行为和状态的不同对象。

在 Java 中,对象的标识由内存地址决定。即使两个对象的属性值完全相同,它们仍然是不同的对象,因为它们在内存中的地址不同。

public class Main {
    public static void main(String[] args) {
        // 创建两个具有相同属性值的 Car 对象
        Car car1 = new Car("Toyota", "Corolla", 2020);
        Car car2 = new Car("Toyota", "Corolla", 2020);

        // 比较两个对象的内存地址
        if (car1 == car2) {
            System.out.println("car1 and car2 are the same object.");
        } else {
            System.out.println("car1 and car2 are different objects.");
        }

        // 比较两个对象的属性值
        if (car1.getMake().equals(car2.getMake()) &&
            car1.getModel().equals(car2.getModel()) &&
            car1.getYear() == car2.getYear()) {
            System.out.println("car1 and car2 have the same state.");
        } else {
            System.out.println("car1 and car2 have different states.");
        }
    }
}

输出结果为:

car1 and car2 are different objects.
car1 and car2 have the same state.

通过理解对象的行为、状态和标识,可以更好地掌握面向对象编程的核心概念,编写出更具模块化和可维护性的代码。

1.3、类之间的关系

在面向对象编程中,类之间的关系至关重要,它们决定了系统的结构和行为。常见的类之间的关系有:依赖、聚合和继承。

1.3.1、依赖关系(Dependency)

依赖关系是指一个类使用另一个类的实例。通常表现为一个类的方法接收另一个类的对象作为参数,或在方法中创建另一个类的对象。这种关系是最弱的耦合关系。

依赖关系可以理解为"使用"关系,即一个类依赖于另一个类来完成某些功能。

示例:

/**
 * Driver 类表示驾驶员
 * 依赖关系:Driver 类依赖于 Car 类
 */
public class Driver {
    public void drive(Car car) {
        car.startEngine();
        System.out.println("The driver is driving the car.");
    }
}

在这个例子中,Driver 类依赖于 Car 类,drive 方法使用了 Car 类的对象。

1.3.2、聚合关系(Aggregation)

聚合关系是一种"整体-部分"关系,一个类包含另一个类的实例,但这种关系并不表示强依赖。被包含的对象可以独立存在,而不会因为包含它的对象被销毁而销毁。

聚合关系通常使用成员变量来实现,一个类拥有另一个类的实例作为其成员变量。

示例:

/**
 * Engine 类表示引擎
 */
public class Engine {
    public void start() {
        System.out.println("Engine started.");
    }
}

/**
 * Car 类表示汽车
 * 聚合关系:Car 类聚合了 Engine 类
 */
public class Car {
    private Engine engine;

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

    public void startCar() {
        engine.start();
        System.out.println("Car started.");
    }
}

在这个例子中,Car 类聚合了 Engine 类,Car 对象包含一个 Engine 对象,但 Engine 对象可以独立存在。

1.3.3、继承关系(Inheritance)

继承关系是面向对象编程中的一种强依赖关系,它表示一个类是另一个类的子类。子类继承了父类的属性和方法,可以复用父类的代码,增加代码的可维护性和扩展性。

继承关系是一种"是一种"关系,子类是父类的一种特殊形式。

示例:

/**
 * Vehicle 类表示交通工具
 * 父类
 */
public class Vehicle {
    private String brand;

    public Vehicle(String brand) {
        this.brand = brand;
    }

    public String getBrand() {
        return brand;
    }
}

/**
 * Car 类表示汽车
 * 继承关系:Car 类继承了 Vehicle 类
 */
public class Car extends Vehicle {
    private String model;

    public Car(String brand, String model) {
        super(brand);
        this.model = model;
    }

    public String getModel() {
        return model;
    }

    public void startEngine() {
        System.out.println("The car engine is starting...");
    }
}

在这个例子中,Car 类继承了 Vehicle 类,Car 类不仅拥有 Vehicle 类的属性和方法,还可以定义自己的属性和方法。


2、构造器

理解构造器之前,首先我们需要了解 Java 中为什么要引入构造器,以及构造器的作用。在很久之前,程序员们编写 C 程序总会忘记初始化变量(这真的是一件琐碎但必须的事),因此后来 C++ 引入了构造器(constructor)的概念,这是一个在创建对象时被自动调用的特殊方法。Java 也采用了构造器。

构造器也被称为构造方法,是一种特殊的方法,调用构造方法可以创建新对象。构造方法可以执行任何操作,实际应用中,构造方法一般用于初始化操作,例如初始化对象的数据域。

构造函数与普通方法的主要区别如下:

  1. 名称:构造函数的名称必须与类名相同,而普通方法可以有任何有效的标识符作为名称;

  2. 返回类型:构造函数没有返回类型,而普通方法必须有返回类型;

  3. 调用方式:构造函数在创建对象时自动调用,无需手动调用。而普通方法需要手动调用;

  4. 用途:构造函数主要用于初始化对象的状态(即设置属性的初始值)。而普通方法用于描述对象的行为。

2.1、构造器的引入

构造器的定义:在定义构造器时,首先使用修饰符(如 publicprivate 等)来指定构造器的可见性,然后构造方法名必须与类名相同,最后是参数列表,可以为空也可以包含参数。构造器的主体包含在 {} 内,用于初始化对象。

修饰符 构造方法名 (参数列表) {}

引入构造器帮助我们解决了哪些问题呢?假设我们每定义一个类都必须定义一个 initialize() 方法,该方法提醒你,每次使用对象之前都要执行一次该方法,这意味着用户每次都必须记得自己去调用此方法,这和上文提到的 C 程序员一样,很容易就忘记了。Java 构造器的出现很好的规避掉了这种问题,创建对象时,java 会在使用对象之前调用相应的构造器,保证对象正确初始化。

首先,让我们看一下没有构造器时的情况:

public class Car {
    private String make;
    private String model;
    private int year;

    // initialize 方法,用于初始化对象
    public void initialize(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public void displayInfo() {
        System.out.println("Car make: " + make + ", model: " + model + ", year: " + year);
    }

    public static void main(String[] args) {
        Car car = new Car();
        // 必须手动调用 initialize 方法进行初始化
        car.initialize("Toyota", "Corolla", 2020);
        car.displayInfo();
    }
}

在这个例子中,我们必须手动调用 initialize 方法来初始化 Car 对象。如果忘记调用 initialize 方法,Car 对象的属性将保持默认值,这可能导致程序错误。

现在,让我们看一下使用构造器的情况:

public class Car {
    private String make;
    private String model;
    private int year;

    // 构造器,用于初始化对象
    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public void displayInfo() {
        System.out.println("Car make: " + make + ", model: " + model + ", year: " + year);
    }

    public static void main(String[] args) {
        // 创建对象时自动调用构造器进行初始化
        Car car = new Car("Toyota", "Corolla", 2020);
        car.displayInfo();
    }
}

在这个例子中,构造器 Car(String make, String model, int year) 在创建对象时自动调用,确保对象在使用前已被正确初始化。这样就不必担心忘记调用初始化方法,从而提高了代码的安全性和可维护性。

2.2、构造器的特点

构造器具有以下特点:

  1. 每一个类都必须有一个构造方法:如果自己不写,编译的时候,系统会给出默认构造方法。默认构造器(又名无参构造器)是没有形式参数的,它创建的是 “默认对象”;
  2. 构造器的命名必须与类名相同:这确保了构造器可以正确地识别和关联到类;
  3. 构造器没有返回类型:包括没有 void,也不需要写返回值。因为它是为构建对象的,对象创建完,方法就执行结束;
  4. 构造器可以有参数,可以重载:有默认无参构造,也有带参构造。为了满足不同的初始化需求,我们通常会需要定义多个带参构造器,由于都是构造器,它们的名称必须相同,为了让方法名相同而参数不同的方法存在,我们就必须使用方法重载,这是构造器所必须的;
  5. 构造器在创建对象时自动调用:而且只执行一次

以下是带有多个构造器的类示例:

public class Car {
    private String make;
    private String model;
    private int year;

    // 默认构造器
    public Car() {
        this.make = "Unknown";
        this.model = "Unknown";
        this.year = 0;
        System.out.println("默认构造器被调用");
    }

    // 带参构造器
    public Car(String make, String model) {
        this.make = make;
        this.model = model;
        this.year = 0;
        System.out.println("带参构造器(make, model)被调用");
    }

    // 带参构造器
    public Car(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
        System.out.println("带参构造器(make, model, year)被调用");
    }

    public void displayInfo() {
        System.out.println("Car make: " + make + ", model: " + model + ", year: " + year);
    }

    public static void main(String[] args) {
      	// 调用默认构造器
        Car car1 = new Car(); 
        car1.displayInfo();
				// 调用带参构造器(make, model)
        Car car2 = new Car("Toyota", "Corolla"); 
        car2.displayInfo();
				// 调用带参构造器(make, model, year)
        Car car3 = new Car("Honda", "Civic", 2022); 
        car3.displayInfo();
    }
}

在这个例子中,Car 类有三个构造器:一个默认构造器和两个带参构造器。通过实例化 Car 类,我们可以看到不同的构造器被调用,并初始化不同的对象属性。

3.3、子父类构造器(涉及继承)

在创建子类对象时,父类的构造方法会先执行,因为子类中所有构造方法的第一行有默认的隐式 super(); 语句,它是用来访问父类中的空参数构造方法,进行父类成员的初始化操作。

this() 是调用本类的构造方法,super() 是调用父类的构造方法,且两条语句不能同时存在。

子类构造方法时使用 super()this 的注意点:

  1. 子类的所有构造方法,直接或间接必须调用到父类构造方法。所以在 Java 中,每个构造方法都必须在其构造方法体的第一行显式或隐式地调用 super()this()
  2. super()this() 调用父类的构造方法,必须在构造方法的第一行。
  3. super()this() 不能同时被构造方法调用!(因为二者均要求放在第一行才行)。

此外,构造方法是可以被 private 修饰,作用是:其他程序无法创建该类的对象。而当父类使用 private 修饰构造方法时,子类将无法正常调用 super() 方法。

以下是一个示例,展示了如何在子类构造方法中使用 super()this()

class Parent {
    /** 父类的名字 */
    private String name;

    /** 
     * 父类无参构造方法 
     */
    public Parent() {
        this.name = "Default Parent";
        System.out.println("Parent class no-arg constructor called");
    }

    /** 
     * 父类带参构造方法 
     * @param name 父类的名字
     */
    public Parent(String name) {
        this.name = name;
        System.out.println("Parent class parameterized constructor called");
    }
}

class Child extends Parent {
    /** 子类的年龄 */
    private int age;

    /** 
     * 子类无参构造方法,调用父类的无参构造方法 
     */
    public Child() {
        // 默认调用 super();
        this.age = 0;
        System.out.println("Child class no-arg constructor called");
    }

    /** 
     * 子类带参构造方法,调用父类的带参构造方法 
     * @param name 子类的名字
     * @param age 子类的年龄
     */
    public Child(String name, int age) {
        super(name); // 调用父类的带参构造方法
        this.age = age;
        System.out.println("Child class parameterized constructor called");
    }

    /** 
     * 子类带单个参数的构造方法,调用本类的无参构造方法 
     * @param age 子类的年龄
     */
    public Child(int age) {
        this(); // 调用本类的无参构造方法
        this.age = age;
        System.out.println("Child class parameterized constructor (age) called");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建子类对象,调用不同的构造方法
        Child child1 = new Child();
        System.out.println();

        Child child2 = new Child("John", 10);
        System.out.println();

        Child child3 = new Child(20);
    }
}

运行结果:

Parent class no-arg constructor called
Child class no-arg constructor called

Parent class parameterized constructor called
Child class parameterized constructor called

Parent class no-arg constructor called
Child class no-arg constructor called
Child class parameterized constructor (age) called

在这个示例中,我们可以看到:

  1. 当调用 Child 类的无参构造方法时,隐式调用了父类的无参构造方法;
  2. 当调用 Child 类的带参构造方法时,显式调用了父类的带参构造方法;
  3. 当调用 Child 类的带有单个参数的构造方法时,显式调用了本类的无参构造方法,然后进行了额外的初始化。

3、static 关键字

static 关键字是 Java 中用于定义类成员(变量或方法)的关键字。使用 static 关键字修饰的成员属于类本身,而不是类的实例。static 关键字可以用于变量、方法、代码块和内部类。

  1. 静态变量:static 关键字用来声明独立于对象的静态变量,无论一个类实例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量。
  2. 静态方法:static 关键字用来声明独立于对象的静态方法。静态方法不能使用类的非静态变量。静态方法从参数列表得到数据,然后计算这些数据。
  3. 静态代码块:static 关键字还可以形成静态代码块以优化程序性能。static 代码块在类加载的时候就运行了,而且只运行一次,同时运行时机是在构造函数之前。

总的来说,static 关键字主要有以下几个作用:实现类的成员共享,节省内存;优化程序性能,提高运行效率。作为工具类的方法修饰符,方便调用。

3.1、static 修饰变量

static 变量,也称为类变量或静态变量,是属于类本身的变量。与实例字段的,在每个实例中都有自己的一个独立 “空间” 不同,静态字段只有一个共享 “空间”,而所有实例都会共享该字段。当一个实例修改该变量时,所有其他实例都可以看到修改后的值。

虽然实例可以访问静态字段,但是它们指向的其实都是Person class的静态字段。所以,所有实例共享一个静态字段。

因此,不推荐用 实例变量.静态字段 去访问静态字段,因为在 Java 程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为 类名.静态字段 来访问静态对象。推荐用类名来访问静态字段

示例:

public class Counter {
    /** 静态变量 count */
    private static int count = 0;

    public Counter() {
        count++;
    }

    public static int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Counter c3 = new Counter();
        
        // 打印出 3,因为创建了三个 Counter 实例
        System.out.println("Count: " + Counter.getCount());
    }
}

在这个例子中,count 是一个 static 变量,所有 Counter 实例共享同一个 count 变量。每次创建 Counter 实例时,count 变量都会增加。

3.2、static 修饰方法

static 方法是属于类本身的方法,可以直接通过类名调用,而不需要创建类的实例。static 方法不能访问实例变量和实例方法,但可以访问静态变量和静态方法。

示例:

public class MathUtils {
    /** 静态方法 add */
    public static int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        // 通过类名直接调用静态方法
        int sum = MathUtils.add(5, 3);
        System.out.println("Sum: " + sum);
    }
}

在这个例子中,add 是一个 static 方法,可以通过类名 MathUtils 直接调用,而不需要创建 MathUtils 的实例。

3.3、static 代码块

static 代码块用于初始化类级别的资源,它在类加载时执行,并且只执行一次。static 代码块通常用于初始化静态变量。

示例:

public class Configuration {
    /** 静态变量 config */
    private static String config;

    /** 静态代码块 */
    static {
        config = "Default Configuration";
        System.out.println("Static block executed");
    }

    public static String getConfig() {
        return config;
    }

    public static void main(String[] args) {
        // 静态代码块已在类加载时执行
        System.out.println("Config: " + Configuration.getConfig());
    }
}

在这个例子中,静态代码块在类加载时执行,初始化静态变量 config

3.4、构造方法与代码块执行顺序

在 Java 中,类的静态代码块、非静态代码块和构造方法有特定的执行顺序。理解这些执行顺序有助于我们更好地掌握类的初始化过程,确保代码按预期运行。

当一个类的实例被创建时,执行顺序如下:

父类B静态代码块 > 子类A静态代码块 > 父类B非静态代码块 > 父类B构造函数 > 子类A非静态代码块 > 子类A构造函数

为了更好地理解这种顺序,我们来看一个示例:

class Parent {
    /** 父类静态代码块 */
    static {
        System.out.println("父类 B 静态代码块");
    }

    /** 父类非静态代码块 */
    {
        System.out.println("父类 B 非静态代码块");
    }

    /** 父类构造方法 */
    public Parent() {
        System.out.println("父类 B 构造方法");
    }
}

class Child extends Parent {
    /** 子类静态代码块 */
    static {
        System.out.println("子类 A 静态代码块");
    }

    /** 子类非静态代码块 */
    {
        System.out.println("子类 A 非静态代码块");
    }

    /** 子类构造方法 */
    public Child() {
        System.out.println("子类 A 构造方法");
    }

    public static void main(String[] args) {
        System.out.println("创建子类 A 的实例:");
        new Child();
    }
}

运行结果:

父类 B 静态代码块
子类 A 静态代码块
创建子类 A 的实例:
父类 B 非静态代码块
父类 B 构造方法
子类 A 非静态代码块
子类 A 构造方法

从输出结果可以看出执行顺序如下:

  1. 父类静态代码块:首先执行父类的静态代码块。静态代码块只在类加载时执行一次;
  2. 子类静态代码块:接着执行子类的静态代码块。静态代码块只在类加载时执行一次;
  3. 父类非静态代码块:然后执行父类的非静态代码块。非静态代码块在每次创建对象时都会执行;
  4. 父类构造方法:在父类的非静态代码块之后,执行父类的构造方法;
  5. 子类非静态代码块:接下来执行子类的非静态代码块。非静态代码块在每次创建对象时都会执行;
  6. 子类构造方法:最后执行子类的构造方法。
3.5、static 内部类(涉及内部类)

static 内部类是使用 static 关键字修饰的内部类。静态内部类不能访问外部类的非静态成员,但可以访问外部类的静态成员。

示例:

public class OuterClass {
    private static String staticOuterField = "Static Outer field";

    /** 静态内部类 */
    public static class StaticInnerClass {
        public void display() {
            System.out.println("Static Outer field: " + staticOuterField);
        }
    }

    public static void main(String[] args) {
        OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass();
        staticInner.display();
    }
}

在这个例子中,StaticInnerClassOuterClass 的静态内部类,可以访问外部类的静态成员 staticOuterField


4、final 关键字

final 关键字是 Java 中的一个修饰符,用于表示最终、不可变的含义。final 关键字可以修饰变量、方法和类,每种用法都有其特定的意义和作用。

4.1、final 变量

在变量层面,final 关键字用于声明常量,一旦被赋值,就无法再修改。这有助于提高代码的可读性和可维护性,同时也避免了一些潜在的 Bug。

final 变量可以是基本类型或引用类型,对于引用类型,final 表示引用本身不可改变,但引用的对象内容可以改变。

示例:

public class Constants {
    /** 定义常量 */
    public static final int MAX_VALUE = 100;

    public static void main(String[] args) {
        // MAX_VALUE = 200; // 编译错误,final 变量不能被重新赋值
        System.out.println("Max value: " + MAX_VALUE);
    }
}

在这个例子中,MAX_VALUE 是一个 final 变量,表示常量,其值不能被改变。

4.2、final 方法

在方法级别,final 关键字表示该方法不能被子类重写。这对于确保某些方法的逻辑不被修改是非常有用的,尤其是一些关键的算法或者安全性相关的方法,这在设计一个类的 API 时非常有用,可以防止子类改变父类中的关键行为。

示例:

public class Parent {
    /** final 方法 */
    public final void show() {
        System.out.println("This is a final method.");
    }
}

public class Child extends Parent {
    // 试图重写 final 方法会导致编译错误
    // public void show() {
    //     System.out.println("Cannot override final method.");
    // }
}

在这个例子中,show 方法被声明为 final,因此不能在 Child 类中被重写。

4.3、final

当我们使用 final 修饰一个类时,意味着这个类不能被继承,也就是说,它是一个终结类,不允许其他类再来继承它。这样做的好处是防止其他类修改或扩展该类,保护了类的完整性。

示例:

public final class ImmutableClass {
    private final int value;

    public ImmutableClass(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

// 试图继承 final 类会导致编译错误
// public class SubClass extends ImmutableClass {
// }

在这个例子中,ImmutableClass 是一个 final 类,不能被继承。

总的来说,final 关键字的作用是为了让我们的代码更加稳定和可靠,避免不必要的修改和继承。当你看到某个类、方法或者变量被标记为 final 时,就知道它是不可变的,可以更加放心地使用。


5、Java 包

Java 包(Package)是用于组织类和接口的一种机制,它提供了命名空间来管理类和接口,避免命名冲突,并且可以控制访问权限。通过使用包,可以将相关的类和接口组织在一起,形成模块化的代码结构,便于维护和管理。

5.1、包名

Java 允许使用包将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。使用包的主要原因是确保类名的唯一性。同名的类放置在不同的包中,就不会产生冲突。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。

示例:

package com.example.myapp;

在这个例子中,com.example.myapp 是包名。通常,包名使用小写字母,以便区分类名。

为了保证包名的绝对唯一性,Sun 公司建议将公司的因特网域名以逆序的形式作为包名,并且对于不同的项目使用不同的子包。所以,我们经常在类库中看到一堆包名像这样的:com.sunorg.apache 等等。

5.2、import 的用法

import 语句用于引入其他包中的类或接口,使得在当前类中可以直接使用它们。可以导入具体的类,也可以使用通配符 * 导入整个包。

示例:

// 导入具体的类
import java.util.ArrayList;

// 导入整个包
import java.util.*;

在这个例子中,ArrayList 类被导入,可以在代码中直接使用。同时,使用 * 可以导入 java.util 包中的所有类和接口。

一个完整的类名是「包名+类名」,在没有 import 导入的情况下,使用一个类需要给出完整的类名,如 java.util.Date。为了方便,Java 自动导入两个包:java.lang 包和默认包。

注意,从编译器的角度来看,嵌套的包之间没有任何关系、例如,java.util 包和 java.util.jar 包毫无关系。每一个都拥有独立的类集合。

还有,只要使用星号(*)来导入一个包,而不能使用 import java.* 来导入以 java 为前缀的所有包。而且,如果要同时使用两个类名相同的类,只能在使用的时候给出类的完整包名。

在包中定位类是编译器的工作。因此,.class 文件中的字节码是使用完整的包名来引用其他的类。这样 Jvm 就会直接根据这个包名来找到对应的 .class 文件。

5.3、定义包

在 Java 中,通过在源文件的第一行使用 package 语句来定义包。包名与目录结构对应,编译器根据包名将类文件放在相应的目录中。

示例:

package com.example.myapp;

public class MyClass {
    public void display() {
        System.out.println("Hello from MyClass in package com.example.myapp");
    }
}

假设源文件存放在 src/com/example/myapp/MyClass.java,编译后的类文件将存放在 bin/com/example/myapp/MyClass.class

5.4、类路径

类路径(Classpath)是 Java 虚拟机和编译器用来寻找类文件的路径。可以通过设置类路径,使得 JVM 和编译器可以找到包和类。

设置类路径的方法有多种:

  1. 通过命令行参数设置:
javac -cp path/to/classes com/example/myapp/MyClass.java
java -cp path/to/classes com.example.myapp.MyClass
  1. 通过环境变量 CLASSPATH 设置:
export CLASSPATH=path/to/classes
  1. 通过 manifest 文件设置:在 JAR 文件的 META-INF/MANIFEST.MF 文件中添加类路径。

示例:

# 编译并运行 MyClass
javac -d bin src/com/example/myapp/MyClass.java
java -cp bin com.example.myapp.MyClass

在这个例子中,javac 命令使用 -d 选项将编译后的类文件放在 bin 目录中,java 命令使用 -cp 选项设置类路径为 bin 目录,然后运行 MyClass


6、JAR 文件

Java Archive (JAR) 文件是用于打包多个 Java 类、元数据和资源(如图像、音频文件等)的压缩文件格式。JAR 文件使用 ZIP 文件格式进行打包,可以包含多个文件和目录结构。JAR 文件的主要用途是分发和部署 Java 应用程序和库。

6.1、创建 JAR 文件

要创建一个 JAR 文件,可以使用 jar 命令行工具。假设我们有以下目录结构:

myapp/
├── com/
│   └── example/
│       └── MyApp.class
└── resources/
    └── config.properties

要创建一个包含 com.example.MyApp 类和 resources/config.properties 文件的 JAR 文件,可以使用以下命令:

jar cvf myapp.jar -C myapp .

参数说明:

  • c:创建新的 JAR 文件。
  • v:生成详细输出。
  • f:指定 JAR 文件名。
  • -C:切换到指定目录并包含目录内容。

此命令会在当前目录下创建一个名为 myapp.jar 的 JAR 文件,包含 myapp 目录中的所有文件和目录结构。

6.2、查看 JAR 文件内容

可以使用 jar 命令查看 JAR 文件的内容:

jar tf myapp.jar

此命令会列出 JAR 文件中的所有文件和目录。

6.3、运行 JAR 文件

要运行一个包含主类(包含 main 方法)的 JAR 文件,需要在创建 JAR 文件时指定主类。在创建 JAR 文件时,可以使用 -e 参数指定主类:

jar cvfe myapp.jar com.example.MyApp -C myapp .

参数说明:

  • e:指定主类。

然后,可以使用 java -jar 命令运行 JAR 文件:

java -jar myapp.jar
6.4、MANIFEST.MF 文件

JAR 文件中包含一个特殊的文件 META-INF/MANIFEST.MF,用于存储关于 JAR 文件的元数据。MANIFEST.MF 文件可以包含以下信息:

  • Manifest-Version:清单文件的版本。
  • Main-Class:指定 JAR 文件的主类。
  • Class-Path:指定依赖的 JAR 文件路径。

示例 MANIFEST.MF 文件内容:

Manifest-Version: 1.0
Main-Class: com.example.MyApp
Class-Path: lib/dependency.jar

可以手动创建或编辑 MANIFEST.MF 文件,然后在创建 JAR 文件时将其包含在内:

jar cvfm myapp.jar MANIFEST.MF -C myapp .
6.5、解压 JAR 文件

可以使用 jar 命令解压 JAR 文件:

jar xvf myapp.jar

此命令会将 JAR 文件中的所有文件解压到当前目录。

06-11 11:02