java专项八股知识点(1)
目录
- 外部类&内部类
- String、StringBuilder和StringBuffer的区别
- super和this关键字
在 Java 中,类可以分为外部类和内部类。
-
外部类是最常见的类,定义在
.java
文件的顶层。它可以用修饰符如public
、default
、abstract
、final
修饰,但不能使用static
修饰。一个源文件中最多只能有一个public
外部类,且文件名必须与该类名一致。外部类适合承载完整的业务逻辑,如服务类、控制器类、实体类等。 -
内部类是定义在另一个类内部的类,主要有三种类型:成员内部类、静态内部类和匿名内部类。其中,只有静态内部类可以使用
static
修饰。成员内部类和匿名内部类都可以访问外部类的属性和方法,即使这些成员是私有的。静态内部类无法访问外部类的非静态成员。
从编译结果来看,外部类会被编译为 Outer.class
,而内部类会被编译为 Outer$Inner.class
的字节码文件。
在实际开发中,内部类常用于构建辅助结构,增强封装性。例如:
-
使用静态内部类实现 Builder 模式;
-
使用匿名内部类实现按钮点击事件或线程回调;
-
使用成员内部类管理与外部类高度耦合的数据结构。
(1)成员内部类(非静态)
成员内部类定义在类的内部,但不是静态的,因此它可以访问外部类的所有成员,包括私有变量。通常用于表达**“整体-部分”关系**,比如学校和学生、公司和员工。
应用场景:
-
一个类的数据对象高度依赖外部类的上下文(如组织内的成员、设备内的部件)
public class School { private String name = "大学"; // 成员内部类 public class Student { private String studentName; public Student(String studentName) { this.studentName = studentName; } public void introduce() { // 可以访问外部类成员 System.out.println("我是 " + studentName + ",来自 " + name); } } public static void main(String[] args) { School school = new School(); Student stu = school.new Student("小明"); stu.introduce(); } }
(2)静态内部类
静态内部类是用 static
修饰的内部类。它不能访问外部类的非静态成员,更像一个逻辑上嵌套的独立类,常用于Builder 模式、工具类组装等场景。
public class User { private String name; private int age; // 静态内部类 - Builder 模式 public static class Builder { private String name; private int age; public Builder name(String name) { this.name = name; return this; } public Builder age(int age) { this.age = age; return this; } public User build() { User user = new User(); user.name = this.name; user.age = this.age; return user; } } @Override public String toString() { return name + " - " + age; } public static void main(String[] args) { User user = new User.Builder() .name("张三") .age(25) .build(); System.out.println(user); } }
(3)匿名内部类
匿名内部类是没有名字的内部类,常用于临时实现接口或继承类的逻辑,适用于只用一次的情况,比如线程创建、回调事件处理。
public class Test { public static void main(String[] args) { // 使用匿名内部类创建线程 Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println("线程已启动!"); } }); t.start(); } }
(1)String(不可变字符串)
String底层是final char[],它是不可变的,所有字符串拼接、替换、substring等操作,都会创建新对象。不可变性有一些好处:
- 线程安全(多个线程共享同一个字符串不会有并发问题)
String s = "hello"; Thread t1 = new Thread(() -> { System.out.println(s); }); Thread t2 = new Thread(() -> { System.out.println(s); });
两个线程都在访问s,但因为String是不可变的,所以很安全:
- 内部的char[]是final;
- 无法修改字符串内容,只能重新赋值;
- 无论多少线程读同一个字符串,都不会出现竞态条件。
- 可以被安全缓存(字符串常量池)
Java 中有个优化机制叫 字符串常量池(String Pool):
String a = "abc"; String b = "abc"; System.out.println(a == b); // true,指向同一个地址
-
JVM 会自动把 字符串字面量缓存到堆外常量池中;
-
下次用同样的字面量,就不会创建新对象,而是复用;
-
前提是:对象不可变,否则池里的值就可能被污染!
🤯 如果 String 是可变的,会发生什么?
String a = "abc"; // 存入常量池
String b = "abc"; // b 复用了 a 的地址
a.charAt(0) = 'x'; // 如果可变,那 b 就变成了 "xbc"
这就相当于你篡改了别人家的数据,整个 JVM 的字符串世界就崩了。所以 Java 强制让 String
不可变,从根本上解决了这个问题。
- 可以作为 HashMap 的 key(哈希值稳定)
Map<String, Integer> map = new HashMap<>(); map.put("user123", 100); System.out.println(map.get("user123")); // 100
背后的原理是:
-
HashMap 根据
key.hashCode()
来定位桶; -
再用
equals()
来比较内容是否一致; -
如果你用的 key 是 可变对象,哈希值可能变了 → 就找不到了!
(2)StringBuilder和StringBuffer
它们底层都是char[],是可变的,都是继承自AbstractStringBuider,逻辑差不多,最大差别是:
对比项 | StringBuilder | StringBuffer |
---|---|---|
是否线程安全 | ❌ 不安全 | ✅ 线程安全(方法加了 synchronized ) |
性能 | 🚀 快(无锁) | 🐢 慢(加锁) |
使用场景 | 单线程字符串拼接等高性能场景 | 多线程共享构建字符串时使用 |
public synchronized void append(String str) { toStringCache = null; super.append(str); }
在StringBuffer中,大部分方法都被syunchronized修饰,而StringBuilder完全没有。
StringBuffer中有很多方法来修改字符串内容,如:
- append(String str):在末尾添加内容
- insert(int offset, String str):在指定位置插入内容。先把插入点之后的所有字符向后移动,再把新字符串复制进去;
- delete(int start, int end):删除从
start
到end - 1
的字符。 - replace(int start, int end, String str):替换
[start, end)
范围内的字符为str
。等价于:delete(start, end)
+insert(start, str)
,中间可能涉及两次数组移动 + 一次扩容。 - reverse():将整个字符数组反转。
(3)应用场景
你想做的事 | 推荐用法 | 原因 |
---|---|---|
多线程拼接字符串 | StringBuffer |
有锁,线程安全 |
单线程频繁拼接字符串 | StringBuilder |
无锁,性能更好 |
不变字符串,作为 key、常量等 | String |
不可变,哈希值稳定,能进常量池 |
(1)对象创建的底层流程(从父到子)
当你创建一个子类对象时:new Child(),Java会自动做这些事:
- 加载类元数据
- 为对象分配内存
- 默认初始化成员变量(为0、null等)
- 执行父类构造器->子类构造器
- 完成对象创建并返回引用
关键是:Java 强制要求先执行父类构造器,否则子类可能会访问一个未初始化的父类成员,这样就违背了 Java 的安全模型。
(2)super()的核心作用
super()
是用来在子类构造器中显式调用父类构造器的。
class Parent { Parent() { System.out.println("Parent构造"); } } class Child extends Parent { Child() { super(); // ✅ 必须在第一行 System.out.println("Child构造"); } }
为什么必须在第一行?
-
因为 Java 编译器要确保父类构造器在子类构造器运行前最先执行;
-
如果不是第一行,可能中间有语句会使用到子类字段/方法,此时父类可能还没准备好 → ❌ 编译失败。
(3)this()的核心作用
this()
是在构造器中调用本类的其他构造方法,用于构造器重载复用代码。
class Book { Book() { this("default title"); } Book(String title) { System.out.println("构造Book:" + title); } }
-
this()
也必须是构造器的第一行; -
原因类似于
super()
,构造器是对象初始化的一部分,调用必须明确且优先。
(4)this() 和 super() 不能同时出现的根本原因:
它们都要求“必须是构造器的第一行”,第一行只能有一个语句,它们就“打架”了。
(5)为什么static方法里不能用this
public static void main(String[] args) { this.doSomething(); // ❌ 编译失败:Cannot use this in a static context }
因为此时你是用 类名来调用方法,没有任何一个对象来充当 this
,编译器无法推断出 this 是谁。
你可以把 this
和 super
理解成“对象的自我和父亲”:
-
this
:我是谁(当前这个对象) -
super
:我爸是谁(当前对象的父类)
但如果你还没出生(new 都没 new),你怎么谈“我是谁”和“我爸是谁”?Java 的 static
方法就是这种“没有对象的静态存在”。
(1)基本定义
项目 | Servlet | CGI(Common Gateway Interface) |
---|---|---|
本质 | Java 提供的动态网页组件,运行在 Web 容器中 | 一种服务器与脚本程序交互的通用接口协议 |
类型 | Java 类 | 可执行脚本(如 .py , .pl , .sh )或可执行文件 |
起源 | Java EE 标准,从 1990s 开始用于 Web 开发 | 早期 Web 开发通用方法,1993 年被广泛采用 |
(3)工作原理
Servlet 工作流程:
浏览器请求 URL ↓ Web 容器(如 Tomcat)解析 URL → 找到对应 Servlet 类 ↓ Servlet 对象在 JVM 内存中已存在(单例) ↓ 为当前请求分配一个线程,执行 service/doGet/doPost 方法 ↓ Servlet 返回动态 HTML 或 JSON 响应
CGI 工作流程:
浏览器请求 hello.cgi ↓ 服务器检测 .cgi,fork 一个新进程运行该脚本 ↓ 读取请求数据(通过环境变量或 stdin) ↓ 脚本生成 HTML 输出 → stdout 返回结果 ↓ 进程销毁
(4)优缺点总结
✅ Servlet 优点:
-
性能高(线程而非进程)
-
易于维护(面向对象)
-
功能丰富(原生支持请求解析、Session、Cookie)
-
高可移植性(只要能跑 JVM 就能跑)
❌ CGI 缺点:
-
每次请求都创建新进程,性能极差
-
难以管理用户状态(无内建 Session)
-
兼容性和安全性差(易受攻击)
-
不适合高并发场景(很快就把服务器拖垮)
A. d << 2
错误
原因:<<
是位运算符,只能用在整型(int、long 等),不能对 double 使用。
B. d / n
合法
原因:double 和 int 可以做除法,int 会自动转换为 double,结果是浮点数。
C. !d && (n - 3)
错误
原因:!
是逻辑非,只能用在 boolean 上,不能对 double 使用。
并且 (n - 3)
是 int,不能直接参与逻辑运算。
D. (d - 0.2) | n
错误
原因:|
是按位或,只能用于整数,不能用在 double 上。