一,世界诞生的前夜[序言]

面向对象程序设计的特点

结构化编程:数据与操作分离,扩展性与复用性差,不适合复杂系统建模。过程与算法

面向对象程序设计:关注“谁来做”,分析系统中的对象、职责和协作关系 。基本单位是类/对象。四大核心机制:封装、继承、多态、对象协作 。

JAVA语言核心特点

完全面向对象:除了 8 个基本数据类型值之外,一切都是对象。所有的设计必须都在类中实现

跨平台性:一次编写,到处运行。Java 源码编译后生成平台无关的字节码(.class)

对比C++:放弃了 C++ 的全局变量、goto语句、宏定义 、结构体、指针、多重继承、友元类和运算符重载,单重继承 + 接口的方式代替多重继承,自动垃圾回收,强类型机制和异常处理

JDK、JRE、JVM

JVM:Java虚拟机,核心部分,负责执行字节码(.class),具有平台独立性

JRE:Java运行环境,包含 JVM + 核心类库

JDK:Java开发工具包,包含 JRE + 开发工具,如编译工具 javac.exe,打包工具 jar.exe,供开发人员使用

编写.java->编译.class->运行(JVM来解释执行字节码)

Java 基础语法规范与命名规则

类是 Java 的最小程序单位,所有代码必须写在类里

程序的执行入口 public static void main(String[] args)

源文件名必须与该 public 类的类名完全一致;一个 Java 源文件里最多只能定义一个 public 类

标识符只能由字母、数字、下划线(_)和美元符号($)组成,数字不能开头

关键字都是小写

二,开天辟地的篇章[类与对象]

基本数据类型&引用数据类型

byte:1 short:2 int:4 long:8(0L)

float:4(0.0f) double:8(0.0d) char:2(\u0000) boolean:1/8(false)

存储位置:基本数据类型保存在栈内存中;引用数据类型变量中保存的是对象的引用地址(指针),实际的对象实体存储在堆内存

赋值:基本类型复制的是值,修改新变量,原变量不会改变;引用类型复制的是地址,通过 s2 修改属性,s1 的属性也会同步改变

局部变量推断 var 关键字

使用 var 声明局部变量时,编译器会根据右边的初始值自动推断类型

不能用于1.类的成员变量,2.方法的参数类型,3.方法的返回值类型,4.声明时必须立刻赋值

基本数据类型转换

  1. 自动类型转换:表数范围小的类型可以自动转换为表数范围大的类型

$$ \text{byte} \rightarrow \text{short} \rightarrow \text{int} \rightarrow \text{long} \rightarrow \text{float} \rightarrow \text{double} $$

  1. 强制类型转换与类型提升

字符串转基本类型使用类似Integer.parseInt(String s)

数组

Java 中的数组必须作为对象来处理,是一种特殊的引用数据类型

特点:一个数组中所有元素必须是同一类型,数组一旦初始化完成,长度不可改变,数组既可以存储基本类型,也可以存储引用类型,在声明数组变量时不能指定长度,但在创建数组对象时必须指定长度(静态初始化除外)

存放情况:栈:存放数组的引用变量(保存指向堆内存的地址),堆:存放真正的数组元素实体

栈被线程私有,每个线程独立拥有自己的栈;堆被线程共享,所有线程均可访问堆

栈主要存储:局部变量、对象引用变量、方法调用信息

堆主要存储:实际的对象实例、数组实体、成员变量

获取数组长度:使用 .length 属性

获取String字符串长度:使用 .length() 方法

如果创建的是引用类型数组(如 Student[] students = new Student[2];),动态初始化后,数组里的每个元素并没有创建对象,其默认值全部是 null。必须后续手动为每个元素实例化

Java 语言里没有真正的多维数组,只是数组里的每个元素又是一个数组,Java 允许二维数组的每一行长度不同

类与对象的基本概念

:是对具有相似属性和行为的一组对象的统一抽象和描述

对象:是客观事物在程序中的数值化描述与实体,是类的一个具体实例

成员变量、方法、构造器、初始化块、内部类。其中成员变量、方法和构造器是最常见的三大结构。

成员变量&局部变量

对比维度成员变量局部变量
定义位置类体内,方法体之外。方法体内、方法形参、代码块内。
内存存储位置实例变量存于堆内存(随对象共存) ; 类变量存于方法区。均存于栈内存中(随方法调用共存)。
生命周期随对象的创建而诞生,随对象的销毁/垃圾回收而消失。随方法调用而诞生,方法结束立即自动释放。
默认初始值系统会自动分配默认初始值。系统不会分配默认值!必须显式初始化后才能使用,否则编译报错。

类变量与实例变量的区别

实例变量(无 static):属于具体的对象。每创建一个对象,堆内存中就会多出一份独立的实例变量空间,各对象互不影响。

类变量 / 静态变量(有 static):属于类本身。在类加载时即分配空间,全类仅此一份,所有该类的对象共同分享这一份数据。可以通过 类名.变量名 直接访问。

形参传递、重载、可变参数

Java 的参数传递只有一种机制:值传递。引用数据类型传入:传递的是地址值的副本,在方法内通过形参修改对象的属性,会影响到外部实参对象的值

重载Overload:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同

形参个数可变的方法:语法为 数据类型... 参数名(例如 int... nums)。被当成数组来处理,一个方法中最多只能有一个可变参数,且它必须位于形参列表的最后一位。

构造器的构造与重载

作用:配合 new 关键字完成对象的创建,并对实例变量进行显式初始化

构造器名必须与类名完全相同,无返回类型。如果没有手动编写任何构造器,系统会自动隐式提供一个无参的默认构造器。但一旦我们定义了任何一个构造器,系统将不再提供默认的无参构造器

构造器可以重载。在一个构造器内部,可以使用 this(形参列表); 来显式调用本类中的另一个重载构造器,而且必须放在该构造器内的第一行执行。

隐藏与封装

Java 通过将对象的属性和实现细节隐藏在对象内部,仅对外提供公共的访问方法来实现封装

访问控制符本类内部同一个包子类外部
private
default (省略不写)
protected
public

三,赋予对象以新生[继承与多态]

继承

使用 extends 关键字实现。一个子类最多只能有一个直接父类,子类继承父类的全部成员变量和方法,不能继承父类的构造器

方法重写Override

当子类觉得父类的方法不满足需求时,可以对父类的方法进行重写

要求方法名相同、形参列表相同。子类方法的返回值类型应该比父类方法更小或相等;子类方法声明抛出的异常类应该比父类方法更小或相等。子类方法的访问权限应该比父类方法更大或相等

super

用于限定在子类对象中显式调用父类被覆盖的实例方法或被隐藏的实例变量。

子类的构造器首行默认都会隐式调用父类的无参构造器。如果父类没有无参构造器,子类构造器首行必须显式使用 super(形参列表); 来调用父类的带参构造器

this(...)super(...) 都要抢占构造器的第一行,因此它们不能同时出现在同一个构造器中。

多态性与转型

多态的前提:必须有继承关系(或接口实现)以及方法重写

BaseClass obj = new SubClass();

编译时类型(左边):由声明变量时的类型决定。编译器只允许调用编译时类型里包含的方法

运行时类型(右边):由实际赋给变量的对象决定。

向上转型:子类对象赋给父类引用,系统自动允许。目的是让代码依赖于抽象而非具体,从而实现多态、解耦和开闭原则

向下转型:如果想调用子类特有的方法,必须将父类引用强转为子类类型。

初始化块

Java 的初始化块分为普通实例初始化块静态初始化块static )。

当存在继承关系时,类的加载与对象实例化的绝对顺序:

  1. 父类静态初始化块(仅在类第一次加载时执行一次)。
  2. 子类静态初始化块(仅在类第一次加载时执行一次)。
  3. 父类普通实例初始化块(每次创建对象都会执行)。
  4. 父类构造器 体内的代码。
  5. 子类普通实例初始化块(每次创建对象都会执行)。
  6. 子类构造器 体内的代码。

基本数据类型的包装类

对应关系与转换机制对应关系int $\rightarrow$ Integerchar $\rightarrow$ Character,其余均为首字母大写

使用静态方法 valueOf() 转换至包装类;使用实例方法 xxxValue() 转换至基本类型

基本类型和包装类之间可以自动转换

Integer 类底层默认缓存了 $-128 \sim 127$ 之间的实例 。

static和final

static 关键字属于类本身,全类仅此一份,在类加载时分配内存。

静态方法内绝对不能访问非静态成员,因为静态方法执行时,堆内存中可能还根本没有创建任何实例对象。静态方法中绝对不能出现 thissuper 关键字,因为静态方法不绑定到具体的对象实例。

final 关键字

修饰类:该类不能被继承

修饰方法:该方法不能被子类重写

修饰变量:变量一旦获得了初始值,就绝对不能被重新赋值。(修饰引用类型时不能再指向新对象,但是该指针指向的对象内部的成员变量依然是可以被修改的)

abstract关键字和抽象类

抽象方法:只有方法声明,没有方法体(没有{})的方法,必须用 abstract 修饰

抽象类:包含抽象方法的类必须被声明为抽象类 。

抽象类不能被实例化(不能 new 抽象类),它只能用来被子类继承。

抽象类的子类必须重写并实现父类的所有抽象方法,否则该子类也必须定义为抽象类。

抽象类中可以包含普通成员变量、普通实例方法、构造器、静态块等一切普通类的元素。

abstract 绝对不能和 privatestaticfinal 同时修饰同一个方法

四,穿越时空的旅程[接口]

Java 为了坚持单重继承,引入了接口来实现多重组合

接口的特性与修饰符规则

成员变量:接口里定义的变量默认且只能是 public static final,修饰符写不写都一样。

方法:接口里定义的方法默认且只能是 public abstract

对比维度抽象类接口
关键字abstract classinterface
继承/实现限制子类只能单继承一个直接父类一个类可以同时实现多个接口
成员变量可以定义任何类型的变量(包括私有、受保护、非最终变量)只能是隐藏的公有静态常量
构造器有构造器,供子类实例化时调用没有构造器
设计理念差异体现 "is-a"(是一类) 关系,对事物本质的抽象体现 "like-a" / "can-do"(像/能做) 关系,对行为/能力的抽象

内部类

定义在另一个类范围内部的类

非静态内部类(成员内部类)

  • 寄生在外部类的实例之中,可以自由访问外部类的所有成员(包括私有成员)。
  • 内部类实例中隐式保存了外部类实例的引用。

静态内部类

  • 属于外部类本身,不寄生于外部类实例。
  • 静态内部类内只能访问外部类的静态成员,不能访问外部类的实例成员。

匿名内部类

  • 适合创建只需使用一次的类。
  • 匿名内部类不能有构造器,它在创建时会立即显式实现一个接口或继承一个父类

Jar 包与可执行打包机制

Jar 包本质是一种与 zip 兼容的压缩格式,用于将编译好的多个 .class 文件和目录结构打包,方便分发与共享。接收者拿到 Jar 包后,只需在系统的 classpath 环境变量中包含该 Jar 文件,JVM 即可自动加载并识别其内部的包结构 。

Java 集合框架/容器类根基

Collection 接口:保存孤立、单一的元素集合 。它派生出三大子接口:

  1. List:元素有序、可重复(如 ArrayListLinkedList)。
  2. Set:元素无序、不可重复(如 HashSetTreeSet)。
  3. Queue:队列,遵循先进先出。

Map 接口:保存具有映射关系的 Key-Value 关联数组 。

  • Key 绝对不可重复(本质上是一个 Set)。
  • 典型实现类:HashMapTreeMap

三大程序错误分类

语法错误:违反语言语法规范的错误,在编译时被编译器发现 。

语义错误:程序语法完全正确,但运行时由于数据、范围或对象状态等问题导致出错(例如除以0、空指针),在运行时才能被发现 。

逻辑错误:程序运行不报错,但结果不是预期的。

Java 异常体系结构

Java 中所有的异常和错误都衍生自一个共同的根类:Throwable。它有两个核心子类:

Error:是严重的问题(如 VirtualMachineError 虚拟机错误、LinkageError 链接错误)。通常由硬件或 JVM 引起,程序无法通过代码进行捕获和处理。

Exception

运行时异常 / 非检查型异常:通常由程序逻辑不严密引起 ,算术异常,如除以0;空指针异常;数组下标越界异常。

检查型异常:编译器强行要求必须处理,要么用 try-catch 捕获,要么用 throws 声明抛出,否则编译直接报错 。(IOExceptionSQLException)

try-catch-finally捕获处理机制

如果多个 catch 块之间存在父子继承关系,必须子类异常在前,父类异常在后

一旦 try 块中某行代码引发了异常,系统会立刻终止执行 try 块中后续的代码,转去与 catch 块逐个匹配。

无论 try 块中的代码是否引发异常,也无论 catch 块是否捕获到对应的异常,finally 块中的代码都必定会被执行 (就算有return也阻止不了先执行finally的命运,之后再return)

throws与throw

对比维度throw (手动抛出)throws (声明抛出)
定义与含义在方法内部主动创建并抛出一个具体的异常对象实体 。在方法签名上声明该方法可能会抛出的异常类型 。
位置位于方法体内部位于方法名和形参列表之后,是方法签名的一部分 。
后续对象后面只能接一个异常类的对象实例(如 throw new Exception();)。后面接一个或多个异常类名,用逗号隔开(如 throws IOException, Exception)。
强制性如果手动抛出的是检查型异常,必须配合处理 。强制调用该方法的上游开发者必须对其声明的检查型异常进行处理 。

throws:听好了-我这个方法可能会出问题,你看着办

throw:我就是问题

自定义异常

// 1. 定义自定义异常
public class IllegalAgeDataException extends Exception {
    public IllegalAgeDataException() {
        super();
    }
    public IllegalAgeDataException(String message) {
        super(message); // 传入错误提示信息
    }
}

// 2. 配合 throw 与 throws 在业务中应用
public class User {
    private int age;

    public void setAge(int age) throws IllegalAgeDataException { // 声明抛出
        if (age > 0 && age < 100) {
            this.age = age;
        } else {
            // 手动创建并抛出自定义异常对象 
            throw new IllegalAgeDataException("IllegalAgeData: 年龄必须在0到100之间"); 
        }
    }
}

五,世界之外的回应[IO操作]

Java IO 流体系结构

三大分类维度:

按流向分:输入流和输出流。

按处理数据单元分:字节流(操作 8 位的字节)和字符流(操作 16 位的字符,适合处理文本)

按流的角色分:节点流(直接连接到实际的物理节点)和处理流(对一个已存在的流进行包装,提供更强大的读写功能)

四大抽象基类与文件流操作

四大基类:InputStreamOutputStreamReaderWriter

只要打开了 IO 流,必须在 finally 块中显式调用 .close() 方法关闭流,释放物理资源。

Scanner类

是一个基于正则表达式的文本扫描器,可以从输入流、文件或字符串中解析出基本类型值和字符串值

hasNextXxx():判断是否还有下一个输入项

nextXxx():获取下一个输入项

next():一定要读取到有效字符后才可以结束输入,对输入有效字符之前遇到的空白(如空格、Tab、回车),next() 方法会自动将其去掉;只有在输入有效字符后,next() 才会将其后面输入的空白作为分隔符或结束符。

nextLine():以回车符作为结束符,返回的是输入回车之前的所有字符,可以包含空格。

系统类与运行环境

System类

代表当前 Java 程序的运行系统,所有成员变量和方法都是静态(static)的。

标准输入输出:System.inSystem.outSystem.err

runtime类

代表 Java 程序的运行时环境

Runtime 类没有构造器,不能被 new 实例化。每个 Java 程序都有一个与之关联的 Runtime 实例,必须通过静态方法 Runtime.getRuntime() 获取。可以调用 .totalMemory().freeMemory() 查看 JVM 内存,或者通过 .exec(String command) 来直接运行操作系统的其他可执行程序。

字符串

String类的不可变性:一旦创建,其内容在内存中绝对无法更改。所有的拼接、截取操作,本质上都是在堆内存中创建了一个全新的 String 对象。

== 比地址(两个引用是否指向同一个对象).equals() 比内容(两个字符串的字面值是否相同)

StringBuffer&StringBuilder(可变字符序列)

需要频繁拼接字符串时,为了避免产生大量内存垃圾,必须使用可变字符序列。

StringBuffer:线程安全,内部方法使用了 synchronized 修饰,速度稍慢

StringBuilder:线程不安全,没有并发锁,速度极快

常用数学与工具类

Math:包含初等数学运算的静态方法

Random:用于生成伪随机数

BigDecimal:提高浮点精度

正则表达式

Java 通过 java.util.regex 包下的 PatternMatcher 提供正则表达式支持 。

只匹配一次使用 str.matches(regex) ;匹配大量字符串使用:Pattern + Matcher

Java 序列化机制

序列化:将一个 Java 对象转换为字节序列,使其可以保存在磁盘上或通过网络传输,脱离程序独立存在 。

反序列化:从 IO 流的字节序列中恢复该 Java 对象 。

一个类如果想支持序列化,必须实现 java.io.Serializable 接口或者 Externalizable 接口 。

如果类中的某个成员变量被 transient 修饰,在序列化时该变量的内容会被彻底忽略,反序列化回来后该变量会变成其对应类型的默认初始值

六,虚实交错的魔法[进程与线程]

基本概念

进程是系统进行资源分配和调度的一个独立单位。每个运行中的程序就是一个进程,进程拥有自己独立的内存空间(数据区、代码区等)。进程之间相互隔离,通信成本较高。

线程是进程内部的一个顺序执行流,是 CPU 调度和分派的基本单位,一个进程可以包含多个线程,它们共享该进程所拥有的全部资源(如堆内存和方法区),但每个线程拥有自己独立的程序计数器和栈内存。

创建和启动线程的两种标准方法

方法一:继承 Thread

定义一个类继承 Thread 类。重写 run() 方法,将线程的执行代码写在其中。创建该类的实例,并调用 .start() 方法启动线程。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("子线程正在运行");
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); //不是.run()
    }
}

方法二:实现 Runnable 接口

定义一个类实现 Runnable 接口。实现 run() 方法。创建该实现类的实例,将其作为参数传给 new Thread(target),最后调用 .start()

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("基于接口的子线程正在运行");
    }
}
public class Test {
    public static void main(String[] args) {
        MyRunnable target = new MyRunnable();
        Thread t = new Thread(target); 
        t.start();
    }
}

实现 Runnable 接口的优势

  1. 避免了 Java 单重继承的限制,子类还可以继承其他父类。
  2. 适合多个相同的线程去处理同一个资源的情况,把虚拟 CPU和代码、数据有效分离,更符合面向对象的设计思想。

线程的生命周期与状态转换

一个线程由创建到消亡,会经历 5 种基本状态

  1. 新建:当 new 了一个线程对象后,该线程就处于新建状态。此时它仅由 JVM 分配了内存。
  2. 就绪 / 可运行:线程对象调用了 .start() 方法后。此时线程位于可运行线程池中,等待获取 CPU 的使用权。
  3. 运行:就绪状态的线程获得了 CPU 资源,开始执行 run() 方法体的代码。
  4. 阻塞:线程因为某些原因放弃了 CPU 使用权,暂时停止运行。主要有:同步阻塞:运行的线程在获取对象的同步锁(synchronized)失败时。等待阻塞:运行的线程调用了 wait() 方法,JVM 会把该线程放入等待队列中。其他阻塞:调用了 Thread.sleep(long ms)join() 方法,或者发出了 I/O 请求时。
  5. 死亡run() 方法正常执行结束,或者因为未捕获的异常导致异常退出。线程一旦死亡,不能再次调用 .start() 重启。

线程同步机制与并发安全

当多个线程并发读写同一个共享资源时,会产生数据不一致的问题。

同步代码块与同步方法:Java 引入了同步监视器(锁)来解决并发冲突,确保同一时刻只有一个线程能进入独占代码:

synchronized(obj) {
    // 需要保证线程安全的临界区代码
}

线程死锁:两个或多个线程互相持有对方所需的锁,同时又等待获取对方手里的锁,导致所有线程都处于无限期等待的状态。

线程间通信

为了解决线程之间的协作配合,不能使用轮询,必须使用显式通信机制。

wait():让当前线程释放它持有的锁,并让线程进入等待队列中隐蔽,直到其他线程唤醒它。

notify():随机唤醒一个在同步监视器上等待的线程。

notifyAll():唤醒所有在同步监视器上等待的线程,让它们重新参与锁的竞争。

class Buffer {
    private int data;
    private boolean hasData = false; //是否有未消费的数据

    // 生产者调用
    public synchronized void produce(int value) {
        while (hasData) {
            try {
                wait(); // 缓冲区有数据,生产者等待,并释放锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.data = value;
        this.hasData = true;
        System.out.println("生产者生产: " + data);
        notifyAll(); // 唤醒正在等待锁的消费者线程
    }

    // 消费者调用
    public synchronized void consume() {
        while (!hasData) {
            try {
                wait(); // 缓冲区没数据,消费者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费者消费: " + data);
        this.hasData = false;
        notifyAll(); // 唤醒正在等待锁的生产者线程
    }
}