Java的面向对象基础

叠甲:以下文章主要是依靠我的实际编码学习中总结出来的经验之谈,求逻辑自洽,不能百分百保证正确,有错误、未定义、不合适的内容请尽情指出!

概要:…

资料:…


1.面向过程和面向对象

面向过程编程(Procedural Programming)和面向对象编程(Object-Oriented Programming,OOP)是两种不同的编程范式,Java 几乎把面向对象的思想贯彻到了极致,因此学习 Java 编程的过程中,对于 Cpp 面向对象的理解也会更加深刻。

吐槽:Cpp 本身是面向过程、面向对象、面向泛型的庞大语言。

2.访问限定符

Java 主要通过类的访问权限来实现访问权限的控制(这点和 Cpp 一样),将数据和封装数据的方法结合在一起,更符合人类对事物的认知。

而访问权限用来控制方法或字段能否直接在类外使用,并且 Java 还可以把访问限定符作用在整个类上(这点 Cpp 是没有的),这点我们后面补充…先姑且认定当前和 Cpp 的用法是一样的:限制成员变量和成员方法的访问。

范围private(私有)default(默认的权限)protected(继承内多用)public(公有)
同包的同类yesyesyesyes
同包的异类yesyesyes
异包内子类yesyes
异包非子类yes

先看下面的知识叭,补充完对 Java 类的一些理解就可以看懂上面访问限定符的使用了。

3.类和对象基础

3.1.类的定义

类描述一系列的对象,而一个类的声明如下:

// 定义类的语法形式
[类修饰符] class 类名 {
    // 一些属性/成员变量...
    // 一些方法/成员方法...
}

Java 的类和 C 语言的结构体很类似,和 Cppclass 几乎一致,和 Cpp 一样可以类中加入方法(方法可以简单理解为函数),方法需要依赖对象才能被调用。

关于类,我们需要注意下面几点:

  1. 一般一个 Java 文件内部只会存在一个类(也就是一一对应,但是我们在学习过程中可以放在一个文件中)

  2. main() 方法所在的类一般要使用 public 修饰(默认会在 public 修饰的类中寻找 main() 方法)

  3. 如果一个类有 public 修饰,那么请不要直接手动修改这个类的类名(我们可以通过开发工具修改,这样才会让所有使用该类名的代码都进行同步修改,这也是 IDEA 强大功能的一处体现)

    在这里插入图片描述

区别:Java 的类和 Cpp 的类最大区别在于默认自带的类成员不太一样,并且 Cpp 无法把访问限定符直接作用在类上,如果希望一个类不被外部访问创建,可以考虑使用友元类、限制构造函数、限制成员变量等做法来简介实现。因此从编写难度上来看 Java 更加直观且简洁。

3.2.类的对象

通过类描述这张“图纸”,可以通过 new 来实例化出多个对象。虽然一个 Java 文件内部只会存在一个类,但我们也可以尝试写到一起试试(这没什么错误,只是不太推荐而已)。

// 尝试描述一个类然后定义一个类对象
// 描述对象的 Person 类
class Person {
    //成员变量
    public String name; // 名字
    public int age; // 年龄

    //成员方法
    public String GetName() { // 获取名字
        return name;
    }
    public int GetAge() { // 获取年龄
        return age;
    }
}

// 主类的主函数
public class Main {
    public static void main(String[] args) {
        Person per = new Person(); //使用 new 语法创建(实例化对象)
        per.name = "limou3434";
        per.age = 18;

        System.out.println("姓名: " + per.GetName());
        System.out.println("年龄: " + per.GetAge());
    }
}

/* 输出结果
姓名: limou3434
年龄: 18
*/

再换成一个类一个文件的方式写,在同一个 Java 项目中有如下两份文件:

// Person.java
// Person 类
class Person {
    // 成员变量
    public String name; // 名字
    public int age; // 年龄

    // 成员方法
    public String GetName() { // 获取名字
        return name;
    }
    public int GetAge() { // 获取年龄
        return age;
    }
}
// Main.java
// Main 类
// 主类内使用 Person 对象
public class Main {
    public static void main(String[] args) {
        Person per = new Person(); // 使用 new 语法创建
        per.name = "limou3434";
        per.age = 18;

        System.out.println("姓名:" + per.GetName());
        System.out.println("年龄:" + per.GetAge());
    }
}

上述的内部成员如果我们自己没有初始化,Java 也是会自己初始化的(内置类型为零值,引用类型为 nullbooleanfalsechar\u0000 等)。

补充:对象内部是不存储方法的,只有在使用方法时才会在栈上开辟空间,而成员变量都存储在堆空间上。

补充:统一一下术语,在本系列中会频繁出现的一些和 Cpp 有些相似但是又不同的概念。Cpp 喜欢称呼“成员函数,成员变量”,而 Java 喜欢称呼“方法,属性”。

3.3.this 引用

3.3.1.this.成员变量

this 引用来源于 C++this 指针,两者有所区别,但是很是类似,首先我们来看一个奇怪的现象。

// 没问题的代码
class Data {
    public int _year;
    public int _month;
    public int _day;

    public void setData(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    public void print() {
        System.out.println(_year + " " + _month + " " + _day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data day1 = new Data();
        day1.setData(2024, 1, 21);
        day1.print();

        Data day2 = new Data();
        day2.setData(2023, 2, 23);
        day2.print();
    }
}

/* 输出结果
2024 1 21
2023 2 23
*/

而下面代码中的 SetData() 如果是在 Cpp 中才可以正确运行,但在 Java 中会出现问题。

// 有问题的代码
class Data {
    public int year;
    public int month;
    public int day;

    public void setData(int year, int month, int day) {
        year = year;
        month = month;
        day = day;
    }

    public void print() {
        System.out.println(year + " " + month + " " + day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data day1 = new Data();
        day1.setData(2024, 1, 21);
        day1.print();

        Data day2 = new Data();
        day2.setData(2023, 2, 23);
        day2.print();

        // Java 怎么知道使用 setData() 后初始化的是 day1 的内部成员而不是 day2 的内部成员呢?
        // 答案是使用了 this 引用
    }
}

/* 输出结果
0 0 0
0 0 0
*/

Java 会认为是局部变量自己给自己赋值(根本不会影响成员变量的取值),因此打印的还是 Java 给变量的默认初始值。但是如果我们使用 this 引用就可以指明赋值关系,这样就不会出现问题。

// 使用 this 引用
class Data {
    public int year;
    public int month;
    public int day;

    public void SetData(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public void Print() {
        System.out.println(year + " " + month + " " + day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data day1 = new Data();
        day1.SetData(2024, 1, 21);
        day1.Print();

        Data day2 = new Data();
        day2.SetData(2023, 2, 23);
        day2.Print();
    }
}

/* 输出结果
2024 1 21
2023 2 23
*/

而这个 this 引用实际上就是调用方法的哪个对象,方法被哪个对象调用了,this 就引用的哪个对象。

第一个代码中,Java 会自动识别类内成员,给其加上 this.。而第二个代码中,由于形参的影响,Java 无法识别哪一个是成员变量,干脆都理解为局部变量,交给用户使用 this 去指定(但 Cpp 可以识别出这种特殊情况,不过我们仍旧不推荐这么书写,在 Cpp 要么使用 this,要么给成员变量加上一个 _ 前缀和形参做区分)。

另外,这也能解释为什么使用 setData()print() 可以明确对哪一个具体的对象做操作,每一个方法的第一个参数实际上就是 this。这是由 Java 自动传递的,我们无需手动给函数传递,只需要在函数内使用即可。

此外我们需要注意,this 引用只能在方法定义内被使用,且只能引用当前对象,不能被修改然后再引用其他对象。

注意:我们一般建议将 this 明确写上,不过度依赖 Java 的默认行为,避免造成误解。

3.3.2.this.成员方法

除了调用对象内的成员变量,还可以调用对象对应类内对应的成员方法(当然也可以不加 this. 也会有类似成员变量一样的自动识别,我还是推荐您加上)。

// 使用 "this.成员方法" 来调用方法
class Data {
    public int _year;
    public int _month;
    public int _day;

    public void setData(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
        this.Print(); // 设定好值后就打印
    }

    public void print() {
        System.out.println(_year + " " + _month + " " + _day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data day1 = new Data();
        day1.setData(2024, 1, 21);

        Data day2 = new Data();
        day2.setData(2023, 2, 23);
    }
}

/* 输出结果
2024 1 21
2023 2 23
*/

3.4.构造方法

我们在定义类的变量时,可以发现,即便我们没有定义类内部成员变量的初始值,Java 会帮助我们自动初始化。但是如果不是在类中,而是直接写在 Main() 中的局部变量则没有进行初始化,代码可能连编译都无法通过。

// 编译通过的代码
class Data {
    public int _year;
    public int _month;
    public int _day;

    public void setData(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
        this.Print(); // 设定好值后就打印
    }

    public void print() {
        System.out.println(_year + " " + _month + " " + _day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data d = new Data();
        // 忘记调用 d.setData() 了
        d.print();
    }
}

/* 输出结果
0 0 0
*/
// 编译失败的代码
public class Main {
    public static void main(String[] args) {
        int year;
        int month;
        int day;
        System.out.println(year + " " + month + " " + day); // 报错
    }
}

/* 输出结果
java: 可能尚未初始化变量year
*/

那究竟是谁帮助我们对类内的成员变量进行了初始化呢?Java 么?是的,但是不够深层太过笼统。准确来说,是类内默认的成员方法:构造方法,该方法没有返回值,方法名和类名一样。

默认的构造方法会给成员变量赋予初始值,而当我们撰写任何自定义的构造方法时,Java 就不会调用编译器提供的默认构造方法(这点和 Cpp 是一样的),而是尝试直接调用我们自己自定义的构造方法(此时一旦失败就会报错,需要把有可能出现的构造方法都写出来)。并且,构造方法只在实例化对象的时候才会被自动调用。

// 自定义构造方法
class Data {
    public int _year;
    public int _month;
    public int _day;

    Data() {
        System.out.println("不带参数的构造方法");
    }
    
    Data(int year, int month, int day) {
        System.out.println("带参数的构造方法");
        this._year = year;
        this._month = month;
        this._day = day;
    }

    public void setData(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
        this.Print(); // 设定好值后就打印
    }

    public void print() {
        System.out.println(_year + " " + _month + " " + _day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data d1 = new Data();
        d1.Print();

        Data d2 = new Data(2021, 1, 2);
        d2.print();
        d2.setData(2024, 10, 9);
    }
}

/* 输出结果
不带参数的构造方法
0 0 0
带参数的构造方法
2021 1 2
2024 10 9
*/

除此以外,还以一种初始化方法叫做 就地初始化,直接在类内成员变量进行初始化,这种方式适用于一些具有默认值的成员变量,但是不具备一般性。用户一旦调用构造方法,就会被携带一个初始值,但这并不一定是符合用户意愿的(就地初始化的值会被传递到构造方法中,这点和 Cpp 类似)。

// 自定义构造方法
class Data {
    public int _year = 1000; // 就地初始化
    public int _month = 1; // 就地初始化
    public int _day = 1; // 就地初始化

    Data() {
        System.out.println("不带参数的构造方法");
    }
    
    Data(int year, int month, int day) {
        System.out.println("带参数的构造方法");
        this._year = year;
        this._month = month;
        this._day = day;
    }

    public void setData(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
        this.print(); // 设定好值后就打印
    }

    public void print() {
        System.out.println(_year + " " + _month + " " + _day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data d1 = new Data();
        d1.print();

        Data d2 = new Data(2021, 1, 2);
        d2.print();
        d2.setData(2024, 10, 9);
    }
}

/* 输出结果
不带参数的构造方法
1000 1 1
带参数的构造方法
2021 1 2
2024 10 9
*/

另外,还有一个 this() 构造方法,这个方法可以做到在一个构造方法内调用类内其他重载的构造函数(但必须是在构造函数方法内的第一条语句内调用,且不能被多次调用)。

// 使用 this()
class Data {
    public int _year = 0; // 就地初始化
    public int _month = 0; // 就地初始化
    public int _day = 0; // 就地初始化

    Data() {
        this(2000, 1, 1); // 调用其他重载的构造方法
        System.out.println("不带参数的构造方法");
    }
    
    Data(int year, int month, int day) {
        System.out.println("带参数的构造方法");
        this._year = year;
        this._month = month;
        this._day = day;
    }

    public void SetData(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
        this.Print(); // 设定好值后就打印
    }

    public void Print() {
        System.out.println(_year + " " + _month + " " + _day);
    }
}

public class Main {
    public static void main(String[] args) {
        Data d = new Data();
        d.Print();
    }
}

/* 输出结果
带参数的构造方法
不带参数的构造方法
2000 1 1
*/

构造方法一般使用 public,只有在某些情况(单例模式)才会使用 provate 来修饰(下面会将这两个关键字的作用)。

补充:另外,构造函数实际上做的工作还有很多

  1. 初始化分配的对象(也就是我们上面学的)
  2. 为对象分配内存空间(这个我们无需理会)
  3. 处理并发安全问题(以后提及)
  4. 检测对象对应的类是否加载(以后提及)

后续学习中将会逐步补充…

3.面向对象特性

面向对象的三个常见特性就是:封装、继承、多态,我们先来理解最容易理解的封装。

3.1.封装

3.1.1.类的本身

Java 本身使用类组织属性和方法时,本就是在做一个封装的过程。

3.1.2.包的使用

3.1.2.1.包的概念

在面向对象体系中,有一个软件包的概念,为了更好的管理类,把多个类收集在一起成为一组,就称为 软件包。因此 Java 也引入了包的概念,包实质上是一种高级封装的体现。

补充在一个工程中允许存在同名类,只要处于不同的包内即可。

3.1.2.2.包的导入

Java 存在很多的包,可以直接在代码中引入,也可以使用 import 语句来导入包。

// 代码中使用包
public class Main {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date(); // 代码中导入包内类的方法
        System.out.println(date.getTime()); // 返回一个时间戳
    }
}
// 使用 import 导入包
import java.util.Date;
public class Main {
    public static void main(String[] args) {
        Date date = new Date(); // 代码中导入包内类的方法
        System.out.println(date.getTime()); // 返回一个时间戳
    }
}

这里 java.util.Datejava.util 就是包,Date 就是包内的类,对应一个 Java 文件。

补充:如果使用 * 代替 Date 就可以看到 java.util 中的所有类,不过一般只推荐显示写一个类,否则有可能出现命名冲突的问题。

// 不同包内同名称类冲突的问题
import java.util.*;
import java.sql.*;
public class Main {
    public static void main(String[] args) {
        // util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, Java 不知道该使用哪一个类, 编译出错
        // Date date = new Date();
        // System.out.println(date.getTime());
    }
}

另外,还可以使用 import static 导入包中静态的成员变量和成员方法。

// 使用静态导入
import static java.lang.Math.*;
public class Main {
    public static void main(String[] args) {
        double x = 3;
        double y = 4;

        double ret = sqrt(pow(x, 2) + pow(y, 2)); //计算 √(30^2 + 50^2)
        System.out.println(ret);
    }
}

/* 输出结果
5.0
*/

注意:importC/C++#include 不是同一种东西,C/C++ 使用 #include 会在编译期间将整个头文件复制下来,再使用链接器链接。而 Javaimport 仅仅是为了编写代码时更加方便,因此更类似 C/C++namespaceusing 的使用。

因此即使不使用import,您仍然可以使用 Java 中的类和方法,但每次使用时都需要指定完整的类名,包括包名。

3.1.2.3.包的制作
// 描述对象的 Person 类
public class Person {
    // 描述对象的 Person 类
    // (1)成员变量
    public String name; // 名字
    public int age; // 年龄

    // (2)成员方法
    public String GetName() { //获取名字
        return name;
    }
    public int GetAge() { //获取年龄
        return age;
    }
}

对于上面类,如何成为一个供人使用的包呢?或者说,我们怎么自定义一个包呢?按照下面步骤来操作:

  1. 要在类的最上方加上 package 包名 语句,一般使用公司域名的颠倒形式(com.limou.blog),包名尽量是唯一的

  2. IDEA 中右键 src 文件夹,在 new 中选择 Package,在弹出的对话框中输入包名

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

  3. 右击包,选择 New 中的 Java Class 输入类名

    在这里插入图片描述

    在这里插入图片描述

  4. 在新出现的 java 文件中粘贴代码即可

    在这里插入图片描述

  5. main() 中调用即可

    // 在主类的 main() 中正常调用
    import com.limou.blog.Person;
    // 主类的主函数
    public class Main {
        public static void main(String[] args) {
            Person per = new Person(); // 使用 new 语法创建(实例化对象)
            per.name = "limou3434";
            per.age = 18;
    
            System.out.println("姓名:" + per.GetName());
            System.out.println("年龄:" + per.GetAge());
        }
    }
    

补充:如果同一个域名不同包呢?

  1. 如果 example.com 域名下有两个项目分别是 project1project2,那么它们的包结构可能如下所示:

    复制代码com.example.project1
    com.example.project2
    
  2. 如果 project1 项目包含了多个模块,如 module1module2,那么它们的包结构可能如下所示:

    复制代码com.example.project1.module1
    com.example.project1.module2
    

通过这种方式,可以在同一个域名下为不同的项目和模块创建独立的命名空间,避免命名冲突,并且能够更清晰地组织和管理代码。

3.2.继承

吐槽:实际上继承的目的更多是为了后续的多态,而不是为了复用代码。

Java 继承允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法,并且没有类似“公有继承、私有继承…”的说法(这点和 Cpp 的不同,Cpp 会使用限定符做到多样的继承方式)。

并且还允许子类对父类方法进行 重定/隐藏,其条件是父子类各自拥有一个标识符相同的属性或方法。我更喜欢 隐藏 这个说法,这意味着父类的属性和方法在子类中依旧存在,只不过需要使用 super 来显式进行访问而已。

// 尝试使用继承
// 定义一个父类
class Animal {
    // 父类的属性
    String name;

    // 父类的构造
    public Animal(String name) {
        this.name = name;
    }

    // 父类的方法
    public void eat() {
        System.out.println(name + " eat " + "food");
    }
}

// 定义一个子类
class Dog extends Animal {
    // 子类的属性
    String breed;

    // 子类的构造
    public Dog(String name, String breed) {
        super(name); // 先调用父类的构造方法(内部自动调用父类构造, 不过仅限于提供的默认构造, 也可以显示调用, 因此最好是都使用 super() 进行显式调用)
        this.breed = breed; // 然后构造自己的属性
    }

    // 子类的方法
    public void bark() {
        System.out.println("bark bark bark bark~");
    }

    // 子类也可以选择 "重写/覆盖" 父类的方法
    public void eat(String food) {
        System.out.print(breed + " " + super.name + " are animals, too. ");
        super.eat(); // 但是依旧可以调用父类的方法, 这个方法使用的是子类继承自父类的成员
    }
}

// 主类包含 main 方法, 程序的入口点
public class Main {
    public static void main(String[] args) {
        // 创建 Animal 类的实例
        Animal animal = new Animal("animal");
        animal.eat();

        // 创建 Dog 类的实例
        Dog dog = new Dog("dog", "black");
        dog.eat("apple");
        dog.bark();
    }
}

在这个例子中,Animal 类是父类,它有一个属性 name 和一个方法 eat()Dog 类是子类,它继承了 Animal 类,并添加了自己的属性 breed 和特有的方法 bark()。同时,Dog 类重写了 eat() 方法,以展示如何使用继承并添加或修改行为。

当你运行这个程序时,它会创建 Animal 类和 Dog 类的实例,并调用它们的方法来演示继承的效果。

3.3.多态

Java 多态允许父类类型接受子类对象,并且使用父类的方法时,程序运行时会根据对象的实际子类型来调用相应的方法,并且一个方法默认允许多态(这点和 Cpp 不同,Cpp 需要使用 virtual 关键字允许方法进行多态)。

并且还允许子类对父类方法进行 重写/覆盖,其条件是父子类拥有签名完全相同的方法(除去协变的情况)。我更喜欢 覆盖 这个说法,在 Cpp 对应多态的实现中,多态的实现就是在调用时函数指针的替换,因此使用覆盖会对 Cpper 会更友好一些。

// 尝试使用多态
// 定义一个父类
class Animal {
    void makeSound() {
        System.out.println("Some generic sound");
    }
}

// 定义一个子类
class Dog extends Animal {
    void makeSound() {
        System.out.println("Bark"); // 覆盖
    }
}

// 定义一个子类
class Cat extends Animal {
    void makeSound() {
        System.out.println("Meow"); // 覆盖
    }
}

// 测试多态性的类
public class Main {
    public static void main(String[] args) {
        // 定义一个父类类型的引用,指向子类的对象
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        // 调用多态方法
        myDog.makeSound(); // 输出: Bark
        myCat.makeSound(); // 输出: Meow

        // 多态允许我们使用父类引用调用子类覆盖的方法
    }
}

补充:重载其实也算一种特殊的多态,也是一种调用对应多种实现,这种多态也叫编译时多态。而我们上面提到的多态,更多发生于继承之中,是运行时多态。

4.static 关键字

4.1.静态属性

同一个类类型实例化出不同的对象,内部属性有可能是不一样的,但是可能存在某个属性是所有对象的公有属性(例如:不同学生对象都是学生类创建出来的,但是都同属于一个学校)。

而我们一般不会让每个对象都存储这个公有属性,一是会出现数据冗余,二是不易修改(如果要对学生的学校名称进行重命名,则每一个学生的学校名称属性都需要被修改)。

因此就诞生了 static 修饰的成员变量,也被称为“静态成员变量”(只是沿用 C/Cpp 的说法本身没有什么含义),该成员不属于任何一个具体的对象,是被所有对象共享的,也是单属于类的属性。

既可以通过类名来访问,也可以使用对象来访问,但是一般推荐使用类名(语义更好)。

// 尝试使用 static 关键字
class Student {
    public String name;
    public String gender;
    public int age;
    public double score;
    public static String school = "limou school";

    public void Init(String name, String gender, int age, double score) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.score = score;
    }
}

public class Main {
    public static void main(String[] args) {
        // 静态成员变量可以直接通过类名访问
        System.out.println(Student.school);
        Student s1 = new Student();
        Student s2 = new Student();
        Student s3 = new Student();
        s1.Init("Li leilei", "男", 18, 3.8);
        s2.Init("Han MeiMei", "女", 19, 4.0);
        s3.Init("Jim", "男", 18, 2.6);
        // 也可以通过对象访问:但是 school 是三个对象共享的
        System.out.println(s1.school);
        System.out.println(s2.school);
        System.out.println(s3.school);
        s1.school = "dimou school"; // 任意的成员尝试进行修改
        System.out.println(Student.school); // 可以发现的确受到了影响
    }
}

4.2.静态方法

实际上,除了静态成员变量,还有静态成员方法,也是在函数的前面加上 static 关键字,同理,该方法也是类的方法,不是某个对象持有的。静态方法和 Cpp 的做法类似,本身会缺失一个隐含参数 this 引用,因此无法访问类内的其他非静态成员,但能够访问所有的静态成员。

// 尝试使用静态方法
class Student {
    public String name;
    public String gender;
    public int age;
    public double score;
    public static String school = "limou school"; //就地初始化静态变量

    public void Init(String name, String gender, int age, double score) {
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.score = score;
    }

    public static void ShowSchool() {
        System.out.println(Student.school);
    }
}

public class Main {
    public static void main(String[] args) {
        // 静态成员变量可以直接通过类名访问
        System.out.println(Student.school);
        Student s1 = new Student();
        Student s2 = new Student();
        Student s3 = new Student();
        s1.Init("Li leilei", "男", 18, 3.8);
        s2.Init("Han MeiMei", "女", 19, 4.0);
        s3.Init("Jim", "男", 18, 2.6);
        // 也可以通过对象访问:但是 school 是三个对象共享的
        Student.ShowSchool();
    }
}

不过,就算不是静态成员方法,方法也同样不是某个对象所持有的,那为什么需要静态成员方法呢?原因有下:

  • 可以省略创建对象的过程,直接使用类名来使用静态函数,使得代码更加整洁。因此适用于一些不需要依赖具体对象的方法,使用起来更加符合语义(无需依赖类对象就可以被调用,这样就不会因为类对象造成代码混淆),更适合作为全局函数(例如一些数学运算实际上只依赖计算方法而不依赖类创建出来的对象)。这种特性非常适合定义一些工具库,只需要使用一个类来统一组织这些静态方法,就可以打造出一个方便的工具库。
  • 由于没有 this 引用,就会约束静态方法只能使用静态属性,这在有些时候很有用。

补充:Cpp 对于 static 的理解有三层。

  • 第一层:首次引入了 static 时,只是为了表示退出代码块后仍然不销毁的静态变量
  • 第二层:第一次复用 static 时,表示不能从其他文件访问全局变量或函数,同时避免了添加关键字带来的麻烦
  • 第三层:第二次复用 static 时,已经和 static 本来的意义有些脱离了,但这层的含义和 Javastatic 类似,使得类拥有静态属性和静态方法

补充:在 Java 中,类内方法内不能直接定义静态变量。静态变量(或称为类变量)是属于类本身的,而不是属于类的实例,因此不能定义在方法内。所以静态变量通常在类的顶层定义,而不是在方法内。在方法内部,只能定义局部变量,这些变量仅在方法被调用时存在,并且在方法执行结束后会被销毁。

而会把 static 关键字放入方法内部的一定学过 Cpp(大概),因为 Javastatic 的使用只有 Cpp 第三层的理解。Cppstatic 的其他用法则会通过其他方式在 Java 中实现,这里如果误用就是误用了第一层的用法。

补充:静态方法无法被重写,不能用来实现多态,这个以后再来补充…

5.代码块

使用 {} 定义的一段代码被称为代码块,根据代码块定义的位置和关键字,可以简单分为:

  • 普通代码块:这种代码块没有太大的用途,单纯是用来方便组织程序的
  • 静态代码块:是定义在类中的静态块,用于执行类加载前(准确来说是在 JVM 加载类前)的初始化操作,静态代码块在构造一个对象前只执行一次(包含多个静态代码块时,就会按照多个静态代码块的顺序来依次执行)。
  • 构造代码块:在类中直接使用代码块就是一个构造代码块,只在对象实例化的时候才会被调用(多次实例化就被多次调用),要比构造函数先调用构造代码块。功能比直接赋值给类属性要更加强大。
  • 同步代码块:多线程的内容,这块内容以后讲到多线程时再进行补充…

下面供您两份代码,请您关注代码块的使用和位置,并且观察调用的执行顺序。

// 代码块演示 1
class MyClass {
    public int data;

    public static String name;

    {
        this.data = 10;
        System.out.println("构造代码块 2, this.data = " + this.data);
    }

    static {
        MyClass.name = "gimou";
        System.out.println("静态代码块 2, MyClass.name = " + MyClass.name);
    }

    MyClass() {
        System.out.println("构造方法: " + this.data);
    }

    {
        this.data = 20;
        System.out.println("构造代码块 1, this.data = " + this.data);
    }

    static {
        MyClass.name = "eimou";
        System.out.println("静态代码块 1, MyClass.name = " + MyClass.name);
    }

    public void print() {
        System.out.println("mc.data = " + this.data + " and " + "MyClass.name = " + MyClass.name);
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass mc = new MyClass();
        mc.print();
        mc.data = 18;
        MyClass.name = "limou";
        System.out.println("mc.data = " + mc.data + " and " + "MyClass.name = " + MyClass.name);
    }
}

/* 执行结果
静态代码块 2, MyClass.name = gimou
静态代码块 1, MyClass.name = eimou
构造代码块 2, this.data = 10
构造代码块 1, this.data = 20
构造方法: 20
mc.data = 20 and MyClass.name = eimou
mc.data = 18 and MyClass.name = limou
*/
// 代码块演示 2
class MyClass {
    public int data;

    public static String name;

    {
        this.data = 10;
        System.out.println("构造代码块 2, this.data = " + this.data);
    }

    static {
        MyClass.name = "gimou";
        System.out.println("静态代码块 2, MyClass.name = " + MyClass.name);
    }

    MyClass() {
        System.out.println("构造方法: " + this.data);
    }

    {
        this.data = 20;
        System.out.println("构造代码块 1, this.data = " + this.data);
    }

    static {
        MyClass.name = "eimou";
        System.out.println("静态代码块 1, MyClass.name = " + MyClass.name);
    }

    public void print() {
        System.out.println("mc.data = " + this.data + " and " + "MyClass.name = " + MyClass.name);
    }
}

public class Main {
    public static void main(String[] args) {
        // MyClass mc = new MyClass(); // 由于没有实例化类对象, 因此就不会调用构造代码块, 同时也不会调用构造方法, 但静态代码块依旧会被执行
        // mc.print();
        // mc.data = 18;
        MyClass.name = "limou";
        // System.out.println("mc.data = " + mc.data + " and " + "MyClass.name = " + MyClass.name);
    }
}

/* 执行结果
静态代码块 2, MyClass.name = gimou
静态代码块 1, MyClass.name = eimou
*/

补充:另外也有把构造代码块叫做实例代码块的。这个构造代码块的概念有些类似 Cpp 中直接给类属性进行赋值或者初始化列表的行为,并且最后也会被传递给构造函数。

6.内部类

当一个类内部需要一个完整的类结构来描述成员时,就需要一个内部类结构(尤其是内部类只为外部类服务而不对外服务时),这种内部类的设计思想其实就是组合的设计思想。

  • Java 的内部类和 Cpp 有很大的不同,Java 的内部类自动带有隐式的 this 引用,可以直接访问外部类的成员属性和成员方法,这也意味着内部类的创建必须依赖外部类的创建,否则使用内部类的某些方法时,这些方法在使用外部类的属性和方法就会导致出错。从这个角度上来看,子类的确依附于父类。
  • Cpp 的子类仅仅是在子类内部的属性和方法如果使用了父类对象时,可以无视访问限定符的限制访问父类的访问限定符。除此以外,父类和子类还是并行的关系,子类对象的创建并不直接依赖于外部类(仅仅是友元类的关系),因此 Java 的静态内部类倒是和 Cpp 的内部类是类似的概念。

至于为什么使用内部类,一是有时我们需要这种内部直接无限制访问父类对象成员的便捷手段;二是内部类可以对同包中的其他类隐藏。

7.1.普通内部类

// 尝试使用普通内部类
class Father {
    // 实例内部类/非静态内部类
    public int data = 1;
    class Son {
        public int data = 2;
        class Grandson {
            public int data = 3;
            public void func() {
                System.out.println(data); // 获取内部对象
                System.out.println(this.data); // 获取内部对象, 这个 this 是内部类自己的
                System.out.println(Son.this.data); // 获取较外部对象, 这个 this 引用较外部类
                System.out.println(Father.this.data); // 获取最外部对象, 这个 this 引用最外部类
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Father f = new Father();
        Father.Son.Grandson fsg = f.new Son().new Grandson();
        fsg.func(); // 创建内部类的对象
    }
}

补充:内部类和外部类的成员如果名字一样,就会导致就近原则。

不过值得注意的是,内部类不能直接定义一个属于自己的静态变量,但是可以通过 final 来规避(不过类内方法还是无法定义 static 变量,就算用了这个也不能避免)。

// 给内部类定义静态对象
class Father {
    // 实例内部类/非静态内部类
    public int data1;
    public static int data2;
    class Son {
        int data3;
        static final int data4 = 10; // 这个值在编译的时候就固定好了

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

public class Main {
    public static void main(String[] args) {
        Father f = new Father();
        Father.Son ds = f.new Son();
        ds.print();
    }
}

补充:不过上述这一无法在内部类中创建 static 的行为已经再高版本中被允许使用。但是由于目前(2024-07-08)依旧有很多的项目会使用 jdk8,至少我在 jdk8 中的测试是不允许直接创建静态成员的,因此这一点您需要注意一下。

补充:final 有些类似 Cpp 中的 const,但是会更容易理解一些,常量就是常量,没有特殊情况…

7.2.静态内部类

// 尝试使用静态内部类
class Outer {
    // 实例内部类/非静态内部类
    public int data1 = 1;
    public int data2 = 2;
    public static int data3 = 3;

    public void test1() {
        System.out.println("Outer.test1()");
    }

    // 创建一个静态内部类
    static class Inner {
        public int data3 = 4;
        public int data4 = 5;
        public static int data5 = 6;

        public void test2() {
            System.out.println("Inner.test2()");
            // 静态内部类因为没有 this 引用, 无法直接访问外部类成员, 因为本身不需要使用外部类来创建实例对象
            // System.out.println(data1); // error
            // System.out.println(data2); // error

            // 但是依旧是有方法的, 可以创建一个外部类成员再访问(不过这种就会受到一些范围限定符的限制了)
            Outer o = new Outer();
            System.out.println(o.data1);

            // 自己的属性还是可以访问的
            System.out.println(data3);
            System.out.println(data4);
            System.out.println(data5);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer.Inner in = new Outer.Inner(); // 无需实例化外部类对象就可以使用
        in.test2();
    }
}

结语:…

  • 29
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

limou3434

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值