被网络工程实践课占掉半天后,我补了 Java 面向对象和两道 N 数之和
前言
大家好,我是瓦瓦gogo,这是我第二篇 CSDN 学习复盘。
这两天节奏有点被学校课程打乱。昨天和今天都在做网络工程实践课相关的内容,差不多浪费了半天时间。倒不是说学校课完全没用,只是现在一边要准备期末,一边又想把 Java 后端实习面试的基础补起来,时间被切碎之后,学习效率确实会下降。
不过今天还是尽量把计划往前推了一点。算法题刷了两道数组双指针相关的经典题:三数之和和四数之和。八股这边看了 Java 基础里的面向对象部分,顺便也理了一下之前在公司写过的 Go 项目数据推送消费链路,这块暂时不展开写,先把自己脑子里的链路理顺。
晚上还得继续复习学校的数据库和数值计算,所以今天这篇就当作一个简短但完整的学习记录。
Java 面向对象复习
今天 Java 基础主要复习的是面向对象。以前刚学 Java 的时候,感觉面向对象就是背三个词:封装、继承、多态。但按面试题重新看一遍后发现,真正容易卡住的不是这三个词本身,而是面试官继续追问时能不能讲出设计目的、使用场景和边界。
怎么理解面向对象
面向对象不是简单地“写很多类”,而是把现实里的业务对象抽象成程序里的对象。对象里有属性,也有行为。比如用户、订单、消息任务都可以看成对象,每个对象负责维护自己的状态和行为。
从面试角度讲,我觉得可以这样回答:面向对象是一种建模方式,通过封装、继承、多态来组织代码,让代码更容易复用、扩展和维护。
这里的重点不是把概念背出来,而是说明它解决的问题。封装主要解决数据和实现细节暴露太多的问题;继承主要解决公共能力复用的问题;多态主要解决代码扩展时不想大量改原有逻辑的问题。
多态体现在哪里,解决了什么问题
多态就是同一个父类引用,在运行时可以指向不同的子类对象,并调用子类重写后的方法。简单说就是“编译看左边,运行看右边”。
interface Sender {
void send(String msg);
}
class HttpSender implements Sender {
@Override
public void send(String msg) {
System.out.println("http send: " + msg);
}
}
class MqSender implements Sender {
@Override
public void send(String msg) {
System.out.println("mq send: " + msg);
}
}
class PushService {
private final Sender sender;
PushService(Sender sender) {
this.sender = sender;
}
void push(String msg) {
sender.send(msg);
}
}
这里 PushService 不关心具体是 HTTP 推送还是 MQ 推送,只依赖 Sender 这个抽象。以后要换一种发送方式,只要新增实现类,尽量不用改 PushService 的核心逻辑。这也是多态比较有价值的地方:减少 if else,让扩展更自然。
重载和重写有什么区别
重载和重写名字很像,但完全不是一回事。
重载发生在同一个类里,方法名相同,参数列表不同,返回值不能作为区分重载的依据。它是在编译期确定调用哪个方法。
class Calculator {
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
重写发生在父子类之间,子类重新实现父类的方法。它和多态关系更紧密,运行时会根据实际对象类型决定调用哪个方法。
class Parent {
void work() {
System.out.println("parent work");
}
}
class Child extends Parent {
@Override
void work() {
System.out.println("child work");
}
}
面试里如果问区别,我会从位置、参数要求、返回值、访问权限、绑定时机这几个角度回答。重写时子类方法的访问权限不能比父类更严格,返回值类型也要兼容父类方法。
抽象类和接口的区别
抽象类和接口都是抽象,但使用场景不一样。
抽象类更像是“半成品父类”,可以有普通成员变量、普通方法、构造方法,也可以有抽象方法。它适合表达一组类的共同模板,比如它们本质上确实属于同一类事物。
接口更像是一组能力规范。一个类可以实现多个接口,所以接口更适合表达“能做什么”。比如 Runnable 表示能被线程执行,Comparable 表示能比较大小。
abstract class BaseTask {
protected String taskName;
BaseTask(String taskName) {
this.taskName = taskName;
}
abstract void execute();
}
interface Retryable {
void retry();
}
还有几个容易被追问的小点:抽象类不能被实例化;抽象类可以有构造方法,因为子类初始化时要先初始化父类;抽象类可以被 final 修饰吗?不可以,因为 final 表示不能被继承,而抽象类本来就是为了被继承。接口里可以定义抽象方法,Java 8 之后也可以有默认方法和静态方法。
static 变量和 static 方法
static 修饰的变量和方法属于类本身,不属于某个具体对象。所以静态变量被所有对象共享,静态方法可以直接通过类名调用。
class Counter {
static int count = 0;
Counter() {
count++;
}
static int getCount() {
return count;
}
}
这里每创建一个 Counter 对象,修改的都是同一个 count。面试里要注意,静态方法不能直接访问非静态成员,因为非静态成员依赖具体对象,而静态方法调用时可能还没有对象。
内部类相关问题
今天还顺了一下内部类。内部类大概可以分成成员内部类、静态内部类、局部内部类和匿名内部类。
成员内部类依赖外部类对象创建,可以访问外部类的成员。静态内部类不依赖外部类对象创建,只能直接访问外部类的静态成员。
class Outer {
private int num = 10;
private static int count = 20;
class Inner {
void print() {
System.out.println(num);
}
}
static class StaticInner {
void print() {
System.out.println(count);
}
}
}
这里比较容易混的是:非静态内部类为什么能访问外部类成员?因为编译器会让内部类持有外部类对象的引用。静态内部类没有这个外部类对象引用,所以它不能直接访问外部类的非静态成员。
复习完这一块之后,我感觉面向对象这部分不能只按教材目录记,最好按面试追问链路整理:先讲概念,再讲解决什么问题,最后补上容易被问到的语法边界。
三数之和:学会的不只是双指针
三数之和这题要求找出数组中所有不重复的三元组,使得三个数相加等于 0。
一开始最容易想到的是三层循环,但时间复杂度是 O(n^3),数据量稍微大一点就不合适。看了题解之后,主流做法是先排序,然后固定一个数,再用双指针找剩下两个数。
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return res;
}
这题我今天最大的收获是去重边界。固定的第一个数要去重,所以 i > 0 && nums[i] == nums[i - 1] 时直接跳过。找到一组答案后,left 和 right 也要继续跳过重复值,否则结果里会出现重复三元组。
还有一个细节是排序之后可以根据和的大小移动指针。sum < 0 说明整体偏小,需要让 left 右移变大;sum > 0 说明整体偏大,需要让 right 左移变小。这比三层循环更有方向感。
四数之和:三数之和的升级版
四数之和和三数之和很像,只不过目标变成了找四个数相加等于 target。它的思路也是排序加双指针,只是前面要固定两个数。
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < n - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1;
int right = n - 1;
while (left < right) {
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
}
return res;
}
四数之和让我更明显地感受到,N 数之和这类题不难在“会不会枚举”,而是难在每一层怎么去重。
第一层 i 要去重,第二层 j 也要去重,而且 j 的去重条件不能写成 j > 0,应该写成 j > i + 1。因为 j 是在当前 i 固定之后才开始枚举的,去重范围也应该限制在这一层循环内部。
另一个要注意的点是溢出。四个 int 相加时可能超过 int 范围,所以这里用 long sum。这个细节平时容易忽略,但在提交题目时可能直接导致结果错误。
今天顺便理了一下 Go 项目链路
除了 Java 和算法,今天还简单理了一下之前在公司接触过的 Go 项目数据推送消费链路。大概包括数据进入系统之后,如何经过缓存、定时任务、消费逻辑,再通过 HTTP 推送到下游。
这块我现在还没有打算展开写,因为里面有很多业务细节不适合直接放出来,而且我自己也还需要再整理。现阶段的目标不是把经历写得多复杂,而是先把“我做过什么、链路怎么走、为什么这么设计、遇到过什么问题”这几个问题讲清楚。
今日总结
今天虽然被网络工程实践课占掉了一部分时间,但还是完成了 Java 面向对象复习和两道算法题。
Java 面向对象这块,重点不是背封装、继承、多态三个词,而是要能顺着面试官的问题继续讲下去,比如多态解决什么问题、重载和重写的区别、抽象类和接口怎么选、static 成员有什么限制、内部类为什么能访问外部类成员。算法这边,三数之和和四数之和让我对“排序 + 双指针 + 去重”这个套路更熟了,尤其是去重边界、指针移动和 int 溢出这些细节。
晚上继续复习学校的数据库和数值计算。最近的状态就是期末、面试、项目经历整理一起挤过来,只能每天尽量往前推进一点。
更多推荐

所有评论(0)