本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《黑马程序员Java基础笔记》是一份系统全面的Java入门学习资料,涵盖Java基础语法、集合框架、多线程、IO操作、反射机制及正则表达式等核心内容。通过详细讲解变量类型、控制流程、集合实现原理、线程创建与同步、文件读写、对象序列化、动态反射和正则匹配等知识点,帮助初学者快速掌握Java编程基础并具备独立开发能力。笔记结合大量实际应用场景,是Java初学者构建扎实编程功底的理想学习资源。

Java核心编程深度解析:从基础语法到运行时机制的工程实践

在现代企业级Java开发中,我们早已不再满足于“能跑就行”的编码方式。一个真正优秀的工程师,必须穿透语言表层的API调用,深入理解其背后的内存模型、执行逻辑与设计哲学。无论是排查线上诡异的并发问题,还是优化高流量场景下的集合性能,亦或是调试框架底层的动态代理行为——这些都要求我们对Java的每一个“基本功”有远超常人的洞察。

今天,我们就以实战视角重新审视那些看似熟悉却极易被误解的核心概念。不谈教科书式的定义堆砌,而是聚焦于真实项目中的陷阱识别、性能权衡和架构演进思路。准备好了吗?让我们从最“基础”却又最易出错的地方开始。


你有没有遇到过这样的情况:两个字符串明明内容一样, .equals() 却返回 false ?或者某个对象改了值,结果另一个毫不相干的对象也跟着变了?这些问题的背后,往往不是业务逻辑写错了,而是你对 值类型与引用类型的本质区别 理解得还不够透彻。

Java程序的基本单位是类( class ),入口方法固定为 public static void main(String[] args) 。这几乎是每个初学者背得滚瓜烂熟的知识点。但真正关键的是,Java采用强类型系统,并内置8种基本数据类型:整型家族包括 byte short int long ;浮点型有 float double ;再加上字符型 char 以及布尔型 boolean 。它们都是 值类型 ,存储在栈上,赋值时直接拷贝内容。

而像 String 、自定义类等则是引用类型,变量本身只是一个指针,指向堆中实际的对象实例。

int age = 25;                    // 值类型,存储在栈中
String name = "ZhangSan";        // 引用类型,对象在堆中

注意这里的细节:局部变量必须显式初始化,否则编译报错;而类的成员变量则会被自动赋予默认值——比如 int 0 ,引用类型是 null 。这个差异看似微不足道,但在大型系统中可能埋下空指针隐患。

更值得警惕的是参数传递过程中的“假共享”。很多人误以为Java既有“值传递”又有“引用传递”,其实不然 —— Java只有值传递 !只不过对于引用类型来说,传的是“引用的副本”。

举个例子:

void modify(Object obj) {
    obj = new Object();  // 只改变了形参指向,并不影响外部实参
}

上面这段代码无论你怎么调用,原始传入的对象都不会受影响。因为 obj 只是外面那个引用的一份拷贝,你让它指向新对象,原引用依然稳如泰山。但如果改成这样:

void changeState(MyBean bean) {
    bean.setName("updated");
}

这就不同了!虽然还是值传递,但两个引用(实参与形参)此刻指向同一个堆对象,通过任何一个都能修改其内部状态。这就是为什么我们在设计不可变类时要格外小心,稍有不慎就会导致外部意外篡改。

所以啊,别再纠结“是不是引用传递”这种伪命题了 🙃。记住一句话就够了: 传递的是变量的值,仅此而已 。理解这一点,才能避免写出那种“我以为改了,其实没改”的坑爹代码。


说到对象的状态管理,就不得不提面向对象三大特性:封装、继承、多态。这三个词听起来像是面试必背八股文,但如果你只停留在“知道名字”的层面,那在复杂系统中迟早要栽跟头。

先来看 封装 。它的核心思想不是简单地加个 private 完事,而是 通过访问控制实现信息隐藏,从而保护对象的内在一致性 。我们来看一个典型的银行账户实现:

public class BankAccount {
    private String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Initial balance cannot be negative");
        }
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive");
        }
        this.balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) {
            return false;
        }
        this.balance -= amount;
        return true;
    }

    public double getBalance() {
        return balance;
    }

    private void logTransaction(String type, double amount) {
        System.out.println("Transaction: " + type + ", Amount: " + amount);
    }
}

看到没?所有字段都是 private ,构造函数做了合法性校验,操作方法内嵌业务规则。哪怕将来你要引入审计日志、风控拦截甚至余额冻结功能,只要接口不变,调用方完全无感。这才是封装的真正价值: 把变化关进笼子里,对外暴露稳定的契约

那么四种访问修饰符到底该怎么选呢?

修饰符 同一类 同一包 子类(不同包) 其他包
private
default
protected
public

这里有个黄金法则: 优先使用最小权限 。除非必要,永远不要暴露 public 字段;能用 private 就不用 protected ;慎用包级私有(default),因为它会让测试变得脆弱。

顺便说一句,很多人喜欢用Lombok的 @Data 一键生成getter/setter,图省事可以理解,但在核心领域模型中这么做其实是危险的。因为你等于把所有属性都开放了修改通道,破坏了封装性。更好的做法是只暴露必要的行为方法,而不是裸露数据。

classDiagram
    class BankAccount {
        -String accountNumber
        -double balance
        +BankAccount(String, double)
        +void deposit(double)
        +boolean withdraw(double)
        +double getBalance()
        -void logTransaction(String, double)
    }

这张UML图清晰展示了封装的结果:私有成员被隐藏,公共方法构成对外接口。这才是一个健壮类应有的样子 ✅。


再来看 继承 。它是代码复用的重要手段,但也最容易被滥用。想象一下,你的基类叫 BaseService ,里面塞满了各种通用工具方法,子类一个个继承下来……几年后这个类膨胀到了上千行,谁都不敢动,改一处可能导致十几个服务出问题。这就是典型的“脆弱基类问题”。

我们看个动物发声的例子:

abstract class Animal {
    protected String name;

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

    public abstract void makeSound();

    public void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(name + " barks: Woof!");
    }
}

class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(name + " meows: Meow!");
    }
}

这里 Animal 是一个抽象类,强制子类实现 makeSound() ,同时提供可复用的 sleep() 方法。 super(name) 确保父类构造器被正确调用, @Override 注解防止拼写错误导致意外重载。

重点来了:JVM是如何决定到底调哪个方法的?答案是—— 动态方法调度

sequenceDiagram
    participant JVM
    participant Ref as Reference Type
    participant Obj as Object Type
    JVM->>Ref: 编译时确定引用类型
    Ref->>Obj: 运行时根据实际对象类型
    Obj->>JVM: 动态绑定调用具体实现

即使你写的是 Animal a = new Dog(); ,当调用 a.makeSound() 时,JVM也不会看左边的 Animal 类型,而是去查右边那个 Dog 对象的虚方法表(vtable),找到它重写的版本执行。这就是所谓的“运行时多态”。

不过请注意,静态方法、 private 方法、构造器都不参与这种动态绑定,它们在编译期就决定了。

关于继承的设计原则,我总结了几条血泪经验:
- 能用组合就别用继承;
- 避免多重继承(Java通过接口解决);
- 父类应定义稳定契约,避免频繁变更;
- protected 成员尽量少用,防止子类过度依赖内部实现。

事实上,“组合优于继承”已经成为现代OOP的共识。Spring框架里那么多 *Aware 接口、各种模板模式,本质上都是在用组合替代继承带来的紧耦合问题。


说到多态,它才是OOP最迷人的地方。同一段代码,面对不同类型表现出不同行为,这让我们的系统具备了极强的扩展性。

继续上面动物园的例子:

public class Zoo {
    public static void performSounds(List<Animal> animals) {
        for (Animal animal : animals) {
            animal.makeSound(); // 多态调用
        }
    }

    public static void main(String[] args) {
        List<Animal> zooAnimals = Arrays.asList(
            new Dog("Buddy"),
            new Cat("Whiskers")
        );
        performSounds(zooAnimals);
    }
}

输出结果会是:

Buddy barks: Woof!
Whiskers meows: Meow!

看到了吗? performSounds 方法根本不知道具体是什么动物,但它能自动调用对应的行为。如果哪天要加个鸟会飞的功能怎么办?

interface Flyable {
    void fly();
}

class Bird extends Animal implements Flyable {
    public Bird(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(name + " chirps: Tweet!");
    }

    @Override
    public void fly() {
        System.out.println(name + " is flying.");
    }
}

完美!既保留了动物的基本特征,又额外实现了飞行能力。接口让这种能力拼装变得无比灵活。

多态的应用场景非常广泛:

场景 技术手段 优势
框架扩展点 定义抽象类/接口 支持插件式架构
策略模式 接口+多种实现 行为可替换,符合开闭原则
工厂模式 返回父类型引用 解耦创建与使用
Spring Bean注入 接口注入具体实现 支持AOP、事务代理等横切关注点

💡 小贴士:JVM通过 方法区中的虚方法表 (Virtual Method Table, vtable)实现快速动态绑定。每个类加载时生成vtable,存储所有可被重写的方法地址,调用时通过查表定位实际方法。这也是为什么重写方法不能改变访问权限的原因之一——否则vtable结构会被破坏!

总之,封装、继承、多态三者相辅相成:封装保障安全,继承促进复用,多态提升弹性。掌握它们的协同机制,是你构建高质量应用的第一步。


进入高并发时代后,多线程编程成了绕不开的话题。但你知道吗?很多所谓的“并发bug”,其实源于对线程创建方式的根本误解。

最常见的两种方式是:继承 Thread 类 or 实现 Runnable 接口。

// 方式一:继承Thread(不推荐)
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("当前线程:" + Thread.currentThread().getName());
    }
}
new MyThread().start();

看着挺直观对吧?但它有几个致命缺点:
- 强耦合:任务逻辑和线程机制绑死了;
- 扩展性差:Java不支持多重继承,你再也无法继承其他类;
- 难以复用:没法丢进线程池,资源利用率低。

相比之下,实现 Runnable 就聪明多了:

// 方式二:实现Runnable(推荐)
class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("任务正在由 " + Thread.currentThread().getName() + " 执行");
    }
}
Thread t = new Thread(new Task());
t.start();

瞧见区别了吗? Task 现在只是一个纯粹的任务载体,至于谁来执行、怎么调度,交给外部决定。这不仅符合单一职责原则,还能轻松接入线程池、Future等高级并发工具。

对比维度 继承Thread 实现Runnable
耦合性
可扩展性 差(无法继承其他类) 好(可实现多个接口)
线程资源共享 不易共享 易于共享同一Runnable实例
与线程池兼容性
推荐程度 ❌ 不推荐 ✅ 推荐

而且从JVM角度看, Thread 对象本身是个重量级结构,包含独立的栈空间、PC寄存器等。频繁创建会加重GC负担。而 Runnable 作为轻量级函数式接口(尤其配合Lambda),更适合短期任务调度。

classDiagram
    class Thread {
        +start()
        +run()
        +interrupt()
    }
    class Runnable {
        <<interface>>
        +run()
    }

    Thread <|-- MyThread : extends
    Runnable <|.. Task : implements
    MyThread : -String taskName
    Task : -String taskId
    note right of MyThread
      强耦合,难以复用
    end note
    note left of Task
      解耦设计,适合线程池
    end note

这张类图直观展示了两者的结构差异。所以请答应我:除非特殊需求,永远选择 Runnable 好吗?🙏


Runnable.run() 有个硬伤:返回类型是 void ,没法带回计算结果。这时候就得请出 Callable<V> Future<V> 组合拳了!

import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Callable<Integer> task = () -> {
            System.out.println("开始计算...");
            Thread.sleep(2000);
            return 42; // 模拟耗时计算结果
        };

        Future<Integer> future = executor.submit(task);

        System.out.println("主线程继续执行其他工作...");

        Integer result = future.get(); // 阻塞直到结果可用
        System.out.println("计算结果:" + result);

        executor.shutdown();
    }
}

关键点解析:
1. Callable.call() 有返回值且允许抛异常;
2. 必须通过 submit() 提交,返回 Future 句柄;
3. future.get() 是阻塞的,主线程可以在此之前做别的事;
4. 如果任务失败,异常会被包装成 ExecutionException 抛出。

Future 还提供了几个实用方法:

方法 功能描述
V get() 获取结果,阻塞直到完成
V get(long timeout, TimeUnit unit) 超时获取结果
boolean isDone() 判断任务是否已完成
boolean cancel(boolean mayInterruptIfRunning) 尝试取消任务
boolean isCancelled() 是否已被取消

举个带超时控制的实际案例:

if (!future.isCancelled() && future.isDone()) {
    try {
        String data = future.get(3, TimeUnit.SECONDS);
        System.out.println("成功获取数据:" + data);
    } catch (TimeoutException e) {
        System.err.println("超时未响应,尝试取消...");
        future.cancel(true); // 中断运行中的任务
    }
}

这套模式特别适合远程API调用这类对响应时间敏感的操作。

不过 Future 也有短板: get() 太霸道,破坏异步流;缺乏链式编排能力;异常处理繁琐。于是,在Java 8中迎来了进化版—— CompletableFuture

CompletableFuture.supplyAsync(() -> fetchUserData())
                 .thenApply(this::validateUser)
                 .exceptionally(this::handleError)
                 .thenAccept(System.out::println);

看,这才是真正的异步编程范式:非阻塞、可组合、声明式!从此告别层层嵌套的回调地狱 😎。


要想写出健壮的并发程序,你还得搞懂线程的生命周期。毕竟,死锁、活锁、饥饿等问题,根源都在状态转换上。

Java线程共有六种状态:

状态 描述
NEW 线程刚创建,尚未调用 start()
RUNNABLE 正在JVM中执行,可能正在运行或等待OS调度(就绪态)
BLOCKED 等待监视器锁进入同步块/方法
WAITING wait() join() LockSupport.park() 无限期等待
TIMED_WAITING sleep(long) wait(timeout) 等带有超时参数的方法进入定时等待
TERMINATED 线程执行完毕或因未捕获异常终止

它们之间的转换关系如下:

stateDiagram-v2
    [*] --> NEW
    NEW --> RUNNABLE: start()
    RUNNABLE --> BLOCKED: 请求synchronized锁失败
    BLOCKED --> RUNNABLE: 获取锁
    RUNNABLE --> WAITING: obj.wait(), thread.join(), LockSupport.park()
    WAITING --> RUNNABLE: obj.notify()/notifyAll(), 被中断
    RUNNABLE --> TIMED_WAITING: sleep(ms), wait(timeout), join(timeout)
    TIMED_WAITING --> RUNNABLE: 超时、被唤醒或中断
    RUNNABLE --> TERMINATED: run()结束 或 抛出未捕获异常
    WAITING --> TERMINATED: 被中断且未处理
    TIMED_WAITING --> TERMINATED: 被中断且未处理
    note right of BLOCKED
      竞争synchronized同步区域
    end note
    note left of WAITING
      需显式唤醒(notify)
    end note

重点区分:
- BLOCKED vs WAITING :前者是抢锁失败被动等待,后者是主动放弃CPU;
- WAITING 必须靠 notify 唤醒,而 TIMED_WAITING 可以自动恢复;
- 一旦进入 TERMINATED ,就再也回不去了,别想着重启线程哦!

生产环境中如何诊断线程问题?可以用下面这个小工具:

public class ThreadDumper {
    public static void dumpAllThreads() {
        Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
        for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) {
            Thread t = entry.getKey();
            Thread.State state = t.getState();
            if (state == Thread.State.BLOCKED || state == Thread.State.WAITING) {
                System.out.printf("线程[%s]处于%s状态\n", t.getName(), state);
                StackTraceElement[] stack = entry.getValue();
                for (int i = 0; i < Math.min(stack.length, 5); i++) {
                    System.out.println("\tat " + stack[i]);
                }
            }
        }
    }
}

配合 jstack 命令,能精准定位到哪一行代码卡住了,简直是救火神器 🔥。


最后聊聊反射,这项让Java框架得以“魔法般”运作的技术。

反射的本质是 运行时自省 ,即程序可以在运行期间检查、调用甚至修改类的结构。Spring的IoC容器、MyBatis的ORM映射、JUnit的测试驱动……背后全靠它撑腰。

第一步是获取 Class 对象,有三种方式:

方式 语法示例 适用场景
类名.class String.class 编译期已知类型,常用于工具类或泛型约束
实例.getClass() "hello".getClass() 运行时通过对象获取类型信息
Class.forName() Class.forName("java.util.ArrayList") 动态加载类,适合配置驱动加载(如 JDBC)
Class<String> stringClass1 = String.class;
Class<?> stringClass2 = "example".getClass();
try {
    Class<?> stringClass3 = Class.forName("java.lang.String");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

其中 Class.forName() 最为强大。比如JDBC中经典的那句:

Class.forName("com.mysql.cj.jdbc.Driver");

它并不是为了得到一个Class对象,而是触发Driver类的静态代码块注册自己到 DriverManager 中。这就是所谓的“类加载副作用”,也是SPI机制的基础。

更厉害的是,反射还能突破访问限制,调用私有构造器和方法:

class SecretClass {
    private SecretClass(String msg) {
        System.out.println("Private constructor called with: " + msg);
    }

    private void secretMethod(int value) {
        System.out.println("Private method executed with value: " + value);
    }
}

// 反射调用
Constructor<?> cons = clazz.getDeclaredConstructor(String.class);
cons.setAccessible(true);
Object instance = cons.newInstance("Hello Reflection");

Method method = clazz.getDeclaredMethod("secretMethod", int.class);
method.setAccessible(true);
method.invoke(instance, 42);

输出结果:

Private constructor called with: Hello Reflection
Private method executed with value: 42

注意 setAccessible(true) 这一步,它会关闭Java的安全检查,相当于开了后门。虽然方便,但也带来安全隐患,所以在安全管理器开启的环境下可能会被阻止。

这项技术广泛应用于JSON反序列化框架(如Jackson)、ORM工具(如Hibernate)中,用来还原包含私有字段的对象状态。

基于此,我们可以构建通用的事件处理器或插件系统。比如下面这个简单的“通用调用器”:

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class MethodInvocationUtil {
    private final Map<String, Method> methodCache = new HashMap<>();

    public Object invokeMethod(Object target, String methodName, Object... args) 
            throws Exception {
        Class<?> clazz = target.getClass();
        String key = generateKey(clazz, methodName, args);
        Method method = methodCache.computeIfAbsent(key, k -> {
            Class<?>[] paramTypes = new Class[args.length];
            for (int i = 0; i < args.length; i++) {
                paramTypes[i] = args[i].getClass();
            }
            try {
                return clazz.getMethod(methodName, paramTypes);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("Method not found: " + methodName, e);
            }
        });

        return method.invoke(target, args);
    }

    private String generateKey(Class<?> clazz, String method, Object[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append(clazz.getName()).append(".").append(method);
        for (Object arg : args) {
            sb.append("-").append(arg.getClass().getSimpleName());
        }
        return sb.toString();
    }
}

这个工具通过缓存 Method 实例避免重复查找,提升了性能;利用 getMethod() 实现公共方法的动态调度,非常适合做轻量级服务路由或脚本引擎。

classDiagram
    class MethodInvocationUtil {
        -Map<String, Method> methodCache
        +Object invokeMethod(Object, String, Object[])
        -String generateKey(Class, String, Object[])
    }
    class Method
    class Object
    MethodInvocationUtil --> Method : caches
    MethodInvocationUtil --> Object : invokes on

当然,反射也不是银弹。它的主要代价是性能损耗(首次调用慢10倍以上)和安全性风险。因此建议:
- 尽量缓存 Field / Method 对象;
- 生产环境慎用 setAccessible(true)
- 优先考虑编译期解决方案(如APT、字节码增强)替代运行时反射。


回顾整篇文章,我们从最基本的变量存储讲到复杂的运行时动态调用,贯穿了Java编程的核心脉络。你会发现,所谓“高级特性”,不过是基础概念在不同场景下的组合与延伸。

真正拉开开发者差距的,从来不是会不会用某个API,而是能否在关键时刻说出:“这个问题的本质是XXX”。而这,正是我们需要不断打磨的底层功力 💪。

希望这篇融合了理论深度与工程实践的分享,能帮你打破认知边界,在下一个技术难题面前,多一份从容与笃定。🌟

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《黑马程序员Java基础笔记》是一份系统全面的Java入门学习资料,涵盖Java基础语法、集合框架、多线程、IO操作、反射机制及正则表达式等核心内容。通过详细讲解变量类型、控制流程、集合实现原理、线程创建与同步、文件读写、对象序列化、动态反射和正则匹配等知识点,帮助初学者快速掌握Java编程基础并具备独立开发能力。笔记结合大量实际应用场景,是Java初学者构建扎实编程功底的理想学习资源。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐