黑马程序员Java基础笔记完整整理与实战解析
简介:《黑马程序员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”。而这,正是我们需要不断打磨的底层功力 💪。
希望这篇融合了理论深度与工程实践的分享,能帮你打破认知边界,在下一个技术难题面前,多一份从容与笃定。🌟
简介:《黑马程序员Java基础笔记》是一份系统全面的Java入门学习资料,涵盖Java基础语法、集合框架、多线程、IO操作、反射机制及正则表达式等核心内容。通过详细讲解变量类型、控制流程、集合实现原理、线程创建与同步、文件读写、对象序列化、动态反射和正则匹配等知识点,帮助初学者快速掌握Java编程基础并具备独立开发能力。笔记结合大量实际应用场景,是Java初学者构建扎实编程功底的理想学习资源。
更多推荐




所有评论(0)