面向过程和面向对象

面向过程:将程序看作一系列步骤或方法的集合,程序通过依次执行这些步骤来完成任务。

  • 方法为中心,强调过程逻辑
  • 数据与操作分离,数据通常作为参数传递给方法。

优点:

  • 简单直观,符合人类解决问题的线性思维。
  • 对于小型程序,开发速度快,代码量少。

缺点:

  • 随着程序规模增大,代码的复杂性和维护成本急剧上升。
  • 难以复用代码,修改功能时容易引入错误。
  • 数据和操作分离,导致代码的耦合性较高。

面向对象:将程序看作一系列对象的集合,对象之间通过消息传递进行交互。每个对象都有自己的属性(数据)和行为(方法)

  • 对象为中心,强调数据封装模块化
  • 通过对象组织代码,支持继承、封装和多态。

优点:

  • 代码结构清晰,易于维护和扩展。
  • 支持代码复用(通过继承、组合等方式)。
  • 数据与操作封装在一起,降低了耦合性。

缺点:

  • 对于简单问题,可能会显得过于复杂。

以爬虫为例:

对于最简单的爬虫,爬取一个网页,只保存为图片,易于解析网页和解密。此时三个方法,分别是发起请求,解析网页,保存图片,足够完成整个程序的功能。此时面向过程最为合适,面向对象则过于复杂化:

def request(url):
    # 发起请求
    pass

def parse(html):
    # 解析网页
    pass

def save_image(data):
    # 保存图片
    pass

# 主程序
url = "http://example.com"
html = request(url)
data = parse(html)
save_image(data)

但是对于增加功能,如增加日志记录、异常处理、多线程、异步,以及多网页处理,多种格式保存,数据库交互,多种的解析方式和反反爬虫,不同的请求方式,对于面向过程而言过于复杂,在后期需要维护代码,以及复用相关代码都比较困难。而面向对象通过分为多种对象,分别维护不同的功能,维护性和复用性都很良好。

将不同功能模块化为对象:

  1. 请求模块:负责发起请求,处理请求参数、请求头等。
  2. 解析模块:负责解析网页内容,支持多种解析方式。
  3. 存储模块:负责保存数据,支持多种格式(图片、文本、数据库)。
  4. 日志模块:负责记录运行日志。
  5. 异常处理模块:负责捕获和处理异常。
  6. 调度模块:负责多线程、异步任务的调度。

上述的模块化也只是一个大概的抽象,实际上会更加复杂,比如python的爬虫框架Scrapy

def request(url):
    # 发起请求
    pass

def parse(html):
    # 解析网页
    pass

def save_image(data):
    # 保存图片
    pass

# 主程序
url = "http://example.com"
html = request(url)
data = parse(html)
save_image(data)

但是对于增加功能,如增加日志记录、异常处理、多线程、异步,以及多网页处理,多种格式保存,数据库交互,多种的解析方式和反反爬虫,不同的请求方式,对于面向过程而言过于复杂,在后期需要维护代码,以及复用相关代码都比较困难。而面向对象通过分为多种对象,分别维护不同的功能,维护性和复用性都很良好。

将不同功能模块化为对象:

  1. 请求模块:负责发起请求,处理请求参数、请求头等。
  2. 解析模块:负责解析网页内容,支持多种解析方式。
  3. 存储模块:负责保存数据,支持多种格式(图片、文本、数据库)。
  4. 日志模块:负责记录运行日志。
  5. 异常处理模块:负责捕获和处理异常。
  6. 调度模块:负责多线程、异步任务的调度。

上述的模块化也只是一个大概的抽象,实际上会更加复杂,比如python的爬虫框架Scrapy

类和对象

定义一个类的语法如下:

[访问修饰符] class 类名 {
    // 成员变量
    // 构造方法
    // 成员方法
}

其中[xxx]的内容是可选的。

示例:

// 新建一个Cat类
class Cat {
    int age;
    String name;
}

在一个Java文件中,只能有一个访问修饰符为public的类。

类的使用

实例化一个类的语法如下:

类名 对象名 = new 类名(参数);

// 也可以:

类名 对象名;
对象名 = new 类名(参数);

示例:

Cat cat;
cat = new Cat();

Cat cat = new Cat;

成员变量

在类中创建一个成员变量的语法如下:

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

其中[xxx]的内容是可选的。

示例:

class Cat {
    int age = 2;
    String name;
}

成员变量的使用

通过对象名.变量名的方式访问成员变量,示例:

Cat cat = new Cat();
cat.name = "小艺"; 
System.out.println(cat.age); // 2
System.out.println(cat.name); // 小艺

成员变量即使没有初始值,也会有默认初始化的值,int 为 0,double 为 0.0,boolean 为 false,char为\u0000,引用类型为null。

成员方法

在类中创建一个成员方法的语法如下:

[访问修饰符] [返回值类型] 方法名([参数列表]) {
    // 方法体
    
    [return 返回值;]
}

其中[xxx]的内容是可选的。示例:

class Cat {
    int age;
    String name;
    
    public void eat() {
        System.out.println("eat");
    }
}
public Main {
    public static int add(int a, int b) {
        return a+b;
    }
}

相当于把传进来的变量赋值给参数,比如在上面的例子中,就是把传进来的两个数分别赋值给a和b。但如果传递的是引用数据类型,那么参数和被传递的变量指向同一处地址,此时参数修改对应地址处的数据会影响到被传递的变量。

当方法执行到第一个return时,就会返回并结束方法的执行,即使后面可能还有return

成员方法的使用

通过对象名.方法名(参数)的方式使用成员方法,示例:

Cat cat = new Cat();
cat.eat();

局部变量

局部变量是在 方法、构造方法或代码块 内部声明的变量,它的作用范围 仅限于定义它的代码块。和成员变量相比,定义的语法是数据类型 变量名 [= 初始值]。局部变量在声明后必需初始化,不会被默认初始化。

成员方法内使用成员变量

在同一个类的成员方法内,成员变量可以直接使用,无需 this 关键字;如果成员变量与局部变量重名,则优先访问局部变量,若要访问成员变量,需要用 this.变量名

public class Example {
    private int number = 10; // 成员变量

    public void showNumber() {
        System.out.println("成员变量 number = " + number); // 直接访问成员变量
    }

    public void setNumber(int number) { // 形参与成员变量同名
        System.out.println("局部变量 number = " + number); // 访问的是局部变量
        System.out.println("成员变量 number = " + this.number); // 访问成员变量
        this.number = number; // 使用 this 赋值给成员变量
    }

    public static void main(String[] args) {
        Example obj = new Example();
        obj.showNumber();
        obj.setNumber(20);
        obj.showNumber();
    }
}

成员方法调用成员方法

直接使用方法名调用,或者通过this.方法名进行调用,两种方式效果相同。

public class Example {
    private int number = 10;

    public void showNumber() {
        System.out.println("成员变量 number = " + number);
    }

    public void display() {
        System.out.println("调用 showNumber 方法");
        showNumber();  // 直接调用成员方法
        this.showNumber();  // 使用 this 调用成员方法
    }

    public static void main(String[] args) {
        Example obj = new Example();
        obj.display();
    }
}

方法重载

在同一个类中,可以定义多个同名方法,只要它们的参数列表不同。

  • 参数类型不同:
public static void add(int a, int b) {
    System.out.println(a + b);
}
public static void add(double a, double b) {
    System.out.println(a + b);
}
  • 参数数量不同:
public static void add(int a, int b) {
    System.out.println(a + b);
}
public static void add(int a, int b, int c) {
    System.out.println(a + b + c);
}
  • 参数类型顺序不同,与变量名无关:
public static void add(int a, double b) {
    System.out.println(a + b);
}
public static void add(double a, int b) {
    System.out.println(a + b);
}

递归

**递归:**一个方法直接或间接调用自身的过程。

基本结构如下:

public void recursiveMethod(parameters) {
    // 基准条件
    if (baseCaseCondition) {
        // 处理基准条件
        return;
    }
    // 递归条件
    recursiveMethod(modifiedParameters);
}

也可以将void替换成返回值的数据类型,前提是递归方法必需在最后返回相同数据类型的返回值。

示例:

计算阶乘:

public int factorial(int n) {
    if (n == 0 || n == 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

计算斐波那契数列:

public int fibonacci(int n) {
    if (n == 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

注意事项:

必需注意递归的深度和条件,防止无限递归或者递归导致超过栈的大小。

可变参数

可以接收多个变量的参数,语法为:参数类型... 参数名,其中的参数名可以被当作数组使用,可变参数的实参可以为0个或任意多个。

示例:

public class Main{
    public static void main(String[] args) {
        int num = sum(1,2,3,4,5,6,7,8,9,10);
        System.out.println(num); // 输出55
    }

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

可变参数的实参也是数组,在上面的例子中:

int[] numbers = {1,2,3,4,5,6,7,8,9,10};
int num = sum(numbers);
System.out.println(num); // 输出55

可变参数的本质就是数组。

注意事项:

一个方法中只能有一个可变参数,并且可变参数必需放到其它参数的最后。

作用域

一段程序代码中所用到的变量并不总是有效/可用的,而限定这个变量的可用性的代码范围就是这个变量的作用域。在Java中,作用域主要是通过{ }来定义的,比如:

public class Main{
    int a = 1;
    public static void main(String[] args) {
        {
            static int b = 2;
            System.out.println(a + b);
        }
        System.out.println(a);
    }
}

main方法中的{}内可以使用变量a,在{}外无法使用变量b。

不同的作用域也关系着变量的声明周期,也就是变量从创建到被销毁的一段时间。在上述的例子中,变量a从程序开始执行到结束,变量b从进入{}执行,到{}中的代码执行完毕。

Java中的作用域主要分为以下几种:

  • 类作用域:
    • 适用范围:类级别的变量(成员变量)。
    • 生命周期:随对象的创建而存在,随对象的销毁而消失。
  • 方法作用域:
    • 适用范围:方法内部定义的变量(局部变量)
    • 生命周期:随方法调用而创建,随方法执行完毕而销毁。
  • 块作用域:
    • 适用范围:代码块 {} 内的变量。典型场景:if 语句、for/while 循环、try-catch 代码块
    • 生命周期:进入块时创建,退出块后销毁。
  • 静态作用域:
    • 适用范围static 变量和方法,属于 类本身
    • 生命周期:程序启动时创建,程序结束时销毁。

构造方法

构造方法是类的一种特殊的成员方法,用于在创建对象时初始化对象,也就是在创建对象的时候会被执行且被最先执行。

基本语法:

[访问修饰符] 类名(参数) {
    // 方法体
}
  • 构造方法的名称必须与类名完全相同。
  • 构造方法没有返回类型(连 void 也不能写)。
public class Animal {
    String name;
    public Animal(String name) {
        this.name = name
    }
}

如果类中没有显式定义构造方法,Java 会提供一个默认的无参构造方法。

构造方法的使用

在创建对象时,传递构造方法的参数。

示例:

public class Main{
    public static void main(String[] args) {
        Animal animal = new Animal("Dog");
        System.out.println(animal.name); // 输出Dog
    }
}

构造方法的重载

构造方法可以重载,在创建对象的时候传递不同的参数调用不同的构造器。

public class Main{
    public static void main(String[] args) {
        Solution s = new Solution(1); // 输出1
        Solution s1 = new Solution(1.0); // 输出1.0
        Solution s2 = new Solution("s1"); // 输出s1
    }
}
class Solution {
    public Solution(int a) {
        System.out.println(a);
    }
    public Solution(double a) {
        System.out.println(a);
    }
    public Solution(String a) {
        System.out.println(a);
    }
}

可以通过this(参数)调用其它的构造方法,但必需写在第一行,并且只能出现一次调用其它的构造方法,只能在构造方法内部才能调用构造方法,比如:

class Solution {
    public Solution() {
        this(0, 0);
    }
    public Solution(int a, int b) {
        System.out.println(a+b);
    }
}
public class Main{
    public static void main(String[] args) {
        Solution s = new Solution(); // 输出0
        Solution s1 = new Solution(1, 2); // 输出3
    }
}

如果类中没有构造方法,Java会默认隐式的为类添加一个空的构造方法。

this关键字

this用于引用当前对象的实例,访问当前对象的成员变量、成员方法、构造方法。

访问当前类的实例的成员变量和成员方法,直接通过this.加上变量名和方法名进行调用。

public class Main{
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.setName("Buddy");
        System.out.println(dog.getName()); // Output: Buddy

    }
}

class Dog {
    private String name;
    
    public void setName(String name) {
        setName(this, name);
    }
    public String getName() {
        return name;
    }
    
    private static void setName(Dog dog, String name) {
        dog.name = name;
    }
}

在上述的例子中,setName(Dog dog, String name)方法接收一个Dog对象进行命名,在调用时直接传入this表示当前对象,改变当前对象name变量的值。

包的语法如下:

package 包名;

包实际上就是一个文件夹,导入不同文件夹下的类,可以避免类的重名问题,更好的组织代码结构。

示例:

package com.animal;
package com.pets;

如果在Animal下有一个Dog类,在Pets下也有一个Dog类,都导入就会发生冲突。

那么在定义的时候可以:

public class Main {
    public static void main(String[] args) {
        Pets.Dog dog = new Pets.Dog();
        Animal.Dog dog2 = new Animal.Dog();
    }
}

代码结构如下:

└───src
    ├───Animal
    |───Pets
    └───Main.java

访问修饰符

Java中有四种权限修饰符:private默认(不写)protectedpublic

同一个包下指的是同一个包下的.java文件,如果包下面还有包,则属于不同的包,具体而言和package后面定义的有关,例如package com.petspackage com.pets.animals,后者在前者的目录内,但是不一样,不是同一个包。

修饰类

只有**默认(不写)**和public才可以修饰类,**默认(不写)**修饰的类只能在同一个包下被使用,在其它包下会找不到这个类,public在任何地方都可以使用,包括:继承、实例化等。

修饰变量和方法

  • private:修饰的变量及方法仅限在定义它们的类内部访问或调用,不能被子类或其他类访问。

  • 默认(不写):修饰的变量及方法仅限在定义它们的类所在包下的类访问或调用。

  • protected:修饰的变量及方法仅限在定义它们的类所在包下的类访问或调用,以及不同包下的子类可以访问或调用。

  • public:修饰的变量及方法在任何地方都可以访问或调用。

封装

封装就是将 对象的状态(属性)和行为(方法) 绑定在一起,并通过 访问控制 限制外部对对象的直接访问。可以隐藏实现细节、添加校验逻辑,防止无效数据赋值。

步骤:

  1. 将属性进行私有化
  2. 提供一个公共(public)的set方法,用于对属性判断并赋值
  3. 提供一个公共(public)的get方法,用于获取属性的值

示例:

public 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) {
        if (age >= 1 && age <= 120) {
            this.age = age;
        } else {
            System.out.println("Age must be between 1 and 120");
        }
    }
}

和构造器一起使用,使用setXXX方法设置成员变量的值:

public Person(String name, int age) {
    setAge(age);
    setName(name);
}

继承

继承是面向对象编程(OOP)的一种机制,允许一个类(子类 / 派生类)继承另一个类(父类 / 超类)的属性和方法,增强代码的复用性和可扩展性。

假设要实现一个企业不同员工的信息管理,不使用继承需要依次写董事长管理员一般员工等,这些类中代码的重复性会比较高,比如属性:姓名、ID、请假天数、基本工资等。可以实现一个staff类,其它的类继承该类,对于类中不同的,可以在对应的类中在具体实现,提高了代码的维护性和复用性。

语法如下:

class Parent {
    // 父类代码
}

class Child extends Parent {
    // 子类代码
}

Java中所有类都是Object的子类,即使不显示的继承,Java也会默认隐式地继承Object类。

示例:

public class Main {
    public static void main(String[] args) {
        B b = new B();
        b.methodB(); // 先输出B,在输出A
        b.info(); // Tom
    }
}
class A {
    String name = "Tom";
    public void methodA() {
        System.out.println("A");
    }
}

class B extends A {
    public void methodB() {
        System.out.println("B");
        this.methodA();
    }
    public void info() {
        System.out.println(this.name);
    }
}

子类可以使用父类的属性和方法,像自己的属性和方法一样使用,但子类依然可以有自己的属性和方法,并且可以"替换"掉父类的属性和方法。即在使用属性和方法的时候,如果子类没有,就会去使用父类的,如果子类有,则优先使用子类的,这时可以使用super指定访问父类的成员变量或调用成员方法。

// 父类
class Parent {
    public String name = "Parent Name"; // 父类属性

    public int age = 7;
    public void show() { // 父类方法
        System.out.println("Parent show()");
    }
}

// 子类
class Child extends Parent {
    public int age = 10; // 隐藏父类的age成员变量

    @Override
    public void show() { // 子类重写(替换)父类方法
        System.out.println("Child show()");
    }
}

public class Test {
    public static void main(String[] args) {
        Child child = new Child();
        
        // 直接使用父类的属性
        System.out.println(child.name); // 输出: Parent Name
        
        // 直接使用父类的方法(如果未重写的话)
        child.show(); // 输出: Child show() (子类替换了父类的方法)
        
        // 子类自己的属性
        System.out.println(child.age); // 输出: 10
    }
}

子类继承了父类的所有成员变量和方法(构造方法除外),但是会受到访问修饰符的限制,即使 private 修饰的变量和方法不能直接访问,但仍然存在于子类对象的内存中,可以通过反射间接获取。

继承后的 查找顺序子类 -> 父类 -> 祖父类 -> ... -> Object

class GrandParent {
    public void show() { System.out.println("GrandParent show"); }
}

class Parent extends GrandParent {
    public void show() { System.out.println("Parent show"); }
}

class Child extends Parent {
    public void sayHello() { System.out.println("Child sayHello"); }
}

public class Test {
    public static void main(String[] args) {
        Child child = new Child();
        child.show(); // 输出: "Parent show"
    }
}

Java中的继承是单继承机制,即一个类只能继承一个父类;不能滥用继承,必需满足is-a的条件,例如:Cat extends Animal可以,但是Music extends Animal并不合适。

子类可以通过super调用父类的构造方法,子类不会继承父类的构造方法,但子类的构造方法必须调用父类的构造方法,以确保父类的部分被正确初始化;如果子类没有显式的调用父类的构造方法,子类默认会隐式地调用父类的无参构造方法如果父类没有无参构造方法,子类必须显式调用带参构造方法,否则编译报错。构造器的执行顺序先执行父类构造器再执行子类构造器

super关键字

super用于引用当前对象父类的实例,访问父类的成员变量、成员方法、构造方法。

调用成员变量和成员方法,通过super.加上变量名或方法名。

调用父类的构造器,无参构造器super(),有参构造器super(参数列表)

注意事项:

  • 子类构造方法默认会隐式地调用父类的无参构造方法 super();(即使代码中没有写)。

  • 如果父类没有无参构造方法,子类必须手动调用带参数的 super(参数...),否则编译错误。

  • super(...) 只能在构造方法的第一行,否则编译错误。

  • this(...)也只能在构造方法的第一行,所以子类的一个构造方法中,super(...)this(...)只能用一个。

变量的隐藏

子类可以定义和父类同名的变量,隐藏父类的成员变量或类,在不使用super或通过父类访问类变量的情况下,默认访问的将会是子类的成员变量,对访问修饰符无要求,对是否是成员变量和类变量无要求。只要两个变量同名即可,成员变量可以隐藏类变量,反之也可以,成员变量之间和类变量之间也可以。

示例:

import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        new Child().show();
    }
}

class Parent {
    int value = 10;
    static int num = 100;
    static String name = "父类的静态变量";
    int id = 10;
}

class Child extends Parent {
    int value = 20;
    static int num = 200;
    String name = "子类的成员变量";
    static int id = 20;
     void show() {
        System.out.println("子类的 value:" + value);
        System.out.println("父类的 value:" + super.value);
        System.out.println("子类的 num:" + num);
        System.out.println("父类的 num:" + Parent.num);
        System.out.println("子类的 name:" + name);
        System.out.println("父类的 name:" + Parent.name);
        System.out.println("子类的 id:" + id);
        System.out.println("父类的 id:" + super.id);
    }
}

上述代码的执行结果为:

子类的 value:20
父类的 value:10
子类的 num:200
父类的 num:100
子类的 name:子类的成员变量
父类的 name:父类的静态变量
子类的 id:20
父类的 id:10

成员方法重写

方法重写是子类父类private 修饰的成员方法进行重新定义,使其行为符合子类的需求。

要求:

  • 方法名相同,参数列表(方法签名)相同
  • 返回值类型相同或为父类方法返回值的子类
  • 访问权限不能比父类更严格
  • 子类方法声明的异常不能比父类方法声明的更广泛,父类不声明异常则子类不能声明异常
  • 都必须是成员方法

示例:

class Parent {
    void show() {
        System.out.println("父类的方法");
    }
}

class Child extends Parent {
    @Override
    void show() {  // 方法重写
        System.out.println("子类的方法");
    }
}

public class Test {
    public static void main(String[] args) {
        Parent p = new Parent();
        p.show();  // 输出:父类的方法

        Child c = new Child();
        c.show();  // 输出:子类的方法

        Parent pc = new Child(); // 父类引用指向子类对象(多态)
        pc.show();  // 输出:子类的方法(调用的是子类的重写方法)
    }
}

在子类重写的方法上方加上@Override,可以让编译器检查确保子类正确重写了父类方法。

类方法的隐藏

要求:

  • 方法名相同,参数列表(方法签名)相同
  • 都必须是类方法,类方法和成员方法之间会报错
  • 返回值类型相同或为父类方法返回值的子类
  • 访问权限不能比父类更严格
  • 子类方法声明的异常不能比父类方法声明的更广泛,父类不声明异常则子类不能声明异常

示例:

public class Main {
    public static void main(String[] args) {
        Parent.show(); // 调用父类的静态方法
        Child.show();  // 调用子类的静态方法

        System.out.println(Parent.getMessage()); // 访问父类的静态方法
        System.out.println(Child.getMessage());  // 访问子类的静态方法
    }
}

class Parent {
    static void show() {
        System.out.println("父类的静态方法 show()");
    }

    static String getMessage() {
        return "父类的 getMessage()";
    }
}

class Child extends Parent {
    static void show() { // 隐藏父类的静态方法
        System.out.println("子类的静态方法 show()");
    }

    static String getMessage() { // 隐藏父类的静态方法
        return "子类的 getMessage()";
    }
}

上述代码的执行结果为:

父类的静态方法 show()
子类的静态方法 show()
父类的 getMessage()
子类的 getMessage()

多态

多态是同一个行为具有多个不同表现形式或形态的能力。在Java中,就是一个引用类型的变量,所指向的具体的类型和通过该变量调用的方法,在编译时不确定,在运行期间才确定。在效果上,就是一个父类的实例,可以指向该父类不同的子类,从而在调用父类的方法时,表现出不同的效果。

Java允许声明一个父类的变量,但实际在内存中指向的,即new后面跟的是子类,这种方式称为向上转型。其中=前面的是编译类型,在编译期间被作为该类型;后面的是运行类型,即实际的类型。

class Parent {
    public void say() {
        System.out.println("Parent say");
    }
}

class Child extends Parent {
    @Override
    public void say() {
        System.out.println("Child say");
    }
    
    public void looK() {
        System.out.println("Child look");
    }
}


public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.say(); // Child say
        p.look; // 报错
    }
}

此时变量p可以调用的属性和方法都是父类的,否则在编译时会无法通过,因为它的编译类型是Person,虽然内存中实际是Child

但是如果调用被子类重写的say方法,最终输出的是Child say。即如果以这种方式创建的变量,调用被子类重写的方法,实际调用的是子类的方法,在效果上等同于子类的对象去调用该方法。

因为p实际上在内存中指向的是Child类型的,所以可以在转换成Child类型,称为向下转型,必需现有向上转型,才可以向下转型,示例如下:

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        Child c = (Child) p;
    }
}

但如果不是内存中指向的不是Child类型,就会报错,在进行向下转型前可以先用instanceof进行判断:

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        if (p instanceof Child) {
            Child c = (Child) p;
        }
    }
}

当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)绑定,称为动态类型绑定。在效果上就是,如果调用没有被子类重写的方法,相当于一个父类的对象调用该方法,所使用的成员变量是父类的,方法内部调用的方法,即使被子类重写了也是父类的;如果调用子类的方法,相当于一个子类的对象调用该方法,所使用的成员变量是子类的,方法内部调用的方法也是子类的。示例如下:

class Parent {
    int i = 10;
    public int getI() {
        return i;
    }

    public int sum() {
        return 20 + getI();
    }
}

class Child extends Parent {
    int i = 20;
    @Override
    public int getI() {
        return i;
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.sum()); // 30
        System.out.println(p.getI()); // 20
    }
}

对于成员变量,子类不会影响到父类,根据声明的编译类型进行访问,即使是子类中存在同名的成员变量。

class Parent {
    String name = "父类的name";
}

class Child extends Parent {
    String name = "子类的name"; // 隐藏父类的 name 变量
}

public class Test {
    public static void main(String[] args) {
        Parent p = new Parent();
        System.out.println(p.name); // 输出:父类的name

        Child c = new Child();
        System.out.println(c.name); // 输出:子类的name

        Parent p2 = new Child();
        System.out.println(p2.name); // 输出:父类的name(变量隐藏,不受多态影响)
    }
}

instanceof运算符

instanceof运算符用于判断变量的运行时类型是不是指定类的实例,以及指定类子类的实例,语法为:对象 instanceof 类名

class Parent {}

class Child extends Parent {}

public class Test {
    public static void main(String[] args) {
        Parent p = new Parent();
        Child c = new Child();
        Parent pc = new Child(); // 向上转型

        System.out.println(p instanceof Parent); //  true
        System.out.println(c instanceof Child); //  true
        System.out.println(c instanceof Parent); //  true(子类对象是父类实例)
        System.out.println(pc instanceof Child); //  true(运行时对象是 Child)
        System.out.println(p instanceof Child);  //  false(父类实例不是子类)
        System.out.println(null instanceof Parent); //  false(null 不是任何类的实例)
    }
}

==运算符

==运算符,对于基本数据类型,比较的是值,但对于引用数据类型,比较的是地址,也就是用来判断是否是同一个对象。

class A extends B {

}

class B {}
public class Main {
    public static void main(String[] args) {
        A a = new A();
        A b = a;
        A c = b;
        B bobj = b;
        System.out.println(a == b); // true
        System.out.println(b == c); // true
        System.out.println(bobj == c); // true
    }
}

对于基本数据类型,直接比较值是否相同,char类型被当作数字对待。

int a = 10;
double b = 10.0;
System.out.println(a == b);  // true(值相同)

char a = 'A'; // A对应的unicode码是65
int b = 65;
System.out.println(x == y);  // true(值相同)

注意事项:

对于String类型,两种初始化的方式所指向的内存是不一样的。

String s1 = new String("Hello");

这种方式会现在堆里面开辟一块内存,在指向常量池中的Hello,通过这种方式创建相同字符串的两个对象比较结果为false

String s2 = "Hello";

这种方式会直接指向常量池中的Hello,通过这种方式创建相同字符串的两个对象比较结果为true,因为字符串一样,会指向同样的地址。

但是如果是:

String s1 = new String("hello").intern();
String s2 = "hello";

System.out.println(s1 == s2);  // true

s1.intern() 会把 "hello" 放入字符串常量池,并返回池中的引用,所以 s1 == s2true

Object

toString方法

toString() 方法用于返回对象的字符串表示形式。

在打印一个对象的时候,实际上隐式地调用了toString()方法,即输出的是方法的返回值。

默认实现

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

语法

object.toString()

返回值

返回对象的字符串表示形式。

默认返回格式:对象的 class 名称 + @ + hashCode 的十六进制字符串

示例

public class Main {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.toString());
    }
}

class A {}

以上程序执行结果为:

A@e580929

重写该方法

默认实现不太有用,通常需要重写,重写该方法的一个示例:

class Person {
    String name;
    int age;

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

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person("Alice", 25);
        System.out.println(p);  // 自动调用 p.toString()
    }
}

以上程序执行结果为:

Person{name='Alice', age=25}

equals() 方法

equals() 方法用于比较两个对象是否相等。equals() 方法比较两个对象,是判断两个对象引用指向的是同一个对象,即它只是检查两个对象是否指向内存中的同一个地址。

注意:重写该方法,就必须重写hashCode方法。

默认实现

public boolean equals(Object obj) {
    return (this == obj);
}

语法

object.equals(obj)

参数

  • obj:要比较的对象。

返回值

如果两个对象相等返回 true,否则返回 false。

示例

class RunoobTest {
    public static void main(String[] args) {
 
        Object obj1 = new Object();
        Object obj2 = new Object();
 
        // 判断 obj1 与 obj2 是否相等
        // 不同对象,内存地址不同,不相等,返回 false
        System.out.println(obj1.equals(obj2)); // false

        Object obj3 = obj1;
        System.out.println(obj1.equals(obj3)); // true
    }
}

以上程序执行结果为:

false
true

重写该方法

默认实现不太有用,通常需要重写,重写时要确保:

  1. 自反性x.equals(x) == true
  2. 对称性x.equals(y) == y.equals(x)
  3. 传递性x.equals(y) && y.equals(z) → x.equals(z)
  4. 一致性:多次调用 equals() 结果不变(前提是对象未修改)
  5. null 比较返回 falsex.equals(null) == false

重写该方法的一个示例:

class Person {
    String name;
    int age;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;  // 地址相同,直接返回 true
        if (obj == null || getClass() != obj.getClass()) return false;  // 类型不同,false

        Person person = (Person) obj;
        return age == person.age && name.equals(person.name);
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Alice", 25);
        System.out.println(p1.equals(p2));  // true
    }
}

以上程序执行结果为:

true

hashCode方法

hashCode()方法用于获取对象的 hash 值。

默认实现

public int hashCode() {
    return System.identityHashCode(this);
}

语法

object.hashCode()

返回值

返回对象哈希值,是一个整数,表示在哈希表中的位置。

示例

class RunoobTest {
    public static void main(String[] args) {
        Object obj1 = new Object();
        System.out.println(obj1.hashCode()); 
 
        Object obj2 = new Object();
        System.out.println(obj2.hashCode());
 
        Object obj3 = new Object();
        System.out.println(obj3.hashCode()); 
    }
}

以上程序执行结果为:

225534817
1878246837
929338653

重写该方法

重写该方法要求:

  • 如果 equals() 被重写,则 hashCode() 也必须重写

  • 相同对象 equals() 返回 true,它们的 hashCode() 必须相同。

  • 不同对象 hashCode() 可以相同,但尽量不相同,减少碰撞,提高哈希效率。

重写该方法的一个示例:

class Person {
    String name;
    int age;

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

    @Override
    public int hashCode() {
        return name.hashCode() * 31 + age;
    }
}

类变量

使用 static 关键字修饰的变量,属于类本身,而不是某个特定对象。类变量在类加载时初始化,仅存在一份。

语法如下:

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

示例:

class Example {
    static int count = 0; // 类变量(静态变量)
}

在访问时,直接通过类名.变量名直接调用,示例:

public class Main {
    public sttic void  Main(String[] args) {
        System.out.println(Example.count); // 输出0
    }
}

注意事项:

  • 成员变量和类变量不能同名,否则会报错。

成员方法使用类变量

如果没有定义同名的局部变量,可以直接使用,否则需要通过类名.变量名的方式访问。

class A {
    static final int age = 0;
    public void show() {
        int age = 1;
        System.out.println(A.age);
    }
}

类方法

使用 static 修饰的方法,属于类本身,不依赖对象实例。不能使用 thissuper 关键字,不能直接访问成员变量和调用成员方法,可以创建一个类的实例或接收参数在访问或调用。

语法如下:

[访问修饰符] static [返回值类型] 方法名([参数列表]) {
    // 方法体
    
    [return 返回值;]
}

示例

class Example {
    static void printMessage() {
        System.out.println("Hello, static method!");
    }
}

在调用时,通过类名.方法名(参数)进行调用,示例:

class Example {
    static void printMessage() {
        Example.printMessage(); // 输出Hello, static method!
    }
}

注意事项:

  • 类方法不能和成员方法名称相同并且参数列表相同。

类方法调用类变量

如果没有同名的局部变量,直接使用即可,如果存在同名的局部变量,必需使用类名.变量名

class Example {
    static int count = 10; // 类变量

    void display() {
        int count = 5; // 局部变量,遮蔽了类变量
        System.out.println(count); // 输出 5(局部变量)
        System.out.println(Example.count); // 访问类变量,输出 10
    }
}

类方法调用类方法

直接调用类方法即可,或者通过类名.方法名调用。

public class Main {
	public static void main(String[] args) {
        Example.methodB();
    }
}

class Example {
    static void methodA() {
        System.out.println("调用 methodA");
    }

    static void methodB() {
        methodA(); // 直接调用同一个类中的 static 方法
        Example.methodA();
        System.out.println("调用 methodB");
    }
}

上述代码执行结果如下:

调用 methodA
调用 methodA
调用 methodB

成员方法调用类方法同理。

main方法

main方法是程序的执行入口,具有如下的标准格式:

public static void main(String[] args)
关键字/部分 作用
public 公开访问,JVM 需要从外部调用该方法。
static 静态方法,无需创建对象即可被 JVM 调用。
void 方法不返回任何值。
main 方法名必须是 main,区分大小写。
String[] args 传递命令行参数,数组名可以更改(如 String[] argumentsString... args)。

main方法作为类方法,可以直接访问本类的类变量和调用本类的类方法,但无法直接访问成员变量和调用成员方法,必需创建本类的实例才可以访问或调用。

args 是一个 字符串数组,用于接收命令行参数,运行 Java 程序时可以传递参数,示例:

java Main Hello Java

main 方法中可以访问这些参数:

public class Main {
    public static void main(String[] args) {
        System.out.println("参数个数:" + args.length);
        for (String arg : args) {
            System.out.println("参数:" + arg);
        }
    }
}

执行示例中的结果为:

参数个数:2
参数:Hello
参数:Java

代码块

代码块又称为从初始化块,属于类中的成员,是类的一部分。类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。不同的是,代码块没有方法名,没有返回,没有参数,只有方法体,而且不通过对象或类显式调用,而是在加载类,或创建对象时隐式调用。

语法如下:

[static] {
  // 方法体   
}

其中static可选,有static的叫静态代码块,没有的叫普通代码块。

普通代码块会在创建类的实例时,优先于构造方法执行,每创建一次执行一次。如果只使用静态成员(访问类变量、调用类方法)则不会执行,因为不会创建类的实例。

静态代码块会在加载类时执行一次,且只执行这一次,因为类只会在第一次被使用的时候加载一次,后续在被使用的时候不会再次加载。因为加载类到内存先于创建类的实例,所以静态代码块会优先于普通代码块和构造方法执行。

类的加载情况:

  1. 创建类的实例的时候
  2. 创建子类的实例的时候,父类也会被加载
  3. 使用类的静态成员(类变量、类方法)

示例:

class A {
    static int a = 1;
    {
        System.out.println("普通代码块执行了");
    }
    static {
        System.out.println("静态代码块执行了");
    }
    public A() {
        System.out.println("构造函数执行了");
    }
}

public class Main {
    public static void main(String[] args) {
        int b = A.a;
        A a = new A();
    }
}

上述代码执行结果如下:

静态代码块执行了 // 加载类执行静态代码块
普通代码块执行了 // 创建类执行普通代码块和构造方法,类已经加载过不在加载,不再执行静态代码块
构造函数执行了

静态代码块只可以访问类变量和调用类方法,而普通代码块还可以调用成员方法和访问成员变量。

静态代码块访问类变量,类变量必需定义在静态代码块之前;普通代码块访问成员变量,成员变量必需定义在普通代码块之前,对类变量无要求,因为类变量的初始化在成员变量之前。

都对调用方法无顺序要求,包括静态方法和类方法,因为方法在变量初始化之前就已经被加载好。此时还可以把静态方法和成员方法的值赋值给类变量或成员变量,对方法和变量的顺序无要求,原因同上。

在类加载的时候,按照代码顺序(自上而下)依次执行静态变量的赋值语句和静态代码块,两者之间没有严格的优先级。在创建类的实例的时候,按照代码顺序(自上而下)依次执行成员变量的赋值语句和普通代码块,两者之间没有严格的优先级。如果变量的值为方法的返回值,就会调用该方法。

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

    static int a = print("变量a被初始化了");
    
    int b = print("变量b被初始化了");

    {
        System.out.println("子类普通代码块");
    }

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

    static int print(String str) {
        System.out.println(str);
        return 0;
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("Main 方法执行");
        new Child();
    }
}

上述代码的执行结果为:

Main 方法执行
子类静态代码块
变量a被初始化了
变量b被初始化了
子类普通代码块
子类构造方法

总结,对于一个类:

  1. 如果类没有被使用过,也就是没有被加载过,则先加载该类,按照代码顺序(自上而下)依次执行静态变量的赋值语句和静态代码块,如果被加载过,则跳过,不执行静态代码块。
  2. 如果需要实例化该类,则按照代码顺序(自上而下)依次执行成员变量的赋值语句和普通代码块,最后在执行类的构造方法。

对于继承,如果要创建子类的实例,执行顺序为:自上而下依次加载父类->加载子类->依次创建父类的实例->创建子类的实例。

class Parent {
    static { System.out.println("父类静态代码块"); }
    { System.out.println("父类普通代码块"); }
    public Parent() { System.out.println("父类构造方法"); }
}

class Child extends Parent {
    static { System.out.println("子类静态代码块"); }
    { System.out.println("子类普通代码块"); }
    public Child() { System.out.println("子类构造方法"); }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("Main 方法执行");
        new Child();
    }
}

上述代码输出结果如下:

Main 方法执行
父类静态代码块
子类静态代码块
父类普通代码块
父类构造方法
子类普通代码块
子类构造方法

如果只调用子类的静态成员,该成员父类没有或子类覆盖了父类的,执行顺序为自上而下依次加载父类->加载子类。

public class Main {
    public static void main(String[] args) {
        System.out.println(Child.a);
    }
}


class Parent {
    static {
        System.out.println("父类的静态代码块被执行了");
    }
    static int a = 1;
}

class Child extends Parent {
    static {
        System.out.println("子类的静态代码块被执行了");
    }
    static int a = 2;
}

上述代码执行结果为:

父类的静态代码块被执行了
子类的静态代码块被执行了
2

但如果调用的静态成员来自于父类,那么执行顺序为自上而下依次加载父类到静态成员源自的父类,要注意这样并没有加载所有类。

public class Main {
    public static void main(String[] args) {
        System.out.println(Child.a);
    }
}

class A {
    static {
        System.out.println("A的静态代码块被执行了");
    }
}

class B extends A{
    static {
        System.out.println("B的静态代码块被执行了");
    }
    static int a = 1;
}



class Parent extends B{
    static {
        System.out.println("Parent的静态代码块被执行了");
    }
}

class Child extends Parent {
    static {
        System.out.println("Child的静态代码块被执行了");
    }
}

上述代码执行结果为:

A的静态代码块被执行了
B的静态代码块被执行了
1

final关键字

final修饰类,表示该类不能被继承;final修饰方法,表示该方法不能被子类重写、隐藏;final修饰变量,表示该变量的值不可修改,但不能不能修饰构造方法。

final class Parent {
}

class Child extends Parent { // 报错

}
class Parent {
    public static final void showA() {
        System.out.println("A");
    }
    public final void showB() {
        System.out.println("B");
    }
}

class Child extends Parent {
    @Override
    public final void showB() { // 报错,方法不能被重写
        System.out.println("childB");
    }

    public static void showA() { // 报错,方法不能被隐藏
        System.out.println("childA");
    }
}
class Parent {
    private final int x = 1;
    private static final int y = 2;
    public void show() {
        final int z = 3;
        x = 2; // 报错,不能被修改
        y = 3; // 报错
        z = 4; // 报错
    }
}

final修饰的属性一般叫常量,一般用XX_XX_XX命名,修饰的属性在定义时必需赋初值,并且为不能再修改,赋值可以在如下位置:

  • 定义时,例如:public final double PI = 3.1415926
  • 在构造器中
  • 在代码块中

如果修饰的属性是静态的,则初始化的位置只能是定义时或静态代码块中,不能在构造器中赋值。

static final 修饰的变量是 编译期常量基本数据类型String),编译器会进行常量折叠优化,直接在字节码中替换变量的值,而不是在运行时访问变量,也就不在加载类。

public class Main {
    public static void main(String[] args) {
        System.out.println(Parent.PI);
    }
}

class Parent {
    public static final double PI = 3.14;
    static {
        System.out.println("静态代码块被执行了");
    }
}

上述代码执行结果为:

3.14

但如果变量不是编译器常量,或值在编译期无法确定,比如调用非编译期可知的函数对象创建依赖外部输入等,编译器不会进行优化,依然需要加载类。

抽象类

抽象类不能被实例化的类,专门用来被子类继承,提供基本行为和通用方法。和一般的类相比,抽象类还可以定义抽象方法,就是没有方法体的方法,但不是必需定义抽象方法。

抽象类的语法如下:

abstract [访问修饰符] class 类名 {
	
}

抽象方法的语法如下:

abstract [访问修饰符] class 类名 {
	[访问修饰符] abstract 返回值类型 方法名;
}

[访问修饰符]不可以是private,方法也不能用finalstatic修饰,这与重写相违背。

如果一个类定义了抽象方法,那么这个类就必须被声明为抽象类。

抽象类不能被实例化,但是可以访问类变量,调用类方法。