文章目录:
- Lesson 8 完善类的设计
- Lesson 8.5 UML图
- Lesson 9 设计原则与设计模式
- 一、 设计原则
- 二、设计模式
- Lesson 10 异常
- Lesson 11 Java集合框架
- 1. 集合框架的宏观架构
- 2. Collection接口与其子接口(List & Set)
- 3. Map 接口(键值对)
- 4. 迭代器(Iterator)——如何遍历?
- 5. Collections 工具类(注意有个s)
- 6. 泛型(Generics)
- Lesson 12 GUI设计
- Lesson 13 IO
- Lesson 14 多线程
- Lesson 14.2 多线程-2
- Lesson 15 网络
(一):大二上 面向对象课程 复习
先占位,复习中
Lesson 8 完善类的设计
感觉是又总结了一遍前面的知识,主要想说如何设计代码
1. 多态
多态简而言之就是“同一个接口,使用不同的实例而执行不同操作”。 多态存在的三个必要条件:
- 继承 (Inheritance):必须有父子类关系。
- 覆盖 (Override):子类必须重写父类的方法。
- 向上转型 (Upcasting):父类引用指向子类对象(
Father f = new Son();)。
核心考点(出现n遍了):
成员变量(字段):编译运行都看左边。
- 如果你写
Father f = new Son();,然后调用f.age。不管Son类里有没有定义age,系统只看Father类里有没有age。如果有,取父类的值。 - 原因:Java 中字段没有多态性,字段是静态绑定的。
- 如果你写
成员方法(非静态):编译看左边,运行看右边。
- 你调用
f.eat()。编译时,编译器检查Father类有没有eat(),没有就报错(编译失败)。运行时,JVM 会去调用Son类重写过的eat()。 - 这就是“动态绑定”。
- 你调用
静态方法:编译运行都看左边。
- 静态方法不属于对象,属于类,不具有多态性。
多态的三种形式
- 普通类多态:父类是普通类。
- 抽象类多态:父类是
abstract class,强制子类重写抽象方法。 - 接口多态:父类型是
interface,最灵活,也是后续“高内聚低耦合”设计的核心。
2. 转型
向上转型
Person p = new Student();- 性质:自动发生,总是安全的。因为学生一定是人。
- 代价:丢失子类特有的方法。你一旦把
Student当做Person看待,你就不能调用Student特有的study()方法了,只能调用Person定义的方法。
向下转型
Student s = (Student) p;- 性质:强制转换,有风险。
- 目的:为了找回并使用子类特有的功能。
- 风险点:如果你把一个本质是
Teacher的对象强制转成Student,编译时不报错,但运行时会报ClassCastException(类型转换异常)。
安全锁:instanceof 关键字 :是否为类的实例
为了避免运行时异常,在向下转型前必须判断。
- 语法:
obj instanceof ClassName - 判断逻辑:判断
obj是否属于ClassName或其子类的实例。 - 考点:
null instanceof AnyClass永远返回false。
老师的建议:在写代码题时,涉及向下转型,一定要写 if (p instanceof Student) 判断,这能体现你的代码健壮性,是加分项。
3. Object 类与相等性判断
Object 的地位 :它是所有类的根父类。任何类如果没有显式继承别的类,默认继承 Object。
考点:== 与 equals() 的区别
操作符
==:- 对于基本类型:比较值(如
3 == 3)。 - 对于引用类型(对象):比较内存地址。即判断它们是不是同一个对象。
- 对于基本类型:比较值(如
方法
equals():- 在
Object类的默认实现中:它等价于==,也是比较内存地址。 - 但是(转折点),像
String,Date,File等类重写了equals(),改为比较内容是否相同。 - 例子:两个
new String("hello"),用==比较是false(地址不同),用equals()比较是true(内容相同)。
- 在
4.内部类
四种内部类及其特点
实例内部类 (Instance Inner Class):
- 依赖性:必须寄生在外部类的实例上。创建时需要
new Outer().new Inner()。 - 权限:可以直接访问外部类的所有成员(包括 private)。
- 依赖性:必须寄生在外部类的实例上。创建时需要
静态内部类 (Static Inner Class):
- 独立性:不依赖外部类实例。
new Outer.Inner()。 - 权限:只能访问外部类的静态成员。
- 独立性:不依赖外部类实例。
局部内部类 (Local Inner Class):
- 定义在方法里。
- 限制:只能访问方法里的
final变量(或者实际上的 final 变量)。这是为了保证生命周期的一致性(闭包概念)。
匿名内部类 (Anonymous Inner Class):
- 没有名字,通常用于创建接口或抽象类的一次性实现对象。
- 场景:GUI 编程中的监听器,如
new ActionListener() { ... }。 - 编译结果:生成
Outer$1.class文件。
内部类的应用:(这个我觉得有点难理解)
- 我们知道,Java 不支持多重继承(一个类不能 extends 两个父类)。
- 但如果你需要一个类同时拥有 A 类和 B 类的行为,或者需要“隐藏实现细节”,你可以让你的类继承 A,然后内部写一个内部类去继承 B(或实现接口)。
- 回调 (Callback):PPT 中提到的
Adjustable接口例子。通过内部类实现接口,并将这个内部类的引用暴露出去,外部调用者只能通过接口方法操作,而不知道具体的类是谁。这是一种高内聚低耦合的表现。
PPT的例子:
PPT 里的场景是这样的: 有一个父类
Base,它有一个方法叫adjust(int speed)(调节速度)。 现在你要写一个子类Sub,你希望它既能调节速度,又能调节温度(实现Adjustable接口的adjust方法)。冲突来了:
- 父类有一个
void adjust(int)。- 接口也有一个
void adjust(int)。- 如果你的子类
Sub直接extends Base implements Adjustable,那么类里只能有一个adjust方法。你无法区分你是要调速度还是调温度,因为方法签名一模一样!内部类如何解决这个问题(PPT 第 87 页逻辑):
- 外部类
Sub继承Base,保留了调节速度的adjust。- 内部类
Closure实现接口Adjustable,在它的adjust里专门处理温度。- 对外暴露:
Sub提供一个方法getCallBackReference(),返回这个内部类对象。这也就是 PPT 想告诉你的核心考点: 通过内部类,我们可以模拟“多重继承”,并且解决“方法名冲突”的问题。
5. final关键字用法
修饰变量:变为常量。一旦赋值不可修改。
- 注意:如果修饰引用(如
final Student s = new Student();),是指s不能指向别人,但s内部的属性是可以改的。
- 注意:如果修饰引用(如
修饰方法:不可被重写 (Override)。
- 目的:锁定算法流程,防止子类篡改;或者为了效率(关闭动态绑定)。
修饰类:不可被继承。
- 例子:
String类、Math类都是 final 的。
- 例子:
Lesson 8.5 UML图
1. 类:矩形框
在UML里,一个矩形框就代表一个 类(Class)。这个框通常被横线分成三层(三格):
第一格(最上面):类名 (Class Name)
- 内容:写这个类的名字。
特殊情况:
- 如果名字是 斜体 的(或者上面有
<<interface>>字样),说明这是一个 抽象类 (Abstract Class) 或者 接口 (Interface)。 - 如果是正体字,说明是普通的 具体类 (Concrete Class)。
- 如果名字是 斜体 的(或者上面有
第二格(中间):属性 (Attributes/Fields)
- 内容:这里写类的成员变量(即代码里的
int age;或String name;这种)。 格式:
权限符号 变量名 : 变量类型- 例如:
- height : double - 翻译成Java代码就是:
private double height;
- 例如:
- 内容:这里写类的成员变量(即代码里的
第三格(最下面):方法 (Methods/Functions)
- 内容:这里写类的成员方法。
格式:
权限符号 方法名(参数名:参数类型) : 返回值类型- 例如:
+ getArea() : double - 翻译成Java代码就是:
public double getArea() { ... }
- 例如:
2. 类里面的权限符号
这些符号放在变量名或方法名的最前面,代表这个东西谁能用(即Java中的访问权限):
+(Plus):代表 public(公有的)。- 谁都能访问,最开放。
-(Minus):代表 private(私有的)。- 只有这个类自己能用,别人看不见。
#(Hash):代表 protected(受保护的)。- 只有在这个包(package)里,或者这个类的子类(儿子)能用。
~(Tilde):代表 default(包级私有,Java默认)。- 通常省略不写,或者用波浪号。PPT里主要考前三个。
如果在图里看到方法名下面有一条下划线(比如 main),那代表它是 Static(静态)的。
3. 线条和箭头:类与类的关系
请死记硬背以下四种线条的形状和含义:
泛化关系 (Generalization) —— “我是你儿子”
- 图形:实线 + 空心三角形箭头(▷)。
- 含义:继承。箭头指向父类。
- 代码对应:
extends 例子:
Circle(圆)连向Geometry(几何体)。class Circle extends Geometry { ... }
实现关系 (Realization) —— “我遵守合同”
- 图形:虚线 + 空心三角形箭头(▷)。
- 含义:实现接口。箭头指向接口。
- 代码对应:
implements 例子:
Dog连向Animal(如果Animal是接口)。class Dog implements Animal { ... }(注:PPT第17页的 Geometry 如果是接口,就该用虚线箭头)
关联关系 (Association) —— “我有你”
- 图形:实线箭头(➝)。
- 含义:引用。一个类把另一个类作为成员变量(属性)长期持有。这是一种强关系。
- 代码对应:成员变量 (Field)。
PPT例子(第15页):
Pillar(柱子)类里有一个bottom(底面)变量。图中从Pillar画一根实线箭头指向Circle。class Pillar { private Circle bottom; // 实线箭头指向Circle }
依赖关系 (Dependency) —— “我用到你”
- 图形:虚线箭头(⇢)。
- 含义:临时使用。一个类在某个方法里“临时”用到了另一个类(比如作为参数传进来,或者在方法里new了一个局部变量)。这是一种弱关系,用完就扔。
- 代码对应:方法参数 (Parameter) 或 局部变量 (Local Variable)。
例子:人(Person)需要用手机(Phone)打电话。
class Person { public void call(Phone p) { // 虚线箭头指向Phone p.dial(); } }
- 看到
extends-> 画 实线空心三角。 - 看到
implements-> 画 虚线空心三角。 - 看到 成员变量 -> 画 实线箭头。
- 看到 方法参数 -> 画 虚线箭头。
Lesson 9 设计原则与设计模式
一、 设计原则
最重要一张图:
1. UML类图复习
考点:给你一张图,你要能认出线条代表什么关系。
- 泛化 (Generalization):实线空心三角箭头。即
extends(继承)。 - 实现 (Realization):虚线空心三角箭头。即
implements(接口实现)。 - 关联 (Association):实线箭头。拥有的关系,比如老师“有”一个学生列表。
- 依赖 (Dependency):虚线箭头。最弱的关系,通常体现为局部变量、方法参数。比如“人”依赖“手机”打电话。
2. 面向接口/抽象编程
逻辑:客户代码(调用者)应该只和接口打交道,不和具体类打交道。
PPT案例(柱体 Pillar):
- 错误示范:
Pillar类中直接定义Circle bottom;。这导致柱子只能是圆柱。如果想要方柱,就得改代码。 - 正确做法:定义抽象类
Geometry(包含getArea()),Circle和Rectangle去继承它。Pillar类中定义Geometry bottom;。
- 错误示范:
- 考点词汇:依赖倒置原则(依赖抽象,不依赖具体细节)。这能应对需求变化(比如新增三角形底面),而无需修改
Pillar类的代码。
3. 优先使用组合,少用继承
逻辑:继承是“白盒复用”,父类细节对子类可见,耦合度太高(父类变,子类必变)。组合是“黑箱复用”,通过持有对象的引用来使用功能,耦合度低。
PPT案例:汽车换驾驶员。
- 如果是继承(Car extends Person),车和人绑死了。
- 如果是组合(Car has a Person),运行时可以随意
setDriver(),非常灵活。
4. 开-闭原则 (OCP)
- 定义:对扩展开放,对修改关闭。
- 含义:当需求变化时(如增加新功能),你应该通过添加新代码(写新类)来完成,而不是去修改已经写好、测试过的旧代码。
- 实现手段:就是前面说的“面向抽象编程”。
5. 高内聚-低耦合 (P29-31)
- 内聚:一个模块自己只干好自己的事(单一职责)。
- 耦合:模块之间尽量少牵连。
- 考试技巧:只要问到设计的好处,答“降低耦合、提高内聚、易于维护扩展”准没错。
七个原则没细讲,AI举例:
一、单一职责原则 (SRP - Single Responsibility Principle)
1. 定义:
一个类应该只有一个引起它变化的原因 (There should never be more than one reason for a class to change)。2. 核心逻辑:
不要把不同的职责揉在一个类里。注意,这里的“职责”不仅仅是“干一件事”,而是指业务上的功能归属。
- 例子: 假设你写了一个
Employee类,里面既有calculatePay()(计算薪水),又有saveToDatabase()(持久化),还有reportHours()(生成工时报表)。- 问题: 财务部门改了薪资算法,你要改这个类;DBA改了数据库Schema,你要改这个类;HR要改报表格式,你还要改这个类。这个类太脆弱了,牵一发而动全身。
- 修正: 拆分。
Employee只负责数据结构,PayCalculator负责薪资,EmployeeRepository负责数据库,HourReporter负责报表。3. 考点提醒:
考试如果给你一段代码,发现一个类既在做逻辑计算,又在处理IO(输入输出),或者既在做业务,又在做UI显示,请毫不犹豫地指出它违反了SRP。二、开闭原则 (OCP - Open/Closed Principle) —— 考试必考
1. 定义:
软件实体(类、模块、函数)应该对扩展开放,对修改关闭 (Open for extension, closed for modification)。2. 核心逻辑:
这是OOP最核心的原则。当需求变更时,我们希望通过增加新的代码来实现,而不是修改已有的代码。
- 怎么做? 关键在于抽象 (Abstraction) 和 多态 (Polymorphism)。
反例: 你写了一个
GraphicEditor,里面有个方法drawShape(Shape s)。if (s.type == 1) drawCircle(s); else if (s.type == 2) drawRectangle(s);如果今天要加一个三角形,你必须去改这个
if-else,这就违反了OCP,因为你“修改”了原有代码。- 正例: 定义一个抽象类或接口
Shape,里面有一个抽象方法draw()。Circle和Rectangle去实现它。GraphicEditor只需要调用s.draw()。当你要加三角形时,新建一个Triangle类即可,GraphicEditor完全不用动。3. 考点提醒:
在设计模式题中,凡是用到“策略模式”、“工厂模式”的地方,通常都是为了满足OCP。如果让你重构一段充斥着 switch-case 或 if-else 的代码,就是让你用多态来实现OCP。三、里氏替换原则 (LSP - Liskov Substitution Principle) —— 最难理解
1. 定义:
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
换句话说:子类必须能够替换掉父类,并且不改变程序的正确性。2. 核心逻辑:
继承不仅仅是语法上的复用,更是行为上的承诺。
经典反例(正方形不是长方形):
数学上正方形是长方形,但在程序里不是。
假设Rectangle有setWidth()和setHeight()。如果你让Square继承Rectangle,当你重写setWidth()时,你被迫也要修改 height(为了保证正方形的性质)。
此时,如果有一个测试用例:void test(Rectangle r) { r.setWidth(5); r.setHeight(4); assert(r.area() == 20); // 如果传入的是Square,这里会失败,因为面积可能是16或25 }这就违反了LSP。
3. 考点提醒 (高分关键):
考试可能会涉及到契约设计 (Design by Contract) 的概念:
为了满足LSP,子类在重写父类方法时:
- 前置条件 (Preconditions) 不能被加强(不能要求更苛刻的输入)。
- 后置条件 (Postconditions) 不能被削弱(不能输出更烂的结果)。
- 不变式 (Invariants) 必须保持。
四、接口隔离原则 (ISP - Interface Segregation Principle)
1. 定义:
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。2. 核心逻辑:
拒绝“胖接口” (Fat Interface)。
- 例子: 你有一个
SmartDevice接口,里面有print(),fax(),scan()。
现在有一台老式打印机OldPrinter实现了这个接口,但它只能打印,不能传真。你被迫在fax()里写个空实现或者抛异常。- 修正: 拆分成
Printable,Faxable,Scannable三个接口。OldPrinter只实现Printable。3. 考点提醒:
如果考题中出现一个接口里有十几个方法,而实现类只用到了其中两三个,其余都是空实现,这就是违反了ISP。五、依赖倒置原则 (DIP - Dependency Inversion Principle) —— 架构核心
1. 定义:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
2. 核心逻辑:
面向接口编程。
场景: 既然是面向对象,我们要解耦。
- 错误:
Customer类直接new了一个SQLServerDAO类来读取数据。此时高层业务(Customer)依赖了低层细节(SQLServer)。如果换成MySQL,就要改代码。- 正确:
Customer依赖一个接口ICustomerDAO。SQLServerDAO实现这个接口。具体的注入过程(Dependency Injection)交给外部容器或工厂。3. 考点提醒:
DIP是实现OCP的重要手段。考试画UML图时,注意箭头方向:实现类指向接口,调用者也指向接口。这就是所谓的“倒置”——依赖关系指向了抽象,而不是具体的实现。六、迪米特法则 (LoD - Law of Demeter) / 最少知识原则
1. 定义:
一个对象应该对其他对象有最少的了解。只与你的直接朋友交谈 (Talk only to your immediate friends)。2. 核心逻辑:
降低耦合度,避免链式调用。
- 代码坏味道:
objectA.getObjectB().getObjectC().doSomething()。
这叫“由于路过踢了狗一眼”。A严重依赖了B和C的内部结构。如果C变了,A可能都要崩。- 修正: 在B里面加一个包装方法。A调用
objectA.getObjectB().doSomethingWrapped()。3. 考点提醒:
看到一长串的点号调用(链式调用除外,特指跨层级访问),基本就是违反LoD。七、合成复用原则 (CRP - Composite Reuse Principle)
1. 定义:
尽量使用对象组合(Composition)/聚合(Aggregation),而不是继承关系达到软件复用的目的。2. 核心逻辑:
- 继承 (Inheritance) 是“白箱复用”,破坏了封装性,父类变了子类不得不变,且静态,编译期就定死了。
- 组合 (Composition) 是“黑箱复用”,耦合度低,且动态,运行时可以切换(比如通过Setter注入不同的策略对象)。
3. 考点提醒:
当考题问你“如何设计一个既能是电动车又能是燃油车,既能是红色又能是蓝色的车”时,千万别写RedElectricCar,BlueGasCar这种类爆炸的继承结构。请用组合:Car类里包含一个Engine成员和一个Color成员。
二、设计模式
这也是最有效率的复习方式:理论对应代码,一眼看穿本质。
既然马上要期末考试了,我们不玩虚的。我把PPT里的核心文字考点(场景、案例、结构、优缺点)和我写的辅助理解代码完美融合在一起。
你只要把下面这 7 个模块看懂,Lesson 9 这章的设计模式大题(通常是画图或填空写代码)你就能拿满分。
1. 策略模式 (Strategy Pattern) —— 算法的动态切换
【核心文字考点】
- 场景:做一件事情有多种算法/逻辑,需要在运行时根据不同情况动态切换。
PPT案例:打分系统。
- 有的比赛规则是“去掉最高最低分取平均”,有的是“直接取平均”。
- 如果不作为模式,你可能会在主类里写一堆
if (type == 1) ... else if (type == 2)...,导致代码臃肿且难以维护。
结构角色:
- Strategy (接口):定义所有支持的算法的公共接口(如
calculateScore())。 - ConcreteStrategy (具体策略):实现各种具体的打分算法。
- Context (上下文):持有
Strategy接口的引用。
- Strategy (接口):定义所有支持的算法的公共接口(如
设计原则:体现了“多用组合,少用继承”。你可以随时调用
setStrategy()换一种算法,符合开闭原则。【代码辅助理解】
// 1. 策略接口 (Strategy):规定算法的入口 interface ScoringStrategy { double calculate(double[] scores); } // 2. 具体策略A (ConcreteStrategy):普通平均分算法 class AverageStrategy implements ScoringStrategy { public double calculate(double[] scores) { System.out.println("执行策略:直接求平均值"); return sum(scores) / scores.length; } } // 3. 具体策略B (ConcreteStrategy):去掉最高最低分算法 class GeometryStrategy implements ScoringStrategy { public double calculate(double[] scores) { System.out.println("执行策略:去掉最高最低分后求平均"); // ...复杂的数学逻辑... return result; } } // 4. 上下文 (Context):比赛环境 class Competition { // 【核心考点】:这里持有的是接口引用,而不是具体类! // 这叫“组合”,比继承灵活。 private ScoringStrategy strategy; // 允许在运行时动态更换策略 public void setStrategy(ScoringStrategy strategy) { this.strategy = strategy; } public void announceResult(double[] scores) { // 【委托】:Context不自己算,而是交给策略对象去算 if(strategy != null) { double finalScore = strategy.calculate(scores); System.out.println("最终得分:" + finalScore); } } } // --- 考试时的用法演示 --- Competition c = new Competition(); c.setStrategy(new AverageStrategy()); // 今天这局比赛用平均分算 c.announceResult(scores); c.setStrategy(new GeometryStrategy()); // 明天那局换个算法算,Context代码不用改
2. 访问者模式 (Visitor Pattern) —— 稳定的数据 vs 变化的操作
【核心文字考点】
- 场景:数据结构很稳定(不会轻易加新类),但作用于这些数据上的操作经常变化/增加。
PPT案例:电表 (Ammeter) 计费。
- 稳定部分:电表(Ammeter)作为一个物体,它的结构是稳定的(有读数功能)。
- 变化部分:“计费方式/访问者”是变化的。之前是电力公司查表算钱,现在可能新增“环保局查表算碳排放”。我们不想为了环保局去修改电表类的代码。
核心逻辑:双重分派 (Double Dispatch)。
- 客户端调用
element.accept(visitor)。 - Element 内部“反手”调用
visitor.visit(this),把自己传回去。
- 客户端调用
优点:符合开闭原则。增加新的操作(如新增一个 Visitor 子类)非常容易,不用修改原来的 Element 类。
【代码辅助理解】
// 1. 抽象元素 (Element):电表 // 必须要有一个 accept 方法,允许访问者进来 interface AmmeterElement { void accept(Visitor visitor); double getElectricity(); // 获取度数 } // 2. 具体元素 (ConcreteElement):家用电表 class HomeAmmeter implements AmmeterElement { public double getElectricity() { return 100.0; } // 【核心难点:双重分派】 // 第一步分派:调用 accept public void accept(Visitor visitor) { // 第二步分派:调用 visit,并把自己(this)交出去 visitor.visit(this); } } // 3. 抽象访问者 (Visitor):定义对每种元素的操作 interface Visitor { // 必须要知道访问的是谁(重载方法) void visit(HomeAmmeter ammeter); void visit(IndustryAmmeter ammeter); } // 4. 具体访问者 (ConcreteVisitor):环保局查电表(新需求) class EnvironmentVisitor implements Visitor { public void visit(HomeAmmeter ammeter) { // 在这里写针对家用电表的具体操作逻辑 System.out.println("家庭用电碳排放:" + ammeter.getElectricity() * 0.5); } public void visit(IndustryAmmeter ammeter) { System.out.println("工业用电碳排放:" + ammeter.getElectricity() * 2.0); } } // --- 考试时的用法演示 --- AmmeterElement myMeter = new HomeAmmeter(); Visitor check = new EnvironmentVisitor(); // 看起来是 meter 接受了 check,实际上是 check 访问了 meter myMeter.accept(check);
3. 装饰模式 (Decorator Pattern) —— 动态套娃加功能
【核心文字考点】
- 场景:希望在运行时动态地给一个对象增加额外的功能,或者撤销功能。相比之下,继承是静态的(编译时确定的),且继承会导致类爆炸(为了各种组合写一堆子类)。
PPT案例:麻雀装电子翅膀。
- 原始对象:鸟(Bird),能飞100米。
- 装饰后:装上电子翅膀(Decorator),调用
eleFly(),在原有的fly()基础上增加距离。 - 套娃特性:你可以装一个翅膀(100+50),也可以再套一层装两个翅膀(100+50+50)。
结构特征:装饰器类既继承了组件接口,又持有一个组件接口的引用。
- 继承 -> 为了让装饰器看起来还是个“组件”(接口一致)。
- 持有引用 -> 为了调用原来的功能。
【代码辅助理解】
// 1. 抽象组件 (Component)
interface Bird {
void fly();
}
// 2. 具体组件 (ConcreteComponent):普通的麻雀
class Sparrow implements Bird {
public void fly() { System.out.println("普通飞行:飞了100米"); }
}
// 3. 装饰器基类 (Decorator) —— 考试画图重点
// 【继承】:为了保持接口一致,能当鸟用
abstract class Decorator implements Bird {
// 【组合】:为了持有原对象
protected Bird bird;
public Decorator(Bird bird) { this.bird = bird; }
public void fly() {
bird.fly(); // 默认行为:还是调原来的飞
}
}
// 4. 具体装饰 (ConcreteDecorator):电子翅膀
class WingDecorator extends Decorator {
public WingDecorator(Bird bird) { super(bird); }
// 重写方法,增加功能
public void fly() {
super.fly(); // 先执行原来的飞行(飞100米)
eleFly(); // 再执行增强的功能
}
private void eleFly() { System.out.println("电子助推:额外飞了50米"); }
}
// --- 考试时的用法演示 ---
Bird b = new Sparrow(); // 1. 创建一只普通鸟
Bird superBird = new WingDecorator(b); // 2. 把它包装进装饰器
superBird.fly();
// 输出结果:
// 普通飞行:飞了100米
// 电子助推:额外飞了50米
// 如果想装两个翅膀?继续套娃:
Bird ultraBird = new WingDecorator(new WingDecorator(new Sparrow()));4. 适配器模式 (Adapter Pattern) —— 接口转换器
【核心文字考点】
- 场景:接口不兼容。你想用一个现有的类,但它的接口(方法名、参数)不符合当前系统的要求。
PPT案例:交流电转直流电(或者发动机适配器)。
- 你的电器需要“直流电接口”。
- 墙上只有“交流电接口”。
- 需要一个适配器(变压器)连在中间。
结构角色:
- Target (目标接口):客户期待的接口。
- Adaptee (被适配者):现有的、但不兼容的接口。
- Adapter (适配器):实现 Target,内部调用 Adaptee 的方法。
考点细分:单接口适配器。如果一个接口方法太多(比如
MouseListener有5个方法),你只想实现其中1个。可以写一个抽象类空实现所有方法,你的类去继承这个抽象类,只重写你需要的那个。【代码辅助理解】
// 1. 目标接口 (Target):我们要用的接口(比如直流电) interface DirectCurrent { void useDC(); } // 2. 被适配者 (Adaptee):已经存在的类(比如220V交流电) // 这个类的方法名 outputAC() 跟我们想要的 useDC() 不一样 class AC220 { void outputAC() { System.out.println("输出220V交流电"); } } // 3. 适配器 (Adapter):实现目标接口 class PowerAdapter implements DirectCurrent { // 【关键】:内部持有一个被适配者的对象 private AC220 ac220 = new AC220(); @Override public void useDC() { // 在这里做“翻译”工作 System.out.println("适配器开始工作..."); ac220.outputAC(); // 实际上调用的是交流电的方法 System.out.println("经过变压...转成了直流电"); } }
5. 责任链模式 (Chain of Responsibility) —— 踢皮球/层层审批
【核心文字考点】
- 场景:请求处理者有多个,形成一条链。请求在链上传递,直到被某个对象处理。发送者不需要知道谁处理了请求,解耦。
PPT案例:请假审批 或 电影院找零。
- 请假 < 2天 -> 班主任批。
- 请假 < 7天 -> 系主任批。
- 请假 < 10天 -> 院长批。
- 结构特征:每个处理者 (Handler) 对象里面都有一个成员变量
nextHandler指向下一个处理者。 - 逻辑:能处理就处理,处理不了就
nextHandler.handle()。
【代码辅助理解】
// 1. 处理者抽象类 (Handler)
abstract class Leader {
// 【核心结构】:持有下一个领导的引用
protected Leader nextLeader;
// 设置链条的下一环
public void setNext(Leader next) { this.nextLeader = next; }
// 抽象的处理方法
public abstract void handleRequest(int days);
}
// 2. 具体处理者:班主任
class Teacher extends Leader {
public void handleRequest(int days) {
if (days <= 2) {
System.out.println("班主任:准了!");
} else {
// 【核心逻辑】:搞不定,传给下家
if (nextLeader != null) {
System.out.println("班主任:权限不够,转交给上一级...");
nextLeader.handleRequest(days);
}
}
}
}
// 3. 具体处理者:院长
class Dean extends Leader {
public void handleRequest(int days) {
if (days <= 10) {
System.out.println("院长:准了!");
} else {
System.out.println("院长:这假太长了,退学吧!");
}
}
}
// --- 考试时的用法演示 ---
Leader teacher = new Teacher();
Leader dean = new Dean();
// 组装责任链:班主任 -> 院长
teacher.setNext(dean);
// 学生直接找班主任请5天假
// 班主任处理不了,会自动转给院长
teacher.handleRequest(5); 6. 门面/外观模式 (Facade Pattern) —— 统一入口管家
【核心文字考点】
- 场景:系统内部非常复杂,有一堆子系统类。外部客户不想跟这一堆乱七八糟的类打交道,也不想知道它们之间的依赖关系。
PPT案例:办房产证。
- 不用你自己跑税务局、社保局、公证处、银行。
- 设立一个“综合服务大厅 (Facade)”,客户只跟大厅窗口打交道,大厅内部去调配后面那堆部门。
设计原则:体现了迪米特法则(最少知识原则,只和最近的朋友说话)。
【代码辅助理解】
// 子系统们 (很复杂,很乱) class TaxBureau { void payTax() { System.out.println("交税"); } } class Notary { void proof() { System.out.println("公证"); } } class Bank { void loan() { System.out.println("放贷"); } } // 门面类 (Facade) —— 也是考点中的“综合服务大厅” class ServiceCenter { // 门面持有所有子系统的引用 private TaxBureau tax = new TaxBureau(); private Notary notary = new Notary(); private Bank bank = new Bank(); // 提供一个简单的接口给客户 public void buyHouse() { System.out.println("--- 开始办理买房手续 ---"); tax.payTax(); notary.proof(); bank.loan(); System.out.println("--- 手续办完 ---"); } } // 客户调用: new ServiceCenter().buyHouse(); // 一行代码搞定,不用管内部逻辑
7. 工厂模式家族 (Factory Family) —— 必考的“造人/造物”
这里有三个递进的模式,考试时一定要分清题目描述的是哪一种!
A. 简单工厂 (Simple Factory)
- PPT案例:女娲造人。传入参数 "M" 造男人,"W" 造女人。
- 缺点:违背开闭原则。如果想造机器人,必须修改工厂类的
if-else代码。 - 代码特征:通常有一个
static方法,里面有switch或if-else。
class NvwaFactory {
public static Person makePerson(String type) {
if (type.equals("M")) return new Man();
else if (type.equals("W")) return new Woman();
// 缺点:想造 Robot?必须在这里改代码加 else if
else return null;
}
}B. 工厂方法 (Factory Method) —— 最常用的标准工厂
- PPT案例:圆珠笔芯。
- 逻辑:把工厂定义为接口。红笔芯有红笔芯工厂,蓝笔芯有蓝笔芯工厂。
- 优点:符合开闭原则。想增加黑笔芯?只需写一个
BlackPenCore和BlackCoreFactory,不需要修改原有代码。 - 定义:延迟实例化到子类。
// 1. 工厂接口
interface PenCoreFactory {
PenCore createCore(); // 不传参,由子类决定造啥
}
// 2. 具体工厂:红笔工厂
class RedCoreFactory implements PenCoreFactory {
public PenCore createCore() { return new RedPenCore(); }
}
// 3. 具体工厂:蓝笔工厂
class BlueCoreFactory implements PenCoreFactory {
public PenCore createCore() { return new BluePenCore(); }
}C. 抽象工厂 (Abstract Factory) —— 针对“产品族”
- PPT案例:农场(既有水果,又有蔬菜)或者 电器(既有电视,又有空调)。
考点关键词:产品族 (Product Family)。
- 工厂方法只能生产一种等级结构(比如只能造笔芯,或者只能造电视)。
- 抽象工厂能生产一整套配套的东西(比如海尔工厂生产海尔电视+海尔空调)。
- 代码特征:工厂接口里有多个创建方法。
// 1. 抽象工厂:能生产一套东西(电视+空调)
interface AppFactory {
TV createTV(); // 生产电视
AC createAC(); // 生产空调
}
// 2. 具体工厂:海尔工厂 (Haier Family)
// 保证了造出来的电视和空调是配套的
class HaierFactory implements AppFactory {
public TV createTV() { return new HaierTV(); }
public AC createAC() { return new HaierAC(); }
}
// 3. 具体工厂:TCL工厂 (TCL Family)
class TCLFactory implements AppFactory {
public TV createTV() { return new TCLTV(); }
public AC createAC() { return new TCLAC(); }
}区别:
工厂方法模式:是去单点。你想要汉堡,就去汉堡窗口(汉堡工厂);你想要炸鸡,就去炸鸡窗口(炸鸡工厂)。
- 关注点:生产一种产品。
抽象工厂模式:是买套餐。你想要吃肯德基风味(汉堡+炸鸡+可乐),就去肯德基店;你想要吃麦当劳风味(巨无霸+麦乐鸡+雪碧),就去麦当劳店。
- 关注点:生产一系列配套的产品(这在考试里叫产品族)。
如果不确定用什么模式:先看需求。
- 如果是一堆
if-else选算法 -> 策略模式。 - 如果是数据结构不变,操作变 -> 访问者模式。
- 如果是动态加功能,不想用继承 -> 装饰模式。
- 如果是接口对不上 -> 适配器模式。
- 如果是层级审批 -> 责任链模式。
- 如果是封装复杂的后台流程 -> 门面模式。
- 如果是创建对象,且希望解耦 -> 工厂模式(通常首选工厂方法)。
Lesson 10 异常
1. 什么是异常
在Java中,异常(Exception)就是程序正常指令流被中断的事件。
三种错误:
- 编译错误 (Compilation Error):语法错了,分号没加,变量没定义。编译器直接报错,程序根本跑不起来。这不属于异常处理的范畴。
- 逻辑错误 (Logic Error):算法写错了,
3 + 5算成了9。程序能跑,但结果不对。这也不属于异常处理范畴,这是你的Bug。 - 运行时错误 (Runtime Error):这才是我们今天的主角。程序语法没问题,逻辑也没大问题,但运行的时候,外部环境(如文件找不到、网络断了)或特殊输入(除数为0、数组越界)导致程序崩了。
2. 异常的结构分类
1. 祖宗类:Throwable 所有的异常和错误都继承自 java.lang.Throwable。它有两个直系儿子:Error 和 Exception。
2. 大儿子:Error(不可抗力)
- 定义:指JVM(Java虚拟机)层面的严重错误。
- 例子:
OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出)。 - 处理原则:不要试图去捕获它。这就像是电脑主板烧了,你写软件代码是修不好的。
3. 二儿子:Exception(程序可处理) 这是我们写代码要关心的。它又分为两派,这是重中之重的区别:
派系一:受检异常(Checked Exception / Non-RuntimeException)
- 特点:编译器也就是Javac会盯着你。如果你调用了一个会抛出这类异常的方法(比如文件操作),你必须处理(要么
try-catch,要么声明throws),否则代码编译都不通过。 - 例子:
IOException(文件IO错误)、FileNotFoundException、SQLException。 - 逻辑:这就像去银行办业务必须带身份证,不带不让你进门。
- 特点:编译器也就是Javac会盯着你。如果你调用了一个会抛出这类异常的方法(比如文件操作),你必须处理(要么
派系二:非受检异常(Unchecked Exception / RuntimeException)
- 特点:编译器不管。这类异常通常是编程错误导致的,编译器默认你代码写得对,运行时才暴露。
例子:
NullPointerException(空指针,NPE)ArithmeticException(算术异常,比如除以0)ArrayIndexOutOfBoundsException(数组越界)ClassCastException(类型转换异常)NumberFormatException(数字格式异常,比如把"abc"转成int)
- 逻辑:这就像走路摔跤,走路本身不需要办手续,但你如果不小心(逻辑漏洞),就会摔倒(运行时报错)。
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error (天灾,不管它)
│ ├── OutOfMemoryError
│ └── StackOverflowError
│
└── java.lang.Exception (我们要处理的)
│
├── RuntimeException (逻辑错误,编译器不强制查)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ArithmeticException
│ ├── ClassCastException
│ └── NumberFormatException
│
└── (其他Checked Exception) (外部意外,编译器强制查)
├── IOException
│ └── FileNotFoundException
├── SQLException
└── ClassNotFoundException3. 如何处理异常
机制叫抓抛模型。
1. 捕获异常:try-catch-finally 这是处理异常的“盾牌”。
try {
// 监控区:把可能出事的代码放这里
// 一旦这里某行代码报错,程序立即跳到catch,后续try里的代码不执行
} catch (ExceptionType1 e) {
// 捕获区:出了ExceptionType1这种错,怎么办
} catch (ExceptionType2 e) {
// 捕获区:出了ExceptionType2这种错,怎么办
} finally {
// 终结区:不管有没有错,这里一定会被执行
}关键考点(必须记住):
Catch的顺序:如果有多个catch块,子类异常必须放在父类异常前面。
- 比喻:如果你先catch了
Exception(万能捕获),后面专门catchIOException的代码就永远执行不到了(因为被万能的截胡了)。编译器会报错。
- 比喻:如果你先catch了
Finally的作用:通常用来释放资源(关闭文件流、数据库连接)。即使
try或catch中有return语句,finally也会在 return 之前(准确说是return逻辑组装好准备返回前)执行。- 特例:只有
System.exit(0)会强制杀掉JVM,finally才不会执行。
- 特例:只有
2. 声明异常:throws(甩锅) 这是处理异常的“推卸责任”。 如果当前方法不知道怎么处理,或者不想处理,就在方法签名上用 throws 告诉调用者:“我可能会出这个问题,你自己看着办”。
public void readFile() throws IOException {
// ...
}- 谁调用
readFile,谁就要负责处理这个IOException(要么try-catch,要么继续throws往上甩)。
4. 自定义异常
1. throw 关键字(注意没有s)
throws是在方法声明处(名词,声明);throw是在方法体内部(动词,动作)。- 作用:程序运行到这里,主动抛出一个异常对象。
if (money < 0) {
throw new IllegalArgumentException("钱不能是负数"); // 程序在此终止,跳出
}2. 自定义异常
Java自带的异常不够用时(比如PPT里的“卡无法识别”、“余额不足”),我们需要自己造异常。
- 怎么写:创建一个类,继承
Exception(如果你想让它是受检的)或者RuntimeException(如果你想让它是非受检的)。 - 构造器:通常写两个,一个无参,一个带
String message(报错信息)并调用super(message)。
案例逻辑(PPT P79 Worker与Car例子):
Car类中检测到故障,throw new CarWrongException()。Worker类调用car.run(),必须捕获这个异常。Worker在 catch 块中,发现车坏了,于是改为步行,并抛出一个新的异常LateException(迟到异常)。这是异常处理中常见的异常转译(把底层异常包装成业务异常)。
5. 考点
1. 方法重写(Override)中的异常限制
- 规则:子类重写父类方法时,子类方法抛出的受检异常,不能比父类方法更多(更宽泛)。
- 比喻:父亲说“我可能感冒”,儿子不能说“我可能得绝症”。儿子只能说“我可能感冒”、“我可能流鼻涕(感冒的子类)”或者“我身体很好(不抛异常)”。
- 理由:这是为了多态的安全性。别人把你当父亲用的时候,你不能给出父亲没承诺过的意外惊吓。
2. return 与 finally 的爱恨情仇
try {
return 1;
} finally {
System.out.println("finally");
}- 结果:先打印 "finally",然后方法返回 1。
- 进阶陷阱:如果在 finally 里修改了返回值(对于基本数据类型),通常不会改变 try 里的返回值(因为try里的返回值已经暂存了);但如果在 finally 里直接写了
return,那就会覆盖 try 里的return。
3. 输出预测题
- 做这种题,画一个箭头。
- 遇到
try进去。 - 遇到
throw或报错,立马跳出try,找匹配的catch。 - 执行
catch。 - 无论如何,最后执行
finally。 - 如果
catch完了没有抛出新异常,继续执行 try-catch-finally 块之后的代码。 - 如果
catch里面又抛了异常,或者没 catch 住,finally执行完后,方法非正常结束,异常往上层方法抛。
Lesson 11 Java集合框架
1. 集合框架的宏观架构
Java集合框架主要由两个根接口派生出来:
Collection接口:处理单列数据(一组对象)。- 它有两个主要的亲儿子:
List(有序、可重复)和Set(无序、不可重复)。
- 它有两个主要的亲儿子:
Map接口:处理键值对(Key-Value)数据(双列数据)。- 它和Collection没有继承关系!这是常考的坑点。
考试重点:
- List:有序,有索引(index),元素可以重复。
- Set:无序,无索引,元素不可以重复(唯一性)。
- Map:存键值对,键(Key)不可以重复,值(Value)可以重复。通过Key找Value。
2. Collection接口与其子接口(List & Set)
1. List 接口(有序列表)
List最典型的特征是:有序,指元素的存入顺序和取出顺序一致。
这里有两个绝对的必考点,就是ArrayList和LinkedList的区别:
ArrayList:
- 底层实现:动态数组(可变长度数组)。
- 内存特性:连续空间。
- 优点:随机访问(get/set)极快(时间复杂度O(1)),因为可以通过索引直接计算内存地址。
- 缺点:插入和删除(add/remove)慢,尤其是从中间删,因为需要移动后面所有的元素。
- 扩容机制(PPT P48):当空间不够时,默认增长50%。
LinkedList:
- 底层实现:双向链表。
- 内存特性:非连续空间,靠指针连接。
- 优点:插入和删除极快(时间复杂度O(1)),只需要改变前后节点的指针指向。
- 缺点:随机访问慢(时间复杂度O(n)),想拿第100个元素,必须从头一个一个数过去。
- 特有方法(P43):
addFirst,addLast,removeFirst等,这是因为它是链表,操作头尾特别方便。
Vector(PPT P48,老古董,了解即可):
- 和ArrayList几乎一样,区别在于Vector是线程安全(Thread-Safe)的,效率低。ArrayList线程不安全,效率高。
- Vector扩容默认增长1倍。
2. Set 接口(唯一集合)
Set注重独一无二。
HashSet:最常用。它是如何保证数据不重复的?
- 考点:放入元素时,先判断
hashCode(),如果哈希冲突了,再调用equals()判断对象是否真的相同。如果你们自定义类(比如Student)要放进Set,必须重写hashCode()和equals()方法。
- 考点:放入元素时,先判断
- TreeSet(P19):元素是排序的(SortedSet),底层是红黑树。
3. Map 接口(键值对)
Map是独立的,专门处理 Key -> Value 的映射。
- Key:无序、唯一(其实Key就是一个Set)。
- Value:无序、不唯一(其实Value就是一个Collection)。
核心实现类对比(必考):
HashMap:
- 最常用。
- 线程不安全,效率高。
- 允许null作为Key或Value。
Hashtable(注意t是小写):
- 老古董,继承自Dictionary类。
- 线程安全,效率低。
- 不允许null作为Key或Value(否则抛异常)。
常用方法:
put(key, value):存。get(key):取。keySet():拿到所有Key的集合(返回Set)。values():拿到所有Value的集合(返回Collection)。entrySet():拿到所有键值对的集合。
4. 迭代器(Iterator)——如何遍历?
对于List,你可以用 for(int i=0; ...) 循环,因为List有索引。 但是对于Set和Map,没有索引,怎么遍历?这就需要Iterator。
- Iterator模式:提供一种统一的方法访问容器对象中的各个元素,而不需要暴露该对象的内部细节。
核心方法:
hasNext(): 还有没有下一个元素?next(): 把下一个元素拿出来,指针下移。remove(): 删除当前元素。
- 增强型for循环(Foreach):
for(Type obj : collection),底层原理其实就是Iterator的语法糖。
注意:在使用Iterator遍历时,不要直接用集合自己的remove方法删除元素,否则会抛出ConcurrentModificationException异常,要用迭代器自己的remove()方法。
5. Collections 工具类(注意有个s)
PPT考点提炼: 这也是一个经典混淆点:
- Collection:是接口(Interface),是List和Set的父接口。
- Collections:是工具类(Utility Class),里面全是
static方法,用来服务集合的。
Collections常用功能:
Collections.sort(list):排序。Collections.shuffle(list):打乱(洗牌)。Collections.reverse(list):反转。Collections.binarySearch(list, key):二分查找(前提是必须有序)。Collections.synchronizedList(list):把不安全的List变成线程安全的List。
6. 泛型(Generics)
在泛型出现之前,集合里装的都是Object。
问题:
- 拿出来时需要强制类型转换(Cast)。
- 不安全,你可能把一个String放进去,取出来以为是Integer,一转就报错(
ClassCastException)。
泛型的好处:
List<String>:明确告诉编译器,我这里只放String。- 编译期检查:如果你放Integer,代码都编译不过,提前发现错误。
- 无需强转:
get()出来的直接就是String。
- 注意(P65):泛型类型必须是类(Reference Type),不能是基本数据类型(int, double)。如果要存int,必须用包装类
Integer。
Lesson 12 GUI设计
(似乎范围没说?)
Lesson 13 IO
| 维度 | 字节流 (Byte) | 字符流 (Char) |
|---|---|---|
| 应用场景 | 图片、视频、二进制数据 | 纯文本 (.txt, .java) |
| 顶层父类 | InputStream / OutputStream | Reader / Writer |
| 节点流 (直接接文件) | FileInputStreamFileOutputStream | FileReaderFileWriter |
| 处理流 (增强功能) | BufferedInputStreamDataInputStreamObjectInputStream | BufferedReader (有readLine)PrintWriter (有println) |
| 转换流 (字节变字符) | 无 | InputStreamReaderOutputStreamWriter |
1. 什么是流
在Java中,流就是数据传输的通道。
- 源头(Source):数据从哪里来(键盘、文件、网络)。
- 终点(Sink):数据到哪里去(屏幕、文件、网络)。
Java把流分成了两个维度:
按流向分(以内存/程序为中心):
- 输入流 (Input/Reader):外界 -> 内存(读)。
- 输出流 (Output/Writer):内存 -> 外界(写)。
- 记忆技巧: 想象你在下载东西,是Input;你在上传东西,是Output。
按处理单元分(核心考点):
- 字节流 (Byte Stream):操作8位二进制(byte)。万能流,处理图片、视频、音频、可执行文件必须用它。
- 字符流 (Character Stream):操作16位Unicode字符(char)。文本专用,处理txt、java、html等文本文件,解决了中文乱码问题。
基类体系:
| 维度 | 字节流 (Byte) | 字符流 (Char) |
|---|---|---|
| 输入 | InputStream (抽象类) | Reader (抽象类) |
| 输出 | OutputStream (抽象类) | Writer (抽象类) |
2. File类
在读写文件内容之前,我们得先能找到文件。这就是File类的作用。
File类只管理文件/目录的属性(如路径、大小、是否存在、创建时间),不涉及文件的具体内容读写。不要试图用File类去读文件里的文字!
常用API(考试编程题可能用到):
- 判断:
exists()(存在吗?),isDirectory()(是目录吗?),isFile()(是文件吗?),canRead()/canWrite()。 - 获取:
getName(),getAbsolutePath()(绝对路径),length()(文件大小,字节数),lastModified()。 操作:
createNewFile(): 创建空文件。mkdir()vsmkdirs(): 必考坑点。mkdir只能建一级目录(如/a),mkdirs可以建多级(如/a/b/c,如果a不存在会自动创建)。delete(): 删除。list()/listFiles(): 获取目录下的文件名或File对象数组。
import java.io.File;
import java.io.IOException;
public class FileDemo {
public static void main(String[] args) {
// 考点:跨平台路径分隔符使用 File.separator,不要死写 "\\"
String path = "data" + File.separator + "test.txt";
File f = new File(path);
// 1. 检查是否存在
if (!f.exists()) {
// 获取父目录对象
File parent = f.getParentFile();
// 考点:mkdirs() 创建多级目录(推荐),mkdir() 只能创建一级
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
try {
// 创建空文件
boolean created = f.createNewFile();
System.out.println("文件创建成功: " + created);
} catch (IOException e) {
e.printStackTrace();
}
}
// 2. 获取属性 (PPT 32-34)
System.out.println("绝对路径: " + f.getAbsolutePath());
System.out.println("文件大小(字节): " + f.length());
System.out.println("是目录吗? " + f.isDirectory());
// 3. 遍历目录
File dir = new File(".");
String[] files = dir.list(); // 返回文件名数组
// File[] files = dir.listFiles(); // 返回File对象数组(更常用)
}
}
3. 字节流
1. 节点流(直接连在文件上的管子):
FileInputStream/FileOutputStream。- 构造方法:
new FileOutputStream("a.txt", true)—— 考点:第二个参数true表示追加模式(append),否则默认是覆盖模式(overwrite)。
2. 读写操作的核心方法(抽象类定义的方法):
读 (
read):int read(): 一次读一个字节。重点:读到末尾返回 -1。int read(byte[] b): 一次读一堆到数组里(效率更高)。返回的是实际读取的字节数。
写 (
write):void write(int b): 写一个字节。void write(byte[] b): 写一个数组。
- 关闭 (
close): 编程规范。IO流用完必须关,放在finally块中或者用try-with-resources语法,否则占用系统资源。
import java.io.*;
public class ByteCopy {
public static void main(String[] args) {
// 语法糖:try(...) 括号里的资源会在结束后自动调用 close(),
// 考试写这个能加分(展示你懂JDK7+新特性),如果不会写就用 finally 块关流。
try (
FileInputStream fis = new FileInputStream("input.jpg");
// 考点:第二个参数 true 代表追加模式,不写默认为覆盖模式(false)
FileOutputStream fos = new FileOutputStream("output.jpg", false)
) {
// 核心逻辑:建立缓冲区(PPT 62页示例的升级版,一次读一个字节太慢了,要读一组)
byte[] buffer = new byte[1024]; // 1KB的缓冲区
int len; // 记录实际读取到的字节数
// 死背这个while循环!
// fis.read(buffer) 会把数据填入buffer,并返回读到的个数。
// 如果读到文件末尾,返回 -1。
while ((len = fis.read(buffer)) != -1) {
// 写出数据:从 buffer 的 0 开始,写 len 这么长
// 严禁写 fos.write(buffer),否则最后一次读取如果没填满,会写入垃圾数据
fos.write(buffer, 0, len);
}
System.out.println("复制完成");
} catch (IOException e) {
e.printStackTrace();
}
}
}4. 装饰者模式
PPT里提到了“Filter流”、“Data流”、“Buffered流”,这其实运用了装饰者模式(Decorator Pattern)。
比喻: 节点流(如FileInputStream)是裸露的水管,虽然能用水,但不方便。处理流就是在裸管外面包了一层层特殊功能的管套。
1. 缓冲流 (BufferedInputStream / BufferedOutputStream)
- 作用: 提高效率。内部维护了一个缓冲区(默认8192字节)。
- 原理: 就像你去超市买东西,不用购物车(节点流)得拿一个苹果跑一趟收银台;用了购物车(缓冲流),装满一车再结一次账。减少了CPU访问磁盘的次数。
- flush(): 输出流中,
flush()强制把缓冲区的数据“冲”进目的地。close()前会自动flush。
2. 数据流 (DataInputStream / DataOutputStream)
- 作用: 读写Java基本数据类型(int, double, boolean等)。
- 方法:
readInt(),writeDouble(),readUTF()(读字符串)。 - 注意: 读写的顺序必须严格一致(先写int再写double,读的时候也得先读int再读double)。
3. 打印流 (PrintStream)
- 代表:
System.out就是一个PrintStream。 - 特点: 永远不会抛出IO异常;有自动flush功能;
println()方法非常方便。
4. 管道流 (PipedInputStream / PipedOutputStream)
- 场景: 线程间通信。一个线程写,另一个线程读。必须成对使用并connect。
5. 字符流与转换
1. 转换流——字节到字符的桥梁 (InputStreamReader / OutputStreamWriter) —— *难点*
- 这是连接字节流和字符流的适配器。
- 功能: 把字节解码成字符,或把字符编码成字节。
- 构造: 可以指定字符集!例如
new InputStreamReader(new FileInputStream("a.txt"), "UTF-8")。
2. 便捷流 (FileReader / FileWriter)
- 其实就是转换流的子类,只不过使用了默认编码。如果需要处理特定编码文件,还是得用上面的转换流。
3. 缓冲字符流 (BufferedReader / BufferedWriter) —— *编程题大杀器*
- BufferedReader 特有方法:
String readLine()。一次读一行文本!读不到返回null。这是读取文本文件最常用的方法。 - BufferedWriter 特有方法:
newLine()。写入一个跨平台的换行符。
6. 对象序列化
- 定义: 把内存中的Java对象(Object)转换成字节序列,保存到文件或网络传输,这叫序列化。反过来叫反序列化。
- 比喻: 就像把宜家组装好的柜子拆成板材(序列化)方便运输,到了家再照图纸组装起来(反序列化)。
- 核心类:
ObjectInputStream(readObject) /ObjectOutputStream(writeObject)。 三个关键考点:
- 接口: 被序列化的类必须实现
java.io.Serializable接口。这是一个标记接口,里面没有任何方法,只是给JVM贴个条:“这货可以拆”。 - transient 关键字: 如果某个属性(比如密码)不想被序列化,用
transient修饰。序列化时会忽略它,反序列化回来时该值为默认值(null或0)。 - EOFException: 读取对象流时,通常用捕获这个异常来判断文件读完了。
- 接口: 被序列化的类必须实现
7. 基础输入输出
- 命令行参数:
public static void main(String[] args)中的args接收命令行输入的字符串数组。 Scanner类:
java.util.Scanner。next(): 读到空格为止。nextLine(): 读完一整行(包括空格)。nextInt()/nextDouble(): 读取特定类型。
Lesson 14 多线程
1. 概念
- 程序 (Program):静态的。就是你写好的代码文件,躺在硬盘里,那是蓝本。
- 进程 (Process):动态的。程序运行起来就是进程。它是系统资源分配的基本单位(拥有独立的内存空间、I/O端口等)。
- 线程 (Thread):比进程更小。它是CPU调度和执行的基本单位。
进程间资源独立,线程间资源共享。
- 一个进程可以有多个线程(比如浏览器是一个进程,里面有下载线程、渲染线程)。
- 同一进程内的线程共享:堆内存(Heap)、方法区(Code/Data)。这意味着它们可以访问同一个全局变量(这也导致了后面的同步问题)。
- 每个线程独有:程序计数器(PC,记录执行到哪一行了)、虚拟机栈(Stack,记录局部变量和方法调用链)。这一点非常重要,线程切换快就是因为不需要切换内存页表,只要切换栈和寄存器。
2.线程的生命周期
1. 五大状态(状态流转图) 想象一个线程的一生:
- 新建 (New):
new Thread(),此时它只是个Java对象,还没进操作系统队列。 - 就绪 (Runnable):调用了
start()。注意:调用start不代表立即运行,而是告诉CPU“我准备好了,随时可以翻我牌子”。 - 运行 (Running):CPU真的分配时间片给它了,开始执行
run()方法。 - 阻塞 (Blocked):因为某些原因(等I/O、睡着了
sleep、等锁synchronized、等别人join)暂停了。阻塞结束后,不能直接回到Running,必须先回Runnable排队。 - 终止 (Terminated):
run()执行完了,或者抛异常挂了
2. 调度策略
Java使用的是抢占式调度 (Preemptive),不是分时调度。
- 优先级 (Priority):1-10,默认是5 (
NORM_PRIORITY)。高优先级更有可能抢到CPU,但不绝对(不要依赖优先级来保证逻辑正确性)。
3. 控制线程的API
start()vsrun()(必考)
start():启动线程,JVM会由本地方法创建系统线程,然后自动调用run()。这是真正的多线程。run():如果你直接调用thread.run(),那它就是个普通的方法调用,还在主线程里跑,根本没有启动新线程。
sleep()vsyield()(必考,背下这个对比)
这两个都是静态方法 Thread.sleep(), Thread.yield()。
| 特性 | sleep(long millis) | yield() |
|---|---|---|
| 状态变化 | Running -> Blocked (阻塞) | Running -> Runnable (就绪) |
| 异常 | 抛出 InterruptedException (必须捕获) | 不抛出异常 |
| 后续执行 | 睡醒后去排队 | 放弃当前时间片,重新去排队 |
| 优先级 | 不在乎优先级,谁都能运行 | 只让给同级或更高优先级的线程 |
| 用途 | 模拟延时、定时等待 | 调试、测试并发性(实际开发很少用) |
sleep()vswait()(深层考点)
PPT稍微提到了 wait(虽然属于同步章节,但这里必须对比)。
- 核心区别:
sleep抱着锁睡觉(不释放资源);wait释放锁(让出资源给别人用)。
join()
- 含义:插队。
- 代码逻辑:在线程A中调用
B.join(),意思是“线程A先歇着,等线程B执行完了,A再接着往下走”。 - 场景:主线程需要等待子线程运算结果再汇总。
4. 创建线程的三种方法
方式一:继承 Thread 类
逻辑:把类变成线程本身。
缺点:Java是单继承,继承了 Thread 就不能继承别的类了,灵活性差。
// 1. 定义:继承 Thread
class MyThread extends Thread {
private String name;
public MyThread(String name) { this.name = name; }
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " 运行: " + i);
}
}
}
// 2. 使用
public class Test1 {
public static void main(String[] args) {
MyThread t1 = new MyThread("线程A");
t1.start(); // 千万别写成 t1.run()
}
}方式二:实现 Runnable 接口(推荐)
逻辑:把任务(Task)和线程(Thread)分离。解耦,且可以实现资源共享。
代码模式:new Thread(Runnable target).start()
// 1. 定义:实现 Runnable
class MyTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
}
}
// 2. 使用
public class Test2 {
public static void main(String[] args) {
MyTask task = new MyTask();
// 需要把任务丢给 Thread 对象
Thread t1 = new Thread(task, "线程A");
Thread t2 = new Thread(task, "线程B");
t1.start();
t2.start();
}
}方式三:实现 Callable 接口(JDK 5.0+,高级)
逻辑:Runnable 的 run() 返回值是 void 且不能抛出受检异常。如果你需要线程算完告诉你结果,或者想捕获异常,就用 Callable。
考点:需要配合 FutureTask 使用。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 1. 定义:实现 Callable<返回值类型>
class MyCallTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum; // 能返回结果!
}
}
// 2. 使用
public class Test3 {
public static void main(String[] args) {
// 创建 Callable 对象
MyCallTask callTask = new MyCallTask();
// 包装成 FutureTask (它同时实现了 Runnable 和 Future)
FutureTask<Integer> futureTask = new FutureTask<>(callTask);
// 启动线程
new Thread(futureTask).start();
try {
// 获取结果,get() 方法会阻塞主线程,直到子线程算完
Integer result = futureTask.get();
System.out.println("计算结果是:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}三种方式对比总结(考试必写):
- Thread:简单,但有单继承限制。
- Runnable:逻辑分离,支持多实现,适合多个线程共享同一个资源(Target),最常用。
- Callable:有返回值,能抛异常,用
Future拿结果。
Lesson 14.2 多线程-2
核心逻辑: 多线程最大的危险在于共享数据。PPT里举了“北京上海同时操作同一个银行账户”的例子。
- 现象:两个线程同时读写同一个变量(比如余额),因为CPU切片的原因,导致数据计算错误(就像我们刚才聊的两个人同时取钱,银行亏了)。
- 术语:这叫线程不安全,这个由于竞争导致错误的情况叫竞态条件。
- 解决方案:我们需要把操作共享数据的动作变成原子操作(Atomic)。意思是:要么不做,要多就一口气做完,中间不允许别人插队。
1. synchronized锁
1. 锁的本质:
锁的是对象,不是代码!synchronized 就像是给某个对象加了一把锁,想要执行这段代码的线程,必须先拿到这个对象的钥匙。
2. 三种写法(必考):
写法A:修饰实例方法
public synchronized void method() { ... }- 锁的是谁?
this(当前对象实例)。 - 后果:如果有两个线程分别操作
new Task()和new Task()(两个不同的对象),这锁就失效了!必须是同一个对象才互斥。
- 锁的是谁?
写法B:修饰静态方法(Static)
public static synchronized void method() { ... }- 锁的是谁?
Class对象(类本身)。 - 后果:这是全局锁。不管你new了多少个对象,只要调这个方法,全得排队。
- 锁的是谁?
写法C:同步代码块(最灵活)
synchronized(obj) { ... }- 锁的是谁? 括号里的
obj。 - 技巧:通常我们用
synchronized(this),或者专门在一个Object lock = new Object()上加锁。
- 锁的是谁? 括号里的
它防的是同一个对象被并发访问。如果线程A访问 obj1,线程B访问 obj2,他俩是不冲突的。
2. 线程通信
光有锁还不够,线程之间还得“聊天”。
场景:你要从卡里取钱,但没钱了。你不能死循环一直问“有钱没?有钱没?”,这样CPU会爆炸。
正确做法:没钱你就等待(Wait),等你女朋友存了钱,她通知(Notify)你醒来。
1. 核心方法(背下来):
wait():我释放锁,去睡觉(进入等待池)。notify():随机叫醒一个在睡觉的人(让他进入锁池去抢锁)。notifyAll():叫醒所有在睡觉的人(推荐用这个,防止漏人)。
2. 必考面试题:sleep() 和 wait() 的区别?:
- 归属不同:
sleep是Thread类的静态方法;wait是Object类的成员方法。 对锁的态度(最关键):
sleep抱着锁睡觉(不释放锁,别人进不来)。wait扔掉锁睡觉(释放锁,别人可以进来干活)。
- 使用场景:
wait必须写在synchronized块里面(不拿锁怎么扔锁?),sleep可以在任何地方。
3. 零碎考点
管道流(PipedStream,46-50页):
- 作用:线程之间直接传输数据,像水管一样。
- 考点:输入流和输出流必须connect(连接)起来才能用。
死锁(Deadlock,51-53页):
- 定义:你等我,我等你,大家都卡死。
- 原因:不是电脑坏了,是逻辑设计错了(比如嵌套锁:A锁里拿B锁,B锁里拿A锁)。
守护线程(Daemon Thread,54-57页):
- 概念:后台服务的备胎线程,比如垃圾回收(GC)。
- 特性:如果所有的普通线程(前台线程)都结束了,守护线程会自动陪葬(立即终止)。
- 用法:
thread.setDaemon(true)必须在start()之前设置。
定时器(Timer,58-61页):
- 知道有
Timer和TimerTask这两个类,能做定时任务即可。
- 知道有
4. 生产者/消费者
这个“生产者-消费者”模型(Producer-Consumer Pattern)是多线程编程的“圣经”。
第一步:定义“馒头筐”(共享资源)
这是最关键的类。所有的逻辑(加锁、判断满没满、wait、notify)都写在这里面。不要写在生产者或者消费者里,那样会很乱。
核心逻辑:
- 必须要有个容器(比如数组或List)装馒头。
- 生产方法
push():满了就等待,不满就放,放完喊人来吃。 - 消费方法
pop():空了就等待,不空就拿,拿完喊人来做。
// 1. 馒头筐 (共享资源)
class Basket {
// 假设筐里最多能装 10 个馒头
private int max = 10;
// 当前筐里的馒头数量
private int count = 0;
// --- 生产馒头的方法 ---
// 加上 synchronized,因为要保证 count 的安全
public synchronized void produce() {
// 1. 判断:如果满了吗?
// 注意:这里必须用 while,不能用 if!
// 为什么?因为线程被 notify 唤醒后,需要再次检查条件是否满足。
while (count == max) {
try {
System.out.println("筐满了,生产者 " + Thread.currentThread().getName() + " 开始等待...");
this.wait(); // 满了就睡,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. 干活:没满,就做一个馒头
count++;
System.out.println(Thread.currentThread().getName() + " 生产了一个馒头,当前库存:" + count);
// 3. 通知:喊醒消费者(可能消费者之前因为没馒头在 wait)
this.notifyAll(); // 喊醒所有睡着的人
}
// --- 消费馒头的方法 ---
public synchronized void consume() {
// 1. 判断:如果空了吗?
while (count == 0) {
try {
System.out.println("筐空了,消费者 " + Thread.currentThread().getName() + " 开始等待...");
this.wait(); // 没得吃就睡,释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. 干活:没空,就吃一个馒头
count--;
System.out.println(Thread.currentThread().getName() + " 吃掉了一个馒头,当前库存:" + count);
// 3. 通知:喊醒生产者(可能生产者之前因为满了在 wait)
this.notifyAll();
}
}第二步:定义“厨师”(生产者线程)
厨师的任务很简单:死循环一直做馒头。
// 2. 厨师 (生产者线程)
class Producer implements Runnable {
private Basket basket;
public Producer(Basket basket) {
this.basket = basket;
}
@Override
public void run() {
while (true) {
basket.produce(); // 调 basket 的方法
try {
// 模拟做馒头需要一点时间,不让刷屏太快
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}第三步:定义“吃货”(消费者线程)
吃货的任务也很简单:死循环一直吃馒头。
// 3. 吃货 (消费者线程)
class Consumer implements Runnable {
private Basket basket;
public Consumer(Basket basket) {
this.basket = basket;
}
@Override
public void run() {
while (true) {
basket.consume(); // 调 basket 的方法
try {
// 模拟消化馒头需要一点时间
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}第四步:主程序(组装运行)
把大家凑到一起。
public class MainTest {
public static void main(String[] args) {
// 1. 只有这一个筐!这是重点!
Basket basket = new Basket();
// 2. 招 2 个厨师
new Thread(new Producer(basket), "厨师大胖").start();
new Thread(new Producer(basket), "厨师二胖").start();
// 3. 招 3 个吃货
new Thread(new Consumer(basket), "吃货小明").start();
new Thread(new Consumer(basket), "吃货小红").start();
new Thread(new Consumer(basket), "吃货小刚").start();
}
}如果考试让你手写,你要检查自己有没有漏掉这4个关键点:
- synchronized:写在了
produce()和consume()方法上。没加锁就是0分。 - while循环检查:
while (count == max)。如果写成if (count == max),虽然大部分时候能跑,但在多消费者情况下会出Bug(虚假唤醒),老师会扣分。 - wait():条件不满足(满了或空了)时调用。
- notifyAll():干完活(生产完或吃完)后调用。推荐写
notifyAll()而不是notify(),防止因为运气不好只唤醒了同伴(比如生产者唤醒了生产者),导致程序卡死。
把这个代码结构在脑子里过一遍:
Basket类管核心逻辑(wait/notify) -> Producer只管调produce -> Consumer只管调consume -> Main里大家共用一个Basket对象。
Lesson 15 网络
1. 网络编程的底层逻辑
PPT的前13页讲的都是计算机网络的基础,Java编程是建立在这些概念之上的。
1. 网络架构模式(考点:区分)
- C/S 模式 (Client/Server):客户端/服务器。必须两边都有专门的软件(比如QQ)。服务器负责管理资源,客户端负责交互。
- B/S 模式 (Browser/Server):浏览器/服务器。客户端不需要专门软件,有浏览器就行(比如网页版淘宝)。
- P2P (Peer to Peer):对等网络。没有固定的服务器,每台电脑既是客户也是服务者(比如迅雷下载)。
2. IP与端口(核心考点:理解寻址)
网络通信解决两个问题:找到电脑,找到电脑上的程序。
IP地址 (InetAddress):用于在网络中唯一标识一台计算机(例如 192.168.0.1)。
- Java类:
InetAddress。 - 考点:它没有构造函数,要通过
InetAddress.getByName("www.sina.com")或getLocalHost()来获取对象。
- Java类:
端口 (Port):用于标识计算机上运行的具体进程。
- 计算机只有一个物理网线接口,但你要同时开微信和浏览器,数据怎么分?靠端口。
- 范围:0-65535。
- 考点:0-1023是保留端口(如Web用的80,FTP用的21),你自己写程序不要占用,建议用1024以后的。
- Socket (套接字):PPT第14页的比喻很好,Socket = IP地址 + 端口号。它是通信的端点,是驱动层提供给我们的编程接口。
3. 协议:TCP vs UDP(必考!背诵全文)
PPT第9页重点讲了这个,必须死记硬背它们的区别:
TCP (传输控制协议):
- 面向连接(打电话,必须先接通)。
- 可靠(保证数据不丢、顺序不错)。
- 流模式(数据像水流一样源源不断)。
- 用途:下载文件、浏览网页、聊天。
UDP (用户数据报协议):
- 无连接(写信/发短信,不管你在不在直接发)。
- 不可靠(可能丢包,不保证顺序)。
- 数据报模式(数据是一个个独立的包,有大小限制)。
- 用途:视频会议、即时语音(丢一帧画面无所谓,要的是快)。
2. 基于TCP的网络编程
这是PPT的第18-54页,也是最容易出编程题的地方。Java中通过 Socket 和 ServerSocket 两个类来实现。
1. 逻辑模型(类似“114查号台”模型)
PPT第21页那个比喻很关键,请理解这个逻辑:
- Server端:需要一个总机(
ServerSocket)负责在门口死等连接。一旦有客户连上来,总机不亲自服务,而是指派一个分机(普通的Socket)去专门服务这个客户,总机继续回门口等下一个。 - Client端:直接发起呼叫(创建一个
Socket)。
2. 核心代码流程(背下这个步骤,代码题满分)
服务器端 (Server) 编写步骤:
- 建站:创建
ServerSocket,绑定监听端口(比如8888)。 - 等待:调用
accept()方法。注意:这个方法是阻塞的,没人连它就一直卡在这里不动。 - 握手:一旦有人连上,
accept()返回一个普通的Socket对象,这个对象代表了与那个特定客户端的连接。 - IO操作:通过这个
Socket拿到输入流getInputStream(听)和输出流getOutputStream(说)。 - 关闭:关流,关Socket。
// Server端伪代码
ServerSocket server = new ServerSocket(8888); // 1. 建站
System.out.println("等待连接...");
Socket socket = server.accept(); // 2. 阻塞等待,直到Client连上,返回一个专线socket
// 3. 拿到流
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// ...读写操作...
socket.close();
server.close();客户端 (Client) 编写步骤:
- 呼叫:创建
Socket,指定服务器的IP和端口。这一步一执行,自动进行TCP“三次握手”建立连接。 - IO操作:拿到输入流和输出流。
- 关闭。
// Client端伪代码
Socket socket = new Socket("127.0.0.1", 8888); // 1. 呼叫服务器
// 2. 拿到流
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
// ...读写操作...
socket.close();3. 进阶考点:多线程聊天(PPT 40-50页)
为什么PPT中间花了大量篇幅讲多线程?
- 问题:单线程程序中,如果你在
read()(听),你就不能write()(说)。导致如果你不说话,我也没法说话,必须一人一句。 解决:真正的聊天软件,“听”和“说”必须是分离的。
- 主线程负责建立连接。
- 启动一个线程专门负责
read(死循环读)。 - 启动另一个线程专门负责
write。 - 这样才能实现“自由聊天”。
3.基于UDP的网络编程
PPT第55-66页。UDP没有客户端和服务器的严格区分,只有“发送端”和“接收端”,或者叫对等节点。
1. 核心类
DatagramSocket:码头。负责发送和接收数据。DatagramPacket:集装箱/信封。负责封装数据,上面写着数据发给谁(IP+Port)或者数据来自谁。
2. 核心代码流程
发送端:
- 建立码头:
DatagramSocket。 - 准备数据:把字符串转成字节数组
byte[]。 - 打包(关键):创建
DatagramPacket,必须指定目标IP和端口。 - 发送:
socket.send(packet)。
// 发送端
DatagramSocket ds = new DatagramSocket();
byte[] buf = "Hello".getBytes();
// 封包:数据,长度,目标地址,目标端口
DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("127.0.0.1"), 9999);
ds.send(dp);
ds.close();接收端:
- 建立码头:
DatagramSocket(9999)。必须指定监听端口,不然没人找得到你。 - 准备空容器:创建一个空的
byte[]。 - 准备接收包:创建空的
DatagramPacket。 - 接收:
socket.receive(packet)。这是阻塞方法。 - 拆包:从
packet里把数据取出来。
// 接收端
DatagramSocket ds = new DatagramSocket(9999); // 监听9999
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length); // 空包
ds.receive(dp); // 阻塞等待
// 拆包
String msg = new String(dp.getData(), 0, dp.getLength());
ds.close();4. 总结
区分Socket类型:
ServerSocket只用于TCP服务器端。Socket用于TCP客户端和服务器端的通信专线。DatagramSocket用于UDP通信(不分服务端客户端)。
区分数据传输方式:
- TCP用的是 IO 流 (
InputStream,OutputStream),像打电话,线连着。 - UDP用的是 数据包 (
DatagramPacket),像寄信,一个包一个包发。
- TCP用的是 IO 流 (
常见异常:
- 如果服务器没开,客户端直接连,会报
ConnectException。 - 端口如果被占用了,会报
BindException。
- 如果服务器没开,客户端直接连,会报
常用方法名:
- TCP Server:
accept() - UDP:
send(),receive() - IP类:
InetAddress.getByName()
- TCP Server:
逻辑题:
- 如果题目让你写一个“能够同时服务多个客户端”的TCP服务器,必须使用多线程。Server每
accept到一个 Socket,就把它扔给一个新的 Thread 去处理,主线程回去继续accept。(这是PPT第51页的思考题核心)。
- 如果题目让你写一个“能够同时服务多个客户端”的TCP服务器,必须使用多线程。Server每


2 条评论
就靠您这个复(预)习了orz
实则看一遍还是不会😂应该把lab都看一遍